From 9ac0c951615d624041836cdd7afd869391c72fff Mon Sep 17 00:00:00 2001 From: CossonLeo Date: Mon, 18 Oct 2021 14:14:50 +0800 Subject: Improve completion trigger (#838) * improve idle completion trigger * add completion-trigger-len to book * rename semantics_completion to language_server_completion and optimize idle completion trigger--- helix-term/src/application.rs | 4 ++-- helix-term/src/commands.rs | 39 +++++++++++++++++++++++++++------------ 2 files changed, 29 insertions(+), 14 deletions(-) (limited to 'helix-term/src') diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 0e7d0e55..a7281ecf 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -235,7 +235,7 @@ impl Application { } pub fn handle_idle_timeout(&mut self) { - use crate::commands::{completion, Context}; + use crate::commands::{insert::idle_completion, Context}; use helix_view::document::Mode; if doc_mut!(self.editor).mode != Mode::Insert || !self.config.editor.auto_completion { @@ -262,7 +262,7 @@ impl Application { callback: None, on_next_key_callback: None, }; - completion(&mut cx); + idle_completion(&mut cx); self.render(); } diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 95c46a4e..2811f293 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -3308,7 +3308,26 @@ pub mod insert { pub type Hook = fn(&Rope, &Selection, char) -> Option; pub type PostHook = fn(&mut Context, char); - fn completion(cx: &mut Context, ch: char) { + // It trigger completion when idle timer reaches deadline + // Only trigger completion if the word under cursor is longer than n characters + pub fn idle_completion(cx: &mut Context) { + let (view, doc) = current!(cx.editor); + let text = doc.text().slice(..); + let cursor = doc.selection(view.id).primary().cursor(text); + + use helix_core::chars::char_is_word; + let mut iter = text.chars_at(cursor); + iter.reverse(); + for _ in 0..cx.editor.config.completion_trigger_len { + match iter.next() { + Some(c) if char_is_word(c) => {} + _ => return, + } + } + super::completion(cx); + } + + fn language_server_completion(cx: &mut Context, ch: char) { // if ch matches completion char, trigger completion let doc = doc_mut!(cx.editor); let language_server = match doc.language_server() { @@ -3318,19 +3337,14 @@ pub mod insert { let capabilities = language_server.capabilities(); - if let lsp::ServerCapabilities { - completion_provider: - Some(lsp::CompletionOptions { - trigger_characters: Some(triggers), - .. - }), + if let Some(lsp::CompletionOptions { + trigger_characters: Some(triggers), .. - } = capabilities + }) = &capabilities.completion_provider { // TODO: what if trigger is multiple chars long - let is_trigger = triggers.iter().any(|trigger| trigger.contains(ch)); - - if is_trigger { + if triggers.iter().any(|trigger| trigger.contains(ch)) { + cx.editor.clear_idle_timer(); super::completion(cx); } } @@ -3412,7 +3426,8 @@ pub mod insert { // TODO: need a post insert hook too for certain triggers (autocomplete, signature help, etc) // this could also generically look at Transaction, but it's a bit annoying to look at // Operation instead of Change. - for hook in &[completion, signature_help] { + for hook in &[language_server_completion, signature_help] { + // for hook in &[signature_help] { hook(cx, c); } } -- cgit v1.2.3-70-g09d2 From 67829976faca1c2fdeb3964c25cb35bc41f0b8df Mon Sep 17 00:00:00 2001 From: VuiMuich Date: Tue, 19 Oct 2021 11:37:38 +0200 Subject: Add `C-j` and `C-k` to keybinds for picker (#876) * Add `C-j` and `C-k` for moving down/up in pickers * Add new binds to keymap doc--- book/src/keymap.md | 18 +++++++++--------- helix-term/src/ui/picker.rs | 8 ++++++++ 2 files changed, 17 insertions(+), 9 deletions(-) (limited to 'helix-term/src') diff --git a/book/src/keymap.md b/book/src/keymap.md index 156b1d99..1e19a4eb 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -242,12 +242,12 @@ commands (including goto) to extend the existing selection instead of replacing Keys to use within picker. Remapping currently not supported. -| Key | Description | -| ----- | ------------- | -| `Up`, `Ctrl-p` | Previous entry | -| `Down`, `Ctrl-n` | Next entry | -| `Ctrl-space` | Filter options | -| `Enter` | Open selected | -| `Ctrl-h` | Open horizontally | -| `Ctrl-v` | Open vertically | -| `Escape`, `Ctrl-c` | Close picker | +| Key | Description | +| ----- | ------------- | +| `Up`, `Ctrl-k`, `Ctrl-p` | Previous entry | +| `Down`, `Ctrl-j`, `Ctrl-n` | Next entry | +| `Ctrl-space` | Filter options | +| `Enter` | Open selected | +| `Ctrl-h` | Open horizontally | +| `Ctrl-v` | Open vertically | +| `Escape`, `Ctrl-c` | Close picker | diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 9be2a73e..6f584178 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -336,6 +336,10 @@ impl Component for Picker { code: KeyCode::BackTab, .. } + | KeyEvent { + code: KeyCode::Char('k'), + modifiers: KeyModifiers::CONTROL, + } | KeyEvent { code: KeyCode::Char('p'), modifiers: KeyModifiers::CONTROL, @@ -349,6 +353,10 @@ impl Component for Picker { | KeyEvent { code: KeyCode::Tab, .. } + | KeyEvent { + code: KeyCode::Char('j'), + modifiers: KeyModifiers::CONTROL, + } | KeyEvent { code: KeyCode::Char('n'), modifiers: KeyModifiers::CONTROL, -- cgit v1.2.3-70-g09d2 From e9b23c29d8e8bca1d4140d02350bdae824a651b0 Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Wed, 20 Oct 2021 00:00:28 +0900 Subject: Ignore errors when disabling mouse capture --- helix-term/src/application.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'helix-term/src') diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index a7281ecf..82ad04d7 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -563,7 +563,9 @@ impl Application { let mut stdout = stdout(); // reset cursor shape write!(stdout, "\x1B[2 q")?; - execute!(stdout, DisableMouseCapture)?; + // Ignore errors on disabling, this might trigger on windows if we call + // disable without calling enable previously + let _ = execute!(stdout, DisableMouseCapture); execute!(stdout, terminal::LeaveAlternateScreen)?; terminal::disable_raw_mode()?; Ok(()) -- cgit v1.2.3-70-g09d2 From b1ebd7a07e55f8ff5b8ef9fcbf09940a9f4a4f39 Mon Sep 17 00:00:00 2001 From: radical3dd Date: Thu, 21 Oct 2021 02:44:53 +0200 Subject: Replace current selection with all yanked values. (#882) --- helix-term/src/commands.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) (limited to 'helix-term/src') diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 2811f293..9f54292d 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -3782,11 +3782,21 @@ fn replace_with_yanked(cx: &mut Context) { let registers = &mut cx.editor.registers; if let Some(values) = registers.read(reg_name) { - if let Some(yank) = values.first() { + if !values.is_empty() { + let repeat = std::iter::repeat( + values + .last() + .map(|value| Tendril::from_slice(value)) + .unwrap(), + ); + let mut values = values + .iter() + .map(|value| Tendril::from_slice(value)) + .chain(repeat); let selection = doc.selection(view.id); let transaction = Transaction::change_by_selection(doc.text(), selection, |range| { if !range.is_empty() { - (range.from(), range.to(), Some(yank.as_str().into())) + (range.from(), range.to(), Some(values.next().unwrap())) } else { (range.from(), range.to(), None) } -- cgit v1.2.3-70-g09d2 From 3b032e8e1fd342261b153aeb375f9c0e8d882b34 Mon Sep 17 00:00:00 2001 From: Daniel S Poulin Date: Thu, 21 Oct 2021 21:02:05 -0400 Subject: First stab at ignoring compressed files from picker (#767) --- helix-term/src/ui/mod.rs | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) (limited to 'helix-term/src') diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 810a9966..30a9ec6b 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -91,9 +91,25 @@ pub fn regex_prompt( } pub fn file_picker(root: PathBuf) -> FilePicker { - use ignore::Walk; + use ignore::{types::TypesBuilder, WalkBuilder}; use std::time; - let files = Walk::new(&root).filter_map(|entry| { + + // We want to exclude files that the editor can't handle yet + let mut type_builder = TypesBuilder::new(); + let mut walk_builder = WalkBuilder::new(&root); + let walk_builder = match type_builder.add( + "compressed", + "*.{zip,gz,bz2,zst,lzo,sz,tgz,tbz2,lz,lz4,lzma,lzo,z,Z,xz,7z,rar,cab}", + ) { + Err(_) => &walk_builder, + _ => { + type_builder.negate("all"); + let excluded_types = type_builder.build().unwrap(); + walk_builder.types(excluded_types) + } + }; + + let files = walk_builder.build().filter_map(|entry| { let entry = entry.ok()?; // Path::is_dir() traverses symlinks, so we use it over DirEntry::is_dir if entry.path().is_dir() { -- cgit v1.2.3-70-g09d2 From 182a59b5528075c0171756bff71275db8a7995f0 Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Fri, 22 Oct 2021 12:07:41 +0900 Subject: Update to rust 1.56 + 2021 edition --- flake.lock | 68 ++++++++++++++++++++++++++++++++++----------- flake.nix | 2 ++ helix-core/Cargo.toml | 2 +- helix-lsp/Cargo.toml | 2 +- helix-syntax/Cargo.toml | 2 +- helix-term/Cargo.toml | 2 +- helix-term/src/ui/menu.rs | 32 ++++++++++----------- helix-term/src/ui/picker.rs | 23 +++++---------- helix-tui/Cargo.toml | 2 +- helix-view/Cargo.toml | 2 +- helix-view/src/theme.rs | 1 - 11 files changed, 82 insertions(+), 56 deletions(-) (limited to 'helix-term/src') diff --git a/flake.lock b/flake.lock index 21e44c6e..2029d580 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "devshell": { "locked": { - "lastModified": 1630239564, - "narHash": "sha256-lv7atkVE1+dFw0llmzONsbSIo5ao85KpNSRoFk4K8vU=", + "lastModified": 1632436039, + "narHash": "sha256-OtITeVWcKXn1SpVEnImpTGH91FycCskGBPqmlxiykv4=", "owner": "numtide", "repo": "devshell", - "rev": "bd86d3a2bb28ce4d223315e0eca0d59fef8a0a73", + "rev": "7a7a7aa0adebe5488e5abaec688fd9ae0f8ea9c6", "type": "github" }, "original": { @@ -15,6 +15,21 @@ "type": "github" } }, + "flake-utils": { + "locked": { + "lastModified": 1623875721, + "narHash": "sha256-A8BU7bjS5GirpAUv4QA+QnJ4CceLHkcXdRp4xITDB0s=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "f7e004a55b120c02ecb6219596820fcd32ca8772", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, "flakeCompat": { "flake": false, "locked": { @@ -37,14 +52,16 @@ "nixpkgs": [ "nixpkgs" ], - "rustOverlay": "rustOverlay" + "rustOverlay": [ + "rust-overlay" + ] }, "locked": { - "lastModified": 1631254163, - "narHash": "sha256-8+nOGLH1fXwWnNMTQq/Igk434BzZF5Vld45xLDLiNDQ=", + "lastModified": 1634796585, + "narHash": "sha256-CW4yx6omk5qCXUIwXHp/sztA7u0SpyLq9NEACPnkiz8=", "owner": "yusdacra", "repo": "nix-cargo-integration", - "rev": "432d8504a32232e8d74710024d5bf5cc31767651", + "rev": "a84a2137a396f303978f1d48341e0390b0e16a8b", "type": "github" }, "original": { @@ -55,11 +72,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1631206977, - "narHash": "sha256-o3Dct9aJ5ht5UaTUBzXrRcK1RZt2eG5/xSlWJuUCVZM=", + "lastModified": 1634782485, + "narHash": "sha256-psfh4OQSokGXG0lpq3zKFbhOo3QfoeudRcaUnwMRkQo=", "owner": "nixos", "repo": "nixpkgs", - "rev": "4f6d8095fd51954120a1d08ea5896fe42dc3923b", + "rev": "34ad3ffe08adfca17fcb4e4a47bb5f3b113687be", "type": "github" }, "original": { @@ -69,21 +86,40 @@ "type": "github" } }, + "nixpkgs_2": { + "locked": { + "lastModified": 1628186154, + "narHash": "sha256-r2d0wvywFnL9z4iptztdFMhaUIAaGzrSs7kSok0PgmE=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "06552b72346632b6943c8032e57e702ea12413bf", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "nixpkgs", + "type": "github" + } + }, "root": { "inputs": { "flakeCompat": "flakeCompat", "nixCargoIntegration": "nixCargoIntegration", - "nixpkgs": "nixpkgs" + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay" } }, - "rustOverlay": { - "flake": false, + "rust-overlay": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs_2" + }, "locked": { - "lastModified": 1631240108, - "narHash": "sha256-ffsTkAGyQLxu4E28nVcqwc8xFL/H1UEwrRw2ITI43Aw=", + "lastModified": 1634869268, + "narHash": "sha256-RVAcEFlFU3877Mm4q/nbXGEYTDg/wQNhzmXGMTV6wBs=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "3a29d5e726b855d9463eb5dfe04f1ec14d413289", + "rev": "c02c2d86354327317546501af001886fbb53d374", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index bcc9383e..c1145268 100644 --- a/flake.nix +++ b/flake.nix @@ -3,9 +3,11 @@ inputs = { nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + rust-overlay.url = "github:oxalica/rust-overlay"; nixCargoIntegration = { url = "github:yusdacra/nix-cargo-integration"; inputs.nixpkgs.follows = "nixpkgs"; + inputs.rustOverlay.follows = "rust-overlay"; }; flakeCompat = { url = "github:edolstra/flake-compat"; diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml index 51096453..93ebb133 100644 --- a/helix-core/Cargo.toml +++ b/helix-core/Cargo.toml @@ -2,7 +2,7 @@ name = "helix-core" version = "0.4.1" authors = ["Blaž Hrastnik "] -edition = "2018" +edition = "2021" license = "MPL-2.0" description = "Helix editor core editing primitives" categories = ["editor"] diff --git a/helix-lsp/Cargo.toml b/helix-lsp/Cargo.toml index b4c8c139..455407ad 100644 --- a/helix-lsp/Cargo.toml +++ b/helix-lsp/Cargo.toml @@ -2,7 +2,7 @@ name = "helix-lsp" version = "0.4.1" authors = ["Blaž Hrastnik "] -edition = "2018" +edition = "2021" license = "MPL-2.0" description = "LSP client implementation for Helix project" categories = ["editor"] diff --git a/helix-syntax/Cargo.toml b/helix-syntax/Cargo.toml index 9c2b8275..122fa460 100644 --- a/helix-syntax/Cargo.toml +++ b/helix-syntax/Cargo.toml @@ -2,7 +2,7 @@ name = "helix-syntax" version = "0.4.1" authors = ["Blaž Hrastnik "] -edition = "2018" +edition = "2021" license = "MPL-2.0" description = "Tree-sitter grammars support" categories = ["editor"] diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index 244d3c13..78afab01 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -3,7 +3,7 @@ name = "helix-term" version = "0.4.1" description = "A post-modern text editor." authors = ["Blaž Hrastnik "] -edition = "2018" +edition = "2021" license = "MPL-2.0" categories = ["editor", "command-line-utilities"] repository = "https://github.com/helix-editor/helix" diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs index 055593fd..1130089d 100644 --- a/helix-term/src/ui/menu.rs +++ b/helix-term/src/ui/menu.rs @@ -64,25 +64,23 @@ impl Menu { } pub fn score(&mut self, pattern: &str) { - // need to borrow via pattern match otherwise it complains about simultaneous borrow - let Self { - ref mut matcher, - ref mut matches, - ref options, - .. - } = *self; - // reuse the matches allocation - matches.clear(); - matches.extend(options.iter().enumerate().filter_map(|(index, option)| { - let text = option.filter_text(); - // TODO: using fuzzy_indices could give us the char idx for match highlighting - matcher - .fuzzy_match(text, pattern) - .map(|score| (index, score)) - })); + self.matches.clear(); + self.matches.extend( + self.options + .iter() + .enumerate() + .filter_map(|(index, option)| { + let text = option.filter_text(); + // TODO: using fuzzy_indices could give us the char idx for match highlighting + self.matcher + .fuzzy_match(text, pattern) + .map(|score| (index, score)) + }), + ); // matches.sort_unstable_by_key(|(_, score)| -score); - matches.sort_unstable_by_key(|(index, _score)| options[*index].sort_text()); + self.matches + .sort_unstable_by_key(|(index, _score)| self.options[*index].sort_text()); // reset cursor position self.cursor = None; diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 6f584178..1f08cf13 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -233,37 +233,28 @@ impl Picker { } pub fn score(&mut self) { - // need to borrow via pattern match otherwise it complains about simultaneous borrow - let Self { - ref mut matcher, - ref mut matches, - ref filters, - ref format_fn, - .. - } = *self; - let pattern = &self.prompt.line; // reuse the matches allocation - matches.clear(); - matches.extend( + self.matches.clear(); + self.matches.extend( self.options .iter() .enumerate() .filter_map(|(index, option)| { // filter options first before matching - if !filters.is_empty() { - filters.binary_search(&index).ok()?; + if !self.filters.is_empty() { + self.filters.binary_search(&index).ok()?; } // TODO: maybe using format_fn isn't the best idea here - let text = (format_fn)(option); + let text = (self.format_fn)(option); // TODO: using fuzzy_indices could give us the char idx for match highlighting - matcher + self.matcher .fuzzy_match(&text, pattern) .map(|score| (index, score)) }), ); - matches.sort_unstable_by_key(|(_, score)| -score); + self.matches.sort_unstable_by_key(|(_, score)| -score); // reset cursor position self.cursor = 0; diff --git a/helix-tui/Cargo.toml b/helix-tui/Cargo.toml index 2b42d299..f0c0d7e2 100644 --- a/helix-tui/Cargo.toml +++ b/helix-tui/Cargo.toml @@ -5,7 +5,7 @@ authors = ["Blaž Hrastnik "] description = """ A library to build rich terminal user interfaces or dashboards """ -edition = "2018" +edition = "2021" license = "MPL-2.0" categories = ["editor"] repository = "https://github.com/helix-editor/helix" diff --git a/helix-view/Cargo.toml b/helix-view/Cargo.toml index fce3fdd1..ef09b964 100644 --- a/helix-view/Cargo.toml +++ b/helix-view/Cargo.toml @@ -2,7 +2,7 @@ name = "helix-view" version = "0.4.1" authors = ["Blaž Hrastnik "] -edition = "2018" +edition = "2021" license = "MPL-2.0" description = "UI abstractions for use in backends" categories = ["editor"] diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs index 9c33685b..757316bd 100644 --- a/helix-view/src/theme.rs +++ b/helix-view/src/theme.rs @@ -1,6 +1,5 @@ use std::{ collections::HashMap, - convert::TryFrom, path::{Path, PathBuf}, }; -- cgit v1.2.3-70-g09d2 From 96945be1a8d551f09865f13a7d8972174dbbc1c8 Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Fri, 22 Oct 2021 12:46:51 +0900 Subject: Fix doctest broken on 2021 edition --- helix-term/src/compositor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'helix-term/src') diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs index cad1df05..dc8b91d7 100644 --- a/helix-term/src/compositor.rs +++ b/helix-term/src/compositor.rs @@ -207,7 +207,7 @@ pub trait AnyComponent { /// /// ```rust /// use helix_term::{ui::Text, compositor::Component}; - /// let boxed: Box = Box::new(Text::new("text".to_string())); + /// let boxed: Box = Box::new(Text::new("text".to_string())); /// let text: Box = boxed.as_boxed_any().downcast().unwrap(); /// ``` fn as_boxed_any(self: Box) -> Box; -- cgit v1.2.3-70-g09d2 From 4ee92cad19cc94f0751f91fa9391d1899353d740 Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Sat, 23 Oct 2021 08:11:19 +0530 Subject: Add treesitter textobjects (#728) * Add treesitter textobject queries Only for Go, Python and Rust for now. * Add tree-sitter textobjects Only has functions and class objects as of now. * Fix tests * Add docs for tree-sitter textobjects * Add guide for creating new textobject queries * Add parameter textobject Only parameter.inside is implemented now, parameter.around will probably require custom predicates akin to nvim' `make-range` since we want to select a trailing comma too (a comma will be an anonymous node and matching against them doesn't work similar to named nodes) * Simplify TextObject cell init--- book/src/SUMMARY.md | 2 ++ book/src/guides/README.md | 4 +++ book/src/guides/textobject.md | 30 ++++++++++++++++++++ book/src/usage.md | 13 +++++++-- helix-core/src/indent.rs | 1 + helix-core/src/syntax.rs | 43 ++++++++++++++++++++++++++-- helix-core/src/textobject.rs | 51 ++++++++++++++++++++++++++++++++++ helix-term/src/commands.rs | 19 +++++++++++++ runtime/queries/go/textobjects.scm | 21 ++++++++++++++ runtime/queries/python/textobjects.scm | 14 ++++++++++ runtime/queries/rust/textobjects.scm | 26 +++++++++++++++++ 11 files changed, 219 insertions(+), 5 deletions(-) create mode 100644 book/src/guides/README.md create mode 100644 book/src/guides/textobject.md create mode 100644 runtime/queries/go/textobjects.scm create mode 100644 runtime/queries/python/textobjects.scm create mode 100644 runtime/queries/rust/textobjects.scm (limited to 'helix-term/src') diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 3fa8e067..56f50e21 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -8,3 +8,5 @@ - [Keymap](./keymap.md) - [Key Remapping](./remapping.md) - [Hooks](./hooks.md) +- [Guides](./guides/README.md) + - [Adding Textobject Queries](./guides/textobject.md) diff --git a/book/src/guides/README.md b/book/src/guides/README.md new file mode 100644 index 00000000..96e62978 --- /dev/null +++ b/book/src/guides/README.md @@ -0,0 +1,4 @@ +# Guides + +This section contains guides for adding new language server configurations, +tree-sitter grammers, textobject queries, etc. diff --git a/book/src/guides/textobject.md b/book/src/guides/textobject.md new file mode 100644 index 00000000..50b3b574 --- /dev/null +++ b/book/src/guides/textobject.md @@ -0,0 +1,30 @@ +# Adding Textobject Queries + +Textobjects that are language specific ([like functions, classes, etc][textobjects]) +require an accompanying tree-sitter grammar and a `textobjects.scm` query file +to work properly. Tree-sitter allows us to query the source code syntax tree +and capture specific parts of it. The queries are written in a lisp dialect. +More information on how to write queries can be found in the [official tree-sitter +documentation](tree-sitter-queries). + +Query files should be placed in `runtime/queries/{language}/textobjects.scm` +when contributing. Note that to test the query files locally you should put +them under your local runtime directory (`~/.config/helix/runtime` on Linux +for example). + +The following [captures][tree-sitter-captures] are recognized: + +| Capture Name | +| --- | +| `function.inside` | +| `function.around` | +| `class.inside` | +| `class.around` | +| `parameter.inside` | + +[Example query files][textobject-examples] can be found in the helix GitHub repository. + +[textobjects]: ../usage.md#textobjects +[tree-sitter-queries]: https://tree-sitter.github.io/tree-sitter/using-parsers#query-syntax +[tree-sitter-captures]: https://tree-sitter.github.io/tree-sitter/using-parsers#capturing-nodes +[textobject-examples]: https://github.com/search?q=repo%3Ahelix-editor%2Fhelix+filename%3Atextobjects.scm&type=Code&ref=advsearch&l=&l= diff --git a/book/src/usage.md b/book/src/usage.md index 2de8d01a..d31e03a1 100644 --- a/book/src/usage.md +++ b/book/src/usage.md @@ -51,9 +51,10 @@ Multiple characters are currently not supported, but planned. ## Textobjects -Currently supported: `word`, `surround`. +Currently supported: `word`, `surround`, `function`, `class`, `parameter`. ![textobject-demo](https://user-images.githubusercontent.com/23398472/124231131-81a4bb00-db2d-11eb-9d10-8e577ca7b177.gif) +![textobject-treesitter-demo](https://user-images.githubusercontent.com/23398472/132537398-2a2e0a54-582b-44ab-a77f-eb818942203d.gif) - `ma` - Select around the object (`va` in vim, `` in kakoune) - `mi` - Select inside the object (`vi` in vim, `` in kakoune) @@ -62,5 +63,11 @@ Currently supported: `word`, `surround`. | --- | --- | | `w` | Word | | `(`, `[`, `'`, etc | Specified surround pairs | - -Textobjects based on treesitter, like `function`, `class`, etc are planned. +| `f` | Function | +| `c` | Class | +| `p` | Parameter | + +Note: `f`, `c`, etc need a tree-sitter grammar active for the current +document and a special tree-sitter query file to work properly. [Only +some grammars](https://github.com/search?q=repo%3Ahelix-editor%2Fhelix+filename%3Atextobjects.scm&type=Code&ref=advsearch&l=&l=) +currently have the query file implemented. Contributions are welcome ! diff --git a/helix-core/src/indent.rs b/helix-core/src/indent.rs index d9a0155f..20f034ea 100644 --- a/helix-core/src/indent.rs +++ b/helix-core/src/indent.rs @@ -464,6 +464,7 @@ where unit: String::from(" "), }), indent_query: OnceCell::new(), + textobject_query: OnceCell::new(), }], }); diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index 0929e38f..f4b4535b 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -49,7 +49,7 @@ pub struct Configuration { #[serde(rename_all = "kebab-case")] pub struct LanguageConfiguration { #[serde(rename = "name")] - pub(crate) language_id: String, + pub language_id: String, pub scope: String, // source.rust pub file_types: Vec, // filename ends_with? pub roots: Vec, // these indicate project roots <.git, Cargo.toml> @@ -76,6 +76,8 @@ pub struct LanguageConfiguration { #[serde(skip)] pub(crate) indent_query: OnceCell>, + #[serde(skip)] + pub(crate) textobject_query: OnceCell>, } #[derive(Debug, Serialize, Deserialize)] @@ -105,6 +107,32 @@ pub struct IndentQuery { pub outdent: HashSet, } +#[derive(Debug)] +pub struct TextObjectQuery { + pub query: Query, +} + +impl TextObjectQuery { + /// Run the query on the given node and return sub nodes which match given + /// capture ("function.inside", "class.around", etc). + pub fn capture_nodes<'a>( + &'a self, + capture_name: &str, + node: Node<'a>, + slice: RopeSlice<'a>, + cursor: &'a mut QueryCursor, + ) -> Option>> { + let capture_idx = self.query.capture_index_for_name(capture_name)?; + let captures = cursor.captures(&self.query, node, RopeProvider(slice)); + + captures + .filter_map(move |(mat, idx)| { + (mat.captures[idx].index == capture_idx).then(|| mat.captures[idx].node) + }) + .into() + } +} + fn load_runtime_file(language: &str, filename: &str) -> Result { let path = crate::RUNTIME_DIR .join("queries") @@ -153,7 +181,6 @@ impl LanguageConfiguration { // highlights_query += "\n(ERROR) @error"; let injections_query = read_query(&language, "injections.scm"); - let locals_query = read_query(&language, "locals.scm"); if highlights_query.is_empty() { @@ -203,6 +230,18 @@ impl LanguageConfiguration { .as_ref() } + pub fn textobject_query(&self) -> Option<&TextObjectQuery> { + self.textobject_query + .get_or_init(|| -> Option { + let lang_name = self.language_id.to_ascii_lowercase(); + let query_text = read_query(&lang_name, "textobjects.scm"); + let lang = self.highlight_config.get()?.as_ref()?.language; + let query = Query::new(lang, &query_text).ok()?; + Some(TextObjectQuery { query }) + }) + .as_ref() + } + pub fn scope(&self) -> &str { &self.scope } diff --git a/helix-core/src/textobject.rs b/helix-core/src/textobject.rs index b965f6df..975ed115 100644 --- a/helix-core/src/textobject.rs +++ b/helix-core/src/textobject.rs @@ -1,9 +1,13 @@ +use std::fmt::Display; + use ropey::RopeSlice; +use tree_sitter::{Node, QueryCursor}; use crate::chars::{categorize_char, char_is_whitespace, CharCategory}; use crate::graphemes::next_grapheme_boundary; use crate::movement::Direction; use crate::surround; +use crate::syntax::LanguageConfiguration; use crate::Range; fn find_word_boundary(slice: RopeSlice, mut pos: usize, direction: Direction) -> usize { @@ -51,6 +55,15 @@ pub enum TextObject { Inside, } +impl Display for TextObject { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + Self::Around => "around", + Self::Inside => "inside", + }) + } +} + // count doesn't do anything yet pub fn textobject_word( slice: RopeSlice, @@ -108,6 +121,44 @@ pub fn textobject_surround( .unwrap_or(range) } +/// Transform the given range to select text objects based on tree-sitter. +/// `object_name` is a query capture base name like "function", "class", etc. +/// `slice_tree` is the tree-sitter node corresponding to given text slice. +pub fn textobject_treesitter( + slice: RopeSlice, + range: Range, + textobject: TextObject, + object_name: &str, + slice_tree: Node, + lang_config: &LanguageConfiguration, + _count: usize, +) -> Range { + let get_range = move || -> Option { + let byte_pos = slice.char_to_byte(range.cursor(slice)); + + let capture_name = format!("{}.{}", object_name, textobject); // eg. function.inner + let mut cursor = QueryCursor::new(); + let node = lang_config + .textobject_query()? + .capture_nodes(&capture_name, slice_tree, slice, &mut cursor)? + .filter(|node| node.byte_range().contains(&byte_pos)) + .min_by_key(|node| node.byte_range().len())?; + + let len = slice.len_bytes(); + let start_byte = node.start_byte(); + let end_byte = node.end_byte(); + if start_byte >= len || end_byte >= len { + return None; + } + + let start_char = slice.byte_to_char(start_byte); + let end_char = slice.byte_to_char(end_byte); + + Some(Range::new(start_char, end_char)) + }; + get_range().unwrap_or(range) +} + #[cfg(test)] mod test { use super::TextObject::*; diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 9f54292d..272a9d9a 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -4465,9 +4465,28 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) { let (view, doc) = current!(cx.editor); let text = doc.text().slice(..); + let textobject_treesitter = |obj_name: &str, range: Range| -> Range { + let (lang_config, syntax) = match doc.language_config().zip(doc.syntax()) { + Some(t) => t, + None => return range, + }; + textobject::textobject_treesitter( + text, + range, + objtype, + obj_name, + syntax.tree().root_node(), + lang_config, + count, + ) + }; + let selection = doc.selection(view.id).clone().transform(|range| { match ch { 'w' => textobject::textobject_word(text, range, objtype, count), + 'c' => textobject_treesitter("class", range), + 'f' => textobject_treesitter("function", range), + 'p' => textobject_treesitter("parameter", range), // TODO: cancel new ranges if inconsistent surround matches across lines ch if !ch.is_ascii_alphanumeric() => { textobject::textobject_surround(text, range, objtype, ch, count) diff --git a/runtime/queries/go/textobjects.scm b/runtime/queries/go/textobjects.scm new file mode 100644 index 00000000..9bcfc690 --- /dev/null +++ b/runtime/queries/go/textobjects.scm @@ -0,0 +1,21 @@ +(function_declaration + body: (block)? @function.inside) @function.around + +(func_literal + (_)? @function.inside) @function.around + +(method_declaration + body: (block)? @function.inside) @function.around + +;; struct and interface declaration as class textobject? +(type_declaration + (type_spec (type_identifier) (struct_type (field_declaration_list (_)?) @class.inside))) @class.around + +(type_declaration + (type_spec (type_identifier) (interface_type (method_spec_list (_)?) @class.inside))) @class.around + +(parameter_list + (_) @parameter.inside) + +(argument_list + (_) @parameter.inside) diff --git a/runtime/queries/python/textobjects.scm b/runtime/queries/python/textobjects.scm new file mode 100644 index 00000000..a52538af --- /dev/null +++ b/runtime/queries/python/textobjects.scm @@ -0,0 +1,14 @@ +(function_definition + body: (block)? @function.inside) @function.around + +(class_definition + body: (block)? @class.inside) @class.around + +(parameters + (_) @parameter.inside) + +(lambda_parameters + (_) @parameter.inside) + +(argument_list + (_) @parameter.inside) diff --git a/runtime/queries/rust/textobjects.scm b/runtime/queries/rust/textobjects.scm new file mode 100644 index 00000000..e3132687 --- /dev/null +++ b/runtime/queries/rust/textobjects.scm @@ -0,0 +1,26 @@ +(function_item + body: (_) @function.inside) @function.around + +(struct_item + body: (_) @class.inside) @class.around + +(enum_item + body: (_) @class.inside) @class.around + +(union_item + body: (_) @class.inside) @class.around + +(trait_item + body: (_) @class.inside) @class.around + +(impl_item + body: (_) @class.inside) @class.around + +(parameters + (_) @parameter.inside) + +(closure_parameters + (_) @parameter.inside) + +(arguments + (_) @parameter.inside) -- cgit v1.2.3-70-g09d2 From 0f886af4b993c836bb2d522f6e036362593ff8b8 Mon Sep 17 00:00:00 2001 From: Oskar Nehlin Date: Sat, 23 Oct 2021 13:06:40 +0200 Subject: Add commands for moving between splits with a direction (#860) * Add commands for moving between splits with a direction * Update keymaps * Change picker mapping * Add test and clean up some comments--- book/src/keymap.md | 18 +++-- helix-term/src/commands.rs | 20 +++++ helix-term/src/keymap.rs | 6 +- helix-term/src/ui/picker.rs | 2 +- helix-view/src/editor.rs | 18 ++++- helix-view/src/tree.rs | 191 +++++++++++++++++++++++++++++++++++++++++--- 6 files changed, 236 insertions(+), 19 deletions(-) (limited to 'helix-term/src') diff --git a/book/src/keymap.md b/book/src/keymap.md index 3dfc5dc4..644dc1c9 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -181,12 +181,16 @@ TODO: Mappings for selecting syntax nodes (a superset of `[`). This layer is similar to vim keybindings as kakoune does not support window. -| Key | Description | Command | -| ----- | ------------- | ------- | -| `w`, `Ctrl-w` | Switch to next window | `rotate_view` | -| `v`, `Ctrl-v` | Vertical right split | `vsplit` | -| `h`, `Ctrl-h` | Horizontal bottom split | `hsplit` | -| `q`, `Ctrl-q` | Close current window | `wclose` | +| Key | Description | Command | +| ----- | ------------- | ------- | +| `w`, `Ctrl-w` | Switch to next window | `rotate_view` | +| `v`, `Ctrl-v` | Vertical right split | `vsplit` | +| `s`, `Ctrl-s` | Horizontal bottom split | `hsplit` | +| `h`, `Ctrl-h` | Move to left split | `jump_view_left` | +| `j`, `Ctrl-j` | Move to split below | `jump_view_down` | +| `k`, `Ctrl-k` | Move to split above | `jump_view_up` | +| `l`, `Ctrl-l` | Move to right split | `jump_view_right` | +| `q`, `Ctrl-q` | Close current window | `wclose` | #### Space mode @@ -249,6 +253,6 @@ Keys to use within picker. Remapping currently not supported. | `Down`, `Ctrl-j`, `Ctrl-n` | Next entry | | `Ctrl-space` | Filter options | | `Enter` | Open selected | -| `Ctrl-h` | Open horizontally | +| `Ctrl-s` | Open horizontally | | `Ctrl-v` | Open vertically | | `Escape`, `Ctrl-c` | Close picker | diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 272a9d9a..9d73ba6e 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -301,6 +301,10 @@ impl Command { expand_selection, "Expand selection to parent syntax node", jump_forward, "Jump forward on jumplist", jump_backward, "Jump backward on jumplist", + jump_view_right, "Jump to the split to the right", + jump_view_left, "Jump to the split to the left", + jump_view_up, "Jump to the split above", + jump_view_down, "Jump to the split below", rotate_view, "Goto next window", hsplit, "Horizontal bottom split", vsplit, "Vertical right split", @@ -4373,6 +4377,22 @@ fn rotate_view(cx: &mut Context) { cx.editor.focus_next() } +fn jump_view_right(cx: &mut Context) { + cx.editor.focus_right() +} + +fn jump_view_left(cx: &mut Context) { + cx.editor.focus_left() +} + +fn jump_view_up(cx: &mut Context) { + cx.editor.focus_up() +} + +fn jump_view_down(cx: &mut Context) { + cx.editor.focus_down() +} + // split helper, clear it later fn split(cx: &mut Context, action: Action) { let (view, doc) = current!(cx.editor); diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index cd4d3a32..f877387c 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -520,9 +520,13 @@ impl Default for Keymaps { "C-w" => { "Window" "C-w" | "w" => rotate_view, - "C-h" | "h" => hsplit, + "C-s" | "s" => hsplit, "C-v" | "v" => vsplit, "C-q" | "q" => wclose, + "C-h" | "h" => jump_view_left, + "C-j" | "j" => jump_view_down, + "C-k" | "k" => jump_view_up, + "C-l" | "l" => jump_view_right, }, // move under c diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 1f08cf13..7e257c0b 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -373,7 +373,7 @@ impl Component for Picker { return close_fn; } KeyEvent { - code: KeyCode::Char('h'), + code: KeyCode::Char('s'), modifiers: KeyModifiers::CONTROL, } => { if let Some(option) = self.selection() { diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 52fca6d2..813c86fd 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -2,7 +2,7 @@ use crate::{ clipboard::{get_clipboard_provider, ClipboardProvider}, graphics::{CursorKind, Rect}, theme::{self, Theme}, - tree::Tree, + tree::{self, Tree}, Document, DocumentId, View, ViewId, }; @@ -355,6 +355,22 @@ impl Editor { self.tree.focus_next(); } + pub fn focus_right(&mut self) { + self.tree.focus_direction(tree::Direction::Right); + } + + pub fn focus_left(&mut self) { + self.tree.focus_direction(tree::Direction::Left); + } + + pub fn focus_up(&mut self) { + self.tree.focus_direction(tree::Direction::Up); + } + + pub fn focus_down(&mut self) { + self.tree.focus_direction(tree::Direction::Down); + } + pub fn should_close(&self) -> bool { self.tree.is_empty() } diff --git a/helix-view/src/tree.rs b/helix-view/src/tree.rs index 576f64f0..064334b1 100644 --- a/helix-view/src/tree.rs +++ b/helix-view/src/tree.rs @@ -47,13 +47,21 @@ impl Node { // TODO: screen coord to container + container coordinate helpers -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Layout { Horizontal, Vertical, // could explore stacked/tabbed } +#[derive(Debug, Clone, Copy)] +pub enum Direction { + Up, + Down, + Left, + Right, +} + #[derive(Debug)] pub struct Container { layout: Layout, @@ -150,7 +158,6 @@ impl Tree { } => container, _ => unreachable!(), }; - if container.layout == layout { // insert node after the current item if there is children already let pos = if container.children.is_empty() { @@ -393,6 +400,112 @@ impl Tree { Traverse::new(self) } + // Finds the split in the given direction if it exists + pub fn find_split_in_direction(&self, id: ViewId, direction: Direction) -> Option { + let parent = self.nodes[id].parent; + // Base case, we found the root of the tree + if parent == id { + return None; + } + // Parent must always be a container + let parent_container = match &self.nodes[parent].content { + Content::Container(container) => container, + Content::View(_) => unreachable!(), + }; + + match (direction, parent_container.layout) { + (Direction::Up, Layout::Vertical) + | (Direction::Left, Layout::Horizontal) + | (Direction::Right, Layout::Horizontal) + | (Direction::Down, Layout::Vertical) => { + // The desired direction of movement is not possible within + // the parent container so the search must continue closer to + // the root of the split tree. + self.find_split_in_direction(parent, direction) + } + (Direction::Up, Layout::Horizontal) + | (Direction::Down, Layout::Horizontal) + | (Direction::Left, Layout::Vertical) + | (Direction::Right, Layout::Vertical) => { + // It's possible to move in the desired direction within + // the parent container so an attempt is made to find the + // correct child. + match self.find_child(id, &parent_container.children, direction) { + // Child is found, search is ended + Some(id) => Some(id), + // A child is not found. This could be because of either two scenarios + // 1. Its not possible to move in the desired direction, and search should end + // 2. A layout like the following with focus at X and desired direction Right + // | _ | x | | + // | _ _ _ | | + // | _ _ _ | | + // The container containing X ends at X so no rightward movement is possible + // however there still exists another view/container to the right that hasn't + // been explored. Thus another search is done here in the parent container + // before concluding it's not possible to move in the desired direction. + None => self.find_split_in_direction(parent, direction), + } + } + } + } + + fn find_child(&self, id: ViewId, children: &[ViewId], direction: Direction) -> Option { + let mut child_id = match direction { + // index wise in the child list the Up and Left represents a -1 + // thus reversed iterator. + Direction::Up | Direction::Left => children + .iter() + .rev() + .skip_while(|i| **i != id) + .copied() + .nth(1)?, + // Down and Right => +1 index wise in the child list + Direction::Down | Direction::Right => { + children.iter().skip_while(|i| **i != id).copied().nth(1)? + } + }; + let (current_x, current_y) = match &self.nodes[self.focus].content { + Content::View(current_view) => (current_view.area.left(), current_view.area.top()), + Content::Container(_) => unreachable!(), + }; + + // If the child is a container the search finds the closest container child + // visually based on screen location. + while let Content::Container(container) = &self.nodes[child_id].content { + match (direction, container.layout) { + (_, Layout::Vertical) => { + // find closest split based on x because y is irrelevant + // in a vertical container (and already correct based on previous search) + child_id = *container.children.iter().min_by_key(|id| { + let x = match &self.nodes[**id].content { + Content::View(view) => view.inner_area().left(), + Content::Container(container) => container.area.left(), + }; + (current_x as i16 - x as i16).abs() + })?; + } + (_, Layout::Horizontal) => { + // find closest split based on y because x is irrelevant + // in a horizontal container (and already correct based on previous search) + child_id = *container.children.iter().min_by_key(|id| { + let y = match &self.nodes[**id].content { + Content::View(view) => view.inner_area().top(), + Content::Container(container) => container.area.top(), + }; + (current_y as i16 - y as i16).abs() + })?; + } + } + } + Some(child_id) + } + + pub fn focus_direction(&mut self, direction: Direction) { + if let Some(id) = self.find_split_in_direction(self.focus, direction) { + self.focus = id; + } + } + pub fn focus_next(&mut self) { // This function is very dumb, but that's because we don't store any parent links. // (we'd be able to go parent.next_sibling() recursively until we find something) @@ -420,13 +533,12 @@ impl Tree { // if found = container -> found = first child // } - let iter = self.traverse(); - - let mut iter = iter.skip_while(|&(key, _view)| key != self.focus); - iter.next(); // take the focused value - - if let Some((key, _)) = iter.next() { - self.focus = key; + let mut views = self + .traverse() + .skip_while(|&(id, _view)| id != self.focus) + .skip(1); // Skip focused value + if let Some((id, _)) = views.next() { + self.focus = id; } else { // extremely crude, take the first item again let (key, _) = self.traverse().next().unwrap(); @@ -472,3 +584,64 @@ impl<'a> Iterator for Traverse<'a> { } } } + +#[cfg(test)] +mod test { + use super::*; + use crate::DocumentId; + + #[test] + fn find_split_in_direction() { + let mut tree = Tree::new(Rect { + x: 0, + y: 0, + width: 180, + height: 80, + }); + let mut view = View::new(DocumentId::default()); + view.area = Rect::new(0, 0, 180, 80); + tree.insert(view); + + let l0 = tree.focus; + let view = View::new(DocumentId::default()); + tree.split(view, Layout::Vertical); + let r0 = tree.focus; + + tree.focus = l0; + let view = View::new(DocumentId::default()); + tree.split(view, Layout::Horizontal); + let l1 = tree.focus; + + tree.focus = l0; + let view = View::new(DocumentId::default()); + tree.split(view, Layout::Vertical); + let l2 = tree.focus; + + // Tree in test + // | L0 | L2 | | + // | L1 | R0 | + tree.focus = l2; + assert_eq!(Some(l0), tree.find_split_in_direction(l2, Direction::Left)); + assert_eq!(Some(l1), tree.find_split_in_direction(l2, Direction::Down)); + assert_eq!(Some(r0), tree.find_split_in_direction(l2, Direction::Right)); + assert_eq!(None, tree.find_split_in_direction(l2, Direction::Up)); + + tree.focus = l1; + assert_eq!(None, tree.find_split_in_direction(l1, Direction::Left)); + assert_eq!(None, tree.find_split_in_direction(l1, Direction::Down)); + assert_eq!(Some(r0), tree.find_split_in_direction(l1, Direction::Right)); + assert_eq!(Some(l0), tree.find_split_in_direction(l1, Direction::Up)); + + tree.focus = l0; + assert_eq!(None, tree.find_split_in_direction(l0, Direction::Left)); + assert_eq!(Some(l1), tree.find_split_in_direction(l0, Direction::Down)); + assert_eq!(Some(l2), tree.find_split_in_direction(l0, Direction::Right)); + assert_eq!(None, tree.find_split_in_direction(l0, Direction::Up)); + + tree.focus = r0; + assert_eq!(Some(l2), tree.find_split_in_direction(r0, Direction::Left)); + assert_eq!(None, tree.find_split_in_direction(r0, Direction::Down)); + assert_eq!(None, tree.find_split_in_direction(r0, Direction::Right)); + assert_eq!(None, tree.find_split_in_direction(r0, Direction::Up)); + } +} -- cgit v1.2.3-70-g09d2 From 0cb5e0b2caba61bbcf6f57ce58506882766d5eea Mon Sep 17 00:00:00 2001 From: Kirawi Date: Sat, 23 Oct 2021 08:52:18 -0400 Subject: log syntax highlighting init errors (#895) --- Cargo.lock | 1 + helix-core/Cargo.toml | 1 + helix-core/src/syntax.rs | 4 +++- helix-term/src/main.rs | 7 ++++++- helix-view/src/clipboard.rs | 22 ++++++---------------- helix-view/src/editor.rs | 6 +++++- 6 files changed, 22 insertions(+), 19 deletions(-) (limited to 'helix-term/src') diff --git a/Cargo.lock b/Cargo.lock index aa6fb141..ad12adf9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -363,6 +363,7 @@ dependencies = [ "arc-swap", "etcetera", "helix-syntax", + "log", "once_cell", "quickcheck", "regex", diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml index 93ebb133..84d029d2 100644 --- a/helix-core/Cargo.toml +++ b/helix-core/Cargo.toml @@ -27,6 +27,7 @@ once_cell = "1.8" arc-swap = "1" regex = "1" +log = "0.4" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" toml = "0.5" diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index f4b4535b..281a70f9 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -186,7 +186,9 @@ impl LanguageConfiguration { if highlights_query.is_empty() { None } else { - let language = get_language(&crate::RUNTIME_DIR, &self.language_id).ok()?; + let language = get_language(&crate::RUNTIME_DIR, &self.language_id) + .map_err(|e| log::info!("{}", e)) + .ok()?; let config = HighlightConfiguration::new( language, &highlights_query, diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs index 180dacd1..2589a375 100644 --- a/helix-term/src/main.rs +++ b/helix-term/src/main.rs @@ -16,6 +16,11 @@ fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> { }; // Separate file config so we can include year, month and day in file logs + let file = std::fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(logpath)?; let file_config = fern::Dispatch::new() .format(|out, message, record| { out.finish(format_args!( @@ -26,7 +31,7 @@ fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> { message )) }) - .chain(fern::log_file(logpath)?); + .chain(file); base_config.chain(file_config).apply()?; diff --git a/helix-view/src/clipboard.rs b/helix-view/src/clipboard.rs index a11224ac..a492652d 100644 --- a/helix-view/src/clipboard.rs +++ b/helix-view/src/clipboard.rs @@ -116,7 +116,7 @@ pub fn get_clipboard_provider() -> Box { } } else { #[cfg(target_os = "windows")] - return Box::new(provider::WindowsProvider::new()); + return Box::new(provider::WindowsProvider::default()); #[cfg(not(target_os = "windows"))] return Box::new(provider::NopProvider::new()); @@ -145,15 +145,15 @@ mod provider { use anyhow::{bail, Context as _, Result}; use std::borrow::Cow; + #[cfg(not(target_os = "windows"))] #[derive(Debug)] pub struct NopProvider { buf: String, primary_buf: String, } + #[cfg(not(target_os = "windows"))] impl NopProvider { - #[allow(dead_code)] - // Only dead_code on Windows. pub fn new() -> Self { Self { buf: String::new(), @@ -162,6 +162,7 @@ mod provider { } } + #[cfg(not(target_os = "windows"))] impl ClipboardProvider for NopProvider { fn name(&self) -> Cow { Cow::Borrowed("none") @@ -186,19 +187,8 @@ mod provider { } #[cfg(target_os = "windows")] - #[derive(Debug)] - pub struct WindowsProvider { - selection_buf: String, - } - - #[cfg(target_os = "windows")] - impl WindowsProvider { - pub fn new() -> Self { - Self { - selection_buf: String::new(), - } - } - } + #[derive(Default, Debug)] + pub struct WindowsProvider; #[cfg(target_os = "windows")] impl ClipboardProvider for WindowsProvider { diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 813c86fd..51fe8a42 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -294,7 +294,11 @@ impl Editor { self.language_servers .get(language) .map_err(|e| { - log::error!("Failed to get LSP, {}, for `{}`", e, language.scope()) + log::error!( + "Failed to initialize the LSP for `{}` {{ {} }}", + language.scope(), + e + ) }) .ok() }); -- cgit v1.2.3-70-g09d2 From 971ba8929fcc879c2c24b9ab204849500ffe6fce Mon Sep 17 00:00:00 2001 From: CossonLeo Date: Sun, 24 Oct 2021 15:55:29 +0800 Subject: Filter completion items from language server by starts_with word under cursor (#883) * filter items by starts_with pre nth char of cursor * add config for filter completion items by starts_with * filter items by starts_with pre nth char of cursor * add config for filter completion items by starts_with * remove completion items pre filter configuratio--- helix-term/src/commands.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) (limited to 'helix-term/src') diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 9d73ba6e..07485f9f 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -4142,6 +4142,7 @@ pub fn completion(cx: &mut Context) { iter.reverse(); let offset = iter.take_while(|ch| chars::char_is_word(*ch)).count(); let start_offset = cursor.saturating_sub(offset); + let prefix = text.slice(start_offset..cursor).to_string(); cx.callback( future, @@ -4154,7 +4155,7 @@ pub fn completion(cx: &mut Context) { return; } - let items = match response { + let mut items = match response { Some(lsp::CompletionResponse::Array(items)) => items, // TODO: do something with is_incomplete Some(lsp::CompletionResponse::List(lsp::CompletionList { @@ -4164,6 +4165,18 @@ pub fn completion(cx: &mut Context) { None => Vec::new(), }; + if !prefix.is_empty() { + items = items + .into_iter() + .filter(|item| { + item.filter_text + .as_ref() + .unwrap_or(&item.label) + .starts_with(&prefix) + }) + .collect(); + } + if items.is_empty() { // editor.set_error("No completion available".to_string()); return; -- cgit v1.2.3-70-g09d2 From cee7ad781e5f6de249d728425a6283a26bb62dc3 Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Sun, 24 Oct 2021 17:28:29 +0900 Subject: Mark a few functions as `const` --- helix-core/src/line_ending.rs | 6 +++--- helix-core/src/register.rs | 4 ++-- helix-term/src/ui/editor.rs | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) (limited to 'helix-term/src') diff --git a/helix-core/src/line_ending.rs b/helix-core/src/line_ending.rs index 18ea5f9f..3541305c 100644 --- a/helix-core/src/line_ending.rs +++ b/helix-core/src/line_ending.rs @@ -20,7 +20,7 @@ pub enum LineEnding { impl LineEnding { #[inline] - pub fn len_chars(&self) -> usize { + pub const fn len_chars(&self) -> usize { match self { Self::Crlf => 2, _ => 1, @@ -28,7 +28,7 @@ impl LineEnding { } #[inline] - pub fn as_str(&self) -> &'static str { + pub const fn as_str(&self) -> &'static str { match self { Self::Crlf => "\u{000D}\u{000A}", Self::LF => "\u{000A}", @@ -42,7 +42,7 @@ impl LineEnding { } #[inline] - pub fn from_char(ch: char) -> Option { + pub const fn from_char(ch: char) -> Option { match ch { '\u{000A}' => Some(LineEnding::LF), '\u{000B}' => Some(LineEnding::VT), diff --git a/helix-core/src/register.rs b/helix-core/src/register.rs index c3e6652e..c5444eb7 100644 --- a/helix-core/src/register.rs +++ b/helix-core/src/register.rs @@ -7,7 +7,7 @@ pub struct Register { } impl Register { - pub fn new(name: char) -> Self { + pub const fn new(name: char) -> Self { Self { name, values: Vec::new(), @@ -18,7 +18,7 @@ impl Register { Self { name, values } } - pub fn name(&self) -> char { + pub const fn name(&self) -> char { self.name } diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 9234bb96..692696a6 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -1106,7 +1106,7 @@ fn canonicalize_key(key: &mut KeyEvent) { } #[inline] -fn abs_diff(a: usize, b: usize) -> usize { +const fn abs_diff(a: usize, b: usize) -> usize { if a > b { a - b } else { -- cgit v1.2.3-70-g09d2 From 2ed01f2d9c8c98a7e59fef1bb19af56ec897084b Mon Sep 17 00:00:00 2001 From: CossonLeo Date: Sun, 24 Oct 2021 21:47:10 +0800 Subject: find motion and textobj motion repeat (#891) --- helix-term/src/commands.rs | 210 +++++++++++++++++++++++---------------------- helix-term/src/keymap.rs | 1 + helix-view/src/editor.rs | 14 +++ 3 files changed, 122 insertions(+), 103 deletions(-) (limited to 'helix-term/src') diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 07485f9f..c698c641 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -12,8 +12,13 @@ use helix_core::{ }; use helix_view::{ - clipboard::ClipboardType, document::Mode, editor::Action, input::KeyEvent, keyboard::KeyCode, - view::View, Document, DocumentId, Editor, ViewId, + clipboard::ClipboardType, + document::Mode, + editor::{Action, Motion}, + input::KeyEvent, + keyboard::KeyCode, + view::View, + Document, DocumentId, Editor, ViewId, }; use anyhow::{anyhow, bail, Context as _}; @@ -198,6 +203,7 @@ impl Command { 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", + repeat_last_motion, "repeat last motion(extend_next_char, extend_till_char, find_next_char, find_till_char...)", replace, "Replace with new char", switch_case, "Switch (toggle) case", switch_to_uppercase, "Switch to uppercase", @@ -666,8 +672,7 @@ fn extend_next_long_word_end(cx: &mut Context) { extend_word_impl(cx, movement::move_next_long_word_end) } -#[inline] -fn find_char_impl(cx: &mut Context, search_fn: F, inclusive: bool, extend: bool) +fn will_find_char(cx: &mut Context, search_fn: F, inclusive: bool, extend: bool) where F: Fn(RopeSlice, char, usize, usize, bool) -> Option + 'static, { @@ -705,29 +710,48 @@ where _ => return, }; - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); + find_char_impl(cx.editor, &search_fn, inclusive, extend, ch, count); + cx.editor.last_motion = Some(Motion(Box::new(move |editor: &mut Editor| { + find_char_impl(editor, &search_fn, inclusive, true, ch, 1); + }))); + }) +} - let selection = doc.selection(view.id).clone().transform(|range| { - // TODO: use `Range::cursor()` here instead. However, that works in terms of - // graphemes, whereas this function doesn't yet. So we're doing the same logic - // here, but just in terms of chars instead. - let search_start_pos = if range.anchor < range.head { - range.head - 1 - } else { - range.head - }; +// - search_fn(text, ch, search_start_pos, count, inclusive).map_or(range, |pos| { - if extend { - range.put_cursor(text, pos, true) - } else { - Range::point(range.cursor(text)).put_cursor(text, pos, true) - } - }) - }); - doc.set_selection(view.id, selection); - }) +#[inline] +fn find_char_impl( + editor: &mut Editor, + search_fn: &F, + inclusive: bool, + extend: bool, + ch: char, + count: usize, +) where + F: Fn(RopeSlice, char, usize, usize, bool) -> Option + 'static, +{ + let (view, doc) = current!(editor); + let text = doc.text().slice(..); + + let selection = doc.selection(view.id).clone().transform(|range| { + // TODO: use `Range::cursor()` here instead. However, that works in terms of + // graphemes, whereas this function doesn't yet. So we're doing the same logic + // here, but just in terms of chars instead. + let search_start_pos = if range.anchor < range.head { + range.head - 1 + } else { + range.head + }; + + search_fn(text, ch, search_start_pos, count, inclusive).map_or(range, |pos| { + if extend { + range.put_cursor(text, pos, true) + } else { + Range::point(range.cursor(text)).put_cursor(text, pos, true) + } + }) + }); + doc.set_selection(view.id, selection); } fn find_next_char_impl( @@ -741,6 +765,10 @@ fn find_next_char_impl( if inclusive { search::find_nth_next(text, ch, pos, n) } else { + let n = match text.get_char(pos) { + Some(next_ch) if next_ch == ch => n + 1, + _ => n, + }; search::find_nth_next(text, ch, pos, n).map(|n| n.saturating_sub(1)) } } @@ -755,80 +783,52 @@ fn find_prev_char_impl( if inclusive { search::find_nth_prev(text, ch, pos, n) } else { + let n = match text.get_char(pos.saturating_sub(1)) { + Some(next_ch) if next_ch == ch => n + 1, + _ => n, + }; search::find_nth_prev(text, ch, pos, n).map(|n| (n + 1).min(text.len_chars())) } } fn find_till_char(cx: &mut Context) { - find_char_impl( - cx, - find_next_char_impl, - false, /* inclusive */ - false, /* extend */ - ) + will_find_char(cx, find_next_char_impl, false, false) } fn find_next_char(cx: &mut Context) { - find_char_impl( - cx, - find_next_char_impl, - true, /* inclusive */ - false, /* extend */ - ) + will_find_char(cx, find_next_char_impl, true, false) } fn extend_till_char(cx: &mut Context) { - find_char_impl( - cx, - find_next_char_impl, - false, /* inclusive */ - true, /* extend */ - ) + will_find_char(cx, find_next_char_impl, false, true) } fn extend_next_char(cx: &mut Context) { - find_char_impl( - cx, - find_next_char_impl, - true, /* inclusive */ - true, /* extend */ - ) + will_find_char(cx, find_next_char_impl, true, true) } fn till_prev_char(cx: &mut Context) { - find_char_impl( - cx, - find_prev_char_impl, - false, /* inclusive */ - false, /* extend */ - ) + will_find_char(cx, find_prev_char_impl, false, false) } fn find_prev_char(cx: &mut Context) { - find_char_impl( - cx, - find_prev_char_impl, - true, /* inclusive */ - false, /* extend */ - ) + will_find_char(cx, find_prev_char_impl, true, false) } fn extend_till_prev_char(cx: &mut Context) { - find_char_impl( - cx, - find_prev_char_impl, - false, /* inclusive */ - true, /* extend */ - ) + will_find_char(cx, find_prev_char_impl, false, true) } fn extend_prev_char(cx: &mut Context) { - find_char_impl( - cx, - find_prev_char_impl, - true, /* inclusive */ - true, /* extend */ - ) + will_find_char(cx, find_prev_char_impl, true, true) +} + +fn repeat_last_motion(cx: &mut Context) { + let last_motion = cx.editor.last_motion.take(); + if let Some(m) = &last_motion { + m.run(cx.editor); + cx.editor.last_motion = last_motion; + } } fn replace(cx: &mut Context) { @@ -4495,39 +4495,43 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) { let count = cx.count(); cx.on_next_key(move |cx, event| { if let Some(ch) = event.char() { - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - - let textobject_treesitter = |obj_name: &str, range: Range| -> Range { - let (lang_config, syntax) = match doc.language_config().zip(doc.syntax()) { - Some(t) => t, - None => return range, + let textobject = move |editor: &mut Editor| { + let (view, doc) = current!(editor); + let text = doc.text().slice(..); + + let textobject_treesitter = |obj_name: &str, range: Range| -> Range { + let (lang_config, syntax) = match doc.language_config().zip(doc.syntax()) { + Some(t) => t, + None => return range, + }; + textobject::textobject_treesitter( + text, + range, + objtype, + obj_name, + syntax.tree().root_node(), + lang_config, + count, + ) }; - textobject::textobject_treesitter( - text, - range, - objtype, - obj_name, - syntax.tree().root_node(), - lang_config, - count, - ) - }; - let selection = doc.selection(view.id).clone().transform(|range| { - match ch { - 'w' => textobject::textobject_word(text, range, objtype, count), - 'c' => textobject_treesitter("class", range), - 'f' => textobject_treesitter("function", range), - 'p' => textobject_treesitter("parameter", range), - // TODO: cancel new ranges if inconsistent surround matches across lines - ch if !ch.is_ascii_alphanumeric() => { - textobject::textobject_surround(text, range, objtype, ch, count) + let selection = doc.selection(view.id).clone().transform(|range| { + match ch { + 'w' => textobject::textobject_word(text, range, objtype, count), + 'c' => textobject_treesitter("class", range), + 'f' => textobject_treesitter("function", range), + 'p' => textobject_treesitter("parameter", range), + // TODO: cancel new ranges if inconsistent surround matches across lines + ch if !ch.is_ascii_alphanumeric() => { + textobject::textobject_surround(text, range, objtype, ch, count) + } + _ => range, } - _ => range, - } - }); - doc.set_selection(view.id, selection); + }); + doc.set_selection(view.id, selection); + }; + textobject(&mut cx.editor); + cx.editor.last_motion = Some(Motion(Box::new(textobject))); } }) } diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index f877387c..495fe892 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -395,6 +395,7 @@ impl Default for Keymaps { "F" => find_prev_char, "r" => replace, "R" => replace_with_yanked, + "A-." => repeat_last_motion, "~" => switch_case, "`" => switch_to_lowercase, diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 51fe8a42..09fc3334 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -93,6 +93,18 @@ impl Default for Config { } } +pub struct Motion(pub Box); +impl Motion { + pub fn run(&self, e: &mut Editor) { + (self.0)(e) + } +} +impl std::fmt::Debug for Motion { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("motion") + } +} + #[derive(Debug)] pub struct Editor { pub tree: Tree, @@ -112,6 +124,7 @@ pub struct Editor { pub config: Config, pub idle_timer: Pin>, + pub last_motion: Option, } #[derive(Debug, Copy, Clone)] @@ -147,6 +160,7 @@ impl Editor { clipboard_provider: get_clipboard_provider(), status_msg: None, idle_timer: Box::pin(sleep(config.idle_timeout)), + last_motion: None, config, } } -- cgit v1.2.3-70-g09d2 From a7d87c79ce751ce5d97843026de82d5ee124d176 Mon Sep 17 00:00:00 2001 From: Omnikar Date: Sun, 24 Oct 2021 20:25:47 -0400 Subject: Fix `:quit!` description and tense of other commands (#902) --- helix-term/src/commands.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) (limited to 'helix-term/src') diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index c698c641..d6e5bfe7 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2110,7 +2110,7 @@ mod cmd { TypableCommand { name: "quit!", aliases: &["q!"], - doc: "Close the current view.", + doc: "Close the current view forcefully (ignoring unsaved changes).", fun: force_quit, completer: None, }, @@ -2173,35 +2173,35 @@ mod cmd { TypableCommand { name: "write-quit", aliases: &["wq", "x"], - doc: "Writes changes to disk and closes the current view. Accepts an optional path (:wq some/path.txt)", + doc: "Write changes to disk and close the current view. Accepts an optional path (:wq some/path.txt)", fun: write_quit, completer: Some(completers::filename), }, TypableCommand { name: "write-quit!", aliases: &["wq!", "x!"], - doc: "Writes changes to disk and closes the current view forcefully. Accepts an optional path (:wq! some/path.txt)", + doc: "Write changes to disk and close the current view forcefully. Accepts an optional path (:wq! some/path.txt)", fun: force_write_quit, completer: Some(completers::filename), }, TypableCommand { name: "write-all", aliases: &["wa"], - doc: "Writes changes from all views to disk.", + doc: "Write changes from all views to disk.", fun: write_all, completer: None, }, TypableCommand { name: "write-quit-all", aliases: &["wqa", "xa"], - doc: "Writes changes from all views to disk and close all views.", + doc: "Write changes from all views to disk and close all views.", fun: write_all_quit, completer: None, }, TypableCommand { name: "write-quit-all!", aliases: &["wqa!", "xa!"], - doc: "Writes changes from all views to disk and close all views forcefully (ignoring unsaved changes).", + doc: "Write changes from all views to disk and close all views forcefully (ignoring unsaved changes).", fun: force_write_all_quit, completer: None, }, -- cgit v1.2.3-70-g09d2 From 3edca7854e66cbdb0c4baca25962a4f390fede55 Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Mon, 25 Oct 2021 11:03:18 +0900 Subject: completion: fully revert state before apply & insertText common prefix --- helix-core/src/transaction.rs | 7 ++++ helix-term/src/ui/completion.rs | 71 +++++++++++++++++------------------------ helix-term/src/ui/editor.rs | 13 ++++++-- helix-view/src/document.rs | 24 +++++++++++++- 4 files changed, 71 insertions(+), 44 deletions(-) (limited to 'helix-term/src') diff --git a/helix-core/src/transaction.rs b/helix-core/src/transaction.rs index 0e49fbe3..dfc18fbe 100644 --- a/helix-core/src/transaction.rs +++ b/helix-core/src/transaction.rs @@ -468,6 +468,13 @@ impl Transaction { } } + pub fn compose(mut self, other: Self) -> Self { + self.changes = self.changes.compose(other.changes); + // Other selection takes precedence + self.selection = other.selection; + self + } + pub fn with_selection(mut self, selection: Selection) -> Self { self.selection = Some(selection); self diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index c75b24f1..44879fcf 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -5,7 +5,7 @@ use tui::buffer::Buffer as Surface; use std::borrow::Cow; use helix_core::Transaction; -use helix_view::{graphics::Rect, Document, Editor, View}; +use helix_view::{graphics::Rect, Document, Editor}; use crate::commands; use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent}; @@ -83,13 +83,13 @@ impl Completion { start_offset: usize, trigger_offset: usize, ) -> Self { - // let items: Vec = Vec::new(); let menu = Menu::new(items, move |editor: &mut Editor, item, event| { fn item_to_transaction( doc: &Document, - view: &View, item: &CompletionItem, offset_encoding: helix_lsp::OffsetEncoding, + start_offset: usize, + trigger_offset: usize, ) -> Transaction { if let Some(edit) = &item.text_edit { let edit = match edit { @@ -105,63 +105,52 @@ impl Completion { ) } else { let text = item.insert_text.as_ref().unwrap_or(&item.label); - let cursor = doc - .selection(view.id) - .primary() - .cursor(doc.text().slice(..)); + // Some LSPs just give you an insertText with no offset ¯\_(ツ)_/¯ + // in these cases we need to check for a common prefix and remove it + let prefix = Cow::from(doc.text().slice(start_offset..trigger_offset)); + let text = text.trim_start_matches::<&str>(&prefix); Transaction::change( doc.text(), - vec![(cursor, cursor, Some(text.as_str().into()))].into_iter(), + vec![(trigger_offset, trigger_offset, Some(text.into()))].into_iter(), ) } } + let (view, doc) = current!(editor); + + // if more text was entered, remove it + doc.restore(view.id); + match event { PromptEvent::Abort => {} PromptEvent::Update => { - let (view, doc) = current!(editor); - // always present here let item = item.unwrap(); - // if more text was entered, remove it - // TODO: ideally to undo we should keep the last completion tx revert, and map it over new changes - let cursor = doc - .selection(view.id) - .primary() - .cursor(doc.text().slice(..)); - if trigger_offset < cursor { - let remove = Transaction::change( - doc.text(), - vec![(trigger_offset, cursor, None)].into_iter(), - ); - doc.apply(&remove, view.id); - } + let transaction = item_to_transaction( + doc, + item, + offset_encoding, + start_offset, + trigger_offset, + ); + + // initialize a savepoint + doc.savepoint(); - let transaction = item_to_transaction(doc, view, item, offset_encoding); doc.apply(&transaction, view.id); } PromptEvent::Validate => { - let (view, doc) = current!(editor); - // always present here let item = item.unwrap(); - // if more text was entered, remove it - // TODO: ideally to undo we should keep the last completion tx revert, and map it over new changes - let cursor = doc - .selection(view.id) - .primary() - .cursor(doc.text().slice(..)); - if trigger_offset < cursor { - let remove = Transaction::change( - doc.text(), - vec![(trigger_offset, cursor, None)].into_iter(), - ); - doc.apply(&remove, view.id); - } - - let transaction = item_to_transaction(doc, view, item, offset_encoding); + let transaction = item_to_transaction( + doc, + item, + offset_encoding, + start_offset, + trigger_offset, + ); doc.apply(&transaction, view.id); if let Some(additional_edits) = &item.additional_text_edits { diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 692696a6..850fec0f 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -13,7 +13,7 @@ use helix_core::{ syntax::{self, HighlightEvent}, unicode::segmentation::UnicodeSegmentation, unicode::width::UnicodeWidthStr, - LineEnding, Position, Range, Selection, + LineEnding, Position, Range, Selection, Transaction, }; use helix_view::{ document::Mode, @@ -721,7 +721,7 @@ impl EditorView { pub fn set_completion( &mut self, - editor: &Editor, + editor: &mut Editor, items: Vec, offset_encoding: helix_lsp::OffsetEncoding, start_offset: usize, @@ -736,6 +736,9 @@ impl EditorView { return; } + // Immediately initialize a savepoint + doc_mut!(editor).savepoint(); + // TODO : propagate required size on resize to completion too completion.required_size((size.width, size.height)); self.completion = Some(completion); @@ -945,6 +948,9 @@ impl Component for EditorView { if callback.is_some() { // assume close_fn self.completion = None; + // Clear any savepoints + let (_, doc) = current!(cxt.editor); + doc.savepoint = None; cxt.editor.clear_idle_timer(); // don't retrigger } } @@ -959,6 +965,9 @@ impl Component for EditorView { completion.update(&mut cxt); if completion.is_empty() { self.completion = None; + // Clear any savepoints + let (_, doc) = current!(cxt.editor); + doc.savepoint = None; cxt.editor.clear_idle_timer(); // don't retrigger } } diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 8804681b..23c2dbc6 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -97,6 +97,9 @@ pub struct Document { // it back as it separated from the edits. We could split out the parts manually but that will // be more troublesome. history: Cell, + + pub savepoint: Option, + last_saved_revision: usize, version: i32, // should be usize? @@ -328,6 +331,7 @@ impl Document { text, selections: HashMap::default(), indent_style: DEFAULT_INDENT, + line_ending: DEFAULT_LINE_ENDING, mode: Mode::Normal, restore_cursor: false, syntax: None, @@ -337,9 +341,9 @@ impl Document { diagnostics: Vec::new(), version: 0, history: Cell::new(History::default()), + savepoint: None, last_saved_revision: 0, language_server: None, - line_ending: DEFAULT_LINE_ENDING, } } @@ -635,6 +639,14 @@ impl Document { if !transaction.changes().is_empty() { self.version += 1; + // generate revert to savepoint + if self.savepoint.is_some() { + take_with(&mut self.savepoint, |prev_revert| { + let revert = transaction.invert(&old_doc); + Some(revert.compose(prev_revert.unwrap())) + }); + } + // update tree-sitter syntax tree if let Some(syntax) = &mut self.syntax { // TODO: no unwrap @@ -724,6 +736,16 @@ impl Document { } } + pub fn savepoint(&mut self) { + self.savepoint = Some(Transaction::new(self.text())); + } + + pub fn restore(&mut self, view_id: ViewId) { + if let Some(revert) = self.savepoint.take() { + self.apply(&revert, view_id); + } + } + /// Undo modifications to the [`Document`] according to `uk`. pub fn earlier(&mut self, view_id: ViewId, uk: helix_core::history::UndoKind) { let txns = self.history.get_mut().earlier(uk); -- cgit v1.2.3-70-g09d2 From acc5ac5e731ea978fdd8a0f6762f2cd5101780b2 Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Mon, 25 Oct 2021 11:11:11 +0900 Subject: fix warning --- helix-term/src/ui/editor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'helix-term/src') diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 850fec0f..97658c64 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -13,7 +13,7 @@ use helix_core::{ syntax::{self, HighlightEvent}, unicode::segmentation::UnicodeSegmentation, unicode::width::UnicodeWidthStr, - LineEnding, Position, Range, Selection, Transaction, + LineEnding, Position, Range, Selection, }; use helix_view::{ document::Mode, -- cgit v1.2.3-70-g09d2 From bca98b5bedfa6c9410384f26a2df6115874351bc Mon Sep 17 00:00:00 2001 From: CossonLeo Date: Tue, 26 Oct 2021 08:42:08 +0800 Subject: Add c-j c-k to menu keymap for move_up move_down (#908) --- helix-term/src/ui/menu.rs | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'helix-term/src') diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs index 1130089d..dd163d34 100644 --- a/helix-term/src/ui/menu.rs +++ b/helix-term/src/ui/menu.rs @@ -214,6 +214,10 @@ impl Component for Menu { | KeyEvent { code: KeyCode::Char('p'), modifiers: KeyModifiers::CONTROL, + } + | KeyEvent { + code: KeyCode::Char('k'), + modifiers: KeyModifiers::CONTROL, } => { self.move_up(); (self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update); @@ -231,6 +235,10 @@ impl Component for Menu { | KeyEvent { code: KeyCode::Char('n'), modifiers: KeyModifiers::CONTROL, + } + | KeyEvent { + code: KeyCode::Char('j'), + modifiers: KeyModifiers::CONTROL, } => { self.move_down(); (self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update); -- cgit v1.2.3-70-g09d2 From b142fd4080d99a7e4f39bb06207ded6771d47b20 Mon Sep 17 00:00:00 2001 From: CossonLeo Date: Tue, 26 Oct 2021 08:42:23 +0800 Subject: move_up will select last item, when no item selected (#907) --- helix-term/src/ui/menu.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'helix-term/src') diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs index dd163d34..3c492d14 100644 --- a/helix-term/src/ui/menu.rs +++ b/helix-term/src/ui/menu.rs @@ -98,7 +98,8 @@ impl Menu { pub fn move_up(&mut self) { let len = self.matches.len(); - let pos = self.cursor.map_or(0, |i| (i + len.saturating_sub(1)) % len) % len; + let max_index = len.saturating_sub(1); + let pos = self.cursor.map_or(max_index, |i| (i + max_index) % len) % len; self.cursor = Some(pos); self.adjust_scroll(); } -- cgit v1.2.3-70-g09d2 From f331ba9df4d7d22a5e9599737da581feb630f748 Mon Sep 17 00:00:00 2001 From: CossonLeo Date: Tue, 26 Oct 2021 08:42:37 +0800 Subject: Clear competion items when start_offset > cursor (#906) --- helix-term/src/ui/completion.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'helix-term/src') diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index 44879fcf..a893e70b 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -199,7 +199,7 @@ impl Completion { .selection(view.id) .primary() .cursor(doc.text().slice(..)); - if self.start_offset <= cursor { + if self.trigger_offset <= cursor { let fragment = doc.text().slice(self.start_offset..cursor); let text = Cow::from(fragment); // TODO: logic is same as ui/picker -- cgit v1.2.3-70-g09d2 From d61e5e686be14b61b6d26c591147f8bfedd378bf Mon Sep 17 00:00:00 2001 From: radical3dd Date: Tue, 26 Oct 2021 02:43:14 +0200 Subject: Use current dir for file picker, after change dir. (#910) --- helix-term/src/application.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'helix-term/src') diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 82ad04d7..662573c6 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -102,7 +102,7 @@ impl Application { if first.is_dir() { std::env::set_current_dir(&first)?; editor.new_file(Action::VerticalSplit); - compositor.push(Box::new(ui::file_picker(first.clone()))); + compositor.push(Box::new(ui::file_picker(".".into()))); } else { let nr_of_files = args.files.len(); editor.open(first.to_path_buf(), Action::VerticalSplit)?; -- cgit v1.2.3-70-g09d2 From 2505802d39f18f2f2dcfe8e00633f895c67beb76 Mon Sep 17 00:00:00 2001 From: Omnikar Date: Tue, 26 Oct 2021 23:24:24 -0400 Subject: Improve statusline (#916) * Improve statusline * Change diagnostic count display to show counts of individual diagnostic types next to their corresponding gutter dots. * Add selection count to the statusline. * Do not display info or hint count in statusline * Reduce padding Co-authored-by: Blaž Hrastnik * Reduce padding Co-authored-by: Blaž Hrastnik * Use `Span::styled` * Reduce padding * Use `Style::patch` * Remove unnecessary `Cow` creation Co-authored-by: Blaž Hrastnik --- helix-term/src/ui/editor.rs | 97 ++++++++++++++++++++++++++++++++------------- 1 file changed, 70 insertions(+), 27 deletions(-) (limited to 'helix-term/src') diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 97658c64..bf316ee3 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -548,6 +548,8 @@ impl EditorView { theme: &Theme, is_focused: bool, ) { + use tui::text::{Span, Spans}; + //------------------------------- // Left side of the status line. //------------------------------- @@ -566,17 +568,17 @@ impl EditorView { }) .unwrap_or(""); - let style = if is_focused { + let base_style = if is_focused { theme.get("ui.statusline") } else { theme.get("ui.statusline.inactive") }; // statusline - surface.set_style(viewport.with_height(1), style); + surface.set_style(viewport.with_height(1), base_style); if is_focused { - surface.set_string(viewport.x + 1, viewport.y, mode, style); + surface.set_string(viewport.x + 1, viewport.y, mode, base_style); } - surface.set_string(viewport.x + 5, viewport.y, progress, style); + surface.set_string(viewport.x + 5, viewport.y, progress, base_style); if let Some(path) = doc.relative_path() { let path = path.to_string_lossy(); @@ -587,7 +589,7 @@ impl EditorView { viewport.y, title, viewport.width.saturating_sub(6) as usize, - style, + base_style, ); } @@ -595,8 +597,50 @@ impl EditorView { // Right side of the status line. //------------------------------- - // Compute the individual info strings. - let diag_count = format!("{}", doc.diagnostics().len()); + let mut right_side_text = Spans::default(); + + // Compute the individual info strings and add them to `right_side_text`. + + // Diagnostics + let diags = doc.diagnostics().iter().fold((0, 0), |mut counts, diag| { + use helix_core::diagnostic::Severity; + match diag.severity { + Some(Severity::Warning) => counts.0 += 1, + Some(Severity::Error) | None => counts.1 += 1, + _ => {} + } + counts + }); + let (warnings, errors) = diags; + let warning_style = theme.get("warning"); + let error_style = theme.get("error"); + for i in 0..2 { + let (count, style) = match i { + 0 => (warnings, warning_style), + 1 => (errors, error_style), + _ => unreachable!(), + }; + if count == 0 { + continue; + } + let style = base_style.patch(style); + right_side_text.0.push(Span::styled("●", style)); + right_side_text + .0 + .push(Span::styled(format!(" {} ", count), base_style)); + } + + // Selections + let sels_count = doc.selection(view.id).len(); + right_side_text.0.push(Span::styled( + format!( + " {} sel{} ", + sels_count, + if sels_count == 1 { "" } else { "s" } + ), + base_style, + )); + // let indent_info = match doc.indent_style { // IndentStyle::Tabs => "tabs", // IndentStyle::Spaces(1) => "spaces:1", @@ -609,29 +653,28 @@ impl EditorView { // IndentStyle::Spaces(8) => "spaces:8", // _ => "indent:ERROR", // }; - let position_info = { - let pos = coords_at_pos( - doc.text().slice(..), - doc.selection(view.id) - .primary() - .cursor(doc.text().slice(..)), - ); - format!("{}:{}", pos.row + 1, pos.col + 1) // convert to 1-indexing - }; - // Render them to the status line together. - let right_side_text = format!( - "{} {} ", - &diag_count[..diag_count.len().min(4)], - // indent_info, - position_info + // Position + let pos = coords_at_pos( + doc.text().slice(..), + doc.selection(view.id) + .primary() + .cursor(doc.text().slice(..)), ); - let text_len = right_side_text.len() as u16; - surface.set_string( - viewport.x + viewport.width.saturating_sub(text_len), + right_side_text.0.push(Span::styled( + format!(" {}:{} ", pos.row + 1, pos.col + 1), // Convert to 1-indexing. + base_style, + )); + + // Render to the statusline. + surface.set_spans( + viewport.x + + viewport + .width + .saturating_sub(right_side_text.width() as u16), viewport.y, - right_side_text, - style, + &right_side_text, + right_side_text.width() as u16, ); } -- cgit v1.2.3-70-g09d2 From 1066b081ddb06a5370371ca139dc8e9b992c1242 Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Wed, 27 Oct 2021 18:23:17 +0900 Subject: fix: When cycling through prompt history, update event needs to trigger --- helix-term/src/ui/prompt.rs | 2 ++ 1 file changed, 2 insertions(+) (limited to 'helix-term/src') diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index 1d512ad2..853adfc2 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -501,6 +501,7 @@ impl Component for Prompt { if let Some(register) = self.history_register { let register = cx.editor.registers.get_mut(register); self.change_history(register.read(), CompletionDirection::Backward); + (self.callback_fn)(cx, &self.line, PromptEvent::Update); } } KeyEvent { @@ -514,6 +515,7 @@ impl Component for Prompt { if let Some(register) = self.history_register { let register = cx.editor.registers.get_mut(register); self.change_history(register.read(), CompletionDirection::Forward); + (self.callback_fn)(cx, &self.line, PromptEvent::Update); } } KeyEvent { -- cgit v1.2.3-70-g09d2 From fbba47fbc09d26cc41db87c5477dd7d27e8f6787 Mon Sep 17 00:00:00 2001 From: Nehliin Date: Sat, 16 Oct 2021 15:22:28 +0200 Subject: Fix panic when using multi-level key mapping --- helix-term/src/keymap.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'helix-term/src') diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 495fe892..c7b8e895 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -140,7 +140,7 @@ impl KeyTrieNode { } } body.sort_unstable_by_key(|(_, keys)| { - self.order.iter().position(|&k| k == keys[0]).unwrap() + self.order.iter().position(|&k| k == keys[0]).unwrap_or(0) }); let prefix = format!("{} ", self.name()); if body.iter().all(|(desc, _)| desc.starts_with(&prefix)) { -- cgit v1.2.3-70-g09d2 From f133d80e70622b9cf8b882473453aca02208024d Mon Sep 17 00:00:00 2001 From: Nehliin Date: Sat, 16 Oct 2021 15:22:36 +0200 Subject: Move test to test module --- helix-term/src/keymap.rs | 122 ++++++++++++++++++++++++----------------------- 1 file changed, 63 insertions(+), 59 deletions(-) (limited to 'helix-term/src') diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index c7b8e895..b91fa055 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -654,63 +654,67 @@ pub fn merge_keys(mut config: Config) -> Config { config } -#[test] -fn merge_partial_keys() { - let config = Config { - keys: Keymaps(hashmap! { - 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 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!( - keymap.get(key!('i')).kind, - KeymapResultKind::Matched(Command::normal_mode), - "Leaf should replace leaf" - ); - assert_eq!( - keymap.get(key!('无')).kind, - KeymapResultKind::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')).kind, - KeymapResultKind::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_last_line), - "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); +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn merge_partial_keys() { + let config = Config { + keys: Keymaps(hashmap! { + 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 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!( + keymap.get(key!('i')).kind, + KeymapResultKind::Matched(Command::normal_mode), + "Leaf should replace leaf" + ); + assert_eq!( + keymap.get(key!('无')).kind, + KeymapResultKind::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')).kind, + KeymapResultKind::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_last_line), + "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); + } } -- cgit v1.2.3-70-g09d2 From a4c5f467398fd3ad769907ca000dd812d5374abc Mon Sep 17 00:00:00 2001 From: Nehliin Date: Sun, 17 Oct 2021 16:06:08 +0200 Subject: Fix order being empty and add test --- helix-term/src/keymap.rs | 55 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 2 deletions(-) (limited to 'helix-term/src') diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index b91fa055..8f59060f 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -118,11 +118,22 @@ impl KeyTrieNode { } self.map.insert(key, trie); } + self.set_order(); + } - for &key in self.map.keys() { + /// Sets the order of the mapping recursivly since the + /// the trie can contain child nodes without order. + /// The order is missing from child nodes since it's not + /// parsed from the config.toml + fn set_order(&mut self) { + for (&key, trie) in self.map.iter_mut() { if !self.order.contains(&key) { self.order.push(key); } + // Order must be recursivly set + if let KeyTrie::Node(node) = trie { + node.set_order(); + } } } @@ -140,7 +151,7 @@ impl KeyTrieNode { } } body.sort_unstable_by_key(|(_, keys)| { - self.order.iter().position(|&k| k == keys[0]).unwrap_or(0) + self.order.iter().position(|&k| k == keys[0]).unwrap() }); let prefix = format!("{} ", self.name()); if body.iter().all(|(desc, _)| desc.starts_with(&prefix)) { @@ -151,6 +162,11 @@ impl KeyTrieNode { } Info::new(self.name(), body) } + + /// Get a reference to the key trie node's order. + pub fn order(&self) -> &[KeyEvent] { + self.order.as_slice() + } } impl Default for KeyTrieNode { @@ -235,6 +251,7 @@ pub enum KeymapResultKind { /// Returned after looking up a key in [`Keymap`]. The `sticky` field has a /// reference to the sticky node if one is currently active. +#[derive(Debug)] pub struct KeymapResult<'a> { pub kind: KeymapResultKind, pub sticky: Option<&'a KeyTrieNode>, @@ -717,4 +734,38 @@ mod tests { assert!(merged_config.keys.0.get(&Mode::Normal).unwrap().len() > 1); assert!(merged_config.keys.0.get(&Mode::Insert).unwrap().len() > 0); } + + #[test] + fn order_should_be_set() { + let config = Config { + keys: Keymaps(hashmap! { + Mode::Normal => Keymap::new( + keymap!({ "Normal mode" + "space" => { "" + "s" => { "" + "v" => vsplit, + "c" => hsplit, + }, + }, + }) + ) + }), + ..Default::default() + }; + let mut merged_config = merge_keys(config.clone()); + assert_ne!(config, merged_config); + let keymap = merged_config.keys.0.get_mut(&Mode::Normal).unwrap(); + // Make sure mapping works + assert_eq!( + keymap + .root() + .search(&[key!(' '), key!('s'), key!('v')]) + .unwrap(), + &KeyTrie::Leaf(Command::vsplit), + "Leaf should be present in merged subnode" + ); + // Make sure an order was set during merge + let node = keymap.root().search(&[crate::key!(' ')]).unwrap(); + assert!(!node.node().unwrap().order().is_empty()) + } } -- cgit v1.2.3-70-g09d2 From da4d9340baca311580ee7a10eaaec1f63a77bb60 Mon Sep 17 00:00:00 2001 From: Nehliin Date: Sun, 17 Oct 2021 16:06:21 +0200 Subject: Make key macro more portable --- helix-term/src/keymap.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'helix-term/src') diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 8f59060f..ed7f7edd 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -12,13 +12,13 @@ use std::{ #[macro_export] macro_rules! key { ($key:ident) => { - KeyEvent { + ::helix_view::input::KeyEvent { code: ::helix_view::keyboard::KeyCode::$key, modifiers: ::helix_view::keyboard::KeyModifiers::NONE, } }; ($($ch:tt)*) => { - KeyEvent { + ::helix_view::input::KeyEvent { code: ::helix_view::keyboard::KeyCode::Char($($ch)*), modifiers: ::helix_view::keyboard::KeyModifiers::NONE, } -- cgit v1.2.3-70-g09d2 From 6e455fd3fb7f0ce3715ca0af72197749a2f19f57 Mon Sep 17 00:00:00 2001 From: Oskar Nehlin Date: Mon, 18 Oct 2021 17:02:03 +0200 Subject: Apply suggestions from code review Co-authored-by: Blaž Hrastnik --- helix-term/src/keymap.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'helix-term/src') diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index ed7f7edd..d51204e1 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -121,7 +121,7 @@ impl KeyTrieNode { self.set_order(); } - /// Sets the order of the mapping recursivly since the + /// Sets the order of the mapping recursively since the /// the trie can contain child nodes without order. /// The order is missing from child nodes since it's not /// parsed from the config.toml @@ -130,7 +130,7 @@ impl KeyTrieNode { if !self.order.contains(&key) { self.order.push(key); } - // Order must be recursivly set + // Order must be recursively set if let KeyTrie::Node(node) = trie { node.set_order(); } -- cgit v1.2.3-70-g09d2 From 3b0c5e993a18a2d59582855784189995c7960d6f Mon Sep 17 00:00:00 2001 From: Nehliin Date: Sat, 23 Oct 2021 15:24:19 +0200 Subject: Use deserialization fix instead --- helix-term/src/keymap.rs | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) (limited to 'helix-term/src') diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index d51204e1..5453020e 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -78,19 +78,30 @@ macro_rules! keymap { }; } -#[derive(Debug, Clone, Deserialize)] +#[derive(Debug, Clone)] pub struct KeyTrieNode { /// A label for keys coming under this node, like "Goto mode" - #[serde(skip)] name: String, - #[serde(flatten)] map: HashMap, - #[serde(skip)] order: Vec, - #[serde(skip)] pub is_sticky: bool, } +impl<'de> Deserialize<'de> for KeyTrieNode { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let map = HashMap::::deserialize(deserializer)?; + let order = map.keys().copied().collect::>(); // NOTE: map.keys() has arbitrary order + Ok(Self { + map, + order, + ..Default::default() + }) + } +} + impl KeyTrieNode { pub fn new(name: &str, map: HashMap, order: Vec) -> Self { Self { @@ -118,22 +129,10 @@ impl KeyTrieNode { } self.map.insert(key, trie); } - self.set_order(); - } - - /// Sets the order of the mapping recursively since the - /// the trie can contain child nodes without order. - /// The order is missing from child nodes since it's not - /// parsed from the config.toml - fn set_order(&mut self) { - for (&key, trie) in self.map.iter_mut() { + for &key in self.map.keys() { if !self.order.contains(&key) { self.order.push(key); } - // Order must be recursively set - if let KeyTrie::Node(node) = trie { - node.set_order(); - } } } -- cgit v1.2.3-70-g09d2 From e2ed6915373adf27881325ebc4e2c6b98e207af3 Mon Sep 17 00:00:00 2001 From: Omnikar Date: Wed, 27 Oct 2021 21:23:46 -0400 Subject: Implement `hx --tutor` and `:tutor` to load `tutor.txt` (#898) * Implement `hx --tutor` and `:tutor` to load `tutor.txt` * Document `hx --tutor` and `:tutor` * Change `Document::set_path` to take an `Option` * `Document::set_path` accepts an `Option<&Path>` instead of `&Path`. * Remove `Editor::open_tutor` and make tutor-open functionality use `Editor::open` and `Document::set_path`. * Use `PathBuf::join` Co-authored-by: Ivan Tham * Add comments explaining unsetting tutor path Co-authored-by: Ivan Tham --- book/src/usage.md | 2 +- helix-term/src/application.rs | 7 ++++++- helix-term/src/args.rs | 2 ++ helix-term/src/commands.rs | 24 ++++++++++++++++++++++-- helix-view/src/document.rs | 12 ++++++++---- 5 files changed, 39 insertions(+), 8 deletions(-) (limited to 'helix-term/src') diff --git a/book/src/usage.md b/book/src/usage.md index d31e03a1..71730fa8 100644 --- a/book/src/usage.md +++ b/book/src/usage.md @@ -2,7 +2,7 @@ (Currently not fully documented, see the [keymappings](./keymap.md) list for more.) -See [tutor.txt](https://github.com/helix-editor/helix/blob/master/runtime/tutor.txt) for a vimtutor-like introduction. +See [tutor.txt](https://github.com/helix-editor/helix/blob/master/runtime/tutor.txt) (accessible via `hx --tutor` or `:tutor`) for a vimtutor-like introduction. ## Registers diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 662573c6..55b12c5a 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -97,7 +97,12 @@ impl Application { let editor_view = Box::new(ui::EditorView::new(std::mem::take(&mut config.keys))); compositor.push(editor_view); - if !args.files.is_empty() { + if args.load_tutor { + let path = helix_core::runtime_dir().join("tutor.txt"); + editor.open(path, Action::VerticalSplit)?; + // Unset path to prevent accidentally saving to the original tutor file. + doc_mut!(editor).set_path(None)?; + } else if !args.files.is_empty() { let first = &args.files[0]; // we know it's not empty if first.is_dir() { std::env::set_current_dir(&first)?; diff --git a/helix-term/src/args.rs b/helix-term/src/args.rs index f0ef09eb..40113db9 100644 --- a/helix-term/src/args.rs +++ b/helix-term/src/args.rs @@ -5,6 +5,7 @@ use std::path::PathBuf; pub struct Args { pub display_help: bool, pub display_version: bool, + pub load_tutor: bool, pub verbosity: u64, pub files: Vec, } @@ -22,6 +23,7 @@ impl Args { "--" => break, // stop parsing at this point treat the remaining as files "--version" => args.display_version = true, "--help" => args.display_help = true, + "--tutor" => args.load_tutor = true, arg if arg.starts_with("--") => { return Err(Error::msg(format!( "unexpected double dash argument: {}", diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index d6e5bfe7..b3be6d5b 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1557,7 +1557,8 @@ mod cmd { let (_, doc) = current!(cx.editor); if let Some(path) = path { - doc.set_path(path.as_ref()).context("invalid filepath")?; + doc.set_path(Some(path.as_ref())) + .context("invalid filepath")?; } if doc.path().is_none() { bail!("cannot write a buffer without a filename"); @@ -2099,6 +2100,18 @@ mod cmd { Ok(()) } + fn tutor( + cx: &mut compositor::Context, + _args: &[&str], + _event: PromptEvent, + ) -> anyhow::Result<()> { + let path = helix_core::runtime_dir().join("tutor.txt"); + cx.editor.open(path, Action::Replace)?; + // Unset path to prevent accidentally saving to the original tutor file. + doc_mut!(cx.editor).set_path(None)?; + Ok(()) + } + pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ TypableCommand { name: "quit", @@ -2351,7 +2364,14 @@ mod cmd { doc: "Open the file in a horizontal split.", fun: hsplit, completer: Some(completers::filename), - } + }, + TypableCommand { + name: "tutor", + aliases: &[], + doc: "Open the tutorial.", + fun: tutor, + completer: None, + }, ]; pub static COMMANDS: Lazy> = Lazy::new(|| { diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index b0257f03..4d779656 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -368,7 +368,7 @@ impl Document { let mut doc = Self::from(rope, Some(encoding)); // set the path and try detecting the language - doc.set_path(path)?; + doc.set_path(Some(path))?; if let Some(loader) = config_loader { doc.detect_language(theme, loader); } @@ -553,12 +553,16 @@ impl Document { self.encoding } - pub fn set_path(&mut self, path: &Path) -> Result<(), std::io::Error> { - let path = helix_core::path::get_canonicalized_path(path)?; + pub fn set_path(&mut self, path: Option<&Path>) -> Result<(), std::io::Error> { + let path = if let Some(p) = path { + Some(helix_core::path::get_canonicalized_path(p)?) + } else { + path.map(|p| p.into()) + }; // if parent doesn't exist we still want to open the document // and error out when document is saved - self.path = Some(path); + self.path = path; Ok(()) } -- cgit v1.2.3-70-g09d2 From db56de589a2b5e5ad3c0ceba05f91372c382f538 Mon Sep 17 00:00:00 2001 From: Omnikar Date: Thu, 28 Oct 2021 03:27:28 -0400 Subject: Add `--tutor` option to `hx --help` output (#924) * Add `--tutor` option to `hx --help` output * Adjust `--tutor` location in help output--- helix-term/src/main.rs | 1 + 1 file changed, 1 insertion(+) (limited to 'helix-term/src') diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs index 2589a375..f746895c 100644 --- a/helix-term/src/main.rs +++ b/helix-term/src/main.rs @@ -60,6 +60,7 @@ ARGS: FLAGS: -h, --help Prints help information + --tutor Loads the tutorial -v Increases logging verbosity each use for up to 3 times (default file: {}) -V, --version Prints version information -- cgit v1.2.3-70-g09d2 From f1d339919f4299d570bb6f7fcf5fcc58c0f281cf Mon Sep 17 00:00:00 2001 From: cossonleo Date: Wed, 27 Oct 2021 17:42:11 +0800 Subject: add expand_selection to last_motion --- helix-term/src/commands.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) (limited to 'helix-term/src') diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index b3be6d5b..08750aeb 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -4348,13 +4348,17 @@ fn rotate_selection_contents_backward(cx: &mut Context) { // tree sitter node selection fn expand_selection(cx: &mut Context) { - let (view, doc) = current!(cx.editor); + let motion = |editor: &mut Editor| { + let (view, doc) = current!(editor); - if let Some(syntax) = doc.syntax() { - let text = doc.text().slice(..); - let selection = object::expand_selection(syntax, text, doc.selection(view.id)); - doc.set_selection(view.id, selection); - } + if let Some(syntax) = doc.syntax() { + let text = doc.text().slice(..); + let selection = object::expand_selection(syntax, text, doc.selection(view.id)); + doc.set_selection(view.id, selection); + } + }; + motion(&mut cx.editor); + cx.editor.last_motion = Some(Motion(Box::new(motion))); } fn match_brackets(cx: &mut Context) { -- cgit v1.2.3-70-g09d2 From 21d535565bfcc5ff4e2eae34329b051944a8d1f5 Mon Sep 17 00:00:00 2001 From: Ivan Tham Date: Fri, 29 Oct 2021 09:07:07 +0800 Subject: Support extend for multiple goto (#909) gg, ge, [n]gg--- helix-term/src/commands.rs | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) (limited to 'helix-term/src') diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 08750aeb..9eda2c23 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -622,14 +622,25 @@ fn goto_file_start(cx: &mut Context) { } else { push_jump(cx.editor); let (view, doc) = current!(cx.editor); - doc.set_selection(view.id, Selection::point(0)); + let text = doc.text().slice(..); + let selection = doc + .selection(view.id) + .clone() + .transform(|range| range.put_cursor(text, 0, doc.mode == Mode::Select)); + doc.set_selection(view.id, selection); } } fn goto_file_end(cx: &mut Context) { push_jump(cx.editor); let (view, doc) = current!(cx.editor); - doc.set_selection(view.id, Selection::point(doc.text().len_chars())); + let text = doc.text().slice(..); + let pos = doc.text().len_chars(); + let selection = doc + .selection(view.id) + .clone() + .transform(|range| range.put_cursor(text, pos, doc.mode == Mode::Select)); + doc.set_selection(view.id, selection); } fn extend_word_impl(cx: &mut Context, extend_fn: F) @@ -2905,8 +2916,13 @@ fn goto_line(cx: &mut Context) { doc.text().len_lines() - 1 }; let line_idx = std::cmp::min(count.get() - 1, max_line); + let text = doc.text().slice(..); let pos = doc.text().line_to_char(line_idx); - doc.set_selection(view.id, Selection::point(pos)); + let selection = doc + .selection(view.id) + .clone() + .transform(|range| range.put_cursor(text, pos, doc.mode == Mode::Select)); + doc.set_selection(view.id, selection); } } @@ -2920,8 +2936,13 @@ fn goto_last_line(cx: &mut Context) { } else { doc.text().len_lines() - 1 }; + let text = doc.text().slice(..); let pos = doc.text().line_to_char(line_idx); - doc.set_selection(view.id, Selection::point(pos)); + let selection = doc + .selection(view.id) + .clone() + .transform(|range| range.put_cursor(text, pos, doc.mode == Mode::Select)); + doc.set_selection(view.id, selection); } fn goto_last_accessed_file(cx: &mut Context) { -- cgit v1.2.3-70-g09d2 From bc6a34d97edae55811c2476278a6288d7d258af3 Mon Sep 17 00:00:00 2001 From: Ivan Tham Date: Fri, 29 Oct 2021 09:08:53 +0800 Subject: Make match work with extend and multi cursors (#920) --- helix-term/src/commands.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) (limited to 'helix-term/src') diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 9eda2c23..28657865 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -4386,14 +4386,15 @@ fn match_brackets(cx: &mut Context) { let (view, doc) = current!(cx.editor); if let Some(syntax) = doc.syntax() { - let pos = doc - .selection(view.id) - .primary() - .cursor(doc.text().slice(..)); - if let Some(pos) = match_brackets::find(syntax, doc.text(), pos) { - let selection = Selection::point(pos); - doc.set_selection(view.id, selection); - }; + let text = doc.text().slice(..); + let selection = doc.selection(view.id).clone().transform(|range| { + if let Some(pos) = match_brackets::find(syntax, doc.text(), range.anchor) { + range.put_cursor(text, pos, doc.mode == Mode::Select) + } else { + range + } + }); + doc.set_selection(view.id, selection); } } -- cgit v1.2.3-70-g09d2 From 49f6c2623fbda5ff4be86e5e7d773bf900d9c75c Mon Sep 17 00:00:00 2001 From: Ivan Tham Date: Fri, 29 Oct 2021 11:00:18 +0800 Subject: Bump lsp-types to 0.91.0 (#932) --- Cargo.lock | 4 ++-- helix-lsp/Cargo.toml | 2 +- helix-lsp/src/client.rs | 7 +++--- helix-term/src/application.rs | 9 ++++---- helix-term/src/ui/completion.rs | 51 +++++++++++++++++++++-------------------- 5 files changed, 38 insertions(+), 35 deletions(-) (limited to 'helix-term/src') diff --git a/Cargo.lock b/Cargo.lock index 45a8f5da..8af5c45c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -584,9 +584,9 @@ dependencies = [ [[package]] name = "lsp-types" -version = "0.90.1" +version = "0.91.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f3734ab1d7d157fc0c45110e06b587c31cd82bea2ccfd6b563cbff0aaeeb1d3" +checksum = "be7801b458592d0998af808d97f6a85a6057af3aaf2a2a5c3c677702bbeb4ed7" dependencies = [ "bitflags", "serde", diff --git a/helix-lsp/Cargo.toml b/helix-lsp/Cargo.toml index f9910cc0..8cbff41d 100644 --- a/helix-lsp/Cargo.toml +++ b/helix-lsp/Cargo.toml @@ -19,7 +19,7 @@ futures-executor = "0.3" futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false } jsonrpc-core = { version = "18.0", default-features = false } # don't pull in all of futures log = "0.4" -lsp-types = { version = "0.90", features = ["proposed"] } +lsp-types = { version = "0.91", features = ["proposed"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" thiserror = "1.0" diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index 4068ae1f..b810feef 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -461,7 +461,7 @@ impl Client { }; let changes = match sync_capabilities { - lsp::TextDocumentSyncKind::Full => { + lsp::TextDocumentSyncKind::FULL => { vec![lsp::TextDocumentContentChangeEvent { // range = None -> whole document range: None, //Some(Range) @@ -469,10 +469,11 @@ impl Client { text: new_text.to_string(), }] } - lsp::TextDocumentSyncKind::Incremental => { + lsp::TextDocumentSyncKind::INCREMENTAL => { Self::changeset_to_changes(old_text, new_text, changes, self.offset_encoding) } - lsp::TextDocumentSyncKind::None => return None, + lsp::TextDocumentSyncKind::NONE => return None, + kind => unimplemented!("{:?}", kind), }; Some(self.notify::( diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 55b12c5a..6037148f 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -389,10 +389,11 @@ impl Application { message: diagnostic.message, severity: diagnostic.severity.map( |severity| match severity { - DiagnosticSeverity::Error => Error, - DiagnosticSeverity::Warning => Warning, - DiagnosticSeverity::Information => Info, - DiagnosticSeverity::Hint => Hint, + DiagnosticSeverity::ERROR => Error, + DiagnosticSeverity::WARNING => Warning, + DiagnosticSeverity::INFORMATION => Info, + DiagnosticSeverity::HINT => Hint, + severity => unimplemented!("{:?}", severity), }, ), // code diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index a893e70b..dcb2bfd8 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -30,31 +30,32 @@ impl menu::Item for CompletionItem { menu::Row::new(vec![ menu::Cell::from(self.label.as_str()), menu::Cell::from(match self.kind { - Some(lsp::CompletionItemKind::Text) => "text", - Some(lsp::CompletionItemKind::Method) => "method", - Some(lsp::CompletionItemKind::Function) => "function", - Some(lsp::CompletionItemKind::Constructor) => "constructor", - Some(lsp::CompletionItemKind::Field) => "field", - Some(lsp::CompletionItemKind::Variable) => "variable", - Some(lsp::CompletionItemKind::Class) => "class", - Some(lsp::CompletionItemKind::Interface) => "interface", - Some(lsp::CompletionItemKind::Module) => "module", - Some(lsp::CompletionItemKind::Property) => "property", - Some(lsp::CompletionItemKind::Unit) => "unit", - Some(lsp::CompletionItemKind::Value) => "value", - Some(lsp::CompletionItemKind::Enum) => "enum", - Some(lsp::CompletionItemKind::Keyword) => "keyword", - Some(lsp::CompletionItemKind::Snippet) => "snippet", - Some(lsp::CompletionItemKind::Color) => "color", - Some(lsp::CompletionItemKind::File) => "file", - Some(lsp::CompletionItemKind::Reference) => "reference", - Some(lsp::CompletionItemKind::Folder) => "folder", - Some(lsp::CompletionItemKind::EnumMember) => "enum_member", - Some(lsp::CompletionItemKind::Constant) => "constant", - Some(lsp::CompletionItemKind::Struct) => "struct", - Some(lsp::CompletionItemKind::Event) => "event", - Some(lsp::CompletionItemKind::Operator) => "operator", - Some(lsp::CompletionItemKind::TypeParameter) => "type_param", + Some(lsp::CompletionItemKind::TEXT) => "text", + Some(lsp::CompletionItemKind::METHOD) => "method", + Some(lsp::CompletionItemKind::FUNCTION) => "function", + Some(lsp::CompletionItemKind::CONSTRUCTOR) => "constructor", + Some(lsp::CompletionItemKind::FIELD) => "field", + Some(lsp::CompletionItemKind::VARIABLE) => "variable", + Some(lsp::CompletionItemKind::CLASS) => "class", + Some(lsp::CompletionItemKind::INTERFACE) => "interface", + Some(lsp::CompletionItemKind::MODULE) => "module", + Some(lsp::CompletionItemKind::PROPERTY) => "property", + Some(lsp::CompletionItemKind::UNIT) => "unit", + Some(lsp::CompletionItemKind::VALUE) => "value", + Some(lsp::CompletionItemKind::ENUM) => "enum", + Some(lsp::CompletionItemKind::KEYWORD) => "keyword", + Some(lsp::CompletionItemKind::SNIPPET) => "snippet", + Some(lsp::CompletionItemKind::COLOR) => "color", + Some(lsp::CompletionItemKind::FILE) => "file", + Some(lsp::CompletionItemKind::REFERENCE) => "reference", + Some(lsp::CompletionItemKind::FOLDER) => "folder", + Some(lsp::CompletionItemKind::ENUM_MEMBER) => "enum_member", + Some(lsp::CompletionItemKind::CONSTANT) => "constant", + Some(lsp::CompletionItemKind::STRUCT) => "struct", + Some(lsp::CompletionItemKind::EVENT) => "event", + Some(lsp::CompletionItemKind::OPERATOR) => "operator", + Some(lsp::CompletionItemKind::TYPE_PARAMETER) => "type_param", + Some(kind) => unimplemented!("{:?}", kind), None => "", }), // self.detail.as_deref().unwrap_or("") -- cgit v1.2.3-70-g09d2 From e5de103728b7a1338056f70524362930695d6c85 Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Fri, 29 Oct 2021 16:48:25 +0900 Subject: Extract a clear_completion method --- helix-term/src/ui/editor.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) (limited to 'helix-term/src') diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index bf316ee3..c0d602c7 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -786,6 +786,14 @@ impl EditorView { completion.required_size((size.width, size.height)); self.completion = Some(completion); } + + pub fn clear_completion(&mut self, editor: &mut Editor) { + self.completion = None; + // Clear any savepoints + let (_, doc) = current!(editor); + doc.savepoint = None; + editor.clear_idle_timer(); // don't retrigger + } } impl EditorView { @@ -990,11 +998,7 @@ impl Component for EditorView { if callback.is_some() { // assume close_fn - self.completion = None; - // Clear any savepoints - let (_, doc) = current!(cxt.editor); - doc.savepoint = None; - cxt.editor.clear_idle_timer(); // don't retrigger + self.clear_completion(cxt.editor); } } } @@ -1007,11 +1011,7 @@ impl Component for EditorView { if let Some(completion) = &mut self.completion { completion.update(&mut cxt); if completion.is_empty() { - self.completion = None; - // Clear any savepoints - let (_, doc) = current!(cxt.editor); - doc.savepoint = None; - cxt.editor.clear_idle_timer(); // don't retrigger + self.clear_completion(cxt.editor); } } } -- cgit v1.2.3-70-g09d2 From f140a2a00eecfe47115634cef3bad4fd51e03b71 Mon Sep 17 00:00:00 2001 From: Gygaxis Vainhardt Date: Fri, 29 Oct 2021 22:48:00 -0300 Subject: Add arrow-key bindings for window switching (#933) --- book/src/keymap.md | 8 ++++---- helix-term/src/keymap.rs | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) (limited to 'helix-term/src') diff --git a/book/src/keymap.md b/book/src/keymap.md index 2ff8bfe6..9ed35f77 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -188,10 +188,10 @@ This layer is similar to vim keybindings as kakoune does not support window. | `w`, `Ctrl-w` | Switch to next window | `rotate_view` | | `v`, `Ctrl-v` | Vertical right split | `vsplit` | | `s`, `Ctrl-s` | Horizontal bottom split | `hsplit` | -| `h`, `Ctrl-h` | Move to left split | `jump_view_left` | -| `j`, `Ctrl-j` | Move to split below | `jump_view_down` | -| `k`, `Ctrl-k` | Move to split above | `jump_view_up` | -| `l`, `Ctrl-l` | Move to right split | `jump_view_right` | +| `h`, `Ctrl-h`, `left` | Move to left split | `jump_view_left` | +| `j`, `Ctrl-j`, `down` | Move to split below | `jump_view_down` | +| `k`, `Ctrl-k`, `up` | Move to split above | `jump_view_up` | +| `l`, `Ctrl-l`, `right` | Move to right split | `jump_view_right` | | `q`, `Ctrl-q` | Close current window | `wclose` | #### Space mode diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 5453020e..cd953c5c 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -540,10 +540,10 @@ impl Default for Keymaps { "C-s" | "s" => hsplit, "C-v" | "v" => vsplit, "C-q" | "q" => wclose, - "C-h" | "h" => jump_view_left, - "C-j" | "j" => jump_view_down, - "C-k" | "k" => jump_view_up, - "C-l" | "l" => jump_view_right, + "C-h" | "h" | "left" => jump_view_left, + "C-j" | "j" | "down" => jump_view_down, + "C-k" | "k" | "up" => jump_view_up, + "C-l" | "l" | "right" => jump_view_right, }, // move under c -- cgit v1.2.3-70-g09d2 From 1720b98760eb5958a31bc6e2b88564dbf5bf63e5 Mon Sep 17 00:00:00 2001 From: CossonLeo Date: Tue, 2 Nov 2021 12:32:57 +0800 Subject: only remove primary index when search next without extend (#948) --- helix-term/src/commands.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'helix-term/src') diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 28657865..cc9106eb 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1161,7 +1161,10 @@ fn search_impl(doc: &mut Document, view: &mut View, contents: &str, regex: &Rege let selection = if extend { selection.clone().push(Range::new(start, end)) } else { - Selection::single(start, end) + selection + .clone() + .remove(selection.primary_index()) + .push(Range::new(start, end)) }; doc.set_selection(view.id, selection); -- cgit v1.2.3-70-g09d2 From eb8745db0999a50464ac183baa138c4e511430f2 Mon Sep 17 00:00:00 2001 From: Daniel Ebert Date: Mon, 1 Nov 2021 15:50:12 +0100 Subject: Implement key ordering for info box --- helix-term/src/keymap.rs | 20 ++++++++++++++------ helix-view/src/info.rs | 4 ++-- helix-view/src/input.rs | 2 +- helix-view/src/keyboard.rs | 2 +- 4 files changed, 18 insertions(+), 10 deletions(-) (limited to 'helix-term/src') diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index cd953c5c..827f71d9 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -5,7 +5,7 @@ use helix_view::{document::Mode, info::Info, input::KeyEvent}; use serde::Deserialize; use std::{ borrow::Cow, - collections::HashMap, + collections::{BTreeSet, HashMap}, ops::{Deref, DerefMut}, }; @@ -137,20 +137,28 @@ impl KeyTrieNode { } pub fn infobox(&self) -> Info { - let mut body: Vec<(&str, Vec)> = Vec::with_capacity(self.len()); + let mut body: Vec<(&str, BTreeSet)> = Vec::with_capacity(self.len()); for (&key, trie) in self.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])), + Some(pos) => { + body[pos].1.insert(key); + } + None => { + let mut keys = BTreeSet::new(); + keys.insert(key); + body.push((desc, keys)); + } } } body.sort_unstable_by_key(|(_, keys)| { - self.order.iter().position(|&k| k == keys[0]).unwrap() + self.order + .iter() + .position(|&k| k == *keys.iter().next().unwrap()) + .unwrap() }); let prefix = format!("{} ", self.name()); if body.iter().all(|(desc, _)| desc.starts_with(&prefix)) { diff --git a/helix-view/src/info.rs b/helix-view/src/info.rs index 629a3112..b5a002fa 100644 --- a/helix-view/src/info.rs +++ b/helix-view/src/info.rs @@ -1,6 +1,6 @@ use crate::input::KeyEvent; use helix_core::unicode::width::UnicodeWidthStr; -use std::fmt::Write; +use std::{collections::BTreeSet, fmt::Write}; #[derive(Debug)] /// Info box used in editor. Rendering logic will be in other crate. @@ -16,7 +16,7 @@ pub struct Info { } impl Info { - pub fn new(title: &str, body: Vec<(&str, Vec)>) -> Info { + pub fn new(title: &str, body: Vec<(&str, BTreeSet)>) -> Info { let body = body .into_iter() .map(|(desc, events)| { diff --git a/helix-view/src/input.rs b/helix-view/src/input.rs index 1e0ddfe2..580204cc 100644 --- a/helix-view/src/input.rs +++ b/helix-view/src/input.rs @@ -8,7 +8,7 @@ use crate::keyboard::{KeyCode, KeyModifiers}; /// Represents a key event. // We use a newtype here because we want to customize Deserialize and Display. -#[derive(Debug, PartialEq, Eq, PartialOrd, Clone, Copy, Hash)] +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)] pub struct KeyEvent { pub code: KeyCode, pub modifiers: KeyModifiers, diff --git a/helix-view/src/keyboard.rs b/helix-view/src/keyboard.rs index 26a4d6d2..810aa063 100644 --- a/helix-view/src/keyboard.rs +++ b/helix-view/src/keyboard.rs @@ -54,7 +54,7 @@ impl From for KeyModifiers { } /// Represents a key. -#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)] +#[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Clone, Copy, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum KeyCode { /// Backspace key. -- cgit v1.2.3-70-g09d2 From 7a0c4322eaeef7325878abe9a99adde4ad905f5e Mon Sep 17 00:00:00 2001 From: Triton171 Date: Tue, 2 Nov 2021 17:43:07 +0100 Subject: Simplify BTreeSet construction Co-authored-by: Ivan Tham --- helix-term/src/keymap.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) (limited to 'helix-term/src') diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 827f71d9..72d0a733 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -147,11 +147,7 @@ impl KeyTrieNode { Some(pos) => { body[pos].1.insert(key); } - None => { - let mut keys = BTreeSet::new(); - keys.insert(key); - body.push((desc, keys)); - } + None => body.push((desc, BTreeSet::from([key]))), } } body.sort_unstable_by_key(|(_, keys)| { -- cgit v1.2.3-70-g09d2 From 3eb829e2330fed5ad1c095f8bba44f62361b4943 Mon Sep 17 00:00:00 2001 From: Ivan Tham Date: Wed, 3 Nov 2021 11:02:29 +0800 Subject: Ensure coords in screen depends on char width (#885) The issue affected files with lots of tabs at the start as well. Fix #840--- helix-core/src/lib.rs | 2 +- helix-core/src/movement.rs | 4 ++ helix-core/src/position.rs | 81 +++++++++++++++++++++++++++++++++++++---- helix-term/src/ui/completion.rs | 10 ++--- helix-view/src/view.rs | 8 ++-- 5 files changed, 87 insertions(+), 18 deletions(-) (limited to 'helix-term/src') diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs index 96f88ee4..d1720df0 100644 --- a/helix-core/src/lib.rs +++ b/helix-core/src/lib.rs @@ -194,7 +194,7 @@ pub use tendril::StrTendril as Tendril; pub use {regex, tree_sitter}; pub use graphemes::RopeGraphemes; -pub use position::{coords_at_pos, pos_at_coords, Position}; +pub use position::{coords_at_pos, pos_at_coords, visual_coords_at_pos, Position}; pub use selection::{Range, Selection}; pub use smallvec::SmallVec; pub use syntax::Syntax; diff --git a/helix-core/src/movement.rs b/helix-core/src/movement.rs index 5d080545..9e85bd21 100644 --- a/helix-core/src/movement.rs +++ b/helix-core/src/movement.rs @@ -53,6 +53,10 @@ pub fn move_vertically( let pos = range.cursor(slice); // Compute the current position's 2d coordinates. + // TODO: switch this to use `visual_coords_at_pos` rather than + // `coords_at_pos` as this will cause a jerky movement when the visual + // position does not match, like moving from a line with tabs/CJK to + // a line without let Position { row, col } = coords_at_pos(slice, pos); let horiz = range.horiz.unwrap_or(col as u32); diff --git a/helix-core/src/position.rs b/helix-core/src/position.rs index 08a8aed5..c6018ce6 100644 --- a/helix-core/src/position.rs +++ b/helix-core/src/position.rs @@ -2,6 +2,7 @@ use crate::{ chars::char_is_line_ending, graphemes::{ensure_grapheme_boundary_prev, RopeGraphemes}, line_ending::line_end_char_index, + unicode::width::UnicodeWidthChar, RopeSlice, }; @@ -54,11 +55,8 @@ impl From for tree_sitter::Point { } /// Convert a character index to (line, column) coordinates. /// -/// TODO: this should be split into two methods: one for visual -/// row/column, and one for "objective" row/column (possibly with -/// the column specified in `char`s). The former would be used -/// for cursor movement, and the latter would be used for e.g. the -/// row:column display in the status line. +/// column in `char` count which can be used for row:column display in +/// status line. See [`visual_coords_at_pos`] for a visual one. pub fn coords_at_pos(text: RopeSlice, pos: usize) -> Position { let line = text.char_to_line(pos); @@ -69,6 +67,28 @@ pub fn coords_at_pos(text: RopeSlice, pos: usize) -> Position { Position::new(line, col) } +/// Convert a character index to (line, column) coordinates visually. +/// +/// Takes \t, double-width characters (CJK) into account as well as text +/// not in the document in the future. +/// See [`coords_at_pos`] for an "objective" one. +pub fn visual_coords_at_pos(text: RopeSlice, pos: usize, tab_width: usize) -> Position { + let line = text.char_to_line(pos); + + let line_start = text.line_to_char(line); + let pos = ensure_grapheme_boundary_prev(text, pos); + let col = text + .slice(line_start..pos) + .chars() + .flat_map(|c| match c { + '\t' => Some(tab_width), + c => UnicodeWidthChar::width(c), + }) + .sum(); + + Position::new(line, col) +} + /// Convert (line, column) coordinates to a character index. /// /// If the `line` coordinate is beyond the end of the file, the EOF @@ -130,7 +150,6 @@ mod test { assert_eq!(coords_at_pos(slice, 10), (1, 4).into()); // position on d // Test with wide characters. - // TODO: account for character width. let text = Rope::from("今日はいい\n"); let slice = text.slice(..); assert_eq!(coords_at_pos(slice, 0), (0, 0).into()); @@ -151,7 +170,6 @@ mod test { assert_eq!(coords_at_pos(slice, 9), (1, 0).into()); // Test with wide-character grapheme clusters. - // TODO: account for character width. let text = Rope::from("किमपि\n"); let slice = text.slice(..); assert_eq!(coords_at_pos(slice, 0), (0, 0).into()); @@ -161,7 +179,6 @@ mod test { assert_eq!(coords_at_pos(slice, 6), (1, 0).into()); // Test with tabs. - // Todo: account for tab stops. let text = Rope::from("\tHello\n"); let slice = text.slice(..); assert_eq!(coords_at_pos(slice, 0), (0, 0).into()); @@ -169,6 +186,54 @@ mod test { assert_eq!(coords_at_pos(slice, 2), (0, 2).into()); } + #[test] + fn test_visual_coords_at_pos() { + let text = Rope::from("ḧëḷḷö\nẅöṛḷḋ"); + let slice = text.slice(..); + assert_eq!(visual_coords_at_pos(slice, 0, 8), (0, 0).into()); + assert_eq!(visual_coords_at_pos(slice, 5, 8), (0, 5).into()); // position on \n + assert_eq!(visual_coords_at_pos(slice, 6, 8), (1, 0).into()); // position on w + assert_eq!(visual_coords_at_pos(slice, 7, 8), (1, 1).into()); // position on o + assert_eq!(visual_coords_at_pos(slice, 10, 8), (1, 4).into()); // position on d + + // Test with wide characters. + let text = Rope::from("今日はいい\n"); + let slice = text.slice(..); + assert_eq!(visual_coords_at_pos(slice, 0, 8), (0, 0).into()); + assert_eq!(visual_coords_at_pos(slice, 1, 8), (0, 2).into()); + assert_eq!(visual_coords_at_pos(slice, 2, 8), (0, 4).into()); + assert_eq!(visual_coords_at_pos(slice, 3, 8), (0, 6).into()); + assert_eq!(visual_coords_at_pos(slice, 4, 8), (0, 8).into()); + assert_eq!(visual_coords_at_pos(slice, 5, 8), (0, 10).into()); + assert_eq!(visual_coords_at_pos(slice, 6, 8), (1, 0).into()); + + // Test with grapheme clusters. + let text = Rope::from("a̐éö̲\r\n"); + let slice = text.slice(..); + assert_eq!(visual_coords_at_pos(slice, 0, 8), (0, 0).into()); + assert_eq!(visual_coords_at_pos(slice, 2, 8), (0, 1).into()); + assert_eq!(visual_coords_at_pos(slice, 4, 8), (0, 2).into()); + assert_eq!(visual_coords_at_pos(slice, 7, 8), (0, 3).into()); + assert_eq!(visual_coords_at_pos(slice, 9, 8), (1, 0).into()); + + // Test with wide-character grapheme clusters. + // TODO: account for cluster. + let text = Rope::from("किमपि\n"); + let slice = text.slice(..); + assert_eq!(visual_coords_at_pos(slice, 0, 8), (0, 0).into()); + assert_eq!(visual_coords_at_pos(slice, 2, 8), (0, 2).into()); + assert_eq!(visual_coords_at_pos(slice, 3, 8), (0, 3).into()); + assert_eq!(visual_coords_at_pos(slice, 5, 8), (0, 5).into()); + assert_eq!(visual_coords_at_pos(slice, 6, 8), (1, 0).into()); + + // Test with tabs. + let text = Rope::from("\tHello\n"); + let slice = text.slice(..); + assert_eq!(visual_coords_at_pos(slice, 0, 8), (0, 0).into()); + assert_eq!(visual_coords_at_pos(slice, 1, 8), (0, 8).into()); + assert_eq!(visual_coords_at_pos(slice, 2, 8), (0, 9).into()); + } + #[test] fn test_pos_at_coords() { let text = Rope::from("ḧëḷḷö\nẅöṛḷḋ"); diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index dcb2bfd8..dd782d29 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -264,12 +264,10 @@ impl Component for Completion { .language() .and_then(|scope| scope.strip_prefix("source.")) .unwrap_or(""); - let cursor_pos = doc - .selection(view.id) - .primary() - .cursor(doc.text().slice(..)); - let cursor_pos = (helix_core::coords_at_pos(doc.text().slice(..), cursor_pos).row - - view.offset.row) as u16; + let text = doc.text().slice(..); + let cursor_pos = doc.selection(view.id).primary().cursor(text); + let coords = helix_core::visual_coords_at_pos(text, cursor_pos, doc.tab_width()); + let cursor_pos = (coords.row - view.offset.row) as u16; let mut markdown_doc = match &option.documentation { Some(lsp::Documentation::String(contents)) | Some(lsp::Documentation::MarkupContent(lsp::MarkupContent { diff --git a/helix-view/src/view.rs b/helix-view/src/view.rs index 01f18c71..11f30155 100644 --- a/helix-view/src/view.rs +++ b/helix-view/src/view.rs @@ -2,10 +2,9 @@ use std::borrow::Cow; use crate::{graphics::Rect, Document, DocumentId, ViewId}; use helix_core::{ - coords_at_pos, graphemes::{grapheme_width, RopeGraphemes}, line_ending::line_end_char_index, - Position, RopeSlice, Selection, + visual_coords_at_pos, Position, RopeSlice, Selection, }; type Jump = (DocumentId, Selection); @@ -91,7 +90,10 @@ impl View { .selection(self.id) .primary() .cursor(doc.text().slice(..)); - let Position { col, row: line } = coords_at_pos(doc.text().slice(..), cursor); + + let Position { col, row: line } = + visual_coords_at_pos(doc.text().slice(..), cursor, doc.tab_width()); + let inner_area = self.inner_area(); let last_line = (self.offset.row + inner_area.height as usize).saturating_sub(1); -- cgit v1.2.3-70-g09d2 From e39cfa40dfc320559c5efdb502e1149c4564cb62 Mon Sep 17 00:00:00 2001 From: Omnikar Date: Wed, 3 Nov 2021 20:50:38 -0400 Subject: Hide keys bound to `no_op` from infobox (#971) --- helix-term/src/keymap.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'helix-term/src') diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 72d0a733..93f64fa4 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -140,7 +140,12 @@ impl KeyTrieNode { let mut body: Vec<(&str, BTreeSet)> = Vec::with_capacity(self.len()); for (&key, trie) in self.iter() { let desc = match trie { - KeyTrie::Leaf(cmd) => cmd.doc(), + KeyTrie::Leaf(cmd) => { + if cmd.name() == "no_op" { + continue; + } + cmd.doc() + } KeyTrie::Node(n) => n.name(), }; match body.iter().position(|(d, _)| d == &desc) { -- cgit v1.2.3-70-g09d2 From 5b5d1b9dfff6b522559174f7f8e99aeb82c674a9 Mon Sep 17 00:00:00 2001 From: Omnikar Date: Wed, 3 Nov 2021 23:24:05 -0400 Subject: Truncate the starts of file paths instead of the ends in picker (#951) * Truncate the starts of file paths in picker * Simplify the truncate implementation * Break loop at appropriate point * Fix alignment and ellipsis presence * Remove extraneous usage of `x_offset` Co-authored-by: Blaž Hrastnik --- helix-term/src/ui/picker.rs | 1 + helix-tui/src/buffer.rs | 78 ++++++++++++++++++++++++++++++++------------- 2 files changed, 57 insertions(+), 22 deletions(-) (limited to 'helix-term/src') diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 7e257c0b..7fc6af0f 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -483,6 +483,7 @@ impl Component for Picker { text_style }, true, + true, ); } } diff --git a/helix-tui/src/buffer.rs b/helix-tui/src/buffer.rs index 377e3e39..f480bc2f 100644 --- a/helix-tui/src/buffer.rs +++ b/helix-tui/src/buffer.rs @@ -266,12 +266,14 @@ impl Buffer { where S: AsRef, { - self.set_string_truncated(x, y, string, width, style, false) + self.set_string_truncated(x, y, string, width, style, false, false) } /// Print at most the first `width` characters of a string if enough space is available - /// until the end of the line. If `markend` is true appends a `…` at the end of - /// truncated lines. + /// until the end of the line. If `ellipsis` is true appends a `…` at the end of + /// truncated lines. If `truncate_start` is `true`, truncate the beginning of the string + /// instead of the end. + #[allow(clippy::too_many_arguments)] pub fn set_string_truncated( &mut self, x: u16, @@ -280,6 +282,7 @@ impl Buffer { width: usize, style: Style, ellipsis: bool, + truncate_start: bool, ) -> (u16, u16) where S: AsRef, @@ -289,28 +292,59 @@ impl Buffer { let width = if ellipsis { width - 1 } else { width }; let graphemes = UnicodeSegmentation::graphemes(string.as_ref(), true); let max_offset = min(self.area.right() as usize, width.saturating_add(x as usize)); - for s in graphemes { - let width = s.width(); - if width == 0 { - continue; + if !truncate_start { + for s in graphemes { + let width = s.width(); + if width == 0 { + continue; + } + // `x_offset + width > max_offset` could be integer overflow on 32-bit machines if we + // change dimenstions to usize or u32 and someone resizes the terminal to 1x2^32. + if width > max_offset.saturating_sub(x_offset) { + break; + } + + self.content[index].set_symbol(s); + self.content[index].set_style(style); + // Reset following cells if multi-width (they would be hidden by the grapheme), + for i in index + 1..index + width { + self.content[i].reset(); + } + index += width; + x_offset += width; } - // `x_offset + width > max_offset` could be integer overflow on 32-bit machines if we - // change dimenstions to usize or u32 and someone resizes the terminal to 1x2^32. - if width > max_offset.saturating_sub(x_offset) { - break; + if ellipsis && x_offset - (x as usize) < string.as_ref().width() { + self.content[index].set_symbol("…"); } - - self.content[index].set_symbol(s); - self.content[index].set_style(style); - // Reset following cells if multi-width (they would be hidden by the grapheme), - for i in index + 1..index + width { - self.content[i].reset(); + } else { + let mut start_index = self.index_of(x, y); + let mut index = self.index_of(max_offset as u16, y); + + let total_width = string.as_ref().width(); + let truncated = total_width > width; + if ellipsis && truncated { + self.content[start_index].set_symbol("…"); + start_index += 1; + } + if !truncated { + index -= width - total_width; + } + for s in graphemes.rev() { + let width = s.width(); + if width == 0 { + continue; + } + let start = index - width; + if start < start_index { + break; + } + self.content[start].set_symbol(s); + self.content[start].set_style(style); + for i in start + 1..index { + self.content[i].reset(); + } + index -= width; } - index += width; - x_offset += width; - } - if ellipsis && x_offset - (x as usize) < string.as_ref().width() { - self.content[index].set_symbol("…"); } (x_offset as u16, y) } -- cgit v1.2.3-70-g09d2 From 70d21a903fef3ec0787c453f369d95e5223a2656 Mon Sep 17 00:00:00 2001 From: diegodox Date: Thu, 4 Nov 2021 12:24:52 +0900 Subject: Prevent preview binary or large file (#939) * Prevent preview binary or large file (#847) * fix wrong method name * fix add use trait * update lock file * rename MAX_PREVIEW_SIZE from MAX_BYTE_PREVIEW * read small bytes to determine cotent type * [WIP] add preview struct to represent calcurated preveiw * Refactor content type detection - Remove unwraps - Reuse a single read buffer to avoid 1kb reallocations between previews * Refactor preview rendering so we don't construct docs when not necessary * Replace unwarap whit Preview::NotFound * Use index access to hide unwrap Co-authored-by: Blaž Hrastnik * fix Get and unwarp equivalent to referce of Index acess * better preview implementation * Rename Preview enum and vairant Co-authored-by: Gokul Soumya * fixup! Rename Preview enum and vairant * simplify long match * Center text, add docs, fix formatting, refactor Co-authored-by: Blaž Hrastnik Co-authored-by: Gokul Soumya --- Cargo.lock | 10 ++++ helix-term/Cargo.toml | 2 + helix-term/src/ui/picker.rs | 123 +++++++++++++++++++++++++++++++++++++------- 3 files changed, 115 insertions(+), 20 deletions(-) (limited to 'helix-term/src') diff --git a/Cargo.lock b/Cargo.lock index 0ce8ee8f..e036828a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -110,6 +110,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "content_inspector" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7bda66e858c683005a53a9a60c69a4aca7eeaa45d124526e389f7aec8e62f38" +dependencies = [ + "memchr", +] + [[package]] name = "crossbeam-utils" version = "0.8.5" @@ -415,6 +424,7 @@ version = "0.5.0" dependencies = [ "anyhow", "chrono", + "content_inspector", "crossterm", "fern", "futures-util", diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index 45b4eb2c..a0079feb 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -46,6 +46,8 @@ fuzzy-matcher = "0.3" ignore = "0.4" # markdown doc rendering pulldown-cmark = { version = "0.8", default-features = false } +# file type detection +content_inspector = "0.2.4" # config toml = "0.5" diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 7fc6af0f..291f1f85 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -12,7 +12,12 @@ use fuzzy_matcher::skim::SkimMatcherV2 as Matcher; use fuzzy_matcher::FuzzyMatcher; use tui::widgets::Widget; -use std::{borrow::Cow, collections::HashMap, path::PathBuf}; +use std::{ + borrow::Cow, + collections::HashMap, + io::Read, + path::{Path, PathBuf}, +}; use crate::ui::{Prompt, PromptEvent}; use helix_core::Position; @@ -23,18 +28,58 @@ use helix_view::{ }; pub const MIN_SCREEN_WIDTH_FOR_PREVIEW: u16 = 80; +/// Biggest file size to preview in bytes +pub const MAX_FILE_SIZE_FOR_PREVIEW: u64 = 10 * 1024 * 1024; -/// File path and line number (used to align and highlight a line) +/// File path and range of lines (used to align and highlight lines) type FileLocation = (PathBuf, Option<(usize, usize)>); pub struct FilePicker { picker: Picker, /// Caches paths to documents - preview_cache: HashMap, + preview_cache: HashMap, + read_buffer: Vec, /// Given an item in the picker, return the file path and line number to display. file_fn: Box Option>, } +pub enum CachedPreview { + Document(Document), + Binary, + LargeFile, + NotFound, +} + +// We don't store this enum in the cache so as to avoid lifetime constraints +// from borrowing a document already opened in the editor. +pub enum Preview<'picker, 'editor> { + Cached(&'picker CachedPreview), + EditorDocument(&'editor Document), +} + +impl Preview<'_, '_> { + fn document(&self) -> Option<&Document> { + match self { + Preview::EditorDocument(doc) => Some(doc), + Preview::Cached(CachedPreview::Document(doc)) => Some(doc), + _ => None, + } + } + + /// Alternate text to show for the preview. + fn placeholder(&self) -> &str { + match *self { + Self::EditorDocument(_) => "", + Self::Cached(preview) => match preview { + CachedPreview::Document(_) => "", + CachedPreview::Binary => "", + CachedPreview::LargeFile => "", + CachedPreview::NotFound => "", + }, + } + } +} + impl FilePicker { pub fn new( options: Vec, @@ -45,6 +90,7 @@ impl FilePicker { Self { picker: Picker::new(false, options, format_fn, callback_fn), preview_cache: HashMap::new(), + read_buffer: Vec::with_capacity(1024), file_fn: Box::new(preview_fn), } } @@ -60,14 +106,45 @@ impl FilePicker { }) } - fn calculate_preview(&mut self, editor: &Editor) { - if let Some((path, _line)) = self.current_file(editor) { - if !self.preview_cache.contains_key(&path) && editor.document_by_path(&path).is_none() { - // TODO: enable syntax highlighting; blocked by async rendering - let doc = Document::open(&path, None, Some(&editor.theme), None).unwrap(); - self.preview_cache.insert(path, doc); - } + /// Get (cached) preview for a given path. If a document corresponding + /// to the path is already open in the editor, it is used instead. + fn get_preview<'picker, 'editor>( + &'picker mut self, + path: &Path, + editor: &'editor Editor, + ) -> Preview<'picker, 'editor> { + if let Some(doc) = editor.document_by_path(path) { + return Preview::EditorDocument(doc); + } + + if self.preview_cache.contains_key(path) { + return Preview::Cached(&self.preview_cache[path]); } + + let data = std::fs::File::open(path).and_then(|file| { + let metadata = file.metadata()?; + // Read up to 1kb to detect the content type + let n = file.take(1024).read_to_end(&mut self.read_buffer)?; + let content_type = content_inspector::inspect(&self.read_buffer[..n]); + self.read_buffer.clear(); + Ok((metadata, content_type)) + }); + let preview = data + .map( + |(metadata, content_type)| match (metadata.len(), content_type) { + (_, content_inspector::ContentType::BINARY) => CachedPreview::Binary, + (size, _) if size > MAX_FILE_SIZE_FOR_PREVIEW => CachedPreview::LargeFile, + _ => { + // TODO: enable syntax highlighting; blocked by async rendering + Document::open(path, None, Some(&editor.theme), None) + .map(CachedPreview::Document) + .unwrap_or(CachedPreview::NotFound) + } + }, + ) + .unwrap_or(CachedPreview::NotFound); + self.preview_cache.insert(path.to_owned(), preview); + Preview::Cached(&self.preview_cache[path]) } } @@ -79,12 +156,12 @@ impl Component for FilePicker { // |picker | | | // | | | | // +---------+ +---------+ - self.calculate_preview(cx.editor); let render_preview = area.width > MIN_SCREEN_WIDTH_FOR_PREVIEW; let area = inner_rect(area); // -- Render the frame: // clear area let background = cx.editor.theme.get("ui.background"); + let text = cx.editor.theme.get("ui.text"); surface.clear_with(area, background); let picker_width = if render_preview { @@ -113,17 +190,23 @@ impl Component for FilePicker { horizontal: 1, }; let inner = inner.inner(&margin); - block.render(preview_area, surface); - if let Some((doc, line)) = self.current_file(cx.editor).and_then(|(path, range)| { - cx.editor - .document_by_path(&path) - .or_else(|| self.preview_cache.get(&path)) - .zip(Some(range)) - }) { + if let Some((path, range)) = self.current_file(cx.editor) { + let preview = self.get_preview(&path, cx.editor); + let doc = match preview.document() { + Some(doc) => doc, + None => { + let alt_text = preview.placeholder(); + let x = inner.x + inner.width.saturating_sub(alt_text.len() as u16) / 2; + let y = inner.y + inner.height / 2; + surface.set_stringn(x, y, alt_text, inner.width as usize, text); + return; + } + }; + // align to middle - let first_line = line + let first_line = range .map(|(start, end)| { let height = end.saturating_sub(start) + 1; let middle = start + (height.saturating_sub(1) / 2); @@ -150,7 +233,7 @@ impl Component for FilePicker { ); // highlight the line - if let Some((start, end)) = line { + if let Some((start, end)) = range { let offset = start.saturating_sub(first_line) as u16; surface.set_style( Rect::new( -- cgit v1.2.3-70-g09d2 From 39584cbccdb06b528220a13b643416f3fd5dc3c8 Mon Sep 17 00:00:00 2001 From: CossonLeo Date: Thu, 4 Nov 2021 11:26:01 +0800 Subject: Add c-s to pick word under doc cursor to prompt line & search completion (#831) * Add prompt shourtcut to book Add completions to search Add c-s to pick word under doc cursor to prompt line * limit 20 last items of search completion, update book * Update book/src/keymap.md Co-authored-by: Ivan Tham * limit search completions 200 Co-authored-by: Ivan Tham --- book/src/keymap.md | 22 ++++++++++++++++++++++ helix-term/src/commands.rs | 29 +++++++++++++++++++++++++++++ helix-term/src/ui/mod.rs | 3 ++- helix-term/src/ui/prompt.rs | 35 +++++++++++++++++++++++++++++++++-- 4 files changed, 86 insertions(+), 3 deletions(-) (limited to 'helix-term/src') diff --git a/book/src/keymap.md b/book/src/keymap.md index a776ee70..4a6f80bb 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -258,3 +258,25 @@ Keys to use within picker. Remapping currently not supported. | `Ctrl-s` | Open horizontally | | `Ctrl-v` | Open vertically | | `Escape`, `Ctrl-c` | Close picker | + +# Prompt +Keys to use within prompt, Remapping currently not supported. +| Key | Description | +| ----- | ------------- | +| `Escape`, `Ctrl-c` | Close prompt | +| `Alt-b`, `Alt-Left` | Backward a word | +| `Ctrl-b`, `Left` | Backward a char | +| `Alt-f`, `Alt-Right` | Forward a word | +| `Ctrl-f`, `Right` | Forward a char | +| `Ctrl-e`, `End` | move prompt end | +| `Ctrl-a`, `Home` | move prompt start | +| `Ctrl-w` | delete previous word | +| `Ctrl-k` | delete to end of line | +| `backspace` | delete previous char | +| `Ctrl-s` | insert a word under doc cursor, may be changed to Ctrl-r Ctrl-w later | +| `Ctrl-p`, `Up` | select previous history | +| `Ctrl-n`, `Down` | select next history | +| `Tab` | slect next completion item | +| `BackTab` | slect previous completion item | +| `Enter` | Open selected | + diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index cc9106eb..6c073fb8 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1087,6 +1087,7 @@ fn select_regex(cx: &mut Context) { cx, "select:".into(), Some(reg), + |_input: &str| Vec::new(), move |view, doc, regex, event| { if event != PromptEvent::Update { return; @@ -1109,6 +1110,7 @@ fn split_selection(cx: &mut Context) { cx, "split:".into(), Some(reg), + |_input: &str| Vec::new(), move |view, doc, regex, event| { if event != PromptEvent::Update { return; @@ -1172,6 +1174,15 @@ fn search_impl(doc: &mut Document, view: &mut View, contents: &str, regex: &Rege }; } +fn search_completions(cx: &mut Context, reg: Option) -> Vec { + let mut items = reg + .and_then(|reg| cx.editor.registers.get(reg)) + .map_or(Vec::new(), |reg| reg.read().iter().take(200).collect()); + items.sort_unstable(); + items.dedup(); + items.into_iter().cloned().collect() +} + // TODO: use one function for search vs extend fn search(cx: &mut Context) { let reg = cx.register.unwrap_or('/'); @@ -1182,11 +1193,19 @@ fn search(cx: &mut Context) { // HAXX: sadly we can't avoid allocating a single string for the whole buffer since we can't // feed chunks into the regex yet let contents = doc.text().slice(..).to_string(); + let completions = search_completions(cx, Some(reg)); let prompt = ui::regex_prompt( cx, "search:".into(), Some(reg), + move |input: &str| { + completions + .iter() + .filter(|comp| comp.starts_with(input)) + .map(|comp| (0.., std::borrow::Cow::Owned(comp.clone()))) + .collect() + }, move |view, doc, regex, event| { if event != PromptEvent::Update { return; @@ -1246,10 +1265,19 @@ fn global_search(cx: &mut Context) { let (all_matches_sx, all_matches_rx) = tokio::sync::mpsc::unbounded_channel::<(usize, PathBuf)>(); let smart_case = cx.editor.config.smart_case; + + let completions = search_completions(cx, None); let prompt = ui::regex_prompt( cx, "global search:".into(), None, + move |input: &str| { + completions + .iter() + .filter(|comp| comp.starts_with(input)) + .map(|comp| (0.., std::borrow::Cow::Owned(comp.clone()))) + .collect() + }, move |_view, _doc, regex, event| { if event != PromptEvent::Validate { return; @@ -4086,6 +4114,7 @@ fn keep_selections(cx: &mut Context) { cx, "keep:".into(), Some(reg), + |_input: &str| Vec::new(), move |view, doc, regex, event| { if event != PromptEvent::Update { return; diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 30a9ec6b..24eb7acd 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -29,6 +29,7 @@ pub fn regex_prompt( cx: &mut crate::commands::Context, prompt: std::borrow::Cow<'static, str>, history_register: Option, + completion_fn: impl FnMut(&str) -> Vec + 'static, fun: impl Fn(&mut View, &mut Document, Regex, PromptEvent) + 'static, ) -> Prompt { let (view, doc) = current!(cx.editor); @@ -38,7 +39,7 @@ pub fn regex_prompt( Prompt::new( prompt, history_register, - |_input: &str| Vec::new(), // this is fine because Vec::new() doesn't allocate + completion_fn, move |cx: &mut crate::compositor::Context, input: &str, event: PromptEvent| { match event { PromptEvent::Abort => { diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index 853adfc2..c999ba14 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -185,6 +185,11 @@ impl Prompt { self.exit_selection(); } + pub fn insert_str(&mut self, s: &str) { + self.line.insert_str(self.cursor, s); + self.cursor += s.len(); + } + pub fn move_cursor(&mut self, movement: Movement) { let pos = self.eval_movement(movement); self.cursor = pos @@ -473,6 +478,26 @@ impl Component for Prompt { self.delete_char_backwards(); (self.callback_fn)(cx, &self.line, PromptEvent::Update); } + KeyEvent { + code: KeyCode::Char('s'), + modifiers: KeyModifiers::CONTROL, + } => { + let (view, doc) = current!(cx.editor); + let text = doc.text().slice(..); + + use helix_core::textobject; + let range = textobject::textobject_word( + text, + doc.selection(view.id).primary(), + textobject::TextObject::Inside, + 1, + ); + let line = text.slice(range.from()..range.to()).to_string(); + if !line.is_empty() { + self.insert_str(line.as_str()); + (self.callback_fn)(cx, &self.line, PromptEvent::Update); + } + } KeyEvent { code: KeyCode::Enter, .. @@ -520,11 +545,17 @@ impl Component for Prompt { } KeyEvent { code: KeyCode::Tab, .. - } => self.change_completion_selection(CompletionDirection::Forward), + } => { + self.change_completion_selection(CompletionDirection::Forward); + (self.callback_fn)(cx, &self.line, PromptEvent::Update) + } KeyEvent { code: KeyCode::BackTab, .. - } => self.change_completion_selection(CompletionDirection::Backward), + } => { + self.change_completion_selection(CompletionDirection::Backward); + (self.callback_fn)(cx, &self.line, PromptEvent::Update) + } KeyEvent { code: KeyCode::Char('q'), modifiers: KeyModifiers::CONTROL, -- cgit v1.2.3-70-g09d2 From e2560f427ef5e75155071e39da342628f5d5896a Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Thu, 4 Nov 2021 13:43:45 +0900 Subject: Replace documents SlotMap with BTreeMap --- helix-term/src/commands.rs | 6 +++--- helix-term/src/ui/editor.rs | 10 ++++----- helix-view/src/editor.rs | 51 ++++++++++++++++++++++++++------------------- helix-view/src/lib.rs | 4 +++- helix-view/src/macros.rs | 5 +++-- 5 files changed, 44 insertions(+), 32 deletions(-) (limited to 'helix-term/src') diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 6c073fb8..547a1d75 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1818,7 +1818,7 @@ mod cmd { let mut errors = String::new(); // save all documents - for (_, doc) in &mut cx.editor.documents { + for doc in &mut cx.editor.documents.values_mut() { if doc.path().is_none() { errors.push_str("cannot write a buffer without a filename\n"); continue; @@ -2512,7 +2512,7 @@ fn buffer_picker(cx: &mut Context) { cx.editor .documents .iter() - .map(|(id, doc)| (id, doc.path().cloned())) + .map(|(id, doc)| (*id, doc.path().cloned())) .collect(), move |(id, path): &(DocumentId, Option)| { let path = path.as_deref().map(helix_core::path::get_relative_path); @@ -2531,7 +2531,7 @@ fn buffer_picker(cx: &mut Context) { editor.switch(*id, Action::Replace); }, |editor, (id, path)| { - let doc = &editor.documents.get(*id)?; + let doc = &editor.documents.get(id)?; let &view_id = doc.selections().keys().next()?; let line = doc .selection(view_id) diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index c0d602c7..0ffde47b 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -813,12 +813,12 @@ impl EditorView { let editor = &mut cxt.editor; let result = editor.tree.views().find_map(|(view, _focus)| { - view.pos_at_screen_coords(&editor.documents[view.doc], row, column) + view.pos_at_screen_coords(&editor.documents[&view.doc], row, column) .map(|pos| (pos, view.id)) }); if let Some((pos, view_id)) = result { - let doc = &mut editor.documents[editor.tree.get(view_id).doc]; + let doc = editor.document_mut(editor.tree.get(view_id).doc).unwrap(); if modifiers == crossterm::event::KeyModifiers::ALT { let selection = doc.selection(view_id).clone(); @@ -870,7 +870,7 @@ impl EditorView { }; let result = cxt.editor.tree.views().find_map(|(view, _focus)| { - view.pos_at_screen_coords(&cxt.editor.documents[view.doc], row, column) + view.pos_at_screen_coords(&cxt.editor.documents[&view.doc], row, column) .map(|_| view.id) }); @@ -926,12 +926,12 @@ impl EditorView { } let result = editor.tree.views().find_map(|(view, _focus)| { - view.pos_at_screen_coords(&editor.documents[view.doc], row, column) + view.pos_at_screen_coords(&editor.documents[&view.doc], row, column) .map(|pos| (pos, view.id)) }); if let Some((pos, view_id)) = result { - let doc = &mut editor.documents[editor.tree.get(view_id).doc]; + let doc = editor.document_mut(editor.tree.get(view_id).doc).unwrap(); doc.set_selection(view_id, Selection::point(pos)); editor.tree.focus = view_id; commands::Command::paste_primary_clipboard_before.execute(cxt); diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 21a64651..633e2541 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -8,6 +8,7 @@ use crate::{ use futures_util::future; use std::{ + collections::BTreeMap, path::{Path, PathBuf}, pin::Pin, sync::Arc, @@ -15,8 +16,6 @@ use std::{ use tokio::time::{sleep, Duration, Instant, Sleep}; -use slotmap::SlotMap; - use anyhow::Error; pub use helix_core::diagnostic::Severity; @@ -108,7 +107,8 @@ impl std::fmt::Debug for Motion { #[derive(Debug)] pub struct Editor { pub tree: Tree, - pub documents: SlotMap, + pub next_document_id: usize, + pub documents: BTreeMap, pub count: Option, pub selected_register: Option, pub registers: Registers, @@ -149,7 +149,8 @@ impl Editor { Self { tree: Tree::new(area), - documents: SlotMap::with_key(), + next_document_id: 0, + documents: BTreeMap::new(), count: None, selected_register: None, theme: themes.default(), @@ -216,7 +217,7 @@ impl Editor { fn _refresh(&mut self) { for (view, _) in self.tree.views_mut() { - let doc = &self.documents[view.doc]; + let doc = &self.documents[&view.doc]; view.ensure_cursor_in_view(doc, self.config.scrolloff) } } @@ -225,7 +226,7 @@ impl Editor { use crate::tree::Layout; use helix_core::Selection; - if !self.documents.contains_key(id) { + if !self.documents.contains_key(&id) { log::error!("cannot switch to document that does not exist (anymore)"); return; } @@ -249,7 +250,7 @@ impl Editor { // Copy `doc.id` into a variable before calling `self.documents.remove`, which requires a mutable // borrow, invalidating direct access to `doc.id`. let id = doc.id; - self.documents.remove(id); + self.documents.remove(&id); } else { let jump = (view.doc, doc.selection(view.id).clone()); view.jumps.push(jump); @@ -281,14 +282,14 @@ impl Editor { let view = View::new(id); let view_id = self.tree.split(view, Layout::Horizontal); // initialize selection for view - let doc = &mut self.documents[id]; + let doc = self.documents.get_mut(&id).unwrap(); doc.selections.insert(view_id, Selection::point(0)); } Action::VerticalSplit => { let view = View::new(id); let view_id = self.tree.split(view, Layout::Vertical); // initialize selection for view - let doc = &mut self.documents[id]; + let doc = self.documents.get_mut(&id).unwrap(); doc.selections.insert(view_id, Selection::point(0)); } } @@ -297,9 +298,11 @@ impl Editor { } pub fn new_file(&mut self, action: Action) -> DocumentId { - let doc = Document::default(); - let id = self.documents.insert(doc); - self.documents[id].id = id; + let id = DocumentId(self.next_document_id); + self.next_document_id += 1; + let mut doc = Document::default(); + doc.id = id; + self.documents.insert(id, doc); self.switch(id, action); id } @@ -349,8 +352,10 @@ impl Editor { doc.set_language_server(Some(language_server)); } - let id = self.documents.insert(doc); - self.documents[id].id = id; + let id = DocumentId(self.next_document_id); + self.next_document_id += 1; + doc.id = id; + self.documents.insert(id, doc); id }; @@ -361,16 +366,20 @@ impl Editor { pub fn close(&mut self, id: ViewId, close_buffer: bool) { let view = self.tree.get(self.tree.focus); // remove selection - self.documents[view.doc].selections.remove(&id); + self.documents + .get_mut(&view.doc) + .unwrap() + .selections + .remove(&id); if close_buffer { // get around borrowck issues - let doc = &self.documents[view.doc]; + let doc = &self.documents[&view.doc]; if let Some(language_server) = doc.language_server() { tokio::spawn(language_server.text_document_did_close(doc.identifier())); } - self.documents.remove(view.doc); + self.documents.remove(&view.doc); } self.tree.remove(id); @@ -409,18 +418,18 @@ impl Editor { pub fn ensure_cursor_in_view(&mut self, id: ViewId) { let view = self.tree.get_mut(id); - let doc = &self.documents[view.doc]; + let doc = &self.documents[&view.doc]; view.ensure_cursor_in_view(doc, self.config.scrolloff) } #[inline] pub fn document(&self, id: DocumentId) -> Option<&Document> { - self.documents.get(id) + self.documents.get(&id) } #[inline] pub fn document_mut(&mut self, id: DocumentId) -> Option<&mut Document> { - self.documents.get_mut(id) + self.documents.get_mut(&id) } #[inline] @@ -445,7 +454,7 @@ impl Editor { pub fn cursor(&self) -> (Option, CursorKind) { let view = view!(self); - let doc = &self.documents[view.doc]; + let doc = &self.documents[&view.doc]; let cursor = doc .selection(view.id) .primary() diff --git a/helix-view/src/lib.rs b/helix-view/src/lib.rs index c37474d6..3e779356 100644 --- a/helix-view/src/lib.rs +++ b/helix-view/src/lib.rs @@ -12,8 +12,10 @@ pub mod theme; pub mod tree; pub mod view; +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)] +pub struct DocumentId(usize); + slotmap::new_key_type! { - pub struct DocumentId; pub struct ViewId; } diff --git a/helix-view/src/macros.rs b/helix-view/src/macros.rs index 0bebd02f..63d76a42 100644 --- a/helix-view/src/macros.rs +++ b/helix-view/src/macros.rs @@ -13,7 +13,8 @@ macro_rules! current { ( $( $editor:ident ).+ ) => {{ let view = $crate::view_mut!( $( $editor ).+ ); - let doc = &mut $( $editor ).+ .documents[view.doc]; + let id = view.doc; + let doc = $( $editor ).+ .documents.get_mut(&id).unwrap(); (view, doc) }}; } @@ -56,7 +57,7 @@ macro_rules! doc { macro_rules! current_ref { ( $( $editor:ident ).+ ) => {{ let view = $( $editor ).+ .tree.get($( $editor ).+ .tree.focus); - let doc = &$( $editor ).+ .documents[view.doc]; + let doc = &$( $editor ).+ .documents[&view.doc]; (view, doc) }}; } -- cgit v1.2.3-70-g09d2 From 78c68fae91579ccda6f65e55f79316b01c5b654a Mon Sep 17 00:00:00 2001 From: ath3 Date: Mon, 1 Nov 2021 20:52:47 +0100 Subject: Implement "Goto next buffer / Goto previous buffer" commands --- book/src/keymap.md | 2 ++ helix-term/src/commands.rs | 29 +++++++++++++++++++++++++++++ helix-term/src/keymap.rs | 2 ++ 3 files changed, 33 insertions(+) (limited to 'helix-term/src') diff --git a/book/src/keymap.md b/book/src/keymap.md index 4a6f80bb..6bcd09bc 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -161,6 +161,8 @@ Jumps to various locations. | `r` | Go to references | `goto_reference` | | `i` | Go to implementation | `goto_implementation` | | `a` | Go to the last accessed/alternate file | `goto_last_accessed_file` | +| `n` | Go to next buffer | `goto_next_buffer` | +| `p` | Go to previous buffer | `goto_previous_buffer` | #### Match mode diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 547a1d75..c1891baf 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -262,6 +262,8 @@ impl Command { goto_prev_diag, "Goto previous diagnostic", goto_line_start, "Goto line start", goto_line_end, "Goto line end", + goto_next_buffer, "Goto next buffer", + goto_previous_buffer, "Goto previous buffer", // TODO: different description ? goto_line_end_newline, "Goto line end", goto_first_nonwhitespace, "Goto first non-blank in line", @@ -519,6 +521,33 @@ fn goto_line_start(cx: &mut Context) { ) } +fn goto_next_buffer(cx: &mut Context) { + goto_buffer(cx, Direction::Forward); +} + +fn goto_previous_buffer(cx: &mut Context) { + goto_buffer(cx, Direction::Backward); +} + +fn goto_buffer(cx: &mut Context, direction: Direction) { + let buf_cur = current!(cx.editor).1.id(); + + if let Some(pos) = cx.editor.documents.iter().position(|(id, _)| id == buf_cur) { + let goto_id = if direction == Direction::Forward { + if pos < cx.editor.documents.iter().count() - 1 { + cx.editor.documents.iter().nth(pos + 1).unwrap().0 + } else { + cx.editor.documents.iter().next().unwrap().0 + } + } else if pos > 0 { + cx.editor.documents.iter().nth(pos - 1).unwrap().0 + } else { + cx.editor.documents.iter().last().unwrap().0 + }; + cx.editor.switch(goto_id, Action::Replace); + } +} + fn extend_to_line_start(cx: &mut Context) { let (view, doc) = current!(cx.editor); goto_line_start_impl(view, doc, Movement::Extend) diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 93f64fa4..b48eea14 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -453,6 +453,8 @@ impl Default for Keymaps { "m" => goto_window_middle, "b" => goto_window_bottom, "a" => goto_last_accessed_file, + "n" => goto_next_buffer, + "p" => goto_previous_buffer, }, ":" => command_mode, -- cgit v1.2.3-70-g09d2 From 7b65a6d687bbf4d12de020a7785082277804bbd3 Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Thu, 4 Nov 2021 13:55:45 +0900 Subject: Rewrite goto_buffer --- helix-term/src/commands.rs | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) (limited to 'helix-term/src') diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index c1891baf..f16afdfe 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -530,22 +530,28 @@ fn goto_previous_buffer(cx: &mut Context) { } fn goto_buffer(cx: &mut Context, direction: Direction) { - let buf_cur = current!(cx.editor).1.id(); + let current = view!(cx.editor).doc; - if let Some(pos) = cx.editor.documents.iter().position(|(id, _)| id == buf_cur) { - let goto_id = if direction == Direction::Forward { - if pos < cx.editor.documents.iter().count() - 1 { - cx.editor.documents.iter().nth(pos + 1).unwrap().0 - } else { - cx.editor.documents.iter().next().unwrap().0 - } - } else if pos > 0 { - cx.editor.documents.iter().nth(pos - 1).unwrap().0 - } else { - cx.editor.documents.iter().last().unwrap().0 - }; - cx.editor.switch(goto_id, Action::Replace); + let id = match direction { + Direction::Forward => { + let iter = cx.editor.documents.keys(); + let mut iter = iter.skip_while(|id| *id != ¤t); + iter.next(); // skip current item + iter.next().or_else(|| cx.editor.documents.keys().next()) + } + Direction::Backward => { + let iter = cx.editor.documents.keys(); + let mut iter = iter.rev().skip_while(|id| *id != ¤t); + iter.next(); // skip current item + iter.next() + .or_else(|| cx.editor.documents.keys().rev().next()) + } } + .unwrap(); + + let id = *id; + + cx.editor.switch(id, Action::Replace); } fn extend_to_line_start(cx: &mut Context) { -- cgit v1.2.3-70-g09d2 From 51b4d35dce92fa7bf85780cb2ba0e531db378448 Mon Sep 17 00:00:00 2001 From: Omnikar Date: Thu, 4 Nov 2021 21:20:06 -0400 Subject: Inform when reaching undo/redo bounds (#981) * Inform when reaching undo/redo bounds * `Already at oldest change` when undo fails * `Already at newest change` when redo fails * Add missing `the`--- helix-term/src/commands.rs | 10 ++++++++-- helix-view/src/document.rs | 10 ++++++---- 2 files changed, 14 insertions(+), 6 deletions(-) (limited to 'helix-term/src') diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index f16afdfe..3d134ce5 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -3679,13 +3679,19 @@ pub mod insert { fn undo(cx: &mut Context) { let (view, doc) = current!(cx.editor); let view_id = view.id; - doc.undo(view_id); + let success = doc.undo(view_id); + if !success { + cx.editor.set_status("Already at oldest change".to_owned()); + } } fn redo(cx: &mut Context) { let (view, doc) = current!(cx.editor); let view_id = view.id; - doc.redo(view_id); + let success = doc.redo(view_id); + if !success { + cx.editor.set_status("Already at newest change".to_owned()); + } } // Yank / Paste diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 02da4b7a..0d86143b 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -704,8 +704,8 @@ impl Document { success } - /// Undo the last modification to the [`Document`]. - pub fn undo(&mut self, view_id: ViewId) { + /// Undo the last modification to the [`Document`]. Returns whether the undo was successful. + pub fn undo(&mut self, view_id: ViewId) -> bool { let mut history = self.history.take(); let success = if let Some(transaction) = history.undo() { self.apply_impl(transaction, view_id) @@ -718,10 +718,11 @@ impl Document { // reset changeset to fix len self.changes = ChangeSet::new(self.text()); } + success } - /// Redo the last modification to the [`Document`]. - pub fn redo(&mut self, view_id: ViewId) { + /// Redo the last modification to the [`Document`]. Returns whether the redo was sucessful. + pub fn redo(&mut self, view_id: ViewId) -> bool { let mut history = self.history.take(); let success = if let Some(transaction) = history.redo() { self.apply_impl(transaction, view_id) @@ -734,6 +735,7 @@ impl Document { // reset changeset to fix len self.changes = ChangeSet::new(self.text()); } + success } pub fn savepoint(&mut self) { -- cgit v1.2.3-70-g09d2 From cfc82858679d264d178a0b072da26828e685de12 Mon Sep 17 00:00:00 2001 From: Omnikar Date: Thu, 4 Nov 2021 22:25:08 -0400 Subject: Allow infoboxes to be disabled (#972) * Allow infoboxes to be disabled * Document `infoboxes` default value * Rename `infoboxes` to `auto_info` * Document `auto-info` * Fix incomplete rename--- book/src/configuration.md | 1 + helix-term/src/ui/editor.rs | 6 ++++-- helix-view/src/editor.rs | 3 +++ 3 files changed, 8 insertions(+), 2 deletions(-) (limited to 'helix-term/src') diff --git a/book/src/configuration.md b/book/src/configuration.md index 7d6ff28f..be25441f 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -22,6 +22,7 @@ To override global configuration parameters, create a `config.toml` file located | `auto-completion` | Enable automatic pop up of auto-completion. | `true` | | `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant. | `400` | | `completion-trigger-len` | The min-length of word under cursor to trigger autocompletion | `2` | +| `auto-info` | Whether to display infoboxes | `true` | ## LSP diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 0ffde47b..a7015577 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -1086,8 +1086,10 @@ impl Component for EditorView { ); } - if let Some(ref mut info) = self.autoinfo { - info.render(area, surface, cx); + if cx.editor.config.auto_info { + if let Some(ref mut info) = self.autoinfo { + info.render(area, surface, cx); + } } let key_width = 15u16; // for showing pending keys diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 63a4ab29..6aa8b04d 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -58,6 +58,8 @@ pub struct Config { #[serde(skip_serializing, deserialize_with = "deserialize_duration_millis")] pub idle_timeout: Duration, pub completion_trigger_len: u8, + /// Whether to display infoboxes. Defaults to true. + pub auto_info: bool, } #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] @@ -88,6 +90,7 @@ impl Default for Config { auto_completion: true, idle_timeout: Duration::from_millis(400), completion_trigger_len: 2, + auto_info: true, } } } -- cgit v1.2.3-70-g09d2 From 911b9b3276cb155eab023b24f1a6f336f4054087 Mon Sep 17 00:00:00 2001 From: Gygaxis Vainhardt Date: Sat, 6 Nov 2021 05:33:30 -0300 Subject: Add reverse search functionality (#958) * Add reverse search functionality * Change keybindings for extend to be in select mode, incorporate Movement and Direction enums * Fix accidental revert of #948 in rebase * Add reverse search to docs, clean up mismatched whitespace * Reverse search optimization * More optimization via github feedback--- book/src/keymap.md | 36 +++++++++---------- helix-term/src/commands.rs | 86 ++++++++++++++++++++++++++++++++++------------ helix-term/src/keymap.rs | 11 ++++-- 3 files changed, 90 insertions(+), 43 deletions(-) (limited to 'helix-term/src') diff --git a/book/src/keymap.md b/book/src/keymap.md index 6bcd09bc..5a6aee41 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -106,13 +106,13 @@ ### Search -> TODO: The search implementation isn't ideal yet -- we don't support searching in reverse. | Key | Description | Command | | ----- | ----------- | ------- | | `/` | Search for regex pattern | `search` | +| `?` | Search for previous pattern | `rsearch` | | `n` | Select next search match | `search_next` | -| `N` | Add next search match to selection | `extend_search_next` | +| `N` | Select previous search match | `search_prev` | | `*` | Use current selection as the search pattern | `search_selection` | ### Minor modes @@ -185,16 +185,16 @@ TODO: Mappings for selecting syntax nodes (a superset of `[`). This layer is similar to vim keybindings as kakoune does not support window. -| Key | Description | Command | -| ----- | ------------- | ------- | -| `w`, `Ctrl-w` | Switch to next window | `rotate_view` | -| `v`, `Ctrl-v` | Vertical right split | `vsplit` | -| `s`, `Ctrl-s` | Horizontal bottom split | `hsplit` | -| `h`, `Ctrl-h`, `left` | Move to left split | `jump_view_left` | -| `j`, `Ctrl-j`, `down` | Move to split below | `jump_view_down` | -| `k`, `Ctrl-k`, `up` | Move to split above | `jump_view_up` | -| `l`, `Ctrl-l`, `right` | Move to right split | `jump_view_right` | -| `q`, `Ctrl-q` | Close current window | `wclose` | +| Key | Description | Command | +| ----- | ------------- | ------- | +| `w`, `Ctrl-w` | Switch to next window | `rotate_view` | +| `v`, `Ctrl-v` | Vertical right split | `vsplit` | +| `s`, `Ctrl-s` | Horizontal bottom split | `hsplit` | +| `h`, `Ctrl-h`, `left` | Move to left split | `jump_view_left` | +| `j`, `Ctrl-j`, `down` | Move to split below | `jump_view_down` | +| `k`, `Ctrl-k`, `up` | Move to split above | `jump_view_up` | +| `l`, `Ctrl-l`, `right` | Move to right split | `jump_view_right` | +| `q`, `Ctrl-q` | Close current window | `wclose` | #### Space mode @@ -222,12 +222,12 @@ This layer is a kludge of mappings, mostly pickers. Mappings in the style of [vim-unimpaired](https://github.com/tpope/vim-unimpaired). -| Key | Description | Command | -| ----- | ----------- | ------- | -| `[d` | Go to previous diagnostic | `goto_prev_diag` | -| `]d` | Go to next diagnostic | `goto_next_diag` | -| `[D` | Go to first diagnostic in document | `goto_first_diag` | -| `]D` | Go to last diagnostic in document | `goto_last_diag` | +| Key | Description | Command | +| ----- | ----------- | ------- | +| `[d` | Go to previous diagnostic | `goto_prev_diag` | +| `]d` | Go to next diagnostic | `goto_next_diag` | +| `[D` | Go to first diagnostic in document | `goto_first_diag` | +| `]D` | Go to last diagnostic in document | `goto_last_diag` | | `[space` | Add newline above | `add_newline_above` | | `]space` | Add newline below | `add_newline_below` | diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 3d134ce5..c8f64531 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -217,8 +217,11 @@ impl Command { split_selection, "Split selection into subselections on regex matches", split_selection_on_newline, "Split selection on newlines", search, "Search for regex pattern", + rsearch, "Reverse search for regex pattern", search_next, "Select next search match", + search_prev, "Select previous search match", extend_search_next, "Add next search match to selection", + extend_search_prev, "Add previous search match to selection", search_selection, "Use current selection as search pattern", global_search, "Global Search in workspace folder", extend_line, "Select current line, if already selected, extend to next line", @@ -1170,38 +1173,62 @@ fn split_selection_on_newline(cx: &mut Context) { doc.set_selection(view.id, selection); } -fn search_impl(doc: &mut Document, view: &mut View, contents: &str, regex: &Regex, extend: bool) { +fn search_impl( + doc: &mut Document, + view: &mut View, + contents: &str, + regex: &Regex, + movement: Movement, + direction: Direction, +) { let text = doc.text().slice(..); let selection = doc.selection(view.id); - // Get the right side of the primary block cursor. - let start = text.char_to_byte(graphemes::next_grapheme_boundary( - text, - selection.primary().cursor(text), - )); + // Get the right side of the primary block cursor for forward search, or the + //grapheme before the start of the selection for reverse search. + let start = match direction { + Direction::Forward => text.char_to_byte(graphemes::next_grapheme_boundary( + text, + selection.primary().to(), + )), + Direction::Backward => text.char_to_byte(graphemes::prev_grapheme_boundary( + text, + selection.primary().from(), + )), + }; + + //A regex::Match returns byte-positions in the str. In the case where we + //do a reverse search and wraparound to the end, we don't need to search + //the text before the current cursor position for matches, but by slicing + //it out, we need to add it back to the position of the selection. + let mut offset = 0; // use find_at to find the next match after the cursor, loop around the end // Careful, `Regex` uses `bytes` as offsets, not character indices! - let mat = regex - .find_at(contents, start) - .or_else(|| regex.find(contents)); + let mat = match direction { + Direction::Forward => regex + .find_at(contents, start) + .or_else(|| regex.find(contents)), + Direction::Backward => regex.find_iter(&contents[..start]).last().or_else(|| { + offset = start; + regex.find_iter(&contents[start..]).last() + }), + }; // TODO: message on wraparound if let Some(mat) = mat { - let start = text.byte_to_char(mat.start()); - let end = text.byte_to_char(mat.end()); + let start = text.byte_to_char(mat.start() + offset); + let end = text.byte_to_char(mat.end() + offset); if end == 0 { // skip empty matches that don't make sense return; } - - let selection = if extend { - selection.clone().push(Range::new(start, end)) - } else { - selection + let selection = match movement { + Movement::Extend => selection.clone().push(Range::new(start, end)), + Movement::Move => selection .clone() .remove(selection.primary_index()) - .push(Range::new(start, end)) + .push(Range::new(start, end)), }; doc.set_selection(view.id, selection); @@ -1220,6 +1247,14 @@ fn search_completions(cx: &mut Context, reg: Option) -> Vec { // TODO: use one function for search vs extend fn search(cx: &mut Context) { + searcher(cx, Direction::Forward) +} + +fn rsearch(cx: &mut Context) { + searcher(cx, Direction::Backward) +} +// TODO: use one function for search vs extend +fn searcher(cx: &mut Context, direction: Direction) { let reg = cx.register.unwrap_or('/'); let (_, doc) = current!(cx.editor); @@ -1245,14 +1280,14 @@ fn search(cx: &mut Context) { if event != PromptEvent::Update { return; } - search_impl(doc, view, &contents, ®ex, false); + search_impl(doc, view, &contents, ®ex, Movement::Move, direction); }, ); cx.push_layer(Box::new(prompt)); } -fn search_next_impl(cx: &mut Context, extend: bool) { +fn search_next_or_prev_impl(cx: &mut Context, movement: Movement, direction: Direction) { let (view, doc) = current!(cx.editor); let registers = &cx.editor.registers; if let Some(query) = registers.read('/') { @@ -1267,7 +1302,7 @@ fn search_next_impl(cx: &mut Context, extend: bool) { .case_insensitive(case_insensitive) .build() { - search_impl(doc, view, &contents, ®ex, extend); + search_impl(doc, view, &contents, ®ex, movement, direction); } else { // get around warning `mutable_borrow_reservation_conflict` // which will be a hard error in the future @@ -1279,11 +1314,18 @@ fn search_next_impl(cx: &mut Context, extend: bool) { } fn search_next(cx: &mut Context) { - search_next_impl(cx, false); + search_next_or_prev_impl(cx, Movement::Move, Direction::Forward); } +fn search_prev(cx: &mut Context) { + search_next_or_prev_impl(cx, Movement::Move, Direction::Backward); +} fn extend_search_next(cx: &mut Context) { - search_next_impl(cx, true); + search_next_or_prev_impl(cx, Movement::Extend, Direction::Forward); +} + +fn extend_search_prev(cx: &mut Context) { + search_next_or_prev_impl(cx, Movement::Extend, Direction::Backward); } fn search_selection(cx: &mut Context) { diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index b48eea14..c85a9c3f 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -504,10 +504,9 @@ impl Default for Keymaps { }, "/" => search, - // ? for search_reverse + "?" => rsearch, "n" => search_next, - "N" => extend_search_next, - // N for search_prev + "N" => search_prev, "*" => search_selection, "u" => undo, @@ -633,11 +632,17 @@ impl Default for Keymaps { "B" => extend_prev_long_word_start, "E" => extend_next_long_word_end, + "n" => extend_search_next, + "N" => extend_search_prev, + "t" => extend_till_char, "f" => extend_next_char, "T" => extend_till_prev_char, "F" => extend_prev_char, + "n" => extend_search_next, + "N" => extend_search_prev, + "home" => extend_to_line_start, "end" => extend_to_line_end, "esc" => exit_select_mode, -- cgit v1.2.3-70-g09d2 From 6431b26a6a5fa4be5b91008f21537721d2ff4ba2 Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Sat, 6 Nov 2021 17:37:45 +0900 Subject: Implement Selection::replace to replace a single range Fixes #985 Co-authored-by: Daniel S Poulin --- helix-core/src/selection.rs | 11 +++++++++++ helix-term/src/commands.rs | 3 +-- 2 files changed, 12 insertions(+), 2 deletions(-) (limited to 'helix-term/src') diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs index 18af4d08..f3b5d2c8 100644 --- a/helix-core/src/selection.rs +++ b/helix-core/src/selection.rs @@ -362,6 +362,11 @@ impl Selection { /// Adds a new range to the selection and makes it the primary range. pub fn remove(mut self, index: usize) -> Self { + assert!( + self.ranges.len() > 1, + "can't remove the last range from a selection!" + ); + self.ranges.remove(index); if index < self.primary_index || self.primary_index == self.ranges.len() { self.primary_index -= 1; @@ -369,6 +374,12 @@ impl Selection { self } + /// Replace a range in the selection with a new range. + pub fn replace(mut self, index: usize, range: Range) -> Self { + self.ranges[index] = range; + self.normalize() + } + /// Map selections over a set of changes. Useful for adjusting the selection position after /// applying changes to a document. pub fn map(self, changes: &ChangeSet) -> Self { diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index c8f64531..e3ebd128 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1227,8 +1227,7 @@ fn search_impl( Movement::Extend => selection.clone().push(Range::new(start, end)), Movement::Move => selection .clone() - .remove(selection.primary_index()) - .push(Range::new(start, end)), + .replace(selection.primary_index(), Range::new(start, end)), }; doc.set_selection(view.id, selection); -- cgit v1.2.3-70-g09d2 From b81a5544248633d84615952ec6130f5104998c18 Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Sat, 6 Nov 2021 17:41:30 +0900 Subject: Retain range direction on search Co-authored-by: CossonLeo <20379044+cossonleo@users.noreply.github.com> --- helix-term/src/commands.rs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) (limited to 'helix-term/src') diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index e3ebd128..752b5900 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1223,11 +1223,18 @@ fn search_impl( // skip empty matches that don't make sense return; } + + // Determine range direction based on the primary range + let primary = selection.primary(); + let range = if primary.head < primary.anchor { + Range::new(end, start) + } else { + Range::new(start, end) + }; + let selection = match movement { - Movement::Extend => selection.clone().push(Range::new(start, end)), - Movement::Move => selection - .clone() - .replace(selection.primary_index(), Range::new(start, end)), + Movement::Extend => selection.clone().push(range), + Movement::Move => selection.clone().replace(selection.primary_index(), range), }; doc.set_selection(view.id, selection); -- cgit v1.2.3-70-g09d2 From 4c1321b3b6b5b2f4e9f33963877a1019ba746e04 Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Sat, 6 Nov 2021 17:49:18 +0900 Subject: minor: Extend search was decclared twice in the keymap --- helix-term/src/keymap.rs | 3 --- 1 file changed, 3 deletions(-) (limited to 'helix-term/src') diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index c85a9c3f..ce50f0ab 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -640,9 +640,6 @@ impl Default for Keymaps { "T" => extend_till_prev_char, "F" => extend_prev_char, - "n" => extend_search_next, - "N" => extend_search_prev, - "home" => extend_to_line_start, "end" => extend_to_line_end, "esc" => exit_select_mode, -- cgit v1.2.3-70-g09d2 From f659e1178a20cfaa151eaf62af3135a233272151 Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Sat, 6 Nov 2021 17:54:04 +0900 Subject: minor: view!(..).doc is slightly more efficient than current!(..).1.id() --- helix-term/src/commands.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) (limited to 'helix-term/src') diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 752b5900..aa18bef0 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2196,8 +2196,7 @@ mod cmd { args: &[&str], _event: PromptEvent, ) -> anyhow::Result<()> { - let (_, doc) = current!(cx.editor); - let id = doc.id(); + let id = view!(cx.editor).doc; if let Some(path) = args.get(0) { cx.editor.open(path.into(), Action::VerticalSplit)?; @@ -2213,8 +2212,7 @@ mod cmd { args: &[&str], _event: PromptEvent, ) -> anyhow::Result<()> { - let (_, doc) = current!(cx.editor); - let id = doc.id(); + let id = view!(cx.editor).doc; if let Some(path) = args.get(0) { cx.editor.open(path.into(), Action::HorizontalSplit)?; -- cgit v1.2.3-70-g09d2 From 0f4cd73000140cd9872f291ddb1473f96cbc7364 Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Sat, 6 Nov 2021 18:04:04 +0900 Subject: Simplify goto_*_diagnostic commands --- helix-term/src/commands.rs | 56 +++++++++++++++++++++------------------------- 1 file changed, 26 insertions(+), 30 deletions(-) (limited to 'helix-term/src') diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index aa18bef0..2e177b59 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -3348,26 +3348,24 @@ fn goto_first_diag(cx: &mut Context) { let editor = &mut cx.editor; let (_, doc) = current!(editor); - let diag = if let Some(diag) = doc.diagnostics().first() { - diag.range.start - } else { - return; + let pos = match doc.diagnostics().first() { + Some(diag) => diag.range.start, + None => return, }; - goto_pos(editor, diag); + goto_pos(editor, pos); } fn goto_last_diag(cx: &mut Context) { let editor = &mut cx.editor; let (_, doc) = current!(editor); - let diag = if let Some(diag) = doc.diagnostics().last() { - diag.range.start - } else { - return; + let pos = match doc.diagnostics().last() { + Some(diag) => diag.range.start, + None => return, }; - goto_pos(editor, diag); + goto_pos(editor, pos); } fn goto_next_diag(cx: &mut Context) { @@ -3378,20 +3376,19 @@ fn goto_next_diag(cx: &mut Context) { .selection(view.id) .primary() .cursor(doc.text().slice(..)); - let diag = if let Some(diag) = doc + + let diag = doc .diagnostics() .iter() - .map(|diag| diag.range.start) - .find(|&pos| pos > cursor_pos) - { - diag - } else if let Some(diag) = doc.diagnostics().first() { - diag.range.start - } else { - return; + .find(|diag| diag.range.start > cursor_pos) + .or_else(|| doc.diagnostics().first()); + + let pos = match diag { + Some(diag) => diag.range.start, + None => return, }; - goto_pos(editor, diag); + goto_pos(editor, pos); } fn goto_prev_diag(cx: &mut Context) { @@ -3402,21 +3399,20 @@ fn goto_prev_diag(cx: &mut Context) { .selection(view.id) .primary() .cursor(doc.text().slice(..)); - let diag = if let Some(diag) = doc + + let diag = doc .diagnostics() .iter() .rev() - .map(|diag| diag.range.start) - .find(|&pos| pos < cursor_pos) - { - diag - } else if let Some(diag) = doc.diagnostics().last() { - diag.range.start - } else { - return; + .find(|diag| diag.range.start < cursor_pos) + .or_else(|| doc.diagnostics().last()); + + let pos = match diag { + Some(diag) => diag.range.start, + None => return, }; - goto_pos(editor, diag); + goto_pos(editor, pos); } fn signature_help(cx: &mut Context) { -- cgit v1.2.3-70-g09d2 From 1a1685acf7fe9836b235dbc73361344d9330800c Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Sat, 6 Nov 2021 23:52:26 +0900 Subject: Simplify current!(..).1 into doc!() --- helix-term/src/commands.rs | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) (limited to 'helix-term/src') diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 2e177b59..f2a1e66d 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -743,13 +743,7 @@ where // usually mix line endings. But we should fix it eventually // anyway. { - current!(cx.editor) - .1 - .line_ending - .as_str() - .chars() - .next() - .unwrap() + doc!(cx.editor).line_ending.as_str().chars().next().unwrap() } KeyEvent { @@ -1746,7 +1740,7 @@ mod cmd { // If no argument, report current indent style. if args.is_empty() { - let style = current!(cx.editor).1.indent_style; + let style = doc!(cx.editor).indent_style; cx.editor.set_status(match style { Tabs => "tabs".into(), Spaces(1) => "1 space".into(), @@ -1785,7 +1779,7 @@ mod cmd { // If no argument, report current line ending setting. if args.is_empty() { - let line_ending = current!(cx.editor).1.line_ending; + let line_ending = doc!(cx.editor).line_ending; cx.editor.set_status(match line_ending { Crlf => "crlf".into(), LF => "line feed".into(), @@ -3794,7 +3788,7 @@ fn yank_joined_to_clipboard_impl( } fn yank_joined_to_clipboard(cx: &mut Context) { - let line_ending = current!(cx.editor).1.line_ending; + let line_ending = doc!(cx.editor).line_ending; let _ = yank_joined_to_clipboard_impl( &mut cx.editor, line_ending.as_str(), @@ -3828,7 +3822,7 @@ fn yank_main_selection_to_clipboard(cx: &mut Context) { } fn yank_joined_to_primary_clipboard(cx: &mut Context) { - let line_ending = current!(cx.editor).1.line_ending; + let line_ending = doc!(cx.editor).line_ending; let _ = yank_joined_to_clipboard_impl( &mut cx.editor, line_ending.as_str(), @@ -4517,7 +4511,7 @@ fn match_brackets(cx: &mut Context) { fn jump_forward(cx: &mut Context) { let count = cx.count(); - let (view, _doc) = current!(cx.editor); + let view = view_mut!(cx.editor); if let Some((id, selection)) = view.jumps.forward(count) { view.doc = *id; -- cgit v1.2.3-70-g09d2 From 2c1313c0648977540c395de584b4293c1909cbf7 Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Sat, 6 Nov 2021 23:52:49 +0900 Subject: Specify vector capacity on surround_add --- helix-term/src/commands.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'helix-term/src') diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index f2a1e66d..80cbd6d2 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -4698,7 +4698,7 @@ fn surround_add(cx: &mut Context) { let selection = doc.selection(view.id); let (open, close) = surround::get_pair(ch); - let mut changes = Vec::new(); + let mut changes = Vec::with_capacity(selection.len() * 2); for range in selection.iter() { changes.push((range.from(), range.from(), Some(Tendril::from_char(open)))); changes.push((range.to(), range.to(), Some(Tendril::from_char(close)))); -- cgit v1.2.3-70-g09d2