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-view/src/editor.rs | 2 ++ 1 file changed, 2 insertions(+) (limited to 'helix-view/src') diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 90e3bf47..52fca6d2 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -58,6 +58,7 @@ pub struct Config { /// Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant. Defaults to 400ms. #[serde(skip_serializing, deserialize_with = "deserialize_duration_millis")] pub idle_timeout: Duration, + pub completion_trigger_len: u8, } #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] @@ -87,6 +88,7 @@ impl Default for Config { auto_pairs: true, auto_completion: true, idle_timeout: Duration::from_millis(400), + completion_trigger_len: 2, } } } -- 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-view/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 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-view/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-view/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 c913bade0a3b0edaaf87e1a5e795edc951cead78 Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Sun, 24 Oct 2021 17:20:30 +0900 Subject: fix: Indentation used different default on `hx` vs `hx new_file.txt` --- helix-view/src/document.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) (limited to 'helix-view/src') diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 1f1b1f5f..8804681b 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -23,6 +23,8 @@ use crate::{DocumentId, Theme, ViewId}; /// 8kB of buffer space for encoding and decoding `Rope`s. const BUF_SIZE: usize = 8192; +const DEFAULT_INDENT: IndentStyle = IndentStyle::Spaces(4); + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub enum Mode { Normal, @@ -325,7 +327,7 @@ impl Document { encoding, text, selections: HashMap::default(), - indent_style: IndentStyle::Spaces(4), + indent_style: DEFAULT_INDENT, mode: Mode::Normal, restore_cursor: false, syntax: None, @@ -495,17 +497,15 @@ impl Document { } /// Detect the indentation used in the file, or otherwise defaults to the language indentation - /// configured in `languages.toml`, with a fallback back to 2 space indentation if it isn't + /// configured in `languages.toml`, with a fallback to 4 space indentation if it isn't /// specified. Line ending is likewise auto-detected, and will fallback to the default OS /// line ending. pub fn detect_indent_and_line_ending(&mut self) { self.indent_style = auto_detect_indent_style(&self.text).unwrap_or_else(|| { - IndentStyle::from_str( - self.language - .as_ref() - .and_then(|config| config.indent.as_ref()) - .map_or(" ", |config| config.unit.as_str()), // Fallback to 2 spaces. - ) + self.language + .as_ref() + .and_then(|config| config.indent.as_ref()) + .map_or(DEFAULT_INDENT, |config| IndentStyle::from_str(&config.unit)) }); self.line_ending = auto_detect_line_ending(&self.text).unwrap_or(DEFAULT_LINE_ENDING); } -- 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-view/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 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-view/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 e36ad8b4edcede174ffad404b70d7c12d007548e Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Mon, 25 Oct 2021 12:07:49 +0900 Subject: minor: Further simplify take_with --- helix-view/src/document.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'helix-view/src') diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 23c2dbc6..b0257f03 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -311,8 +311,7 @@ where T: Default, F: FnOnce(T) -> T, { - let t = mem::take(mut_ref); - let _ = mem::replace(mut_ref, f(t)); + *mut_ref = f(mem::take(mut_ref)); } use helix_lsp::lsp; -- 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-view/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 3e69a4852eb9914ced7438c0788170c43756d741 Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Thu, 28 Oct 2021 10:50:17 +0900 Subject: Simplify set_path --- helix-view/src/document.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) (limited to 'helix-view/src') diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 4d779656..27a19f3c 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -554,11 +554,9 @@ impl Document { } 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()) - }; + let path = path + .map(|path| helix_core::path::get_canonicalized_path(path)) + .transpose()?; // if parent doesn't exist we still want to open the document // and error out when document is saved -- cgit v1.2.3-70-g09d2 From c1e5831b2186a12ab593df1dc1dc698875484dc6 Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Thu, 28 Oct 2021 10:51:19 +0900 Subject: set_path: Pass in the function directly --- helix-view/src/document.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'helix-view/src') diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 27a19f3c..c623277d 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -555,7 +555,7 @@ impl Document { pub fn set_path(&mut self, path: Option<&Path>) -> Result<(), std::io::Error> { let path = path - .map(|path| helix_core::path::get_canonicalized_path(path)) + .map(helix_core::path::get_canonicalized_path) .transpose()?; // if parent doesn't exist we still want to open the document -- cgit v1.2.3-70-g09d2 From cec0cfdaecdb9b3e7fdf3ae3bb3252ba3f2f2752 Mon Sep 17 00:00:00 2001 From: Kirawi Date: Thu, 28 Oct 2021 21:11:30 -0400 Subject: Uncomment mapping LSP diagnostics through changes (#925) --- helix-view/src/document.rs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) (limited to 'helix-view/src') diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index c623277d..02da4b7a 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -657,14 +657,13 @@ impl Document { } // map state.diagnostics over changes::map_pos too - // NOTE: seems to do nothing since the language server resends diagnostics on each edit - // for diagnostic in &mut self.diagnostics { - // use helix_core::Assoc; - // let changes = transaction.changes(); - // diagnostic.range.start = changes.map_pos(diagnostic.range.start, Assoc::After); - // diagnostic.range.end = changes.map_pos(diagnostic.range.end, Assoc::After); - // diagnostic.line = self.text.char_to_line(diagnostic.range.start); - // } + for diagnostic in &mut self.diagnostics { + use helix_core::Assoc; + let changes = transaction.changes(); + diagnostic.range.start = changes.map_pos(diagnostic.range.start, Assoc::After); + diagnostic.range.end = changes.map_pos(diagnostic.range.end, Assoc::After); + diagnostic.line = self.text.char_to_line(diagnostic.range.start); + } // emit lsp notification if let Some(language_server) = self.language_server() { -- cgit v1.2.3-70-g09d2 From 2f8ad7f890d99dfcad7c7a5e0b35ee5a39fcd3d0 Mon Sep 17 00:00:00 2001 From: Omnikar Date: Sat, 30 Oct 2021 20:42:49 -0400 Subject: If switching away from an empty scratch buffer, remove it (#935) * If switching away from an empty scratch buffer, remove it * Move `view.jumps.push` call into `else` clause * Refactor--- helix-view/src/editor.rs | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) (limited to 'helix-view/src') diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 09fc3334..21a64651 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -232,15 +232,29 @@ impl Editor { match action { Action::Replace => { - let view = view!(self); - let jump = ( - view.doc, - self.documents[view.doc].selection(view.id).clone(), - ); - + let (view, doc) = current_ref!(self); + // If the current view is an empty scratch buffer and is not displayed in any other views, delete it. + // Boolean value is determined before the call to `view_mut` because the operation requires a borrow + // of `self.tree`, which is mutably borrowed when `view_mut` is called. + let remove_empty_scratch = !doc.is_modified() + // If the buffer has no path and is not modified, it is an empty scratch buffer. + && doc.path().is_none() + // Ensure the buffer is not displayed in any other splits. + && !self + .tree + .traverse() + .any(|(_, v)| v.doc == doc.id && v.id != view.id); let view = view_mut!(self); - view.jumps.push(jump); - view.last_accessed_doc = Some(view.doc); + if remove_empty_scratch { + // 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); + } else { + let jump = (view.doc, doc.selection(view.id).clone()); + view.jumps.push(jump); + view.last_accessed_doc = Some(view.doc); + } view.doc = id; view.offset = Position::default(); -- 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-view/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 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-view/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 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-view/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 aa4d0b464645b2834d7af483d17fdc11d61d994d Mon Sep 17 00:00:00 2001 From: ath3 Date: Thu, 4 Nov 2021 09:57:04 +0100 Subject: Fix crash on changing from empty scratch buffer to itself (#975) --- helix-view/src/editor.rs | 2 ++ 1 file changed, 2 insertions(+) (limited to 'helix-view/src') diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 633e2541..63a4ab29 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -240,6 +240,8 @@ impl Editor { let remove_empty_scratch = !doc.is_modified() // If the buffer has no path and is not modified, it is an empty scratch buffer. && doc.path().is_none() + // If the buffer we are changing to is not this buffer + && id != doc.id // Ensure the buffer is not displayed in any other splits. && !self .tree -- 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-view/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-view/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 e80708eba7e9959fa720e2563231bba542570295 Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Sat, 6 Nov 2021 18:58:40 +0900 Subject: Make sure document diagnostics are sorted --- helix-core/src/diagnostic.rs | 2 +- helix-view/src/document.rs | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) (limited to 'helix-view/src') diff --git a/helix-core/src/diagnostic.rs b/helix-core/src/diagnostic.rs index ab47e075..ad1ba16a 100644 --- a/helix-core/src/diagnostic.rs +++ b/helix-core/src/diagnostic.rs @@ -10,7 +10,7 @@ pub enum Severity { } /// A range of `char`s within the text. -#[derive(Debug)] +#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq)] pub struct Range { pub start: usize, pub end: usize, diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 0d86143b..ce5df8ee 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -918,6 +918,9 @@ impl Document { pub fn set_diagnostics(&mut self, diagnostics: Vec) { self.diagnostics = diagnostics; + // sort by range + self.diagnostics + .sort_unstable_by_key(|diagnostic| diagnostic.range); } } -- cgit v1.2.3-70-g09d2