From 112ae5cffe8cc645623cd2cb04cbee2e69c37a09 Mon Sep 17 00:00:00 2001 From: Omnikar Date: Sun, 25 Jul 2021 22:00:58 -0400 Subject: Determine whether to use a margin of 0 or 1 when uncommenting (#476) * Implement `margin` calculation for uncommenting * Move `margin` calculation to `find_line_comment` * Fix comment bug with multiple selections on a line * Fix `find_line_comment` test for new return type * Generate a single vec of lines for comment toggle `toggle_line_comments` collects the lines covered by all selections into a `Vec`, skipping duplicates. `find_line_comment` now returns the lines to operate on, instead of returning the lines to skip. * Fix test for `find_line_comment` * Reserve length of `to_change` instead of `lines` The length of `lines` includes blank lines which will be skipped, and as such do not need space for a change reserved for them. `to_change` includes only the lines which will be changed. * Use `token.chars().count()` for token char length * Create `changes` with capacity instead of reserving * Remove unnecessary clones in `test_find_line_comment` * Add test case for 0 margin comments * Add comments explaining `find_line_comment`--- helix-core/src/comment.rs | 89 ++++++++++++++++++++++++++++++----------------- 1 file changed, 57 insertions(+), 32 deletions(-) (limited to 'helix-core/src') diff --git a/helix-core/src/comment.rs b/helix-core/src/comment.rs index 5d564055..fadd88e0 100644 --- a/helix-core/src/comment.rs +++ b/helix-core/src/comment.rs @@ -1,17 +1,27 @@ use crate::{ find_first_non_whitespace_char, Change, Rope, RopeSlice, Selection, Tendril, Transaction, }; -use core::ops::Range; use std::borrow::Cow; +/// Given text, a comment token, and a set of line indices, returns the following: +/// - Whether the given lines should be considered commented +/// - If any of the lines are uncommented, all lines are considered as such. +/// - The lines to change for toggling comments +/// - This is all provided lines excluding blanks lines. +/// - The column of the comment tokens +/// - Column of existing tokens, if the lines are commented; column to place tokens at otherwise. +/// - The margin to the right of the comment tokens +/// - Defaults to `1`. If any existing comment token is not followed by a space, changes to `0`. fn find_line_comment( token: &str, text: RopeSlice, - lines: Range, -) -> (bool, Vec, usize) { + lines: impl IntoIterator, +) -> (bool, Vec, usize, usize) { let mut commented = true; - let mut skipped = Vec::new(); + let mut to_change = Vec::new(); let mut min = usize::MAX; // minimum col for find_first_non_whitespace_char + let mut margin = 1; + let token_len = token.chars().count(); for line in lines { let line_slice = text.line(line); if let Some(pos) = find_first_non_whitespace_char(line_slice) { @@ -29,47 +39,53 @@ fn find_line_comment( // considered uncommented. commented = false; } - } else { - // blank line - skipped.push(line); + + // determine margin of 0 or 1 for uncommenting; if any comment token is not followed by a space, + // a margin of 0 is used for all lines. + if matches!(line_slice.get_char(pos + token_len), Some(c) if c != ' ') { + margin = 0; + } + + // blank lines don't get pushed. + to_change.push(line); } } - (commented, skipped, min) + (commented, to_change, min, margin) } #[must_use] pub fn toggle_line_comments(doc: &Rope, selection: &Selection, token: Option<&str>) -> Transaction { let text = doc.slice(..); - let mut changes: Vec = Vec::new(); let token = token.unwrap_or("//"); let comment = Tendril::from(format!("{} ", token)); + let mut lines: Vec = Vec::new(); + + let mut min_next_line = 0; for selection in selection { - let start = text.char_to_line(selection.from()); - let end = text.char_to_line(selection.to()); - let lines = start..end + 1; - let (commented, skipped, min) = find_line_comment(&token, text, lines.clone()); + let start = text.char_to_line(selection.from()).max(min_next_line); + let end = text.char_to_line(selection.to()) + 1; + lines.extend(start..end); + min_next_line = end + 1; + } - changes.reserve((end - start).saturating_sub(skipped.len())); + let (commented, to_change, min, margin) = find_line_comment(&token, text, lines); - for line in lines { - if skipped.contains(&line) { - continue; - } + let mut changes: Vec = Vec::with_capacity(to_change.len()); - let pos = text.line_to_char(line) + min; + for line in to_change { + let pos = text.line_to_char(line) + min; - if !commented { - // comment line - changes.push((pos, pos, Some(comment.clone()))) - } else { - // uncomment line - let margin = 1; // TODO: margin is hardcoded 1 but could easily be 0 - changes.push((pos, pos + token.len() + margin, None)) - } + if !commented { + // comment line + changes.push((pos, pos, Some(comment.clone()))); + } else { + // uncomment line + changes.push((pos, pos + token.len() + margin, None)); } } + Transaction::change(doc, changes.into_iter()) } @@ -91,23 +107,32 @@ mod test { let text = state.doc.slice(..); let res = find_line_comment("//", text, 0..3); - // (commented = true, skipped = [line 1], min = col 2) - assert_eq!(res, (false, vec![1], 2)); + // (commented = true, to_change = [line 0, line 2], min = col 2, margin = 1) + assert_eq!(res, (false, vec![0, 2], 2, 1)); // comment let transaction = toggle_line_comments(&state.doc, &state.selection, None); transaction.apply(&mut state.doc); - state.selection = state.selection.clone().map(transaction.changes()); + state.selection = state.selection.map(transaction.changes()); assert_eq!(state.doc, " // 1\n\n // 2\n // 3"); // uncomment let transaction = toggle_line_comments(&state.doc, &state.selection, None); transaction.apply(&mut state.doc); - state.selection = state.selection.clone().map(transaction.changes()); + state.selection = state.selection.map(transaction.changes()); + assert_eq!(state.doc, " 1\n\n 2\n 3"); + + // 0 margin comments + state.doc = Rope::from(" //1\n\n //2\n //3"); + // reset the selection. + state.selection = Selection::single(0, state.doc.len_chars() - 1); + + let transaction = toggle_line_comments(&state.doc, &state.selection, None); + transaction.apply(&mut state.doc); + state.selection = state.selection.map(transaction.changes()); assert_eq!(state.doc, " 1\n\n 2\n 3"); - // TODO: account for no margin after comment // TODO: account for uncommenting with uneven comment indentation } } -- cgit v1.2.3-70-g09d2 From 63e54e30a74bb0d1d782877ddbbcf95f2817d061 Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Sat, 24 Jul 2021 17:48:45 +0900 Subject: Implement in-memory prompt history Implementation is similar to kakoune: we store the entries into a register. --- helix-core/src/register.rs | 14 +++++------ helix-term/src/commands.rs | 5 ++-- helix-term/src/ui/mod.rs | 1 + helix-term/src/ui/picker.rs | 1 + helix-term/src/ui/prompt.rs | 58 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 70 insertions(+), 9 deletions(-) (limited to 'helix-core/src') diff --git a/helix-core/src/register.rs b/helix-core/src/register.rs index cc881a17..c3e6652e 100644 --- a/helix-core/src/register.rs +++ b/helix-core/src/register.rs @@ -22,13 +22,17 @@ impl Register { self.name } - pub fn read(&self) -> &Vec { + pub fn read(&self) -> &[String] { &self.values } pub fn write(&mut self, values: Vec) { self.values = values; } + + pub fn push(&mut self, value: String) { + self.values.push(value); + } } /// Currently just wraps a `HashMap` of `Register`s @@ -42,11 +46,7 @@ impl Registers { self.inner.get(&name) } - pub fn get_mut(&mut self, name: char) -> Option<&mut Register> { - self.inner.get_mut(&name) - } - - pub fn get_or_insert(&mut self, name: char) -> &mut Register { + pub fn get_mut(&mut self, name: char) -> &mut Register { self.inner .entry(name) .or_insert_with(|| Register::new(name)) @@ -57,7 +57,7 @@ impl Registers { .insert(name, Register::new_with_values(name, values)); } - pub fn read(&self, name: char) -> Option<&Vec> { + pub fn read(&self, name: char) -> Option<&[String]> { self.get(name).map(|reg| reg.read()) } } diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 06dca5d5..c51453b0 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1126,7 +1126,7 @@ fn delete_selection(cx: &mut Context) { let reg_name = cx.selected_register.name(); let (view, doc) = current!(cx.editor); let registers = &mut cx.editor.registers; - let reg = registers.get_or_insert(reg_name); + let reg = registers.get_mut(reg_name); delete_selection_impl(reg, doc, view.id); doc.append_changes_to_history(view.id); @@ -1139,7 +1139,7 @@ fn change_selection(cx: &mut Context) { let reg_name = cx.selected_register.name(); let (view, doc) = current!(cx.editor); let registers = &mut cx.editor.registers; - let reg = registers.get_or_insert(reg_name); + let reg = registers.get_mut(reg_name); delete_selection_impl(reg, doc, view.id); enter_insert_mode(doc); } @@ -1920,6 +1920,7 @@ mod cmd { fn command_mode(cx: &mut Context) { let mut prompt = Prompt::new( ":".to_owned(), + Some(':'), |input: &str| { // we use .this over split_whitespace() because we care about empty segments let parts = input.split(' ').collect::>(); diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 288d3d2e..9e71cfe7 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -36,6 +36,7 @@ pub fn regex_prompt( Prompt::new( prompt, + None, |_input: &str| Vec::new(), // this is fine because Vec::new() doesn't allocate move |cx: &mut crate::compositor::Context, input: &str, event: PromptEvent| { match event { diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 733be2fc..0b67cd9c 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -43,6 +43,7 @@ impl Picker { ) -> Self { let prompt = Prompt::new( "".to_string(), + None, |_pattern: &str| Vec::new(), |_editor: &mut Context, _pattern: &str, _event: PromptEvent| { // diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index 2df1e281..57daef3a 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -20,6 +20,8 @@ pub struct Prompt { cursor: usize, completion: Vec, selection: Option, + history_register: Option, + history_pos: Option, completion_fn: Box Vec>, callback_fn: Box, pub doc_fn: Box Option<&'static str>>, @@ -54,6 +56,7 @@ pub enum Movement { impl Prompt { pub fn new( prompt: String, + history_register: Option, mut completion_fn: impl FnMut(&str) -> Vec + 'static, callback_fn: impl FnMut(&mut Context, &str, PromptEvent) + 'static, ) -> Self { @@ -63,6 +66,8 @@ impl Prompt { cursor: 0, completion: completion_fn(""), selection: None, + history_register, + history_pos: None, completion_fn: Box::new(completion_fn), callback_fn: Box::new(callback_fn), doc_fn: Box::new(|_| None), @@ -226,6 +231,28 @@ impl Prompt { self.exit_selection(); } + pub fn change_history(&mut self, register: &[String], direction: CompletionDirection) { + if register.is_empty() { + return; + } + + let end = register.len().saturating_sub(1); + + let index = match direction { + CompletionDirection::Forward => self.history_pos.map_or(0, |i| i + 1), + CompletionDirection::Backward => { + self.history_pos.unwrap_or(register.len()).saturating_sub(1) + } + } + .min(end); + + self.line = register[index].clone(); + + self.history_pos = Some(index); + + self.move_end(); + } + pub fn change_completion_selection(&mut self, direction: CompletionDirection) { if self.completion.is_empty() { return; @@ -468,9 +495,40 @@ impl Component for Prompt { self.exit_selection(); } else { (self.callback_fn)(cx, &self.line, PromptEvent::Validate); + + if let Some(register) = self.history_register { + // store in history + let register = cx.editor.registers.get_mut(register); + register.push(self.line.clone()); + } return close_fn; } } + KeyEvent { + code: KeyCode::Char('p'), + modifiers: KeyModifiers::CONTROL, + } + | KeyEvent { + code: KeyCode::Up, .. + } => { + if let Some(register) = self.history_register { + let register = cx.editor.registers.get_mut(register); + self.change_history(register.read(), CompletionDirection::Backward); + } + } + KeyEvent { + code: KeyCode::Char('n'), + modifiers: KeyModifiers::CONTROL, + } + | KeyEvent { + code: KeyCode::Down, + .. + } => { + if let Some(register) = self.history_register { + let register = cx.editor.registers.get_mut(register); + self.change_history(register.read(), CompletionDirection::Forward); + } + } KeyEvent { code: KeyCode::Tab, .. } => self.change_completion_selection(CompletionDirection::Forward), -- cgit v1.2.3-70-g09d2 From f7c85007972b18bf57c1ed23d40f42d56fe1f470 Mon Sep 17 00:00:00 2001 From: Ivan Tham Date: Sat, 24 Jul 2021 22:37:33 +0800 Subject: Fix append newline indent Fix #492 --- helix-core/src/indent.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) (limited to 'helix-core/src') diff --git a/helix-core/src/indent.rs b/helix-core/src/indent.rs index 0ca05fb3..5ae66769 100644 --- a/helix-core/src/indent.rs +++ b/helix-core/src/indent.rs @@ -47,7 +47,7 @@ fn calculate_indentation(query: &IndentQuery, node: Option, newline: bool) // NOTE: can't use contains() on query because of comparing Vec and &str // https://doc.rust-lang.org/std/vec/struct.Vec.html#method.contains - let mut increment: i32 = 0; + let mut increment: isize = 0; let mut node = match node { Some(node) => node, @@ -93,9 +93,7 @@ fn calculate_indentation(query: &IndentQuery, node: Option, newline: bool) node = parent; } - assert!(increment >= 0); - - increment as usize + increment.max(0) as usize } #[allow(dead_code)] -- cgit v1.2.3-70-g09d2