diff --git a/book/book.toml b/book/book.toml index 3ccaf71e..2277a0bd 100644 --- a/book/book.toml +++ b/book/book.toml @@ -3,8 +3,9 @@ authors = ["Blaž Hrastnik"] language = "en" multilingual = false src = "src" -theme = "colibri" edit-url-template = "https://github.com/helix-editor/helix/tree/master/book/{path}?mode=edit" [output.html] cname = "docs.helix-editor.com" +default-theme = "colibri" +preferred-dark-theme = "colibri" diff --git a/book/src/configuration.md b/book/src/configuration.md index 00dfbbd8..5a28362d 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -5,6 +5,19 @@ To override global configuration parameters, create a `config.toml` file located * Linux and Mac: `~/.config/helix/config.toml` * Windows: `%AppData%\helix\config.toml` +## Editor + +`[editor]` section of the config. + +| Key | Description | Default | +|--|--|---------| +| `scrolloff` | Number of lines of padding around the edge of the screen when scrolling. | `3` | +| `mouse` | Enable mouse mode. | `true` | +| `middle-click-paste` | Middle click paste support. | `true` | +| `scroll-lines` | Number of lines to scroll per scroll wheel step. | `3` | +| `shell` | Shell to use when running external commands. | Unix: `["sh", "-c"]`
Windows: `["cmd", "/C"]` | +| `line-number` | Line number display (`absolute`, `relative`) | `absolute` | + ## LSP To display all language server messages in the status line add the following to your `config.toml`: diff --git a/book/src/from-vim.md b/book/src/from-vim.md index 8e9bbac3..09f33386 100644 --- a/book/src/from-vim.md +++ b/book/src/from-vim.md @@ -7,4 +7,6 @@ going to act on (a word, a paragraph, a line, etc) is selected first and the action itself (delete, change, yank, etc) comes second. A cursor is simply a single width selection. +See also Kakoune's [Migrating from Vim](https://github.com/mawww/kakoune/wiki/Migrating-from-Vim). + > TODO: Mention texobjects, surround, registers diff --git a/book/src/install.md b/book/src/install.md index cd9c980e..b9febbcc 100644 --- a/book/src/install.md +++ b/book/src/install.md @@ -23,7 +23,9 @@ shell for working on Helix. ### Arch Linux -Binary packages are available on AUR: +Releases are available in the `community` repository. + +Packages are also available on AUR: - [helix-bin](https://aur.archlinux.org/packages/helix-bin/) contains the pre-built release - [helix-git](https://aur.archlinux.org/packages/helix-git/) builds the master branch diff --git a/book/src/keymap.md b/book/src/keymap.md index 861e46ac..51e56eaa 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -4,7 +4,7 @@ ### Movement -> NOTE: `f`, `F`, `t` and `T` are not confined to the current line. +> NOTE: Unlike vim, `f`, `F`, `t` and `T` are not confined to the current line. | Key | Description | Command | | ----- | ----------- | ------- | @@ -28,14 +28,14 @@ | `PageDown` | Move page down | `page_down` | | `Ctrl-u` | Move half page up | `half_page_up` | | `Ctrl-d` | Move half page down | `half_page_down` | -| `Ctrl-i` | Jump forward on the jumplist TODO: conflicts tab | `jump_forward` | +| `Ctrl-i` | Jump forward on the jumplist | `jump_forward` | | `Ctrl-o` | Jump backward on the jumplist | `jump_backward` | | `v` | Enter [select (extend) mode](#select--extend-mode) | `select_mode` | | `g` | Enter [goto mode](#goto-mode) | N/A | | `m` | Enter [match mode](#match-mode) | N/A | | `:` | Enter command mode | `command_mode` | | `z` | Enter [view mode](#view-mode) | N/A | -| `Ctrl-w` | Enter [window mode](#window-mode) (maybe will be remove for spc w w later) | N/A | +| `Ctrl-w` | Enter [window mode](#window-mode) | N/A | | `Space` | Enter [space mode](#space-mode) | N/A | | `K` | Show documentation for the item under the cursor | `hover` | @@ -66,6 +66,16 @@ | `d` | Delete selection | `delete_selection` | | `c` | Change selection (delete and enter insert mode) | `change_selection` | +#### Shell + +| Key | Description | Command | +| ------ | ----------- | ------- | +| | | Pipe each selection through shell command, replacing with output | `shell_pipe` | +| A-| | Pipe each selection into shell command, ignoring output | `shell_pipe_to` | +| `!` | Run shell command, inserting output before each selection | `shell_insert_output` | +| `A-!` | Run shell command, appending output after each selection | `shell_append_output` | + + ### Selection manipulation | Key | Description | Command | @@ -87,17 +97,10 @@ | | Expand selection to parent syntax node TODO: pick a key | `expand_selection` | | `J` | Join lines inside selection | `join_selections` | | `K` | Keep selections matching the regex TODO: overlapped by hover help | `keep_selections` | +| `$` | Pipe each selection into shell command, keep selections where command returned 0 | `shell_keep_pipe` | | `Space` | Keep only the primary selection TODO: overlapped by space mode | `keep_primary_selection` | | `Ctrl-c` | Comment/uncomment the selections | `toggle_comments` | -### Insert Mode - -| Key | Description | Command | -| ----- | ----------- | ------- | -| `Escape` | Switch to normal mode | `normal_mode` | -| `Ctrl-x` | Autocomplete | `completion` | -| `Ctrl-w` | Delete previous word | `delete_word_backward` | - ### Search > TODO: The search implementation isn't ideal yet -- we don't support searching @@ -110,38 +113,11 @@ in reverse, or searching via smartcase. | `N` | Add next search match to selection | `extend_search_next` | | `*` | Use current selection as the search pattern | `search_selection` | -### Unimpaired - -Mappings in the style of [vim-unimpaired](https://github.com/tpope/vim-unimpaired) - -| Key | Description | Command | -| ----- | ----------- | ------- | -| `[d` | Go to previous diagnostic | `goto_prev_diag` | -| `]d` | Go to next diagnostic | `goto_next_diag` | -| `[D` | Go to first diagnostic in document | `goto_first_diag` | -| `]D` | Go to last diagnostic in document | `goto_last_diag` | -| `[space` | Add newline above | `add_newline_above` | -| `]space` | Add newline below | `add_newline_below` | - -### Shell +### Minor modes -| Key | Description | Command | -| ------ | ----------- | ------- | -| `\|` | Pipe each selection through shell command, replacing with output | `shell_pipe` | -| `A-\|` | Pipe each selection into shell command, ignoring output | `shell_pipe_to` | -| `!` | Run shell command, inserting output before each selection | `shell_insert_output` | -| `A-!` | Run shell command, appending output after each selection | `shell_append_output` | -| `$` | Pipe each selection into shell command, keep selections where command returned 0 | `shell_keep_pipe` | - -## Select / extend mode - -I'm still pondering whether to keep this mode or not. It changes movement -commands to extend the existing selection instead of replacing it. - -> NOTE: It's a bit confusing at the moment because extend hasn't been -> implemented for all movement commands yet. +These sub-modes are accessible from normal mode and typically switch back to normal mode after a command. -## View mode +#### View mode View mode is intended for scrolling and manipulating the view without changing the selection. @@ -155,7 +131,7 @@ the selection. | `j` | Scroll the view downwards | `scroll_down` | | `k` | Scroll the view upwards | `scroll_up` | -## Goto mode +#### Goto mode Jumps to various locations. @@ -177,7 +153,7 @@ Jumps to various locations. | `i` | Go to implementation | `goto_implementation` | | `a` | Go to the last accessed/alternate file | `goto_last_accessed_file` | -## Match mode +#### Match mode Enter this mode using `m` from normal mode. See the relavant section in [Usage](./usage.md) for an explanation about [surround](./usage.md#surround) @@ -192,11 +168,9 @@ and [textobject](./usage.md#textobject) usage. | `a` `` | Select around textobject | `select_textobject_around` | | `i` `` | Select inside textobject | `select_textobject_inner` | -## Object mode - TODO: Mappings for selecting syntax nodes (a superset of `[`). -## Window mode +#### Window mode This layer is similar to vim keybindings as kakoune does not support window. @@ -207,9 +181,9 @@ This layer is similar to vim keybindings as kakoune does not support window. | `h`, `Ctrl-h` | Horizontal bottom split | `hsplit` | | `q`, `Ctrl-q` | Close current window | `wclose` | -## Space mode +#### Space mode -This layer is a kludge of mappings I had under leader key in neovim. +This layer is a kludge of mappings, mostly pickers. | Key | Description | Command | | ----- | ----------- | ------- | @@ -226,6 +200,36 @@ This layer is a kludge of mappings I had under leader key in neovim. | `Y` | Yank main selection to clipboard | `yank_main_selection_to_clipboard` | | `R` | Replace selections by clipboard contents | `replace_selections_with_clipboard` | + +#### Unimpaired + +Mappings in the style of [vim-unimpaired](https://github.com/tpope/vim-unimpaired). + +| Key | Description | Command | +| ----- | ----------- | ------- | +| `[d` | Go to previous diagnostic | `goto_prev_diag` | +| `]d` | Go to next diagnostic | `goto_next_diag` | +| `[D` | Go to first diagnostic in document | `goto_first_diag` | +| `]D` | Go to last diagnostic in document | `goto_last_diag` | +| `[space` | Add newline above | `add_newline_above` | +| `]space` | Add newline below | `add_newline_below` | + +## Insert Mode + +| Key | Description | Command | +| ----- | ----------- | ------- | +| `Escape` | Switch to normal mode | `normal_mode` | +| `Ctrl-x` | Autocomplete | `completion` | +| `Ctrl-w` | Delete previous word | `delete_word_backward` | + +## Select / extend mode + +I'm still pondering whether to keep this mode or not. It changes movement +commands (including goto) to extend the existing selection instead of replacing it. + +> NOTE: It's a bit confusing at the moment because extend hasn't been +> implemented for all movement commands yet. + # Picker Keys to use within picker. Remapping currently not supported. diff --git a/book/src/themes.md b/book/src/themes.md index 804baa1c..fe5259d5 100644 --- a/book/src/themes.md +++ b/book/src/themes.md @@ -30,85 +30,9 @@ if the key contains a dot `'.'`, it must be quoted to prevent it being parsed as "key.key" = "#ffffff" ``` -Possible modifiers: +### Color palettes -| Modifier | -| --- | -| `bold` | -| `dim` | -| `italic` | -| `underlined` | -| `slow\_blink` | -| `rapid\_blink` | -| `reversed` | -| `hidden` | -| `crossed\_out` | - -Possible keys: - -| Key | Notes | -| --- | --- | -| `attribute` | | -| `keyword` | | -| `keyword.directive` | Preprocessor directives (\#if in C) | -| `keyword.control` | Control flow | -| `namespace` | | -| `punctuation` | | -| `punctuation.delimiter` | | -| `operator` | | -| `special` | | -| `property` | | -| `variable` | | -| `variable.parameter` | | -| `type` | | -| `type.builtin` | | -| `type.enum.variant` | Enum variants | -| `constructor` | | -| `function` | | -| `function.macro` | | -| `function.builtin` | | -| `comment` | | -| `variable.builtin` | | -| `constant` | | -| `constant.builtin` | | -| `string` | | -| `number` | | -| `escape` | Escaped characters | -| `label` | For lifetimes | -| `module` | | -| `ui.background` | | -| `ui.cursor` | | -| `ui.cursor.insert` | | -| `ui.cursor.select` | | -| `ui.cursor.match` | Matching bracket etc. | -| `ui.cursor.primary` | Cursor with primary selection | -| `ui.linenr` | | -| `ui.linenr.selected` | | -| `ui.statusline` | | -| `ui.statusline.inactive` | | -| `ui.popup` | | -| `ui.window` | | -| `ui.help` | | -| `ui.text` | | -| `ui.text.focus` | | -| `ui.info` | | -| `ui.info.text` | | -| `ui.menu` | | -| `ui.menu.selected` | | -| `ui.selection` | For selections in the editing area | -| `ui.selection.primary` | | -| `warning` | LSP warning | -| `error` | LSP error | -| `info` | LSP info | -| `hint` | LSP hint | - -These keys match [tree-sitter scopes](https://tree-sitter.github.io/tree-sitter/syntax-highlighting#theme). We half-follow the common scopes from [macromates language grammars](https://macromates.com/manual/en/language_grammars) with some differences. - -For a given highlight produced, styling will be determined based on the longest matching theme key. So it's enough to provide function to highlight `function.macro` and `function.builtin` as well, but you can use more specific scopes to highlight specific cases differently. - -## Color palettes - -You can define a palette of named colors, and refer to them from the +It's recommended define a palette of named colors, and refer to them from the configuration values in your theme. To do this, add a table called `palette` to your theme file: @@ -146,3 +70,125 @@ over it and is merged into the default palette. | `light-cyan` | | `light-gray` | | `white` | + +### Modifiers + +The following values may be used as modifiers. + +Less common modifiers might not be supported by your terminal emulator. + +| Modifier | +| --- | +| `bold` | +| `dim` | +| `italic` | +| `underlined` | +| `slow_blink` | +| `rapid_blink` | +| `reversed` | +| `hidden` | +| `crossed_out` | + +### Scopes + +The following is a list of scopes available to use for styling. + +#### Syntax highlighting + +These keys match [tree-sitter scopes](https://tree-sitter.github.io/tree-sitter/syntax-highlighting#theme). + +For a given highlight produced, styling will be determined based on the longest matching theme key. For example, the highlight `function.builtin.static` would match the key `function.builtin` rather than `function`. + +We use a similar set of scopes as +[SublimeText](https://www.sublimetext.com/docs/scope_naming.html). See also +[TextMate](https://macromates.com/manual/en/language_grammars) scopes. + +- `escape` (TODO: rename to (constant).character.escape) + +- `type` - Types + - `builtin` - Primitive types provided by the language (`int`, `usize`) + +- `constant` (TODO: constant.other.placeholder for %v) + - `builtin` Special constants provided by the language (`true`, `false`, `nil` etc) + - `boolean` + - `character` + +- `number` (TODO: rename to constant.number/.numeric.{integer, float, complex}) +- `string` (TODO: string.quoted.{single, double}, string.raw/.unquoted)? + - `regexp` - Regular expressions + - `special` + - `path` + - `url` + +- `comment` - Code comments + - `line` - Single line comments (`//`) + - `block` - Block comments (e.g. (`/* */`) + - `documentation` - Documentation comments (e.g. `///` in Rust) + +- `variable` - Variables + - `builtin` - Reserved language variables (`self`, `this`, `super`, etc) + - `parameter` - Function parameters + - `property` + - `function` (TODO: ?) + +- `label` + +- `punctuation` + - `delimiter` - Commas, colons + - `bracket` - Parentheses, angle brackets, etc. + +- `keyword` + - `control` + - `conditional` - `if`, `else` + - `repeat` - `for`, `while`, `loop` + - `import` - `import`, `export` + - (TODO: return?) + - `directive` - Preprocessor directives (`#if` in C) + - `function` - `fn`, `func` + +- `operator` - `||`, `+=`, `>`, `or` + +- `function` + - `builtin` + - `method` + - `macro` + - `special` (preprocesor in C) + +- `tag` - Tags (e.g. `` in HTML) + +- `namespace` + +#### Interface + +These scopes are used for theming the editor interface. + + +| Key | Notes | +| --- | --- | +| `ui.background` | | +| `ui.cursor` | | +| `ui.cursor.insert` | | +| `ui.cursor.select` | | +| `ui.cursor.match` | Matching bracket etc. | +| `ui.cursor.primary` | Cursor with primary selection | +| `ui.linenr` | | +| `ui.linenr.selected` | | +| `ui.statusline` | Statusline | +| `ui.statusline.inactive` | Statusline (unfocused document) | +| `ui.popup` | | +| `ui.window` | | +| `ui.help` | | +| `ui.text` | | +| `ui.text.focus` | | +| `ui.info` | | +| `ui.info.text` | | +| `ui.menu` | | +| `ui.menu.selected` | | +| `ui.selection` | For selections in the editing area | +| `ui.selection.primary` | | +| `warning` | Diagnostics warning | +| `error` | Diagnostics error | +| `info` | Diagnostics info | +| `hint` | Diagnostics hint | + + diff --git a/book/theme/css/general.css b/book/theme/css/general.css index 7749bded..ddc2387a 100644 --- a/book/theme/css/general.css +++ b/book/theme/css/general.css @@ -114,6 +114,19 @@ h6:target::before { margin-bottom: .875em; } +.content ul li { +margin-bottom: .25rem; +} +.content ul { + list-style-type: square; +} +.content ul ul, .content ol ul { + margin-bottom: .5rem; +} +.content li p { + margin-bottom: .5em; +} + .content p { line-height: 1.45em; } .content ol { line-height: 1.45em; } .content ul { line-height: 1.45em; } diff --git a/book/theme/css/variables.css b/book/theme/css/variables.css index a49d6794..db1a11b8 100644 --- a/book/theme/css/variables.css +++ b/book/theme/css/variables.css @@ -69,7 +69,7 @@ --links: #2b79a2; - --inline-code-color: #c5c8c6;; + --inline-code-color: #c5c8c6; --theme-popup-bg: #141617; --theme-popup-border: #43484d; @@ -110,7 +110,7 @@ --links: #20609f; - --inline-code-color: #301900; + --inline-code-color: #a39e9b; --theme-popup-bg: #fafafa; --theme-popup-border: #cccccc; @@ -151,7 +151,7 @@ --links: #2b79a2; - --inline-code-color: #c5c8c6;; + --inline-code-color: #c5c8c6; --theme-popup-bg: #161923; --theme-popup-border: #737480; @@ -192,7 +192,7 @@ --links: #2b79a2; - --inline-code-color: #6e6b5e; + --inline-code-color: #c5c8c6; --theme-popup-bg: #e1e1db; --theme-popup-border: #b38f6b; @@ -234,7 +234,7 @@ --links: #2b79a2; - --inline-code-color: #c5c8c6;; + --inline-code-color: #6e6b5e; --theme-popup-bg: #141617; --theme-popup-border: #43484d; @@ -261,6 +261,7 @@ .colibri { --bg: #3b224c; --fg: #bcbdd0; + --heading-fg: #fff; --sidebar-bg: #281733; --sidebar-fg: #c8c9db; @@ -276,18 +277,19 @@ /* --links: #a4a0e8; */ --links: #ECCDBA; - --inline-code-color: #c5c8c6;; + --inline-code-color: hsl(48.7, 7.8%, 70%); --theme-popup-bg: #161923; --theme-popup-border: #737480; --theme-hover: rgba(0,0,0, .2); - --quote-bg: hsl(226, 15%, 17%); + --quote-bg: #281733; --quote-border: hsl(226, 15%, 22%); - --table-border-color: hsl(226, 23%, 16%); - --table-header-bg: hsl(226, 23%, 31%); + --table-border-color: hsl(226, 23%, 76%); + --table-header-bg: hsla(226, 23%, 31%, 0); --table-alternate-bg: hsl(226, 23%, 14%); + --table-border-line: hsla(201deg, 20%, 92%, 0.2); --searchbar-border-color: #aaa; --searchbar-bg: #aeaec6; @@ -300,6 +302,7 @@ } .colibri { +/* --bg: #ffffff; --fg: #452859; --fg: #5a5977; @@ -318,7 +321,7 @@ --links: #6F44F0; - --inline-code-color: #697C81; + --inline-code-color: #a39e9b; --theme-popup-bg: #161923; --theme-popup-border: #737480; @@ -341,4 +344,5 @@ --searchresults-border-color: #5c5c68; --searchresults-li-bg: #242430; --search-mark-bg: #a2cff5; +*/ } diff --git a/book/theme/highlight.css b/book/theme/highlight.css index c2343227..8dce7d65 100644 --- a/book/theme/highlight.css +++ b/book/theme/highlight.css @@ -1,83 +1,56 @@ -/* - * An increased contrast highlighting scheme loosely based on the - * "Base16 Atelier Dune Light" theme by Bram de Haan - * (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/dune) - * Original Base16 color scheme by Chris Kempson - * (https://github.com/chriskempson/base16) - */ - -/* Comment */ +pre code.hljs { + display:block; + overflow-x:auto; + padding:1em +} +code.hljs { + padding:3px 5px +} +.hljs { + background:#2f1e2e; + color:#a39e9b +} .hljs-comment, .hljs-quote { - color: #575757; + color:#8d8687 } - -/* Red */ -.hljs-variable, -.hljs-template-variable, -.hljs-attribute, -.hljs-tag, -.hljs-name, -.hljs-regexp, .hljs-link, +.hljs-meta, .hljs-name, +.hljs-regexp, +.hljs-selector-class, .hljs-selector-id, -.hljs-selector-class { - color: #d70025; +.hljs-tag, +.hljs-template-variable, +.hljs-variable { + color:#ef6155 } - -/* Orange */ -.hljs-number, -.hljs-meta, .hljs-built_in, -.hljs-builtin-name, +.hljs-deletion, .hljs-literal, -.hljs-type, -.hljs-params { - color: #b21e00; +.hljs-number, +.hljs-params, +.hljs-type { + color:#f99b15 } - -/* Green */ -.hljs-string, -.hljs-symbol, -.hljs-bullet { - color: #008200; +.hljs-attribute, +.hljs-section, +.hljs-title { + color:#fec418 } - -/* Blue */ -.hljs-title, -.hljs-section { - color: #0030f2; +.hljs-addition, +.hljs-bullet, +.hljs-string, +.hljs-symbol { + color:#48b685 } - -/* Purple */ .hljs-keyword, .hljs-selector-tag { - color: #9d00ec; -} - -.hljs { - display: block; - overflow-x: auto; - background: #f6f7f6; - color: #000; - padding: 0.5em; + color:#815ba4 } - .hljs-emphasis { - font-style: italic; + font-style:italic } - .hljs-strong { - font-weight: bold; -} - -.hljs-addition { - color: #22863a; - background-color: #f0fff4; -} - -.hljs-deletion { - color: #b31d28; - background-color: #ffeef0; + font-weight:700 } diff --git a/helix-core/src/indent.rs b/helix-core/src/indent.rs index f5f36aca..55802059 100644 --- a/helix-core/src/indent.rs +++ b/helix-core/src/indent.rs @@ -316,8 +316,12 @@ pub fn suggested_indent_for_pos( pub fn get_scopes(syntax: Option<&Syntax>, text: RopeSlice, pos: usize) -> Vec<&'static str> { let mut scopes = Vec::new(); if let Some(syntax) = syntax { - let byte_start = text.char_to_byte(pos); - let node = match get_highest_syntax_node_at_bytepos(syntax, byte_start) { + let pos = text.char_to_byte(pos); + let mut node = match syntax + .tree() + .root_node() + .descendant_for_byte_range(pos, pos) + { Some(node) => node, None => return scopes, }; @@ -325,7 +329,8 @@ pub fn get_scopes(syntax: Option<&Syntax>, text: RopeSlice, pos: usize) -> Vec<& scopes.push(node.kind()); while let Some(parent) = node.parent() { - scopes.push(parent.kind()) + scopes.push(parent.kind()); + node = parent; } } diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index a7a5d022..1afe0e25 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -144,8 +144,12 @@ impl LanguageConfiguration { &highlights_query, &injections_query, &locals_query, - ) - .unwrap(); // TODO: no unwrap + ); + + let config = match config { + Ok(config) => config, + Err(err) => panic!("{}", err), + }; // TODO: avoid panic config.configure(scopes); Some(Arc::new(config)) } diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index d0a8183f..f2bb0059 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -9,11 +9,17 @@ use lsp_types as lsp; use serde_json::Value; use std::future::Future; use std::process::Stdio; -use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::{ + atomic::{AtomicU64, Ordering}, + Arc, +}; use tokio::{ io::{BufReader, BufWriter}, process::{Child, Command}, - sync::mpsc::{channel, UnboundedReceiver, UnboundedSender}, + sync::{ + mpsc::{channel, UnboundedReceiver, UnboundedSender}, + Notify, OnceCell, + }, }; #[derive(Debug)] @@ -22,18 +28,19 @@ pub struct Client { _process: Child, server_tx: UnboundedSender, request_counter: AtomicU64, - capabilities: Option, + pub(crate) capabilities: OnceCell, offset_encoding: OffsetEncoding, config: Option, } impl Client { + #[allow(clippy::type_complexity)] pub fn start( cmd: &str, args: &[String], config: Option, id: usize, - ) -> Result<(Self, UnboundedReceiver<(usize, Call)>)> { + ) -> Result<(Self, UnboundedReceiver<(usize, Call)>, Arc)> { let process = Command::new(cmd) .args(args) .stdin(Stdio::piped()) @@ -50,22 +57,20 @@ impl Client { let reader = BufReader::new(process.stdout.take().expect("Failed to open stdout")); let stderr = BufReader::new(process.stderr.take().expect("Failed to open stderr")); - let (server_rx, server_tx) = Transport::start(reader, writer, stderr, id); + let (server_rx, server_tx, initialize_notify) = + Transport::start(reader, writer, stderr, id); let client = Self { id, _process: process, server_tx, request_counter: AtomicU64::new(0), - capabilities: None, + capabilities: OnceCell::new(), offset_encoding: OffsetEncoding::Utf8, config, }; - // TODO: async client.initialize() - // maybe use an arc flag - - Ok((client, server_rx)) + Ok((client, server_rx, initialize_notify)) } pub fn id(&self) -> usize { @@ -88,9 +93,13 @@ impl Client { } } + pub fn is_initialized(&self) -> bool { + self.capabilities.get().is_some() + } + pub fn capabilities(&self) -> &lsp::ServerCapabilities { self.capabilities - .as_ref() + .get() .expect("language server not yet initialized!") } @@ -143,7 +152,8 @@ impl Client { }) .map_err(|e| Error::Other(e.into()))?; - timeout(Duration::from_secs(2), rx.recv()) + // TODO: specifiable timeout, delay other calls until initialize success + timeout(Duration::from_secs(20), rx.recv()) .await .map_err(|_| Error::Timeout)? // return Timeout .ok_or(Error::StreamClosed)? @@ -151,7 +161,7 @@ impl Client { } /// Send a RPC notification to the language server. - fn notify( + pub fn notify( &self, params: R::Params, ) -> impl Future> @@ -213,7 +223,7 @@ impl Client { // General messages // ------------------------------------------------------------------------------------------- - pub(crate) async fn initialize(&mut self) -> Result<()> { + pub(crate) async fn initialize(&self) -> Result { // TODO: delay any requests that are triggered prior to initialize let root = find_root(None).and_then(|root| lsp::Url::from_file_path(root).ok()); @@ -281,14 +291,7 @@ impl Client { locale: None, // TODO }; - let response = self.request::(params).await?; - self.capabilities = Some(response.capabilities); - - // next up, notify - self.notify::(lsp::InitializedParams {}) - .await?; - - Ok(()) + self.request::(params).await } pub async fn shutdown(&self) -> Result<()> { @@ -445,7 +448,7 @@ impl Client { ) -> Option>> { // figure out what kind of sync the server supports - let capabilities = self.capabilities.as_ref().unwrap(); + let capabilities = self.capabilities.get().unwrap(); let sync_capabilities = match capabilities.text_document_sync { Some(lsp::TextDocumentSyncCapability::Kind(kind)) @@ -463,7 +466,7 @@ impl Client { // range = None -> whole document range: None, //Some(Range) range_length: None, // u64 apparently deprecated - text: "".to_string(), + text: new_text.to_string(), }] } lsp::TextDocumentSyncKind::Incremental => { @@ -491,12 +494,12 @@ impl Client { // will_save / will_save_wait_until - pub async fn text_document_did_save( + pub fn text_document_did_save( &self, text_document: lsp::TextDocumentIdentifier, text: &Rope, - ) -> Result<()> { - let capabilities = self.capabilities.as_ref().unwrap(); + ) -> Option>> { + let capabilities = self.capabilities.get().unwrap(); let include_text = match &capabilities.text_document_sync { Some(lsp::TextDocumentSyncCapability::Options(lsp::TextDocumentSyncOptions { @@ -508,17 +511,18 @@ impl Client { include_text, }) => include_text.unwrap_or(false), // Supported(false) - _ => return Ok(()), + _ => return None, }, // unsupported - _ => return Ok(()), + _ => return None, }; - self.notify::(lsp::DidSaveTextDocumentParams { - text_document, - text: include_text.then(|| text.into()), - }) - .await + Some(self.notify::( + lsp::DidSaveTextDocumentParams { + text_document, + text: include_text.then(|| text.into()), + }, + )) } pub fn completion( @@ -584,19 +588,19 @@ impl Client { // formatting - pub async fn text_document_formatting( + pub fn text_document_formatting( &self, text_document: lsp::TextDocumentIdentifier, options: lsp::FormattingOptions, work_done_token: Option, - ) -> anyhow::Result> { - let capabilities = self.capabilities.as_ref().unwrap(); + ) -> Option>>> { + let capabilities = self.capabilities.get().unwrap(); // check if we're able to format match capabilities.document_formatting_provider { Some(lsp::OneOf::Left(true)) | Some(lsp::OneOf::Right(_)) => (), // None | Some(false) - _ => return Ok(Vec::new()), + _ => return None, }; // TODO: return err::unavailable so we can fall back to tree sitter formatting @@ -606,9 +610,13 @@ impl Client { work_done_progress_params: lsp::WorkDoneProgressParams { work_done_token }, }; - let response = self.request::(params).await?; + let request = self.call::(params); - Ok(response.unwrap_or_default()) + Some(async move { + let json = request.await?; + let response: Vec = serde_json::from_value(json)?; + Ok(response) + }) } pub async fn text_document_range_formatting( @@ -618,7 +626,7 @@ impl Client { options: lsp::FormattingOptions, work_done_token: Option, ) -> anyhow::Result> { - let capabilities = self.capabilities.as_ref().unwrap(); + let capabilities = self.capabilities.get().unwrap(); // check if we're able to format match capabilities.document_range_formatting_provider { diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index 72606b70..7357c885 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -226,6 +226,8 @@ impl MethodCall { #[derive(Debug, PartialEq, Clone)] pub enum Notification { + // we inject this notification to signal the LSP is ready + Initialized, PublishDiagnostics(lsp::PublishDiagnosticsParams), ShowMessage(lsp::ShowMessageParams), LogMessage(lsp::LogMessageParams), @@ -237,6 +239,7 @@ impl Notification { use lsp::notification::Notification as _; let notification = match method { + lsp::notification::Initialized::METHOD => Self::Initialized, lsp::notification::PublishDiagnostics::METHOD => { let params: lsp::PublishDiagnosticsParams = params .parse() @@ -294,7 +297,7 @@ impl Registry { } } - pub fn get_by_id(&mut self, id: usize) -> Option<&Client> { + pub fn get_by_id(&self, id: usize) -> Option<&Client> { self.inner .values() .find(|(client_id, _)| client_id == &id) @@ -302,33 +305,52 @@ impl Registry { } pub fn get(&mut self, language_config: &LanguageConfiguration) -> Result> { - if let Some(config) = &language_config.language_server { - // avoid borrow issues - let inner = &mut self.inner; - let s_incoming = &mut self.incoming; - - match inner.entry(language_config.scope.clone()) { - Entry::Occupied(entry) => Ok(entry.get().1.clone()), - Entry::Vacant(entry) => { - // initialize a new client - let id = self.counter.fetch_add(1, Ordering::Relaxed); - let (mut client, incoming) = Client::start( - &config.command, - &config.args, - serde_json::from_str(language_config.config.as_deref().unwrap_or("")).ok(), - id, - )?; - // TODO: run this async without blocking - futures_executor::block_on(client.initialize())?; - s_incoming.push(UnboundedReceiverStream::new(incoming)); - let client = Arc::new(client); - - entry.insert((id, client.clone())); - Ok(client) - } + let config = match &language_config.language_server { + Some(config) => config, + None => return Err(Error::LspNotDefined), + }; + + match self.inner.entry(language_config.scope.clone()) { + Entry::Occupied(entry) => Ok(entry.get().1.clone()), + Entry::Vacant(entry) => { + // initialize a new client + let id = self.counter.fetch_add(1, Ordering::Relaxed); + let (client, incoming, initialize_notify) = Client::start( + &config.command, + &config.args, + serde_json::from_str(language_config.config.as_deref().unwrap_or("")).ok(), + id, + )?; + self.incoming.push(UnboundedReceiverStream::new(incoming)); + let client = Arc::new(client); + + // Initialize the client asynchronously + let _client = client.clone(); + tokio::spawn(async move { + use futures_util::TryFutureExt; + let value = _client + .capabilities + .get_or_try_init(|| { + _client + .initialize() + .map_ok(|response| response.capabilities) + }) + .await; + + value.expect("failed to initialize capabilities"); + + // next up, notify + _client + .notify::(lsp::InitializedParams {}) + .await + .unwrap(); + + initialize_notify.notify_one(); + }); + + entry.insert((id, client.clone())); + Ok(client) } - } else { - Err(Error::LspNotDefined) } } @@ -415,32 +437,6 @@ impl LspProgressMap { } } -// REGISTRY = HashMap>> -// spawn one server per language type, need to spawn one per workspace if server doesn't support -// workspaces -// -// could also be a client per root dir -// -// storing a copy of Option>> on Document would make the LSP client easily -// accessible during edit/save callbacks -// -// the event loop needs to process all incoming streams, maybe we can just have that be a separate -// task that's continually running and store the state on the client, then use read lock to -// retrieve data during render -// -> PROBLEM: how do you trigger an update on the editor side when data updates? -// -// -> The data updates should pull all events until we run out so we don't frequently re-render -// -// -// v2: -// -// there should be a registry of lsp clients, one per language type (or workspace). -// the clients should lazy init on first access -// the client.initialize() should be called async and we buffer any requests until that completes -// there needs to be a way to process incoming lsp messages from all clients. -// -> notifications need to be dispatched to wherever -// -> requests need to generate a reply and travel back to the same lsp! - #[cfg(test)] mod tests { use super::{lsp, util::*, OffsetEncoding}; diff --git a/helix-lsp/src/transport.rs b/helix-lsp/src/transport.rs index 67b7b48f..6e28094d 100644 --- a/helix-lsp/src/transport.rs +++ b/helix-lsp/src/transport.rs @@ -1,7 +1,7 @@ -use crate::Result; +use crate::{Error, Result}; use anyhow::Context; use jsonrpc_core as jsonrpc; -use log::{debug, error, info, warn}; +use log::{error, info}; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; @@ -11,7 +11,7 @@ use tokio::{ process::{ChildStderr, ChildStdin, ChildStdout}, sync::{ mpsc::{unbounded_channel, Sender, UnboundedReceiver, UnboundedSender}, - Mutex, + Mutex, Notify, }, }; @@ -51,9 +51,11 @@ impl Transport { ) -> ( UnboundedReceiver<(usize, jsonrpc::Call)>, UnboundedSender, + Arc, ) { let (client_tx, rx) = unbounded_channel(); let (tx, client_rx) = unbounded_channel(); + let notify = Arc::new(Notify::new()); let transport = Self { id, @@ -62,11 +64,21 @@ impl Transport { let transport = Arc::new(transport); - tokio::spawn(Self::recv(transport.clone(), server_stdout, client_tx)); + tokio::spawn(Self::recv( + transport.clone(), + server_stdout, + client_tx.clone(), + )); tokio::spawn(Self::err(transport.clone(), server_stderr)); - tokio::spawn(Self::send(transport, server_stdin, client_rx)); - - (rx, tx) + tokio::spawn(Self::send( + transport, + server_stdin, + client_tx, + client_rx, + notify.clone(), + )); + + (rx, tx, notify) } async fn recv_server_message( @@ -76,14 +88,18 @@ impl Transport { let mut content_length = None; loop { buffer.truncate(0); - reader.read_line(buffer).await?; - let header = buffer.trim(); + if reader.read_line(buffer).await? == 0 { + return Err(Error::StreamClosed); + }; + + // debug!("<- header {:?}", buffer); - if header.is_empty() { + if buffer == "\r\n" { + // look for an empty CRLF line break; } - debug!("<- header {}", header); + let header = buffer.trim(); let parts = header.split_once(": "); @@ -96,7 +112,8 @@ impl Transport { // Workaround: Some non-conformant language servers will output logging and other garbage // into the same stream as JSON-RPC messages. This can also happen from shell scripts that spawn // the server. Skip such lines and log a warning. - warn!("Failed to parse header: {:?}", header); + + // warn!("Failed to parse header: {:?}", header); } } } @@ -121,8 +138,10 @@ impl Transport { buffer: &mut String, ) -> Result<()> { buffer.truncate(0); - err.read_line(buffer).await?; - error!("err <- {}", buffer); + if err.read_line(buffer).await? == 0 { + return Err(Error::StreamClosed); + }; + error!("err <- {:?}", buffer); Ok(()) } @@ -255,16 +274,90 @@ impl Transport { async fn send( transport: Arc, mut server_stdin: BufWriter, + client_tx: UnboundedSender<(usize, jsonrpc::Call)>, mut client_rx: UnboundedReceiver, + initialize_notify: Arc, ) { - while let Some(msg) = client_rx.recv().await { - match transport - .send_payload_to_server(&mut server_stdin, msg) - .await - { - Ok(_) => {} - Err(err) => { - error!("err: <- {:?}", err); + let mut pending_messages: Vec = Vec::new(); + let mut is_pending = true; + + // Determine if a message is allowed to be sent early + fn is_initialize(payload: &Payload) -> bool { + use lsp_types::{ + notification::{Initialized, Notification}, + request::{Initialize, Request}, + }; + match payload { + Payload::Request { + value: jsonrpc::MethodCall { method, .. }, + .. + } if method == Initialize::METHOD => true, + Payload::Notification(jsonrpc::Notification { method, .. }) + if method == Initialized::METHOD => + { + true + } + _ => false, + } + } + + // TODO: events that use capabilities need to do the right thing + + loop { + tokio::select! { + biased; + _ = initialize_notify.notified() => { // TODO: notified is technically not cancellation safe + // server successfully initialized + is_pending = false; + + use lsp_types::notification::Notification; + // Hack: inject an initialized notification so we trigger code that needs to happen after init + let notification = ServerMessage::Call(jsonrpc::Call::Notification(jsonrpc::Notification { + jsonrpc: None, + + method: lsp_types::notification::Initialized::METHOD.to_string(), + params: jsonrpc::Params::None, + })); + match transport.process_server_message(&client_tx, notification).await { + Ok(_) => {} + Err(err) => { + error!("err: <- {:?}", err); + } + } + + // drain the pending queue and send payloads to server + for msg in pending_messages.drain(..) { + log::info!("Draining pending message {:?}", msg); + match transport.send_payload_to_server(&mut server_stdin, msg).await { + Ok(_) => {} + Err(err) => { + error!("err: <- {:?}", err); + } + } + } + } + msg = client_rx.recv() => { + if let Some(msg) = msg { + if is_pending && !is_initialize(&msg) { + // ignore notifications + if let Payload::Notification(_) = msg { + continue; + } + + log::info!("Language server not initialized, delaying request"); + pending_messages.push(msg); + } else { + match transport.send_payload_to_server(&mut server_stdin, msg).await { + Ok(_) => {} + Err(err) => { + error!("err: <- {:?}", err); + } + } + } + } else { + // channel closed + break; + } } } } diff --git a/helix-syntax/build.rs b/helix-syntax/build.rs index 473646fd..28f85e74 100644 --- a/helix-syntax/build.rs +++ b/helix-syntax/build.rs @@ -158,10 +158,9 @@ fn build_dir(dir: &str, language: &str) { .is_none() { eprintln!( - "The directory {} is empty, did you use 'git clone --recursive'?", + "The directory {} is empty, you probably need to use 'git submodule update --init --recursive'?", dir ); - eprintln!("You can fix in using 'git submodule init && git submodule update --recursive'."); std::process::exit(1); } diff --git a/helix-syntax/languages/tree-sitter-julia b/helix-syntax/languages/tree-sitter-julia index 0ba7a24b..12ea5972 160000 --- a/helix-syntax/languages/tree-sitter-julia +++ b/helix-syntax/languages/tree-sitter-julia @@ -1 +1 @@ -Subproject commit 0ba7a24b062b671263ae08e707e9e94383b25bb7 +Subproject commit 12ea597262125fc22fd2e91aa953ac69b19c26ca diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 1fcca681..e21c5504 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -4,7 +4,7 @@ use helix_view::{theme, Editor}; use crate::{args::Args, compositor::Compositor, config::Config, job::Jobs, ui}; -use log::error; +use log::{error, warn}; use std::{ io::{stdout, Write}, @@ -275,16 +275,42 @@ impl Application { }; match notification { - Notification::PublishDiagnostics(params) => { - let path = Some(params.uri.to_file_path().unwrap()); + Notification::Initialized => { + let language_server = + match self.editor.language_servers.get_by_id(server_id) { + Some(language_server) => language_server, + None => { + warn!("can't find language server with id `{}`", server_id); + return; + } + }; + + let docs = self.editor.documents().filter(|doc| { + doc.language_server().map(|server| server.id()) == Some(server_id) + }); - let doc = self - .editor - .documents - .iter_mut() - .find(|(_, doc)| doc.path() == path.as_ref()); + // trigger textDocument/didOpen for docs that are already open + for doc in docs { + // TODO: extract and share with editor.open + let language_id = doc + .language() + .and_then(|s| s.split('.').last()) // source.rust + .map(ToOwned::to_owned) + .unwrap_or_default(); + + tokio::spawn(language_server.text_document_did_open( + doc.url().unwrap(), + doc.version(), + doc.text(), + language_id, + )); + } + } + Notification::PublishDiagnostics(params) => { + let path = params.uri.to_file_path().unwrap(); + let doc = self.editor.document_by_path_mut(&path); - if let Some((_, doc)) = doc { + if let Some(doc) = doc { let text = doc.text(); let diagnostics = params @@ -429,10 +455,27 @@ impl Application { Call::MethodCall(helix_lsp::jsonrpc::MethodCall { method, params, id, .. }) => { + let language_server = match self.editor.language_servers.get_by_id(server_id) { + Some(language_server) => language_server, + None => { + warn!("can't find language server with id `{}`", server_id); + return; + } + }; + let call = match MethodCall::parse(&method, params) { Some(call) => call, None => { error!("Method not found {}", method); + // language_server.reply( + // call.id, + // // TODO: make a Into trait that can cast to Err(jsonrpc::Error) + // Err(helix_lsp::jsonrpc::Error { + // code: helix_lsp::jsonrpc::ErrorCode::MethodNotFound, + // message: "Method not found".to_string(), + // data: None, + // }), + // ); return; } }; @@ -445,53 +488,9 @@ impl Application { if spinner.is_stopped() { spinner.start(); } - - let doc = self.editor.documents().find(|doc| { - doc.language_server() - .map(|server| server.id() == server_id) - .unwrap_or_default() - }); - match doc { - Some(doc) => { - // it's ok to unwrap, we check for the language server before - let server = doc.language_server().unwrap(); - tokio::spawn(server.reply(id, Ok(serde_json::Value::Null))); - } - None => { - if let Some(server) = - self.editor.language_servers.get_by_id(server_id) - { - log::warn!( - "missing document with language server id `{}`", - server_id - ); - tokio::spawn(server.reply( - id, - Err(helix_lsp::jsonrpc::Error { - code: helix_lsp::jsonrpc::ErrorCode::InternalError, - message: "document missing".to_string(), - data: None, - }), - )); - } else { - log::warn!( - "can't find language server with id `{}`", - server_id - ); - } - } - } + tokio::spawn(language_server.reply(id, Ok(serde_json::Value::Null))); } } - // self.language_server.reply( - // call.id, - // // TODO: make a Into trait that can cast to Err(jsonrpc::Error) - // Err(helix_lsp::jsonrpc::Error { - // code: helix_lsp::jsonrpc::ErrorCode::MethodNotFound, - // message: "Method not found".to_string(), - // data: None, - // }), - // ); } e => unreachable!("{:?}", e), } diff --git a/helix-term/src/job.rs b/helix-term/src/job.rs index 2ac41926..4fa38174 100644 --- a/helix-term/src/job.rs +++ b/helix-term/src/job.rs @@ -61,7 +61,7 @@ impl Jobs { } pub fn handle_callback( - &mut self, + &self, editor: &mut Editor, compositor: &mut Compositor, call: anyhow::Result>, @@ -84,7 +84,7 @@ impl Jobs { } } - pub fn add(&mut self, j: Job) { + pub fn add(&self, j: Job) { if j.wait { self.wait_futures.push(j.future); } else { diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index b2c02927..6de60995 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -386,21 +386,24 @@ impl Document { /// If supported, returns the changes that should be applied to this document in order /// to format it nicely. pub fn format(&self) -> Option + 'static> { - if let Some(language_server) = self.language_server.clone() { + if let Some(language_server) = self.language_server() { let text = self.text.clone(); - let id = self.identifier(); + let offset_encoding = language_server.offset_encoding(); + let request = language_server.text_document_formatting( + self.identifier(), + lsp::FormattingOptions::default(), + None, + )?; + let fut = async move { - let edits = language_server - .text_document_formatting(id, lsp::FormattingOptions::default(), None) - .await - .unwrap_or_else(|e| { - log::warn!("LSP formatting failed: {}", e); - Default::default() - }); + let edits = request.await.unwrap_or_else(|e| { + log::warn!("LSP formatting failed: {}", e); + Default::default() + }); LspFormatting { doc: text, edits, - offset_encoding: language_server.offset_encoding(), + offset_encoding, } }; Some(fut) @@ -469,9 +472,14 @@ impl Document { to_writer(&mut file, encoding, &text).await?; if let Some(language_server) = language_server { - language_server - .text_document_did_save(identifier, &text) - .await?; + if language_server.is_initialized() { + return Ok(()); + } + if let Some(notification) = + language_server.text_document_did_save(identifier, &text) + { + notification.await?; + } } Ok(()) @@ -646,7 +654,7 @@ impl Document { // } // emit lsp notification - if let Some(language_server) = &self.language_server { + if let Some(language_server) = self.language_server() { let notify = language_server.text_document_did_change( self.versioned_identifier(), &old_doc, @@ -795,9 +803,18 @@ impl Document { self.version } - #[inline] pub fn language_server(&self) -> Option<&helix_lsp::Client> { - self.language_server.as_deref() + let server = self.language_server.as_deref(); + let initialized = server + .map(|server| server.is_initialized()) + .unwrap_or(false); + + // only resolve language_server if it's initialized + if initialized { + server + } else { + None + } } #[inline] diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 562c3c60..3d2d4a87 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -255,20 +255,21 @@ impl Editor { .and_then(|language| self.language_servers.get(language).ok()); if let Some(language_server) = language_server { - doc.set_language_server(Some(language_server.clone())); - let language_id = doc .language() .and_then(|s| s.split('.').last()) // source.rust .map(ToOwned::to_owned) .unwrap_or_default(); + // TODO: this now races with on_init code if the init happens too quickly tokio::spawn(language_server.text_document_did_open( doc.url().unwrap(), doc.version(), doc.text(), language_id, )); + + doc.set_language_server(Some(language_server)); } let id = self.documents.insert(doc); @@ -287,14 +288,9 @@ impl Editor { if close_buffer { // get around borrowck issues - let language_servers = &mut self.language_servers; let doc = &self.documents[view.doc]; - let language_server = doc - .language - .as_ref() - .and_then(|language| language_servers.get(language).ok()); - if let Some(language_server) = language_server { + if let Some(language_server) = doc.language_server() { tokio::spawn(language_server.text_document_did_close(doc.identifier())); } self.documents.remove(view.doc); @@ -324,20 +320,24 @@ impl Editor { view.ensure_cursor_in_view(doc, self.config.scrolloff) } + #[inline] pub fn document(&self, id: DocumentId) -> Option<&Document> { self.documents.get(id) } + #[inline] pub fn document_mut(&mut self, id: DocumentId) -> Option<&mut Document> { self.documents.get_mut(id) } + #[inline] pub fn documents(&self) -> impl Iterator { - self.documents.iter().map(|(_id, doc)| doc) + self.documents.values() } + #[inline] pub fn documents_mut(&mut self) -> impl Iterator { - self.documents.iter_mut().map(|(_id, doc)| doc) + self.documents.values_mut() } pub fn document_by_path>(&self, path: P) -> Option<&Document> { @@ -345,6 +345,11 @@ impl Editor { .find(|doc| doc.path().map(|p| p == path.as_ref()).unwrap_or(false)) } + pub fn document_by_path_mut>(&mut self, path: P) -> Option<&mut Document> { + self.documents_mut() + .find(|doc| doc.path().map(|p| p == path.as_ref()).unwrap_or(false)) + } + pub fn cursor(&self) -> (Option, CursorKind) { let view = view!(self); let doc = &self.documents[view.doc]; diff --git a/languages.toml b/languages.toml index c04435fe..a29dd38d 100644 --- a/languages.toml +++ b/languages.toml @@ -116,6 +116,17 @@ roots = [] language-server = { command = "typescript-language-server", args = ["--stdio"] } indent = { tab-width = 2, unit = " " } +[[language]] +name = "tsx" +scope = "source.tsx" +injection-regex = "^(tsx)$" # |typescript +file-types = ["tsx"] +roots = [] +# TODO: highlights-jsx, highlights-params + +language-server = { command = "typescript-language-server", args = ["--stdio"] } +indent = { tab-width = 2, unit = " " } + [[language]] name = "css" scope = "source.css" @@ -204,7 +215,22 @@ injection-regex = "julia" file-types = ["jl"] roots = [] comment-token = "#" -language-server = { command = "julia", args = [ "--startup-file=no", "--history-file=no", "-e", "using LanguageServer;using Pkg;import StaticLint;import SymbolServer;env_path = dirname(Pkg.Types.Context().env.project_file);server = LanguageServer.LanguageServerInstance(stdin, stdout, env_path, \"\");server.runlinter = true;run(server);" ] } +language-server = { command = "julia", args = [ + "--startup-file=no", + "--history-file=no", + "--quiet", + "-e", + """ + using LanguageServer; + using Pkg; + import StaticLint; + env_path = dirname(Pkg.Types.Context().env.project_file); + + server = LanguageServer.LanguageServerInstance(stdin, stdout, env_path, ""); + server.runlinter = true; + run(server); + """, + ] } indent = { tab-width = 2, unit = " " } [[language]] diff --git a/runtime/queries/c/highlights.scm b/runtime/queries/c/highlights.scm index 258e07e7..2c42710f 100644 --- a/runtime/queries/c/highlights.scm +++ b/runtime/queries/c/highlights.scm @@ -61,7 +61,7 @@ (null) @constant (number_literal) @number -(char_literal) @number +(char_literal) @string (call_expression function: (identifier) @function) diff --git a/runtime/queries/go/highlights.scm b/runtime/queries/go/highlights.scm index 224c8b78..3129c4b2 100644 --- a/runtime/queries/go/highlights.scm +++ b/runtime/queries/go/highlights.scm @@ -17,9 +17,18 @@ ; Identifiers +((identifier) @constant (match? @constant "^[A-Z][A-Z\\d_]+$")) +(const_spec + name: (identifier) @constant) + +(parameter_declaration (identifier) @variable.parameter) +(variadic_parameter_declaration (identifier) @variable.parameter) + (type_identifier) @type (field_identifier) @property (identifier) @variable +(package_identifier) @variable + ; Operators @@ -79,10 +88,8 @@ "go" "goto" "if" - "import" "interface" "map" - "package" "range" "return" "select" @@ -92,6 +99,29 @@ "var" ] @keyword +[ + "import" + "package" +] @keyword.control.import + +; Delimiters + +[ + ":" + "." + "," + ";" +] @punctuation.delimiter + +[ + "(" + ")" + "[" + "]" + "{" + "}" +] @punctuation.bracket + ; Literals [ @@ -111,7 +141,8 @@ [ (true) (false) - (nil) -] @constant.builtin +] @constant.builtin.boolean + +(nil) @constant.builtin (comment) @comment diff --git a/runtime/queries/go/locals.scm b/runtime/queries/go/locals.scm new file mode 100644 index 00000000..d240e2b7 --- /dev/null +++ b/runtime/queries/go/locals.scm @@ -0,0 +1,30 @@ +; Scopes + +(block) @local.scope + +; Definitions + +(parameter_declaration (identifier) @local.definition) +(variadic_parameter_declaration (identifier) @local.definition) + +(short_var_declaration + left: (expression_list + (identifier) @local.definition)) + +(var_spec + name: (identifier) @local.definition) + +(for_statement + (range_clause + left: (expression_list + (identifier) @local.definition))) + +(const_declaration + (const_spec + name: (identifier) @local.definition)) + +; References + +(identifier) @local.reference +(field_identifier) @local.reference + diff --git a/runtime/queries/haskell/highlights.scm b/runtime/queries/haskell/highlights.scm index ecaa2d2c..dada80b6 100644 --- a/runtime/queries/haskell/highlights.scm +++ b/runtime/queries/haskell/highlights.scm @@ -2,19 +2,19 @@ (operator) @operator (exp_name (constructor) @constructor) (constructor_operator) @operator -(module) @module_name +(module) @namespace (type) @type (type) @class (constructor) @constructor (pragma) @pragma (comment) @comment (signature name: (variable) @fun_type_name) -(function name: (variable) @fun_name) +(function name: (variable) @function) (constraint class: (class_name (type)) @class) (class (class_head class: (class_name (type)) @class)) (instance (instance_head class: (class_name (type)) @class)) -(integer) @literal -(exp_literal (float)) @literal +(integer) @number +(exp_literal (float)) @number (char) @literal (con_unit) @literal (con_list) @literal @@ -39,5 +39,7 @@ "do" @keyword "mdo" @keyword "rec" @keyword -"(" @paren -")" @paren +[ + "(" + ")" +] @punctuation.bracket diff --git a/runtime/queries/javascript/highlights.scm b/runtime/queries/javascript/highlights.scm index a18c38d9..e29829bf 100644 --- a/runtime/queries/javascript/highlights.scm +++ b/runtime/queries/javascript/highlights.scm @@ -87,7 +87,7 @@ (template_string) ] @string -(regex) @string.special +(regex) @string.regexp (number) @number ; Tokens diff --git a/runtime/queries/julia/highlights.scm b/runtime/queries/julia/highlights.scm index a53dabe5..7b7d426c 100644 --- a/runtime/queries/julia/highlights.scm +++ b/runtime/queries/julia/highlights.scm @@ -1,9 +1,3 @@ -(identifier) @variable -;; In case you want type highlighting based on Julia naming conventions (this might collide with mathematical notation) -;((identifier) @type ; exception: mark `A_foo` sort of identifiers as variables - ;(match? @type "^[A-Z][^_]")) -((identifier) @constant - (match? @constant "^[A-Z][A-Z_]{2}[A-Z_]*$")) [ (triple_string) @@ -28,43 +22,43 @@ (call_expression (identifier) @function) (call_expression - (field_expression (identifier) @method .)) + (field_expression (identifier) @function.method .)) (broadcast_call_expression (identifier) @function) (broadcast_call_expression - (field_expression (identifier) @method .)) + (field_expression (identifier) @function.method .)) (parameter_list - (identifier) @parameter) + (identifier) @variable.parameter) (parameter_list (optional_parameter . - (identifier) @parameter)) + (identifier) @variable.parameter)) (typed_parameter - (identifier) @parameter + (identifier) @variable.parameter (identifier) @type) (type_parameter_list (identifier) @type) (typed_parameter - (identifier) @parameter + (identifier) @variable.parameter (parameterized_identifier) @type) (function_expression - . (identifier) @parameter) -(spread_parameter) @parameter + . (identifier) @variable.parameter) +(spread_parameter) @variable.parameter (spread_parameter - (identifier) @parameter) + (identifier) @variable.parameter) (named_argument - . (identifier) @parameter) + . (identifier) @variable.parameter) (argument_list (typed_expression - (identifier) @parameter + (identifier) @variable.parameter (identifier) @type)) (argument_list (typed_expression - (identifier) @parameter + (identifier) @variable.parameter (parameterized_identifier) @type)) ;; Symbol expressions (:my-wanna-be-lisp-keyword) (quote_expression - (identifier)) @symbol + (identifier)) @string.special.symbol ;; Parsing error! foo (::Type) get's parsed as two quote expressions (argument_list @@ -76,7 +70,7 @@ (identifier) @type) (parameterized_identifier (_)) @type (argument_list - (typed_expression . (identifier) @parameter)) + (typed_expression . (identifier) @variable.parameter)) (typed_expression (identifier) @type .) @@ -113,13 +107,13 @@ "end" @keyword (if_statement - ["if" "end"] @conditional) + ["if" "end"] @keyword.control.conditional) (elseif_clause - ["elseif"] @conditional) + ["elseif"] @keyword.control.conditional) (else_clause - ["else"] @conditional) + ["else"] @keyword.control.conditional) (ternary_expression - ["?" ":"] @conditional) + ["?" ":"] @keyword.control.conditional) (function_definition ["function" "end"] @keyword.function) @@ -134,47 +128,57 @@ "type" ] @keyword -((identifier) @keyword (#any-of? @keyword "global" "local")) +((identifier) @keyword (match? @keyword "global|local")) (compound_expression ["begin" "end"] @keyword) (try_statement - ["try" "end" ] @exception) + ["try" "end" ] @keyword.control.exception) (finally_clause - "finally" @exception) + "finally" @keyword.control.exception) (catch_clause - "catch" @exception) + "catch" @keyword.control.exception) (quote_statement ["quote" "end"] @keyword) (let_statement ["let" "end"] @keyword) (for_statement - ["for" "end"] @repeat) + ["for" "end"] @keyword.control.repeat) (while_statement - ["while" "end"] @repeat) -(break_statement) @repeat -(continue_statement) @repeat + ["while" "end"] @keyword.control.repeat) +(break_statement) @keyword.control.repeat +(continue_statement) @keyword.control.repeat (for_binding - "in" @repeat) + "in" @keyword.control.repeat) (for_clause - "for" @repeat) + "for" @keyword.control.repeat) (do_clause ["do" "end"] @keyword) (export_statement - ["export"] @include) + ["export"] @keyword.control.import) [ "using" "module" "import" -] @include +] @keyword.control.import -((identifier) @include (#eq? @include "baremodule")) +((identifier) @keyword.control.import (#eq? @keyword.control.import "baremodule")) (((identifier) @constant.builtin) (match? @constant.builtin "^(nothing|Inf|NaN)$")) -(((identifier) @boolean) (eq? @boolean "true")) -(((identifier) @boolean) (eq? @boolean "false")) +(((identifier) @constant.builtin.boolean) (#eq? @constant.builtin.boolean "true")) +(((identifier) @constant.builtin.boolean) (#eq? @constant.builtin.boolean "false")) + ["::" ":" "." "," "..." "!"] @punctuation.delimiter ["[" "]" "(" ")" "{" "}"] @punctuation.bracket + +["="] @operator + +(identifier) @variable +;; In case you want type highlighting based on Julia naming conventions (this might collide with mathematical notation) +;((identifier) @type ; exception: mark `A_foo` sort of identifiers as variables + ;(match? @type "^[A-Z][^_]")) +((identifier) @constant + (match? @constant "^[A-Z][A-Z_]{2}[A-Z_]*$")) diff --git a/runtime/queries/latex/highlights.scm b/runtime/queries/latex/highlights.scm index cd04a62c..f045c82d 100644 --- a/runtime/queries/latex/highlights.scm +++ b/runtime/queries/latex/highlights.scm @@ -259,7 +259,7 @@ (comment) @comment -(bracket_group) @parameter +(bracket_group) @variable.parameter [(math_operator) "="] @operator @@ -312,7 +312,7 @@ key: (word) @text.reference) (key_val_pair - key: (_) @parameter + key: (_) @variable.parameter value: (_)) ["[" "]" "{" "}"] @punctuation.bracket ;"(" ")" is has no special meaning in LaTeX diff --git a/runtime/queries/lua/highlights.scm b/runtime/queries/lua/highlights.scm index 8e27a39a..40c2be70 100644 --- a/runtime/queries/lua/highlights.scm +++ b/runtime/queries/lua/highlights.scm @@ -23,27 +23,27 @@ "for" "do" "end" -] @keyword.control.loop) +] @keyword.control.repeat) (for_in_statement [ "for" "do" "end" -] @keyword.control.loop) +] @keyword.control.repeat) (while_statement [ "while" "do" "end" -] @keyword.control.loop) +] @keyword.control.repeat) (repeat_statement [ "repeat" "until" -] @keyword.control.loop) +] @keyword.control.repeat) (do_statement [ @@ -65,7 +65,7 @@ "not" "and" "or" -] @keyword.operator +] @operator [ "=" @@ -108,7 +108,7 @@ [ (false) (true) -] @boolean +] @constant.builtin.boolean (nil) @constant.builtin (spread) @constant ;; "..." ((identifier) @constant @@ -116,7 +116,7 @@ ;; Parameters (parameters - (identifier) @parameter) + (identifier) @variable.parameter) ; ;; Functions (function [(function_name) (identifier)] @function) @@ -139,8 +139,8 @@ (function_call [ - ((identifier) @variable (method) @method) - ((_) (method) @method) + ((identifier) @variable (method) @function.method) + ((_) (method) @function.method) (identifier) @function (field_expression (property_identifier) @function) ] diff --git a/runtime/queries/ocaml/highlights.scm b/runtime/queries/ocaml/highlights.scm index 093b3cce..160f2cb4 100644 --- a/runtime/queries/ocaml/highlights.scm +++ b/runtime/queries/ocaml/highlights.scm @@ -25,12 +25,12 @@ (external (value_name) @function) -(method_name) @method +(method_name) @function.method ; Variables ;---------- -(value_pattern) @parameter +(value_pattern) @variable.parameter ; Application ;------------ @@ -60,7 +60,7 @@ [(number) (signed_number)] @number -(character) @character +(character) @constant.character (string) @string @@ -92,7 +92,7 @@ ["include" "open"] @include -["for" "to" "downto" "while" "do" "done"] @keyword.control.loop +["for" "to" "downto" "while" "do" "done"] @keyword.control.repeat ; Macros ;------- diff --git a/runtime/queries/ruby/highlights.scm b/runtime/queries/ruby/highlights.scm index 7f296f3b..8617d6f0 100644 --- a/runtime/queries/ruby/highlights.scm +++ b/runtime/queries/ruby/highlights.scm @@ -100,7 +100,7 @@ (bare_symbol) ] @string.special.symbol -(regex) @string.special.regex +(regex) @string.regexp (escape_sequence) @escape [ diff --git a/runtime/queries/rust/highlights.scm b/runtime/queries/rust/highlights.scm index 6b14d74d..956a5dac 100644 --- a/runtime/queries/rust/highlights.scm +++ b/runtime/queries/rust/highlights.scm @@ -17,7 +17,7 @@ (escape_sequence) @escape (primitive_type) @type.builtin -(boolean_literal) @constant.builtin +(boolean_literal) @constant.builtin.boolean [ (integer_literal) (float_literal) @@ -149,7 +149,7 @@ (mutable_specifier) @keyword.mut - +; TODO: variable.mut to highlight mutable identifiers via locals.scm ; ------- ; Guess Other Types diff --git a/runtime/queries/rust/locals.scm b/runtime/queries/rust/locals.scm new file mode 100644 index 00000000..6428f9b4 --- /dev/null +++ b/runtime/queries/rust/locals.scm @@ -0,0 +1,17 @@ +; Scopes + +(block) @local.scope + +; Definitions + +(parameter + (identifier) @local.definition) + +(let_declaration + pattern: (identifier) @local.definition) + +(closure_parameters (identifier)) @local.definition + +; References +(identifier) @local.reference + diff --git a/runtime/queries/tsx/highlights.scm b/runtime/queries/tsx/highlights.scm new file mode 100644 index 00000000..1b61e36d --- /dev/null +++ b/runtime/queries/tsx/highlights.scm @@ -0,0 +1 @@ +; inherits: typescript diff --git a/runtime/queries/yaml/highlights.scm b/runtime/queries/yaml/highlights.scm index 4ebb4440..2955a4ce 100644 --- a/runtime/queries/yaml/highlights.scm +++ b/runtime/queries/yaml/highlights.scm @@ -1,6 +1,6 @@ (block_mapping_pair key: (_) @property) (flow_mapping (_ key: (_) @property)) -(boolean_scalar) @boolean +(boolean_scalar) @constant.builtin.boolean (null_scalar) @constant.builtin (double_quote_scalar) @string (single_quote_scalar) @string diff --git a/runtime/themes/dark_plus.toml b/runtime/themes/dark_plus.toml index c105d52b..7eeb3f95 100644 --- a/runtime/themes/dark_plus.toml +++ b/runtime/themes/dark_plus.toml @@ -34,6 +34,7 @@ "comment" = { fg = "#6A9955" } "string" = { fg = "#ce9178" } +"string.regexp" = { fg = "regex" } "number" = { fg = "#b5cea8" } "escape" = { fg = "#d7ba7d" } diff --git a/runtime/themes/monokai.toml b/runtime/themes/monokai.toml index 2407591a..a8f03ff3 100644 --- a/runtime/themes/monokai.toml +++ b/runtime/themes/monokai.toml @@ -34,6 +34,7 @@ "comment" = { fg = "#88846F" } "string" = { fg = "#e6db74" } +"string.regexp" = { fg = "regex" } "number" = { fg = "#ae81ff" } "escape" = { fg = "#ae81ff" } diff --git a/theme.toml b/theme.toml index 3166b2d6..49f46b0b 100644 --- a/theme.toml +++ b/theme.toml @@ -9,7 +9,7 @@ special = "honey" property = "white" variable = "lavender" # variable = "almond" # TODO: metavariables only -"variable.parameter" = "lavender" +"variable.parameter" = { fg = "lavender", modifiers = ["underlined"] } "variable.builtin" = "mint" type = "white" "type.builtin" = "white" # TODO: distinguish? @@ -28,9 +28,7 @@ escape = "honey" label = "honey" # TODO: diferentiate doc comment -# concat (ERROR) @syntax-error and "MISSING ;" selectors for errors - -module = "#ff0000" +# concat (ERROR) @error.syntax and "MISSING ;" selectors for errors "ui.background" = { bg = "midnight" } "ui.linenr" = { fg = "comet" }