From 07fe4a6a40b4a3e3dd45a8e9f7e7c20a2124bd73 Mon Sep 17 00:00:00 2001 From: Kangwook Lee (이강욱) Date: Sat, 4 Sep 2021 22:30:32 +0900 Subject: Add commands that extends to long words (#706) --- helix-term/src/commands.rs | 42 ++++++++++++++++++++++++++++++++++++++++++ helix-term/src/keymap.rs | 3 +++ 2 files changed, 45 insertions(+) (limited to 'helix-term/src') diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 116f39bd..dfbfe1d5 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -180,6 +180,9 @@ impl Command { move_next_long_word_end, "Move to end of next long word", extend_next_word_start, "Extend to beginning of next word", extend_prev_word_start, "Extend to beginning of previous word", + extend_next_long_word_start, "Extend to beginning of next long word", + extend_prev_long_word_start, "Extend to beginning of previous long word", + extend_next_long_word_end, "Extend to end of next long word", extend_next_word_end, "Extend to end of next word", find_till_char, "Move till next occurance of char", find_next_char, "Move to next occurance of char", @@ -622,6 +625,45 @@ fn extend_next_word_end(cx: &mut Context) { doc.set_selection(view.id, selection); } +fn extend_next_long_word_start(cx: &mut Context) { + let count = cx.count(); + let (view, doc) = current!(cx.editor); + let text = doc.text().slice(..); + + let selection = doc.selection(view.id).clone().transform(|range| { + let word = movement::move_next_long_word_start(text, range, count); + let pos = word.cursor(text); + range.put_cursor(text, pos, true) + }); + doc.set_selection(view.id, selection); +} + +fn extend_prev_long_word_start(cx: &mut Context) { + let count = cx.count(); + let (view, doc) = current!(cx.editor); + let text = doc.text().slice(..); + + let selection = doc.selection(view.id).clone().transform(|range| { + let word = movement::move_prev_long_word_start(text, range, count); + let pos = word.cursor(text); + range.put_cursor(text, pos, true) + }); + doc.set_selection(view.id, selection); +} + +fn extend_next_long_word_end(cx: &mut Context) { + let count = cx.count(); + let (view, doc) = current!(cx.editor); + let text = doc.text().slice(..); + + let selection = doc.selection(view.id).clone().transform(|range| { + let word = movement::move_next_long_word_end(text, range, count); + let pos = word.cursor(text); + range.put_cursor(text, pos, true) + }); + doc.set_selection(view.id, selection); +} + #[inline] fn find_char_impl(cx: &mut Context, search_fn: F, inclusive: bool, extend: bool) where diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 71ac01a9..a936dccc 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -527,6 +527,9 @@ impl Default for Keymaps { "w" => extend_next_word_start, "b" => extend_prev_word_start, "e" => extend_next_word_end, + "W" => extend_next_long_word_start, + "B" => extend_prev_long_word_start, + "E" => extend_next_long_word_end, "t" => extend_till_char, "f" => extend_next_char, -- cgit v1.2.3-70-g09d2 From ea2b4c687d4ee360cc8cbdbd6cf1c1cb2728a23d Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Sat, 4 Sep 2021 20:16:43 +0530 Subject: Refactor {move,extend}_char_* commands --- helix-term/src/commands.rs | 103 ++++++++++++++------------------------------- 1 file changed, 32 insertions(+), 71 deletions(-) (limited to 'helix-term/src') diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index dfbfe1d5..c248fe18 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -358,48 +358,53 @@ impl PartialEq for Command { } } -fn move_char_left(cx: &mut Context) { +fn move_impl(cx: &mut Context, move_fn: F, dir: Direction, behaviour: Movement) +where + F: Fn(RopeSlice, Range, Direction, usize, Movement) -> Range, +{ let count = cx.count(); let (view, doc) = current!(cx.editor); let text = doc.text().slice(..); - let selection = doc.selection(view.id).clone().transform(|range| { - movement::move_horizontally(text, range, Direction::Backward, count, Movement::Move) - }); + let selection = doc + .selection(view.id) + .clone() + .transform(|range| move_fn(text, range, dir, count, behaviour)); doc.set_selection(view.id, selection); } -fn move_char_right(cx: &mut Context) { - let count = cx.count(); - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); +use helix_core::movement::{move_horizontally, move_vertically}; - let selection = doc.selection(view.id).clone().transform(|range| { - movement::move_horizontally(text, range, Direction::Forward, count, Movement::Move) - }); - doc.set_selection(view.id, selection); +fn move_char_left(cx: &mut Context) { + move_impl(cx, move_horizontally, Direction::Backward, Movement::Move) } -fn move_line_up(cx: &mut Context) { - let count = cx.count(); - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); +fn move_char_right(cx: &mut Context) { + move_impl(cx, move_horizontally, Direction::Forward, Movement::Move) +} - let selection = doc.selection(view.id).clone().transform(|range| { - movement::move_vertically(text, range, Direction::Backward, count, Movement::Move) - }); - doc.set_selection(view.id, selection); +fn move_line_up(cx: &mut Context) { + move_impl(cx, move_vertically, Direction::Backward, Movement::Move) } fn move_line_down(cx: &mut Context) { - let count = cx.count(); - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); + move_impl(cx, move_vertically, Direction::Forward, Movement::Move) +} - let selection = doc.selection(view.id).clone().transform(|range| { - movement::move_vertically(text, range, Direction::Forward, count, Movement::Move) - }); - doc.set_selection(view.id, selection); +fn extend_char_left(cx: &mut Context) { + move_impl(cx, move_horizontally, Direction::Backward, Movement::Extend) +} + +fn extend_char_right(cx: &mut Context) { + move_impl(cx, move_horizontally, Direction::Forward, Movement::Extend) +} + +fn extend_line_up(cx: &mut Context) { + move_impl(cx, move_vertically, Direction::Backward, Movement::Extend) +} + +fn extend_line_down(cx: &mut Context) { + move_impl(cx, move_vertically, Direction::Forward, Movement::Extend) } fn goto_line_end(cx: &mut Context) { @@ -1001,28 +1006,6 @@ fn half_page_down(cx: &mut Context) { scroll(cx, offset, Direction::Forward); } -fn extend_char_left(cx: &mut Context) { - let count = cx.count(); - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - - let selection = doc.selection(view.id).clone().transform(|range| { - movement::move_horizontally(text, range, Direction::Backward, count, Movement::Extend) - }); - doc.set_selection(view.id, selection); -} - -fn extend_char_right(cx: &mut Context) { - let count = cx.count(); - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - - let selection = doc.selection(view.id).clone().transform(|range| { - movement::move_horizontally(text, range, Direction::Forward, count, Movement::Extend) - }); - doc.set_selection(view.id, selection); -} - fn copy_selection_on_line(cx: &mut Context, direction: Direction) { let count = cx.count(); let (view, doc) = current!(cx.editor); @@ -1093,28 +1076,6 @@ fn copy_selection_on_next_line(cx: &mut Context) { copy_selection_on_line(cx, Direction::Forward) } -fn extend_line_up(cx: &mut Context) { - let count = cx.count(); - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - - let selection = doc.selection(view.id).clone().transform(|range| { - movement::move_vertically(text, range, Direction::Backward, count, Movement::Extend) - }); - doc.set_selection(view.id, selection); -} - -fn extend_line_down(cx: &mut Context) { - let count = cx.count(); - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - - let selection = doc.selection(view.id).clone().transform(|range| { - movement::move_vertically(text, range, Direction::Forward, count, Movement::Extend) - }); - doc.set_selection(view.id, selection); -} - fn select_all(cx: &mut Context) { let (view, doc) = current!(cx.editor); -- cgit v1.2.3-70-g09d2 From 33ce8779fd43ae330fb21c698e0ce117ae40abc4 Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Sat, 4 Sep 2021 20:29:11 +0530 Subject: Refactor {move,extend}_word_* commands --- helix-term/src/commands.rs | 127 ++++++++++----------------------------------- 1 file changed, 28 insertions(+), 99 deletions(-) (limited to 'helix-term/src') diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index c248fe18..cad8cf60 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -503,7 +503,10 @@ fn goto_window_bottom(cx: &mut Context) { goto_window(cx, Align::Bottom) } -fn move_next_word_start(cx: &mut Context) { +fn move_word_impl(cx: &mut Context, move_fn: F) +where + F: Fn(RopeSlice, Range, usize) -> Range, +{ let count = cx.count(); let (view, doc) = current!(cx.editor); let text = doc.text().slice(..); @@ -511,68 +514,32 @@ fn move_next_word_start(cx: &mut Context) { let selection = doc .selection(view.id) .clone() - .transform(|range| movement::move_next_word_start(text, range, count)); + .transform(|range| move_fn(text, range, count)); doc.set_selection(view.id, selection); } -fn move_prev_word_start(cx: &mut Context) { - let count = cx.count(); - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); +fn move_next_word_start(cx: &mut Context) { + move_word_impl(cx, movement::move_next_word_start) +} - let selection = doc - .selection(view.id) - .clone() - .transform(|range| movement::move_prev_word_start(text, range, count)); - doc.set_selection(view.id, selection); +fn move_prev_word_start(cx: &mut Context) { + move_word_impl(cx, movement::move_prev_word_start) } fn move_next_word_end(cx: &mut Context) { - let count = cx.count(); - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - - let selection = doc - .selection(view.id) - .clone() - .transform(|range| movement::move_next_word_end(text, range, count)); - doc.set_selection(view.id, selection); + move_word_impl(cx, movement::move_next_word_end) } fn move_next_long_word_start(cx: &mut Context) { - let count = cx.count(); - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - - let selection = doc - .selection(view.id) - .clone() - .transform(|range| movement::move_next_long_word_start(text, range, count)); - doc.set_selection(view.id, selection); + move_word_impl(cx, movement::move_next_long_word_start) } fn move_prev_long_word_start(cx: &mut Context) { - let count = cx.count(); - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - - let selection = doc - .selection(view.id) - .clone() - .transform(|range| movement::move_prev_long_word_start(text, range, count)); - doc.set_selection(view.id, selection); + move_word_impl(cx, movement::move_prev_long_word_start) } fn move_next_long_word_end(cx: &mut Context) { - let count = cx.count(); - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - - let selection = doc - .selection(view.id) - .clone() - .transform(|range| movement::move_next_long_word_end(text, range, count)); - doc.set_selection(view.id, selection); + move_word_impl(cx, movement::move_next_long_word_end) } fn goto_file_start(cx: &mut Context) { @@ -591,82 +558,44 @@ fn goto_file_end(cx: &mut Context) { doc.set_selection(view.id, Selection::point(doc.text().len_chars())); } -fn extend_next_word_start(cx: &mut Context) { +fn extend_word_impl(cx: &mut Context, extend_fn: F) +where + F: Fn(RopeSlice, Range, usize) -> Range, +{ let count = cx.count(); let (view, doc) = current!(cx.editor); let text = doc.text().slice(..); let selection = doc.selection(view.id).clone().transform(|range| { - let word = movement::move_next_word_start(text, range, count); + let word = extend_fn(text, range, count); let pos = word.cursor(text); range.put_cursor(text, pos, true) }); doc.set_selection(view.id, selection); } -fn extend_prev_word_start(cx: &mut Context) { - let count = cx.count(); - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); +fn extend_next_word_start(cx: &mut Context) { + extend_word_impl(cx, movement::move_next_word_start) +} - let selection = doc.selection(view.id).clone().transform(|range| { - let word = movement::move_prev_word_start(text, range, count); - let pos = word.cursor(text); - range.put_cursor(text, pos, true) - }); - doc.set_selection(view.id, selection); +fn extend_prev_word_start(cx: &mut Context) { + extend_word_impl(cx, movement::move_prev_word_start) } fn extend_next_word_end(cx: &mut Context) { - let count = cx.count(); - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - - let selection = doc.selection(view.id).clone().transform(|range| { - let word = movement::move_next_word_end(text, range, count); - let pos = word.cursor(text); - range.put_cursor(text, pos, true) - }); - doc.set_selection(view.id, selection); + extend_word_impl(cx, movement::move_next_word_end) } fn extend_next_long_word_start(cx: &mut Context) { - let count = cx.count(); - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - - let selection = doc.selection(view.id).clone().transform(|range| { - let word = movement::move_next_long_word_start(text, range, count); - let pos = word.cursor(text); - range.put_cursor(text, pos, true) - }); - doc.set_selection(view.id, selection); + extend_word_impl(cx, movement::move_next_long_word_start) } fn extend_prev_long_word_start(cx: &mut Context) { - let count = cx.count(); - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - - let selection = doc.selection(view.id).clone().transform(|range| { - let word = movement::move_prev_long_word_start(text, range, count); - let pos = word.cursor(text); - range.put_cursor(text, pos, true) - }); - doc.set_selection(view.id, selection); + extend_word_impl(cx, movement::move_prev_long_word_start) } fn extend_next_long_word_end(cx: &mut Context) { - let count = cx.count(); - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - - let selection = doc.selection(view.id).clone().transform(|range| { - let word = movement::move_next_long_word_end(text, range, count); - let pos = word.cursor(text); - range.put_cursor(text, pos, true) - }); - doc.set_selection(view.id, selection); + extend_word_impl(cx, movement::move_next_long_word_end) } #[inline] -- cgit v1.2.3-70-g09d2 From 95cd2c645b853b8b31792a5f97a144676198927f Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Sat, 4 Sep 2021 20:59:08 +0530 Subject: Refactor switch_case commands --- helix-term/src/commands.rs | 48 ++++++++++++++++++---------------------------- 1 file changed, 19 insertions(+), 29 deletions(-) (limited to 'helix-term/src') diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index cad8cf60..2fbed6b3 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -811,12 +811,25 @@ fn replace(cx: &mut Context) { }) } -fn switch_case(cx: &mut Context) { +fn switch_case_impl(cx: &mut Context, change_fn: F) +where + F: Fn(Cow) -> Tendril, +{ let (view, doc) = current!(cx.editor); let selection = doc.selection(view.id); let transaction = Transaction::change_by_selection(doc.text(), selection, |range| { - let text: Tendril = range - .fragment(doc.text().slice(..)) + let text: Tendril = change_fn(range.fragment(doc.text().slice(..))); + + (range.from(), range.to(), Some(text)) + }); + + doc.apply(&transaction, view.id); + doc.append_changes_to_history(view.id); +} + +fn switch_case(cx: &mut Context) { + switch_case_impl(cx, |string| { + string .chars() .flat_map(|ch| { if ch.is_lowercase() { @@ -827,39 +840,16 @@ fn switch_case(cx: &mut Context) { vec![ch] } }) - .collect(); - - (range.from(), range.to(), Some(text)) + .collect() }); - - doc.apply(&transaction, view.id); - doc.append_changes_to_history(view.id); } fn switch_to_uppercase(cx: &mut Context) { - let (view, doc) = current!(cx.editor); - let selection = doc.selection(view.id); - let transaction = Transaction::change_by_selection(doc.text(), selection, |range| { - let text: Tendril = range.fragment(doc.text().slice(..)).to_uppercase().into(); - - (range.from(), range.to(), Some(text)) - }); - - doc.apply(&transaction, view.id); - doc.append_changes_to_history(view.id); + switch_case_impl(cx, |string| string.to_uppercase().into()); } fn switch_to_lowercase(cx: &mut Context) { - let (view, doc) = current!(cx.editor); - let selection = doc.selection(view.id); - let transaction = Transaction::change_by_selection(doc.text(), selection, |range| { - let text: Tendril = range.fragment(doc.text().slice(..)).to_lowercase().into(); - - (range.from(), range.to(), Some(text)) - }); - - doc.apply(&transaction, view.id); - doc.append_changes_to_history(view.id); + switch_case_impl(cx, |string| string.to_lowercase().into()); } pub fn scroll(cx: &mut Context, offset: usize, direction: Direction) { -- cgit v1.2.3-70-g09d2 From 183dcce992d7c5b2065a93c5835d61e8ee4e9f05 Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Sun, 5 Sep 2021 09:25:13 +0530 Subject: Add a sticky mode for keymaps (#635) --- helix-term/src/keymap.rs | 113 ++++++++++++++++++++++++++++++++------------ helix-term/src/ui/editor.rs | 36 +++++++------- 2 files changed, 104 insertions(+), 45 deletions(-) (limited to 'helix-term/src') diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index a936dccc..f0f980bd 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -4,6 +4,7 @@ use helix_core::hashmap; use helix_view::{document::Mode, info::Info, input::KeyEvent}; use serde::Deserialize; use std::{ + borrow::Cow, collections::HashMap, ops::{Deref, DerefMut}, }; @@ -47,13 +48,13 @@ macro_rules! keymap { }; (@trie - { $label:literal $($($key:literal)|+ => $value:tt,)+ } + { $label:literal $(sticky=$sticky:literal)? $($($key:literal)|+ => $value:tt,)+ } ) => { - keymap!({ $label $($($key)|+ => $value,)+ }) + keymap!({ $label $(sticky=$sticky)? $($($key)|+ => $value,)+ }) }; ( - { $label:literal $($($key:literal)|+ => $value:tt,)+ } + { $label:literal $(sticky=$sticky:literal)? $($($key:literal)|+ => $value:tt,)+ } ) => { // modified from the hashmap! macro { @@ -70,7 +71,9 @@ macro_rules! keymap { _order.push(_key); )+ )* - $crate::keymap::KeyTrie::Node($crate::keymap::KeyTrieNode::new($label, _map, _order)) + let mut _node = $crate::keymap::KeyTrieNode::new($label, _map, _order); + $( _node.is_sticky = $sticky; )? + $crate::keymap::KeyTrie::Node(_node) } }; } @@ -84,6 +87,8 @@ pub struct KeyTrieNode { map: HashMap, #[serde(skip)] order: Vec, + #[serde(skip)] + pub is_sticky: bool, } impl KeyTrieNode { @@ -92,6 +97,7 @@ impl KeyTrieNode { name: name.to_string(), map, order, + is_sticky: false, } } @@ -119,12 +125,10 @@ impl KeyTrieNode { } } } -} -impl From for Info { - fn from(node: KeyTrieNode) -> Self { - let mut body: Vec<(&str, Vec)> = Vec::with_capacity(node.len()); - for (&key, trie) in node.iter() { + pub fn infobox(&self) -> Info { + let mut body: Vec<(&str, Vec)> = 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(), @@ -136,16 +140,16 @@ impl From for Info { } } body.sort_unstable_by_key(|(_, keys)| { - node.order.iter().position(|&k| k == keys[0]).unwrap() + self.order.iter().position(|&k| k == keys[0]).unwrap() }); - let prefix = format!("{} ", node.name()); + let prefix = format!("{} ", self.name()); if body.iter().all(|(desc, _)| desc.starts_with(&prefix)) { body = body .into_iter() .map(|(desc, keys)| (desc.strip_prefix(&prefix).unwrap(), keys)) .collect(); } - Info::new(node.name(), body) + Info::new(self.name(), body) } } @@ -218,7 +222,7 @@ impl KeyTrie { } #[derive(Debug, Clone, PartialEq)] -pub enum KeymapResult { +pub enum KeymapResultKind { /// Needs more keys to execute a command. Contains valid keys for next keystroke. Pending(KeyTrieNode), Matched(Command), @@ -229,14 +233,31 @@ pub enum KeymapResult { Cancelled(Vec), } +/// Returned after looking up a key in [`Keymap`]. The `sticky` field has a +/// reference to the sticky node if one is currently active. +pub struct KeymapResult<'a> { + pub kind: KeymapResultKind, + pub sticky: Option<&'a KeyTrieNode>, +} + +impl<'a> KeymapResult<'a> { + pub fn new(kind: KeymapResultKind, sticky: Option<&'a KeyTrieNode>) -> Self { + Self { kind, sticky } + } +} + #[derive(Debug, Clone, PartialEq, Deserialize)] pub struct Keymap { /// Always a Node #[serde(flatten)] root: KeyTrie, - /// Stores pending keys waiting for the next key + /// Stores pending keys waiting for the next key. This is relative to a + /// sticky node if one is in use. #[serde(skip)] state: Vec, + /// Stores the sticky node if one is activated. + #[serde(skip)] + sticky: Option, } impl Keymap { @@ -244,6 +265,7 @@ impl Keymap { Keymap { root, state: Vec::new(), + sticky: None, } } @@ -251,27 +273,60 @@ impl Keymap { &self.root } + pub fn sticky(&self) -> Option<&KeyTrieNode> { + self.sticky.as_ref() + } + /// Returns list of keys waiting to be disambiguated. pub fn pending(&self) -> &[KeyEvent] { &self.state } - /// Lookup `key` in the keymap to try and find a command to execute + /// Lookup `key` in the keymap to try and find a command to execute. Escape + /// key cancels pending keystrokes. If there are no pending keystrokes but a + /// sticky node is in use, it will be cleared. pub fn get(&mut self, key: KeyEvent) -> KeymapResult { - let &first = self.state.get(0).unwrap_or(&key); - let trie = match self.root.search(&[first]) { - Some(&KeyTrie::Leaf(cmd)) => return KeymapResult::Matched(cmd), - None => return KeymapResult::NotFound, + if let key!(Esc) = key { + if self.state.is_empty() { + self.sticky = None; + } + return KeymapResult::new( + KeymapResultKind::Cancelled(self.state.drain(..).collect()), + self.sticky(), + ); + } + + let first = self.state.get(0).unwrap_or(&key); + let trie_node = match self.sticky { + Some(ref trie) => Cow::Owned(KeyTrie::Node(trie.clone())), + None => Cow::Borrowed(&self.root), + }; + + let trie = match trie_node.search(&[*first]) { + Some(&KeyTrie::Leaf(cmd)) => { + return KeymapResult::new(KeymapResultKind::Matched(cmd), self.sticky()) + } + None => return KeymapResult::new(KeymapResultKind::NotFound, self.sticky()), Some(t) => t, }; + self.state.push(key); match trie.search(&self.state[1..]) { - Some(&KeyTrie::Node(ref map)) => KeymapResult::Pending(map.clone()), - Some(&KeyTrie::Leaf(command)) => { + Some(&KeyTrie::Node(ref map)) => { + if map.is_sticky { + self.state.clear(); + self.sticky = Some(map.clone()); + } + KeymapResult::new(KeymapResultKind::Pending(map.clone()), self.sticky()) + } + Some(&KeyTrie::Leaf(cmd)) => { self.state.clear(); - KeymapResult::Matched(command) + return KeymapResult::new(KeymapResultKind::Matched(cmd), self.sticky()); } - None => KeymapResult::Cancelled(self.state.drain(..).collect()), + None => KeymapResult::new( + KeymapResultKind::Cancelled(self.state.drain(..).collect()), + self.sticky(), + ), } } @@ -602,19 +657,19 @@ fn merge_partial_keys() { let keymap = merged_config.keys.0.get_mut(&Mode::Normal).unwrap(); assert_eq!( - keymap.get(key!('i')), - KeymapResult::Matched(Command::normal_mode), + keymap.get(key!('i')).kind, + KeymapResultKind::Matched(Command::normal_mode), "Leaf should replace leaf" ); assert_eq!( - keymap.get(key!('无')), - KeymapResult::Matched(Command::insert_mode), + 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')), - KeymapResult::Matched(Command::jump_backward), + keymap.get(key!('z')).kind, + KeymapResultKind::Matched(Command::jump_backward), "Leaf should replace node" ); // Assumes that `g` is a node in default keymap diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 4b9c56e7..e8cd40cf 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -2,7 +2,7 @@ use crate::{ commands, compositor::{Component, Context, EventResult}, key, - keymap::{KeymapResult, Keymaps}, + keymap::{KeymapResult, KeymapResultKind, Keymaps}, ui::{Completion, ProgressSpinners}, }; @@ -638,7 +638,7 @@ impl EditorView { /// Handle events by looking them up in `self.keymaps`. Returns None /// if event was handled (a command was executed or a subkeymap was - /// activated). Only KeymapResult::{NotFound, Cancelled} is returned + /// activated). Only KeymapResultKind::{NotFound, Cancelled} is returned /// otherwise. fn handle_keymap_event( &mut self, @@ -647,29 +647,32 @@ impl EditorView { event: KeyEvent, ) -> Option { self.autoinfo = None; - match self.keymaps.get_mut(&mode).unwrap().get(event) { - KeymapResult::Matched(command) => command.execute(cxt), - KeymapResult::Pending(node) => self.autoinfo = Some(node.into()), - k @ KeymapResult::NotFound | k @ KeymapResult::Cancelled(_) => return Some(k), + let key_result = self.keymaps.get_mut(&mode).unwrap().get(event); + self.autoinfo = key_result.sticky.map(|node| node.infobox()); + + match &key_result.kind { + KeymapResultKind::Matched(command) => command.execute(cxt), + KeymapResultKind::Pending(node) => self.autoinfo = Some(node.infobox()), + KeymapResultKind::NotFound | KeymapResultKind::Cancelled(_) => return Some(key_result), } None } fn insert_mode(&mut self, cx: &mut commands::Context, event: KeyEvent) { if let Some(keyresult) = self.handle_keymap_event(Mode::Insert, cx, event) { - match keyresult { - KeymapResult::NotFound => { + match keyresult.kind { + KeymapResultKind::NotFound => { if let Some(ch) = event.char() { commands::insert::insert_char(cx, ch) } } - KeymapResult::Cancelled(pending) => { + KeymapResultKind::Cancelled(pending) => { for ev in pending { match ev.char() { Some(ch) => commands::insert::insert_char(cx, ch), None => { - if let KeymapResult::Matched(command) = - self.keymaps.get_mut(&Mode::Insert).unwrap().get(ev) + if let KeymapResultKind::Matched(command) = + self.keymaps.get_mut(&Mode::Insert).unwrap().get(ev).kind { command.execute(cx); } @@ -976,11 +979,12 @@ impl Component for EditorView { // how we entered insert mode is important, and we should track that so // we can repeat the side effect. - self.last_insert.0 = match self.keymaps.get_mut(&mode).unwrap().get(key) { - KeymapResult::Matched(command) => command, - // FIXME: insert mode can only be entered through single KeyCodes - _ => unimplemented!(), - }; + self.last_insert.0 = + match self.keymaps.get_mut(&mode).unwrap().get(key).kind { + KeymapResultKind::Matched(command) => command, + // FIXME: insert mode can only be entered through single KeyCodes + _ => unimplemented!(), + }; self.last_insert.1.clear(); } (Mode::Insert, Mode::Normal) => { -- cgit v1.2.3-70-g09d2 From 6e21a748b87a4eb9381ea0d24117711c5b547ab1 Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Sun, 5 Sep 2021 17:50:11 +0530 Subject: Fix escape not exiting insert mode (#712) Regression due to #635 where escape key in insert mode would not exit normal mode. This happened due to hard coding the escape key to cancel a sticky keymap node.--- helix-term/src/keymap.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) (limited to 'helix-term/src') diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index f0f980bd..aa60482d 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -287,13 +287,14 @@ impl Keymap { /// sticky node is in use, it will be cleared. pub fn get(&mut self, key: KeyEvent) -> KeymapResult { if let key!(Esc) = key { - if self.state.is_empty() { - self.sticky = None; + if !self.state.is_empty() { + return KeymapResult::new( + // Note that Esc is not included here + KeymapResultKind::Cancelled(self.state.drain(..).collect()), + self.sticky(), + ); } - return KeymapResult::new( - KeymapResultKind::Cancelled(self.state.drain(..).collect()), - self.sticky(), - ); + self.sticky = None; } let first = self.state.get(0).unwrap_or(&key); -- cgit v1.2.3-70-g09d2 From 10b690b5bd5d3e9ee477782ebfe3f6ff8d11cb3f Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Thu, 2 Sep 2021 10:49:23 +0900 Subject: Drop some &mut bounds where & would have sufficed --- helix-term/src/job.rs | 4 ++-- helix-view/src/document.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'helix-term/src') diff --git a/helix-term/src/job.rs b/helix-term/src/job.rs index 2ac41926..4fa38174 100644 --- a/helix-term/src/job.rs +++ b/helix-term/src/job.rs @@ -61,7 +61,7 @@ impl Jobs { } pub fn handle_callback( - &mut self, + &self, editor: &mut Editor, compositor: &mut Compositor, call: anyhow::Result>, @@ -84,7 +84,7 @@ impl Jobs { } } - pub fn add(&mut self, j: Job) { + pub fn add(&self, j: Job) { if j.wait { self.wait_futures.push(j.future); } else { diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 5677eb44..71f6680c 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -649,7 +649,7 @@ impl Document { // } // emit lsp notification - if let Some(language_server) = &self.language_server { + if let Some(language_server) = self.language_server() { let notify = language_server.text_document_did_change( self.versioned_identifier(), &old_doc, -- cgit v1.2.3-70-g09d2 From 63e191ea3b2ce116c39a446b8fab10a360fd8a33 Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Thu, 2 Sep 2021 11:19:32 +0900 Subject: lsp: Simplify lookup under method call --- helix-term/src/application.rs | 65 +++++++++++++------------------------------ 1 file changed, 19 insertions(+), 46 deletions(-) (limited to 'helix-term/src') diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 1fcca681..8241ce3a 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -4,7 +4,7 @@ use helix_view::{theme, Editor}; use crate::{args::Args, compositor::Compositor, config::Config, job::Jobs, ui}; -use log::error; +use log::{error, warn}; use std::{ io::{stdout, Write}, @@ -429,10 +429,27 @@ impl Application { Call::MethodCall(helix_lsp::jsonrpc::MethodCall { method, params, id, .. }) => { + let language_server = match self.editor.language_servers.get_by_id(server_id) { + Some(language_server) => language_server, + None => { + warn!("can't find language server with id `{}`", server_id); + return; + } + }; + let call = match MethodCall::parse(&method, params) { Some(call) => call, None => { error!("Method not found {}", method); + // language_server.reply( + // call.id, + // // TODO: make a Into trait that can cast to Err(jsonrpc::Error) + // Err(helix_lsp::jsonrpc::Error { + // code: helix_lsp::jsonrpc::ErrorCode::MethodNotFound, + // message: "Method not found".to_string(), + // data: None, + // }), + // ); return; } }; @@ -445,53 +462,9 @@ impl Application { if spinner.is_stopped() { spinner.start(); } - - let doc = self.editor.documents().find(|doc| { - doc.language_server() - .map(|server| server.id() == server_id) - .unwrap_or_default() - }); - match doc { - Some(doc) => { - // it's ok to unwrap, we check for the language server before - let server = doc.language_server().unwrap(); - tokio::spawn(server.reply(id, Ok(serde_json::Value::Null))); - } - None => { - if let Some(server) = - self.editor.language_servers.get_by_id(server_id) - { - log::warn!( - "missing document with language server id `{}`", - server_id - ); - tokio::spawn(server.reply( - id, - Err(helix_lsp::jsonrpc::Error { - code: helix_lsp::jsonrpc::ErrorCode::InternalError, - message: "document missing".to_string(), - data: None, - }), - )); - } else { - log::warn!( - "can't find language server with id `{}`", - server_id - ); - } - } - } + tokio::spawn(language_server.reply(id, Ok(serde_json::Value::Null))); } } - // self.language_server.reply( - // call.id, - // // TODO: make a Into trait that can cast to Err(jsonrpc::Error) - // Err(helix_lsp::jsonrpc::Error { - // code: helix_lsp::jsonrpc::ErrorCode::MethodNotFound, - // message: "Method not found".to_string(), - // data: None, - // }), - // ); } e => unreachable!("{:?}", e), } -- cgit v1.2.3-70-g09d2 From dc7799b980826ffe33ed635968def79daf20bd10 Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Thu, 2 Sep 2021 11:28:40 +0900 Subject: lsp: Refactor code that could use document_by_path_mut --- helix-term/src/application.rs | 11 +++-------- helix-view/src/editor.rs | 5 +++++ 2 files changed, 8 insertions(+), 8 deletions(-) (limited to 'helix-term/src') diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 8241ce3a..d3b65a4f 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -276,15 +276,10 @@ impl Application { match notification { Notification::PublishDiagnostics(params) => { - let path = Some(params.uri.to_file_path().unwrap()); + let path = params.uri.to_file_path().unwrap(); + let doc = self.editor.document_by_path_mut(&path); - let doc = self - .editor - .documents - .iter_mut() - .find(|(_, doc)| doc.path() == path.as_ref()); - - if let Some((_, doc)) = doc { + if let Some(doc) = doc { let text = doc.text(); let diagnostics = params diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 050f2645..0d914e45 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -340,6 +340,11 @@ impl Editor { .find(|doc| doc.path().map(|p| p == path.as_ref()).unwrap_or(false)) } + pub fn document_by_path_mut>(&mut self, path: P) -> Option<&mut Document> { + self.documents_mut() + .find(|doc| doc.path().map(|p| p == path.as_ref()).unwrap_or(false)) + } + pub fn cursor(&self) -> (Option, CursorKind) { let view = view!(self); let doc = &self.documents[view.doc]; -- cgit v1.2.3-70-g09d2 From 46f3c69f06cc55f36bcc6244a9f96c2481836dea Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Thu, 2 Sep 2021 13:55:08 +0900 Subject: lsp: Don't send notifications until initialize completes Then send open events for all documents with the LSP attached. --- helix-lsp/src/lib.rs | 98 +++++++++++++++++++++---------------------- helix-lsp/src/transport.rs | 29 ++++++++++++- helix-term/src/application.rs | 31 ++++++++++++++ helix-view/src/editor.rs | 5 ++- 4 files changed, 111 insertions(+), 52 deletions(-) (limited to 'helix-term/src') diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index e10c107b..7357c885 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -226,6 +226,8 @@ impl MethodCall { #[derive(Debug, PartialEq, Clone)] pub enum Notification { + // we inject this notification to signal the LSP is ready + Initialized, PublishDiagnostics(lsp::PublishDiagnosticsParams), ShowMessage(lsp::ShowMessageParams), LogMessage(lsp::LogMessageParams), @@ -237,6 +239,7 @@ impl Notification { use lsp::notification::Notification as _; let notification = match method { + lsp::notification::Initialized::METHOD => Self::Initialized, lsp::notification::PublishDiagnostics::METHOD => { let params: lsp::PublishDiagnosticsParams = params .parse() @@ -294,7 +297,7 @@ impl Registry { } } - pub fn get_by_id(&mut self, id: usize) -> Option<&Client> { + pub fn get_by_id(&self, id: usize) -> Option<&Client> { self.inner .values() .find(|(client_id, _)| client_id == &id) @@ -302,55 +305,52 @@ impl Registry { } pub fn get(&mut self, language_config: &LanguageConfiguration) -> Result> { - if let Some(config) = &language_config.language_server { - // avoid borrow issues - let inner = &mut self.inner; - let s_incoming = &mut self.incoming; - - match inner.entry(language_config.scope.clone()) { - Entry::Occupied(entry) => Ok(entry.get().1.clone()), - Entry::Vacant(entry) => { - // initialize a new client - let id = self.counter.fetch_add(1, Ordering::Relaxed); - let (client, incoming, initialize_notify) = Client::start( - &config.command, - &config.args, - serde_json::from_str(language_config.config.as_deref().unwrap_or("")).ok(), - id, - )?; - s_incoming.push(UnboundedReceiverStream::new(incoming)); - let client = Arc::new(client); - - let _client = client.clone(); - // Initialize the client asynchronously - tokio::spawn(async move { - use futures_util::TryFutureExt; - let value = _client - .capabilities - .get_or_try_init(|| { - _client - .initialize() - .map_ok(|response| response.capabilities) - }) - .await; - - value.expect("failed to initialize capabilities"); - - // next up, notify - _client - .notify::(lsp::InitializedParams {}) - .await - .unwrap(); - - initialize_notify.notify_one(); - }); - - entry.insert((id, client.clone())); - Ok(client) - } + let config = match &language_config.language_server { + Some(config) => config, + None => return Err(Error::LspNotDefined), + }; + + match self.inner.entry(language_config.scope.clone()) { + Entry::Occupied(entry) => Ok(entry.get().1.clone()), + Entry::Vacant(entry) => { + // initialize a new client + let id = self.counter.fetch_add(1, Ordering::Relaxed); + let (client, incoming, initialize_notify) = Client::start( + &config.command, + &config.args, + serde_json::from_str(language_config.config.as_deref().unwrap_or("")).ok(), + id, + )?; + self.incoming.push(UnboundedReceiverStream::new(incoming)); + let client = Arc::new(client); + + // Initialize the client asynchronously + let _client = client.clone(); + tokio::spawn(async move { + use futures_util::TryFutureExt; + let value = _client + .capabilities + .get_or_try_init(|| { + _client + .initialize() + .map_ok(|response| response.capabilities) + }) + .await; + + value.expect("failed to initialize capabilities"); + + // next up, notify + _client + .notify::(lsp::InitializedParams {}) + .await + .unwrap(); + + initialize_notify.notify_one(); + }); + + entry.insert((id, client.clone())); + Ok(client) } - } else { - Err(Error::LspNotDefined) } } diff --git a/helix-lsp/src/transport.rs b/helix-lsp/src/transport.rs index 071c5b93..cf7e66a8 100644 --- a/helix-lsp/src/transport.rs +++ b/helix-lsp/src/transport.rs @@ -64,11 +64,16 @@ impl Transport { let transport = Arc::new(transport); - tokio::spawn(Self::recv(transport.clone(), server_stdout, client_tx)); + tokio::spawn(Self::recv( + transport.clone(), + server_stdout, + client_tx.clone(), + )); tokio::spawn(Self::err(transport.clone(), server_stderr)); tokio::spawn(Self::send( transport, server_stdin, + client_tx, client_rx, notify.clone(), )); @@ -269,6 +274,7 @@ impl Transport { async fn send( transport: Arc, mut server_stdin: BufWriter, + mut client_tx: UnboundedSender<(usize, jsonrpc::Call)>, mut client_rx: UnboundedReceiver, initialize_notify: Arc, ) { @@ -303,6 +309,22 @@ impl Transport { _ = initialize_notify.notified() => { // TODO: notified is technically not cancellation safe // server successfully initialized is_pending = false; + + use lsp_types::notification::Notification; + // Hack: inject an initialized notification so we trigger code that needs to happen after init + let notification = ServerMessage::Call(jsonrpc::Call::Notification(jsonrpc::Notification { + jsonrpc: None, + + method: lsp_types::notification::Initialized::METHOD.to_string(), + params: jsonrpc::Params::None, + })); + match transport.process_server_message(&mut client_tx, notification).await { + Ok(_) => {} + Err(err) => { + error!("err: <- {:?}", err); + } + } + // drain the pending queue and send payloads to server for msg in pending_messages.drain(..) { log::info!("Draining pending message {:?}", msg); @@ -317,6 +339,11 @@ impl Transport { msg = client_rx.recv() => { if let Some(msg) = msg { if is_pending && !is_initialize(&msg) { + // ignore notifications + if let Payload::Notification(_) = msg { + continue; + } + log::info!("Language server not initialized, delaying request"); pending_messages.push(msg); } else { diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index d3b65a4f..e21c5504 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -275,6 +275,37 @@ impl Application { }; match notification { + Notification::Initialized => { + let language_server = + match self.editor.language_servers.get_by_id(server_id) { + Some(language_server) => language_server, + None => { + warn!("can't find language server with id `{}`", server_id); + return; + } + }; + + let docs = self.editor.documents().filter(|doc| { + doc.language_server().map(|server| server.id()) == Some(server_id) + }); + + // trigger textDocument/didOpen for docs that are already open + for doc in docs { + // TODO: extract and share with editor.open + let language_id = doc + .language() + .and_then(|s| s.split('.').last()) // source.rust + .map(ToOwned::to_owned) + .unwrap_or_default(); + + tokio::spawn(language_server.text_document_did_open( + doc.url().unwrap(), + doc.version(), + doc.text(), + language_id, + )); + } + } Notification::PublishDiagnostics(params) => { let path = params.uri.to_file_path().unwrap(); let doc = self.editor.document_by_path_mut(&path); diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index c8abd5b5..3d2d4a87 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -255,20 +255,21 @@ impl Editor { .and_then(|language| self.language_servers.get(language).ok()); if let Some(language_server) = language_server { - doc.set_language_server(Some(language_server.clone())); - let language_id = doc .language() .and_then(|s| s.split('.').last()) // source.rust .map(ToOwned::to_owned) .unwrap_or_default(); + // TODO: this now races with on_init code if the init happens too quickly tokio::spawn(language_server.text_document_did_open( doc.url().unwrap(), doc.version(), doc.text(), language_id, )); + + doc.set_language_server(Some(language_server)); } let id = self.documents.insert(doc); -- cgit v1.2.3-70-g09d2 From 7a9db951829d37447a414f03802297f4b43e02a6 Mon Sep 17 00:00:00 2001 From: Kangwook Lee (이강욱) Date: Tue, 7 Sep 2021 23:22:39 +0900 Subject: Add command to extend to line start or end (#717) --- helix-term/src/commands.rs | 39 +++++++++++++++++++++++++++++++++------ helix-term/src/keymap.rs | 4 ++-- 2 files changed, 35 insertions(+), 8 deletions(-) (limited to 'helix-term/src') diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 2fbed6b3..38e65537 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -252,6 +252,9 @@ impl Command { // TODO: different description ? goto_line_end_newline, "Goto line end", goto_first_nonwhitespace, "Goto first non-blank in line", + extend_to_line_start, "Extend to line start", + extend_to_line_end, "Extend to line end", + extend_to_line_end_newline, "Extend to line end", signature_help, "Show signature help", insert_tab, "Insert tab char", insert_newline, "Insert newline char", @@ -407,7 +410,7 @@ fn extend_line_down(cx: &mut Context) { move_impl(cx, move_vertically, Direction::Forward, Movement::Extend) } -fn goto_line_end(cx: &mut Context) { +fn goto_line_end_impl(cx: &mut Context, movement: Movement) { let (view, doc) = current!(cx.editor); let text = doc.text().slice(..); @@ -418,12 +421,20 @@ fn goto_line_end(cx: &mut Context) { let pos = graphemes::prev_grapheme_boundary(text, line_end_char_index(&text, line)) .max(line_start); - range.put_cursor(text, pos, doc.mode == Mode::Select) + range.put_cursor(text, pos, movement == Movement::Extend) }); doc.set_selection(view.id, selection); } -fn goto_line_end_newline(cx: &mut Context) { +fn goto_line_end(cx: &mut Context) { + goto_line_end_impl(cx, Movement::Move) +} + +fn extend_to_line_end(cx: &mut Context) { + goto_line_end_impl(cx, Movement::Extend) +} + +fn goto_line_end_newline_impl(cx: &mut Context, movement: Movement) { let (view, doc) = current!(cx.editor); let text = doc.text().slice(..); @@ -431,12 +442,20 @@ fn goto_line_end_newline(cx: &mut Context) { let line = range.cursor_line(text); let pos = line_end_char_index(&text, line); - range.put_cursor(text, pos, doc.mode == Mode::Select) + range.put_cursor(text, pos, movement == Movement::Extend) }); doc.set_selection(view.id, selection); } -fn goto_line_start(cx: &mut Context) { +fn goto_line_end_newline(cx: &mut Context) { + goto_line_end_newline_impl(cx, Movement::Move) +} + +fn extend_to_line_end_newline(cx: &mut Context) { + goto_line_end_newline_impl(cx, Movement::Extend) +} + +fn goto_line_start_impl(cx: &mut Context, movement: Movement) { let (view, doc) = current!(cx.editor); let text = doc.text().slice(..); @@ -445,11 +464,19 @@ fn goto_line_start(cx: &mut Context) { // adjust to start of the line let pos = text.line_to_char(line); - range.put_cursor(text, pos, doc.mode == Mode::Select) + range.put_cursor(text, pos, movement == Movement::Extend) }); doc.set_selection(view.id, selection); } +fn goto_line_start(cx: &mut Context) { + goto_line_start_impl(cx, Movement::Move) +} + +fn extend_to_line_start(cx: &mut Context) { + goto_line_start_impl(cx, Movement::Extend) +} + fn goto_first_nonwhitespace(cx: &mut Context) { let (view, doc) = current!(cx.editor); let text = doc.text().slice(..); diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index aa60482d..1b9d87b5 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -592,8 +592,8 @@ impl Default for Keymaps { "T" => extend_till_prev_char, "F" => extend_prev_char, - "home" => goto_line_start, - "end" => goto_line_end, + "home" => extend_to_line_start, + "end" => extend_to_line_end, "esc" => exit_select_mode, "v" => normal_mode, -- cgit v1.2.3-70-g09d2 From 2ce87968cd5983167271cd306ab1c1d72a9c488d Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Wed, 8 Sep 2021 14:19:25 +0900 Subject: ui: Be smarter about centering previews Try centering the whole block. If the block is too big for the viewport, then make sure that the first line is within the preview. --- helix-term/src/ui/picker.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) (limited to 'helix-term/src') diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 06e424ea..e040e0ff 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -124,9 +124,13 @@ impl Component for FilePicker { }) { // align to middle let first_line = line - .map(|(start, _)| start) - .unwrap_or(0) - .saturating_sub(inner.height as usize / 2); + .map(|(start, end)| { + let height = end.saturating_sub(start) + 1; + let middle = start + (height.saturating_sub(1) / 2); + middle.saturating_sub(inner.height as usize / 2).min(start) + }) + .unwrap_or(0); + let offset = Position::new(first_line, 0); let highlights = EditorView::doc_syntax_highlights( -- cgit v1.2.3-70-g09d2 From 011f9aa47f2316f120da48d342430c7c5caaf107 Mon Sep 17 00:00:00 2001 From: CossonLeo Date: Wed, 8 Sep 2021 07:33:59 +0000 Subject: Optimize completion doc position. (#691) * optimize completion doc's render * optimize completion doc's render * optimize completion doc position * cargo fmt * fix panic * use saturating_sub * fixs * fix clippy * limit completion doc max width 120--- helix-term/src/ui/completion.rs | 45 +++++++++++++++++++++++++------------ helix-term/src/ui/markdown.rs | 28 +++++++++++++++++++---- helix-term/src/ui/popup.rs | 50 ++++++++++++++++++++++++----------------- 3 files changed, 84 insertions(+), 39 deletions(-) (limited to 'helix-term/src') diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index 90657764..6c9e3a80 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -262,8 +262,7 @@ impl Component for Completion { .cursor(doc.text().slice(..)); let cursor_pos = (helix_core::coords_at_pos(doc.text().slice(..), cursor_pos).row - view.offset.row) as u16; - - let mut doc = match &option.documentation { + let mut markdown_doc = match &option.documentation { Some(lsp::Documentation::String(contents)) | Some(lsp::Documentation::MarkupContent(lsp::MarkupContent { kind: lsp::MarkupKind::PlainText, @@ -311,24 +310,42 @@ impl Component for Completion { None => return, }; - let half = area.height / 2; - let height = 15.min(half); - // we want to make sure the cursor is visible (not hidden behind the documentation) - let y = if cursor_pos + area.y - >= (cx.editor.tree.area().height - height - 2/* statusline + commandline */) - { - 0 + let (popup_x, popup_y) = self.popup.get_rel_position(area, cx); + let (popup_width, _popup_height) = self.popup.get_size(); + let mut width = area + .width + .saturating_sub(popup_x) + .saturating_sub(popup_width); + let area = if width > 30 { + let mut height = area.height.saturating_sub(popup_y); + let x = popup_x + popup_width; + let y = popup_y; + + if let Some((rel_width, rel_height)) = markdown_doc.required_size((width, height)) { + width = rel_width; + height = rel_height; + } + Rect::new(x, y, width, height) } else { - // -2 to subtract command line + statusline. a bit of a hack, because of splits. - area.height.saturating_sub(height).saturating_sub(2) - }; + let half = area.height / 2; + let height = 15.min(half); + // we want to make sure the cursor is visible (not hidden behind the documentation) + let y = if cursor_pos + area.y + >= (cx.editor.tree.area().height - height - 2/* statusline + commandline */) + { + 0 + } else { + // -2 to subtract command line + statusline. a bit of a hack, because of splits. + area.height.saturating_sub(height).saturating_sub(2) + }; - let area = Rect::new(0, y, area.width, height); + Rect::new(0, y, area.width, height) + }; // clear area let background = cx.editor.theme.get("ui.popup"); surface.clear_with(area, background); - doc.render(area, surface, cx); + markdown_doc.render(area, surface, cx); } } } diff --git a/helix-term/src/ui/markdown.rs b/helix-term/src/ui/markdown.rs index 28542cdc..87b35a2d 100644 --- a/helix-term/src/ui/markdown.rs +++ b/helix-term/src/ui/markdown.rs @@ -215,10 +215,30 @@ impl Component for Markdown { } fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> { - let contents = parse(&self.contents, None, &self.config_loader); let padding = 2; - let width = std::cmp::min(contents.width() as u16 + padding, viewport.0); - let height = std::cmp::min(contents.height() as u16 + padding, viewport.1); - Some((width, height)) + if padding >= viewport.1 || padding >= viewport.0 { + return None; + } + let contents = parse(&self.contents, None, &self.config_loader); + let max_text_width = (viewport.0 - padding).min(120); + let mut text_width = 0; + let mut height = padding; + for content in contents { + height += 1; + let content_width = content.width() as u16; + if content_width > max_text_width { + text_width = max_text_width; + height += content_width / max_text_width; + } else if content_width > text_width { + text_width = content_width; + } + + if height >= viewport.1 { + height = viewport.1; + break; + } + } + + Some((text_width + padding, height)) } } diff --git a/helix-term/src/ui/popup.rs b/helix-term/src/ui/popup.rs index e126c845..846cefb8 100644 --- a/helix-term/src/ui/popup.rs +++ b/helix-term/src/ui/popup.rs @@ -31,6 +31,33 @@ impl Popup { self.position = pos; } + pub fn get_rel_position(&mut self, viewport: Rect, cx: &Context) -> (u16, u16) { + let position = self + .position + .get_or_insert_with(|| cx.editor.cursor().0.unwrap_or_default()); + + let (width, height) = self.size; + + // -- make sure frame doesn't stick out of bounds + let mut rel_x = position.col as u16; + let rel_y = position.row as u16; + if viewport.width <= rel_x + width { + rel_x = rel_x.saturating_sub((rel_x + width).saturating_sub(viewport.width)); + }; + + // TODO: be able to specify orientation preference. We want above for most popups, below + // for menus/autocomplete. + if height <= rel_y { + (rel_x, rel_y.saturating_sub(height)) // position above point + } else { + (rel_x, rel_y + 1) // position below point + } + } + + pub fn get_size(&self) -> (u16, u16) { + (self.size.0, self.size.1) + } + pub fn scroll(&mut self, offset: usize, direction: bool) { if direction { self.scroll += offset; @@ -108,29 +135,10 @@ impl Component for Popup { fn render(&mut self, viewport: Rect, surface: &mut Surface, cx: &mut Context) { cx.scroll = Some(self.scroll); - let position = self - .position - .get_or_insert_with(|| cx.editor.cursor().0.unwrap_or_default()); - - let (width, height) = self.size; - - // -- make sure frame doesn't stick out of bounds - let mut rel_x = position.col as u16; - let mut rel_y = position.row as u16; - if viewport.width <= rel_x + width { - rel_x = rel_x.saturating_sub((rel_x + width).saturating_sub(viewport.width)); - }; - - // TODO: be able to specify orientation preference. We want above for most popups, below - // for menus/autocomplete. - if height <= rel_y { - rel_y = rel_y.saturating_sub(height) // position above point - } else { - rel_y += 1 // position below point - } + let (rel_x, rel_y) = self.get_rel_position(viewport, cx); // clip to viewport - let area = viewport.intersection(Rect::new(rel_x, rel_y, width, height)); + let area = viewport.intersection(Rect::new(rel_x, rel_y, self.size.0, self.size.1)); // clear area let background = cx.editor.theme.get("ui.popup"); -- cgit v1.2.3-70-g09d2 From 72cf86e4626c01c6ce2371c7b134ab7a7041620c Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Wed, 8 Sep 2021 14:52:09 +0900 Subject: Regex prompts should have a history with a specifiable register --- helix-term/src/commands.rs | 38 ++++++++++++++-------------- helix-term/src/ui/editor.rs | 4 +-- helix-term/src/ui/mod.rs | 9 +++---- helix-view/src/editor.rs | 6 ++--- helix-view/src/lib.rs | 2 -- helix-view/src/register_selection.rs | 48 ------------------------------------ 6 files changed, 29 insertions(+), 78 deletions(-) delete mode 100644 helix-view/src/register_selection.rs (limited to 'helix-term/src') diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 38e65537..841af22a 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -44,7 +44,7 @@ use once_cell::sync::Lazy; use serde::de::{self, Deserialize, Deserializer}; pub struct Context<'a> { - pub selected_register: helix_view::RegisterSelection, + pub register: Option, pub count: Option, pub editor: &'a mut Editor, @@ -1030,7 +1030,8 @@ fn select_all(cx: &mut Context) { } fn select_regex(cx: &mut Context) { - let prompt = ui::regex_prompt(cx, "select:".into(), move |view, doc, _, regex| { + let reg = cx.register.unwrap_or('/'); + let prompt = ui::regex_prompt(cx, "select:".into(), Some(reg), move |view, doc, regex| { let text = doc.text().slice(..); if let Some(selection) = selection::select_on_matches(text, doc.selection(view.id), ®ex) { @@ -1042,7 +1043,8 @@ fn select_regex(cx: &mut Context) { } fn split_selection(cx: &mut Context) { - let prompt = ui::regex_prompt(cx, "split:".into(), move |view, doc, _, regex| { + let reg = cx.register.unwrap_or('/'); + let prompt = ui::regex_prompt(cx, "split:".into(), Some(reg), move |view, doc, regex| { let text = doc.text().slice(..); let selection = selection::split_on_matches(text, doc.selection(view.id), ®ex); doc.set_selection(view.id, selection); @@ -1100,6 +1102,7 @@ fn search_impl(doc: &mut Document, view: &mut View, contents: &str, regex: &Rege // TODO: use one function for search vs extend fn search(cx: &mut Context) { + let reg = cx.register.unwrap_or('/'); let (_, doc) = current!(cx.editor); // TODO: could probably share with select_on_matches? @@ -1108,10 +1111,8 @@ fn search(cx: &mut Context) { // feed chunks into the regex yet let contents = doc.text().slice(..).to_string(); - let prompt = ui::regex_prompt(cx, "search:".into(), move |view, doc, registers, regex| { + let prompt = ui::regex_prompt(cx, "search:".into(), Some(reg), move |view, doc, regex| { search_impl(doc, view, &contents, ®ex, false); - // TODO: only store on enter (accept), not update - registers.write('/', vec![regex.as_str().to_string()]); }); cx.push_layer(Box::new(prompt)); @@ -1119,9 +1120,9 @@ fn search(cx: &mut Context) { fn search_next_impl(cx: &mut Context, extend: bool) { let (view, doc) = current!(cx.editor); - let registers = &mut cx.editor.registers; + let registers = &cx.editor.registers; if let Some(query) = registers.read('/') { - let query = query.first().unwrap(); + let query = query.last().unwrap(); let contents = doc.text().slice(..).to_string(); let regex = Regex::new(query).unwrap(); search_impl(doc, view, &contents, ®ex, extend); @@ -1141,7 +1142,7 @@ fn search_selection(cx: &mut Context) { let contents = doc.text().slice(..); let query = doc.selection(view.id).primary().fragment(contents); let regex = regex::escape(&query); - cx.editor.registers.write('/', vec![regex]); + cx.editor.registers.get_mut('/').push(regex); search_next(cx); } @@ -1200,7 +1201,7 @@ fn delete_selection_impl(reg: &mut Register, doc: &mut Document, view_id: ViewId } fn delete_selection(cx: &mut Context) { - let reg_name = cx.selected_register.name(); + let reg_name = cx.register.unwrap_or('"'); let (view, doc) = current!(cx.editor); let registers = &mut cx.editor.registers; let reg = registers.get_mut(reg_name); @@ -1213,7 +1214,7 @@ fn delete_selection(cx: &mut Context) { } fn change_selection(cx: &mut Context) { - let reg_name = cx.selected_register.name(); + let reg_name = cx.register.unwrap_or('"'); let (view, doc) = current!(cx.editor); let registers = &mut cx.editor.registers; let reg = registers.get_mut(reg_name); @@ -3346,12 +3347,12 @@ fn yank(cx: &mut Context) { let msg = format!( "yanked {} selection(s) to register {}", values.len(), - cx.selected_register.name() + cx.register.unwrap_or('"') ); cx.editor .registers - .write(cx.selected_register.name(), values); + .write(cx.register.unwrap_or('"'), values); cx.editor.set_status(msg); exit_select_mode(cx); @@ -3524,7 +3525,7 @@ fn paste_primary_clipboard_before(cx: &mut Context) { } fn replace_with_yanked(cx: &mut Context) { - let reg_name = cx.selected_register.name(); + let reg_name = cx.register.unwrap_or('"'); let (view, doc) = current!(cx.editor); let registers = &mut cx.editor.registers; @@ -3575,7 +3576,7 @@ fn replace_selections_with_primary_clipboard(cx: &mut Context) { } fn paste_after(cx: &mut Context) { - let reg_name = cx.selected_register.name(); + let reg_name = cx.register.unwrap_or('"'); let (view, doc) = current!(cx.editor); let registers = &mut cx.editor.registers; @@ -3589,7 +3590,7 @@ fn paste_after(cx: &mut Context) { } fn paste_before(cx: &mut Context) { - let reg_name = cx.selected_register.name(); + let reg_name = cx.register.unwrap_or('"'); let (view, doc) = current!(cx.editor); let registers = &mut cx.editor.registers; @@ -3770,7 +3771,8 @@ fn join_selections(cx: &mut Context) { fn keep_selections(cx: &mut Context) { // keep selections matching regex - let prompt = ui::regex_prompt(cx, "keep:".into(), move |view, doc, _, regex| { + let reg = cx.register.unwrap_or('/'); + let prompt = ui::regex_prompt(cx, "keep:".into(), Some(reg), move |view, doc, regex| { let text = doc.text().slice(..); if let Some(selection) = selection::keep_matches(text, doc.selection(view.id), ®ex) { @@ -4103,7 +4105,7 @@ fn wclose(cx: &mut Context) { fn select_register(cx: &mut Context) { cx.on_next_key(move |cx, event| { if let Some(ch) = event.char() { - cx.editor.selected_register.select(ch); + cx.editor.selected_register = Some(ch); } }) } diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index e8cd40cf..52cf3d2b 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -710,7 +710,7 @@ impl EditorView { // debug_assert!(cxt.count != 0); // set the register - cxt.selected_register = cxt.editor.selected_register.take(); + cxt.register = cxt.editor.selected_register.take(); self.handle_keymap_event(mode, cxt, event); if self.keymaps.pending().is_empty() { @@ -887,9 +887,9 @@ impl EditorView { impl Component for EditorView { fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult { let mut cxt = commands::Context { - selected_register: helix_view::RegisterSelection::default(), editor: &mut cx.editor, count: None, + register: None, callback: None, on_next_key_callback: None, jobs: cx.jobs, diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 0a1e24b5..07eef352 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -20,7 +20,6 @@ pub use spinner::{ProgressSpinners, Spinner}; pub use text::Text; use helix_core::regex::Regex; -use helix_core::register::Registers; use helix_view::{Document, Editor, View}; use std::path::PathBuf; @@ -28,7 +27,8 @@ use std::path::PathBuf; pub fn regex_prompt( cx: &mut crate::commands::Context, prompt: std::borrow::Cow<'static, str>, - fun: impl Fn(&mut View, &mut Document, &mut Registers, Regex) + 'static, + history_register: Option, + fun: impl Fn(&mut View, &mut Document, Regex) + 'static, ) -> Prompt { let (view, doc) = current!(cx.editor); let view_id = view.id; @@ -36,7 +36,7 @@ pub fn regex_prompt( Prompt::new( prompt, - None, + history_register, |_input: &str| Vec::new(), // this is fine because Vec::new() doesn't allocate move |cx: &mut crate::compositor::Context, input: &str, event: PromptEvent| { match event { @@ -56,12 +56,11 @@ pub fn regex_prompt( match Regex::new(input) { Ok(regex) => { let (view, doc) = current!(cx.editor); - let registers = &mut cx.editor.registers; // revert state to what it was before the last update doc.set_selection(view.id, snapshot.clone()); - fun(view, doc, registers, regex); + fun(view, doc, regex); view.ensure_cursor_in_view(doc, cx.editor.config.scrolloff); } diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 3d2d4a87..52a0060c 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -3,7 +3,7 @@ use crate::{ graphics::{CursorKind, Rect}, theme::{self, Theme}, tree::Tree, - Document, DocumentId, RegisterSelection, View, ViewId, + Document, DocumentId, View, ViewId, }; use futures_util::future; @@ -73,7 +73,7 @@ pub struct Editor { pub tree: Tree, pub documents: SlotMap, pub count: Option, - pub selected_register: RegisterSelection, + pub selected_register: Option, pub registers: Registers, pub theme: Theme, pub language_servers: helix_lsp::Registry, @@ -111,7 +111,7 @@ impl Editor { tree: Tree::new(area), documents: SlotMap::with_key(), count: None, - selected_register: RegisterSelection::default(), + selected_register: None, theme: themes.default(), language_servers, syn_loader: config_loader, diff --git a/helix-view/src/lib.rs b/helix-view/src/lib.rs index 9bcc0b7d..c37474d6 100644 --- a/helix-view/src/lib.rs +++ b/helix-view/src/lib.rs @@ -8,7 +8,6 @@ pub mod graphics; pub mod info; pub mod input; pub mod keyboard; -pub mod register_selection; pub mod theme; pub mod tree; pub mod view; @@ -20,6 +19,5 @@ slotmap::new_key_type! { pub use document::Document; pub use editor::Editor; -pub use register_selection::RegisterSelection; pub use theme::Theme; pub use view::View; diff --git a/helix-view/src/register_selection.rs b/helix-view/src/register_selection.rs deleted file mode 100644 index a2b78f72..00000000 --- a/helix-view/src/register_selection.rs +++ /dev/null @@ -1,48 +0,0 @@ -/// Register selection and configuration -/// -/// This is a kind a of specialized `Option` for register selection. -/// Point is to keep whether the register selection has been explicitely -/// set or not while being convenient by knowing the default register name. -#[derive(Debug)] -pub struct RegisterSelection { - selected: char, - default_name: char, -} - -impl RegisterSelection { - pub fn new(default_name: char) -> Self { - Self { - selected: default_name, - default_name, - } - } - - pub fn select(&mut self, name: char) { - self.selected = name; - } - - pub fn take(&mut self) -> Self { - Self { - selected: std::mem::replace(&mut self.selected, self.default_name), - default_name: self.default_name, - } - } - - pub fn is_default(&self) -> bool { - self.selected == self.default_name - } - - pub fn name(&self) -> char { - self.selected - } -} - -impl Default for RegisterSelection { - fn default() -> Self { - let default_name = '"'; - Self { - selected: default_name, - default_name, - } - } -} -- cgit v1.2.3-70-g09d2 From 3426285a6341702ace35817d38cefd2bb8b16437 Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Wed, 8 Sep 2021 14:58:11 +0900 Subject: fix: Don't automatically search_next on * Refs #713 --- helix-term/src/commands.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'helix-term/src') diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 841af22a..a7a71576 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1143,7 +1143,8 @@ fn search_selection(cx: &mut Context) { let query = doc.selection(view.id).primary().fragment(contents); let regex = regex::escape(&query); cx.editor.registers.get_mut('/').push(regex); - search_next(cx); + let msg = format!("register '{}' set to '{}'", '\\', query); + cx.editor.set_status(msg); } fn extend_line(cx: &mut Context) { -- cgit v1.2.3-70-g09d2 From 94abc52b3b0929f399cea14e1efcf2c1d0a31ad8 Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Fri, 10 Sep 2021 19:44:23 +0530 Subject: feat: Sticky view mode with Z (#719) --- book/src/keymap.md | 74 +++++++++++++++++++++++++++--------------------- helix-term/src/keymap.rs | 16 +++++++++++ 2 files changed, 57 insertions(+), 33 deletions(-) (limited to 'helix-term/src') diff --git a/book/src/keymap.md b/book/src/keymap.md index 51e56eaa..4fa5033d 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -6,38 +6,39 @@ > NOTE: Unlike vim, `f`, `F`, `t` and `T` are not confined to the current line. -| Key | Description | Command | -| ----- | ----------- | ------- | -| `h`, `Left` | Move left | `move_char_left` | -| `j`, `Down` | Move down | `move_char_right` | -| `k`, `Up` | Move up | `move_line_up` | -| `l`, `Right` | Move right | `move_line_down` | -| `w` | Move next word start | `move_next_word_start` | -| `b` | Move previous word start | `move_prev_word_start` | -| `e` | Move next word end | `move_next_word_end` | -| `W` | Move next WORD start | `move_next_long_word_start` | -| `B` | Move previous WORD start | `move_prev_long_word_start` | -| `E` | Move next WORD end | `move_next_long_word_end` | -| `t` | Find 'till next char | `find_till_char` | -| `f` | Find next char | `find_next_char` | -| `T` | Find 'till previous char | `till_prev_char` | -| `F` | Find previous char | `find_prev_char` | -| `Home` | Move to the start of the line | `goto_line_start` | -| `End` | Move to the end of the line | `goto_line_end` | -| `PageUp` | Move page up | `page_up` | -| `PageDown` | Move page down | `page_down` | -| `Ctrl-u` | Move half page up | `half_page_up` | -| `Ctrl-d` | Move half page down | `half_page_down` | -| `Ctrl-i` | Jump forward on the jumplist | `jump_forward` | -| `Ctrl-o` | Jump backward on the jumplist | `jump_backward` | -| `v` | Enter [select (extend) mode](#select--extend-mode) | `select_mode` | -| `g` | Enter [goto mode](#goto-mode) | N/A | -| `m` | Enter [match mode](#match-mode) | N/A | -| `:` | Enter command mode | `command_mode` | -| `z` | Enter [view mode](#view-mode) | N/A | -| `Ctrl-w` | Enter [window mode](#window-mode) | N/A | -| `Space` | Enter [space mode](#space-mode) | N/A | -| `K` | Show documentation for the item under the cursor | `hover` | +| Key | Description | Command | +| ----- | ----------- | ------- | +| `h`, `Left` | Move left | `move_char_left` | +| `j`, `Down` | Move down | `move_char_right` | +| `k`, `Up` | Move up | `move_line_up` | +| `l`, `Right` | Move right | `move_line_down` | +| `w` | Move next word start | `move_next_word_start` | +| `b` | Move previous word start | `move_prev_word_start` | +| `e` | Move next word end | `move_next_word_end` | +| `W` | Move next WORD start | `move_next_long_word_start` | +| `B` | Move previous WORD start | `move_prev_long_word_start` | +| `E` | Move next WORD end | `move_next_long_word_end` | +| `t` | Find 'till next char | `find_till_char` | +| `f` | Find next char | `find_next_char` | +| `T` | Find 'till previous char | `till_prev_char` | +| `F` | Find previous char | `find_prev_char` | +| `Home` | Move to the start of the line | `goto_line_start` | +| `End` | Move to the end of the line | `goto_line_end` | +| `PageUp` | Move page up | `page_up` | +| `PageDown` | Move page down | `page_down` | +| `Ctrl-u` | Move half page up | `half_page_up` | +| `Ctrl-d` | Move half page down | `half_page_down` | +| `Ctrl-i` | Jump forward on the jumplist | `jump_forward` | +| `Ctrl-o` | Jump backward on the jumplist | `jump_backward` | +| `v` | Enter [select (extend) mode](#select--extend-mode) | `select_mode` | +| `g` | Enter [goto mode](#goto-mode) | N/A | +| `m` | Enter [match mode](#match-mode) | N/A | +| `:` | Enter command mode | `command_mode` | +| `z` | Enter [view mode](#view-mode) | N/A | +| `Z` | Enter sticky [view mode](#view-mode) | N/A | +| `Ctrl-w` | Enter [window mode](#window-mode) | N/A | +| `Space` | Enter [space mode](#space-mode) | N/A | +| `K` | Show documentation for the item under the cursor | `hover` | ### Changes @@ -120,7 +121,10 @@ These sub-modes are accessible from normal mode and typically switch back to nor #### View mode View mode is intended for scrolling and manipulating the view without changing -the selection. +the selection. The "sticky" variant of this mode is persistent; use the Escape +key to return to normal mode after usage (useful when you're simply looking +over text and not actively editing it). + | Key | Description | Command | | ----- | ----------- | ------- | @@ -130,6 +134,10 @@ the selection. | `m` | Align the line to the middle of the screen (horizontally) | `align_view_middle` | | `j` | Scroll the view downwards | `scroll_down` | | `k` | Scroll the view upwards | `scroll_up` | +| `f` | Move page down | `page_down` | +| `b` | Move page up | `page_up` | +| `d` | Move half page down | `half_page_down` | +| `u` | Move half page up | `half_page_up` | #### Goto mode diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 1b9d87b5..f38c8a40 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -563,6 +563,22 @@ impl Default for Keymaps { "m" => align_view_middle, "k" => scroll_up, "j" => scroll_down, + "b" => page_up, + "f" => page_down, + "u" => half_page_up, + "d" => half_page_down, + }, + "Z" => { "View" sticky=true + "z" | "c" => align_view_center, + "t" => align_view_top, + "b" => align_view_bottom, + "m" => align_view_middle, + "k" => scroll_up, + "j" => scroll_down, + "b" => page_up, + "f" => page_down, + "u" => half_page_up, + "d" => half_page_down, }, "\"" => select_register, -- cgit v1.2.3-70-g09d2 From 987d8e6dd66d65c2503cc81a3b9ea8787435839a Mon Sep 17 00:00:00 2001 From: Kirawi Date: Fri, 10 Sep 2021 11:12:26 -0400 Subject: Convert clipboard line ending to document line ending when pasting (#716) * convert a paste's line-ending to the current doc's line-ending * move paste regex into paste_impl--- helix-term/src/commands.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) (limited to 'helix-term/src') diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index a7a71576..f9ebb801 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -3462,7 +3462,14 @@ fn paste_impl( .iter() .any(|value| get_line_ending_of_str(value).is_some()); - let mut values = values.iter().cloned().map(Tendril::from).chain(repeat); + // Only compiled once. + #[allow(clippy::trivial_regex)] + static REGEX: Lazy = Lazy::new(|| Regex::new(r"\r\n|\r|\n").unwrap()); + let mut values = values + .iter() + .map(|value| REGEX.replace_all(value, doc.line_ending.as_str())) + .map(|value| Tendril::from(value.as_ref())) + .chain(repeat); let text = doc.text(); let selection = doc.selection(view.id); -- cgit v1.2.3-70-g09d2 From 05c2a72ccb7f79e8e581d2703816c74543d1995c Mon Sep 17 00:00:00 2001 From: Kangwook Lee (이강욱) Date: Sat, 11 Sep 2021 18:31:40 +0900 Subject: goto line start/end commands extend when in select mode (#739) --- helix-term/src/commands.rs | 51 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 39 insertions(+), 12 deletions(-) (limited to 'helix-term/src') diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index f9ebb801..fb885740 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -410,8 +410,7 @@ fn extend_line_down(cx: &mut Context) { move_impl(cx, move_vertically, Direction::Forward, Movement::Extend) } -fn goto_line_end_impl(cx: &mut Context, movement: Movement) { - let (view, doc) = current!(cx.editor); +fn goto_line_end_impl(view: &mut View, doc: &mut Document, movement: Movement) { let text = doc.text().slice(..); let selection = doc.selection(view.id).clone().transform(|range| { @@ -427,15 +426,24 @@ fn goto_line_end_impl(cx: &mut Context, movement: Movement) { } fn goto_line_end(cx: &mut Context) { - goto_line_end_impl(cx, Movement::Move) + let (view, doc) = current!(cx.editor); + goto_line_end_impl( + view, + doc, + if doc.mode == Mode::Select { + Movement::Extend + } else { + Movement::Move + }, + ) } fn extend_to_line_end(cx: &mut Context) { - goto_line_end_impl(cx, Movement::Extend) + let (view, doc) = current!(cx.editor); + goto_line_end_impl(view, doc, Movement::Extend) } -fn goto_line_end_newline_impl(cx: &mut Context, movement: Movement) { - let (view, doc) = current!(cx.editor); +fn goto_line_end_newline_impl(view: &mut View, doc: &mut Document, movement: Movement) { let text = doc.text().slice(..); let selection = doc.selection(view.id).clone().transform(|range| { @@ -448,15 +456,24 @@ fn goto_line_end_newline_impl(cx: &mut Context, movement: Movement) { } fn goto_line_end_newline(cx: &mut Context) { - goto_line_end_newline_impl(cx, Movement::Move) + let (view, doc) = current!(cx.editor); + goto_line_end_newline_impl( + view, + doc, + if doc.mode == Mode::Select { + Movement::Extend + } else { + Movement::Move + }, + ) } fn extend_to_line_end_newline(cx: &mut Context) { - goto_line_end_newline_impl(cx, Movement::Extend) + let (view, doc) = current!(cx.editor); + goto_line_end_newline_impl(view, doc, Movement::Extend) } -fn goto_line_start_impl(cx: &mut Context, movement: Movement) { - let (view, doc) = current!(cx.editor); +fn goto_line_start_impl(view: &mut View, doc: &mut Document, movement: Movement) { let text = doc.text().slice(..); let selection = doc.selection(view.id).clone().transform(|range| { @@ -470,11 +487,21 @@ fn goto_line_start_impl(cx: &mut Context, movement: Movement) { } fn goto_line_start(cx: &mut Context) { - goto_line_start_impl(cx, Movement::Move) + let (view, doc) = current!(cx.editor); + goto_line_start_impl( + view, + doc, + if doc.mode == Mode::Select { + Movement::Extend + } else { + Movement::Move + }, + ) } fn extend_to_line_start(cx: &mut Context) { - goto_line_start_impl(cx, Movement::Extend) + let (view, doc) = current!(cx.editor); + goto_line_start_impl(view, doc, Movement::Extend) } fn goto_first_nonwhitespace(cx: &mut Context) { -- cgit v1.2.3-70-g09d2 From 32977ed34124a99af7b51057a6723203ce23c59c Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Thu, 9 Sep 2021 12:35:14 +0900 Subject: ui: Trigger recalculate_size per popup render so contents can readjust --- helix-term/src/ui/menu.rs | 76 ++++++++++++++++++++++++++-------------------- helix-term/src/ui/popup.rs | 21 ++++++++----- helix-term/src/ui/text.rs | 20 +++++++++--- 3 files changed, 72 insertions(+), 45 deletions(-) (limited to 'helix-term/src') diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs index 24dd3e61..dab0c34f 100644 --- a/helix-term/src/ui/menu.rs +++ b/helix-term/src/ui/menu.rs @@ -33,6 +33,8 @@ pub struct Menu { scroll: usize, size: (u16, u16), + viewport: (u16, u16), + recalculate: bool, } impl Menu { @@ -51,6 +53,8 @@ impl Menu { callback_fn: Box::new(callback_fn), scroll: 0, size: (0, 0), + viewport: (0, 0), + recalculate: true, }; // TODO: scoring on empty input should just use a fastpath @@ -83,6 +87,7 @@ impl Menu { // reset cursor position self.cursor = None; self.scroll = 0; + self.recalculate = true; } pub fn move_up(&mut self) { @@ -99,6 +104,41 @@ impl Menu { self.adjust_scroll(); } + fn recalculate_size(&mut self, viewport: (u16, u16)) { + let n = self + .options + .first() + .map(|option| option.row().cells.len()) + .unwrap_or_default(); + let max_lens = self.options.iter().fold(vec![0; n], |mut acc, option| { + let row = option.row(); + // maintain max for each column + for (acc, cell) in acc.iter_mut().zip(row.cells.iter()) { + let width = cell.content.width(); + if width > *acc { + *acc = width; + } + } + + acc + }); + let len = max_lens.iter().sum::() + n + 1; // +1: reserve some space for scrollbar + let width = len.min(viewport.0 as usize); + + self.widths = max_lens + .into_iter() + .map(|len| Constraint::Length(len as u16)) + .collect(); + + let height = self.matches.len().min(10).min(viewport.1 as usize); + + self.size = (width as u16, height as u16); + + // adjust scroll offsets if size changed + self.adjust_scroll(); + self.recalculate = false; + } + fn adjust_scroll(&mut self) { let win_height = self.size.1 as usize; if let Some(cursor) = self.cursor { @@ -221,43 +261,13 @@ impl Component for Menu { } fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> { - let n = self - .options - .first() - .map(|option| option.row().cells.len()) - .unwrap_or_default(); - let max_lens = self.options.iter().fold(vec![0; n], |mut acc, option| { - let row = option.row(); - // maintain max for each column - for (acc, cell) in acc.iter_mut().zip(row.cells.iter()) { - let width = cell.content.width(); - if width > *acc { - *acc = width; - } - } - - acc - }); - let len = max_lens.iter().sum::() + n + 1; // +1: reserve some space for scrollbar - let width = len.min(viewport.0 as usize); - - self.widths = max_lens - .into_iter() - .map(|len| Constraint::Length(len as u16)) - .collect(); - - let height = self.options.len().min(10).min(viewport.1 as usize); - - self.size = (width as u16, height as u16); - - // adjust scroll offsets if size changed - self.adjust_scroll(); + if viewport != self.viewport || self.recalculate { + self.recalculate_size(viewport); + } Some(self.size) } - // TODO: required size should re-trigger when we filter items so we can draw a smaller menu - fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { let theme = &cx.editor.theme; let style = theme diff --git a/helix-term/src/ui/popup.rs b/helix-term/src/ui/popup.rs index 846cefb8..1bab1eae 100644 --- a/helix-term/src/ui/popup.rs +++ b/helix-term/src/ui/popup.rs @@ -16,8 +16,6 @@ pub struct Popup { } impl Popup { - // TODO: it's like a slimmed down picker, share code? (picker = menu + prompt with different - // rendering) pub fn new(contents: T) -> Self { Self { contents, @@ -38,20 +36,26 @@ impl Popup { let (width, height) = self.size; + // if there's a orientation preference, use that + // if we're on the top part of the screen, do below + // if we're on the bottom part, do above + // -- make sure frame doesn't stick out of bounds let mut rel_x = position.col as u16; - let rel_y = position.row as u16; + let mut rel_y = position.row as u16; if viewport.width <= rel_x + width { rel_x = rel_x.saturating_sub((rel_x + width).saturating_sub(viewport.width)); - }; + } // TODO: be able to specify orientation preference. We want above for most popups, below // for menus/autocomplete. - if height <= rel_y { - (rel_x, rel_y.saturating_sub(height)) // position above point + if viewport.height > rel_y + height { + rel_y += 1 // position below point } else { - (rel_x, rel_y + 1) // position below point + rel_y = rel_y.saturating_sub(height) // position above point } + + (rel_x, rel_y) } pub fn get_size(&self) -> (u16, u16) { @@ -133,6 +137,9 @@ impl Component for Popup { } fn render(&mut self, viewport: Rect, surface: &mut Surface, cx: &mut Context) { + // trigger required_size so we recalculate if the child changed + self.required_size((viewport.width, viewport.height)); + cx.scroll = Some(self.scroll); let (rel_x, rel_y) = self.get_rel_position(viewport, cx); diff --git a/helix-term/src/ui/text.rs b/helix-term/src/ui/text.rs index 65a75a4a..4641fae1 100644 --- a/helix-term/src/ui/text.rs +++ b/helix-term/src/ui/text.rs @@ -5,11 +5,17 @@ use helix_view::graphics::Rect; pub struct Text { contents: String, + size: (u16, u16), + viewport: (u16, u16), } impl Text { pub fn new(contents: String) -> Self { - Self { contents } + Self { + contents, + size: (0, 0), + viewport: (0, 0), + } } } impl Component for Text { @@ -24,9 +30,13 @@ impl Component for Text { } fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> { - let contents = tui::text::Text::from(self.contents.clone()); - let width = std::cmp::min(contents.width() as u16, viewport.0); - let height = std::cmp::min(contents.height() as u16, viewport.1); - Some((width, height)) + if viewport != self.viewport { + let contents = tui::text::Text::from(self.contents.clone()); + let width = std::cmp::min(contents.width() as u16, viewport.0); + let height = std::cmp::min(contents.height() as u16, viewport.1); + self.size = (width, height); + self.viewport = viewport; + } + Some(self.size) } } -- cgit v1.2.3-70-g09d2 From 1540b37f3455326f9b0052f137f9e565f936dc12 Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Fri, 10 Sep 2021 18:24:34 +0900 Subject: lsp: Silence window/logMessage if -v isn't used --- 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 e21c5504..6206e6f2 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -373,7 +373,7 @@ impl Application { log::warn!("unhandled window/showMessage: {:?}", params); } Notification::LogMessage(params) => { - log::warn!("unhandled window/logMessage: {:?}", params); + log::info!("window/logMessage: {:?}", params); } Notification::ProgressMessage(params) => { let lsp::ProgressParams { token, value } = params; -- cgit v1.2.3-70-g09d2 From 3e12b0099342be12db1db64e36ca4ff29613f122 Mon Sep 17 00:00:00 2001 From: Omnikar Date: Mon, 13 Sep 2021 04:48:12 -0400 Subject: Add `no_op` command (#743) * Add `no_op` command * Document `no_op` in `remapping.md`--- book/src/remapping.md | 2 ++ helix-term/src/commands.rs | 3 +++ 2 files changed, 5 insertions(+) (limited to 'helix-term/src') diff --git a/book/src/remapping.md b/book/src/remapping.md index 3f25e364..81f45da3 100644 --- a/book/src/remapping.md +++ b/book/src/remapping.md @@ -49,4 +49,6 @@ Control, Shift and Alt modifiers are encoded respectively with the prefixes | Null | `"null"` | | Escape | `"esc"` | +Keys can be disabled by binding them to the `no_op` command. + Commands can be found in the source code at [`helix-term/src/commands.rs`](https://github.com/helix-editor/helix/blob/master/helix-term/src/commands.rs) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index fb885740..c5409494 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -162,6 +162,7 @@ impl Command { #[rustfmt::skip] commands!( + no_op, "Do nothing", move_char_left, "Move left", move_char_right, "Move right", move_line_up, "Move up", @@ -361,6 +362,8 @@ impl PartialEq for Command { } } +fn no_op(_cx: &mut Context) {} + fn move_impl(cx: &mut Context, move_fn: F, dir: Direction, behaviour: Movement) where F: Fn(RopeSlice, Range, Direction, usize, Movement) -> Range, -- cgit v1.2.3-70-g09d2 From dd0b15e1f1540d6ca8c58594be302c66005d755c Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Thu, 16 Sep 2021 15:47:51 +0900 Subject: syntax: Properly handle injection-regex for language injections --- helix-core/src/syntax.rs | 36 +++++++++++++++++++++++++++++++++++- helix-term/src/ui/editor.rs | 3 +-- helix-term/src/ui/markdown.rs | 2 +- 3 files changed, 37 insertions(+), 4 deletions(-) (limited to 'helix-term/src') diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index 93da869b..8bbf363f 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -21,6 +21,15 @@ use std::{ use once_cell::sync::{Lazy, OnceCell}; use serde::{Deserialize, Serialize}; +fn deserialize_regex<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + Option::::deserialize(deserializer)? + .map(|buf| Regex::new(&buf).map_err(serde::de::Error::custom)) + .transpose() +} + #[derive(Debug, Serialize, Deserialize)] pub struct Configuration { pub language: Vec, @@ -42,7 +51,8 @@ pub struct LanguageConfiguration { pub auto_format: bool, // content_regex - // injection_regex + #[serde(default, skip_serializing, deserialize_with = "deserialize_regex")] + injection_regex: Option, // first_line_regex // #[serde(skip)] @@ -243,6 +253,30 @@ impl Loader { .cloned() } + pub fn language_configuration_for_injection_string( + &self, + string: &str, + ) -> Option> { + let mut best_match_length = 0; + let mut best_match_position = None; + for (i, configuration) in self.language_configs.iter().enumerate() { + if let Some(injection_regex) = &configuration.injection_regex { + if let Some(mat) = injection_regex.find(string) { + let length = mat.end() - mat.start(); + if length > best_match_length { + best_match_position = Some(i); + best_match_length = length; + } + } + } + } + + if let Some(i) = best_match_position { + let configuration = &self.language_configs[i]; + return Some(configuration.clone()); + } + None + } pub fn language_configs_iter(&self) -> impl Iterator> { self.language_configs.iter() } diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 52cf3d2b..0605e2c7 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -147,8 +147,7 @@ impl EditorView { let scopes = theme.scopes(); syntax .highlight_iter(text.slice(..), Some(range), None, |language| { - loader - .language_config_for_scope(&format!("source.{}", language)) + loader.language_configuration_for_injection_string(language) .and_then(|language_config| { let config = language_config.highlight_config(scopes)?; let config_ref = config.as_ref(); diff --git a/helix-term/src/ui/markdown.rs b/helix-term/src/ui/markdown.rs index 87b35a2d..4144ed3c 100644 --- a/helix-term/src/ui/markdown.rs +++ b/helix-term/src/ui/markdown.rs @@ -88,7 +88,7 @@ fn parse<'a>( if let Some(theme) = theme { let rope = Rope::from(text.as_ref()); let syntax = loader - .language_config_for_scope(&format!("source.{}", language)) + .language_configuration_for_injection_string(language) .and_then(|config| config.highlight_config(theme.scopes())) .map(|config| Syntax::new(&rope, config)); -- cgit v1.2.3-70-g09d2 From b02d872938395566c82658c54a22449b2c968beb Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Fri, 17 Sep 2021 14:31:56 +0900 Subject: fix: Refactor apply_workspace_edit to remove assert Fixes #698 --- helix-term/src/commands.rs | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) (limited to 'helix-term/src') diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index c5409494..010a6986 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2445,7 +2445,7 @@ fn apply_workspace_edit( ) { if let Some(ref changes) = workspace_edit.changes { log::debug!("workspace changes: {:?}", changes); - editor.set_error(String::from("Handling workspace changesis not implemented yet, see https://github.com/helix-editor/helix/issues/183")); + editor.set_error(String::from("Handling workspace_edit.changes is not implemented yet, see https://github.com/helix-editor/helix/issues/183")); return; // Not sure if it works properly, it'll be safer to just panic here to avoid breaking some parts of code on which code actions will be used // TODO: find some example that uses workspace changes, and test it @@ -2463,8 +2463,30 @@ fn apply_workspace_edit( match document_changes { lsp::DocumentChanges::Edits(document_edits) => { for document_edit in document_edits { - let (view, doc) = current!(editor); - assert_eq!(doc.url().unwrap(), document_edit.text_document.uri); + let path = document_edit + .text_document + .uri + .to_file_path() + .expect("unable to convert URI to filepath"); + let current_view_id = view!(editor).id; + let doc = editor + .document_by_path_mut(path) + .expect("Document for document_changes not found"); + + // Need to determine a view for apply/append_changes_to_history + let selections = doc.selections(); + let view_id = if selections.contains_key(¤t_view_id) { + // use current if possible + current_view_id + } else { + // Hack: we take the first available view_id + selections + .keys() + .next() + .copied() + .expect("No view_id available") + }; + let edits = document_edit .edits .iter() @@ -2482,8 +2504,8 @@ fn apply_workspace_edit( edits, offset_encoding, ); - doc.apply(&transaction, view.id); - doc.append_changes_to_history(view.id); + doc.apply(&transaction, view_id); + doc.append_changes_to_history(view_id); } } lsp::DocumentChanges::Operations(operations) => { -- cgit v1.2.3-70-g09d2 From c7d6e4461f249108189cddf44b487b4c71c5520a Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Fri, 17 Sep 2021 14:34:59 +0900 Subject: fix: Wrap around the top of the picker menu when scrolling Forgot to port the improvements in menu.rs Fixes #734 --- helix-term/src/ui/picker.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) (limited to 'helix-term/src') diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index e040e0ff..c5b90a9c 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -270,17 +270,15 @@ impl Picker { } pub fn move_up(&mut self) { - self.cursor = self.cursor.saturating_sub(1); + let len = self.matches.len(); + let pos = ((self.cursor + len.saturating_sub(1)) % len) % len; + self.cursor = pos; } pub fn move_down(&mut self) { - if self.matches.is_empty() { - return; - } - - if self.cursor < self.matches.len() - 1 { - self.cursor += 1; - } + let len = self.matches.len(); + let pos = (self.cursor + 1) % len; + self.cursor = pos; } pub fn selection(&self) -> Option<&T> { -- cgit v1.2.3-70-g09d2 From 3ff5b001ac721606b68a594958abeee8832a023e Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Fri, 17 Sep 2021 14:42:14 +0900 Subject: fix: Don't allow closing the last split if there's unsaved changes Fixes #674 --- helix-term/src/commands.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'helix-term/src') diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 010a6986..d2a838ba 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1568,7 +1568,7 @@ mod cmd { /// Results an error if there are modified buffers remaining and sets editor error, /// otherwise returns `Ok(())` - fn buffers_remaining_impl(editor: &mut Editor) -> anyhow::Result<()> { + pub(super) fn buffers_remaining_impl(editor: &mut Editor) -> anyhow::Result<()> { let modified: Vec<_> = editor .documents() .filter(|doc| doc.is_modified()) @@ -4157,6 +4157,12 @@ fn vsplit(cx: &mut Context) { } fn wclose(cx: &mut Context) { + if cx.editor.tree.views().count() == 1 { + if let Err(err) = cmd::buffers_remaining_impl(cx.editor) { + cx.editor.set_error(err.to_string()); + return; + } + } let view_id = view!(cx.editor).id; // close current split cx.editor.close(view_id, /* close_buffer */ false); -- cgit v1.2.3-70-g09d2 From 1d04e5938daf178dbbcdb2249f42f8485047d4cf Mon Sep 17 00:00:00 2001 From: Leoi Hung Kin Date: Fri, 17 Sep 2021 16:22:17 +0800 Subject: search_next_impl: don't panic on invalid regex (#740) --- helix-term/src/commands.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) (limited to 'helix-term/src') diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index d2a838ba..703b92d1 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1154,8 +1154,15 @@ fn search_next_impl(cx: &mut Context, extend: bool) { if let Some(query) = registers.read('/') { let query = query.last().unwrap(); let contents = doc.text().slice(..).to_string(); - let regex = Regex::new(query).unwrap(); - search_impl(doc, view, &contents, ®ex, extend); + if let Ok(regex) = Regex::new(query) { + search_impl(doc, view, &contents, ®ex, extend); + } else { + // get around warning `mutable_borrow_reservation_conflict` + // which will be a hard error in the future + // see: https://github.com/rust-lang/rust/issues/59159 + let query = query.clone(); + cx.editor.set_error(format!("Invalid regex: {}", query)); + } } } -- cgit v1.2.3-70-g09d2 From 4a003782a51a94259ef3b5ddfacb2a148c5056e7 Mon Sep 17 00:00:00 2001 From: kraem Date: Mon, 20 Sep 2021 06:45:07 +0200 Subject: enable smart case regex search by default (#761) --- TODO.md | 2 -- book/src/configuration.md | 1 + book/src/keymap.md | 3 +-- helix-term/src/commands.rs | 12 ++++++++++-- helix-term/src/ui/mod.rs | 12 +++++++++++- helix-view/src/editor.rs | 3 +++ 6 files changed, 26 insertions(+), 7 deletions(-) (limited to 'helix-term/src') diff --git a/TODO.md b/TODO.md index d81cf302..90e7e450 100644 --- a/TODO.md +++ b/TODO.md @@ -22,8 +22,6 @@ as you type completion! - [ ] lsp: signature help -- [ ] search: smart case by default: insensitive unless upper detected - 2 - [ ] macro recording - [ ] extend selection (treesitter select parent node) (replaces viw, vi(, va( etc ) diff --git a/book/src/configuration.md b/book/src/configuration.md index 5a28362d..90cdd8a7 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -17,6 +17,7 @@ To override global configuration parameters, create a `config.toml` file located | `scroll-lines` | Number of lines to scroll per scroll wheel step. | `3` | | `shell` | Shell to use when running external commands. | Unix: `["sh", "-c"]`
Windows: `["cmd", "/C"]` | | `line-number` | Line number display (`absolute`, `relative`) | `absolute` | +| `smart-case` | Enable smart case regex searching (case insensitive unless pattern contains upper case characters) | `true` | ## LSP diff --git a/book/src/keymap.md b/book/src/keymap.md index 4fa5033d..16d2420d 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -104,8 +104,7 @@ ### Search -> TODO: The search implementation isn't ideal yet -- we don't support searching -in reverse, or searching via smartcase. +> TODO: The search implementation isn't ideal yet -- we don't support searching in reverse. | Key | Description | Command | | ----- | ----------- | ------- | diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 703b92d1..d40bb9cf 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -5,7 +5,7 @@ use helix_core::{ match_brackets, movement::{self, Direction}, object, pos_at_coords, - regex::{self, Regex}, + regex::{self, Regex, RegexBuilder}, register::Register, search, selection, surround, textobject, LineEnding, Position, Range, Rope, RopeGraphemes, RopeSlice, Selection, SmallVec, Tendril, Transaction, @@ -1154,7 +1154,15 @@ fn search_next_impl(cx: &mut Context, extend: bool) { if let Some(query) = registers.read('/') { let query = query.last().unwrap(); let contents = doc.text().slice(..).to_string(); - if let Ok(regex) = Regex::new(query) { + let case_insensitive = if cx.editor.config.smart_case { + !query.chars().any(char::is_uppercase) + } else { + false + }; + if let Ok(regex) = RegexBuilder::new(query) + .case_insensitive(case_insensitive) + .build() + { search_impl(doc, view, &contents, ®ex, extend); } else { // get around warning `mutable_borrow_reservation_conflict` diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 07eef352..f6536eb2 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -20,6 +20,7 @@ pub use spinner::{ProgressSpinners, Spinner}; pub use text::Text; use helix_core::regex::Regex; +use helix_core::regex::RegexBuilder; use helix_view::{Document, Editor, View}; use std::path::PathBuf; @@ -53,7 +54,16 @@ pub fn regex_prompt( return; } - match Regex::new(input) { + let case_insensitive = if cx.editor.config.smart_case { + !input.chars().any(char::is_uppercase) + } else { + false + }; + + match RegexBuilder::new(input) + .case_insensitive(case_insensitive) + .build() + { Ok(regex) => { let (view, doc) = current!(cx.editor); diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index a3d0d032..b7df4a9b 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -39,6 +39,8 @@ pub struct Config { pub line_number: LineNumber, /// Middle click paste support. Defaults to true pub middle_click_paste: bool, + /// Smart case: Case insensitive searching unless pattern contains upper case characters. Defaults to true. + pub smart_case: bool, } #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] @@ -64,6 +66,7 @@ impl Default for Config { }, line_number: LineNumber::Absolute, middle_click_paste: true, + smart_case: true, } } } -- cgit v1.2.3-70-g09d2 From 9456d5c1a258e71bbb7e391dec8c3efb819e2d7d Mon Sep 17 00:00:00 2001 From: Leoi Hung Kin Date: Wed, 22 Sep 2021 00:03:12 +0800 Subject: Initial implementation of global search (#651) * initial implementation of global search * use tokio::sync::mpsc::unbounded_channel instead of Arc, Mutex, Waker poll_fn * use tokio_stream::wrappers::UnboundedReceiverStream to collect all search matches * regex_prompt: unified callback; refactor * global search doc--- Cargo.lock | 74 ++++++++++++++++++ book/src/keymap.md | 4 +- helix-term/Cargo.toml | 5 ++ helix-term/src/commands.rs | 188 +++++++++++++++++++++++++++++++++++++++------ helix-term/src/keymap.rs | 1 + helix-term/src/ui/mod.rs | 12 ++- 6 files changed, 259 insertions(+), 25 deletions(-) (limited to 'helix-term/src') diff --git a/Cargo.lock b/Cargo.lock index 858c374b..2f586cb1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -41,9 +41,17 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90682c8d613ad3373e66de8c6411e0ae2ab2571e879d2efbf73558cc66f21279" dependencies = [ + "lazy_static", "memchr", + "regex-automata", ] +[[package]] +name = "bytecount" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72feb31ffc86498dacdbd0fcebb56138e7177a8cc5cea4516031d15ae85a742e" + [[package]] name = "bytes" version = "1.0.1" @@ -174,6 +182,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "encoding_rs_io" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cc3c5651fb62ab8aa3103998dade57efdd028544bd300516baa31840c252a83" +dependencies = [ + "encoding_rs", +] + [[package]] name = "error-code" version = "2.3.0" @@ -300,6 +317,45 @@ dependencies = [ "regex", ] +[[package]] +name = "grep-matcher" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d27563c33062cd33003b166ade2bb4fd82db1fd6a86db764dfdad132d46c1cc" +dependencies = [ + "memchr", +] + +[[package]] +name = "grep-regex" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121553c9768c363839b92fc2d7cdbbad44a3b70e8d6e7b1b72b05c977527bd06" +dependencies = [ + "aho-corasick", + "bstr", + "grep-matcher", + "log", + "regex", + "regex-syntax", + "thread_local", +] + +[[package]] +name = "grep-searcher" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fbdbde90ba52adc240d2deef7b6ad1f99f53142d074b771fe9b7bede6c4c23d" +dependencies = [ + "bstr", + "bytecount", + "encoding_rs", + "encoding_rs_io", + "grep-matcher", + "log", + "memmap2", +] + [[package]] name = "helix-core" version = "0.4.1" @@ -361,6 +417,8 @@ dependencies = [ "fern", "futures-util", "fuzzy-matcher", + "grep-regex", + "grep-searcher", "helix-core", "helix-lsp", "helix-tui", @@ -375,6 +433,7 @@ dependencies = [ "signal-hook", "signal-hook-tokio", "tokio", + "tokio-stream", "toml", ] @@ -552,6 +611,15 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" +[[package]] +name = "memmap2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b6c2ebff6180198788f5db08d7ce3bc1d0b617176678831a7510825973e357" +dependencies = [ + "libc", +] + [[package]] name = "mio" version = "0.7.13" @@ -753,6 +821,12 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" + [[package]] name = "regex-syntax" version = "0.6.25" diff --git a/book/src/keymap.md b/book/src/keymap.md index 16d2420d..5928a1ae 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -206,8 +206,10 @@ This layer is a kludge of mappings, mostly pickers. | `y` | Join and yank selections to clipboard | `yank_joined_to_clipboard` | | `Y` | Yank main selection to clipboard | `yank_main_selection_to_clipboard` | | `R` | Replace selections by clipboard contents | `replace_selections_with_clipboard` | +| `/` | Global search in workspace folder | `global_search` | - +> NOTE: Global search display results in a fuzzy picker, use `space + '` to bring it back up after opening a file. + #### Unimpaired Mappings in the style of [vim-unimpaired](https://github.com/tpope/vim-unimpaired). diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index 57d592cc..fe4da96e 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -55,5 +55,10 @@ toml = "0.5" serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } +# ripgrep for global search +grep-regex = "0.1.9" +grep-searcher = "0.1.8" +tokio-stream = "0.1.7" + [target.'cfg(not(windows))'.dependencies] # https://github.com/vorner/signal-hook/issues/100 signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] } diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index d40bb9cf..5005962f 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -31,7 +31,7 @@ use crate::{ }; use crate::job::{self, Job, Jobs}; -use futures_util::FutureExt; +use futures_util::{FutureExt, StreamExt}; use std::num::NonZeroUsize; use std::{fmt, future::Future}; @@ -43,6 +43,11 @@ use std::{ use once_cell::sync::Lazy; use serde::de::{self, Deserialize, Deserializer}; +use grep_regex::RegexMatcher; +use grep_searcher::{sinks, BinaryDetection, SearcherBuilder}; +use ignore::{DirEntry, WalkBuilder, WalkState}; +use tokio_stream::wrappers::UnboundedReceiverStream; + pub struct Context<'a> { pub register: Option, pub count: Option, @@ -209,6 +214,7 @@ impl Command { search_next, "Select next search match", extend_search_next, "Add next 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", extend_to_line_bounds, "Extend selection to line bounds (line-wise selection)", delete_selection, "Delete selection", @@ -1061,24 +1067,41 @@ fn select_all(cx: &mut Context) { fn select_regex(cx: &mut Context) { let reg = cx.register.unwrap_or('/'); - let prompt = ui::regex_prompt(cx, "select:".into(), Some(reg), move |view, doc, regex| { - let text = doc.text().slice(..); - if let Some(selection) = selection::select_on_matches(text, doc.selection(view.id), ®ex) - { - doc.set_selection(view.id, selection); - } - }); + let prompt = ui::regex_prompt( + cx, + "select:".into(), + Some(reg), + move |view, doc, regex, event| { + if event != PromptEvent::Update { + return; + } + let text = doc.text().slice(..); + if let Some(selection) = + selection::select_on_matches(text, doc.selection(view.id), ®ex) + { + doc.set_selection(view.id, selection); + } + }, + ); cx.push_layer(Box::new(prompt)); } fn split_selection(cx: &mut Context) { let reg = cx.register.unwrap_or('/'); - let prompt = ui::regex_prompt(cx, "split:".into(), Some(reg), move |view, doc, regex| { - let text = doc.text().slice(..); - let selection = selection::split_on_matches(text, doc.selection(view.id), ®ex); - doc.set_selection(view.id, selection); - }); + let prompt = ui::regex_prompt( + cx, + "split:".into(), + Some(reg), + move |view, doc, regex, event| { + if event != PromptEvent::Update { + return; + } + let text = doc.text().slice(..); + let selection = selection::split_on_matches(text, doc.selection(view.id), ®ex); + doc.set_selection(view.id, selection); + }, + ); cx.push_layer(Box::new(prompt)); } @@ -1141,9 +1164,17 @@ fn search(cx: &mut Context) { // feed chunks into the regex yet let contents = doc.text().slice(..).to_string(); - let prompt = ui::regex_prompt(cx, "search:".into(), Some(reg), move |view, doc, regex| { - search_impl(doc, view, &contents, ®ex, false); - }); + let prompt = ui::regex_prompt( + cx, + "search:".into(), + Some(reg), + move |view, doc, regex, event| { + if event != PromptEvent::Update { + return; + } + search_impl(doc, view, &contents, ®ex, false); + }, + ); cx.push_layer(Box::new(prompt)); } @@ -1192,6 +1223,111 @@ fn search_selection(cx: &mut Context) { cx.editor.set_status(msg); } +fn global_search(cx: &mut Context) { + let (all_matches_sx, all_matches_rx) = + tokio::sync::mpsc::unbounded_channel::<(usize, PathBuf)>(); + let prompt = ui::regex_prompt( + cx, + "global search:".into(), + None, + move |_view, _doc, regex, event| { + if event != PromptEvent::Validate { + return; + } + if let Ok(matcher) = RegexMatcher::new_line_matcher(regex.as_str()) { + let searcher = SearcherBuilder::new() + .binary_detection(BinaryDetection::quit(b'\x00')) + .build(); + + let search_root = std::env::current_dir() + .expect("Global search error: Failed to get current dir"); + WalkBuilder::new(search_root).build_parallel().run(|| { + let mut searcher_cl = searcher.clone(); + let matcher_cl = matcher.clone(); + let all_matches_sx_cl = all_matches_sx.clone(); + Box::new(move |dent: Result| -> WalkState { + let dent = match dent { + Ok(dent) => dent, + Err(_) => return WalkState::Continue, + }; + + match dent.file_type() { + Some(fi) => { + if !fi.is_file() { + return WalkState::Continue; + } + } + None => return WalkState::Continue, + } + + let result_sink = sinks::UTF8(|line_num, _| { + match all_matches_sx_cl + .send((line_num as usize - 1, dent.path().to_path_buf())) + { + Ok(_) => Ok(true), + Err(_) => Ok(false), + } + }); + let result = searcher_cl.search_path(&matcher_cl, dent.path(), result_sink); + + if let Err(err) = result { + log::error!("Global search error: {}, {}", dent.path().display(), err); + } + WalkState::Continue + }) + }); + } else { + // Otherwise do nothing + // log::warn!("Global Search Invalid Pattern") + } + }, + ); + + cx.push_layer(Box::new(prompt)); + + let show_picker = async move { + let all_matches: Vec<(usize, PathBuf)> = + UnboundedReceiverStream::new(all_matches_rx).collect().await; + let call: job::Callback = + Box::new(move |editor: &mut Editor, compositor: &mut Compositor| { + if all_matches.is_empty() { + editor.set_status("No matches found".to_string()); + return; + } + let picker = FilePicker::new( + all_matches, + move |(_line_num, path)| path.to_str().unwrap().into(), + move |editor: &mut Editor, (line_num, path), action| { + match editor.open(path.into(), action) { + Ok(_) => {} + Err(e) => { + editor.set_error(format!( + "Failed to open file '{}': {}", + path.display(), + e + )); + return; + } + } + + let line_num = *line_num; + let (view, doc) = current!(editor); + let text = doc.text(); + let start = text.line_to_char(line_num); + let end = text.line_to_char((line_num + 1).min(text.len_lines())); + + doc.set_selection(view.id, Selection::single(start, end)); + align_view(doc, view, Align::Center); + }, + |_editor, (line_num, path)| Some((path.clone(), Some((*line_num, *line_num)))), + ); + compositor.push(Box::new(picker)); + }); + Ok(call) + }; + cx.jobs.callback(show_picker); +} + fn extend_line(cx: &mut Context) { let count = cx.count(); let (view, doc) = current!(cx.editor); @@ -3847,13 +3983,21 @@ fn join_selections(cx: &mut Context) { fn keep_selections(cx: &mut Context) { // keep selections matching regex let reg = cx.register.unwrap_or('/'); - let prompt = ui::regex_prompt(cx, "keep:".into(), Some(reg), move |view, doc, regex| { - let text = doc.text().slice(..); + let prompt = ui::regex_prompt( + cx, + "keep:".into(), + Some(reg), + move |view, doc, regex, event| { + if event != PromptEvent::Update { + return; + } + let text = doc.text().slice(..); - if let Some(selection) = selection::keep_matches(text, doc.selection(view.id), ®ex) { - doc.set_selection(view.id, selection); - } - }); + if let Some(selection) = selection::keep_matches(text, doc.selection(view.id), ®ex) { + doc.set_selection(view.id, selection); + } + }, + ); cx.push_layer(Box::new(prompt)); } diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index f38c8a40..f9bfcc50 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -555,6 +555,7 @@ impl Default for Keymaps { "P" => paste_clipboard_before, "R" => replace_selections_with_clipboard, "space" => keep_primary_selection, + "/" => global_search, }, "z" => { "View" "z" | "c" => align_view_center, diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index f6536eb2..810a9966 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -29,7 +29,7 @@ pub fn regex_prompt( cx: &mut crate::commands::Context, prompt: std::borrow::Cow<'static, str>, history_register: Option, - fun: impl Fn(&mut View, &mut Document, Regex) + 'static, + fun: impl Fn(&mut View, &mut Document, Regex, PromptEvent) + 'static, ) -> Prompt { let (view, doc) = current!(cx.editor); let view_id = view.id; @@ -47,6 +47,14 @@ pub fn regex_prompt( } PromptEvent::Validate => { // TODO: push_jump to store selection just before jump + + match Regex::new(input) { + Ok(regex) => { + let (view, doc) = current!(cx.editor); + fun(view, doc, regex, event); + } + Err(_err) => (), // TODO: mark command line as error + } } PromptEvent::Update => { // skip empty input, TODO: trigger default @@ -70,7 +78,7 @@ pub fn regex_prompt( // revert state to what it was before the last update doc.set_selection(view.id, snapshot.clone()); - fun(view, doc, regex); + fun(view, doc, regex, event); view.ensure_cursor_in_view(doc, cx.editor.config.scrolloff); } -- cgit v1.2.3-70-g09d2 From 432bec10eddb3f51f3a6e32aedbfd566d74cde75 Mon Sep 17 00:00:00 2001 From: Leoi Hung Kin Date: Fri, 24 Sep 2021 09:27:16 +0800 Subject: allow smart case in global search (#781) --- helix-term/src/commands.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) (limited to 'helix-term/src') diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 5005962f..ac93b5d0 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -43,7 +43,7 @@ use std::{ use once_cell::sync::Lazy; use serde::de::{self, Deserialize, Deserializer}; -use grep_regex::RegexMatcher; +use grep_regex::RegexMatcherBuilder; use grep_searcher::{sinks, BinaryDetection, SearcherBuilder}; use ignore::{DirEntry, WalkBuilder, WalkState}; use tokio_stream::wrappers::UnboundedReceiverStream; @@ -1226,6 +1226,7 @@ fn search_selection(cx: &mut Context) { 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 prompt = ui::regex_prompt( cx, "global search:".into(), @@ -1234,7 +1235,11 @@ fn global_search(cx: &mut Context) { if event != PromptEvent::Validate { return; } - if let Ok(matcher) = RegexMatcher::new_line_matcher(regex.as_str()) { + + if let Ok(matcher) = RegexMatcherBuilder::new() + .case_smart(smart_case) + .build(regex.as_str()) + { let searcher = SearcherBuilder::new() .binary_detection(BinaryDetection::quit(b'\x00')) .build(); -- cgit v1.2.3-70-g09d2 From a958d34bfbcf45c01ce0d9c0d76e681fb863fc6a Mon Sep 17 00:00:00 2001 From: lurpahi Date: Thu, 23 Sep 2021 18:28:44 -0700 Subject: Add option for automatic insertion of closing-parens/brackets/etc (#779) * Add auto-pair editor option * Document auto-pair editor option * Make cargo fmt happy * Actually make cargo fmt happy * Rename auto-pair option to auto-pairs * Inline a few constants Co-authored-by: miaomai --- book/src/configuration.md | 1 + helix-term/src/commands.rs | 11 +++++++---- helix-view/src/editor.rs | 3 +++ 3 files changed, 11 insertions(+), 4 deletions(-) (limited to 'helix-term/src') diff --git a/book/src/configuration.md b/book/src/configuration.md index 90cdd8a7..60b12bfd 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -18,6 +18,7 @@ To override global configuration parameters, create a `config.toml` file located | `shell` | Shell to use when running external commands. | Unix: `["sh", "-c"]`
Windows: `["cmd", "/C"]` | | `line-number` | Line number display (`absolute`, `relative`) | `absolute` | | `smart-case` | Enable smart case regex searching (case insensitive unless pattern contains upper case characters) | `true` | +| `auto-pairs` | Enable automatic insertion of pairs to parenthese, brackets, etc. | `true` | ## LSP diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index ac93b5d0..117ba046 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -3371,17 +3371,20 @@ pub mod insert { } use helix_core::auto_pairs; - const HOOKS: &[Hook] = &[auto_pairs::hook, insert]; - const POST_HOOKS: &[PostHook] = &[completion, signature_help]; pub fn insert_char(cx: &mut Context, c: char) { let (view, doc) = current!(cx.editor); + let hooks: &[Hook] = match cx.editor.config.auto_pairs { + true => &[auto_pairs::hook, insert], + false => &[insert], + }; + let text = doc.text(); let selection = doc.selection(view.id).clone().cursors(text.slice(..)); // run through insert hooks, stopping on the first one that returns Some(t) - for hook in HOOKS { + for hook in hooks { if let Some(transaction) = hook(text, &selection, c) { doc.apply(&transaction, view.id); break; @@ -3391,7 +3394,7 @@ 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 POST_HOOKS { + for hook in &[completion, signature_help] { hook(cx, c); } } diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index b7df4a9b..b08a2df2 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -41,6 +41,8 @@ pub struct Config { pub middle_click_paste: bool, /// Smart case: Case insensitive searching unless pattern contains upper case characters. Defaults to true. pub smart_case: bool, + /// Automatic insertion of pairs to parentheses, brackets, etc. Defaults to true. + pub auto_pairs: bool, } #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] @@ -67,6 +69,7 @@ impl Default for Config { line_number: LineNumber::Absolute, middle_click_paste: true, smart_case: true, + auto_pairs: true, } } } -- cgit v1.2.3-70-g09d2 From 9ea9e779b2eef293c14ae50d5767035c0a9544a5 Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Wed, 8 Sep 2021 16:53:10 +0900 Subject: experiment: Move keep_primary_selection to , --- book/src/keymap.md | 3 +-- helix-term/src/commands.rs | 4 ++++ helix-term/src/keymap.rs | 5 +---- 3 files changed, 6 insertions(+), 6 deletions(-) (limited to 'helix-term/src') diff --git a/book/src/keymap.md b/book/src/keymap.md index 5928a1ae..aed48d5b 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -86,6 +86,7 @@ | `Alt-s` | Split selection on newlines | `split_selection_on_newline` | | `;` | Collapse selection onto a single cursor | `collapse_selection` | | `Alt-;` | Flip selection cursor and anchor | `flip_selections` | +| `,` | Keep only the primary selection | `keep_primary_selection` | | `C` | Copy selection onto the next line | `copy_selection_on_next_line` | | `Alt-C` | Copy selection onto the previous line | `copy_selection_on_prev_line` | | `(` | Rotate main selection forward | `rotate_selections_backward` | @@ -99,7 +100,6 @@ | `J` | Join lines inside selection | `join_selections` | | `K` | Keep selections matching the regex TODO: overlapped by hover help | `keep_selections` | | `$` | Pipe each selection into shell command, keep selections where command returned 0 | `shell_keep_pipe` | -| `Space` | Keep only the primary selection TODO: overlapped by space mode | `keep_primary_selection` | | `Ctrl-c` | Comment/uncomment the selections | `toggle_comments` | ### Search @@ -200,7 +200,6 @@ This layer is a kludge of mappings, mostly pickers. | `a` | Apply code action | `code_action` | | `'` | Open last fuzzy picker | `last_picker` | | `w` | Enter [window mode](#window-mode) | N/A | -| `space` | Keep primary selection TODO: it's here because space mode replaced it | `keep_primary_selection` | | `p` | Paste system clipboard after selections | `paste_clipboard_after` | | `P` | Paste system clipboard before selections | `paste_clipboard_before` | | `y` | Join and yank selections to clipboard | `yank_joined_to_clipboard` | diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 117ba046..e3c351f6 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2825,6 +2825,10 @@ fn open_above(cx: &mut Context) { fn normal_mode(cx: &mut Context) { let (view, doc) = current!(cx.editor); + if doc.mode == Mode::Normal { + return; + } + doc.mode = Mode::Normal; doc.append_changes_to_history(view.id); diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index f9bfcc50..a83b960e 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -436,7 +436,6 @@ impl Default for Keymaps { "A" => append_to_line, "o" => open_below, "O" => open_above, - // [ ] equivalents too (add blank new line, no edit) "d" => delete_selection, // TODO: also delete without yanking @@ -500,8 +499,7 @@ impl Default for Keymaps { "K" => keep_selections, // TODO: and another method for inverse - // TODO: clashes with space mode - "space" => keep_primary_selection, + "," => keep_primary_selection, // "q" => record_macro, // "Q" => replay_macro, @@ -554,7 +552,6 @@ impl Default for Keymaps { "p" => paste_clipboard_after, "P" => paste_clipboard_before, "R" => replace_selections_with_clipboard, - "space" => keep_primary_selection, "/" => global_search, }, "z" => { "View" -- cgit v1.2.3-70-g09d2 From 75dba1f9560c6ea579e79ff074e60ba2fb87ca63 Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Wed, 8 Sep 2021 17:21:10 +0900 Subject: experiment: space+k for LSP doc, K for keep_selections --- book/src/keymap.md | 4 ++-- helix-term/src/keymap.rs | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) (limited to 'helix-term/src') diff --git a/book/src/keymap.md b/book/src/keymap.md index aed48d5b..78bac0cf 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -38,7 +38,6 @@ | `Z` | Enter sticky [view mode](#view-mode) | N/A | | `Ctrl-w` | Enter [window mode](#window-mode) | N/A | | `Space` | Enter [space mode](#space-mode) | N/A | -| `K` | Show documentation for the item under the cursor | `hover` | ### Changes @@ -98,7 +97,7 @@ | `X` | Extend selection to line bounds (line-wise selection) | `extend_to_line_bounds` | | | Expand selection to parent syntax node TODO: pick a key | `expand_selection` | | `J` | Join lines inside selection | `join_selections` | -| `K` | Keep selections matching the regex TODO: overlapped by hover help | `keep_selections` | +| `K` | Keep selections matching the regex | `keep_selections` | | `$` | Pipe each selection into shell command, keep selections where command returned 0 | `shell_keep_pipe` | | `Ctrl-c` | Comment/uncomment the selections | `toggle_comments` | @@ -194,6 +193,7 @@ This layer is a kludge of mappings, mostly pickers. | Key | Description | Command | | ----- | ----------- | ------- | +| `k` | Show documentation for the item under the cursor | `hover` | | `f` | Open file picker | `file_picker` | | `b` | Open buffer picker | `buffer_picker` | | `s` | Open symbol picker (current document) | `symbol_picker` | diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index a83b960e..4343a0b6 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -495,7 +495,6 @@ impl Default for Keymaps { "<" => unindent, "=" => format_selections, "J" => join_selections, - // TODO: conflicts hover/doc "K" => keep_selections, // TODO: and another method for inverse @@ -527,7 +526,6 @@ impl Default for Keymaps { // move under c "C-c" => toggle_comments, - "K" => hover, // z family for save/restore/combine from/to sels from register @@ -553,6 +551,7 @@ impl Default for Keymaps { "P" => paste_clipboard_before, "R" => replace_selections_with_clipboard, "/" => global_search, + "k" => hover, }, "z" => { "View" "z" | "c" => align_view_center, -- cgit v1.2.3-70-g09d2 From 2e0803c8d9ec0028c0d018be251c7c2b781247b3 Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Wed, 22 Sep 2021 00:51:49 +0900 Subject: Implement 'remove_primary_selection' as Alt-, This allows removing search matches from the selection Fixes #713 --- helix-core/src/selection.rs | 9 +++++++++ helix-term/src/commands.rs | 17 +++++++++++++++++ helix-term/src/keymap.rs | 1 + 3 files changed, 27 insertions(+) (limited to 'helix-term/src') diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs index a3ea2ed4..755ee679 100644 --- a/helix-core/src/selection.rs +++ b/helix-core/src/selection.rs @@ -360,6 +360,15 @@ impl Selection { self.normalize() } + /// Adds a new range to the selection and makes it the primary range. + pub fn remove(mut self, index: usize) -> Self { + self.ranges.remove(index); + if index < self.primary_index || self.primary_index == self.ranges.len() { + self.primary_index -= 1; + } + self + } + /// 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 e3c351f6..025639a5 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -290,6 +290,7 @@ impl Command { join_selections, "Join lines inside selection", keep_selections, "Keep selections matching regex", keep_primary_selection, "Keep primary selection", + remove_primary_selection, "Remove primary selection", completion, "Invoke completion popup", hover, "Show docs for item under cursor", toggle_comments, "Comment/uncomment selections", @@ -4016,11 +4017,27 @@ fn keep_selections(cx: &mut Context) { fn keep_primary_selection(cx: &mut Context) { let (view, doc) = current!(cx.editor); + // TODO: handle count let range = doc.selection(view.id).primary(); doc.set_selection(view.id, Selection::single(range.anchor, range.head)); } +fn remove_primary_selection(cx: &mut Context) { + let (view, doc) = current!(cx.editor); + // TODO: handle count + + let selection = doc.selection(view.id); + if selection.len() == 1 { + cx.editor.set_error("no selections remaining".to_owned()); + return; + } + let index = selection.primary_index(); + let selection = selection.clone().remove(index); + + doc.set_selection(view.id, selection); +} + fn completion(cx: &mut Context) { // trigger on trigger char, or if user calls it // (or on word char typing??) diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 4343a0b6..cd4d3a32 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -499,6 +499,7 @@ impl Default for Keymaps { // TODO: and another method for inverse "," => keep_primary_selection, + "A-," => remove_primary_selection, // "q" => record_macro, // "Q" => replay_macro, -- cgit v1.2.3-70-g09d2 From df55eaae69d0388de26448e82f9ded483fca2f44 Mon Sep 17 00:00:00 2001 From: Matt W Date: Thu, 23 Sep 2021 19:21:04 -0700 Subject: Add tilde expansion for file opening (#782) * change to helix_core's tilde expansion, from helix-core::path::expand_tilde--- 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 025639a5..26f599bd 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1524,8 +1524,11 @@ mod cmd { args: &[&str], _event: PromptEvent, ) -> anyhow::Result<()> { + use helix_core::path::expand_tilde; let path = args.get(0).context("wrong argument count")?; - let _ = cx.editor.open(path.into(), Action::Replace)?; + let _ = cx + .editor + .open(expand_tilde(Path::new(path)), Action::Replace)?; Ok(()) } -- cgit v1.2.3-70-g09d2