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