diff options
Diffstat (limited to 'helix-term/src/commands.rs')
-rw-r--r-- | helix-term/src/commands.rs | 630 |
1 files changed, 433 insertions, 197 deletions
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index f3761d7d..3616d6a8 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -16,8 +16,13 @@ use helix_core::{ }; use helix_view::{ - clipboard::ClipboardType, document::Mode, editor::Action, input::KeyEvent, keyboard::KeyCode, - view::View, Document, DocumentId, Editor, ViewId, + clipboard::ClipboardType, + document::Mode, + editor::{Action, Motion}, + input::KeyEvent, + keyboard::KeyCode, + view::View, + Document, DocumentId, Editor, ViewId, }; use anyhow::{anyhow, bail, Context as _}; @@ -202,6 +207,7 @@ impl Command { find_prev_char, "Move to previous occurance of char", extend_till_prev_char, "Extend till previous occurance of char", extend_prev_char, "Extend to previous occurance of char", + repeat_last_motion, "repeat last motion(extend_next_char, extend_till_char, find_next_char, find_till_char...)", replace, "Replace with new char", switch_case, "Switch (toggle) case", switch_to_uppercase, "Switch to uppercase", @@ -215,8 +221,11 @@ impl Command { split_selection, "Split selection into subselections on regex matches", split_selection_on_newline, "Split selection on newlines", search, "Search for regex pattern", + rsearch, "Reverse search for regex pattern", search_next, "Select next search match", + search_prev, "Select previous search match", extend_search_next, "Add next search match to selection", + extend_search_prev, "Add previous search match to selection", search_selection, "Use current selection as search pattern", global_search, "Global Search in workspace folder", extend_line, "Select current line, if already selected, extend to next line", @@ -260,6 +269,8 @@ impl Command { goto_prev_diag, "Goto previous diagnostic", goto_line_start, "Goto line start", goto_line_end, "Goto line end", + goto_next_buffer, "Goto next buffer", + goto_previous_buffer, "Goto previous buffer", // TODO: different description ? goto_line_end_newline, "Goto line end", goto_first_nonwhitespace, "Goto first non-blank in line", @@ -305,6 +316,10 @@ impl Command { expand_selection, "Expand selection to parent syntax node", jump_forward, "Jump forward on jumplist", jump_backward, "Jump backward on jumplist", + jump_view_right, "Jump to the split to the right", + jump_view_left, "Jump to the split to the left", + jump_view_up, "Jump to the split above", + jump_view_down, "Jump to the split below", rotate_view, "Goto next window", hsplit, "Horizontal bottom split", vsplit, "Vertical right split", @@ -528,6 +543,39 @@ fn goto_line_start(cx: &mut Context) { ) } +fn goto_next_buffer(cx: &mut Context) { + goto_buffer(cx, Direction::Forward); +} + +fn goto_previous_buffer(cx: &mut Context) { + goto_buffer(cx, Direction::Backward); +} + +fn goto_buffer(cx: &mut Context, direction: Direction) { + let current = view!(cx.editor).doc; + + let id = match direction { + Direction::Forward => { + let iter = cx.editor.documents.keys(); + let mut iter = iter.skip_while(|id| *id != ¤t); + iter.next(); // skip current item + iter.next().or_else(|| cx.editor.documents.keys().next()) + } + Direction::Backward => { + let iter = cx.editor.documents.keys(); + let mut iter = iter.rev().skip_while(|id| *id != ¤t); + iter.next(); // skip current item + iter.next() + .or_else(|| cx.editor.documents.keys().rev().next()) + } + } + .unwrap(); + + let id = *id; + + cx.editor.switch(id, Action::Replace); +} + fn extend_to_line_start(cx: &mut Context) { let (view, doc) = current!(cx.editor); goto_line_start_impl(view, doc, Movement::Extend) @@ -631,14 +679,25 @@ fn goto_file_start(cx: &mut Context) { } else { push_jump(cx.editor); let (view, doc) = current!(cx.editor); - doc.set_selection(view.id, Selection::point(0)); + let text = doc.text().slice(..); + let selection = doc + .selection(view.id) + .clone() + .transform(|range| range.put_cursor(text, 0, doc.mode == Mode::Select)); + doc.set_selection(view.id, selection); } } fn goto_file_end(cx: &mut Context) { push_jump(cx.editor); let (view, doc) = current!(cx.editor); - doc.set_selection(view.id, Selection::point(doc.text().len_chars())); + let text = doc.text().slice(..); + let pos = doc.text().len_chars(); + let selection = doc + .selection(view.id) + .clone() + .transform(|range| range.put_cursor(text, pos, doc.mode == Mode::Select)); + doc.set_selection(view.id, selection); } fn extend_word_impl<F>(cx: &mut Context, extend_fn: F) @@ -681,8 +740,7 @@ fn extend_next_long_word_end(cx: &mut Context) { extend_word_impl(cx, movement::move_next_long_word_end) } -#[inline] -fn find_char_impl<F>(cx: &mut Context, search_fn: F, inclusive: bool, extend: bool) +fn will_find_char<F>(cx: &mut Context, search_fn: F, inclusive: bool, extend: bool) where F: Fn(RopeSlice, char, usize, usize, bool) -> Option<usize> + 'static, { @@ -704,13 +762,7 @@ where // usually mix line endings. But we should fix it eventually // anyway. { - current!(cx.editor) - .1 - .line_ending - .as_str() - .chars() - .next() - .unwrap() + doc!(cx.editor).line_ending.as_str().chars().next().unwrap() } KeyEvent { @@ -720,29 +772,48 @@ where _ => return, }; - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); + find_char_impl(cx.editor, &search_fn, inclusive, extend, ch, count); + cx.editor.last_motion = Some(Motion(Box::new(move |editor: &mut Editor| { + find_char_impl(editor, &search_fn, inclusive, true, ch, 1); + }))); + }) +} - let selection = doc.selection(view.id).clone().transform(|range| { - // TODO: use `Range::cursor()` here instead. However, that works in terms of - // graphemes, whereas this function doesn't yet. So we're doing the same logic - // here, but just in terms of chars instead. - let search_start_pos = if range.anchor < range.head { - range.head - 1 - } else { - range.head - }; +// - search_fn(text, ch, search_start_pos, count, inclusive).map_or(range, |pos| { - if extend { - range.put_cursor(text, pos, true) - } else { - Range::point(range.cursor(text)).put_cursor(text, pos, true) - } - }) - }); - doc.set_selection(view.id, selection); - }) +#[inline] +fn find_char_impl<F>( + editor: &mut Editor, + search_fn: &F, + inclusive: bool, + extend: bool, + ch: char, + count: usize, +) where + F: Fn(RopeSlice, char, usize, usize, bool) -> Option<usize> + 'static, +{ + let (view, doc) = current!(editor); + let text = doc.text().slice(..); + + let selection = doc.selection(view.id).clone().transform(|range| { + // TODO: use `Range::cursor()` here instead. However, that works in terms of + // graphemes, whereas this function doesn't yet. So we're doing the same logic + // here, but just in terms of chars instead. + let search_start_pos = if range.anchor < range.head { + range.head - 1 + } else { + range.head + }; + + search_fn(text, ch, search_start_pos, count, inclusive).map_or(range, |pos| { + if extend { + range.put_cursor(text, pos, true) + } else { + Range::point(range.cursor(text)).put_cursor(text, pos, true) + } + }) + }); + doc.set_selection(view.id, selection); } fn find_next_char_impl( @@ -756,6 +827,10 @@ fn find_next_char_impl( if inclusive { search::find_nth_next(text, ch, pos, n) } else { + let n = match text.get_char(pos) { + Some(next_ch) if next_ch == ch => n + 1, + _ => n, + }; search::find_nth_next(text, ch, pos, n).map(|n| n.saturating_sub(1)) } } @@ -770,80 +845,52 @@ fn find_prev_char_impl( if inclusive { search::find_nth_prev(text, ch, pos, n) } else { + let n = match text.get_char(pos.saturating_sub(1)) { + Some(next_ch) if next_ch == ch => n + 1, + _ => n, + }; search::find_nth_prev(text, ch, pos, n).map(|n| (n + 1).min(text.len_chars())) } } fn find_till_char(cx: &mut Context) { - find_char_impl( - cx, - find_next_char_impl, - false, /* inclusive */ - false, /* extend */ - ) + will_find_char(cx, find_next_char_impl, false, false) } fn find_next_char(cx: &mut Context) { - find_char_impl( - cx, - find_next_char_impl, - true, /* inclusive */ - false, /* extend */ - ) + will_find_char(cx, find_next_char_impl, true, false) } fn extend_till_char(cx: &mut Context) { - find_char_impl( - cx, - find_next_char_impl, - false, /* inclusive */ - true, /* extend */ - ) + will_find_char(cx, find_next_char_impl, false, true) } fn extend_next_char(cx: &mut Context) { - find_char_impl( - cx, - find_next_char_impl, - true, /* inclusive */ - true, /* extend */ - ) + will_find_char(cx, find_next_char_impl, true, true) } fn till_prev_char(cx: &mut Context) { - find_char_impl( - cx, - find_prev_char_impl, - false, /* inclusive */ - false, /* extend */ - ) + will_find_char(cx, find_prev_char_impl, false, false) } fn find_prev_char(cx: &mut Context) { - find_char_impl( - cx, - find_prev_char_impl, - true, /* inclusive */ - false, /* extend */ - ) + will_find_char(cx, find_prev_char_impl, true, false) } fn extend_till_prev_char(cx: &mut Context) { - find_char_impl( - cx, - find_prev_char_impl, - false, /* inclusive */ - true, /* extend */ - ) + will_find_char(cx, find_prev_char_impl, false, true) } fn extend_prev_char(cx: &mut Context) { - find_char_impl( - cx, - find_prev_char_impl, - true, /* inclusive */ - true, /* extend */ - ) + will_find_char(cx, find_prev_char_impl, true, true) +} + +fn repeat_last_motion(cx: &mut Context) { + let last_motion = cx.editor.last_motion.take(); + if let Some(m) = &last_motion { + m.run(cx.editor); + cx.editor.last_motion = last_motion; + } } fn replace(cx: &mut Context) { @@ -1091,6 +1138,7 @@ fn select_regex(cx: &mut Context) { cx, "select:".into(), Some(reg), + |_input: &str| Vec::new(), move |view, doc, regex, event| { if event != PromptEvent::Update { return; @@ -1113,6 +1161,7 @@ fn split_selection(cx: &mut Context) { cx, "split:".into(), Some(reg), + |_input: &str| Vec::new(), move |view, doc, regex, event| { if event != PromptEvent::Update { return; @@ -1137,35 +1186,68 @@ fn split_selection_on_newline(cx: &mut Context) { doc.set_selection(view.id, selection); } -fn search_impl(doc: &mut Document, view: &mut View, contents: &str, regex: &Regex, extend: bool) { +fn search_impl( + doc: &mut Document, + view: &mut View, + contents: &str, + regex: &Regex, + movement: Movement, + direction: Direction, +) { let text = doc.text().slice(..); let selection = doc.selection(view.id); - // Get the right side of the primary block cursor. - let start = text.char_to_byte(graphemes::next_grapheme_boundary( - text, - selection.primary().cursor(text), - )); + // Get the right side of the primary block cursor for forward search, or the + //grapheme before the start of the selection for reverse search. + let start = match direction { + Direction::Forward => text.char_to_byte(graphemes::next_grapheme_boundary( + text, + selection.primary().to(), + )), + Direction::Backward => text.char_to_byte(graphemes::prev_grapheme_boundary( + text, + selection.primary().from(), + )), + }; + + //A regex::Match returns byte-positions in the str. In the case where we + //do a reverse search and wraparound to the end, we don't need to search + //the text before the current cursor position for matches, but by slicing + //it out, we need to add it back to the position of the selection. + let mut offset = 0; // use find_at to find the next match after the cursor, loop around the end // Careful, `Regex` uses `bytes` as offsets, not character indices! - let mat = regex - .find_at(contents, start) - .or_else(|| regex.find(contents)); + let mat = match direction { + Direction::Forward => regex + .find_at(contents, start) + .or_else(|| regex.find(contents)), + Direction::Backward => regex.find_iter(&contents[..start]).last().or_else(|| { + offset = start; + regex.find_iter(&contents[start..]).last() + }), + }; // TODO: message on wraparound if let Some(mat) = mat { - let start = text.byte_to_char(mat.start()); - let end = text.byte_to_char(mat.end()); + let start = text.byte_to_char(mat.start() + offset); + let end = text.byte_to_char(mat.end() + offset); if end == 0 { // skip empty matches that don't make sense return; } - let selection = if extend { - selection.clone().push(Range::new(start, end)) + // Determine range direction based on the primary range + let primary = selection.primary(); + let range = if primary.head < primary.anchor { + Range::new(end, start) } else { - Selection::single(start, end) + Range::new(start, end) + }; + + let selection = match movement { + Movement::Extend => selection.clone().push(range), + Movement::Move => selection.clone().replace(selection.primary_index(), range), }; doc.set_selection(view.id, selection); @@ -1173,8 +1255,25 @@ fn search_impl(doc: &mut Document, view: &mut View, contents: &str, regex: &Rege }; } +fn search_completions(cx: &mut Context, reg: Option<char>) -> Vec<String> { + let mut items = reg + .and_then(|reg| cx.editor.registers.get(reg)) + .map_or(Vec::new(), |reg| reg.read().iter().take(200).collect()); + items.sort_unstable(); + items.dedup(); + items.into_iter().cloned().collect() +} + // TODO: use one function for search vs extend fn search(cx: &mut Context) { + searcher(cx, Direction::Forward) +} + +fn rsearch(cx: &mut Context) { + searcher(cx, Direction::Backward) +} +// TODO: use one function for search vs extend +fn searcher(cx: &mut Context, direction: Direction) { let reg = cx.register.unwrap_or('/'); let (_, doc) = current!(cx.editor); @@ -1183,23 +1282,31 @@ fn search(cx: &mut Context) { // HAXX: sadly we can't avoid allocating a single string for the whole buffer since we can't // feed chunks into the regex yet let contents = doc.text().slice(..).to_string(); + let completions = search_completions(cx, Some(reg)); let prompt = ui::regex_prompt( cx, "search:".into(), Some(reg), + move |input: &str| { + completions + .iter() + .filter(|comp| comp.starts_with(input)) + .map(|comp| (0.., std::borrow::Cow::Owned(comp.clone()))) + .collect() + }, move |view, doc, regex, event| { if event != PromptEvent::Update { return; } - search_impl(doc, view, &contents, ®ex, false); + search_impl(doc, view, &contents, ®ex, Movement::Move, direction); }, ); cx.push_layer(Box::new(prompt)); } -fn search_next_impl(cx: &mut Context, extend: bool) { +fn search_next_or_prev_impl(cx: &mut Context, movement: Movement, direction: Direction) { let (view, doc) = current!(cx.editor); let registers = &cx.editor.registers; if let Some(query) = registers.read('/') { @@ -1214,7 +1321,7 @@ fn search_next_impl(cx: &mut Context, extend: bool) { .case_insensitive(case_insensitive) .build() { - search_impl(doc, view, &contents, ®ex, extend); + search_impl(doc, view, &contents, ®ex, movement, direction); } else { // get around warning `mutable_borrow_reservation_conflict` // which will be a hard error in the future @@ -1226,11 +1333,18 @@ fn search_next_impl(cx: &mut Context, extend: bool) { } fn search_next(cx: &mut Context) { - search_next_impl(cx, false); + search_next_or_prev_impl(cx, Movement::Move, Direction::Forward); } +fn search_prev(cx: &mut Context) { + search_next_or_prev_impl(cx, Movement::Move, Direction::Backward); +} fn extend_search_next(cx: &mut Context) { - search_next_impl(cx, true); + search_next_or_prev_impl(cx, Movement::Extend, Direction::Forward); +} + +fn extend_search_prev(cx: &mut Context) { + search_next_or_prev_impl(cx, Movement::Extend, Direction::Backward); } fn search_selection(cx: &mut Context) { @@ -1247,10 +1361,19 @@ fn global_search(cx: &mut Context) { let (all_matches_sx, all_matches_rx) = tokio::sync::mpsc::unbounded_channel::<(usize, PathBuf)>(); let smart_case = cx.editor.config.smart_case; + + let completions = search_completions(cx, None); let prompt = ui::regex_prompt( cx, "global search:".into(), None, + move |input: &str| { + completions + .iter() + .filter(|comp| comp.starts_with(input)) + .map(|comp| (0.., std::borrow::Cow::Owned(comp.clone()))) + .collect() + }, move |_view, _doc, regex, event| { if event != PromptEvent::Validate { return; @@ -1572,7 +1695,8 @@ mod cmd { let (_, doc) = current!(cx.editor); if let Some(path) = path { - doc.set_path(path.as_ref()).context("invalid filepath")?; + doc.set_path(Some(path.as_ref())) + .context("invalid filepath")?; } if doc.path().is_none() { bail!("cannot write a buffer without a filename"); @@ -1635,7 +1759,7 @@ mod cmd { // If no argument, report current indent style. if args.is_empty() { - let style = current!(cx.editor).1.indent_style; + let style = doc!(cx.editor).indent_style; cx.editor.set_status(match style { Tabs => "tabs".into(), Spaces(1) => "1 space".into(), @@ -1674,7 +1798,7 @@ mod cmd { // If no argument, report current line ending setting. if args.is_empty() { - let line_ending = current!(cx.editor).1.line_ending; + let line_ending = doc!(cx.editor).line_ending; cx.editor.set_status(match line_ending { Crlf => "crlf".into(), LF => "line feed".into(), @@ -1790,7 +1914,7 @@ mod cmd { let mut errors = String::new(); // save all documents - for (_, doc) in &mut cx.editor.documents { + for doc in &mut cx.editor.documents.values_mut() { if doc.path().is_none() { errors.push_str("cannot write a buffer without a filename\n"); continue; @@ -2085,8 +2209,7 @@ mod cmd { args: &[&str], _event: PromptEvent, ) -> anyhow::Result<()> { - let (_, doc) = current!(cx.editor); - let id = doc.id(); + let id = view!(cx.editor).doc; if let Some(path) = args.get(0) { cx.editor.open(path.into(), Action::VerticalSplit)?; @@ -2102,8 +2225,7 @@ mod cmd { args: &[&str], _event: PromptEvent, ) -> anyhow::Result<()> { - let (_, doc) = current!(cx.editor); - let id = doc.id(); + let id = view!(cx.editor).doc; if let Some(path) = args.get(0) { cx.editor.open(path.into(), Action::HorizontalSplit)?; @@ -2188,6 +2310,18 @@ mod cmd { Ok(()) } + fn tutor( + cx: &mut compositor::Context, + _args: &[&str], + _event: PromptEvent, + ) -> anyhow::Result<()> { + let path = helix_core::runtime_dir().join("tutor.txt"); + cx.editor.open(path, Action::Replace)?; + // Unset path to prevent accidentally saving to the original tutor file. + doc_mut!(cx.editor).set_path(None)?; + Ok(()) + } + pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ TypableCommand { name: "quit", @@ -2199,7 +2333,7 @@ mod cmd { TypableCommand { name: "quit!", aliases: &["q!"], - doc: "Close the current view.", + doc: "Close the current view forcefully (ignoring unsaved changes).", fun: force_quit, completer: None, }, @@ -2262,35 +2396,35 @@ mod cmd { TypableCommand { name: "write-quit", aliases: &["wq", "x"], - doc: "Writes changes to disk and closes the current view. Accepts an optional path (:wq some/path.txt)", + doc: "Write changes to disk and close the current view. Accepts an optional path (:wq some/path.txt)", fun: write_quit, completer: Some(completers::filename), }, TypableCommand { name: "write-quit!", aliases: &["wq!", "x!"], - doc: "Writes changes to disk and closes the current view forcefully. Accepts an optional path (:wq! some/path.txt)", + doc: "Write changes to disk and close the current view forcefully. Accepts an optional path (:wq! some/path.txt)", fun: force_write_quit, completer: Some(completers::filename), }, TypableCommand { name: "write-all", aliases: &["wa"], - doc: "Writes changes from all views to disk.", + doc: "Write changes from all views to disk.", fun: write_all, completer: None, }, TypableCommand { name: "write-quit-all", aliases: &["wqa", "xa"], - doc: "Writes changes from all views to disk and close all views.", + doc: "Write changes from all views to disk and close all views.", fun: write_all_quit, completer: None, }, TypableCommand { name: "write-quit-all!", aliases: &["wqa!", "xa!"], - doc: "Writes changes from all views to disk and close all views forcefully (ignoring unsaved changes).", + doc: "Write changes from all views to disk and close all views forcefully (ignoring unsaved changes).", fun: force_write_all_quit, completer: None, }, @@ -2461,7 +2595,14 @@ mod cmd { doc: "Open the file in a horizontal split.", fun: hsplit, completer: Some(completers::filename), - } + }, + TypableCommand { + name: "tutor", + aliases: &[], + doc: "Open the tutorial.", + fun: tutor, + completer: None, + }, ]; pub static COMMANDS: Lazy<HashMap<&'static str, &'static TypableCommand>> = Lazy::new(|| { @@ -2561,7 +2702,7 @@ fn buffer_picker(cx: &mut Context) { cx.editor .documents .iter() - .map(|(id, doc)| (id, doc.path().cloned())) + .map(|(id, doc)| (*id, doc.path().cloned())) .collect(), move |(id, path): &(DocumentId, Option<PathBuf>)| { let path = path.as_deref().map(helix_core::path::get_relative_path); @@ -2580,7 +2721,7 @@ fn buffer_picker(cx: &mut Context) { editor.switch(*id, Action::Replace); }, |editor, (id, path)| { - let doc = &editor.documents.get(*id)?; + let doc = &editor.documents.get(id)?; let &view_id = doc.selections().keys().next()?; let line = doc .selection(view_id) @@ -2996,8 +3137,13 @@ fn goto_line(cx: &mut Context) { doc.text().len_lines() - 1 }; let line_idx = std::cmp::min(count.get() - 1, max_line); + let text = doc.text().slice(..); let pos = doc.text().line_to_char(line_idx); - doc.set_selection(view.id, Selection::point(pos)); + let selection = doc + .selection(view.id) + .clone() + .transform(|range| range.put_cursor(text, pos, doc.mode == Mode::Select)); + doc.set_selection(view.id, selection); } } @@ -3011,8 +3157,13 @@ fn goto_last_line(cx: &mut Context) { } else { doc.text().len_lines() - 1 }; + let text = doc.text().slice(..); let pos = doc.text().line_to_char(line_idx); - doc.set_selection(view.id, Selection::point(pos)); + let selection = doc + .selection(view.id) + .clone() + .transform(|range| range.put_cursor(text, pos, doc.mode == Mode::Select)); + doc.set_selection(view.id, selection); } fn goto_last_accessed_file(cx: &mut Context) { @@ -3306,26 +3457,24 @@ fn goto_first_diag(cx: &mut Context) { let editor = &mut cx.editor; let (_, doc) = current!(editor); - let diag = if let Some(diag) = doc.diagnostics().first() { - diag.range.start - } else { - return; + let pos = match doc.diagnostics().first() { + Some(diag) => diag.range.start, + None => return, }; - goto_pos(editor, diag); + goto_pos(editor, pos); } fn goto_last_diag(cx: &mut Context) { let editor = &mut cx.editor; let (_, doc) = current!(editor); - let diag = if let Some(diag) = doc.diagnostics().last() { - diag.range.start - } else { - return; + let pos = match doc.diagnostics().last() { + Some(diag) => diag.range.start, + None => return, }; - goto_pos(editor, diag); + goto_pos(editor, pos); } fn goto_next_diag(cx: &mut Context) { @@ -3336,20 +3485,19 @@ fn goto_next_diag(cx: &mut Context) { .selection(view.id) .primary() .cursor(doc.text().slice(..)); - let diag = if let Some(diag) = doc + + let diag = doc .diagnostics() .iter() - .map(|diag| diag.range.start) - .find(|&pos| pos > cursor_pos) - { - diag - } else if let Some(diag) = doc.diagnostics().first() { - diag.range.start - } else { - return; + .find(|diag| diag.range.start > cursor_pos) + .or_else(|| doc.diagnostics().first()); + + let pos = match diag { + Some(diag) => diag.range.start, + None => return, }; - goto_pos(editor, diag); + goto_pos(editor, pos); } fn goto_prev_diag(cx: &mut Context) { @@ -3360,21 +3508,20 @@ fn goto_prev_diag(cx: &mut Context) { .selection(view.id) .primary() .cursor(doc.text().slice(..)); - let diag = if let Some(diag) = doc + + let diag = doc .diagnostics() .iter() .rev() - .map(|diag| diag.range.start) - .find(|&pos| pos < cursor_pos) - { - diag - } else if let Some(diag) = doc.diagnostics().last() { - diag.range.start - } else { - return; + .find(|diag| diag.range.start < cursor_pos) + .or_else(|| doc.diagnostics().last()); + + let pos = match diag { + Some(diag) => diag.range.start, + None => return, }; - goto_pos(editor, diag); + goto_pos(editor, pos); } fn signature_help(cx: &mut Context) { @@ -3423,7 +3570,26 @@ pub mod insert { pub type Hook = fn(&Rope, &Selection, char) -> Option<Transaction>; pub type PostHook = fn(&mut Context, char); - fn completion(cx: &mut Context, ch: char) { + // It trigger completion when idle timer reaches deadline + // Only trigger completion if the word under cursor is longer than n characters + pub fn idle_completion(cx: &mut Context) { + let (view, doc) = current!(cx.editor); + let text = doc.text().slice(..); + let cursor = doc.selection(view.id).primary().cursor(text); + + use helix_core::chars::char_is_word; + let mut iter = text.chars_at(cursor); + iter.reverse(); + for _ in 0..cx.editor.config.completion_trigger_len { + match iter.next() { + Some(c) if char_is_word(c) => {} + _ => return, + } + } + super::completion(cx); + } + + fn language_server_completion(cx: &mut Context, ch: char) { // if ch matches completion char, trigger completion let doc = doc_mut!(cx.editor); let language_server = match doc.language_server() { @@ -3433,19 +3599,14 @@ pub mod insert { let capabilities = language_server.capabilities(); - if let lsp::ServerCapabilities { - completion_provider: - Some(lsp::CompletionOptions { - trigger_characters: Some(triggers), - .. - }), + if let Some(lsp::CompletionOptions { + trigger_characters: Some(triggers), .. - } = capabilities + }) = &capabilities.completion_provider { // TODO: what if trigger is multiple chars long - let is_trigger = triggers.iter().any(|trigger| trigger.contains(ch)); - - if is_trigger { + if triggers.iter().any(|trigger| trigger.contains(ch)) { + cx.editor.clear_idle_timer(); super::completion(cx); } } @@ -3527,7 +3688,8 @@ pub mod insert { // TODO: need a post insert hook too for certain triggers (autocomplete, signature help, etc) // this could also generically look at Transaction, but it's a bit annoying to look at // Operation instead of Change. - for hook in &[completion, signature_help] { + for hook in &[language_server_completion, signature_help] { + // for hook in &[signature_help] { hook(cx, c); } } @@ -3668,13 +3830,19 @@ pub mod insert { fn undo(cx: &mut Context) { let (view, doc) = current!(cx.editor); let view_id = view.id; - doc.undo(view_id); + let success = doc.undo(view_id); + if !success { + cx.editor.set_status("Already at oldest change".to_owned()); + } } fn redo(cx: &mut Context) { let (view, doc) = current!(cx.editor); let view_id = view.id; - doc.redo(view_id); + let success = doc.redo(view_id); + if !success { + cx.editor.set_status("Already at newest change".to_owned()); + } } // Yank / Paste @@ -3735,7 +3903,7 @@ fn yank_joined_to_clipboard_impl( } fn yank_joined_to_clipboard(cx: &mut Context) { - let line_ending = current!(cx.editor).1.line_ending; + let line_ending = doc!(cx.editor).line_ending; let _ = yank_joined_to_clipboard_impl( &mut cx.editor, line_ending.as_str(), @@ -3769,7 +3937,7 @@ fn yank_main_selection_to_clipboard(cx: &mut Context) { } fn yank_joined_to_primary_clipboard(cx: &mut Context) { - let line_ending = current!(cx.editor).1.line_ending; + let line_ending = doc!(cx.editor).line_ending; let _ = yank_joined_to_clipboard_impl( &mut cx.editor, line_ending.as_str(), @@ -3882,11 +4050,21 @@ fn replace_with_yanked(cx: &mut Context) { let registers = &mut cx.editor.registers; if let Some(values) = registers.read(reg_name) { - if let Some(yank) = values.first() { + if !values.is_empty() { + let repeat = std::iter::repeat( + values + .last() + .map(|value| Tendril::from_slice(value)) + .unwrap(), + ); + let mut values = values + .iter() + .map(|value| Tendril::from_slice(value)) + .chain(repeat); let selection = doc.selection(view.id); let transaction = Transaction::change_by_selection(doc.text(), selection, |range| { if !range.is_empty() { - (range.from(), range.to(), Some(yank.as_str().into())) + (range.from(), range.to(), Some(values.next().unwrap())) } else { (range.from(), range.to(), None) } @@ -4128,6 +4306,7 @@ fn keep_selections(cx: &mut Context) { cx, "keep:".into(), Some(reg), + |_input: &str| Vec::new(), move |view, doc, regex, event| { if event != PromptEvent::Update { return; @@ -4228,6 +4407,7 @@ pub fn completion(cx: &mut Context) { iter.reverse(); let offset = iter.take_while(|ch| chars::char_is_word(*ch)).count(); let start_offset = cursor.saturating_sub(offset); + let prefix = text.slice(start_offset..cursor).to_string(); cx.callback( future, @@ -4240,7 +4420,7 @@ pub fn completion(cx: &mut Context) { return; } - let items = match response { + let mut items = match response { Some(lsp::CompletionResponse::Array(items)) => items, // TODO: do something with is_incomplete Some(lsp::CompletionResponse::List(lsp::CompletionList { @@ -4250,6 +4430,18 @@ pub fn completion(cx: &mut Context) { None => Vec::new(), }; + if !prefix.is_empty() { + items = items + .into_iter() + .filter(|item| { + item.filter_text + .as_ref() + .unwrap_or(&item.label) + .starts_with(&prefix) + }) + .collect(); + } + if items.is_empty() { // editor.set_error("No completion available".to_string()); return; @@ -4401,27 +4593,32 @@ fn rotate_selection_contents_backward(cx: &mut Context) { // tree sitter node selection fn expand_selection(cx: &mut Context) { - let (view, doc) = current!(cx.editor); + let motion = |editor: &mut Editor| { + let (view, doc) = current!(editor); - if let Some(syntax) = doc.syntax() { - let text = doc.text().slice(..); - let selection = object::expand_selection(syntax, text, doc.selection(view.id)); - doc.set_selection(view.id, selection); - } + if let Some(syntax) = doc.syntax() { + let text = doc.text().slice(..); + let selection = object::expand_selection(syntax, text, doc.selection(view.id)); + doc.set_selection(view.id, selection); + } + }; + motion(&mut cx.editor); + cx.editor.last_motion = Some(Motion(Box::new(motion))); } fn match_brackets(cx: &mut Context) { let (view, doc) = current!(cx.editor); if let Some(syntax) = doc.syntax() { - let pos = doc - .selection(view.id) - .primary() - .cursor(doc.text().slice(..)); - if let Some(pos) = match_brackets::find(syntax, doc.text(), pos) { - let selection = Selection::point(pos); - doc.set_selection(view.id, selection); - }; + let text = doc.text().slice(..); + let selection = doc.selection(view.id).clone().transform(|range| { + if let Some(pos) = match_brackets::find(syntax, doc.text(), range.anchor) { + range.put_cursor(text, pos, doc.mode == Mode::Select) + } else { + range + } + }); + doc.set_selection(view.id, selection); } } @@ -4429,7 +4626,7 @@ fn match_brackets(cx: &mut Context) { fn jump_forward(cx: &mut Context) { let count = cx.count(); - let (view, _doc) = current!(cx.editor); + let view = view_mut!(cx.editor); if let Some((id, selection)) = view.jumps.forward(count) { view.doc = *id; @@ -4463,6 +4660,22 @@ fn rotate_view(cx: &mut Context) { cx.editor.focus_next() } +fn jump_view_right(cx: &mut Context) { + cx.editor.focus_right() +} + +fn jump_view_left(cx: &mut Context) { + cx.editor.focus_left() +} + +fn jump_view_up(cx: &mut Context) { + cx.editor.focus_up() +} + +fn jump_view_down(cx: &mut Context) { + cx.editor.focus_down() +} + // split helper, clear it later fn split(cx: &mut Context, action: Action) { let (view, doc) = current!(cx.editor); @@ -4552,20 +4765,43 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) { let count = cx.count(); cx.on_next_key(move |cx, event| { if let Some(ch) = event.char() { - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); + let textobject = move |editor: &mut Editor| { + let (view, doc) = current!(editor); + let text = doc.text().slice(..); + + let textobject_treesitter = |obj_name: &str, range: Range| -> Range { + let (lang_config, syntax) = match doc.language_config().zip(doc.syntax()) { + Some(t) => t, + None => return range, + }; + textobject::textobject_treesitter( + text, + range, + objtype, + obj_name, + syntax.tree().root_node(), + lang_config, + count, + ) + }; - let selection = doc.selection(view.id).clone().transform(|range| { - match ch { - 'w' => textobject::textobject_word(text, range, objtype, count), - // TODO: cancel new ranges if inconsistent surround matches across lines - ch if !ch.is_ascii_alphanumeric() => { - textobject::textobject_surround(text, range, objtype, ch, count) + let selection = doc.selection(view.id).clone().transform(|range| { + match ch { + 'w' => textobject::textobject_word(text, range, objtype, count), + 'c' => textobject_treesitter("class", range), + 'f' => textobject_treesitter("function", range), + 'p' => textobject_treesitter("parameter", range), + // TODO: cancel new ranges if inconsistent surround matches across lines + ch if !ch.is_ascii_alphanumeric() => { + textobject::textobject_surround(text, range, objtype, ch, count) + } + _ => range, } - _ => range, - } - }); - doc.set_selection(view.id, selection); + }); + doc.set_selection(view.id, selection); + }; + textobject(&mut cx.editor); + cx.editor.last_motion = Some(Motion(Box::new(textobject))); } }) } @@ -4577,7 +4813,7 @@ fn surround_add(cx: &mut Context) { let selection = doc.selection(view.id); let (open, close) = surround::get_pair(ch); - let mut changes = Vec::new(); + let mut changes = Vec::with_capacity(selection.len() * 2); for range in selection.iter() { changes.push((range.from(), range.from(), Some(Tendril::from_char(open)))); changes.push((range.to(), range.to(), Some(Tendril::from_char(close)))); |