From 48e344a2a813b875ca813be726387692b7ba0762 Mon Sep 17 00:00:00 2001 From: Grzegorz Baranski Date: Sat, 24 Jul 2021 03:26:43 +0200 Subject: [PATCH 01/12] feat: code actions - document edits (#478) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * wip: Code actions * fix(term): use current macro instead Context::context * feat(lsp): set code_action capabilities * feat(term): set SPC-a to code_action * feat(term): wip on applying code actions * deps: `cargo update` * feat(term): applying code actions edits * fix(term): cleanup of apply_edit * fix(term): applying edits as a whole thing instead one by one * refactor(term): move apply_edits below * fix(term): improve unimplemented messages for further investigation * fix(term): change code action command comment Co-authored-by: Ivan Tham * fix(term): add matching `}` * fix(term): cleanup, todo!() on workspace edit * fix(term): remove unrelated workspace_symbol_picker * fix(term): apply cargo-clippy suggestions * fix(term): replace todo!'s with editor.set_error Co-authored-by: Blaž Hrastnik Co-authored-by: Ivan Tham --- Cargo.lock | 24 ++++---- helix-lsp/src/client.rs | 47 ++++++++++++++ helix-term/src/commands.rs | 121 ++++++++++++++++++++++++++++++++++++- 3 files changed, 178 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0d96b7008..a1f0a7980 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -422,9 +422,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" dependencies = [ "libc", ] @@ -460,9 +460,9 @@ dependencies = [ [[package]] name = "instant" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec" +checksum = "bee0328b1209d157ef001c94dd85b4f8f64139adb0eac2659f4b08382b2f474d" dependencies = [ "cfg-if 1.0.0", ] @@ -494,9 +494,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.97" +version = "0.2.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12b8adadd720df158f4d70dfe7ccc6adb0472d7c55ca83445f6a5ab3e36f8fb6" +checksum = "320cfe77175da3a483efed4bc0adc1968ca050b098ce4f2f1c13a56626128790" [[package]] name = "libloading" @@ -662,9 +662,9 @@ checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" [[package]] name = "pin-project-lite" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc0e1f259c92177c30a4c9d177246edd0a3568b25756a977d0632cf8fa37e905" +checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443" [[package]] name = "pin-utils" @@ -994,9 +994,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b5220f05bb7de7f3f53c7c065e1199b3172696fe2db9f9c4d8ad9b4ee74c342" +checksum = "4ac2e1d4bd0f75279cfd5a076e0d578bbf02c22b7c39e766c437dd49b3ec43e0" dependencies = [ "tinyvec_macros", ] @@ -1029,9 +1029,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c49e3df43841dafb86046472506755d8501c5615673955f6aa17181125d13c37" +checksum = "54473be61f4ebe4efd09cec9bd5d16fa51d70ea0192213d754d2d500457db110" dependencies = [ "proc-macro2", "quote", diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index 1c2a49b52..fd34f45d2 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -247,6 +247,26 @@ impl Client { content_format: Some(vec![lsp::MarkupKind::Markdown]), ..Default::default() }), + code_action: Some(lsp::CodeActionClientCapabilities { + code_action_literal_support: Some(lsp::CodeActionLiteralSupport { + code_action_kind: lsp::CodeActionKindLiteralSupport { + value_set: [ + lsp::CodeActionKind::EMPTY, + lsp::CodeActionKind::QUICKFIX, + lsp::CodeActionKind::REFACTOR, + lsp::CodeActionKind::REFACTOR_EXTRACT, + lsp::CodeActionKind::REFACTOR_INLINE, + lsp::CodeActionKind::REFACTOR_REWRITE, + lsp::CodeActionKind::SOURCE, + lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS, + ] + .iter() + .map(|kind| kind.as_str().to_string()) + .collect(), + }, + }), + ..Default::default() + }), ..Default::default() }), window: Some(lsp::WindowClientCapabilities { @@ -713,4 +733,31 @@ impl Client { self.call::(params) } + + // empty string to get all symbols + pub fn workspace_symbols(&self, query: String) -> impl Future> { + let params = lsp::WorkspaceSymbolParams { + query, + work_done_progress_params: lsp::WorkDoneProgressParams::default(), + partial_result_params: lsp::PartialResultParams::default(), + }; + + self.call::(params) + } + + pub fn code_actions( + &self, + text_document: lsp::TextDocumentIdentifier, + range: lsp::Range, + ) -> impl Future> { + let params = lsp::CodeActionParams { + text_document, + range, + context: lsp::CodeActionContext::default(), + work_done_progress_params: lsp::WorkDoneProgressParams::default(), + partial_result_params: lsp::PartialResultParams::default(), + }; + + self.call::(params) + } } diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 9b72a8e90..24c53897c 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -9,8 +9,8 @@ use helix_core::{ object, pos_at_coords, regex::{self, Regex}, register::Register, - search, selection, surround, textobject, LineEnding, Position, Range, Rope, RopeGraphemes, - RopeSlice, Selection, SmallVec, Tendril, Transaction, + search, selection, surround, textobject, Change, LineEnding, Position, Range, Rope, + RopeGraphemes, RopeSlice, Selection, SmallVec, Tendril, Transaction, }; use helix_view::{ @@ -215,6 +215,7 @@ impl Command { append_mode, command_mode, file_picker, + code_action, buffer_picker, symbol_picker, last_picker, @@ -2092,6 +2093,120 @@ fn symbol_picker(cx: &mut Context) { ) } +pub fn code_action(cx: &mut Context) { + let (view, doc) = current!(cx.editor); + + let language_server = match doc.language_server() { + Some(language_server) => language_server, + None => return, + }; + + let range = range_to_lsp_range( + doc.text(), + doc.selection(view.id).primary(), + language_server.offset_encoding(), + ); + + let future = language_server.code_actions(doc.identifier(), range); + let offset_encoding = language_server.offset_encoding(); + + cx.callback( + future, + move |_editor: &mut Editor, + compositor: &mut Compositor, + response: Option| { + if let Some(actions) = response { + let picker = Picker::new( + actions, + |action| match action { + lsp::CodeActionOrCommand::CodeAction(action) => { + action.title.as_str().into() + } + lsp::CodeActionOrCommand::Command(command) => command.title.as_str().into(), + }, + move |editor, code_action, _action| match code_action { + lsp::CodeActionOrCommand::Command(command) => { + log::debug!("code action command: {:?}", command); + editor.set_error(String::from("Handling code action command is not implemented yet, see https://github.com/helix-editor/helix/issues/183")); + } + lsp::CodeActionOrCommand::CodeAction(code_action) => { + log::debug!("code action: {:?}", code_action); + if let Some(ref workspace_edit) = code_action.edit { + apply_workspace_edit(editor, offset_encoding, workspace_edit) + } + } + }, + ); + compositor.push(Box::new(picker)) + } + }, + ) +} + +fn apply_workspace_edit( + editor: &mut Editor, + offset_encoding: OffsetEncoding, + workspace_edit: &lsp::WorkspaceEdit, +) { + let edits_to_transaction = |doc: &Rope, edits: &Vec<&lsp::TextEdit>| { + let lsp_pos_to_pos = |lsp_pos| lsp_pos_to_pos(&doc, lsp_pos, offset_encoding).unwrap(); + let changes = edits.iter().map(|edit| -> Change { + log::debug!("text edit: {:?}", edit); + // This clone probably could be optimized if Picker::new would give T instead of &T + let text_replacement = Tendril::from(edit.new_text.clone()); + ( + lsp_pos_to_pos(edit.range.start), + lsp_pos_to_pos(edit.range.end), + Some(text_replacement), + ) + }); + Transaction::change(doc, changes) + }; + + if let Some(ref changes) = workspace_edit.changes { + log::debug!("workspace changes: {:?}", changes); + editor.set_error(String::from("Handling workspace changesis not implemented yet, see https://github.com/helix-editor/helix/issues/183")); + return; + // Not sure if it works properly, it'll be safer to just panic here to avoid breaking some parts of code on which code actions will be used + // TODO: find some example that uses workspace changes, and test it + // for (url, edits) in changes.iter() { + // let file_path = url.origin().ascii_serialization(); + // let file_path = std::path::PathBuf::from(file_path); + // let file = std::fs::File::open(file_path).unwrap(); + // let mut text = Rope::from_reader(file).unwrap(); + // let transaction = edits_to_changes(&text, edits); + // transaction.apply(&mut text); + // } + } + + if let Some(ref document_changes) = workspace_edit.document_changes { + match document_changes { + lsp::DocumentChanges::Edits(document_edits) => { + for document_edit in document_edits { + let (view, doc) = current!(editor); + assert_eq!(doc.url().unwrap(), document_edit.text_document.uri); + let edits = document_edit + .edits + .iter() + .map(|edit| match edit { + lsp::OneOf::Left(text_edit) => text_edit, + lsp::OneOf::Right(annotated_text_edit) => { + &annotated_text_edit.text_edit + } + }) + .collect(); + let transaction = edits_to_transaction(doc.text(), &edits); + doc.apply(&transaction, view.id); + } + } + lsp::DocumentChanges::Operations(operations) => { + log::debug!("document changes - operations: {:?}", operations); + editor.set_error(String::from("Handling document operations is not implemented yet, see https://github.com/helix-editor/helix/issues/183")); + } + } + } +} + fn last_picker(cx: &mut Context) { // TODO: last picker does not seemed to work well with buffer_picker cx.callback = Some(Box::new(|compositor: &mut Compositor| { @@ -3781,6 +3896,8 @@ mode_info! { "P" => paste_clipboard_before, /// replace selections with clipboard "R" => replace_selections_with_clipboard, + /// perform code action + "a" => code_action, /// keep primary selection "space" => keep_primary_selection, } From 6a8a01df6b54f7c169b2c22d4e057eb4925c8a1b Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Sat, 24 Jul 2021 15:44:25 +0530 Subject: [PATCH 02/12] Add missing keybinds to docs --- book/src/keymap.md | 72 ++++++++++++++++++++++++++-------------------- 1 file changed, 41 insertions(+), 31 deletions(-) diff --git a/book/src/keymap.md b/book/src/keymap.md index 5d6e57956..d1d23a6d6 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -41,29 +41,29 @@ ### Changes -| Key | Description | -| ----- | ----------- | -| `r` | Replace with a character | -| `R` | Replace with yanked text | -| `~` | Switch case of the selected text | -| `\`` | Set the selected text to upper case | -| `Alt-\`` | Set the selected text to lower case | -| `i` | Insert before selection | -| `a` | Insert after selection (append) | -| `I` | Insert at the start of the line | -| `A` | Insert at the end of the line | -| `o` | Open new line below selection | -| `o` | Open new line above selection | -| `u` | Undo change | -| `U` | Redo change | -| `y` | Yank selection | -| `p` | Paste after selection | -| `P` | Paste before selection | -| `>` | Indent selection | -| `<` | Unindent selection | -| `=` | Format selection | -| `d` | Delete selection | -| `c` | Change selection (delete and enter insert mode) | +| Key | Description | +| ----- | ----------- | +| `r` | Replace with a character | +| `R` | Replace with yanked text | +| `~` | Switch case of the selected text | +| `` ` `` | Set the selected text to lower case | +| `` Alt-` `` | Set the selected text to upper case | +| `i` | Insert before selection | +| `a` | Insert after selection (append) | +| `I` | Insert at the start of the line | +| `A` | Insert at the end of the line | +| `o` | Open new line below selection | +| `o` | Open new line above selection | +| `u` | Undo change | +| `U` | Redo change | +| `y` | Yank selection | +| `p` | Paste after selection | +| `P` | Paste before selection | +| `>` | Indent selection | +| `<` | Unindent selection | +| `=` | Format selection | +| `d` | Delete selection | +| `c` | Change selection (delete and enter insert mode) | ### Selection manipulation @@ -78,11 +78,19 @@ | `x` | Select current line, if already selected, extend to next line | | `X` | Extend selection to line bounds (line-wise selection) | | | Expand selection to parent syntax node TODO: pick a key | -| `J` | join lines inside selection | -| `K` | keep selections matching the regex TODO: overlapped by hover help | -| `Space` | keep only the primary selection TODO: overlapped by space mode | +| `J` | Join lines inside selection | +| `K` | Keep selections matching the regex TODO: overlapped by hover help | +| `Space` | Keep only the primary selection TODO: overlapped by space mode | | `Ctrl-c` | Comment/uncomment the selections | +### Insert Mode + +| Key | Description | +| ----- | ----------- | +| `Escape` | Switch to normal mode | +| `Ctrl-x` | Autocomplete | +| `Ctrl-w` | Delete previous word | + ### Search > TODO: The search implementation isn't ideal yet -- we don't support searching @@ -190,13 +198,15 @@ This layer is a kludge of mappings I had under leader key in neovim. | `f` | Open file picker | | `b` | Open buffer picker | | `s` | Open symbol picker (current document) | +| `a` | Apply code action | +| `'` | Open last fuzzy picker | | `w` | Enter [window mode](#window-mode) | | `space` | Keep primary selection TODO: it's here because space mode replaced it | -| `p` | paste system clipboard after selections | -| `P` | paste system clipboard before selections | -| `y` | join and yank selections to clipboard | -| `Y` | yank main selection to clipboard | -| `R` | replace selections by clipboard contents | +| `p` | Paste system clipboard after selections | +| `P` | Paste system clipboard before selections | +| `y` | Join and yank selections to clipboard | +| `Y` | Yank main selection to clipboard | +| `R` | Replace selections by clipboard contents | # Picker From 41f62c31572ce05080e8a3469457895f0de7c6e8 Mon Sep 17 00:00:00 2001 From: Yusuf Bera Ertan Date: Sat, 24 Jul 2021 21:36:35 +0300 Subject: [PATCH 03/12] build(nix): fix build issues --- flake.lock | 24 ++++++++++++------------ flake.nix | 55 +++++++++++++++++++++++++++++++++++++++--------------- 2 files changed, 52 insertions(+), 27 deletions(-) diff --git a/flake.lock b/flake.lock index aa5937548..34b9ea899 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "devshell": { "locked": { - "lastModified": 1622711433, - "narHash": "sha256-rGjXz7FA7HImAT3TtoqwecByLO5yhVPSwPdaYPBFRQw=", + "lastModified": 1625086391, + "narHash": "sha256-IpNPv1v8s4L3CoxhwcgZIitGpcrnNgnj09X7TA0QV3k=", "owner": "numtide", "repo": "devshell", - "rev": "1f4fb67b662b65fa7cfe696fc003fcc1e8f7cc36", + "rev": "4b5ac7cf7d9a1cc60b965bb51b59922f2210cbc7", "type": "github" }, "original": { @@ -40,11 +40,11 @@ "rustOverlay": "rustOverlay" }, "locked": { - "lastModified": 1624244973, - "narHash": "sha256-h+b4CwPjyibgwMYAeBaT5qBnxI0fsmGf66k23FqEH5Y=", + "lastModified": 1627106928, + "narHash": "sha256-JaQE0BEk1G1eT539WbYyrA2re4YYL9xo7cB+ZiV4nNM=", "owner": "yusdacra", "repo": "nix-cargo-integration", - "rev": "00f5df6d8e7eeeac2764b7fa2c57e2e81f5d47cd", + "rev": "e08af05413a2d53dadbd1a39976e1da0e5385970", "type": "github" }, "original": { @@ -55,11 +55,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1624024598, - "narHash": "sha256-X++38oH5MKEmPW4/2WdMaHQvwJzO8pJfbnzMD7DbG1E=", + "lastModified": 1626852498, + "narHash": "sha256-lOXUJvi0FJUXHTVSiC5qsMRtEUgqM4mGZpMESLuGhmo=", "owner": "nixos", "repo": "nixpkgs", - "rev": "33d42ad7cf2769ce6364ed4e52afa8e9d1439d58", + "rev": "16105403bdd843540cbef9c63fc0f16c1c6eaa70", "type": "github" }, "original": { @@ -79,11 +79,11 @@ "rustOverlay": { "flake": false, "locked": { - "lastModified": 1624242197, - "narHash": "sha256-J0+j4DYFaE0O0marb4QN/S1bUhpGwAjQ4O04kIYKcb8=", + "lastModified": 1627092891, + "narHash": "sha256-6nN+rfsP+SNpnL3UPbrcwZe4qfh9/NH0LWtXhn9w/a4=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "df5d330f34b64194d64dcbafb91e82e01a89a229", + "rev": "939f2cf1aebc86bc3e9544645b495cd05995524a", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 19d61e62a..334458a9d 100644 --- a/flake.nix +++ b/flake.nix @@ -28,27 +28,53 @@ preConfigure = "ln -s ${common.root}/{languages.toml,theme.toml} .."; buildInputs = (prev.buildInputs or [ ]) ++ [ common.cCompiler.cc.lib ]; }; - # link runtime since helix-core expects it because of embed_runtime feature - helix-core = _: { preConfigure = "ln -s ${common.root + "/runtime"} ../runtime"; }; # link languages and theme toml files since helix-view expects them helix-view = _: { preConfigure = "ln -s ${common.root}/{languages.toml,theme.toml} .."; }; - helix-syntax = prev: - let - helix = common.pkgs.fetchgit { - url = "https://github.com/helix-editor/helix.git"; - rev = "9fd17d4ff5b81211317da1a28d2b30442a512ffc"; - fetchSubmodules = true; - sha256 = "sha256-y652sn/tCc1XoKr3YxDZv6bS2Cmr6+9K/wzzNAMFZJw="; - }; - in - { - src = common.pkgs.runCommand prev.src.name { } '' + helix-syntax = prev: { + src = + let + pkgs = common.pkgs; + helix = pkgs.fetchgit { + url = "https://github.com/helix-editor/helix.git"; + rev = "d4bd5b37669708361a0a6cd2917464b010e6b7f5"; + fetchSubmodules = true; + sha256 = "sha256-KayR7K7UC0mT6EjHsZsCYY9IVDJzft63fGpPKGSY8nQ="; + }; + in + pkgs.runCommand prev.src.name { } '' mkdir -p $out ln -s ${prev.src}/* $out ln -sf ${helix}/helix-syntax/languages $out ''; - }; + preConfigure = "mkdir -p ../runtime/grammars"; + postInstall = "cp -r ../runtime $out/runtime"; + }; }; + mainBuild = common: prev: + let + inherit (common) pkgs lib; + helixSyntax = lib.buildCrate { + root = self; + memberName = "helix-syntax"; + defaultCrateOverrides = { + helix-syntax = common.crateOverrides.helix-syntax; + }; + release = false; + }; + runtimeDir = pkgs.runCommand "helix-runtime" { } '' + mkdir -p $out + ln -s ${common.root}/runtime/* $out + ln -sf ${helixSyntax}/runtime/grammars $out + ''; + in + lib.optionalAttrs (common.memberName == "helix-term") { + nativeBuildInputs = [ pkgs.makeWrapper ]; + postFixup = '' + if [ -f "$out/bin/hx" ]; then + wrapProgram "$out/bin/hx" --set HELIX_RUNTIME "${runtimeDir}" + fi + ''; + }; shell = common: prev: { packages = prev.packages ++ (with common.pkgs; [ lld_10 lldb cargo-tarpaulin ]); env = prev.env ++ [ @@ -57,7 +83,6 @@ { name = "RUSTFLAGS"; value = "-C link-arg=-fuse-ld=lld -C target-cpu=native"; } ]; }; - build = _: prev: { rootFeatures = prev.rootFeatures ++ [ "embed_runtime" ]; }; }; }; } From 8da58fe44a040ad6219891251d3b86d6026d0260 Mon Sep 17 00:00:00 2001 From: gbaranski Date: Sat, 24 Jul 2021 23:37:33 +0200 Subject: [PATCH 04/12] fix(term): use existing implementation of edits_to_transaction --- helix-term/src/commands.rs | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 24c53897c..4d707de4b 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -9,8 +9,8 @@ use helix_core::{ object, pos_at_coords, regex::{self, Regex}, register::Register, - search, selection, surround, textobject, Change, LineEnding, Position, Range, Rope, - RopeGraphemes, RopeSlice, Selection, SmallVec, Tendril, Transaction, + search, selection, surround, textobject, LineEnding, Position, Range, Rope, RopeGraphemes, + RopeSlice, Selection, SmallVec, Tendril, Transaction, }; use helix_view::{ @@ -2148,21 +2148,6 @@ fn apply_workspace_edit( offset_encoding: OffsetEncoding, workspace_edit: &lsp::WorkspaceEdit, ) { - let edits_to_transaction = |doc: &Rope, edits: &Vec<&lsp::TextEdit>| { - let lsp_pos_to_pos = |lsp_pos| lsp_pos_to_pos(&doc, lsp_pos, offset_encoding).unwrap(); - let changes = edits.iter().map(|edit| -> Change { - log::debug!("text edit: {:?}", edit); - // This clone probably could be optimized if Picker::new would give T instead of &T - let text_replacement = Tendril::from(edit.new_text.clone()); - ( - lsp_pos_to_pos(edit.range.start), - lsp_pos_to_pos(edit.range.end), - Some(text_replacement), - ) - }); - Transaction::change(doc, changes) - }; - if let Some(ref changes) = workspace_edit.changes { log::debug!("workspace changes: {:?}", changes); editor.set_error(String::from("Handling workspace changesis not implemented yet, see https://github.com/helix-editor/helix/issues/183")); @@ -2194,8 +2179,14 @@ fn apply_workspace_edit( &annotated_text_edit.text_edit } }) + .cloned() .collect(); - let transaction = edits_to_transaction(doc.text(), &edits); + + let transaction = helix_lsp::util::generate_transaction_from_edits( + doc.text(), + edits, + offset_encoding, + ); doc.apply(&transaction, view.id); } } From e07e42dcfb24dae348c625822abf74fa87968bff Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Sat, 24 Jul 2021 23:44:19 +0200 Subject: [PATCH 05/12] fix(term): undo-ing code actions --- helix-term/src/commands.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 4d707de4b..06dca5d56 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2188,6 +2188,7 @@ fn apply_workspace_edit( offset_encoding, ); doc.apply(&transaction, view.id); + doc.append_changes_to_history(view.id); } } lsp::DocumentChanges::Operations(operations) => { From 112ae5cffe8cc645623cd2cb04cbee2e69c37a09 Mon Sep 17 00:00:00 2001 From: Omnikar Date: Sun, 25 Jul 2021 22:00:58 -0400 Subject: [PATCH 06/12] Determine whether to use a margin of 0 or 1 when uncommenting (#476) * Implement `margin` calculation for uncommenting * Move `margin` calculation to `find_line_comment` * Fix comment bug with multiple selections on a line * Fix `find_line_comment` test for new return type * Generate a single vec of lines for comment toggle `toggle_line_comments` collects the lines covered by all selections into a `Vec`, skipping duplicates. `find_line_comment` now returns the lines to operate on, instead of returning the lines to skip. * Fix test for `find_line_comment` * Reserve length of `to_change` instead of `lines` The length of `lines` includes blank lines which will be skipped, and as such do not need space for a change reserved for them. `to_change` includes only the lines which will be changed. * Use `token.chars().count()` for token char length * Create `changes` with capacity instead of reserving * Remove unnecessary clones in `test_find_line_comment` * Add test case for 0 margin comments * Add comments explaining `find_line_comment` --- helix-core/src/comment.rs | 89 +++++++++++++++++++++++++-------------- 1 file changed, 57 insertions(+), 32 deletions(-) diff --git a/helix-core/src/comment.rs b/helix-core/src/comment.rs index 5d564055d..fadd88e05 100644 --- a/helix-core/src/comment.rs +++ b/helix-core/src/comment.rs @@ -1,17 +1,27 @@ use crate::{ find_first_non_whitespace_char, Change, Rope, RopeSlice, Selection, Tendril, Transaction, }; -use core::ops::Range; use std::borrow::Cow; +/// Given text, a comment token, and a set of line indices, returns the following: +/// - Whether the given lines should be considered commented +/// - If any of the lines are uncommented, all lines are considered as such. +/// - The lines to change for toggling comments +/// - This is all provided lines excluding blanks lines. +/// - The column of the comment tokens +/// - Column of existing tokens, if the lines are commented; column to place tokens at otherwise. +/// - The margin to the right of the comment tokens +/// - Defaults to `1`. If any existing comment token is not followed by a space, changes to `0`. fn find_line_comment( token: &str, text: RopeSlice, - lines: Range, -) -> (bool, Vec, usize) { + lines: impl IntoIterator, +) -> (bool, Vec, usize, usize) { let mut commented = true; - let mut skipped = Vec::new(); + let mut to_change = Vec::new(); let mut min = usize::MAX; // minimum col for find_first_non_whitespace_char + let mut margin = 1; + let token_len = token.chars().count(); for line in lines { let line_slice = text.line(line); if let Some(pos) = find_first_non_whitespace_char(line_slice) { @@ -29,47 +39,53 @@ fn find_line_comment( // considered uncommented. commented = false; } - } else { - // blank line - skipped.push(line); + + // determine margin of 0 or 1 for uncommenting; if any comment token is not followed by a space, + // a margin of 0 is used for all lines. + if matches!(line_slice.get_char(pos + token_len), Some(c) if c != ' ') { + margin = 0; + } + + // blank lines don't get pushed. + to_change.push(line); } } - (commented, skipped, min) + (commented, to_change, min, margin) } #[must_use] pub fn toggle_line_comments(doc: &Rope, selection: &Selection, token: Option<&str>) -> Transaction { let text = doc.slice(..); - let mut changes: Vec = Vec::new(); let token = token.unwrap_or("//"); let comment = Tendril::from(format!("{} ", token)); + let mut lines: Vec = Vec::new(); + + let mut min_next_line = 0; for selection in selection { - let start = text.char_to_line(selection.from()); - let end = text.char_to_line(selection.to()); - let lines = start..end + 1; - let (commented, skipped, min) = find_line_comment(&token, text, lines.clone()); + let start = text.char_to_line(selection.from()).max(min_next_line); + let end = text.char_to_line(selection.to()) + 1; + lines.extend(start..end); + min_next_line = end + 1; + } - changes.reserve((end - start).saturating_sub(skipped.len())); + let (commented, to_change, min, margin) = find_line_comment(&token, text, lines); - for line in lines { - if skipped.contains(&line) { - continue; - } + let mut changes: Vec = Vec::with_capacity(to_change.len()); - let pos = text.line_to_char(line) + min; + for line in to_change { + let pos = text.line_to_char(line) + min; - if !commented { - // comment line - changes.push((pos, pos, Some(comment.clone()))) - } else { - // uncomment line - let margin = 1; // TODO: margin is hardcoded 1 but could easily be 0 - changes.push((pos, pos + token.len() + margin, None)) - } + if !commented { + // comment line + changes.push((pos, pos, Some(comment.clone()))); + } else { + // uncomment line + changes.push((pos, pos + token.len() + margin, None)); } } + Transaction::change(doc, changes.into_iter()) } @@ -91,23 +107,32 @@ mod test { let text = state.doc.slice(..); let res = find_line_comment("//", text, 0..3); - // (commented = true, skipped = [line 1], min = col 2) - assert_eq!(res, (false, vec![1], 2)); + // (commented = true, to_change = [line 0, line 2], min = col 2, margin = 1) + assert_eq!(res, (false, vec![0, 2], 2, 1)); // comment let transaction = toggle_line_comments(&state.doc, &state.selection, None); transaction.apply(&mut state.doc); - state.selection = state.selection.clone().map(transaction.changes()); + state.selection = state.selection.map(transaction.changes()); assert_eq!(state.doc, " // 1\n\n // 2\n // 3"); // uncomment let transaction = toggle_line_comments(&state.doc, &state.selection, None); transaction.apply(&mut state.doc); - state.selection = state.selection.clone().map(transaction.changes()); + state.selection = state.selection.map(transaction.changes()); + assert_eq!(state.doc, " 1\n\n 2\n 3"); + + // 0 margin comments + state.doc = Rope::from(" //1\n\n //2\n //3"); + // reset the selection. + state.selection = Selection::single(0, state.doc.len_chars() - 1); + + let transaction = toggle_line_comments(&state.doc, &state.selection, None); + transaction.apply(&mut state.doc); + state.selection = state.selection.map(transaction.changes()); assert_eq!(state.doc, " 1\n\n 2\n 3"); - // TODO: account for no margin after comment // TODO: account for uncommenting with uneven comment indentation } } From f24007b30f9655b2d5e74ccf8164fafed8b54d77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Sat, 24 Jul 2021 15:31:03 +0900 Subject: [PATCH 07/12] Improve rust indentation queries if/if let are already handled by block, and keeping these scopes would indent else blocks one level too far. --- runtime/queries/rust/indents.toml | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/runtime/queries/rust/indents.toml b/runtime/queries/rust/indents.toml index d0115cb59..6609a756e 100644 --- a/runtime/queries/rust/indents.toml +++ b/runtime/queries/rust/indents.toml @@ -1,11 +1,4 @@ indent = [ - "while_expression", - "for_expression", - "loop_expression", - "if_expression", - "if_let_expression", - "tuple_expression", - "array_expression", "use_list", "block", "match_block", @@ -19,6 +12,8 @@ indent = [ "enum_variant_list", "binary_expression", "field_expression", + "tuple_expression", + "array_expression", "where_clause", "macro_invocation" ] From 29cefa1be860e38a11347f0798159e0f4ddfe173 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Sat, 24 Jul 2021 15:33:55 +0900 Subject: [PATCH 08/12] rust: Indent multi line call expressions one level deeper --- runtime/queries/rust/indents.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/runtime/queries/rust/indents.toml b/runtime/queries/rust/indents.toml index 6609a756e..3900f0b91 100644 --- a/runtime/queries/rust/indents.toml +++ b/runtime/queries/rust/indents.toml @@ -10,6 +10,7 @@ indent = [ "struct_pattern", "tuple_pattern", "enum_variant_list", + "call_expression", "binary_expression", "field_expression", "tuple_expression", From 63e54e30a74bb0d1d782877ddbbcf95f2817d061 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Sat, 24 Jul 2021 17:48:45 +0900 Subject: [PATCH 09/12] Implement in-memory prompt history Implementation is similar to kakoune: we store the entries into a register. --- helix-core/src/register.rs | 14 ++++----- helix-term/src/commands.rs | 5 ++-- helix-term/src/ui/mod.rs | 1 + helix-term/src/ui/picker.rs | 1 + helix-term/src/ui/prompt.rs | 58 +++++++++++++++++++++++++++++++++++++ 5 files changed, 70 insertions(+), 9 deletions(-) diff --git a/helix-core/src/register.rs b/helix-core/src/register.rs index cc881a178..c3e6652e6 100644 --- a/helix-core/src/register.rs +++ b/helix-core/src/register.rs @@ -22,13 +22,17 @@ impl Register { self.name } - pub fn read(&self) -> &Vec { + pub fn read(&self) -> &[String] { &self.values } pub fn write(&mut self, values: Vec) { self.values = values; } + + pub fn push(&mut self, value: String) { + self.values.push(value); + } } /// Currently just wraps a `HashMap` of `Register`s @@ -42,11 +46,7 @@ impl Registers { self.inner.get(&name) } - pub fn get_mut(&mut self, name: char) -> Option<&mut Register> { - self.inner.get_mut(&name) - } - - pub fn get_or_insert(&mut self, name: char) -> &mut Register { + pub fn get_mut(&mut self, name: char) -> &mut Register { self.inner .entry(name) .or_insert_with(|| Register::new(name)) @@ -57,7 +57,7 @@ impl Registers { .insert(name, Register::new_with_values(name, values)); } - pub fn read(&self, name: char) -> Option<&Vec> { + pub fn read(&self, name: char) -> Option<&[String]> { self.get(name).map(|reg| reg.read()) } } diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 06dca5d56..c51453b01 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1126,7 +1126,7 @@ fn delete_selection(cx: &mut Context) { let reg_name = cx.selected_register.name(); let (view, doc) = current!(cx.editor); let registers = &mut cx.editor.registers; - let reg = registers.get_or_insert(reg_name); + let reg = registers.get_mut(reg_name); delete_selection_impl(reg, doc, view.id); doc.append_changes_to_history(view.id); @@ -1139,7 +1139,7 @@ fn change_selection(cx: &mut Context) { let reg_name = cx.selected_register.name(); let (view, doc) = current!(cx.editor); let registers = &mut cx.editor.registers; - let reg = registers.get_or_insert(reg_name); + let reg = registers.get_mut(reg_name); delete_selection_impl(reg, doc, view.id); enter_insert_mode(doc); } @@ -1920,6 +1920,7 @@ mod cmd { fn command_mode(cx: &mut Context) { let mut prompt = Prompt::new( ":".to_owned(), + Some(':'), |input: &str| { // we use .this over split_whitespace() because we care about empty segments let parts = input.split(' ').collect::>(); diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 288d3d2ec..9e71cfe73 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -36,6 +36,7 @@ pub fn regex_prompt( Prompt::new( prompt, + None, |_input: &str| Vec::new(), // this is fine because Vec::new() doesn't allocate move |cx: &mut crate::compositor::Context, input: &str, event: PromptEvent| { match event { diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 733be2fc6..0b67cd9c6 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -43,6 +43,7 @@ impl Picker { ) -> Self { let prompt = Prompt::new( "".to_string(), + None, |_pattern: &str| Vec::new(), |_editor: &mut Context, _pattern: &str, _event: PromptEvent| { // diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index 2df1e281f..57daef3a6 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -20,6 +20,8 @@ pub struct Prompt { cursor: usize, completion: Vec, selection: Option, + history_register: Option, + history_pos: Option, completion_fn: Box Vec>, callback_fn: Box, pub doc_fn: Box Option<&'static str>>, @@ -54,6 +56,7 @@ pub enum Movement { impl Prompt { pub fn new( prompt: String, + history_register: Option, mut completion_fn: impl FnMut(&str) -> Vec + 'static, callback_fn: impl FnMut(&mut Context, &str, PromptEvent) + 'static, ) -> Self { @@ -63,6 +66,8 @@ impl Prompt { cursor: 0, completion: completion_fn(""), selection: None, + history_register, + history_pos: None, completion_fn: Box::new(completion_fn), callback_fn: Box::new(callback_fn), doc_fn: Box::new(|_| None), @@ -226,6 +231,28 @@ impl Prompt { self.exit_selection(); } + pub fn change_history(&mut self, register: &[String], direction: CompletionDirection) { + if register.is_empty() { + return; + } + + let end = register.len().saturating_sub(1); + + let index = match direction { + CompletionDirection::Forward => self.history_pos.map_or(0, |i| i + 1), + CompletionDirection::Backward => { + self.history_pos.unwrap_or(register.len()).saturating_sub(1) + } + } + .min(end); + + self.line = register[index].clone(); + + self.history_pos = Some(index); + + self.move_end(); + } + pub fn change_completion_selection(&mut self, direction: CompletionDirection) { if self.completion.is_empty() { return; @@ -468,9 +495,40 @@ impl Component for Prompt { self.exit_selection(); } else { (self.callback_fn)(cx, &self.line, PromptEvent::Validate); + + if let Some(register) = self.history_register { + // store in history + let register = cx.editor.registers.get_mut(register); + register.push(self.line.clone()); + } return close_fn; } } + KeyEvent { + code: KeyCode::Char('p'), + modifiers: KeyModifiers::CONTROL, + } + | KeyEvent { + code: KeyCode::Up, .. + } => { + if let Some(register) = self.history_register { + let register = cx.editor.registers.get_mut(register); + self.change_history(register.read(), CompletionDirection::Backward); + } + } + KeyEvent { + code: KeyCode::Char('n'), + modifiers: KeyModifiers::CONTROL, + } + | KeyEvent { + code: KeyCode::Down, + .. + } => { + if let Some(register) = self.history_register { + let register = cx.editor.registers.get_mut(register); + self.change_history(register.read(), CompletionDirection::Forward); + } + } KeyEvent { code: KeyCode::Tab, .. } => self.change_completion_selection(CompletionDirection::Forward), From f7c85007972b18bf57c1ed23d40f42d56fe1f470 Mon Sep 17 00:00:00 2001 From: Ivan Tham Date: Sat, 24 Jul 2021 22:37:33 +0800 Subject: [PATCH 10/12] Fix append newline indent Fix #492 --- helix-core/src/indent.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/helix-core/src/indent.rs b/helix-core/src/indent.rs index 0ca05fb3c..5ae667695 100644 --- a/helix-core/src/indent.rs +++ b/helix-core/src/indent.rs @@ -47,7 +47,7 @@ fn calculate_indentation(query: &IndentQuery, node: Option, newline: bool) // NOTE: can't use contains() on query because of comparing Vec and &str // https://doc.rust-lang.org/std/vec/struct.Vec.html#method.contains - let mut increment: i32 = 0; + let mut increment: isize = 0; let mut node = match node { Some(node) => node, @@ -93,9 +93,7 @@ fn calculate_indentation(query: &IndentQuery, node: Option, newline: bool) node = parent; } - assert!(increment >= 0); - - increment as usize + increment.max(0) as usize } #[allow(dead_code)] From a630fb5d2022b264bcac34317bbadd0973bd57d4 Mon Sep 17 00:00:00 2001 From: gbaranski Date: Mon, 26 Jul 2021 16:04:28 +0200 Subject: [PATCH 11/12] fix: change primary cursor color in bogster theme --- runtime/themes/bogster.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/runtime/themes/bogster.toml b/runtime/themes/bogster.toml index fecbd605e..37b9adbfd 100644 --- a/runtime/themes/bogster.toml +++ b/runtime/themes/bogster.toml @@ -42,6 +42,7 @@ "ui.selection" = { bg = "#313f4e" } # "ui.cursor.match" # TODO might want to override this because dimmed is not widely supported +"ui.cursor.primary" = { fg = "#ABB2BF", modifiers = ["reversed"] } "ui.menu.selected" = { fg = "#e5ded6", bg = "#313f4e" } "warning" = "#dc7759" From 88d6f652390922b389667f469b6d308db569bdaf Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Mon, 26 Jul 2021 21:37:13 +0530 Subject: [PATCH 12/12] Allow multi key remappings in config file (#454) * Use tree like structure to store keymaps * Allow multi key keymaps in config file * Allow multi key keymaps in insert mode * Make keymap state self contained * Add keymap! macro for ergonomic declaration * Add descriptions for editor commands * Allow keymap! to take multiple keys * Restore infobox display * Fix keymap merging and add infobox titles * Fix and add tests for keymaps * Clean up comments and apply suggestions * Allow trailing commas in keymap! * Remove mode suffixes from keymaps * Preserve order of keys when showing infobox * Make command descriptions smaller * Strip infobox title prefix from items * Strip infobox title prefix from items --- helix-term/src/commands.rs | 502 ++++++++------------------ helix-term/src/config.rs | 34 +- helix-term/src/keymap.rs | 692 +++++++++++++++++++++++++----------- helix-term/src/ui/editor.rs | 76 +++- helix-term/src/ui/info.rs | 2 +- helix-view/src/editor.rs | 3 - helix-view/src/info.rs | 17 +- helix-view/src/input.rs | 12 +- 8 files changed, 730 insertions(+), 608 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index c51453b01..baac8f00d 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -16,7 +16,6 @@ use helix_core::{ use helix_view::{ document::{IndentStyle, Mode}, editor::Action, - info::Info, input::KeyEvent, keyboard::KeyCode, view::{View, PADDING}, @@ -39,7 +38,6 @@ use crate::{ use crate::job::{self, Job, Jobs}; use futures_util::{FutureExt, TryFutureExt}; -use std::collections::HashMap; use std::num::NonZeroUsize; use std::{fmt, future::Future}; @@ -48,7 +46,7 @@ use std::{ path::{Path, PathBuf}, }; -use once_cell::sync::{Lazy, OnceCell}; +use once_cell::sync::Lazy; use serde::de::{self, Deserialize, Deserializer}; pub struct Context<'a> { @@ -77,18 +75,6 @@ impl<'a> Context<'a> { self.on_next_key_callback = Some(Box::new(on_next_key_callback)); } - #[inline] - pub fn on_next_key_mode(&mut self, map: HashMap) { - let count = self.count; - self.on_next_key(move |cx, event| { - cx.count = count; - cx.editor.autoinfo = None; - if let Some(func) = map.get(&event) { - func(cx); - } - }); - } - #[inline] pub fn callback( &mut self, @@ -139,13 +125,21 @@ fn align_view(doc: &Document, view: &mut View, align: Align) { /// A command is composed of a static name, and a function that takes the current state plus a count, /// and does a side-effect on the state (usually by creating and applying a transaction). #[derive(Copy, Clone)] -pub struct Command(&'static str, fn(cx: &mut Context)); +pub struct Command { + name: &'static str, + fun: fn(cx: &mut Context), + doc: &'static str, +} macro_rules! commands { - ( $($name:ident),* ) => { + ( $($name:ident, $doc:literal),* ) => { $( #[allow(non_upper_case_globals)] - pub const $name: Self = Self(stringify!($name), $name); + pub const $name: Self = Self { + name: stringify!($name), + fun: $name, + doc: $doc + }; )* pub const COMMAND_LIST: &'static [Self] = &[ @@ -156,145 +150,159 @@ macro_rules! commands { impl Command { pub fn execute(&self, cx: &mut Context) { - (self.1)(cx); + (self.fun)(cx); } pub fn name(&self) -> &'static str { - self.0 + self.name + } + + pub fn doc(&self) -> &'static str { + self.doc } + #[rustfmt::skip] commands!( - move_char_left, - move_char_right, - move_line_up, - move_line_down, - move_next_word_start, - move_prev_word_start, - move_next_word_end, - move_next_long_word_start, - move_prev_long_word_start, - move_next_long_word_end, - extend_next_word_start, - extend_prev_word_start, - extend_next_word_end, - find_till_char, - find_next_char, - extend_till_char, - extend_next_char, - till_prev_char, - find_prev_char, - extend_till_prev_char, - extend_prev_char, - replace, - switch_case, - switch_to_uppercase, - switch_to_lowercase, - page_up, - page_down, - half_page_up, - half_page_down, - extend_char_left, - extend_char_right, - extend_line_up, - extend_line_down, - select_all, - select_regex, - split_selection, - split_selection_on_newline, - search, - search_next, - extend_search_next, - search_selection, - extend_line, - extend_to_line_bounds, - delete_selection, - change_selection, - collapse_selection, - flip_selections, - insert_mode, - append_mode, - command_mode, - file_picker, - code_action, - buffer_picker, - symbol_picker, - last_picker, - prepend_to_line, - append_to_line, - open_below, - open_above, - normal_mode, - goto_mode, - select_mode, - exit_select_mode, - goto_definition, - goto_type_definition, - goto_implementation, - goto_file_start, - goto_file_end, - goto_reference, - goto_first_diag, - goto_last_diag, - goto_next_diag, - goto_prev_diag, - goto_line_start, - goto_line_end, - goto_line_end_newline, - goto_first_nonwhitespace, - signature_help, - insert_tab, - insert_newline, - delete_char_backward, - delete_char_forward, - delete_word_backward, - undo, - redo, - yank, - yank_joined_to_clipboard, - yank_main_selection_to_clipboard, - replace_with_yanked, - replace_selections_with_clipboard, - paste_after, - paste_before, - paste_clipboard_after, - paste_clipboard_before, - indent, - unindent, - format_selections, - join_selections, - keep_selections, - keep_primary_selection, - completion, - hover, - toggle_comments, - expand_selection, - match_brackets, - jump_forward, - jump_backward, - window_mode, - rotate_view, - hsplit, - vsplit, - wclose, - select_register, - space_mode, - view_mode, - left_bracket_mode, - right_bracket_mode, - match_mode + move_char_left, "Move left", + move_char_right, "Move right", + move_line_up, "Move up", + move_line_down, "Move down", + extend_char_left, "Extend left", + extend_char_right, "Extend right", + extend_line_up, "Extend up", + extend_line_down, "Extend down", + move_next_word_start, "Move to beginning of next word", + move_prev_word_start, "Move to beginning of previous word", + move_next_word_end, "Move to end of next word", + move_next_long_word_start, "Move to beginning of next long word", + move_prev_long_word_start, "Move to beginning of previous long word", + move_next_long_word_end, "Move to end of next long word", + extend_next_word_start, "Extend to beginning of next word", + extend_prev_word_start, "Extend to beginning of previous word", + extend_next_word_end, "Extend to end of next word", + find_till_char, "Move till next occurance of char", + find_next_char, "Move to next occurance of char", + extend_till_char, "Extend till next occurance of char", + extend_next_char, "Extend to next occurance of char", + till_prev_char, "Move till previous occurance of char", + find_prev_char, "Move to previous occurance of char", + extend_till_prev_char, "Extend till previous occurance of char", + extend_prev_char, "Extend to previous occurance of char", + replace, "Replace with new char", + switch_case, "Switch (toggle) case", + switch_to_uppercase, "Switch to uppercase", + switch_to_lowercase, "Switch to lowercase", + page_up, "Move page up", + page_down, "Move page down", + half_page_up, "Move half page up", + half_page_down, "Move half page down", + select_all, "Select whole document", + select_regex, "Select all regex matches inside selections", + split_selection, "Split selection into subselections on regex matches", + split_selection_on_newline, "Split selection on newlines", + search, "Search for regex pattern", + search_next, "Select next search match", + extend_search_next, "Add next search match to selection", + search_selection, "Use current selection as search pattern", + extend_line, "Select current line, if already selected, extend to next line", + extend_to_line_bounds, "Extend selection to line bounds (line-wise selection)", + delete_selection, "Delete selection", + change_selection, "Change selection (delete and enter insert mode)", + collapse_selection, "Collapse selection onto a single cursor", + flip_selections, "Flip selection cursor and anchor", + insert_mode, "Insert before selection", + append_mode, "Insert after selection (append)", + command_mode, "Enter command mode", + file_picker, "Open file picker", + code_action, "Perform code action", + buffer_picker, "Open buffer picker", + symbol_picker, "Open symbol picker", + last_picker, "Open last picker", + prepend_to_line, "Insert at start of line", + append_to_line, "Insert at end of line", + open_below, "Open new line below selection", + open_above, "Open new line above selection", + normal_mode, "Enter normal mode", + select_mode, "Enter selection extend mode", + exit_select_mode, "Exit selection mode", + goto_definition, "Goto definition", + goto_type_definition, "Goto type definition", + goto_implementation, "Goto implementation", + goto_file_start, "Goto file start", + goto_file_end, "Goto file end", + goto_reference, "Goto references", + goto_window_top, "Goto window top", + goto_window_middle, "Goto window middle", + goto_window_bottom, "Goto window bottom", + goto_last_accessed_file, "Goto last accessed file", + goto_first_diag, "Goto first diagnostic", + goto_last_diag, "Goto last diagnostic", + goto_next_diag, "Goto next diagnostic", + goto_prev_diag, "Goto previous diagnostic", + goto_line_start, "Goto line start", + goto_line_end, "Goto line end", + // TODO: different description ? + goto_line_end_newline, "Goto line end", + goto_first_nonwhitespace, "Goto first non-blank in line", + signature_help, "Show signature help", + insert_tab, "Insert tab char", + insert_newline, "Insert newline char", + delete_char_backward, "Delete previous char", + delete_char_forward, "Delete next char", + delete_word_backward, "Delete previous word", + undo, "Undo change", + redo, "Redo change", + yank, "Yank selection", + yank_joined_to_clipboard, "Join and yank selections to clipboard", + yank_main_selection_to_clipboard, "Yank main selection to clipboard", + replace_with_yanked, "Replace with yanked text", + replace_selections_with_clipboard, "Replace selections by clipboard content", + paste_after, "Paste after selection", + paste_before, "Paste before selection", + paste_clipboard_after, "Paste clipboard after selections", + paste_clipboard_before, "Paste clipboard before selections", + indent, "Indent selection", + unindent, "Unindent selection", + format_selections, "Format selection", + join_selections, "Join lines inside selection", + keep_selections, "Keep selections matching regex", + keep_primary_selection, "Keep primary selection", + completion, "Invoke completion popup", + hover, "Show docs for item under cursor", + toggle_comments, "Comment/uncomment selections", + expand_selection, "Expand selection to parent syntax node", + jump_forward, "Jump forward on jumplist", + jump_backward, "Jump backward on jumplist", + rotate_view, "Goto next window", + hsplit, "Horizontal bottom split", + vsplit, "Vertical right split", + wclose, "Close window", + select_register, "Select register", + align_view_middle, "Align view middle", + align_view_top, "Align view top", + align_view_center, "Align view center", + align_view_bottom, "Align view bottom", + scroll_up, "Scroll view up", + scroll_down, "Scroll view down", + match_brackets, "Goto matching bracket", + surround_add, "Surround add", + surround_replace, "Surround replace", + surround_delete, "Surround delete", + select_textobject_around, "Select around object", + select_textobject_inner, "Select inside object" ); } impl fmt::Debug for Command { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let Command(name, _) = self; + let Command { name, .. } = self; f.debug_tuple("Command").field(name).finish() } } impl fmt::Display for Command { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let Command(name, _) = self; + let Command { name, .. } = self; f.write_str(name) } } @@ -306,7 +314,7 @@ impl std::str::FromStr for Command { Command::COMMAND_LIST .iter() .copied() - .find(|cmd| cmd.0 == s) + .find(|cmd| cmd.name == s) .ok_or_else(|| anyhow!("No command named '{}'", s)) } } @@ -2396,20 +2404,6 @@ fn exit_select_mode(cx: &mut Context) { doc_mut!(cx.editor).mode = Mode::Normal; } -fn goto_prehook(cx: &mut Context) -> bool { - if let Some(count) = cx.count { - push_jump(cx.editor); - - let (view, doc) = current!(cx.editor); - let line_idx = std::cmp::min(count.get() - 1, doc.text().len_lines().saturating_sub(2)); - let pos = doc.text().line_to_char(line_idx); - doc.set_selection(view.id, Selection::point(pos)); - true - } else { - false - } -} - fn goto_impl( editor: &mut Editor, compositor: &mut Compositor, @@ -3794,201 +3788,3 @@ fn surround_delete(cx: &mut Context) { } }) } - -/// Do nothing, just for modeinfo. -fn noop(_cx: &mut Context) -> bool { - false -} - -/// Generate modeinfo. -/// -/// If prehook returns true then it will stop the rest. -macro_rules! mode_info { - // TODO: reuse $mode for $stat - (@join $first:expr $(,$rest:expr)*) => { - concat!($first, $(", ", $rest),*) - }; - (@name #[doc = $name:literal] $(#[$rest:meta])*) => { - $name - }; - { - #[doc = $name:literal] $(#[$doc:meta])* $mode:ident, $stat:ident, - $(#[doc = $desc:literal] $($key:tt)|+ => $func:expr),+, - } => { - mode_info! { - #[doc = $name] - $(#[$doc])* - $mode, $stat, noop, - $( - #[doc = $desc] - $($key)|+ => $func - ),+, - } - }; - { - #[doc = $name:literal] $(#[$doc:meta])* $mode:ident, $stat:ident, $prehook:expr, - $(#[doc = $desc:literal] $($key:tt)|+ => $func:expr),+, - } => { - #[doc = $name] - $(#[$doc])* - #[doc = ""] - #[doc = ""] - $( - #[doc = ""] - )+ - #[doc = "
keydesc
"] - // TODO switch to this once we use rust 1.54 - // right now it will produce multiple rows - // #[doc = mode_info!(@join $($key),+)] - $( - #[doc = $key] - )+ - // <- - #[doc = ""] - #[doc = $desc] - #[doc = "
"] - pub fn $mode(cx: &mut Context) { - if $prehook(cx) { - return; - } - static $stat: OnceCell = OnceCell::new(); - cx.editor.autoinfo = Some($stat.get_or_init(|| Info::key( - $name.trim(), - vec![$((&[$($key.parse().unwrap()),+], $desc)),+], - ))); - use helix_core::hashmap; - // TODO: try and convert this to match later - let map = hashmap! { - $($($key.parse::().unwrap() => $func as for<'r, 's> fn(&'r mut Context<'s>)),+),* - }; - cx.on_next_key_mode(map); - } - }; -} - -mode_info! { - /// space mode - space_mode, SPACE_MODE, - /// resume last picker - "'" => last_picker, - /// file picker - "f" => file_picker, - /// buffer picker - "b" => buffer_picker, - /// symbol picker - "s" => symbol_picker, - /// window mode - "w" => window_mode, - /// yank joined to clipboard - "y" => yank_joined_to_clipboard, - /// yank main selection to clipboard - "Y" => yank_main_selection_to_clipboard, - /// paste system clipboard after selections - "p" => paste_clipboard_after, - /// paste system clipboard before selections - "P" => paste_clipboard_before, - /// replace selections with clipboard - "R" => replace_selections_with_clipboard, - /// perform code action - "a" => code_action, - /// keep primary selection - "space" => keep_primary_selection, -} - -mode_info! { - /// goto - /// - /// When specified with a count, it will go to that line without entering the mode. - goto_mode, GOTO_MODE, goto_prehook, - /// file start - "g" => goto_file_start, - /// file end - "e" => goto_file_end, - /// line start - "h" => goto_line_start, - /// line end - "l" => goto_line_end, - /// line first non blank - "s" => goto_first_nonwhitespace, - /// definition - "d" => goto_definition, - /// type references - "y" => goto_type_definition, - /// references - "r" => goto_reference, - /// implementation - "i" => goto_implementation, - /// window top - "t" => goto_window_top, - /// window middle - "m" => goto_window_middle, - /// window bottom - "b" => goto_window_bottom, - /// last accessed file - "a" => goto_last_accessed_file, -} - -mode_info! { - /// window - window_mode, WINDOW_MODE, - /// rotate - "w" | "C-w" => rotate_view, - /// horizontal split - "h" => hsplit, - /// vertical split - "v" => vsplit, - /// close - "q" => wclose, -} - -mode_info! { - /// match - match_mode, MATCH_MODE, - /// matching character - "m" => match_brackets, - /// surround add - "s" => surround_add, - /// surround replace - "r" => surround_replace, - /// surround delete - "d" => surround_delete, - /// around object - "a" => select_textobject_around, - /// inside object - "i" => select_textobject_inner, -} - -mode_info! { - /// select to previous - left_bracket_mode, LEFT_BRACKET_MODE, - /// previous diagnostic - "d" => goto_prev_diag, - /// diagnostic (first) - "D" => goto_first_diag, -} - -mode_info! { - /// select to next - right_bracket_mode, RIGHT_BRACKET_MODE, - /// diagnostic - "d" => goto_next_diag, - /// diagnostic (last) - "D" => goto_last_diag, -} - -mode_info! { - /// view - view_mode, VIEW_MODE, - /// align view top - "t" => align_view_top, - /// align view center - "z" | "c" => align_view_center, - /// align view bottom - "b" => align_view_bottom, - /// align view middle - "m" => align_view_middle, - /// scroll up - "k" => scroll_up, - /// scroll down - "j" => scroll_down, -} diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index b5ccbdfb0..f3f0ba531 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -2,9 +2,6 @@ use serde::Deserialize; use crate::keymap::Keymaps; -#[cfg(test)] -use crate::commands::Command; - #[derive(Debug, Default, Clone, PartialEq, Deserialize)] pub struct Config { pub theme: Option, @@ -22,12 +19,10 @@ pub struct LspConfig { #[test] fn parsing_keymaps_config_file() { + use crate::keymap; + use crate::keymap::Keymap; use helix_core::hashmap; - use helix_view::{ - document::Mode, - input::KeyEvent, - keyboard::{KeyCode, KeyModifiers}, - }; + use helix_view::document::Mode; let sample_keymaps = r#" [keys.insert] @@ -42,22 +37,13 @@ fn parsing_keymaps_config_file() { toml::from_str::(sample_keymaps).unwrap(), Config { keys: Keymaps(hashmap! { - Mode::Insert => hashmap! { - KeyEvent { - code: KeyCode::Char('y'), - modifiers: KeyModifiers::NONE, - } => Command::move_line_down, - KeyEvent { - code: KeyCode::Char('a'), - modifiers: KeyModifiers::SHIFT | KeyModifiers::CONTROL, - } => Command::delete_selection, - }, - Mode::Normal => hashmap! { - KeyEvent { - code: KeyCode::F(12), - modifiers: KeyModifiers::ALT, - } => Command::move_next_word_end, - }, + Mode::Insert => Keymap::new(keymap!({ "Insert mode" + "y" => move_line_down, + "S-C-a" => delete_selection, + })), + Mode::Normal => Keymap::new(keymap!({ "Normal mode" + "A-F12" => move_next_word_end, + })), }), ..Default::default() } diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 32994c37a..93cc53289 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -1,7 +1,7 @@ pub use crate::commands::Command; use crate::config::Config; use helix_core::hashmap; -use helix_view::{document::Mode, input::KeyEvent}; +use helix_view::{document::Mode, info::Info, input::KeyEvent}; use serde::Deserialize; use std::{ collections::HashMap, @@ -24,30 +24,276 @@ macro_rules! key { }; } -macro_rules! ctrl { - ($($ch:tt)*) => { - KeyEvent { - code: ::helix_view::keyboard::KeyCode::Char($($ch)*), - modifiers: ::helix_view::keyboard::KeyModifiers::CONTROL, +/// Macro for defining the root of a `Keymap` object. Example: +/// +/// ``` +/// # use helix_core::hashmap; +/// # use helix_term::keymap; +/// # use helix_term::keymap::Keymap; +/// let normal_mode = keymap!({ "Normal mode" +/// "i" => insert_mode, +/// "g" => { "Goto" +/// "g" => goto_file_start, +/// "e" => goto_file_end, +/// }, +/// "j" | "down" => move_line_down, +/// }); +/// let keymap = Keymap::new(normal_mode); +/// ``` +#[macro_export] +macro_rules! keymap { + (@trie $cmd:ident) => { + $crate::keymap::KeyTrie::Leaf($crate::commands::Command::$cmd) + }; + + (@trie + { $label:literal $($($key:literal)|+ => $value:tt,)+ } + ) => { + keymap!({ $label $($($key)|+ => $value,)+ }) + }; + + ( + { $label:literal $($($key:literal)|+ => $value:tt,)+ } + ) => { + // modified from the hashmap! macro + { + let _cap = hashmap!(@count $($($key),+),*); + let mut _map = ::std::collections::HashMap::with_capacity(_cap); + let mut _order = ::std::vec::Vec::with_capacity(_cap); + $( + $( + let _key = $key.parse::<::helix_view::input::KeyEvent>().unwrap(); + _map.insert( + _key, + keymap!(@trie $value) + ); + _order.push(_key); + )+ + )* + $crate::keymap::KeyTrie::Node($crate::keymap::KeyTrieNode::new($label, _map, _order)) } }; } -macro_rules! alt { - ($($ch:tt)*) => { - KeyEvent { - code: ::helix_view::keyboard::KeyCode::Char($($ch)*), - modifiers: ::helix_view::keyboard::KeyModifiers::ALT, +#[derive(Debug, Clone, Deserialize)] +pub struct KeyTrieNode { + /// A label for keys coming under this node, like "Goto mode" + #[serde(skip)] + name: String, + #[serde(flatten)] + map: HashMap, + #[serde(skip)] + order: Vec, +} + +impl KeyTrieNode { + pub fn new(name: &str, map: HashMap, order: Vec) -> Self { + Self { + name: name.to_string(), + map, + order, } - }; + } + + pub fn name(&self) -> &str { + &self.name + } + + /// Merge another Node in. Leaves and subnodes from the other node replace + /// corresponding keyevent in self, except when both other and self have + /// subnodes for same key. In that case the merge is recursive. + pub fn merge(&mut self, mut other: Self) { + for (key, trie) in std::mem::take(&mut other.map) { + if let Some(KeyTrie::Node(node)) = self.map.get_mut(&key) { + if let KeyTrie::Node(other_node) = trie { + node.merge(other_node); + continue; + } + } + self.map.insert(key, trie); + } + + for &key in self.map.keys() { + if !self.order.contains(&key) { + self.order.push(key); + } + } + } +} + +impl From for Info { + fn from(node: KeyTrieNode) -> Self { + let mut body: Vec<(&str, Vec)> = Vec::with_capacity(node.len()); + for (&key, trie) in node.iter() { + let desc = match trie { + KeyTrie::Leaf(cmd) => cmd.doc(), + KeyTrie::Node(n) => n.name(), + }; + match body.iter().position(|(d, _)| d == &desc) { + // FIXME: multiple keys are ordered randomly (use BTreeSet) + Some(pos) => body[pos].1.push(key), + None => body.push((desc, vec![key])), + } + } + body.sort_unstable_by_key(|(_, keys)| { + node.order.iter().position(|&k| k == keys[0]).unwrap() + }); + let prefix = format!("{} ", node.name()); + if body.iter().all(|(desc, _)| desc.starts_with(&prefix)) { + body = body + .into_iter() + .map(|(desc, keys)| (desc.strip_prefix(&prefix).unwrap(), keys)) + .collect(); + } + Info::key(node.name(), body) + } +} + +impl Default for KeyTrieNode { + fn default() -> Self { + Self::new("", HashMap::new(), Vec::new()) + } +} + +impl PartialEq for KeyTrieNode { + fn eq(&self, other: &Self) -> bool { + self.map == other.map + } +} + +impl Deref for KeyTrieNode { + type Target = HashMap; + + fn deref(&self) -> &Self::Target { + &self.map + } +} + +impl DerefMut for KeyTrieNode { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.map + } +} + +#[derive(Debug, Clone, PartialEq, Deserialize)] +#[serde(untagged)] +pub enum KeyTrie { + Leaf(Command), + Node(KeyTrieNode), +} + +impl KeyTrie { + pub fn node(&self) -> Option<&KeyTrieNode> { + match *self { + KeyTrie::Node(ref node) => Some(node), + KeyTrie::Leaf(_) => None, + } + } + + pub fn node_mut(&mut self) -> Option<&mut KeyTrieNode> { + match *self { + KeyTrie::Node(ref mut node) => Some(node), + KeyTrie::Leaf(_) => None, + } + } + + /// Merge another KeyTrie in, assuming that this KeyTrie and the other + /// are both Nodes. Panics otherwise. + pub fn merge_nodes(&mut self, mut other: Self) { + let node = std::mem::take(other.node_mut().unwrap()); + self.node_mut().unwrap().merge(node); + } + + pub fn search(&self, keys: &[KeyEvent]) -> Option<&KeyTrie> { + let mut trie = self; + for key in keys { + trie = match trie { + KeyTrie::Node(map) => map.get(key), + // leaf encountered while keys left to process + KeyTrie::Leaf(_) => None, + }? + } + Some(trie) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub enum KeymapResult { + /// Needs more keys to execute a command. Contains valid keys for next keystroke. + Pending(KeyTrieNode), + Matched(Command), + /// Key was not found in the root keymap + NotFound, + /// Key is invalid in combination with previous keys. Contains keys leading upto + /// and including current (invalid) key. + Cancelled(Vec), +} + +#[derive(Debug, Clone, PartialEq, Deserialize)] +pub struct Keymap { + /// Always a Node + #[serde(flatten)] + root: KeyTrie, + #[serde(skip)] + state: Vec, +} + +impl Keymap { + pub fn new(root: KeyTrie) -> Self { + Keymap { + root, + state: Vec::new(), + } + } + + pub fn root(&self) -> &KeyTrie { + &self.root + } + + /// Lookup `key` in the keymap to try and find a command to execute + pub fn get(&mut self, key: KeyEvent) -> KeymapResult { + let &first = self.state.get(0).unwrap_or(&key); + let trie = match self.root.search(&[first]) { + Some(&KeyTrie::Leaf(cmd)) => return KeymapResult::Matched(cmd), + None => return KeymapResult::NotFound, + Some(t) => t, + }; + self.state.push(key); + match trie.search(&self.state[1..]) { + Some(&KeyTrie::Node(ref map)) => KeymapResult::Pending(map.clone()), + Some(&KeyTrie::Leaf(command)) => { + self.state.clear(); + KeymapResult::Matched(command) + } + None => KeymapResult::Cancelled(self.state.drain(..).collect()), + } + } + + pub fn merge(&mut self, other: Self) { + self.root.merge_nodes(other.root); + } +} + +impl Deref for Keymap { + type Target = KeyTrieNode; + + fn deref(&self) -> &Self::Target { + &self.root.node().unwrap() + } +} + +impl Default for Keymap { + fn default() -> Self { + Self::new(KeyTrie::Node(KeyTrieNode::default())) + } } #[derive(Debug, Clone, PartialEq, Deserialize)] #[serde(transparent)] -pub struct Keymaps(pub HashMap>); +pub struct Keymaps(pub HashMap); impl Deref for Keymaps { - type Target = HashMap>; + type Target = HashMap; fn deref(&self) -> &Self::Target { &self.0 @@ -62,252 +308,298 @@ impl DerefMut for Keymaps { impl Default for Keymaps { fn default() -> Keymaps { - let normal = hashmap!( - key!('h') => Command::move_char_left, - key!('j') => Command::move_line_down, - key!('k') => Command::move_line_up, - key!('l') => Command::move_char_right, - - key!(Left) => Command::move_char_left, - key!(Down) => Command::move_line_down, - key!(Up) => Command::move_line_up, - key!(Right) => Command::move_char_right, - - key!('t') => Command::find_till_char, - key!('f') => Command::find_next_char, - key!('T') => Command::till_prev_char, - key!('F') => Command::find_prev_char, - // and matching set for select mode (extend) - // - key!('r') => Command::replace, - key!('R') => Command::replace_with_yanked, - - key!('~') => Command::switch_case, - alt!('`') => Command::switch_to_uppercase, - key!('`') => Command::switch_to_lowercase, - - key!(Home) => Command::goto_line_start, - key!(End) => Command::goto_line_end, - - key!('w') => Command::move_next_word_start, - key!('b') => Command::move_prev_word_start, - key!('e') => Command::move_next_word_end, - - key!('W') => Command::move_next_long_word_start, - key!('B') => Command::move_prev_long_word_start, - key!('E') => Command::move_next_long_word_end, - - key!('v') => Command::select_mode, - key!('g') => Command::goto_mode, - key!(':') => Command::command_mode, - - key!('i') => Command::insert_mode, - key!('I') => Command::prepend_to_line, - key!('a') => Command::append_mode, - key!('A') => Command::append_to_line, - key!('o') => Command::open_below, - key!('O') => Command::open_above, + let normal = keymap!({ "Normal mode" + "h" | "left" => move_char_left, + "j" | "down" => move_line_down, + "k" | "up" => move_line_up, + "l" | "right" => move_char_right, + + "t" => find_till_char, + "f" => find_next_char, + "T" => till_prev_char, + "F" => find_prev_char, + "r" => replace, + "R" => replace_with_yanked, + + "~" => switch_case, + "`" => switch_to_lowercase, + "A-`" => switch_to_uppercase, + + "home" => goto_line_start, + "end" => goto_line_end, + + "w" => move_next_word_start, + "b" => move_prev_word_start, + "e" => move_next_word_end, + + "W" => move_next_long_word_start, + "B" => move_prev_long_word_start, + "E" => move_next_long_word_end, + + "v" => select_mode, + "g" => { "Goto" + "g" => goto_file_start, + "e" => goto_file_end, + "h" => goto_line_start, + "l" => goto_line_end, + "s" => goto_first_nonwhitespace, + "d" => goto_definition, + "y" => goto_type_definition, + "r" => goto_reference, + "i" => goto_implementation, + "t" => goto_window_top, + "m" => goto_window_middle, + "b" => goto_window_bottom, + "a" => goto_last_accessed_file, + }, + ":" => command_mode, + + "i" => insert_mode, + "I" => prepend_to_line, + "a" => append_mode, + "A" => append_to_line, + "o" => open_below, + "O" => open_above, // [ ] equivalents too (add blank new line, no edit) - - key!('d') => Command::delete_selection, + "d" => delete_selection, // TODO: also delete without yanking - key!('c') => Command::change_selection, + "c" => change_selection, // TODO: also change delete without yanking - // key!('r') => Command::replace_with_char, - - key!('s') => Command::select_regex, - alt!('s') => Command::split_selection_on_newline, - key!('S') => Command::split_selection, - key!(';') => Command::collapse_selection, - alt!(';') => Command::flip_selections, - key!('%') => Command::select_all, - key!('x') => Command::extend_line, - key!('X') => Command::extend_to_line_bounds, + "s" => select_regex, + "A-s" => split_selection_on_newline, + "S" => split_selection, + ";" => collapse_selection, + "A-;" => flip_selections, + "%" => select_all, + "x" => extend_line, + "X" => extend_to_line_bounds, // crop_to_whole_line + "m" => { "Match" + "m" => match_brackets, + "s" => surround_add, + "r" => surround_replace, + "d" => surround_delete, + "a" => select_textobject_around, + "i" => select_textobject_inner, + }, + "[" => { "Left bracket" + "d" => goto_prev_diag, + "D" => goto_first_diag, + }, + "]" => { "Right bracket" + "d" => goto_next_diag, + "D" => goto_last_diag, + }, - key!('m') => Command::match_mode, - key!('[') => Command::left_bracket_mode, - key!(']') => Command::right_bracket_mode, - - key!('/') => Command::search, + "/" => search, // ? for search_reverse - key!('n') => Command::search_next, - key!('N') => Command::extend_search_next, + "n" => search_next, + "N" => extend_search_next, // N for search_prev - key!('*') => Command::search_selection, + "*" => search_selection, - key!('u') => Command::undo, - key!('U') => Command::redo, + "u" => undo, + "U" => redo, - key!('y') => Command::yank, + "y" => yank, // yank_all - key!('p') => Command::paste_after, + "p" => paste_after, // paste_all - key!('P') => Command::paste_before, + "P" => paste_before, - key!('>') => Command::indent, - key!('<') => Command::unindent, - key!('=') => Command::format_selections, - key!('J') => Command::join_selections, + ">" => indent, + "<" => unindent, + "=" => format_selections, + "J" => join_selections, // TODO: conflicts hover/doc - key!('K') => Command::keep_selections, + "K" => keep_selections, // TODO: and another method for inverse // TODO: clashes with space mode - key!(' ') => Command::keep_primary_selection, + "space" => keep_primary_selection, - // key!('q') => Command::record_macro, - // key!('Q') => Command::replay_macro, + // "q" => record_macro, + // "Q" => replay_macro, - // ~ / apostrophe => change case // & align selections // _ trim selections // C / altC = copy (repeat) selections on prev/next lines - key!(Esc) => Command::normal_mode, - key!(PageUp) => Command::page_up, - key!(PageDown) => Command::page_down, - ctrl!('b') => Command::page_up, - ctrl!('f') => Command::page_down, - ctrl!('u') => Command::half_page_up, - ctrl!('d') => Command::half_page_down, - - ctrl!('w') => Command::window_mode, + "esc" => normal_mode, + "C-b" | "pageup" => page_up, + "C-f" | "pagedown" => page_down, + "C-u" => half_page_up, + "C-d" => half_page_down, + + "C-w" => { "Window" + "C-w" | "w" => rotate_view, + "C-h" | "h" => hsplit, + "C-v" | "v" => vsplit, + "C-q" | "q" => wclose, + }, // move under c - ctrl!('c') => Command::toggle_comments, - key!('K') => Command::hover, + "C-c" => toggle_comments, + "K" => hover, // z family for save/restore/combine from/to sels from register - // supposedly ctrl!('i') but did not work - key!(Tab) => Command::jump_forward, - ctrl!('o') => Command::jump_backward, - // ctrl!('s') => Command::save_selection, - - key!(' ') => Command::space_mode, - key!('z') => Command::view_mode, + // supposedly "C-i" but did not work + "tab" => jump_forward, + "C-o" => jump_backward, + // "C-s" => save_selection, + + "space" => { "Space" + "f" => file_picker, + "b" => buffer_picker, + "s" => symbol_picker, + "a" => code_action, + "'" => last_picker, + "w" => { "Window" + "C-w" | "w" => rotate_view, + "C-h" | "h" => hsplit, + "C-v" | "v" => vsplit, + "C-q" | "q" => wclose, + }, + "y" => yank_joined_to_clipboard, + "Y" => yank_main_selection_to_clipboard, + "p" => paste_clipboard_after, + "P" => paste_clipboard_before, + "R" => replace_selections_with_clipboard, + "space" => keep_primary_selection, + }, + "z" => { "View" + "z" | "c" => align_view_center, + "t" => align_view_top, + "b" => align_view_bottom, + "m" => align_view_middle, + "k" => scroll_up, + "j" => scroll_down, + }, - key!('"') => Command::select_register, - ); + "\"" => select_register, + }); // TODO: decide whether we want normal mode to also be select mode (kakoune-like), or whether // we keep this separate select mode. More keys can fit into normal mode then, but it's weird // because some selection operations can now be done from normal mode, some from select mode. let mut select = normal.clone(); - select.extend( - hashmap!( - key!('h') => Command::extend_char_left, - key!('j') => Command::extend_line_down, - key!('k') => Command::extend_line_up, - key!('l') => Command::extend_char_right, - - key!(Left) => Command::extend_char_left, - key!(Down) => Command::extend_line_down, - key!(Up) => Command::extend_line_up, - key!(Right) => Command::extend_char_right, - - key!('w') => Command::extend_next_word_start, - key!('b') => Command::extend_prev_word_start, - key!('e') => Command::extend_next_word_end, - - key!('t') => Command::extend_till_char, - key!('f') => Command::extend_next_char, - - key!('T') => Command::extend_till_prev_char, - key!('F') => Command::extend_prev_char, - key!(Home) => Command::goto_line_start, - key!(End) => Command::goto_line_end, - key!(Esc) => Command::exit_select_mode, - ) - .into_iter(), - ); - + select.merge_nodes(keymap!({ "Select mode" + "h" | "left" => extend_char_left, + "j" | "down" => extend_line_down, + "k" | "up" => extend_line_up, + "l" | "right" => extend_char_right, + + "w" => extend_next_word_start, + "b" => extend_prev_word_start, + "e" => extend_next_word_end, + + "t" => extend_till_char, + "f" => extend_next_char, + "T" => extend_till_prev_char, + "F" => extend_prev_char, + + "home" => goto_line_start, + "end" => goto_line_end, + "esc" => exit_select_mode, + })); + let insert = keymap!({ "Insert mode" + "esc" => normal_mode, + + "backspace" => delete_char_backward, + "del" => delete_char_forward, + "ret" => insert_newline, + "tab" => insert_tab, + "C-w" => delete_word_backward, + + "left" => move_char_left, + "down" => move_line_down, + "up" => move_line_up, + "right" => move_char_right, + "pageup" => page_up, + "pagedown" => page_down, + "home" => goto_line_start, + "end" => goto_line_end_newline, + + "C-x" => completion, + }); Keymaps(hashmap!( - // as long as you cast the first item, rust is able to infer the other cases - // TODO: select could be normal mode with some bindings merged over - Mode::Normal => normal, - Mode::Select => select, - Mode::Insert => hashmap!( - key!(Esc) => Command::normal_mode as Command, - key!(Backspace) => Command::delete_char_backward, - key!(Delete) => Command::delete_char_forward, - key!(Enter) => Command::insert_newline, - key!(Tab) => Command::insert_tab, - key!(Left) => Command::move_char_left, - key!(Down) => Command::move_line_down, - key!(Up) => Command::move_line_up, - key!(Right) => Command::move_char_right, - key!(PageUp) => Command::page_up, - key!(PageDown) => Command::page_down, - key!(Home) => Command::goto_line_start, - key!(End) => Command::goto_line_end_newline, - ctrl!('x') => Command::completion, - ctrl!('w') => Command::delete_word_backward, - ), + Mode::Normal => Keymap::new(normal), + Mode::Select => Keymap::new(select), + Mode::Insert => Keymap::new(insert), )) } } -/// Merge default config keys with user overwritten keys for custom -/// user config. +/// Merge default config keys with user overwritten keys for custom user config. pub fn merge_keys(mut config: Config) -> Config { let mut delta = std::mem::take(&mut config.keys); for (mode, keys) in &mut *config.keys { - keys.extend(delta.remove(mode).unwrap_or_default()); + keys.merge(delta.remove(mode).unwrap_or_default()) } config } #[test] fn merge_partial_keys() { - use helix_view::keyboard::{KeyCode, KeyModifiers}; let config = Config { keys: Keymaps(hashmap! { - Mode::Normal => hashmap! { - KeyEvent { - code: KeyCode::Char('i'), - modifiers: KeyModifiers::NONE, - } => Command::normal_mode, - KeyEvent { // key that does not exist - code: KeyCode::Char('无'), - modifiers: KeyModifiers::NONE, - } => Command::insert_mode, - }, + Mode::Normal => Keymap::new( + keymap!({ "Normal mode" + "i" => normal_mode, + "无" => insert_mode, + "z" => jump_backward, + "g" => { "Merge into goto mode" + "$" => goto_line_end, + "g" => delete_char_forward, + }, + }) + ) }), ..Default::default() }; - let merged_config = merge_keys(config.clone()); + let mut merged_config = merge_keys(config.clone()); assert_ne!(config, merged_config); + + let keymap = merged_config.keys.0.get_mut(&Mode::Normal).unwrap(); assert_eq!( - *merged_config - .keys - .0 - .get(&Mode::Normal) - .unwrap() - .get(&KeyEvent { - code: KeyCode::Char('i'), - modifiers: KeyModifiers::NONE - }) - .unwrap(), - Command::normal_mode + keymap.get(key!('i')), + KeymapResult::Matched(Command::normal_mode), + "Leaf should replace leaf" ); assert_eq!( - *merged_config - .keys - .0 - .get(&Mode::Normal) - .unwrap() - .get(&KeyEvent { - code: KeyCode::Char('无'), - modifiers: KeyModifiers::NONE - }) - .unwrap(), - Command::insert_mode + keymap.get(key!('无')), + KeymapResult::Matched(Command::insert_mode), + "New leaf should be present in merged keymap" ); + // Assumes that z is a node in the default keymap + assert_eq!( + keymap.get(key!('z')), + KeymapResult::Matched(Command::jump_backward), + "Leaf should replace node" + ); + // Assumes that `g` is a node in default keymap + assert_eq!( + keymap.root().search(&[key!('g'), key!('$')]).unwrap(), + &KeyTrie::Leaf(Command::goto_line_end), + "Leaf should be present in merged subnode" + ); + // Assumes that `gg` is in default keymap + assert_eq!( + keymap.root().search(&[key!('g'), key!('g')]).unwrap(), + &KeyTrie::Leaf(Command::delete_char_forward), + "Leaf should replace old leaf in merged subnode" + ); + // Assumes that `ge` is in default keymap + assert_eq!( + keymap.root().search(&[key!('g'), key!('e')]).unwrap(), + &KeyTrie::Leaf(Command::goto_file_end), + "Old leaves in subnode should be present in merged node" + ); + assert!(merged_config.keys.0.get(&Mode::Normal).unwrap().len() > 1); assert!(merged_config.keys.0.get(&Mode::Insert).unwrap().len() > 0); } diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 9a2fbf571..78a54079d 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -2,7 +2,7 @@ use crate::{ commands, compositor::{Component, Context, EventResult}, key, - keymap::Keymaps, + keymap::{KeymapResult, Keymaps}, ui::{Completion, ProgressSpinners}, }; @@ -15,6 +15,7 @@ use helix_core::{ use helix_view::{ document::Mode, graphics::{CursorKind, Modifier, Rect, Style}, + info::Info, input::KeyEvent, keyboard::{KeyCode, KeyModifiers}, Document, Editor, Theme, View, @@ -30,6 +31,7 @@ pub struct EditorView { last_insert: (commands::Command, Vec), completion: Option, spinners: ProgressSpinners, + pub autoinfo: Option, } const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter @@ -48,6 +50,7 @@ impl EditorView { last_insert: (commands::Command::normal_mode, Vec::new()), completion: None, spinners: ProgressSpinners::default(), + autoinfo: None, } } @@ -559,19 +562,53 @@ impl EditorView { ); } - fn insert_mode(&self, cx: &mut commands::Context, event: KeyEvent) { - if let Some(command) = self.keymaps[&Mode::Insert].get(&event) { - command.execute(cx); - } else if let KeyEvent { - code: KeyCode::Char(ch), - .. - } = event - { - commands::insert::insert_char(cx, ch); + /// Handle events by looking them up in `self.keymaps`. Returns None + /// if event was handled (a command was executed or a subkeymap was + /// activated). Only KeymapResult::{NotFound, Cancelled} is returned + /// otherwise. + fn handle_keymap_event( + &mut self, + mode: Mode, + cxt: &mut commands::Context, + event: KeyEvent, + ) -> Option { + self.autoinfo = None; + match self.keymaps.get_mut(&mode).unwrap().get(event) { + KeymapResult::Matched(command) => command.execute(cxt), + KeymapResult::Pending(node) => self.autoinfo = Some(node.into()), + k @ KeymapResult::NotFound | k @ KeymapResult::Cancelled(_) => return Some(k), } + None } - fn command_mode(&self, mode: Mode, cxt: &mut commands::Context, event: KeyEvent) { + fn insert_mode(&mut self, cx: &mut commands::Context, event: KeyEvent) { + if let Some(keyresult) = self.handle_keymap_event(Mode::Insert, cx, event) { + match keyresult { + KeymapResult::NotFound => { + if let Some(ch) = event.char() { + commands::insert::insert_char(cx, ch) + } + } + KeymapResult::Cancelled(pending) => { + for ev in pending { + match ev.char() { + Some(ch) => commands::insert::insert_char(cx, ch), + None => { + if let KeymapResult::Matched(command) = + self.keymaps.get_mut(&Mode::Insert).unwrap().get(ev) + { + command.execute(cx); + } + } + } + } + } + _ => unreachable!(), + } + } + } + + fn command_mode(&mut self, mode: Mode, cxt: &mut commands::Context, event: KeyEvent) { match event { // count handling key!(i @ '0'..='9') => { @@ -584,8 +621,8 @@ impl EditorView { // first execute whatever put us into insert mode self.last_insert.0.execute(cxt); // then replay the inputs - for key in &self.last_insert.1 { - self.insert_mode(cxt, *key) + for &key in &self.last_insert.1.clone() { + self.insert_mode(cxt, key) } } _ => { @@ -598,9 +635,7 @@ impl EditorView { // set the register cxt.selected_register = cxt.editor.selected_register.take(); - if let Some(command) = self.keymaps[&mode].get(&event) { - command.execute(cxt); - } + self.handle_keymap_event(mode, cxt, event); } } } @@ -714,7 +749,11 @@ impl Component for EditorView { // how we entered insert mode is important, and we should track that so // we can repeat the side effect. - self.last_insert.0 = self.keymaps[&mode][&key]; + self.last_insert.0 = match self.keymaps.get_mut(&mode).unwrap().get(key) { + KeymapResult::Matched(command) => command, + // FIXME: insert mode can only be entered through single KeyCodes + _ => unimplemented!(), + }; self.last_insert.1.clear(); } (Mode::Insert, Mode::Normal) => { @@ -752,9 +791,8 @@ impl Component for EditorView { ); } - if let Some(info) = std::mem::take(&mut cx.editor.autoinfo) { + if let Some(ref info) = self.autoinfo { info.render(area, surface, cx); - cx.editor.autoinfo = Some(info); } // render status msg diff --git a/helix-term/src/ui/info.rs b/helix-term/src/ui/info.rs index e5f20562f..36b096db4 100644 --- a/helix-term/src/ui/info.rs +++ b/helix-term/src/ui/info.rs @@ -8,7 +8,7 @@ impl Component for Info { fn render(&self, viewport: Rect, surface: &mut Surface, cx: &mut Context) { let style = cx.editor.theme.get("ui.popup"); let block = Block::default() - .title(self.title) + .title(self.title.as_str()) .borders(Borders::ALL) .border_style(style); let Info { width, height, .. } = self; diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index cd9d0a925..7ff689df6 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -1,7 +1,6 @@ use crate::{ clipboard::{get_clipboard_provider, ClipboardProvider}, graphics::{CursorKind, Rect}, - info::Info, theme::{self, Theme}, tree::Tree, Document, DocumentId, RegisterSelection, View, ViewId, @@ -33,7 +32,6 @@ pub struct Editor { pub syn_loader: Arc, pub theme_loader: Arc, - pub autoinfo: Option<&'static Info>, pub status_msg: Option<(String, Severity)>, } @@ -67,7 +65,6 @@ impl Editor { theme_loader: themes, registers: Registers::default(), clipboard_provider: get_clipboard_provider(), - autoinfo: None, status_msg: None, } } diff --git a/helix-view/src/info.rs b/helix-view/src/info.rs index f3df50fe3..70e934cd6 100644 --- a/helix-view/src/info.rs +++ b/helix-view/src/info.rs @@ -5,9 +5,9 @@ use std::fmt::Write; #[derive(Debug)] /// Info box used in editor. Rendering logic will be in other crate. pub struct Info { - /// Title kept as static str for now. - pub title: &'static str, - /// Text body, should contains newline. + /// Title shown at top. + pub title: String, + /// Text body, should contain newlines. pub text: String, /// Body width. pub width: u16, @@ -16,17 +16,20 @@ pub struct Info { } impl Info { - pub fn key(title: &'static str, body: Vec<(&[KeyEvent], &'static str)>) -> Info { + // body is a BTreeMap instead of a HashMap because keymaps are represented + // with nested hashmaps with no ordering, and each invocation of infobox would + // show different orders of items + pub fn key(title: &str, body: Vec<(&str, Vec)>) -> Info { let (lpad, mpad, rpad) = (1, 2, 1); let keymaps_width: u16 = body .iter() - .map(|r| r.0.iter().map(|e| e.width() as u16 + 2).sum::() - 2) + .map(|r| r.1.iter().map(|e| e.width() as u16 + 2).sum::() - 2) .max() .unwrap(); let mut text = String::new(); let mut width = 0; let height = body.len() as u16; - for (keyevents, desc) in body { + for (desc, keyevents) in body { let keyevent = keyevents[0]; let mut left = keymaps_width - keyevent.width() as u16; for _ in 0..lpad { @@ -48,7 +51,7 @@ impl Info { writeln!(text, "{}", desc).ok(); } Info { - title, + title: title.to_string(), text, width, height, diff --git a/helix-view/src/input.rs b/helix-view/src/input.rs index 2847bb696..8d9ee6fb9 100644 --- a/helix-view/src/input.rs +++ b/helix-view/src/input.rs @@ -14,6 +14,16 @@ pub struct KeyEvent { pub modifiers: KeyModifiers, } +impl KeyEvent { + /// Get only the character involved in this event + pub fn char(&self) -> Option { + match self.code { + KeyCode::Char(ch) => Some(ch), + _ => None, + } + } +} + pub(crate) mod keys { pub(crate) const BACKSPACE: &str = "backspace"; pub(crate) const ENTER: &str = "ret"; @@ -168,7 +178,7 @@ impl std::str::FromStr for KeyEvent { keys::MINUS => KeyCode::Char('-'), keys::SEMICOLON => KeyCode::Char(';'), keys::PERCENT => KeyCode::Char('%'), - single if single.len() == 1 => KeyCode::Char(single.chars().next().unwrap()), + single if single.chars().count() == 1 => KeyCode::Char(single.chars().next().unwrap()), function if function.len() > 1 && function.starts_with('F') => { let function: String = function.chars().skip(1).collect(); let function = str::parse::(&function)?;