on master help sign in
Merge branch 'master' into great_line_ending_and_cursor_range_cleanup
f62ec6eNathan Vegdahl
Cargo.lock+12−12
@@ -422,9 +422,9 @@ dependencies = [
422
422
423[[package]]
423[[package]]
424name = "hermit-abi"
424name = "hermit-abi"
425−version = "0.1.18"
425+version = "0.1.19"
426source = "registry+https://github.com/rust-lang/crates.io-index"
426source = "registry+https://github.com/rust-lang/crates.io-index"
427−checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c"
427+checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
428dependencies = [
428dependencies = [
429 "libc",
429 "libc",
430]
430]
@@ -460,9 +460,9 @@ dependencies = [
460
460
461[[package]]
461[[package]]
462name = "instant"
462name = "instant"
463−version = "0.1.9"
463+version = "0.1.10"
464source = "registry+https://github.com/rust-lang/crates.io-index"
464source = "registry+https://github.com/rust-lang/crates.io-index"
465−checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec"
465+checksum = "bee0328b1209d157ef001c94dd85b4f8f64139adb0eac2659f4b08382b2f474d"
466dependencies = [
466dependencies = [
467 "cfg-if 1.0.0",
467 "cfg-if 1.0.0",
468]
468]
@@ -494,9 +494,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
494
494
495[[package]]
495[[package]]
496name = "libc"
496name = "libc"
497−version = "0.2.97"
497+version = "0.2.98"
498source = "registry+https://github.com/rust-lang/crates.io-index"
498source = "registry+https://github.com/rust-lang/crates.io-index"
499−checksum = "12b8adadd720df158f4d70dfe7ccc6adb0472d7c55ca83445f6a5ab3e36f8fb6"
499+checksum = "320cfe77175da3a483efed4bc0adc1968ca050b098ce4f2f1c13a56626128790"
500
500
501[[package]]
501[[package]]
502name = "libloading"
502name = "libloading"
@@ -662,9 +662,9 @@ checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
662
662
663[[package]]
663[[package]]
664name = "pin-project-lite"
664name = "pin-project-lite"
665−version = "0.2.6"
665+version = "0.2.7"
666source = "registry+https://github.com/rust-lang/crates.io-index"
666source = "registry+https://github.com/rust-lang/crates.io-index"
667−checksum = "dc0e1f259c92177c30a4c9d177246edd0a3568b25756a977d0632cf8fa37e905"
667+checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443"
668
668
669[[package]]
669[[package]]
670name = "pin-utils"
670name = "pin-utils"
@@ -994,9 +994,9 @@ dependencies = [
994
994
995[[package]]
995[[package]]
996name = "tinyvec"
996name = "tinyvec"
997−version = "1.2.0"
997+version = "1.3.0"
998source = "registry+https://github.com/rust-lang/crates.io-index"
998source = "registry+https://github.com/rust-lang/crates.io-index"
999−checksum = "5b5220f05bb7de7f3f53c7c065e1199b3172696fe2db9f9c4d8ad9b4ee74c342"
999+checksum = "4ac2e1d4bd0f75279cfd5a076e0d578bbf02c22b7c39e766c437dd49b3ec43e0"
1000dependencies = [
1000dependencies = [
1001 "tinyvec_macros",
1001 "tinyvec_macros",
1002]
1002]
@@ -1029,9 +1029,9 @@ dependencies = [
1029
1029
1030[[package]]
1030[[package]]
1031name = "tokio-macros"
1031name = "tokio-macros"
1032−version = "1.2.0"
1032+version = "1.3.0"
1033source = "registry+https://github.com/rust-lang/crates.io-index"
1033source = "registry+https://github.com/rust-lang/crates.io-index"
1034−checksum = "c49e3df43841dafb86046472506755d8501c5615673955f6aa17181125d13c37"
1034+checksum = "54473be61f4ebe4efd09cec9bd5d16fa51d70ea0192213d754d2d500457db110"
1035dependencies = [
1035dependencies = [
1036 "proc-macro2",
1036 "proc-macro2",
1037 "quote",
1037 "quote",
book/src/keymap.md+41−31
@@ -41,29 +41,29 @@
41
41
42### Changes
42### Changes
43
43
44−| Key | Description |
44+| Key | Description |
45−| ----- | ----------- |
45+| ----- | ----------- |
46−| `r` | Replace with a character |
46+| `r` | Replace with a character |
47−| `R` | Replace with yanked text |
47+| `R` | Replace with yanked text |
48−| `~` | Switch case of the selected text |
48+| `~` | Switch case of the selected text |
49−| `\`` | Set the selected text to upper case |
49+| `` ` `` | Set the selected text to lower case |
50−| `Alt-\`` | Set the selected text to lower case |
50+| `` Alt-` `` | Set the selected text to upper case |
51−| `i` | Insert before selection |
51+| `i` | Insert before selection |
52−| `a` | Insert after selection (append) |
52+| `a` | Insert after selection (append) |
53−| `I` | Insert at the start of the line |
53+| `I` | Insert at the start of the line |
54−| `A` | Insert at the end of the line |
54+| `A` | Insert at the end of the line |
55−| `o` | Open new line below selection |
55+| `o` | Open new line below selection |
56−| `o` | Open new line above selection |
56+| `o` | Open new line above selection |
57−| `u` | Undo change |
57+| `u` | Undo change |
58−| `U` | Redo change |
58+| `U` | Redo change |
59−| `y` | Yank selection |
59+| `y` | Yank selection |
60−| `p` | Paste after selection |
60+| `p` | Paste after selection |
61−| `P` | Paste before selection |
61+| `P` | Paste before selection |
62−| `>` | Indent selection |
62+| `>` | Indent selection |
63−| `<` | Unindent selection |
63+| `<` | Unindent selection |
64−| `=` | Format selection |
64+| `=` | Format selection |
65−| `d` | Delete selection |
65+| `d` | Delete selection |
66−| `c` | Change selection (delete and enter insert mode) |
66+| `c` | Change selection (delete and enter insert mode) |
67
67
68### Selection manipulation
68### Selection manipulation
69
69
@@ -78,11 +78,19 @@
78| `x` | Select current line, if already selected, extend to next line |
78| `x` | Select current line, if already selected, extend to next line |
79| `X` | Extend selection to line bounds (line-wise selection) |
79| `X` | Extend selection to line bounds (line-wise selection) |
80| | Expand selection to parent syntax node TODO: pick a key |
80| | Expand selection to parent syntax node TODO: pick a key |
81−| `J` | join lines inside selection |
81+| `J` | Join lines inside selection |
82−| `K` | keep selections matching the regex TODO: overlapped by hover help |
82+| `K` | Keep selections matching the regex TODO: overlapped by hover help |
83−| `Space` | keep only the primary selection TODO: overlapped by space mode |
83+| `Space` | Keep only the primary selection TODO: overlapped by space mode |
84| `Ctrl-c` | Comment/uncomment the selections |
84| `Ctrl-c` | Comment/uncomment the selections |
85
85
86+### Insert Mode
87+
88+| Key | Description |
89+| ----- | ----------- |
90+| `Escape` | Switch to normal mode |
91+| `Ctrl-x` | Autocomplete |
92+| `Ctrl-w` | Delete previous word |
93+
86### Search
94### Search
87
95
88> TODO: The search implementation isn't ideal yet -- we don't support searching
96> 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.
190| `f` | Open file picker |
198| `f` | Open file picker |
191| `b` | Open buffer picker |
199| `b` | Open buffer picker |
192| `s` | Open symbol picker (current document) |
200| `s` | Open symbol picker (current document) |
201+| `a` | Apply code action |
202+| `'` | Open last fuzzy picker |
193| `w` | Enter [window mode](#window-mode) |
203| `w` | Enter [window mode](#window-mode) |
194| `space` | Keep primary selection TODO: it's here because space mode replaced it |
204| `space` | Keep primary selection TODO: it's here because space mode replaced it |
195−| `p` | paste system clipboard after selections |
205+| `p` | Paste system clipboard after selections |
196−| `P` | paste system clipboard before selections |
206+| `P` | Paste system clipboard before selections |
197−| `y` | join and yank selections to clipboard |
207+| `y` | Join and yank selections to clipboard |
198−| `Y` | yank main selection to clipboard |
208+| `Y` | Yank main selection to clipboard |
199−| `R` | replace selections by clipboard contents |
209+| `R` | Replace selections by clipboard contents |
200
210
201# Picker
211# Picker
202
212
flake.lock+12−12
@@ -2,11 +2,11 @@
2 "nodes": {
2 "nodes": {
3 "devshell": {
3 "devshell": {
4 "locked": {
4 "locked": {
5− "lastModified": 1622711433,
5+ "lastModified": 1625086391,
6− "narHash": "sha256-rGjXz7FA7HImAT3TtoqwecByLO5yhVPSwPdaYPBFRQw=",
6+ "narHash": "sha256-IpNPv1v8s4L3CoxhwcgZIitGpcrnNgnj09X7TA0QV3k=",
7 "owner": "numtide",
7 "owner": "numtide",
8 "repo": "devshell",
8 "repo": "devshell",
9− "rev": "1f4fb67b662b65fa7cfe696fc003fcc1e8f7cc36",
9+ "rev": "4b5ac7cf7d9a1cc60b965bb51b59922f2210cbc7",
10 "type": "github"
10 "type": "github"
11 },
11 },
12 "original": {
12 "original": {
@@ -40,11 +40,11 @@
40 "rustOverlay": "rustOverlay"
40 "rustOverlay": "rustOverlay"
41 },
41 },
42 "locked": {
42 "locked": {
43− "lastModified": 1624244973,
43+ "lastModified": 1627106928,
44− "narHash": "sha256-h+b4CwPjyibgwMYAeBaT5qBnxI0fsmGf66k23FqEH5Y=",
44+ "narHash": "sha256-JaQE0BEk1G1eT539WbYyrA2re4YYL9xo7cB+ZiV4nNM=",
45 "owner": "yusdacra",
45 "owner": "yusdacra",
46 "repo": "nix-cargo-integration",
46 "repo": "nix-cargo-integration",
47− "rev": "00f5df6d8e7eeeac2764b7fa2c57e2e81f5d47cd",
47+ "rev": "e08af05413a2d53dadbd1a39976e1da0e5385970",
48 "type": "github"
48 "type": "github"
49 },
49 },
50 "original": {
50 "original": {
@@ -55,11 +55,11 @@
55 },
55 },
56 "nixpkgs": {
56 "nixpkgs": {
57 "locked": {
57 "locked": {
58− "lastModified": 1624024598,
58+ "lastModified": 1626852498,
59− "narHash": "sha256-X++38oH5MKEmPW4/2WdMaHQvwJzO8pJfbnzMD7DbG1E=",
59+ "narHash": "sha256-lOXUJvi0FJUXHTVSiC5qsMRtEUgqM4mGZpMESLuGhmo=",
60 "owner": "nixos",
60 "owner": "nixos",
61 "repo": "nixpkgs",
61 "repo": "nixpkgs",
62− "rev": "33d42ad7cf2769ce6364ed4e52afa8e9d1439d58",
62+ "rev": "16105403bdd843540cbef9c63fc0f16c1c6eaa70",
63 "type": "github"
63 "type": "github"
64 },
64 },
65 "original": {
65 "original": {
@@ -79,11 +79,11 @@
79 "rustOverlay": {
79 "rustOverlay": {
80 "flake": false,
80 "flake": false,
81 "locked": {
81 "locked": {
82− "lastModified": 1624242197,
82+ "lastModified": 1627092891,
83− "narHash": "sha256-J0+j4DYFaE0O0marb4QN/S1bUhpGwAjQ4O04kIYKcb8=",
83+ "narHash": "sha256-6nN+rfsP+SNpnL3UPbrcwZe4qfh9/NH0LWtXhn9w/a4=",
84 "owner": "oxalica",
84 "owner": "oxalica",
85 "repo": "rust-overlay",
85 "repo": "rust-overlay",
86− "rev": "df5d330f34b64194d64dcbafb91e82e01a89a229",
86+ "rev": "939f2cf1aebc86bc3e9544645b495cd05995524a",
87 "type": "github"
87 "type": "github"
88 },
88 },
89 "original": {
89 "original": {
flake.nix+40−15
@@ -28,27 +28,53 @@
28 preConfigure = "ln -s ${common.root}/{languages.toml,theme.toml} ..";
28 preConfigure = "ln -s ${common.root}/{languages.toml,theme.toml} ..";
29 buildInputs = (prev.buildInputs or [ ]) ++ [ common.cCompiler.cc.lib ];
29 buildInputs = (prev.buildInputs or [ ]) ++ [ common.cCompiler.cc.lib ];
30 };
30 };
31− # link runtime since helix-core expects it because of embed_runtime feature
32− helix-core = _: { preConfigure = "ln -s ${common.root + "/runtime"} ../runtime"; };
33 # link languages and theme toml files since helix-view expects them
31 # link languages and theme toml files since helix-view expects them
34 helix-view = _: { preConfigure = "ln -s ${common.root}/{languages.toml,theme.toml} .."; };
32 helix-view = _: { preConfigure = "ln -s ${common.root}/{languages.toml,theme.toml} .."; };
35− helix-syntax = prev:
33+ helix-syntax = prev: {
34+ src =
36− let
35+ let
37− helix = common.pkgs.fetchgit {
36+ pkgs = common.pkgs;
37+ helix = pkgs.fetchgit {
38− url = "https://github.com/helix-editor/helix.git";
38+ url = "https://github.com/helix-editor/helix.git";
39− rev = "9fd17d4ff5b81211317da1a28d2b30442a512ffc";
39+ rev = "d4bd5b37669708361a0a6cd2917464b010e6b7f5";
40− fetchSubmodules = true;
40+ fetchSubmodules = true;
41− sha256 = "sha256-y652sn/tCc1XoKr3YxDZv6bS2Cmr6+9K/wzzNAMFZJw=";
41+ sha256 = "sha256-KayR7K7UC0mT6EjHsZsCYY9IVDJzft63fGpPKGSY8nQ=";
42− };
42+ };
43− in
43+ in
44− {
45− src = common.pkgs.runCommand prev.src.name { } ''
44+ pkgs.runCommand prev.src.name { } ''
46 mkdir -p $out
45 mkdir -p $out
47 ln -s ${prev.src}/* $out
46 ln -s ${prev.src}/* $out
48 ln -sf ${helix}/helix-syntax/languages $out
47 ln -sf ${helix}/helix-syntax/languages $out
49 '';
48 '';
49+ preConfigure = "mkdir -p ../runtime/grammars";
50+ postInstall = "cp -r ../runtime $out/runtime";
50− };
51+ };
51 };
52 };
53+ mainBuild = common: prev:
54+ let
55+ inherit (common) pkgs lib;
56+ helixSyntax = lib.buildCrate {
57+ root = self;
58+ memberName = "helix-syntax";
59+ defaultCrateOverrides = {
60+ helix-syntax = common.crateOverrides.helix-syntax;
61+ };
62+ release = false;
63+ };
64+ runtimeDir = pkgs.runCommand "helix-runtime" { } ''
65+ mkdir -p $out
66+ ln -s ${common.root}/runtime/* $out
67+ ln -sf ${helixSyntax}/runtime/grammars $out
68+ '';
69+ in
70+ lib.optionalAttrs (common.memberName == "helix-term") {
71+ nativeBuildInputs = [ pkgs.makeWrapper ];
72+ postFixup = ''
73+ if [ -f "$out/bin/hx" ]; then
74+ wrapProgram "$out/bin/hx" --set HELIX_RUNTIME "${runtimeDir}"
75+ fi
76+ '';
77+ };
52 shell = common: prev: {
78 shell = common: prev: {
53 packages = prev.packages ++ (with common.pkgs; [ lld_10 lldb cargo-tarpaulin ]);
79 packages = prev.packages ++ (with common.pkgs; [ lld_10 lldb cargo-tarpaulin ]);
54 env = prev.env ++ [
80 env = prev.env ++ [
@@ -57,7 +83,6 @@
57 { name = "RUSTFLAGS"; value = "-C link-arg=-fuse-ld=lld -C target-cpu=native"; }
83 { name = "RUSTFLAGS"; value = "-C link-arg=-fuse-ld=lld -C target-cpu=native"; }
58 ];
84 ];
59 };
85 };
60− build = _: prev: { rootFeatures = prev.rootFeatures ++ [ "embed_runtime" ]; };
61 };
86 };
62 };
87 };
63}
88}
helix-core/src/comment.rs+58−30
@@ -1,17 +1,27 @@
1use crate::{
1use crate::{
2 find_first_non_whitespace_char, Change, Rope, RopeSlice, Selection, Tendril, Transaction,
2 find_first_non_whitespace_char, Change, Rope, RopeSlice, Selection, Tendril, Transaction,
3};
3};
4−use core::ops::Range;
5use std::borrow::Cow;
4use std::borrow::Cow;
6
5
6+/// Given text, a comment token, and a set of line indices, returns the following:
7+/// - Whether the given lines should be considered commented
8+/// - If any of the lines are uncommented, all lines are considered as such.
9+/// - The lines to change for toggling comments
10+/// - This is all provided lines excluding blanks lines.
11+/// - The column of the comment tokens
12+/// - Column of existing tokens, if the lines are commented; column to place tokens at otherwise.
13+/// - The margin to the right of the comment tokens
14+/// - Defaults to `1`. If any existing comment token is not followed by a space, changes to `0`.
7fn find_line_comment(
15fn find_line_comment(
8 token: &str,
16 token: &str,
9 text: RopeSlice,
17 text: RopeSlice,
10− lines: Range<usize>,
18+ lines: impl IntoIterator<Item = usize>,
11−) -> (bool, Vec<usize>, usize) {
19+) -> (bool, Vec<usize>, usize, usize) {
12 let mut commented = true;
20 let mut commented = true;
13− let mut skipped = Vec::new();
21+ let mut to_change = Vec::new();
14 let mut min = usize::MAX; // minimum col for find_first_non_whitespace_char
22 let mut min = usize::MAX; // minimum col for find_first_non_whitespace_char
23+ let mut margin = 1;
24+ let token_len = token.chars().count();
15 for line in lines {
25 for line in lines {
16 let line_slice = text.line(line);
26 let line_slice = text.line(line);
17 if let Some(pos) = find_first_non_whitespace_char(line_slice) {
27 if let Some(pos) = find_first_non_whitespace_char(line_slice) {
@@ -29,46 +39,55 @@ fn find_line_comment(
29 // considered uncommented.
39 // considered uncommented.
30 commented = false;
40 commented = false;
31 }
41 }
32− } else {
42+
33− // blank line
43+ // determine margin of 0 or 1 for uncommenting; if any comment token is not followed by a space,
44+ // a margin of 0 is used for all lines.
45+ if matches!(line_slice.get_char(pos + token_len), Some(c) if c != ' ') {
46+ margin = 0;
47+ }
48+
49+ // blank lines don't get pushed.
34− skipped.push(line);
50+ to_change.push(line);
35 }
51 }
36 }
52 }
37− (commented, skipped, min)
53+ (commented, to_change, min, margin)
38}
54}
39
55
40#[must_use]
56#[must_use]
41pub fn toggle_line_comments(doc: &Rope, selection: &Selection, token: Option<&str>) -> Transaction {
57pub fn toggle_line_comments(doc: &Rope, selection: &Selection, token: Option<&str>) -> Transaction {
42 let text = doc.slice(..);
58 let text = doc.slice(..);
43− let mut changes: Vec<Change> = Vec::new();
44
59
45 let token = token.unwrap_or("//");
60 let token = token.unwrap_or("//");
46 let comment = Tendril::from(format!("{} ", token));
61 let comment = Tendril::from(format!("{} ", token));
47
62
63+ let mut lines: Vec<usize> = Vec::new();
64+
65+ let mut min_next_line = 0;
48 for selection in selection {
66 for selection in selection {
49 let (start, end) = selection.line_range(text);
67 let (start, end) = selection.line_range(text);
50− let lines = start..end + 1;
68+ let start = start.max(min_next_line).min(text.len_lines());
51− let (commented, skipped, min) = find_line_comment(&token, text, lines.clone());
69+ let end = (end + 1).min(text.len_lines());
52
70
53− changes.reserve((end - start).saturating_sub(skipped.len()));
71+ lines.extend(start..end);
72+ min_next_line = end + 1;
73+ }
54
74
55− for line in lines {
75+ let (commented, to_change, min, margin) = find_line_comment(&token, text, lines);
56− if skipped.contains(&line) {
57− continue;
58− }
59
76
60− let pos = text.line_to_char(line) + min;
77+ let mut changes: Vec<Change> = Vec::with_capacity(to_change.len());
61
78
79+ for line in to_change {
80+ let pos = text.line_to_char(line) + min;
81+
62− if !commented {
82+ if !commented {
63− // comment line
83+ // comment line
64− changes.push((pos, pos, Some(comment.clone())))
84+ changes.push((pos, pos, Some(comment.clone())));
65− } else {
85+ } else {
66− // uncomment line
86+ // uncomment line
67− let margin = 1; // TODO: margin is hardcoded 1 but could easily be 0
68− changes.push((pos, pos + token.len() + margin, None))
87+ changes.push((pos, pos + token.len() + margin, None));
69− }
70 }
88 }
71 }
89 }
90+
72 Transaction::change(doc, changes.into_iter())
91 Transaction::change(doc, changes.into_iter())
73}
92}
74
93
@@ -90,23 +109,32 @@ mod test {
90 let text = state.doc.slice(..);
109 let text = state.doc.slice(..);
91
110
92 let res = find_line_comment("//", text, 0..3);
111 let res = find_line_comment("//", text, 0..3);
93− // (commented = true, skipped = [line 1], min = col 2)
112+ // (commented = true, to_change = [line 0, line 2], min = col 2, margin = 1)
94− assert_eq!(res, (false, vec![1], 2));
113+ assert_eq!(res, (false, vec![0, 2], 2, 1));
95
114
96 // comment
115 // comment
97 let transaction = toggle_line_comments(&state.doc, &state.selection, None);
116 let transaction = toggle_line_comments(&state.doc, &state.selection, None);
98 transaction.apply(&mut state.doc);
117 transaction.apply(&mut state.doc);
99− state.selection = state.selection.clone().map(transaction.changes());
118+ state.selection = state.selection.map(transaction.changes());
100
119
101 assert_eq!(state.doc, " // 1\n\n // 2\n // 3");
120 assert_eq!(state.doc, " // 1\n\n // 2\n // 3");
102
121
103 // uncomment
122 // uncomment
104 let transaction = toggle_line_comments(&state.doc, &state.selection, None);
123 let transaction = toggle_line_comments(&state.doc, &state.selection, None);
105 transaction.apply(&mut state.doc);
124 transaction.apply(&mut state.doc);
106− state.selection = state.selection.clone().map(transaction.changes());
125+ state.selection = state.selection.map(transaction.changes());
126+ assert_eq!(state.doc, " 1\n\n 2\n 3");
127+
128+ // 0 margin comments
129+ state.doc = Rope::from(" //1\n\n //2\n //3");
130+ // reset the selection.
131+ state.selection = Selection::single(0, state.doc.len_chars() - 1);
132+
133+ let transaction = toggle_line_comments(&state.doc, &state.selection, None);
134+ transaction.apply(&mut state.doc);
135+ state.selection = state.selection.map(transaction.changes());
107 assert_eq!(state.doc, " 1\n\n 2\n 3");
136 assert_eq!(state.doc, " 1\n\n 2\n 3");
108
137
109− // TODO: account for no margin after comment
110 // TODO: account for uncommenting with uneven comment indentation
138 // TODO: account for uncommenting with uneven comment indentation
111 }
139 }
112}
140}
helix-core/src/indent.rs+2−4
@@ -47,7 +47,7 @@ fn calculate_indentation(query: &IndentQuery, node: Option<Node>, newline: bool)
47 // NOTE: can't use contains() on query because of comparing Vec<String> and &str
47 // NOTE: can't use contains() on query because of comparing Vec<String> and &str
48 // https://doc.rust-lang.org/std/vec/struct.Vec.html#method.contains
48 // https://doc.rust-lang.org/std/vec/struct.Vec.html#method.contains
49
49
50− let mut increment: i32 = 0;
50+ let mut increment: isize = 0;
51
51
52 let mut node = match node {
52 let mut node = match node {
53 Some(node) => node,
53 Some(node) => node,
@@ -93,9 +93,7 @@ fn calculate_indentation(query: &IndentQuery, node: Option<Node>, newline: bool)
93 node = parent;
93 node = parent;
94 }
94 }
95
95
96− assert!(increment >= 0);
96+ increment.max(0) as usize
97−
98− increment as usize
99}
97}
100
98
101#[allow(dead_code)]
99#[allow(dead_code)]
helix-core/src/register.rs+7−7
@@ -22,13 +22,17 @@ impl Register {
22 self.name
22 self.name
23 }
23 }
24
24
25− pub fn read(&self) -> &Vec<String> {
25+ pub fn read(&self) -> &[String] {
26 &self.values
26 &self.values
27 }
27 }
28
28
29 pub fn write(&mut self, values: Vec<String>) {
29 pub fn write(&mut self, values: Vec<String>) {
30 self.values = values;
30 self.values = values;
31 }
31 }
32+
33+ pub fn push(&mut self, value: String) {
34+ self.values.push(value);
35+ }
32}
36}
33
37
34/// Currently just wraps a `HashMap` of `Register`s
38/// Currently just wraps a `HashMap` of `Register`s
@@ -42,11 +46,7 @@ impl Registers {
42 self.inner.get(&name)
46 self.inner.get(&name)
43 }
47 }
44
48
45− pub fn get_mut(&mut self, name: char) -> Option<&mut Register> {
49+ pub fn get_mut(&mut self, name: char) -> &mut Register {
46− self.inner.get_mut(&name)
47− }
48−
49− pub fn get_or_insert(&mut self, name: char) -> &mut Register {
50 self.inner
50 self.inner
51 .entry(name)
51 .entry(name)
52 .or_insert_with(|| Register::new(name))
52 .or_insert_with(|| Register::new(name))
@@ -57,7 +57,7 @@ impl Registers {
57 .insert(name, Register::new_with_values(name, values));
57 .insert(name, Register::new_with_values(name, values));
58 }
58 }
59
59
60− pub fn read(&self, name: char) -> Option<&Vec<String>> {
60+ pub fn read(&self, name: char) -> Option<&[String]> {
61 self.get(name).map(|reg| reg.read())
61 self.get(name).map(|reg| reg.read())
62 }
62 }
63}
63}
helix-lsp/src/client.rs+47
@@ -247,6 +247,26 @@ impl Client {
247 content_format: Some(vec![lsp::MarkupKind::Markdown]),
247 content_format: Some(vec![lsp::MarkupKind::Markdown]),
248 ..Default::default()
248 ..Default::default()
249 }),
249 }),
250+ code_action: Some(lsp::CodeActionClientCapabilities {
251+ code_action_literal_support: Some(lsp::CodeActionLiteralSupport {
252+ code_action_kind: lsp::CodeActionKindLiteralSupport {
253+ value_set: [
254+ lsp::CodeActionKind::EMPTY,
255+ lsp::CodeActionKind::QUICKFIX,
256+ lsp::CodeActionKind::REFACTOR,
257+ lsp::CodeActionKind::REFACTOR_EXTRACT,
258+ lsp::CodeActionKind::REFACTOR_INLINE,
259+ lsp::CodeActionKind::REFACTOR_REWRITE,
260+ lsp::CodeActionKind::SOURCE,
261+ lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
262+ ]
263+ .iter()
264+ .map(|kind| kind.as_str().to_string())
265+ .collect(),
266+ },
267+ }),
268+ ..Default::default()
269+ }),
250 ..Default::default()
270 ..Default::default()
251 }),
271 }),
252 window: Some(lsp::WindowClientCapabilities {
272 window: Some(lsp::WindowClientCapabilities {
@@ -713,4 +733,31 @@ impl Client {
713
733
714 self.call::<lsp::request::DocumentSymbolRequest>(params)
734 self.call::<lsp::request::DocumentSymbolRequest>(params)
715 }
735 }
736+
737+ // empty string to get all symbols
738+ pub fn workspace_symbols(&self, query: String) -> impl Future<Output = Result<Value>> {
739+ let params = lsp::WorkspaceSymbolParams {
740+ query,
741+ work_done_progress_params: lsp::WorkDoneProgressParams::default(),
742+ partial_result_params: lsp::PartialResultParams::default(),
743+ };
744+
745+ self.call::<lsp::request::WorkspaceSymbol>(params)
746+ }
747+
748+ pub fn code_actions(
749+ &self,
750+ text_document: lsp::TextDocumentIdentifier,
751+ range: lsp::Range,
752+ ) -> impl Future<Output = Result<Value>> {
753+ let params = lsp::CodeActionParams {
754+ text_document,
755+ range,
756+ context: lsp::CodeActionContext::default(),
757+ work_done_progress_params: lsp::WorkDoneProgressParams::default(),
758+ partial_result_params: lsp::PartialResultParams::default(),
759+ };
760+
761+ self.call::<lsp::request::CodeActionRequest>(params)
762+ }
716}
763}
helix-term/src/commands.rs+258−352
@@ -13,7 +13,6 @@ use helix_core::{
13use helix_view::{
13use helix_view::{
14 document::{IndentStyle, Mode},
14 document::{IndentStyle, Mode},
15 editor::Action,
15 editor::Action,
16− info::Info,
17 input::KeyEvent,
16 input::KeyEvent,
18 keyboard::KeyCode,
17 keyboard::KeyCode,
19 view::{View, PADDING},
18 view::{View, PADDING},
@@ -36,7 +35,6 @@ use crate::{
36
35
37use crate::job::{self, Job, Jobs};
36use crate::job::{self, Job, Jobs};
38use futures_util::{FutureExt, TryFutureExt};
37use futures_util::{FutureExt, TryFutureExt};
39−use std::collections::HashMap;
40use std::num::NonZeroUsize;
38use std::num::NonZeroUsize;
41use std::{fmt, future::Future};
39use std::{fmt, future::Future};
42
40
@@ -45,7 +43,7 @@ use std::{
45 path::{Path, PathBuf},
43 path::{Path, PathBuf},
46};
44};
47
45
48−use once_cell::sync::{Lazy, OnceCell};
46+use once_cell::sync::Lazy;
49use serde::de::{self, Deserialize, Deserializer};
47use serde::de::{self, Deserialize, Deserializer};
50
48
51pub struct Context<'a> {
49pub struct Context<'a> {
@@ -74,18 +72,6 @@ impl<'a> Context<'a> {
74 self.on_next_key_callback = Some(Box::new(on_next_key_callback));
72 self.on_next_key_callback = Some(Box::new(on_next_key_callback));
75 }
73 }
76
74
77− #[inline]
78− pub fn on_next_key_mode(&mut self, map: HashMap<KeyEvent, fn(&mut Context)>) {
79− let count = self.count;
80− self.on_next_key(move |cx, event| {
81− cx.count = count;
82− cx.editor.autoinfo = None;
83− if let Some(func) = map.get(&event) {
84− func(cx);
85− }
86− });
87− }
88−
89 #[inline]
75 #[inline]
90 pub fn callback<T, F>(
76 pub fn callback<T, F>(
91 &mut self,
77 &mut self,
@@ -139,13 +125,21 @@ fn align_view(doc: &Document, view: &mut View, align: Align) {
139/// A command is composed of a static name, and a function that takes the current state plus a count,
125/// A command is composed of a static name, and a function that takes the current state plus a count,
140/// and does a side-effect on the state (usually by creating and applying a transaction).
126/// and does a side-effect on the state (usually by creating and applying a transaction).
141#[derive(Copy, Clone)]
127#[derive(Copy, Clone)]
142−pub struct Command(&'static str, fn(cx: &mut Context));
128+pub struct Command {
129+ name: &'static str,
130+ fun: fn(cx: &mut Context),
131+ doc: &'static str,
132+}
143
133
144macro_rules! commands {
134macro_rules! commands {
145− ( $($name:ident),* ) => {
135+ ( $($name:ident, $doc:literal),* ) => {
146 $(
136 $(
147 #[allow(non_upper_case_globals)]
137 #[allow(non_upper_case_globals)]
148− pub const $name: Self = Self(stringify!($name), $name);
138+ pub const $name: Self = Self {
139+ name: stringify!($name),
140+ fun: $name,
141+ doc: $doc
142+ };
149 )*
143 )*
150
144
151 pub const COMMAND_LIST: &'static [Self] = &[
145 pub const COMMAND_LIST: &'static [Self] = &[
@@ -156,144 +150,159 @@ macro_rules! commands {
156
150
157impl Command {
151impl Command {
158 pub fn execute(&self, cx: &mut Context) {
152 pub fn execute(&self, cx: &mut Context) {
159− (self.1)(cx);
153+ (self.fun)(cx);
160 }
154 }
161
155
162 pub fn name(&self) -> &'static str {
156 pub fn name(&self) -> &'static str {
163− self.0
157+ self.name
158+ }
159+
160+ pub fn doc(&self) -> &'static str {
161+ self.doc
164 }
162 }
165
163
164+ #[rustfmt::skip]
166 commands!(
165 commands!(
167− move_char_left,
166+ move_char_left, "Move left",
168− move_char_right,
167+ move_char_right, "Move right",
169− move_line_up,
168+ move_line_up, "Move up",
170− move_line_down,
169+ move_line_down, "Move down",
170+ extend_char_left, "Extend left",
171+ extend_char_right, "Extend right",
172+ extend_line_up, "Extend up",
173+ extend_line_down, "Extend down",
171− move_next_word_start,
174+ move_next_word_start, "Move to beginning of next word",
172− move_prev_word_start,
175+ move_prev_word_start, "Move to beginning of previous word",
173− move_next_word_end,
176+ move_next_word_end, "Move to end of next word",
174− move_next_long_word_start,
177+ move_next_long_word_start, "Move to beginning of next long word",
175− move_prev_long_word_start,
178+ move_prev_long_word_start, "Move to beginning of previous long word",
176− move_next_long_word_end,
179+ move_next_long_word_end, "Move to end of next long word",
177− extend_next_word_start,
180+ extend_next_word_start, "Extend to beginning of next word",
178− extend_prev_word_start,
181+ extend_prev_word_start, "Extend to beginning of previous word",
179− extend_next_word_end,
182+ extend_next_word_end, "Extend to end of next word",
180− find_till_char,
183+ find_till_char, "Move till next occurance of char",
181− find_next_char,
184+ find_next_char, "Move to next occurance of char",
182− extend_till_char,
185+ extend_till_char, "Extend till next occurance of char",
183− extend_next_char,
186+ extend_next_char, "Extend to next occurance of char",
184− till_prev_char,
187+ till_prev_char, "Move till previous occurance of char",
185− find_prev_char,
188+ find_prev_char, "Move to previous occurance of char",
186− extend_till_prev_char,
189+ extend_till_prev_char, "Extend till previous occurance of char",
187− extend_prev_char,
190+ extend_prev_char, "Extend to previous occurance of char",
188− replace,
191+ replace, "Replace with new char",
189− switch_case,
192+ switch_case, "Switch (toggle) case",
190− switch_to_uppercase,
193+ switch_to_uppercase, "Switch to uppercase",
191− switch_to_lowercase,
194+ switch_to_lowercase, "Switch to lowercase",
192− page_up,
195+ page_up, "Move page up",
193− page_down,
196+ page_down, "Move page down",
194− half_page_up,
197+ half_page_up, "Move half page up",
195− half_page_down,
198+ half_page_down, "Move half page down",
196− extend_char_left,
197− extend_char_right,
198− extend_line_up,
199− extend_line_down,
200− select_all,
199+ select_all, "Select whole document",
201− select_regex,
200+ select_regex, "Select all regex matches inside selections",
202− split_selection,
201+ split_selection, "Split selection into subselections on regex matches",
203− split_selection_on_newline,
202+ split_selection_on_newline, "Split selection on newlines",
204− search,
203+ search, "Search for regex pattern",
205− search_next,
204+ search_next, "Select next search match",
206− extend_search_next,
205+ extend_search_next, "Add next search match to selection",
207− search_selection,
206+ search_selection, "Use current selection as search pattern",
208− extend_line,
207+ extend_line, "Select current line, if already selected, extend to next line",
209− extend_to_line_bounds,
208+ extend_to_line_bounds, "Extend selection to line bounds (line-wise selection)",
210− delete_selection,
209+ delete_selection, "Delete selection",
211− change_selection,
210+ change_selection, "Change selection (delete and enter insert mode)",
212− collapse_selection,
211+ collapse_selection, "Collapse selection onto a single cursor",
213− flip_selections,
212+ flip_selections, "Flip selection cursor and anchor",
214− insert_mode,
213+ insert_mode, "Insert before selection",
215− append_mode,
214+ append_mode, "Insert after selection (append)",
216− command_mode,
215+ command_mode, "Enter command mode",
217− file_picker,
216+ file_picker, "Open file picker",
217+ code_action, "Perform code action",
218− buffer_picker,
218+ buffer_picker, "Open buffer picker",
219− symbol_picker,
219+ symbol_picker, "Open symbol picker",
220− last_picker,
220+ last_picker, "Open last picker",
221− prepend_to_line,
221+ prepend_to_line, "Insert at start of line",
222− append_to_line,
222+ append_to_line, "Insert at end of line",
223− open_below,
223+ open_below, "Open new line below selection",
224− open_above,
224+ open_above, "Open new line above selection",
225− normal_mode,
225+ normal_mode, "Enter normal mode",
226− goto_mode,
227− select_mode,
226+ select_mode, "Enter selection extend mode",
228− exit_select_mode,
227+ exit_select_mode, "Exit selection mode",
229− goto_definition,
228+ goto_definition, "Goto definition",
230− goto_type_definition,
229+ goto_type_definition, "Goto type definition",
231− goto_implementation,
230+ goto_implementation, "Goto implementation",
232− goto_file_start,
231+ goto_file_start, "Goto file start",
233− goto_file_end,
232+ goto_file_end, "Goto file end",
234− goto_reference,
233+ goto_reference, "Goto references",
234+ goto_window_top, "Goto window top",
235+ goto_window_middle, "Goto window middle",
236+ goto_window_bottom, "Goto window bottom",
237+ goto_last_accessed_file, "Goto last accessed file",
235− goto_first_diag,
238+ goto_first_diag, "Goto first diagnostic",
236− goto_last_diag,
239+ goto_last_diag, "Goto last diagnostic",
237− goto_next_diag,
240+ goto_next_diag, "Goto next diagnostic",
238− goto_prev_diag,
241+ goto_prev_diag, "Goto previous diagnostic",
239− goto_line_start,
242+ goto_line_start, "Goto line start",
240− goto_line_end,
243+ goto_line_end, "Goto line end",
244+ // TODO: different description ?
241− goto_line_end_newline,
245+ goto_line_end_newline, "Goto line end",
242− goto_first_nonwhitespace,
246+ goto_first_nonwhitespace, "Goto first non-blank in line",
243− signature_help,
247+ signature_help, "Show signature help",
244− insert_tab,
248+ insert_tab, "Insert tab char",
245− insert_newline,
249+ insert_newline, "Insert newline char",
246− delete_char_backward,
250+ delete_char_backward, "Delete previous char",
247− delete_char_forward,
251+ delete_char_forward, "Delete next char",
248− delete_word_backward,
252+ delete_word_backward, "Delete previous word",
249− undo,
253+ undo, "Undo change",
250− redo,
254+ redo, "Redo change",
251− yank,
255+ yank, "Yank selection",
252− yank_joined_to_clipboard,
256+ yank_joined_to_clipboard, "Join and yank selections to clipboard",
253− yank_main_selection_to_clipboard,
257+ yank_main_selection_to_clipboard, "Yank main selection to clipboard",
254− replace_with_yanked,
258+ replace_with_yanked, "Replace with yanked text",
255− replace_selections_with_clipboard,
259+ replace_selections_with_clipboard, "Replace selections by clipboard content",
256− paste_after,
260+ paste_after, "Paste after selection",
257− paste_before,
261+ paste_before, "Paste before selection",
258− paste_clipboard_after,
262+ paste_clipboard_after, "Paste clipboard after selections",
259− paste_clipboard_before,
263+ paste_clipboard_before, "Paste clipboard before selections",
260− indent,
264+ indent, "Indent selection",
261− unindent,
265+ unindent, "Unindent selection",
262− format_selections,
266+ format_selections, "Format selection",
263− join_selections,
267+ join_selections, "Join lines inside selection",
264− keep_selections,
268+ keep_selections, "Keep selections matching regex",
265− keep_primary_selection,
269+ keep_primary_selection, "Keep primary selection",
266− completion,
270+ completion, "Invoke completion popup",
267− hover,
271+ hover, "Show docs for item under cursor",
268− toggle_comments,
272+ toggle_comments, "Comment/uncomment selections",
269− expand_selection,
273+ expand_selection, "Expand selection to parent syntax node",
270− match_brackets,
271− jump_forward,
274+ jump_forward, "Jump forward on jumplist",
272− jump_backward,
275+ jump_backward, "Jump backward on jumplist",
273− window_mode,
274− rotate_view,
276+ rotate_view, "Goto next window",
275− hsplit,
277+ hsplit, "Horizontal bottom split",
276− vsplit,
278+ vsplit, "Vertical right split",
277− wclose,
279+ wclose, "Close window",
278− select_register,
280+ select_register, "Select register",
279− space_mode,
281+ align_view_middle, "Align view middle",
280− view_mode,
282+ align_view_top, "Align view top",
281− left_bracket_mode,
283+ align_view_center, "Align view center",
282− right_bracket_mode,
284+ align_view_bottom, "Align view bottom",
283− match_mode
285+ scroll_up, "Scroll view up",
286+ scroll_down, "Scroll view down",
287+ match_brackets, "Goto matching bracket",
288+ surround_add, "Surround add",
289+ surround_replace, "Surround replace",
290+ surround_delete, "Surround delete",
291+ select_textobject_around, "Select around object",
292+ select_textobject_inner, "Select inside object"
284 );
293 );
285}
294}
286
295
287impl fmt::Debug for Command {
296impl fmt::Debug for Command {
288 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
297 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
289− let Command(name, _) = self;
298+ let Command { name, .. } = self;
290 f.debug_tuple("Command").field(name).finish()
299 f.debug_tuple("Command").field(name).finish()
291 }
300 }
292}
301}
293
302
294impl fmt::Display for Command {
303impl fmt::Display for Command {
295 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
304 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
296− let Command(name, _) = self;
305+ let Command { name, .. } = self;
297 f.write_str(name)
306 f.write_str(name)
298 }
307 }
299}
308}
@@ -305,7 +314,7 @@ impl std::str::FromStr for Command {
305 Command::COMMAND_LIST
314 Command::COMMAND_LIST
306 .iter()
315 .iter()
307 .copied()
316 .copied()
308− .find(|cmd| cmd.0 == s)
317+ .find(|cmd| cmd.name == s)
309 .ok_or_else(|| anyhow!("No command named '{}'", s))
318 .ok_or_else(|| anyhow!("No command named '{}'", s))
310 }
319 }
311}
320}
@@ -1171,7 +1180,7 @@ fn delete_selection(cx: &mut Context) {
1171 let reg_name = cx.selected_register.name();
1180 let reg_name = cx.selected_register.name();
1172 let (view, doc) = current!(cx.editor);
1181 let (view, doc) = current!(cx.editor);
1173 let registers = &mut cx.editor.registers;
1182 let registers = &mut cx.editor.registers;
1174− let reg = registers.get_or_insert(reg_name);
1183+ let reg = registers.get_mut(reg_name);
1175 delete_selection_impl(reg, doc, view.id);
1184 delete_selection_impl(reg, doc, view.id);
1176
1185
1177 doc.append_changes_to_history(view.id);
1186 doc.append_changes_to_history(view.id);
@@ -1184,7 +1193,7 @@ fn change_selection(cx: &mut Context) {
1184 let reg_name = cx.selected_register.name();
1193 let reg_name = cx.selected_register.name();
1185 let (view, doc) = current!(cx.editor);
1194 let (view, doc) = current!(cx.editor);
1186 let registers = &mut cx.editor.registers;
1195 let registers = &mut cx.editor.registers;
1187− let reg = registers.get_or_insert(reg_name);
1196+ let reg = registers.get_mut(reg_name);
1188 delete_selection_impl(reg, doc, view.id);
1197 delete_selection_impl(reg, doc, view.id);
1189 enter_insert_mode(doc);
1198 enter_insert_mode(doc);
1190}
1199}
@@ -1974,6 +1983,7 @@ mod cmd {
1974fn command_mode(cx: &mut Context) {
1983fn command_mode(cx: &mut Context) {
1975 let mut prompt = Prompt::new(
1984 let mut prompt = Prompt::new(
1976 ":".to_owned(),
1985 ":".to_owned(),
1986+ Some(':'),
1977 |input: &str| {
1987 |input: &str| {
1978 // we use .this over split_whitespace() because we care about empty segments
1988 // we use .this over split_whitespace() because we care about empty segments
1979 let parts = input.split(' ').collect::<Vec<&str>>();
1989 let parts = input.split(' ').collect::<Vec<&str>>();
@@ -2147,6 +2157,112 @@ fn symbol_picker(cx: &mut Context) {
2147 )
2157 )
2148}
2158}
2149
2159
2160+pub fn code_action(cx: &mut Context) {
2161+ let (view, doc) = current!(cx.editor);
2162+
2163+ let language_server = match doc.language_server() {
2164+ Some(language_server) => language_server,
2165+ None => return,
2166+ };
2167+
2168+ let range = range_to_lsp_range(
2169+ doc.text(),
2170+ doc.selection(view.id).primary(),
2171+ language_server.offset_encoding(),
2172+ );
2173+
2174+ let future = language_server.code_actions(doc.identifier(), range);
2175+ let offset_encoding = language_server.offset_encoding();
2176+
2177+ cx.callback(
2178+ future,
2179+ move |_editor: &mut Editor,
2180+ compositor: &mut Compositor,
2181+ response: Option<lsp::CodeActionResponse>| {
2182+ if let Some(actions) = response {
2183+ let picker = Picker::new(
2184+ actions,
2185+ |action| match action {
2186+ lsp::CodeActionOrCommand::CodeAction(action) => {
2187+ action.title.as_str().into()
2188+ }
2189+ lsp::CodeActionOrCommand::Command(command) => command.title.as_str().into(),
2190+ },
2191+ move |editor, code_action, _action| match code_action {
2192+ lsp::CodeActionOrCommand::Command(command) => {
2193+ log::debug!("code action command: {:?}", command);
2194+ editor.set_error(String::from("Handling code action command is not implemented yet, see https://github.com/helix-editor/helix/issues/183"));
2195+ }
2196+ lsp::CodeActionOrCommand::CodeAction(code_action) => {
2197+ log::debug!("code action: {:?}", code_action);
2198+ if let Some(ref workspace_edit) = code_action.edit {
2199+ apply_workspace_edit(editor, offset_encoding, workspace_edit)
2200+ }
2201+ }
2202+ },
2203+ );
2204+ compositor.push(Box::new(picker))
2205+ }
2206+ },
2207+ )
2208+}
2209+
2210+fn apply_workspace_edit(
2211+ editor: &mut Editor,
2212+ offset_encoding: OffsetEncoding,
2213+ workspace_edit: &lsp::WorkspaceEdit,
2214+) {
2215+ if let Some(ref changes) = workspace_edit.changes {
2216+ log::debug!("workspace changes: {:?}", changes);
2217+ editor.set_error(String::from("Handling workspace changesis not implemented yet, see https://github.com/helix-editor/helix/issues/183"));
2218+ return;
2219+ // 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
2220+ // TODO: find some example that uses workspace changes, and test it
2221+ // for (url, edits) in changes.iter() {
2222+ // let file_path = url.origin().ascii_serialization();
2223+ // let file_path = std::path::PathBuf::from(file_path);
2224+ // let file = std::fs::File::open(file_path).unwrap();
2225+ // let mut text = Rope::from_reader(file).unwrap();
2226+ // let transaction = edits_to_changes(&text, edits);
2227+ // transaction.apply(&mut text);
2228+ // }
2229+ }
2230+
2231+ if let Some(ref document_changes) = workspace_edit.document_changes {
2232+ match document_changes {
2233+ lsp::DocumentChanges::Edits(document_edits) => {
2234+ for document_edit in document_edits {
2235+ let (view, doc) = current!(editor);
2236+ assert_eq!(doc.url().unwrap(), document_edit.text_document.uri);
2237+ let edits = document_edit
2238+ .edits
2239+ .iter()
2240+ .map(|edit| match edit {
2241+ lsp::OneOf::Left(text_edit) => text_edit,
2242+ lsp::OneOf::Right(annotated_text_edit) => {
2243+ &annotated_text_edit.text_edit
2244+ }
2245+ })
2246+ .cloned()
2247+ .collect();
2248+
2249+ let transaction = helix_lsp::util::generate_transaction_from_edits(
2250+ doc.text(),
2251+ edits,
2252+ offset_encoding,
2253+ );
2254+ doc.apply(&transaction, view.id);
2255+ doc.append_changes_to_history(view.id);
2256+ }
2257+ }
2258+ lsp::DocumentChanges::Operations(operations) => {
2259+ log::debug!("document changes - operations: {:?}", operations);
2260+ editor.set_error(String::from("Handling document operations is not implemented yet, see https://github.com/helix-editor/helix/issues/183"));
2261+ }
2262+ }
2263+ }
2264+}
2265+
2150fn last_picker(cx: &mut Context) {
2266fn last_picker(cx: &mut Context) {
2151 // TODO: last picker does not seemed to work well with buffer_picker
2267 // TODO: last picker does not seemed to work well with buffer_picker
2152 cx.callback = Some(Box::new(|compositor: &mut Compositor| {
2268 cx.callback = Some(Box::new(|compositor: &mut Compositor| {
@@ -2360,20 +2476,6 @@ fn exit_select_mode(cx: &mut Context) {
2360 doc_mut!(cx.editor).mode = Mode::Normal;
2476 doc_mut!(cx.editor).mode = Mode::Normal;
2361}
2477}
2362
2478
2363−fn goto_prehook(cx: &mut Context) -> bool {
2364− if let Some(count) = cx.count {
2365− push_jump(cx.editor);
2366−
2367− let (view, doc) = current!(cx.editor);
2368− let line_idx = std::cmp::min(count.get() - 1, doc.text().len_lines().saturating_sub(1));
2369− let pos = doc.text().line_to_char(line_idx);
2370− doc.set_selection(view.id, Selection::point(pos));
2371− true
2372− } else {
2373− false
2374− }
2375−}
2376−
2377fn goto_impl(
2479fn goto_impl(
2378 editor: &mut Editor,
2480 editor: &mut Editor,
2379 compositor: &mut Compositor,
2481 compositor: &mut Compositor,
@@ -3823,199 +3925,3 @@ fn surround_delete(cx: &mut Context) {
3823 }
3925 }
3824 })
3926 })
3825}
3927}
3826−
3827−/// Do nothing, just for modeinfo.
3828−fn noop(_cx: &mut Context) -> bool {
3829− false
3830−}
3831−
3832−/// Generate modeinfo.
3833−///
3834−/// If prehook returns true then it will stop the rest.
3835−macro_rules! mode_info {
3836− // TODO: reuse $mode for $stat
3837− (@join $first:expr $(,$rest:expr)*) => {
3838− concat!($first, $(", ", $rest),*)
3839− };
3840− (@name #[doc = $name:literal] $(#[$rest:meta])*) => {
3841− $name
3842− };
3843− {
3844− #[doc = $name:literal] $(#[$doc:meta])* $mode:ident, $stat:ident,
3845− $(#[doc = $desc:literal] $($key:tt)|+ => $func:expr),+,
3846− } => {
3847− mode_info! {
3848− #[doc = $name]
3849− $(#[$doc])*
3850− $mode, $stat, noop,
3851− $(
3852− #[doc = $desc]
3853− $($key)|+ => $func
3854− ),+,
3855− }
3856− };
3857− {
3858− #[doc = $name:literal] $(#[$doc:meta])* $mode:ident, $stat:ident, $prehook:expr,
3859− $(#[doc = $desc:literal] $($key:tt)|+ => $func:expr),+,
3860− } => {
3861− #[doc = $name]
3862− $(#[$doc])*
3863− #[doc = ""]
3864− #[doc = "<table><tr><th>key</th><th>desc</th></tr><tbody>"]
3865− $(
3866− #[doc = "<tr><td>"]
3867− // TODO switch to this once we use rust 1.54
3868− // right now it will produce multiple rows
3869− // #[doc = mode_info!(@join $($key),+)]
3870− $(
3871− #[doc = $key]
3872− )+
3873− // <-
3874− #[doc = "</td><td>"]
3875− #[doc = $desc]
3876− #[doc = "</td></tr>"]
3877− )+
3878− #[doc = "</tbody></table>"]
3879− pub fn $mode(cx: &mut Context) {
3880− if $prehook(cx) {
3881− return;
3882− }
3883− static $stat: OnceCell<Info> = OnceCell::new();
3884− cx.editor.autoinfo = Some($stat.get_or_init(|| Info::key(
3885− $name.trim(),
3886− vec![$((&[$($key.parse().unwrap()),+], $desc)),+],
3887− )));
3888− use helix_core::hashmap;
3889− // TODO: try and convert this to match later
3890− let map = hashmap! {
3891− $($($key.parse::<KeyEvent>().unwrap() => $func as for<'r, 's> fn(&'r mut Context<'s>)),+),*
3892− };
3893− cx.on_next_key_mode(map);
3894− }
3895− };
3896−}
3897−
3898−mode_info! {
3899− /// space mode
3900− space_mode, SPACE_MODE,
3901− /// resume last picker
3902− "'" => last_picker,
3903− /// file picker
3904− "f" => file_picker,
3905− /// buffer picker
3906− "b" => buffer_picker,
3907− /// symbol picker
3908− "s" => symbol_picker,
3909− /// window mode
3910− "w" => window_mode,
3911− /// yank joined to clipboard
3912− "y" => yank_joined_to_clipboard,
3913− /// yank main selection to clipboard
3914− "Y" => yank_main_selection_to_clipboard,
3915− /// paste system clipboard after selections
3916− "p" => paste_clipboard_after,
3917− /// paste system clipboard before selections
3918− "P" => paste_clipboard_before,
3919− /// replace selections with clipboard
3920− "R" => replace_selections_with_clipboard,
3921− /// keep primary selection
3922− "space" => keep_primary_selection,
3923−}
3924−
3925−mode_info! {
3926− /// goto
3927− ///
3928− /// When specified with a count, it will go to that line without entering the mode.
3929− goto_mode, GOTO_MODE, goto_prehook,
3930− /// file start
3931− "g" => goto_file_start,
3932− /// file end
3933− "e" => goto_file_end,
3934− /// line start
3935− "h" => goto_line_start,
3936− /// line end
3937− "l" => goto_line_end,
3938− /// line first non blank
3939− "s" => goto_first_nonwhitespace,
3940− /// definition
3941− "d" => goto_definition,
3942− /// type references
3943− "y" => goto_type_definition,
3944− /// references
3945− "r" => goto_reference,
3946− /// implementation
3947− "i" => goto_implementation,
3948− /// window top
3949− "t" => goto_window_top,
3950− /// window middle
3951− "m" => goto_window_middle,
3952− /// window bottom
3953− "b" => goto_window_bottom,
3954− /// last accessed file
3955− "a" => goto_last_accessed_file,
3956−}
3957−
3958−mode_info! {
3959− /// window
3960− window_mode, WINDOW_MODE,
3961− /// rotate
3962− "w" | "C-w" => rotate_view,
3963− /// horizontal split
3964− "h" => hsplit,
3965− /// vertical split
3966− "v" => vsplit,
3967− /// close
3968− "q" => wclose,
3969−}
3970−
3971−mode_info! {
3972− /// match
3973− match_mode, MATCH_MODE,
3974− /// matching character
3975− "m" => match_brackets,
3976− /// surround add
3977− "s" => surround_add,
3978− /// surround replace
3979− "r" => surround_replace,
3980− /// surround delete
3981− "d" => surround_delete,
3982− /// around object
3983− "a" => select_textobject_around,
3984− /// inside object
3985− "i" => select_textobject_inner,
3986−}
3987−
3988−mode_info! {
3989− /// select to previous
3990− left_bracket_mode, LEFT_BRACKET_MODE,
3991− /// previous diagnostic
3992− "d" => goto_prev_diag,
3993− /// diagnostic (first)
3994− "D" => goto_first_diag,
3995−}
3996−
3997−mode_info! {
3998− /// select to next
3999− right_bracket_mode, RIGHT_BRACKET_MODE,
4000− /// diagnostic
4001− "d" => goto_next_diag,
4002− /// diagnostic (last)
4003− "D" => goto_last_diag,
4004−}
4005−
4006−mode_info! {
4007− /// view
4008− view_mode, VIEW_MODE,
4009− /// align view top
4010− "t" => align_view_top,
4011− /// align view center
4012− "z" | "c" => align_view_center,
4013− /// align view bottom
4014− "b" => align_view_bottom,
4015− /// align view middle
4016− "m" => align_view_middle,
4017− /// scroll up
4018− "k" => scroll_up,
4019− /// scroll down
4020− "j" => scroll_down,
4021−}
helix-term/src/config.rs+10−24
@@ -2,9 +2,6 @@ use serde::Deserialize;
2
2
3use crate::keymap::Keymaps;
3use crate::keymap::Keymaps;
4
4
5−#[cfg(test)]
6−use crate::commands::Command;
7−
8#[derive(Debug, Default, Clone, PartialEq, Deserialize)]
5#[derive(Debug, Default, Clone, PartialEq, Deserialize)]
9pub struct Config {
6pub struct Config {
10 pub theme: Option<String>,
7 pub theme: Option<String>,
@@ -22,12 +19,10 @@ pub struct LspConfig {
22
19
23#[test]
20#[test]
24fn parsing_keymaps_config_file() {
21fn parsing_keymaps_config_file() {
22+ use crate::keymap;
23+ use crate::keymap::Keymap;
25 use helix_core::hashmap;
24 use helix_core::hashmap;
26− use helix_view::{
25+ use helix_view::document::Mode;
27− document::Mode,
28− input::KeyEvent,
29− keyboard::{KeyCode, KeyModifiers},
30− };
31
26
32 let sample_keymaps = r#"
27 let sample_keymaps = r#"
33 [keys.insert]
28 [keys.insert]
@@ -42,22 +37,13 @@ fn parsing_keymaps_config_file() {
42 toml::from_str::<Config>(sample_keymaps).unwrap(),
37 toml::from_str::<Config>(sample_keymaps).unwrap(),
43 Config {
38 Config {
44 keys: Keymaps(hashmap! {
39 keys: Keymaps(hashmap! {
45− Mode::Insert => hashmap! {
40+ Mode::Insert => Keymap::new(keymap!({ "Insert mode"
46− KeyEvent {
47− code: KeyCode::Char('y'),
48− modifiers: KeyModifiers::NONE,
49− } => Command::move_line_down,
41+ "y" => move_line_down,
50− KeyEvent {
51− code: KeyCode::Char('a'),
52− modifiers: KeyModifiers::SHIFT | KeyModifiers::CONTROL,
53− } => Command::delete_selection,
42+ "S-C-a" => delete_selection,
54− },
43+ })),
55− Mode::Normal => hashmap! {
44+ Mode::Normal => Keymap::new(keymap!({ "Normal mode"
56− KeyEvent {
57− code: KeyCode::F(12),
58− modifiers: KeyModifiers::ALT,
59− } => Command::move_next_word_end,
45+ "A-F12" => move_next_word_end,
60− },
46+ })),
61 }),
47 }),
62 ..Default::default()
48 ..Default::default()
63 }
49 }
helix-term/src/keymap.rs+492−200
@@ -1,7 +1,7 @@
1pub use crate::commands::Command;
1pub use crate::commands::Command;
2use crate::config::Config;
2use crate::config::Config;
3use helix_core::hashmap;
3use helix_core::hashmap;
4−use helix_view::{document::Mode, input::KeyEvent};
4+use helix_view::{document::Mode, info::Info, input::KeyEvent};
5use serde::Deserialize;
5use serde::Deserialize;
6use std::{
6use std::{
7 collections::HashMap,
7 collections::HashMap,
@@ -24,30 +24,276 @@ macro_rules! key {
24 };
24 };
25}
25}
26
26
27+/// Macro for defining the root of a `Keymap` object. Example:
28+///
29+/// ```
30+/// # use helix_core::hashmap;
31+/// # use helix_term::keymap;
32+/// # use helix_term::keymap::Keymap;
33+/// let normal_mode = keymap!({ "Normal mode"
34+/// "i" => insert_mode,
35+/// "g" => { "Goto"
36+/// "g" => goto_file_start,
37+/// "e" => goto_file_end,
38+/// },
39+/// "j" | "down" => move_line_down,
40+/// });
41+/// let keymap = Keymap::new(normal_mode);
42+/// ```
43+#[macro_export]
27−macro_rules! ctrl {
44+macro_rules! keymap {
45+ (@trie $cmd:ident) => {
46+ $crate::keymap::KeyTrie::Leaf($crate::commands::Command::$cmd)
47+ };
48+
49+ (@trie
50+ { $label:literal $($($key:literal)|+ => $value:tt,)+ }
28− ($($ch:tt)*) => {
51+ ) => {
29− KeyEvent {
52+ keymap!({ $label $($($key)|+ => $value,)+ })
30− code: ::helix_view::keyboard::KeyCode::Char($($ch)*),
53+ };
31− modifiers: ::helix_view::keyboard::KeyModifiers::CONTROL,
54+
55+ (
56+ { $label:literal $($($key:literal)|+ => $value:tt,)+ }
57+ ) => {
58+ // modified from the hashmap! macro
59+ {
60+ let _cap = hashmap!(@count $($($key),+),*);
61+ let mut _map = ::std::collections::HashMap::with_capacity(_cap);
62+ let mut _order = ::std::vec::Vec::with_capacity(_cap);
63+ $(
64+ $(
65+ let _key = $key.parse::<::helix_view::input::KeyEvent>().unwrap();
66+ _map.insert(
67+ _key,
68+ keymap!(@trie $value)
69+ );
70+ _order.push(_key);
71+ )+
72+ )*
73+ $crate::keymap::KeyTrie::Node($crate::keymap::KeyTrieNode::new($label, _map, _order))
32 }
74 }
33 };
75 };
34}
76}
35
77
36−macro_rules! alt {
78+#[derive(Debug, Clone, Deserialize)]
37− ($($ch:tt)*) => {
79+pub struct KeyTrieNode {
38− KeyEvent {
80+ /// A label for keys coming under this node, like "Goto mode"
39− code: ::helix_view::keyboard::KeyCode::Char($($ch)*),
81+ #[serde(skip)]
40− modifiers: ::helix_view::keyboard::KeyModifiers::ALT,
82+ name: String,
83+ #[serde(flatten)]
84+ map: HashMap<KeyEvent, KeyTrie>,
85+ #[serde(skip)]
86+ order: Vec<KeyEvent>,
87+}
88+
89+impl KeyTrieNode {
90+ pub fn new(name: &str, map: HashMap<KeyEvent, KeyTrie>, order: Vec<KeyEvent>) -> Self {
91+ Self {
92+ name: name.to_string(),
93+ map,
94+ order,
41 }
95 }
42− };
96+ }
97+
98+ pub fn name(&self) -> &str {
99+ &self.name
100+ }
101+
102+ /// Merge another Node in. Leaves and subnodes from the other node replace
103+ /// corresponding keyevent in self, except when both other and self have
104+ /// subnodes for same key. In that case the merge is recursive.
105+ pub fn merge(&mut self, mut other: Self) {
106+ for (key, trie) in std::mem::take(&mut other.map) {
107+ if let Some(KeyTrie::Node(node)) = self.map.get_mut(&key) {
108+ if let KeyTrie::Node(other_node) = trie {
109+ node.merge(other_node);
110+ continue;
111+ }
112+ }
113+ self.map.insert(key, trie);
114+ }
115+
116+ for &key in self.map.keys() {
117+ if !self.order.contains(&key) {
118+ self.order.push(key);
119+ }
120+ }
121+ }
122+}
123+
124+impl From<KeyTrieNode> for Info {
125+ fn from(node: KeyTrieNode) -> Self {
126+ let mut body: Vec<(&str, Vec<KeyEvent>)> = Vec::with_capacity(node.len());
127+ for (&key, trie) in node.iter() {
128+ let desc = match trie {
129+ KeyTrie::Leaf(cmd) => cmd.doc(),
130+ KeyTrie::Node(n) => n.name(),
131+ };
132+ match body.iter().position(|(d, _)| d == &desc) {
133+ // FIXME: multiple keys are ordered randomly (use BTreeSet)
134+ Some(pos) => body[pos].1.push(key),
135+ None => body.push((desc, vec![key])),
136+ }
137+ }
138+ body.sort_unstable_by_key(|(_, keys)| {
139+ node.order.iter().position(|&k| k == keys[0]).unwrap()
140+ });
141+ let prefix = format!("{} ", node.name());
142+ if body.iter().all(|(desc, _)| desc.starts_with(&prefix)) {
143+ body = body
144+ .into_iter()
145+ .map(|(desc, keys)| (desc.strip_prefix(&prefix).unwrap(), keys))
146+ .collect();
147+ }
148+ Info::key(node.name(), body)
149+ }
150+}
151+
152+impl Default for KeyTrieNode {
153+ fn default() -> Self {
154+ Self::new("", HashMap::new(), Vec::new())
155+ }
156+}
157+
158+impl PartialEq for KeyTrieNode {
159+ fn eq(&self, other: &Self) -> bool {
160+ self.map == other.map
161+ }
162+}
163+
164+impl Deref for KeyTrieNode {
165+ type Target = HashMap<KeyEvent, KeyTrie>;
166+
167+ fn deref(&self) -> &Self::Target {
168+ &self.map
169+ }
170+}
171+
172+impl DerefMut for KeyTrieNode {
173+ fn deref_mut(&mut self) -> &mut Self::Target {
174+ &mut self.map
175+ }
176+}
177+
178+#[derive(Debug, Clone, PartialEq, Deserialize)]
179+#[serde(untagged)]
180+pub enum KeyTrie {
181+ Leaf(Command),
182+ Node(KeyTrieNode),
183+}
184+
185+impl KeyTrie {
186+ pub fn node(&self) -> Option<&KeyTrieNode> {
187+ match *self {
188+ KeyTrie::Node(ref node) => Some(node),
189+ KeyTrie::Leaf(_) => None,
190+ }
191+ }
192+
193+ pub fn node_mut(&mut self) -> Option<&mut KeyTrieNode> {
194+ match *self {
195+ KeyTrie::Node(ref mut node) => Some(node),
196+ KeyTrie::Leaf(_) => None,
197+ }
198+ }
199+
200+ /// Merge another KeyTrie in, assuming that this KeyTrie and the other
201+ /// are both Nodes. Panics otherwise.
202+ pub fn merge_nodes(&mut self, mut other: Self) {
203+ let node = std::mem::take(other.node_mut().unwrap());
204+ self.node_mut().unwrap().merge(node);
205+ }
206+
207+ pub fn search(&self, keys: &[KeyEvent]) -> Option<&KeyTrie> {
208+ let mut trie = self;
209+ for key in keys {
210+ trie = match trie {
211+ KeyTrie::Node(map) => map.get(key),
212+ // leaf encountered while keys left to process
213+ KeyTrie::Leaf(_) => None,
214+ }?
215+ }
216+ Some(trie)
217+ }
218+}
219+
220+#[derive(Debug, Clone, PartialEq)]
221+pub enum KeymapResult {
222+ /// Needs more keys to execute a command. Contains valid keys for next keystroke.
223+ Pending(KeyTrieNode),
224+ Matched(Command),
225+ /// Key was not found in the root keymap
226+ NotFound,
227+ /// Key is invalid in combination with previous keys. Contains keys leading upto
228+ /// and including current (invalid) key.
229+ Cancelled(Vec<KeyEvent>),
230+}
231+
232+#[derive(Debug, Clone, PartialEq, Deserialize)]
233+pub struct Keymap {
234+ /// Always a Node
235+ #[serde(flatten)]
236+ root: KeyTrie,
237+ #[serde(skip)]
238+ state: Vec<KeyEvent>,
239+}
240+
241+impl Keymap {
242+ pub fn new(root: KeyTrie) -> Self {
243+ Keymap {
244+ root,
245+ state: Vec::new(),
246+ }
247+ }
248+
249+ pub fn root(&self) -> &KeyTrie {
250+ &self.root
251+ }
252+
253+ /// Lookup `key` in the keymap to try and find a command to execute
254+ pub fn get(&mut self, key: KeyEvent) -> KeymapResult {
255+ let &first = self.state.get(0).unwrap_or(&key);
256+ let trie = match self.root.search(&[first]) {
257+ Some(&KeyTrie::Leaf(cmd)) => return KeymapResult::Matched(cmd),
258+ None => return KeymapResult::NotFound,
259+ Some(t) => t,
260+ };
261+ self.state.push(key);
262+ match trie.search(&self.state[1..]) {
263+ Some(&KeyTrie::Node(ref map)) => KeymapResult::Pending(map.clone()),
264+ Some(&KeyTrie::Leaf(command)) => {
265+ self.state.clear();
266+ KeymapResult::Matched(command)
267+ }
268+ None => KeymapResult::Cancelled(self.state.drain(..).collect()),
269+ }
270+ }
271+
272+ pub fn merge(&mut self, other: Self) {
273+ self.root.merge_nodes(other.root);
274+ }
275+}
276+
277+impl Deref for Keymap {
278+ type Target = KeyTrieNode;
279+
280+ fn deref(&self) -> &Self::Target {
281+ &self.root.node().unwrap()
282+ }
283+}
284+
285+impl Default for Keymap {
286+ fn default() -> Self {
287+ Self::new(KeyTrie::Node(KeyTrieNode::default()))
288+ }
43}
289}
44
290
45#[derive(Debug, Clone, PartialEq, Deserialize)]
291#[derive(Debug, Clone, PartialEq, Deserialize)]
46#[serde(transparent)]
292#[serde(transparent)]
47−pub struct Keymaps(pub HashMap<Mode, HashMap<KeyEvent, Command>>);
293+pub struct Keymaps(pub HashMap<Mode, Keymap>);
48
294
49impl Deref for Keymaps {
295impl Deref for Keymaps {
50− type Target = HashMap<Mode, HashMap<KeyEvent, Command>>;
296+ type Target = HashMap<Mode, Keymap>;
51
297
52 fn deref(&self) -> &Self::Target {
298 fn deref(&self) -> &Self::Target {
53 &self.0
299 &self.0
@@ -62,252 +308,298 @@ impl DerefMut for Keymaps {
62
308
63impl Default for Keymaps {
309impl Default for Keymaps {
64 fn default() -> Keymaps {
310 fn default() -> Keymaps {
65− let normal = hashmap!(
311+ let normal = keymap!({ "Normal mode"
66− key!('h') => Command::move_char_left,
312+ "h" | "left" => move_char_left,
67− key!('j') => Command::move_line_down,
313+ "j" | "down" => move_line_down,
68− key!('k') => Command::move_line_up,
314+ "k" | "up" => move_line_up,
69− key!('l') => Command::move_char_right,
70−
71− key!(Left) => Command::move_char_left,
72− key!(Down) => Command::move_line_down,
73− key!(Up) => Command::move_line_up,
74− key!(Right) => Command::move_char_right,
315+ "l" | "right" => move_char_right,
75−
316+
76− key!('t') => Command::find_till_char,
317+ "t" => find_till_char,
77− key!('f') => Command::find_next_char,
318+ "f" => find_next_char,
78− key!('T') => Command::till_prev_char,
319+ "T" => till_prev_char,
79− key!('F') => Command::find_prev_char,
320+ "F" => find_prev_char,
80− // and matching set for select mode (extend)
81− //
82− key!('r') => Command::replace,
321+ "r" => replace,
83− key!('R') => Command::replace_with_yanked,
322+ "R" => replace_with_yanked,
84−
323+
85− key!('~') => Command::switch_case,
324+ "~" => switch_case,
86− alt!('`') => Command::switch_to_uppercase,
87− key!('`') => Command::switch_to_lowercase,
325+ "`" => switch_to_lowercase,
88−
326+ "A-`" => switch_to_uppercase,
327+
89− key!(Home) => Command::goto_line_start,
328+ "home" => goto_line_start,
90− key!(End) => Command::goto_line_end,
329+ "end" => goto_line_end,
91−
330+
92− key!('w') => Command::move_next_word_start,
331+ "w" => move_next_word_start,
93− key!('b') => Command::move_prev_word_start,
332+ "b" => move_prev_word_start,
94− key!('e') => Command::move_next_word_end,
333+ "e" => move_next_word_end,
95−
334+
96− key!('W') => Command::move_next_long_word_start,
335+ "W" => move_next_long_word_start,
97− key!('B') => Command::move_prev_long_word_start,
336+ "B" => move_prev_long_word_start,
98− key!('E') => Command::move_next_long_word_end,
337+ "E" => move_next_long_word_end,
99−
338+
100− key!('v') => Command::select_mode,
339+ "v" => select_mode,
101− key!('g') => Command::goto_mode,
340+ "g" => { "Goto"
341+ "g" => goto_file_start,
342+ "e" => goto_file_end,
343+ "h" => goto_line_start,
344+ "l" => goto_line_end,
345+ "s" => goto_first_nonwhitespace,
346+ "d" => goto_definition,
347+ "y" => goto_type_definition,
348+ "r" => goto_reference,
349+ "i" => goto_implementation,
350+ "t" => goto_window_top,
351+ "m" => goto_window_middle,
352+ "b" => goto_window_bottom,
353+ "a" => goto_last_accessed_file,
354+ },
102− key!(':') => Command::command_mode,
355+ ":" => command_mode,
103−
356+
104− key!('i') => Command::insert_mode,
357+ "i" => insert_mode,
105− key!('I') => Command::prepend_to_line,
358+ "I" => prepend_to_line,
106− key!('a') => Command::append_mode,
359+ "a" => append_mode,
107− key!('A') => Command::append_to_line,
360+ "A" => append_to_line,
108− key!('o') => Command::open_below,
361+ "o" => open_below,
109− key!('O') => Command::open_above,
362+ "O" => open_above,
110 // [<space> ]<space> equivalents too (add blank new line, no edit)
363 // [<space> ]<space> equivalents too (add blank new line, no edit)
111
364
112−
113− key!('d') => Command::delete_selection,
365+ "d" => delete_selection,
114 // TODO: also delete without yanking
366 // TODO: also delete without yanking
115− key!('c') => Command::change_selection,
367+ "c" => change_selection,
116 // TODO: also change delete without yanking
368 // TODO: also change delete without yanking
117
369
118− // key!('r') => Command::replace_with_char,
119−
120− key!('s') => Command::select_regex,
370+ "s" => select_regex,
121− alt!('s') => Command::split_selection_on_newline,
371+ "A-s" => split_selection_on_newline,
122− key!('S') => Command::split_selection,
372+ "S" => split_selection,
123− key!(';') => Command::collapse_selection,
373+ ";" => collapse_selection,
124− alt!(';') => Command::flip_selections,
374+ "A-;" => flip_selections,
125− key!('%') => Command::select_all,
375+ "%" => select_all,
126− key!('x') => Command::extend_line,
376+ "x" => extend_line,
127− key!('X') => Command::extend_to_line_bounds,
377+ "X" => extend_to_line_bounds,
128 // crop_to_whole_line
378 // crop_to_whole_line
129
379
380+ "m" => { "Match"
381+ "m" => match_brackets,
382+ "s" => surround_add,
383+ "r" => surround_replace,
384+ "d" => surround_delete,
385+ "a" => select_textobject_around,
386+ "i" => select_textobject_inner,
387+ },
388+ "[" => { "Left bracket"
389+ "d" => goto_prev_diag,
390+ "D" => goto_first_diag,
391+ },
392+ "]" => { "Right bracket"
393+ "d" => goto_next_diag,
394+ "D" => goto_last_diag,
395+ },
130
396
131− key!('m') => Command::match_mode,
132− key!('[') => Command::left_bracket_mode,
133− key!(']') => Command::right_bracket_mode,
134−
135− key!('/') => Command::search,
397+ "/" => search,
136 // ? for search_reverse
398 // ? for search_reverse
137− key!('n') => Command::search_next,
399+ "n" => search_next,
138− key!('N') => Command::extend_search_next,
400+ "N" => extend_search_next,
139 // N for search_prev
401 // N for search_prev
140− key!('*') => Command::search_selection,
402+ "*" => search_selection,
141
403
142− key!('u') => Command::undo,
404+ "u" => undo,
143− key!('U') => Command::redo,
405+ "U" => redo,
144
406
145− key!('y') => Command::yank,
407+ "y" => yank,
146 // yank_all
408 // yank_all
147− key!('p') => Command::paste_after,
409+ "p" => paste_after,
148 // paste_all
410 // paste_all
149− key!('P') => Command::paste_before,
411+ "P" => paste_before,
150
412
151− key!('>') => Command::indent,
413+ ">" => indent,
152− key!('<') => Command::unindent,
414+ "<" => unindent,
153− key!('=') => Command::format_selections,
415+ "=" => format_selections,
154− key!('J') => Command::join_selections,
416+ "J" => join_selections,
155 // TODO: conflicts hover/doc
417 // TODO: conflicts hover/doc
156− key!('K') => Command::keep_selections,
418+ "K" => keep_selections,
157 // TODO: and another method for inverse
419 // TODO: and another method for inverse
158
420
159 // TODO: clashes with space mode
421 // TODO: clashes with space mode
160− key!(' ') => Command::keep_primary_selection,
422+ "space" => keep_primary_selection,
161
423
162− // key!('q') => Command::record_macro,
424+ // "q" => record_macro,
163− // key!('Q') => Command::replay_macro,
425+ // "Q" => replay_macro,
164
426
165− // ~ / apostrophe => change case
166 // & align selections
427 // & align selections
167 // _ trim selections
428 // _ trim selections
168
429
169 // C / altC = copy (repeat) selections on prev/next lines
430 // C / altC = copy (repeat) selections on prev/next lines
170
431
171− key!(Esc) => Command::normal_mode,
432+ "esc" => normal_mode,
172− key!(PageUp) => Command::page_up,
173− key!(PageDown) => Command::page_down,
174− ctrl!('b') => Command::page_up,
433+ "C-b" | "pageup" => page_up,
175− ctrl!('f') => Command::page_down,
434+ "C-f" | "pagedown" => page_down,
176− ctrl!('u') => Command::half_page_up,
435+ "C-u" => half_page_up,
177− ctrl!('d') => Command::half_page_down,
436+ "C-d" => half_page_down,
178−
437+
179− ctrl!('w') => Command::window_mode,
438+ "C-w" => { "Window"
439+ "C-w" | "w" => rotate_view,
440+ "C-h" | "h" => hsplit,
441+ "C-v" | "v" => vsplit,
442+ "C-q" | "q" => wclose,
443+ },
180
444
181 // move under <space>c
445 // move under <space>c
182− ctrl!('c') => Command::toggle_comments,
446+ "C-c" => toggle_comments,
183− key!('K') => Command::hover,
447+ "K" => hover,
184
448
185 // z family for save/restore/combine from/to sels from register
449 // z family for save/restore/combine from/to sels from register
186
450
187− // supposedly ctrl!('i') but did not work
451+ // supposedly "C-i" but did not work
188− key!(Tab) => Command::jump_forward,
452+ "tab" => jump_forward,
189− ctrl!('o') => Command::jump_backward,
453+ "C-o" => jump_backward,
190− // ctrl!('s') => Command::save_selection,
454+ // "C-s" => save_selection,
191−
455+
192− key!(' ') => Command::space_mode,
456+ "space" => { "Space"
193− key!('z') => Command::view_mode,
457+ "f" => file_picker,
458+ "b" => buffer_picker,
459+ "s" => symbol_picker,
460+ "a" => code_action,
461+ "'" => last_picker,
462+ "w" => { "Window"
463+ "C-w" | "w" => rotate_view,
464+ "C-h" | "h" => hsplit,
465+ "C-v" | "v" => vsplit,
466+ "C-q" | "q" => wclose,
467+ },
468+ "y" => yank_joined_to_clipboard,
469+ "Y" => yank_main_selection_to_clipboard,
470+ "p" => paste_clipboard_after,
471+ "P" => paste_clipboard_before,
472+ "R" => replace_selections_with_clipboard,
473+ "space" => keep_primary_selection,
474+ },
475+ "z" => { "View"
476+ "z" | "c" => align_view_center,
477+ "t" => align_view_top,
478+ "b" => align_view_bottom,
479+ "m" => align_view_middle,
480+ "k" => scroll_up,
481+ "j" => scroll_down,
482+ },
194
483
195− key!('"') => Command::select_register,
484+ "\"" => select_register,
196− );
485+ });
197 // TODO: decide whether we want normal mode to also be select mode (kakoune-like), or whether
486 // TODO: decide whether we want normal mode to also be select mode (kakoune-like), or whether
198 // we keep this separate select mode. More keys can fit into normal mode then, but it's weird
487 // we keep this separate select mode. More keys can fit into normal mode then, but it's weird
199 // because some selection operations can now be done from normal mode, some from select mode.
488 // because some selection operations can now be done from normal mode, some from select mode.
200 let mut select = normal.clone();
489 let mut select = normal.clone();
201− select.extend(
490+ select.merge_nodes(keymap!({ "Select mode"
202− hashmap!(
203− key!('h') => Command::extend_char_left,
491+ "h" | "left" => extend_char_left,
204− key!('j') => Command::extend_line_down,
492+ "j" | "down" => extend_line_down,
205− key!('k') => Command::extend_line_up,
493+ "k" | "up" => extend_line_up,
206− key!('l') => Command::extend_char_right,
207−
208− key!(Left) => Command::extend_char_left,
209− key!(Down) => Command::extend_line_down,
210− key!(Up) => Command::extend_line_up,
211− key!(Right) => Command::extend_char_right,
494+ "l" | "right" => extend_char_right,
212−
495+
213− key!('w') => Command::extend_next_word_start,
496+ "w" => extend_next_word_start,
214− key!('b') => Command::extend_prev_word_start,
497+ "b" => extend_prev_word_start,
215− key!('e') => Command::extend_next_word_end,
498+ "e" => extend_next_word_end,
216−
499+
217− key!('t') => Command::extend_till_char,
500+ "t" => extend_till_char,
218− key!('f') => Command::extend_next_char,
501+ "f" => extend_next_char,
219−
220− key!('T') => Command::extend_till_prev_char,
502+ "T" => extend_till_prev_char,
221− key!('F') => Command::extend_prev_char,
503+ "F" => extend_prev_char,
504+
222− key!(Home) => Command::goto_line_start,
505+ "home" => goto_line_start,
223− key!(End) => Command::goto_line_end,
506+ "end" => goto_line_end,
224− key!(Esc) => Command::exit_select_mode,
507+ "esc" => exit_select_mode,
225− )
508+ }));
226− .into_iter(),
509+ let insert = keymap!({ "Insert mode"
510+ "esc" => normal_mode,
511+
512+ "backspace" => delete_char_backward,
513+ "del" => delete_char_forward,
514+ "ret" => insert_newline,
515+ "tab" => insert_tab,
516+ "C-w" => delete_word_backward,
517+
518+ "left" => move_char_left,
519+ "down" => move_line_down,
520+ "up" => move_line_up,
521+ "right" => move_char_right,
522+ "pageup" => page_up,
523+ "pagedown" => page_down,
524+ "home" => goto_line_start,
525+ "end" => goto_line_end_newline,
526+
527+ "C-x" => completion,
227− );
528+ });
228−
229 Keymaps(hashmap!(
529 Keymaps(hashmap!(
230− // as long as you cast the first item, rust is able to infer the other cases
231− // TODO: select could be normal mode with some bindings merged over
232− Mode::Normal => normal,
530+ Mode::Normal => Keymap::new(normal),
233− Mode::Select => select,
531+ Mode::Select => Keymap::new(select),
234− Mode::Insert => hashmap!(
532+ Mode::Insert => Keymap::new(insert),
235− key!(Esc) => Command::normal_mode as Command,
236− key!(Backspace) => Command::delete_char_backward,
237− key!(Delete) => Command::delete_char_forward,
238− key!(Enter) => Command::insert_newline,
239− key!(Tab) => Command::insert_tab,
240− key!(Left) => Command::move_char_left,
241− key!(Down) => Command::move_line_down,
242− key!(Up) => Command::move_line_up,
243− key!(Right) => Command::move_char_right,
244− key!(PageUp) => Command::page_up,
245− key!(PageDown) => Command::page_down,
246− key!(Home) => Command::goto_line_start,
247− key!(End) => Command::goto_line_end_newline,
248− ctrl!('x') => Command::completion,
249− ctrl!('w') => Command::delete_word_backward,
250− ),
251 ))
533 ))
252 }
534 }
253}
535}
254
536
255−/// Merge default config keys with user overwritten keys for custom
537+/// Merge default config keys with user overwritten keys for custom user config.
256−/// user config.
257pub fn merge_keys(mut config: Config) -> Config {
538pub fn merge_keys(mut config: Config) -> Config {
258 let mut delta = std::mem::take(&mut config.keys);
539 let mut delta = std::mem::take(&mut config.keys);
259 for (mode, keys) in &mut *config.keys {
540 for (mode, keys) in &mut *config.keys {
260− keys.extend(delta.remove(mode).unwrap_or_default());
541+ keys.merge(delta.remove(mode).unwrap_or_default())
261 }
542 }
262 config
543 config
263}
544}
264
545
265#[test]
546#[test]
266fn merge_partial_keys() {
547fn merge_partial_keys() {
267− use helix_view::keyboard::{KeyCode, KeyModifiers};
268 let config = Config {
548 let config = Config {
269 keys: Keymaps(hashmap! {
549 keys: Keymaps(hashmap! {
270− Mode::Normal => hashmap! {
550+ Mode::Normal => Keymap::new(
271− KeyEvent {
551+ keymap!({ "Normal mode"
272− code: KeyCode::Char('i'),
273− modifiers: KeyModifiers::NONE,
274− } => Command::normal_mode,
552+ "i" => normal_mode,
275− KeyEvent { // key that does not exist
276− code: KeyCode::Char('无'),
277− modifiers: KeyModifiers::NONE,
278− } => Command::insert_mode,
553+ "无" => insert_mode,
554+ "z" => jump_backward,
555+ "g" => { "Merge into goto mode"
556+ "$" => goto_line_end,
557+ "g" => delete_char_forward,
279− },
558+ },
559+ })
560+ )
280 }),
561 }),
281 ..Default::default()
562 ..Default::default()
282 };
563 };
283− let merged_config = merge_keys(config.clone());
564+ let mut merged_config = merge_keys(config.clone());
284 assert_ne!(config, merged_config);
565 assert_ne!(config, merged_config);
566+
567+ let keymap = merged_config.keys.0.get_mut(&Mode::Normal).unwrap();
285 assert_eq!(
568 assert_eq!(
286− *merged_config
569+ keymap.get(key!('i')),
287− .keys
288− .0
289− .get(&Mode::Normal)
290− .unwrap()
291− .get(&KeyEvent {
292− code: KeyCode::Char('i'),
293− modifiers: KeyModifiers::NONE
294− })
295− .unwrap(),
296− Command::normal_mode
570+ KeymapResult::Matched(Command::normal_mode),
571+ "Leaf should replace leaf"
297 );
572 );
298 assert_eq!(
573 assert_eq!(
299− *merged_config
574+ keymap.get(key!('无')),
300− .keys
301− .0
302− .get(&Mode::Normal)
303− .unwrap()
304− .get(&KeyEvent {
305− code: KeyCode::Char('无'),
306− modifiers: KeyModifiers::NONE
307− })
308− .unwrap(),
309− Command::insert_mode
575+ KeymapResult::Matched(Command::insert_mode),
576+ "New leaf should be present in merged keymap"
310 );
577 );
578+ // Assumes that z is a node in the default keymap
579+ assert_eq!(
580+ keymap.get(key!('z')),
581+ KeymapResult::Matched(Command::jump_backward),
582+ "Leaf should replace node"
583+ );
584+ // Assumes that `g` is a node in default keymap
585+ assert_eq!(
586+ keymap.root().search(&[key!('g'), key!('$')]).unwrap(),
587+ &KeyTrie::Leaf(Command::goto_line_end),
588+ "Leaf should be present in merged subnode"
589+ );
590+ // Assumes that `gg` is in default keymap
591+ assert_eq!(
592+ keymap.root().search(&[key!('g'), key!('g')]).unwrap(),
593+ &KeyTrie::Leaf(Command::delete_char_forward),
594+ "Leaf should replace old leaf in merged subnode"
595+ );
596+ // Assumes that `ge` is in default keymap
597+ assert_eq!(
598+ keymap.root().search(&[key!('g'), key!('e')]).unwrap(),
599+ &KeyTrie::Leaf(Command::goto_file_end),
600+ "Old leaves in subnode should be present in merged node"
601+ );
602+
311 assert!(merged_config.keys.0.get(&Mode::Normal).unwrap().len() > 1);
603 assert!(merged_config.keys.0.get(&Mode::Normal).unwrap().len() > 1);
312 assert!(merged_config.keys.0.get(&Mode::Insert).unwrap().len() > 0);
604 assert!(merged_config.keys.0.get(&Mode::Insert).unwrap().len() > 0);
313}
605}
helix-term/src/ui/editor.rs+57−19
@@ -2,7 +2,7 @@ use crate::{
2 commands,
2 commands,
3 compositor::{Component, Context, EventResult},
3 compositor::{Component, Context, EventResult},
4 key,
4 key,
5− keymap::Keymaps,
5+ keymap::{KeymapResult, Keymaps},
6 ui::{Completion, ProgressSpinners},
6 ui::{Completion, ProgressSpinners},
7};
7};
8
8
@@ -15,6 +15,7 @@ use helix_core::{
15use helix_view::{
15use helix_view::{
16 document::Mode,
16 document::Mode,
17 graphics::{CursorKind, Modifier, Rect, Style},
17 graphics::{CursorKind, Modifier, Rect, Style},
18+ info::Info,
18 input::KeyEvent,
19 input::KeyEvent,
19 keyboard::{KeyCode, KeyModifiers},
20 keyboard::{KeyCode, KeyModifiers},
20 Document, Editor, Theme, View,
21 Document, Editor, Theme, View,
@@ -30,6 +31,7 @@ pub struct EditorView {
30 last_insert: (commands::Command, Vec<KeyEvent>),
31 last_insert: (commands::Command, Vec<KeyEvent>),
31 completion: Option<Completion>,
32 completion: Option<Completion>,
32 spinners: ProgressSpinners,
33 spinners: ProgressSpinners,
34+ pub autoinfo: Option<Info>,
33}
35}
34
36
35const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
37const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
@@ -48,6 +50,7 @@ impl EditorView {
48 last_insert: (commands::Command::normal_mode, Vec::new()),
50 last_insert: (commands::Command::normal_mode, Vec::new()),
49 completion: None,
51 completion: None,
50 spinners: ProgressSpinners::default(),
52 spinners: ProgressSpinners::default(),
53+ autoinfo: None,
51 }
54 }
52 }
55 }
53
56
@@ -594,19 +597,53 @@ impl EditorView {
594 );
597 );
595 }
598 }
596
599
600+ /// Handle events by looking them up in `self.keymaps`. Returns None
601+ /// if event was handled (a command was executed or a subkeymap was
602+ /// activated). Only KeymapResult::{NotFound, Cancelled} is returned
603+ /// otherwise.
604+ fn handle_keymap_event(
605+ &mut self,
606+ mode: Mode,
597− fn insert_mode(&self, cx: &mut commands::Context, event: KeyEvent) {
607+ cxt: &mut commands::Context,
608+ event: KeyEvent,
609+ ) -> Option<KeymapResult> {
610+ self.autoinfo = None;
598− if let Some(command) = self.keymaps[&Mode::Insert].get(&event) {
611+ match self.keymaps.get_mut(&mode).unwrap().get(event) {
599− command.execute(cx);
612+ KeymapResult::Matched(command) => command.execute(cxt),
600− } else if let KeyEvent {
613+ KeymapResult::Pending(node) => self.autoinfo = Some(node.into()),
601− code: KeyCode::Char(ch),
614+ k @ KeymapResult::NotFound | k @ KeymapResult::Cancelled(_) => return Some(k),
602− ..
603− } = event
604− {
605− commands::insert::insert_char(cx, ch);
606 }
615 }
616+ None
607 }
617 }
608
618
619+ fn insert_mode(&mut self, cx: &mut commands::Context, event: KeyEvent) {
620+ if let Some(keyresult) = self.handle_keymap_event(Mode::Insert, cx, event) {
621+ match keyresult {
622+ KeymapResult::NotFound => {
623+ if let Some(ch) = event.char() {
624+ commands::insert::insert_char(cx, ch)
625+ }
626+ }
627+ KeymapResult::Cancelled(pending) => {
628+ for ev in pending {
629+ match ev.char() {
630+ Some(ch) => commands::insert::insert_char(cx, ch),
631+ None => {
632+ if let KeymapResult::Matched(command) =
633+ self.keymaps.get_mut(&Mode::Insert).unwrap().get(ev)
634+ {
635+ command.execute(cx);
636+ }
637+ }
638+ }
639+ }
640+ }
641+ _ => unreachable!(),
642+ }
643+ }
644+ }
645+
609− fn command_mode(&self, mode: Mode, cxt: &mut commands::Context, event: KeyEvent) {
646+ fn command_mode(&mut self, mode: Mode, cxt: &mut commands::Context, event: KeyEvent) {
610 match event {
647 match event {
611 // count handling
648 // count handling
612 key!(i @ '0'..='9') => {
649 key!(i @ '0'..='9') => {
@@ -619,8 +656,8 @@ impl EditorView {
619 // first execute whatever put us into insert mode
656 // first execute whatever put us into insert mode
620 self.last_insert.0.execute(cxt);
657 self.last_insert.0.execute(cxt);
621 // then replay the inputs
658 // then replay the inputs
622− for key in &self.last_insert.1 {
659+ for &key in &self.last_insert.1.clone() {
623− self.insert_mode(cxt, *key)
660+ self.insert_mode(cxt, key)
624 }
661 }
625 }
662 }
626 _ => {
663 _ => {
@@ -633,9 +670,7 @@ impl EditorView {
633 // set the register
670 // set the register
634 cxt.selected_register = cxt.editor.selected_register.take();
671 cxt.selected_register = cxt.editor.selected_register.take();
635
672
636− if let Some(command) = self.keymaps[&mode].get(&event) {
673+ self.handle_keymap_event(mode, cxt, event);
637− command.execute(cxt);
638− }
639 }
674 }
640 }
675 }
641 }
676 }
@@ -749,7 +784,11 @@ impl Component for EditorView {
749 // how we entered insert mode is important, and we should track that so
784 // how we entered insert mode is important, and we should track that so
750 // we can repeat the side effect.
785 // we can repeat the side effect.
751
786
752− self.last_insert.0 = self.keymaps[&mode][&key];
787+ self.last_insert.0 = match self.keymaps.get_mut(&mode).unwrap().get(key) {
788+ KeymapResult::Matched(command) => command,
789+ // FIXME: insert mode can only be entered through single KeyCodes
790+ _ => unimplemented!(),
791+ };
753 self.last_insert.1.clear();
792 self.last_insert.1.clear();
754 }
793 }
755 (Mode::Insert, Mode::Normal) => {
794 (Mode::Insert, Mode::Normal) => {
@@ -787,9 +826,8 @@ impl Component for EditorView {
787 );
826 );
788 }
827 }
789
828
790− if let Some(info) = std::mem::take(&mut cx.editor.autoinfo) {
829+ if let Some(ref info) = self.autoinfo {
791 info.render(area, surface, cx);
830 info.render(area, surface, cx);
792− cx.editor.autoinfo = Some(info);
793 }
831 }
794
832
795 // render status msg
833 // render status msg
helix-term/src/ui/info.rs+1−1
@@ -8,7 +8,7 @@ impl Component for Info {
8 fn render(&self, viewport: Rect, surface: &mut Surface, cx: &mut Context) {
8 fn render(&self, viewport: Rect, surface: &mut Surface, cx: &mut Context) {
9 let style = cx.editor.theme.get("ui.popup");
9 let style = cx.editor.theme.get("ui.popup");
10 let block = Block::default()
10 let block = Block::default()
11− .title(self.title)
11+ .title(self.title.as_str())
12 .borders(Borders::ALL)
12 .borders(Borders::ALL)
13 .border_style(style);
13 .border_style(style);
14 let Info { width, height, .. } = self;
14 let Info { width, height, .. } = self;
helix-term/src/ui/mod.rs+1
@@ -36,6 +36,7 @@ pub fn regex_prompt(
36
36
37 Prompt::new(
37 Prompt::new(
38 prompt,
38 prompt,
39+ None,
39 |_input: &str| Vec::new(), // this is fine because Vec::new() doesn't allocate
40 |_input: &str| Vec::new(), // this is fine because Vec::new() doesn't allocate
40 move |cx: &mut crate::compositor::Context, input: &str, event: PromptEvent| {
41 move |cx: &mut crate::compositor::Context, input: &str, event: PromptEvent| {
41 match event {
42 match event {
helix-term/src/ui/picker.rs+1
@@ -43,6 +43,7 @@ impl<T> Picker<T> {
43 ) -> Self {
43 ) -> Self {
44 let prompt = Prompt::new(
44 let prompt = Prompt::new(
45 "".to_string(),
45 "".to_string(),
46+ None,
46 |_pattern: &str| Vec::new(),
47 |_pattern: &str| Vec::new(),
47 |_editor: &mut Context, _pattern: &str, _event: PromptEvent| {
48 |_editor: &mut Context, _pattern: &str, _event: PromptEvent| {
48 //
49 //
helix-term/src/ui/prompt.rs+58
@@ -20,6 +20,8 @@ pub struct Prompt {
20 cursor: usize,
20 cursor: usize,
21 completion: Vec<Completion>,
21 completion: Vec<Completion>,
22 selection: Option<usize>,
22 selection: Option<usize>,
23+ history_register: Option<char>,
24+ history_pos: Option<usize>,
23 completion_fn: Box<dyn FnMut(&str) -> Vec<Completion>>,
25 completion_fn: Box<dyn FnMut(&str) -> Vec<Completion>>,
24 callback_fn: Box<dyn FnMut(&mut Context, &str, PromptEvent)>,
26 callback_fn: Box<dyn FnMut(&mut Context, &str, PromptEvent)>,
25 pub doc_fn: Box<dyn Fn(&str) -> Option<&'static str>>,
27 pub doc_fn: Box<dyn Fn(&str) -> Option<&'static str>>,
@@ -54,6 +56,7 @@ pub enum Movement {
54impl Prompt {
56impl Prompt {
55 pub fn new(
57 pub fn new(
56 prompt: String,
58 prompt: String,
59+ history_register: Option<char>,
57 mut completion_fn: impl FnMut(&str) -> Vec<Completion> + 'static,
60 mut completion_fn: impl FnMut(&str) -> Vec<Completion> + 'static,
58 callback_fn: impl FnMut(&mut Context, &str, PromptEvent) + 'static,
61 callback_fn: impl FnMut(&mut Context, &str, PromptEvent) + 'static,
59 ) -> Self {
62 ) -> Self {
@@ -63,6 +66,8 @@ impl Prompt {
63 cursor: 0,
66 cursor: 0,
64 completion: completion_fn(""),
67 completion: completion_fn(""),
65 selection: None,
68 selection: None,
69+ history_register,
70+ history_pos: None,
66 completion_fn: Box::new(completion_fn),
71 completion_fn: Box::new(completion_fn),
67 callback_fn: Box::new(callback_fn),
72 callback_fn: Box::new(callback_fn),
68 doc_fn: Box::new(|_| None),
73 doc_fn: Box::new(|_| None),
@@ -226,6 +231,28 @@ impl Prompt {
226 self.exit_selection();
231 self.exit_selection();
227 }
232 }
228
233
234+ pub fn change_history(&mut self, register: &[String], direction: CompletionDirection) {
235+ if register.is_empty() {
236+ return;
237+ }
238+
239+ let end = register.len().saturating_sub(1);
240+
241+ let index = match direction {
242+ CompletionDirection::Forward => self.history_pos.map_or(0, |i| i + 1),
243+ CompletionDirection::Backward => {
244+ self.history_pos.unwrap_or(register.len()).saturating_sub(1)
245+ }
246+ }
247+ .min(end);
248+
249+ self.line = register[index].clone();
250+
251+ self.history_pos = Some(index);
252+
253+ self.move_end();
254+ }
255+
229 pub fn change_completion_selection(&mut self, direction: CompletionDirection) {
256 pub fn change_completion_selection(&mut self, direction: CompletionDirection) {
230 if self.completion.is_empty() {
257 if self.completion.is_empty() {
231 return;
258 return;
@@ -468,9 +495,40 @@ impl Component for Prompt {
468 self.exit_selection();
495 self.exit_selection();
469 } else {
496 } else {
470 (self.callback_fn)(cx, &self.line, PromptEvent::Validate);
497 (self.callback_fn)(cx, &self.line, PromptEvent::Validate);
498+
499+ if let Some(register) = self.history_register {
500+ // store in history
501+ let register = cx.editor.registers.get_mut(register);
502+ register.push(self.line.clone());
503+ }
471 return close_fn;
504 return close_fn;
472 }
505 }
473 }
506 }
507+ KeyEvent {
508+ code: KeyCode::Char('p'),
509+ modifiers: KeyModifiers::CONTROL,
510+ }
511+ | KeyEvent {
512+ code: KeyCode::Up, ..
513+ } => {
514+ if let Some(register) = self.history_register {
515+ let register = cx.editor.registers.get_mut(register);
516+ self.change_history(register.read(), CompletionDirection::Backward);
517+ }
518+ }
519+ KeyEvent {
520+ code: KeyCode::Char('n'),
521+ modifiers: KeyModifiers::CONTROL,
522+ }
523+ | KeyEvent {
524+ code: KeyCode::Down,
525+ ..
526+ } => {
527+ if let Some(register) = self.history_register {
528+ let register = cx.editor.registers.get_mut(register);
529+ self.change_history(register.read(), CompletionDirection::Forward);
530+ }
531+ }
474 KeyEvent {
532 KeyEvent {
475 code: KeyCode::Tab, ..
533 code: KeyCode::Tab, ..
476 } => self.change_completion_selection(CompletionDirection::Forward),
534 } => self.change_completion_selection(CompletionDirection::Forward),
helix-view/src/editor.rs−3
@@ -1,7 +1,6 @@
1use crate::{
1use crate::{
2 clipboard::{get_clipboard_provider, ClipboardProvider},
2 clipboard::{get_clipboard_provider, ClipboardProvider},
3 graphics::{CursorKind, Rect},
3 graphics::{CursorKind, Rect},
4− info::Info,
5 theme::{self, Theme},
4 theme::{self, Theme},
6 tree::Tree,
5 tree::Tree,
7 Document, DocumentId, RegisterSelection, View, ViewId,
6 Document, DocumentId, RegisterSelection, View, ViewId,
@@ -33,7 +32,6 @@ pub struct Editor {
33 pub syn_loader: Arc<syntax::Loader>,
32 pub syn_loader: Arc<syntax::Loader>,
34 pub theme_loader: Arc<theme::Loader>,
33 pub theme_loader: Arc<theme::Loader>,
35
34
36− pub autoinfo: Option<&'static Info>,
37 pub status_msg: Option<(String, Severity)>,
35 pub status_msg: Option<(String, Severity)>,
38}
36}
39
37
@@ -67,7 +65,6 @@ impl Editor {
67 theme_loader: themes,
65 theme_loader: themes,
68 registers: Registers::default(),
66 registers: Registers::default(),
69 clipboard_provider: get_clipboard_provider(),
67 clipboard_provider: get_clipboard_provider(),
70− autoinfo: None,
71 status_msg: None,
68 status_msg: None,
72 }
69 }
73 }
70 }
helix-view/src/info.rs+10−7
@@ -5,9 +5,9 @@ use std::fmt::Write;
5#[derive(Debug)]
5#[derive(Debug)]
6/// Info box used in editor. Rendering logic will be in other crate.
6/// Info box used in editor. Rendering logic will be in other crate.
7pub struct Info {
7pub struct Info {
8− /// Title kept as static str for now.
8+ /// Title shown at top.
9− pub title: &'static str,
9+ pub title: String,
10− /// Text body, should contains newline.
10+ /// Text body, should contain newlines.
11 pub text: String,
11 pub text: String,
12 /// Body width.
12 /// Body width.
13 pub width: u16,
13 pub width: u16,
@@ -16,17 +16,20 @@ pub struct Info {
16}
16}
17
17
18impl Info {
18impl Info {
19+ // body is a BTreeMap instead of a HashMap because keymaps are represented
20+ // with nested hashmaps with no ordering, and each invocation of infobox would
21+ // show different orders of items
19− pub fn key(title: &'static str, body: Vec<(&[KeyEvent], &'static str)>) -> Info {
22+ pub fn key(title: &str, body: Vec<(&str, Vec<KeyEvent>)>) -> Info {
20 let (lpad, mpad, rpad) = (1, 2, 1);
23 let (lpad, mpad, rpad) = (1, 2, 1);
21 let keymaps_width: u16 = body
24 let keymaps_width: u16 = body
22 .iter()
25 .iter()
23− .map(|r| r.0.iter().map(|e| e.width() as u16 + 2).sum::<u16>() - 2)
26+ .map(|r| r.1.iter().map(|e| e.width() as u16 + 2).sum::<u16>() - 2)
24 .max()
27 .max()
25 .unwrap();
28 .unwrap();
26 let mut text = String::new();
29 let mut text = String::new();
27 let mut width = 0;
30 let mut width = 0;
28 let height = body.len() as u16;
31 let height = body.len() as u16;
29− for (keyevents, desc) in body {
32+ for (desc, keyevents) in body {
30 let keyevent = keyevents[0];
33 let keyevent = keyevents[0];
31 let mut left = keymaps_width - keyevent.width() as u16;
34 let mut left = keymaps_width - keyevent.width() as u16;
32 for _ in 0..lpad {
35 for _ in 0..lpad {
@@ -48,7 +51,7 @@ impl Info {
48 writeln!(text, "{}", desc).ok();
51 writeln!(text, "{}", desc).ok();
49 }
52 }
50 Info {
53 Info {
51− title,
54+ title: title.to_string(),
52 text,
55 text,
53 width,
56 width,
54 height,
57 height,
helix-view/src/input.rs+11−1
@@ -14,6 +14,16 @@ pub struct KeyEvent {
14 pub modifiers: KeyModifiers,
14 pub modifiers: KeyModifiers,
15}
15}
16
16
17+impl KeyEvent {
18+ /// Get only the character involved in this event
19+ pub fn char(&self) -> Option<char> {
20+ match self.code {
21+ KeyCode::Char(ch) => Some(ch),
22+ _ => None,
23+ }
24+ }
25+}
26+
17pub(crate) mod keys {
27pub(crate) mod keys {
18 pub(crate) const BACKSPACE: &str = "backspace";
28 pub(crate) const BACKSPACE: &str = "backspace";
19 pub(crate) const ENTER: &str = "ret";
29 pub(crate) const ENTER: &str = "ret";
@@ -168,7 +178,7 @@ impl std::str::FromStr for KeyEvent {
168 keys::MINUS => KeyCode::Char('-'),
178 keys::MINUS => KeyCode::Char('-'),
169 keys::SEMICOLON => KeyCode::Char(';'),
179 keys::SEMICOLON => KeyCode::Char(';'),
170 keys::PERCENT => KeyCode::Char('%'),
180 keys::PERCENT => KeyCode::Char('%'),
171− single if single.len() == 1 => KeyCode::Char(single.chars().next().unwrap()),
181+ single if single.chars().count() == 1 => KeyCode::Char(single.chars().next().unwrap()),
172 function if function.len() > 1 && function.starts_with('F') => {
182 function if function.len() > 1 && function.starts_with('F') => {
173 let function: String = function.chars().skip(1).collect();
183 let function: String = function.chars().skip(1).collect();
174 let function = str::parse::<u8>(&function)?;
184 let function = str::parse::<u8>(&function)?;
runtime/queries/rust/indents.toml+3−7
@@ -1,11 +1,4 @@
1indent = [
1indent = [
2− "while_expression",
3− "for_expression",
4− "loop_expression",
5− "if_expression",
6− "if_let_expression",
7− "tuple_expression",
8− "array_expression",
9 "use_list",
2 "use_list",
10 "block",
3 "block",
11 "match_block",
4 "match_block",
@@ -17,8 +10,11 @@ indent = [
17 "struct_pattern",
10 "struct_pattern",
18 "tuple_pattern",
11 "tuple_pattern",
19 "enum_variant_list",
12 "enum_variant_list",
13+ "call_expression",
20 "binary_expression",
14 "binary_expression",
21 "field_expression",
15 "field_expression",
16+ "tuple_expression",
17+ "array_expression",
22 "where_clause",
18 "where_clause",
23 "macro_invocation"
19 "macro_invocation"
24]
20]
runtime/themes/bogster.toml+1
@@ -42,6 +42,7 @@
42
42
43"ui.selection" = { bg = "#313f4e" }
43"ui.selection" = { bg = "#313f4e" }
44# "ui.cursor.match" # TODO might want to override this because dimmed is not widely supported
44# "ui.cursor.match" # TODO might want to override this because dimmed is not widely supported
45+"ui.cursor.primary" = { fg = "#ABB2BF", modifiers = ["reversed"] }
45"ui.menu.selected" = { fg = "#e5ded6", bg = "#313f4e" }
46"ui.menu.selected" = { fg = "#e5ded6", bg = "#313f4e" }
46
47
47"warning" = "#dc7759"
48"warning" = "#dc7759"