Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-term/src/commands.rs')
| -rw-r--r-- | helix-term/src/commands.rs | 1521 |
1 files changed, 376 insertions, 1145 deletions
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 430d4430..097c3493 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1,53 +1,46 @@ pub(crate) mod dap; pub(crate) mod lsp; -pub(crate) mod syntax; pub(crate) mod typed; pub use dap::*; use futures_util::FutureExt; use helix_event::status; use helix_stdx::{ - path::{self, find_paths}, + path::expand_tilde, rope::{self, RopeSliceExt}, }; use helix_vcs::{FileChange, Hunk}; pub use lsp::*; -pub use syntax::*; -use tui::{ - text::{Span, Spans}, - widgets::Cell, -}; +use tui::text::Span; pub use typed::*; use helix_core::{ char_idx_at_visual_offset, chars::char_is_word, - command_line::{self, Args}, comment, doc_formatter::TextFormat, encoding, find_workspace, - graphemes::{self, next_grapheme_boundary}, + graphemes::{self, next_grapheme_boundary, RevRopeGraphemes}, history::UndoKind, - increment, - indent::{self, IndentStyle}, + increment, indent, + indent::IndentStyle, line_ending::{get_line_ending_of_str, line_end_char_index}, match_brackets, movement::{self, move_vertically_visual, Direction}, object, pos_at_coords, regex::{self, Regex}, search::{self, CharMatcher}, - selection, surround, - syntax::config::{BlockCommentToken, LanguageServerFeature}, + selection, shellwords, surround, + syntax::{BlockCommentToken, LanguageServerFeature}, text_annotations::{Overlay, TextAnnotations}, textobject, unicode::width::UnicodeWidthChar, - visual_offset_from_block, Deletion, LineEnding, Position, Range, Rope, RopeReader, RopeSlice, - Selection, SmallVec, Syntax, Tendril, Transaction, + visual_offset_from_block, Deletion, LineEnding, Position, Range, Rope, RopeGraphemes, + RopeReader, RopeSlice, Selection, SmallVec, Syntax, Tendril, Transaction, }; use helix_view::{ document::{FormatterError, Mode, SCRATCH_BUFFER_NAME}, editor::Action, - expansion, info::Info, input::KeyEvent, keyboard::KeyCode, @@ -62,6 +55,7 @@ use insert::*; use movement::Movement; use crate::{ + args, compositor::{self, Component, Compositor}, filter_picker_entry, job::Callback, @@ -70,7 +64,6 @@ use crate::{ use crate::job::{self, Jobs}; use std::{ - char::{ToLowercase, ToUppercase}, cmp::Ordering, collections::{HashMap, HashSet}, error::Error, @@ -94,11 +87,6 @@ use grep_searcher::{sinks, BinaryDetection, SearcherBuilder}; use ignore::{DirEntry, WalkBuilder, WalkState}; pub type OnKeyCallback = Box<dyn FnOnce(&mut Context, KeyEvent)>; -#[derive(PartialEq, Eq, Clone, Copy, Debug)] -pub enum OnKeyCallbackKind { - PseudoPending, - Fallback, -} pub struct Context<'a> { pub register: Option<char>, @@ -106,11 +94,11 @@ pub struct Context<'a> { pub editor: &'a mut Editor, pub callback: Vec<crate::compositor::Callback>, - pub on_next_key_callback: Option<(OnKeyCallback, OnKeyCallbackKind)>, + pub on_next_key_callback: Option<OnKeyCallback>, pub jobs: &'a mut Jobs, } -impl Context<'_> { +impl<'a> Context<'a> { /// Push a new component onto the compositor. pub fn push_layer(&mut self, component: Box<dyn Component>) { self.callback @@ -132,28 +120,16 @@ impl Context<'_> { &mut self, on_next_key_callback: impl FnOnce(&mut Context, KeyEvent) + 'static, ) { - self.on_next_key_callback = Some(( - Box::new(on_next_key_callback), - OnKeyCallbackKind::PseudoPending, - )); - } - - #[inline] - pub fn on_next_key_fallback( - &mut self, - on_next_key_callback: impl FnOnce(&mut Context, KeyEvent) + 'static, - ) { - self.on_next_key_callback = - Some((Box::new(on_next_key_callback), OnKeyCallbackKind::Fallback)); + self.on_next_key_callback = Some(Box::new(on_next_key_callback)); } #[inline] pub fn callback<T, F>( &mut self, - call: impl Future<Output = helix_lsp::Result<T>> + 'static + Send, + call: impl Future<Output = helix_lsp::Result<serde_json::Value>> + 'static + Send, callback: F, ) where - T: Send + 'static, + T: for<'de> serde::Deserialize<'de> + Send + 'static, F: FnOnce(&mut Editor, &mut Compositor, T) + Send + 'static, { self.jobs.callback(make_job_callback(call, callback)); @@ -179,15 +155,16 @@ impl Context<'_> { #[inline] fn make_job_callback<T, F>( - call: impl Future<Output = helix_lsp::Result<T>> + 'static + Send, + call: impl Future<Output = helix_lsp::Result<serde_json::Value>> + 'static + Send, callback: F, ) -> std::pin::Pin<Box<impl Future<Output = Result<Callback, anyhow::Error>>>> where - T: Send + 'static, + T: for<'de> serde::Deserialize<'de> + Send + 'static, F: FnOnce(&mut Editor, &mut Compositor, T) + Send + 'static, { Box::pin(async move { - let response = call.await?; + let json = call.await?; + let response = serde_json::from_value(json)?; let call: job::Callback = Callback::EditorCompositor(Box::new( move |editor: &mut Editor, compositor: &mut Compositor| { callback(editor, compositor, response) @@ -199,21 +176,14 @@ where use helix_view::{align_view, Align}; -/// MappableCommands are commands that can be bound to keys, executable in -/// normal, insert or select mode. -/// -/// There are three kinds: -/// -/// * Static: commands usually bound to keys and used for editing, movement, -/// etc., for example `move_char_left`. -/// * Typable: commands executable from command mode, prefixed with a `:`, -/// for example `:write!`. -/// * Macro: a sequence of keys to execute, for example `@miw`. +/// A MappableCommand is either a static command like "jump_view_up" or a Typable command like +/// :format. It causes a side-effect on the state (usually by creating and applying a transaction). +/// Both of these types of commands can be mapped with keybindings in the config.toml. #[derive(Clone)] pub enum MappableCommand { Typable { name: String, - args: String, + args: Vec<String>, doc: String, }, Static { @@ -221,10 +191,6 @@ pub enum MappableCommand { fun: fn(cx: &mut Context), doc: &'static str, }, - Macro { - name: String, - keys: Vec<KeyEvent>, - }, } macro_rules! static_commands { @@ -248,39 +214,19 @@ impl MappableCommand { pub fn execute(&self, cx: &mut Context) { match &self { Self::Typable { name, args, doc: _ } => { + let args: Vec<Cow<str>> = args.iter().map(Cow::from).collect(); if let Some(command) = typed::TYPABLE_COMMAND_MAP.get(name.as_str()) { let mut cx = compositor::Context { editor: cx.editor, jobs: cx.jobs, scroll: None, }; - if let Err(e) = - typed::execute_command(&mut cx, command, args, PromptEvent::Validate) - { + if let Err(e) = (command.fun)(&mut cx, &args[..], PromptEvent::Validate) { cx.editor.set_error(format!("{}", e)); } - } else { - cx.editor.set_error(format!("no such command: '{name}'")); } } Self::Static { fun, .. } => (fun)(cx), - Self::Macro { keys, .. } => { - // Protect against recursive macros. - if cx.editor.macro_replaying.contains(&'@') { - cx.editor.set_error( - "Cannot execute macro because the [@] register is already playing a macro", - ); - return; - } - cx.editor.macro_replaying.push('@'); - let keys = keys.clone(); - cx.callback.push(Box::new(move |compositor, cx| { - for key in keys.into_iter() { - compositor.handle_event(&compositor::Event::Key(key), cx); - } - cx.editor.macro_replaying.pop(); - })); - } } } @@ -288,7 +234,6 @@ impl MappableCommand { match &self { Self::Typable { name, .. } => name, Self::Static { name, .. } => name, - Self::Macro { name, .. } => name, } } @@ -296,7 +241,6 @@ impl MappableCommand { match &self { Self::Typable { doc, .. } => doc, Self::Static { doc, .. } => doc, - Self::Macro { name, .. } => name, } } @@ -325,10 +269,6 @@ impl MappableCommand { move_prev_long_word_start, "Move to start of previous long word", move_next_long_word_end, "Move to end of next long word", move_prev_long_word_end, "Move to end of previous long word", - move_next_sub_word_start, "Move to start of next sub word", - move_prev_sub_word_start, "Move to start of previous sub word", - move_next_sub_word_end, "Move to end of next sub word", - move_prev_sub_word_end, "Move to end of previous sub word", move_parent_node_end, "Move to end of the parent node", move_parent_node_start, "Move to beginning of the parent node", extend_next_word_start, "Extend to start of next word", @@ -339,10 +279,6 @@ impl MappableCommand { extend_prev_long_word_start, "Extend to start of previous long word", extend_next_long_word_end, "Extend to end of next long word", extend_prev_long_word_end, "Extend to end of prev long word", - extend_next_sub_word_start, "Extend to start of next sub word", - extend_prev_sub_word_start, "Extend to start of previous sub word", - extend_next_sub_word_end, "Extend to end of next sub word", - extend_prev_sub_word_end, "Extend to end of prev sub word", extend_parent_node_end, "Extend to end of the parent node", extend_parent_node_start, "Extend to beginning of the parent node", find_till_char, "Move till next occurrence of char", @@ -379,7 +315,6 @@ impl MappableCommand { 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", - search_selection_detect_word_boundaries, "Use current selection as the search pattern, automatically wrapping with `\\b` on word boundaries", make_search_word_bounded, "Modify current search to make it word bounded", global_search, "Global search in workspace folder", extend_line, "Select current line, if already selected, extend to another line based on the anchor", @@ -402,20 +337,13 @@ impl MappableCommand { file_picker, "Open file picker", file_picker_in_current_buffer_directory, "Open file picker at current buffer's directory", file_picker_in_current_directory, "Open file picker at current working directory", - file_explorer, "Open file explorer in workspace root", - file_explorer_in_current_buffer_directory, "Open file explorer at current buffer's directory", - file_explorer_in_current_directory, "Open file explorer at current working directory", code_action, "Perform code action", buffer_picker, "Open buffer picker", jumplist_picker, "Open jumplist picker", symbol_picker, "Open symbol picker", - syntax_symbol_picker, "Open symbol picker from syntax information", - lsp_or_syntax_symbol_picker, "Open symbol picker from LSP or syntax information", changed_file_picker, "Open changed file picker", select_references_to_symbol_under_cursor, "Select symbol references", workspace_symbol_picker, "Open workspace symbol picker", - syntax_workspace_symbol_picker, "Open workspace symbol picker from syntax information", - lsp_or_syntax_workspace_symbol_picker, "Open workspace symbol picker from LSP or syntax information", diagnostics_picker, "Open diagnostic picker", workspace_diagnostics_picker, "Open workspace diagnostic picker", last_picker, "Open last picker", @@ -434,8 +362,6 @@ impl MappableCommand { goto_implementation, "Goto implementation", goto_file_start, "Goto line number <n> else file start", goto_file_end, "Goto file end", - extend_to_file_start, "Extend to line number<n> else file start", - extend_to_file_end, "Extend to file end", goto_file, "Goto files/URLs in selections", goto_file_hsplit, "Goto files in selections (hsplit)", goto_file_vsplit, "Goto files in selections (vsplit)", @@ -448,7 +374,6 @@ impl MappableCommand { goto_last_modification, "Goto last modification", goto_line, "Goto line", goto_last_line, "Goto last line", - extend_to_last_line, "Extend to last line", goto_first_diag, "Goto first diagnostic", goto_last_diag, "Goto last diagnostic", goto_next_diag, "Goto next diagnostic", @@ -459,8 +384,6 @@ impl MappableCommand { goto_last_change, "Goto last change", goto_line_start, "Goto line start", goto_line_end, "Goto line end", - goto_column, "Goto column", - extend_to_column, "Extend to column", goto_next_buffer, "Goto next buffer", goto_previous_buffer, "Goto previous buffer", goto_line_end_newline, "Goto newline at line end", @@ -474,8 +397,6 @@ impl MappableCommand { smart_tab, "Insert tab if all cursors have all whitespace to their left; otherwise, run a separate command.", insert_tab, "Insert tab char", insert_newline, "Insert newline char", - insert_char_interactive, "Insert an interactively-chosen char", - append_char_interactive, "Append an interactively-chosen char", delete_char_backward, "Delete previous char", delete_char_forward, "Delete next char", delete_word_backward, "Delete previous word", @@ -552,7 +473,6 @@ impl MappableCommand { wonly, "Close windows except current", select_register, "Select register", insert_register, "Insert register", - copy_between_registers, "Copy between two registers", align_view_middle, "Align view middle", align_view_top, "Align view top", align_view_center, "Align view center", @@ -575,8 +495,6 @@ impl MappableCommand { goto_prev_comment, "Goto previous comment", goto_next_test, "Goto next test", goto_prev_test, "Goto previous test", - goto_next_xml_element, "Goto next (X)HTML element", - goto_prev_xml_element, "Goto previous (X)HTML element", goto_next_entry, "Goto next pairing", goto_prev_entry, "Goto previous pairing", goto_next_paragraph, "Goto next paragraph", @@ -611,10 +529,6 @@ impl MappableCommand { command_palette, "Open command palette", goto_word, "Jump to a two-character label", extend_to_word, "Extend to a two-character label", - goto_next_tabstop, "Goto next snippet placeholder", - goto_prev_tabstop, "Goto next snippet placeholder", - rotate_selections_first, "Make the first selection your primary one", - rotate_selections_last, "Make the last selection your primary one", ); } @@ -629,11 +543,6 @@ impl fmt::Debug for MappableCommand { .field(name) .field(args) .finish(), - MappableCommand::Macro { name, keys, .. } => f - .debug_tuple("MappableCommand") - .field(name) - .field(keys) - .finish(), } } } @@ -649,28 +558,21 @@ impl std::str::FromStr for MappableCommand { fn from_str(s: &str) -> Result<Self, Self::Err> { if let Some(suffix) = s.strip_prefix(':') { - let (name, args, _) = command_line::split(suffix); - ensure!(!name.is_empty(), "Expected typable command name"); + let mut typable_command = suffix.split(' ').map(|arg| arg.trim()); + let name = typable_command + .next() + .ok_or_else(|| anyhow!("Expected typable command name"))?; + let args = typable_command + .map(|s| s.to_owned()) + .collect::<Vec<String>>(); typed::TYPABLE_COMMAND_MAP .get(name) - .map(|cmd| { - let doc = if args.is_empty() { - cmd.doc.to_string() - } else { - format!(":{} {:?}", cmd.name, args) - }; - MappableCommand::Typable { - name: cmd.name.to_owned(), - doc, - args: args.to_string(), - } + .map(|cmd| MappableCommand::Typable { + name: cmd.name.to_owned(), + doc: format!(":{} {:?}", cmd.name, args), + args, }) .ok_or_else(|| anyhow!("No TypableCommand named '{}'", s)) - } else if let Some(suffix) = s.strip_prefix('@') { - helix_view::input::parse_macro(suffix).map(|keys| Self::Macro { - name: s.to_string(), - keys, - }) } else { MappableCommand::STATIC_COMMAND_LIST .iter() @@ -1130,7 +1032,6 @@ fn goto_window(cx: &mut Context, align: Align) { let count = cx.count() - 1; let config = cx.editor.config(); let (view, doc) = current!(cx.editor); - let view_offset = doc.view_offset(view.id); let height = view.inner_height(); @@ -1143,15 +1044,15 @@ fn goto_window(cx: &mut Context, align: Align) { let last_visual_line = view.last_visual_line(doc); let visual_line = match align { - Align::Top => view_offset.vertical_offset + scrolloff + count, - Align::Center => view_offset.vertical_offset + (last_visual_line / 2), + Align::Top => view.offset.vertical_offset + scrolloff + count, + Align::Center => view.offset.vertical_offset + (last_visual_line / 2), Align::Bottom => { - view_offset.vertical_offset + last_visual_line.saturating_sub(scrolloff + count) + view.offset.vertical_offset + last_visual_line.saturating_sub(scrolloff + count) } }; let visual_line = visual_line - .max(view_offset.vertical_offset + scrolloff) - .min(view_offset.vertical_offset + last_visual_line.saturating_sub(scrolloff)); + .max(view.offset.vertical_offset + scrolloff) + .min(view.offset.vertical_offset + last_visual_line.saturating_sub(scrolloff)); let pos = view .pos_at_visual_coords(doc, visual_line as u16, 0, false) @@ -1224,22 +1125,6 @@ fn move_next_long_word_end(cx: &mut Context) { move_word_impl(cx, movement::move_next_long_word_end) } -fn move_next_sub_word_start(cx: &mut Context) { - move_word_impl(cx, movement::move_next_sub_word_start) -} - -fn move_prev_sub_word_start(cx: &mut Context) { - move_word_impl(cx, movement::move_prev_sub_word_start) -} - -fn move_prev_sub_word_end(cx: &mut Context) { - move_word_impl(cx, movement::move_prev_sub_word_end) -} - -fn move_next_sub_word_end(cx: &mut Context) { - move_word_impl(cx, movement::move_next_sub_word_end) -} - fn goto_para_impl<F>(cx: &mut Context, move_fn: F) where F: Fn(RopeSlice, Range, usize, Movement) -> Range + 'static, @@ -1272,44 +1157,28 @@ fn goto_next_paragraph(cx: &mut Context) { } fn goto_file_start(cx: &mut Context) { - goto_file_start_impl(cx, Movement::Move); -} - -fn extend_to_file_start(cx: &mut Context) { - goto_file_start_impl(cx, Movement::Extend); -} - -fn goto_file_start_impl(cx: &mut Context, movement: Movement) { if cx.count.is_some() { - goto_line_impl(cx, movement); + goto_line(cx); } else { let (view, doc) = current!(cx.editor); let text = doc.text().slice(..); let selection = doc .selection(view.id) .clone() - .transform(|range| range.put_cursor(text, 0, movement == Movement::Extend)); + .transform(|range| range.put_cursor(text, 0, cx.editor.mode == Mode::Select)); push_jump(view, doc); doc.set_selection(view.id, selection); } } fn goto_file_end(cx: &mut Context) { - goto_file_end_impl(cx, Movement::Move); -} - -fn extend_to_file_end(cx: &mut Context) { - goto_file_end_impl(cx, Movement::Extend) -} - -fn goto_file_end_impl(cx: &mut Context, movement: Movement) { let (view, doc) = current!(cx.editor); 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, movement == Movement::Extend)); + .transform(|range| range.put_cursor(text, pos, cx.editor.mode == Mode::Select)); push_jump(view, doc); doc.set_selection(view.id, selection); } @@ -1329,7 +1198,7 @@ fn goto_file_vsplit(cx: &mut Context) { /// Goto files in selection. fn goto_file_impl(cx: &mut Context, action: Action) { let (view, doc) = current_ref!(cx.editor); - let text = doc.text().slice(..); + let text = doc.text(); let selections = doc.selection(view.id); let primary = selections.primary(); let rel_path = doc @@ -1338,31 +1207,53 @@ fn goto_file_impl(cx: &mut Context, action: Action) { .unwrap_or_default(); let paths: Vec<_> = if selections.len() == 1 && primary.len() == 1 { - // Cap the search at roughly 1k bytes around the cursor. - let lookaround = 1000; - let pos = text.char_to_byte(primary.cursor(text)); - let search_start = text - .line_to_byte(text.byte_to_line(pos)) - .max(text.floor_char_boundary(pos.saturating_sub(lookaround))); - let search_end = text - .line_to_byte(text.byte_to_line(pos) + 1) - .min(text.ceil_char_boundary(pos + lookaround)); - let search_range = text.byte_slice(search_start..search_end); - // we also allow paths that are next to the cursor (can be ambiguous but - // rarely so in practice) so that gf on quoted/braced path works (not sure about this - // but apparently that is how gf has worked historically in helix) - let path = find_paths(search_range, true) - .take_while(|range| search_start + range.start <= pos + 1) - .find(|range| pos <= search_start + range.end) - .map(|range| Cow::from(search_range.byte_slice(range))); - log::debug!("goto_file auto-detected path: {path:?}"); - let path = path.unwrap_or_else(|| primary.fragment(text)); - vec![path.into_owned()] + // Secial case: if there is only one one-width selection, try to detect the + // path under the cursor. + let is_valid_path_char = |c: &char| { + #[cfg(target_os = "windows")] + let valid_chars = &[ + '@', '/', '\\', '.', '-', '_', '+', '#', '$', '%', '{', '}', '[', ']', ':', '!', + '~', '=', + ]; + #[cfg(not(target_os = "windows"))] + let valid_chars = &['@', '/', '.', '-', '_', '+', '#', '$', '%', '~', '=', ':']; + + valid_chars.contains(c) || c.is_alphabetic() || c.is_numeric() + }; + + let cursor_pos = primary.cursor(text.slice(..)); + let pre_cursor_pos = cursor_pos.saturating_sub(1); + let post_cursor_pos = cursor_pos + 1; + let start_pos = if is_valid_path_char(&text.char(cursor_pos)) { + cursor_pos + } else if is_valid_path_char(&text.char(pre_cursor_pos)) { + pre_cursor_pos + } else { + post_cursor_pos + }; + + let prefix_len = text + .chars_at(start_pos) + .reversed() + .take_while(is_valid_path_char) + .count(); + + let postfix_len = text + .chars_at(start_pos) + .take_while(is_valid_path_char) + .count(); + + let path: String = text + .slice((start_pos - prefix_len)..(start_pos + postfix_len)) + .into(); + log::debug!("goto_file auto-detected path: {}", path); + + vec![path] } else { // Otherwise use each selection, trimmed. selections - .fragments(text) - .map(|sel| sel.trim().to_owned()) + .fragments(text.slice(..)) + .map(|sel| sel.trim().to_string()) .filter(|sel| !sel.is_empty()) .collect() }; @@ -1373,10 +1264,10 @@ fn goto_file_impl(cx: &mut Context, action: Action) { continue; } - let path = path::expand(&sel); + let path = expand_tilde(Cow::from(PathBuf::from(sel))); let path = &rel_path.join(path); if path.is_dir() { - let picker = ui::file_picker(cx.editor, path.into()); + let picker = ui::file_picker(path.into(), &cx.editor.config()); cx.push_layer(Box::new(overlaid(picker))); } else if let Err(e) = cx.editor.open(path, action) { cx.editor.set_error(format!("Open file failed: {:?}", e)); @@ -1413,7 +1304,7 @@ fn open_url(cx: &mut Context, url: Url, action: Action) { Ok(_) | Err(_) => { let path = &rel_path.join(url.path()); if path.is_dir() { - let picker = ui::file_picker(cx.editor, path.into()); + let picker = ui::file_picker(path.into(), &cx.editor.config()); cx.push_layer(Box::new(overlaid(picker))); } else if let Err(e) = cx.editor.open(path, action) { cx.editor.set_error(format!("Open file failed: {:?}", e)); @@ -1470,22 +1361,6 @@ fn extend_next_long_word_end(cx: &mut Context) { extend_word_impl(cx, movement::move_next_long_word_end) } -fn extend_next_sub_word_start(cx: &mut Context) { - extend_word_impl(cx, movement::move_next_sub_word_start) -} - -fn extend_prev_sub_word_start(cx: &mut Context) { - extend_word_impl(cx, movement::move_prev_sub_word_start) -} - -fn extend_prev_sub_word_end(cx: &mut Context) { - extend_word_impl(cx, movement::move_prev_sub_word_end) -} - -fn extend_next_sub_word_end(cx: &mut Context) { - extend_word_impl(cx, movement::move_next_sub_word_end) -} - /// Separate branch to find_char designed only for `<ret>` char. // // This is necessary because the one document can have different line endings inside. And we @@ -1724,12 +1599,10 @@ fn replace(cx: &mut Context) { if let Some(ch) = ch { let transaction = Transaction::change_by_selection(doc.text(), selection, |range| { if !range.is_empty() { - let text: Tendril = doc - .text() - .slice(range.from()..range.to()) - .graphemes() - .map(|_g| ch) - .collect(); + let text: Tendril = + RopeGraphemes::new(doc.text().slice(range.from()..range.to())) + .map(|_g| ch) + .collect(); (range.from(), range.to(), Some(text)) } else { // No change. @@ -1759,48 +1632,17 @@ where exit_select_mode(cx); } -enum CaseSwitcher { - Upper(ToUppercase), - Lower(ToLowercase), - Keep(Option<char>), -} - -impl Iterator for CaseSwitcher { - type Item = char; - - fn next(&mut self) -> Option<Self::Item> { - match self { - CaseSwitcher::Upper(upper) => upper.next(), - CaseSwitcher::Lower(lower) => lower.next(), - CaseSwitcher::Keep(ch) => ch.take(), - } - } - - fn size_hint(&self) -> (usize, Option<usize>) { - match self { - CaseSwitcher::Upper(upper) => upper.size_hint(), - CaseSwitcher::Lower(lower) => lower.size_hint(), - CaseSwitcher::Keep(ch) => { - let n = if ch.is_some() { 1 } else { 0 }; - (n, Some(n)) - } - } - } -} - -impl ExactSizeIterator for CaseSwitcher {} - fn switch_case(cx: &mut Context) { switch_case_impl(cx, |string| { string .chars() .flat_map(|ch| { if ch.is_lowercase() { - CaseSwitcher::Upper(ch.to_uppercase()) + ch.to_uppercase().collect() } else if ch.is_uppercase() { - CaseSwitcher::Lower(ch.to_lowercase()) + ch.to_lowercase().collect() } else { - CaseSwitcher::Keep(Some(ch)) + vec![ch] } }) .collect() @@ -1823,7 +1665,6 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction, sync_cursor use Direction::*; let config = cx.editor.config(); let (view, doc) = current!(cx.editor); - let mut view_offset = doc.view_offset(view.id); let range = doc.selection(view.id).primary(); let text = doc.text().slice(..); @@ -1840,19 +1681,15 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction, sync_cursor let doc_text = doc.text().slice(..); let viewport = view.inner_area(doc); let text_fmt = doc.text_format(viewport.width, None); - (view_offset.anchor, view_offset.vertical_offset) = char_idx_at_visual_offset( + let mut annotations = view.text_annotations(&*doc, None); + (view.offset.anchor, view.offset.vertical_offset) = char_idx_at_visual_offset( doc_text, - view_offset.anchor, - view_offset.vertical_offset as isize + offset, + view.offset.anchor, + view.offset.vertical_offset as isize + offset, 0, &text_fmt, - // &annotations, - &view.text_annotations(&*doc, None), + &annotations, ); - doc.set_view_offset(view.id, view_offset); - - let doc_text = doc.text().slice(..); - let mut annotations = view.text_annotations(&*doc, None); if sync_cursor { let movement = match cx.editor.mode { @@ -1874,21 +1711,18 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction, sync_cursor &mut annotations, ) }); - drop(annotations); doc.set_selection(view.id, selection); return; } - let view_offset = doc.view_offset(view.id); - let mut head; match direction { Forward => { let off; (head, off) = char_idx_at_visual_offset( doc_text, - view_offset.anchor, - (view_offset.vertical_offset + scrolloff) as isize, + view.offset.anchor, + (view.offset.vertical_offset + scrolloff) as isize, 0, &text_fmt, &annotations, @@ -1901,8 +1735,8 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction, sync_cursor Backward => { head = char_idx_at_visual_offset( doc_text, - view_offset.anchor, - (view_offset.vertical_offset + height - scrolloff - 1) as isize, + view.offset.anchor, + (view.offset.vertical_offset + height - scrolloff - 1) as isize, 0, &text_fmt, &annotations, @@ -2096,8 +1930,6 @@ fn select_regex(cx: &mut Context) { selection::select_on_matches(text, doc.selection(view.id), ®ex) { doc.set_selection(view.id, selection); - } else { - cx.editor.set_error("nothing selected"); } }, ); @@ -2263,7 +2095,7 @@ fn searcher(cx: &mut Context, direction: Direction) { completions .iter() .filter(|comp| comp.starts_with(input)) - .map(|comp| (0.., comp.clone().into())) + .map(|comp| (0.., std::borrow::Cow::Owned(comp.clone()))) .collect() }, move |cx, regex, event| { @@ -2342,59 +2174,14 @@ fn extend_search_prev(cx: &mut Context) { } fn search_selection(cx: &mut Context) { - search_selection_impl(cx, false) -} - -fn search_selection_detect_word_boundaries(cx: &mut Context) { - search_selection_impl(cx, true) -} - -fn search_selection_impl(cx: &mut Context, detect_word_boundaries: bool) { - fn is_at_word_start(text: RopeSlice, index: usize) -> bool { - // This can happen when the cursor is at the last character in - // the document +1 (ge + j), in this case text.char(index) will panic as - // it will index out of bounds. See https://github.com/helix-editor/helix/issues/12609 - if index == text.len_chars() { - return false; - } - let ch = text.char(index); - if index == 0 { - return char_is_word(ch); - } - let prev_ch = text.char(index - 1); - - !char_is_word(prev_ch) && char_is_word(ch) - } - - fn is_at_word_end(text: RopeSlice, index: usize) -> bool { - if index == 0 || index == text.len_chars() { - return false; - } - let ch = text.char(index); - let prev_ch = text.char(index - 1); - - char_is_word(prev_ch) && !char_is_word(ch) - } - let register = cx.register.unwrap_or('/'); let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); + let contents = doc.text().slice(..); let regex = doc .selection(view.id) .iter() - .map(|selection| { - let add_boundary_prefix = - detect_word_boundaries && is_at_word_start(text, selection.from()); - let add_boundary_suffix = - detect_word_boundaries && is_at_word_end(text, selection.to()); - - let prefix = if add_boundary_prefix { "\\b" } else { "" }; - let suffix = if add_boundary_suffix { "\\b" } else { "" }; - - let word = regex::escape(&selection.fragment(text)); - format!("{}{}{}", prefix, word, suffix) - }) + .map(|selection| regex::escape(&selection.fragment(contents))) .collect::<HashSet<_>>() // Collect into hashset to deduplicate identical regexes .into_iter() .collect::<Vec<_>>() @@ -2470,42 +2257,18 @@ fn global_search(cx: &mut Context) { struct GlobalSearchConfig { smart_case: bool, file_picker_config: helix_view::editor::FilePickerConfig, - directory_style: Style, - number_style: Style, - colon_style: Style, } let config = cx.editor.config(); let config = GlobalSearchConfig { smart_case: config.search.smart_case, file_picker_config: config.file_picker.clone(), - directory_style: cx.editor.theme.get("ui.text.directory"), - number_style: cx.editor.theme.get("constant.numeric.integer"), - colon_style: cx.editor.theme.get("punctuation"), }; let columns = [ - PickerColumn::new("path", |item: &FileResult, config: &GlobalSearchConfig| { + PickerColumn::new("path", |item: &FileResult, _| { let path = helix_stdx::path::get_relative_path(&item.path); - - let directories = path - .parent() - .filter(|p| !p.as_os_str().is_empty()) - .map(|p| format!("{}{}", p.display(), std::path::MAIN_SEPARATOR)) - .unwrap_or_default(); - - let filename = item - .path - .file_name() - .expect("global search paths are normalized (can't end in `..`)") - .to_string_lossy(); - - Cell::from(Spans::from(vec![ - Span::styled(directories, config.directory_style), - Span::raw(filename), - Span::styled(":", config.colon_style), - Span::styled((item.line_num + 1).to_string(), config.number_style), - ])) + format!("{}:{}", path.to_string_lossy(), item.line_num + 1).into() }), PickerColumn::hidden("contents"), ]; @@ -2597,7 +2360,7 @@ fn global_search(cx: &mut Context) { let doc = documents.iter().find(|&(doc_path, _)| { doc_path .as_ref() - .is_some_and(|doc_path| doc_path == entry.path()) + .map_or(false, |doc_path| doc_path == entry.path()) }); let result = if let Some((_, doc)) = doc { @@ -2605,7 +2368,7 @@ fn global_search(cx: &mut Context) { // search the buffer instead of the file because it's faster // and captures new edits without requiring a save if searcher.multi_line_with_matcher(&matcher) { - // in this case a continuous buffer is required + // in this case a continous buffer is required // convert the rope to a string let text = doc.to_string(); searcher.search_slice(&matcher, text.as_bytes(), sink) @@ -2881,9 +2644,7 @@ fn delete_selection_impl(cx: &mut Context, op: Operation, yank: YankAction) { // yank the selection let text = doc.text().slice(..); let values: Vec<String> = selection.fragments(text).map(Cow::into_owned).collect(); - let reg_name = cx - .register - .unwrap_or_else(|| cx.editor.config.load().default_yank_register); + let reg_name = cx.register.unwrap_or('"'); if let Err(err) = cx.editor.registers.write(reg_name, values) { cx.editor.set_error(err.to_string()); return; @@ -2902,7 +2663,7 @@ fn delete_selection_impl(cx: &mut Context, op: Operation, yank: YankAction) { } Operation::Change => { if only_whole_lines { - open(cx, Open::Above, CommentContinuation::Disabled); + open_above(cx); } else { enter_insert_mode(cx); } @@ -3064,7 +2825,7 @@ fn file_picker(cx: &mut Context) { cx.editor.set_error("Workspace directory does not exist"); return; } - let picker = ui::file_picker(cx.editor, root); + let picker = ui::file_picker(root, &cx.editor.config()); cx.push_layer(Box::new(overlaid(picker))); } @@ -3081,7 +2842,7 @@ fn file_picker_in_current_buffer_directory(cx: &mut Context) { } }; - let picker = ui::file_picker(cx.editor, path); + let picker = ui::file_picker(path, &cx.editor.config()); cx.push_layer(Box::new(overlaid(picker))); } @@ -3092,62 +2853,10 @@ fn file_picker_in_current_directory(cx: &mut Context) { .set_error("Current working directory does not exist"); return; } - let picker = ui::file_picker(cx.editor, cwd); + let picker = ui::file_picker(cwd, &cx.editor.config()); cx.push_layer(Box::new(overlaid(picker))); } -fn file_explorer(cx: &mut Context) { - let root = find_workspace().0; - if !root.exists() { - cx.editor.set_error("Workspace directory does not exist"); - return; - } - - if let Ok(picker) = ui::file_explorer(root, cx.editor) { - cx.push_layer(Box::new(overlaid(picker))); - } -} - -fn file_explorer_in_current_buffer_directory(cx: &mut Context) { - let doc_dir = doc!(cx.editor) - .path() - .and_then(|path| path.parent().map(|path| path.to_path_buf())); - - let path = match doc_dir { - Some(path) => path, - None => { - let cwd = helix_stdx::env::current_working_dir(); - if !cwd.exists() { - cx.editor.set_error( - "Current buffer has no parent and current working directory does not exist", - ); - return; - } - cx.editor.set_error( - "Current buffer has no parent, opening file explorer in current working directory", - ); - cwd - } - }; - - if let Ok(picker) = ui::file_explorer(path, cx.editor) { - cx.push_layer(Box::new(overlaid(picker))); - } -} - -fn file_explorer_in_current_directory(cx: &mut Context) { - let cwd = helix_stdx::env::current_working_dir(); - if !cwd.exists() { - cx.editor - .set_error("Current working directory does not exist"); - return; - } - - if let Ok(picker) = ui::file_explorer(cwd, cx.editor) { - cx.push_layer(Box::new(overlaid(picker))); - } -} - fn buffer_picker(cx: &mut Context) { let current = view!(cx.editor).doc; @@ -3201,18 +2910,17 @@ fn buffer_picker(cx: &mut Context) { .into() }), ]; - let initial_cursor = if items.len() <= 1 { 0 } else { 1 }; let picker = Picker::new(columns, 2, items, (), |cx, meta, action| { cx.editor.switch(meta.id, action); }) - .with_initial_cursor(initial_cursor) .with_preview(|editor, meta| { let doc = &editor.documents.get(&meta.id)?; - let lines = doc.selections().values().next().map(|selection| { - let cursor_line = selection.primary().cursor_line(doc.text().slice(..)); - (cursor_line, cursor_line) - }); - Some((meta.id.into(), lines)) + let &view_id = doc.selections().keys().next()?; + let line = doc + .selection(view_id) + .primary() + .cursor_line(doc.text().slice(..)); + Some((meta.id.into(), Some((line, line)))) }); cx.push_layer(Box::new(overlaid(picker))); } @@ -3417,7 +3125,7 @@ pub fn command_palette(cx: &mut Context) { .iter() .map(|cmd| MappableCommand::Typable { name: cmd.name.to_owned(), - args: String::new(), + args: Vec::new(), doc: cmd.doc.to_owned(), }), ); @@ -3426,9 +3134,6 @@ pub fn command_palette(cx: &mut Context) { ui::PickerColumn::new("name", |item, _| match item { MappableCommand::Typable { name, .. } => format!(":{name}").into(), MappableCommand::Static { name, .. } => (*name).into(), - MappableCommand::Macro { .. } => { - unreachable!("macros aren't included in the command palette") - } }), ui::PickerColumn::new( "bindings", @@ -3519,12 +3224,12 @@ fn insert_with_indent(cx: &mut Context, cursor_fallback: IndentFallbackPos) { enter_insert_mode(cx); let (view, doc) = current!(cx.editor); - let loader = cx.editor.syn_loader.load(); let text = doc.text().slice(..); let contents = doc.text(); let selection = doc.selection(view.id); + let language_config = doc.language_config(); let syntax = doc.syntax(); let tab_width = doc.tab_width(); @@ -3540,7 +3245,7 @@ fn insert_with_indent(cx: &mut Context, cursor_fallback: IndentFallbackPos) { let line_end_index = cursor_line_start; let indent = indent::indent_for_newline( - &loader, + language_config, syntax, &doc.config.load().indent_heuristic, &doc.indent_style, @@ -3602,23 +3307,14 @@ async fn make_format_callback( let doc = doc_mut!(editor, &doc_id); let view = view_mut!(editor, view_id); - match format { - Ok(format) => { - if doc.version() == doc_version { - doc.apply(&format, view.id); - doc.append_changes_to_history(view); - doc.detect_indent_and_line_ending(); - view.ensure_cursor_in_view(doc, scrolloff); - } else { - log::info!("discarded formatting changes because the document changed"); - } - } - Err(err) => { - if write.is_none() { - editor.set_error(err.to_string()); - return; - } - log::info!("failed to format '{}': {err}", doc.display_name()); + if let Ok(format) = format { + if doc.version() == doc_version { + doc.apply(&format, view.id); + doc.append_changes_to_history(view); + doc.detect_indent_and_line_ending(); + view.ensure_cursor_in_view(doc, scrolloff); + } else { + log::info!("discarded formatting changes because the document changed"); } } @@ -3639,125 +3335,74 @@ pub enum Open { Above, } -#[derive(PartialEq)] -pub enum CommentContinuation { - Enabled, - Disabled, -} - -fn open(cx: &mut Context, open: Open, comment_continuation: CommentContinuation) { +fn open(cx: &mut Context, open: Open) { let count = cx.count(); enter_insert_mode(cx); - let config = cx.editor.config(); let (view, doc) = current!(cx.editor); - let loader = cx.editor.syn_loader.load(); let text = doc.text().slice(..); let contents = doc.text(); let selection = doc.selection(view.id); - let mut offs = 0; let mut ranges = SmallVec::with_capacity(selection.len()); - - let continue_comment_tokens = - if comment_continuation == CommentContinuation::Enabled && config.continue_comments { - doc.language_config() - .and_then(|config| config.comment_tokens.as_ref()) - } else { - None - }; + let mut offs = 0; let mut transaction = Transaction::change_by_selection(contents, selection, |range| { - // the line number, where the cursor is currently - let curr_line_num = text.char_to_line(match open { + let cursor_line = text.char_to_line(match open { Open::Below => graphemes::prev_grapheme_boundary(text, range.to()), Open::Above => range.from(), }); - // the next line number, where the cursor will be, after finishing the transaction - let next_new_line_num = match open { - Open::Below => curr_line_num + 1, - Open::Above => curr_line_num, + let new_line = match open { + // adjust position to the end of the line (next line - 1) + Open::Below => cursor_line + 1, + // adjust position to the end of the previous line (current line - 1) + Open::Above => cursor_line, }; - let above_next_new_line_num = next_new_line_num.saturating_sub(1); - - let continue_comment_token = continue_comment_tokens - .and_then(|tokens| comment::get_comment_token(text, tokens, curr_line_num)); + let line_num = new_line.saturating_sub(1); // Index to insert newlines after, as well as the char width // to use to compensate for those inserted newlines. - let (above_next_line_end_index, above_next_line_end_width) = if next_new_line_num == 0 { + let (line_end_index, line_end_offset_width) = if new_line == 0 { (0, 0) } else { ( - line_end_char_index(&text, above_next_new_line_num), + line_end_char_index(&text, line_num), doc.line_ending.len_chars(), ) }; - let line = text.line(curr_line_num); - let indent = match line.first_non_whitespace_char() { - Some(pos) if continue_comment_token.is_some() => line.slice(..pos).to_string(), - _ => indent::indent_for_newline( - &loader, - doc.syntax(), - &config.indent_heuristic, - &doc.indent_style, - doc.tab_width(), - text, - above_next_new_line_num, - above_next_line_end_index, - curr_line_num, - ), - }; + let indent = indent::indent_for_newline( + doc.language_config(), + doc.syntax(), + &doc.config.load().indent_heuristic, + &doc.indent_style, + doc.tab_width(), + text, + line_num, + line_end_index, + cursor_line, + ); let indent_len = indent.len(); let mut text = String::with_capacity(1 + indent_len); - - if open == Open::Above && next_new_line_num == 0 { - text.push_str(&indent); - if let Some(token) = continue_comment_token { - text.push_str(token); - text.push(' '); - } - text.push_str(doc.line_ending.as_str()); - } else { - text.push_str(doc.line_ending.as_str()); - text.push_str(&indent); - - if let Some(token) = continue_comment_token { - text.push_str(token); - text.push(' '); - } - } - + text.push_str(doc.line_ending.as_str()); + text.push_str(&indent); let text = text.repeat(count); // calculate new selection ranges - let pos = offs + above_next_line_end_index + above_next_line_end_width; - let comment_len = continue_comment_token - .map(|token| token.len() + 1) // `+ 1` for the extra space added - .unwrap_or_default(); + let pos = offs + line_end_index + line_end_offset_width; for i in 0..count { - // pos -> beginning of reference line, - // + (i * (line_ending_len + indent_len + comment_len)) -> beginning of i'th line from pos (possibly including comment token) - // + indent_len + comment_len -> -> indent for i'th line - ranges.push(Range::point( - pos + (i * (doc.line_ending.len_chars() + indent_len + comment_len)) - + indent_len - + comment_len, - )); + // pos -> beginning of reference line, + // + (i * (1+indent_len)) -> beginning of i'th line from pos + // + indent_len -> -> indent for i'th line + ranges.push(Range::point(pos + (i * (1 + indent_len)) + indent_len)); } - // update the offset for the next range offs += text.chars().count(); - ( - above_next_line_end_index, - above_next_line_end_index, - Some(text.into()), - ) + (line_end_index, line_end_index, Some(text.into())) }); transaction = transaction.with_selection(Selection::new(ranges, selection.primary_index())); @@ -3767,12 +3412,12 @@ fn open(cx: &mut Context, open: Open, comment_continuation: CommentContinuation) // o inserts a new line after each line with a selection fn open_below(cx: &mut Context) { - open(cx, Open::Below, CommentContinuation::Enabled) + open(cx, Open::Below) } // O inserts a new line before each line with a selection fn open_above(cx: &mut Context) { - open(cx, Open::Above, CommentContinuation::Enabled) + open(cx, Open::Above) } fn normal_mode(cx: &mut Context) { @@ -3780,30 +3425,21 @@ fn normal_mode(cx: &mut Context) { } // Store a jump on the jumplist. -fn push_jump(view: &mut View, doc: &mut Document) { - doc.append_changes_to_history(view); +fn push_jump(view: &mut View, doc: &Document) { let jump = (doc.id(), doc.selection(view.id).clone()); view.jumps.push(jump); } fn goto_line(cx: &mut Context) { - goto_line_impl(cx, Movement::Move); -} - -fn goto_line_impl(cx: &mut Context, movement: Movement) { if cx.count.is_some() { let (view, doc) = current!(cx.editor); push_jump(view, doc); - goto_line_without_jumplist(cx.editor, cx.count, movement); + goto_line_without_jumplist(cx.editor, cx.count); } } -fn goto_line_without_jumplist( - editor: &mut Editor, - count: Option<NonZeroUsize>, - movement: Movement, -) { +fn goto_line_without_jumplist(editor: &mut Editor, count: Option<NonZeroUsize>) { if let Some(count) = count { let (view, doc) = current!(editor); let text = doc.text().slice(..); @@ -3818,21 +3454,13 @@ fn goto_line_without_jumplist( let selection = doc .selection(view.id) .clone() - .transform(|range| range.put_cursor(text, pos, movement == Movement::Extend)); + .transform(|range| range.put_cursor(text, pos, editor.mode == Mode::Select)); doc.set_selection(view.id, selection); } } fn goto_last_line(cx: &mut Context) { - goto_last_line_impl(cx, Movement::Move) -} - -fn extend_to_last_line(cx: &mut Context) { - goto_last_line_impl(cx, Movement::Extend) -} - -fn goto_last_line_impl(cx: &mut Context, movement: Movement) { let (view, doc) = current!(cx.editor); let text = doc.text().slice(..); let line_idx = if text.line(text.len_lines() - 1).len_chars() == 0 { @@ -3845,35 +3473,12 @@ fn goto_last_line_impl(cx: &mut Context, movement: Movement) { let selection = doc .selection(view.id) .clone() - .transform(|range| range.put_cursor(text, pos, movement == Movement::Extend)); + .transform(|range| range.put_cursor(text, pos, cx.editor.mode == Mode::Select)); push_jump(view, doc); doc.set_selection(view.id, selection); } -fn goto_column(cx: &mut Context) { - goto_column_impl(cx, Movement::Move); -} - -fn extend_to_column(cx: &mut Context) { - goto_column_impl(cx, Movement::Extend); -} - -fn goto_column_impl(cx: &mut Context, movement: Movement) { - 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 line = range.cursor_line(text); - let line_start = text.line_to_char(line); - let line_end = line_end_char_index(&text, line); - let pos = graphemes::nth_next_grapheme_boundary(text, line_start, count - 1).min(line_end); - range.put_cursor(text, pos, movement == Movement::Extend) - }); - push_jump(view, doc); - doc.set_selection(view.id, selection); -} - fn goto_last_accessed_file(cx: &mut Context) { let view = view_mut!(cx.editor); if let Some(alt) = view.docs_access_history.pop() { @@ -3892,7 +3497,6 @@ fn goto_last_modification(cx: &mut Context) { .selection(view.id) .clone() .transform(|range| range.put_cursor(text, pos, cx.editor.mode == Mode::Select)); - push_jump(view, doc); doc.set_selection(view.id, selection); } } @@ -3944,10 +3548,7 @@ fn goto_first_diag(cx: &mut Context) { Some(diag) => Selection::single(diag.range.start, diag.range.end), None => return, }; - push_jump(view, doc); doc.set_selection(view.id, selection); - view.diagnostics_handler - .immediately_show_diagnostic(doc, view.id); } fn goto_last_diag(cx: &mut Context) { @@ -3956,10 +3557,7 @@ fn goto_last_diag(cx: &mut Context) { Some(diag) => Selection::single(diag.range.start, diag.range.end), None => return, }; - push_jump(view, doc); doc.set_selection(view.id, selection); - view.diagnostics_handler - .immediately_show_diagnostic(doc, view.id); } fn goto_next_diag(cx: &mut Context) { @@ -3974,16 +3572,14 @@ fn goto_next_diag(cx: &mut Context) { let diag = doc .diagnostics() .iter() - .find(|diag| diag.range.start > cursor_pos); + .find(|diag| diag.range.start > cursor_pos) + .or_else(|| doc.diagnostics().first()); let selection = match diag { Some(diag) => Selection::single(diag.range.start, diag.range.end), None => return, }; - push_jump(view, doc); doc.set_selection(view.id, selection); - view.diagnostics_handler - .immediately_show_diagnostic(doc, view.id); }; cx.editor.apply_motion(motion); @@ -4002,7 +3598,8 @@ fn goto_prev_diag(cx: &mut Context) { .diagnostics() .iter() .rev() - .find(|diag| diag.range.start < cursor_pos); + .find(|diag| diag.range.start < cursor_pos) + .or_else(|| doc.diagnostics().last()); let selection = match diag { // NOTE: the selection is reversed because we're jumping to the @@ -4010,10 +3607,7 @@ fn goto_prev_diag(cx: &mut Context) { Some(diag) => Selection::single(diag.range.end, diag.range.start), None => return, }; - push_jump(view, doc); doc.set_selection(view.id, selection); - view.diagnostics_handler - .immediately_show_diagnostic(doc, view.id); }; cx.editor.apply_motion(motion) } @@ -4041,7 +3635,6 @@ fn goto_first_change_impl(cx: &mut Context, reverse: bool) { }; if hunk != Hunk::NONE { let range = hunk_range(hunk, doc.text().slice(..)); - push_jump(view, doc); doc.set_selection(view.id, Selection::single(range.anchor, range.head)); } } @@ -4097,7 +3690,6 @@ fn goto_next_change_impl(cx: &mut Context, direction: Direction) { } }); - push_jump(view, doc); doc.set_selection(view.id, selection) }; cx.editor.apply_motion(motion); @@ -4118,7 +3710,7 @@ fn hunk_range(hunk: Hunk, text: RopeSlice) -> Range { } pub mod insert { - use crate::{events::PostInsertChar, key}; + use crate::events::PostInsertChar; use super::*; pub type Hook = fn(&Rope, &Selection, char) -> Option<Transaction>; @@ -4184,11 +3776,7 @@ pub mod insert { }); if !cursors_after_whitespace { - if doc.active_snippet.is_some() { - goto_next_tabstop(cx); - } else { - move_parent_node_end(cx); - } + move_parent_node_end(cx); return; } } @@ -4197,15 +3785,11 @@ pub mod insert { } pub fn insert_tab(cx: &mut Context) { - insert_tab_impl(cx, 1) - } - - fn insert_tab_impl(cx: &mut Context, count: usize) { let (view, doc) = current!(cx.editor); // TODO: round out to nearest indentation level (for example a line with 3 spaces should // indent by one to reach 4 spaces). - let indent = Tendril::from(doc.indent_style.as_str().repeat(count)); + let indent = Tendril::from(doc.indent_style.as_str()); let transaction = Transaction::insert( doc.text(), &doc.selection(view.id).clone().cursors(doc.text().slice(..)), @@ -4214,75 +3798,18 @@ pub mod insert { doc.apply(&transaction, view.id); } - pub fn append_char_interactive(cx: &mut Context) { - // Save the current mode, so we can restore it later. - let mode = cx.editor.mode; - append_mode(cx); - insert_selection_interactive(cx, mode); - } - - pub fn insert_char_interactive(cx: &mut Context) { - let mode = cx.editor.mode; - insert_mode(cx); - insert_selection_interactive(cx, mode); - } - - fn insert_selection_interactive(cx: &mut Context, old_mode: Mode) { - let count = cx.count(); - - // need to wait for next key - cx.on_next_key(move |cx, event| { - match event { - KeyEvent { - code: KeyCode::Char(ch), - .. - } => { - for _ in 0..count { - insert::insert_char(cx, ch) - } - } - key!(Enter) => { - if count != 1 { - cx.editor - .set_error("inserting multiple newlines not yet supported"); - return; - } - insert_newline(cx) - } - key!(Tab) => insert_tab_impl(cx, count), - _ => (), - }; - // Restore the old mode. - cx.editor.mode = old_mode; - }); - } - pub fn insert_newline(cx: &mut Context) { - let config = cx.editor.config(); let (view, doc) = current_ref!(cx.editor); - let loader = cx.editor.syn_loader.load(); let text = doc.text().slice(..); - let line_ending = doc.line_ending.as_str(); let contents = doc.text(); - let selection = doc.selection(view.id); + let selection = doc.selection(view.id).clone(); let mut ranges = SmallVec::with_capacity(selection.len()); // TODO: this is annoying, but we need to do it to properly calculate pos after edits let mut global_offs = 0; - let mut new_text = String::new(); - let continue_comment_tokens = if config.continue_comments { - doc.language_config() - .and_then(|config| config.comment_tokens.as_ref()) - } else { - None - }; - - let mut last_pos = 0; - let mut transaction = Transaction::change_by_selection(contents, selection, |range| { - // Tracks the number of trailing whitespace characters deleted by this selection. - let mut chars_deleted = 0; + let mut transaction = Transaction::change_by_selection(contents, &selection, |range| { let pos = range.cursor(text); let prev = if pos == 0 { @@ -4293,32 +3820,33 @@ pub mod insert { let curr = contents.get_char(pos).unwrap_or(' '); let current_line = text.char_to_line(pos); - let line_start = text.line_to_char(current_line); + let line_is_only_whitespace = text + .line(current_line) + .chars() + .all(|char| char.is_ascii_whitespace()); - let continue_comment_token = continue_comment_tokens - .and_then(|tokens| comment::get_comment_token(text, tokens, current_line)); + let mut new_text = String::new(); - let (from, to, local_offs) = if let Some(idx) = - text.slice(line_start..pos).last_non_whitespace_char() - { - let first_trailing_whitespace_char = (line_start + idx + 1).clamp(last_pos, pos); - last_pos = pos; - let line = text.line(current_line); - - let indent = match line.first_non_whitespace_char() { - Some(pos) if continue_comment_token.is_some() => line.slice(..pos).to_string(), - _ => indent::indent_for_newline( - &loader, - doc.syntax(), - &config.indent_heuristic, - &doc.indent_style, - doc.tab_width(), - text, - current_line, - pos, - current_line, - ), - }; + // If the current line is all whitespace, insert a line ending at the beginning of + // the current line. This makes the current line empty and the new line contain the + // indentation of the old line. + let (from, to, local_offs) = if line_is_only_whitespace { + let line_start = text.line_to_char(current_line); + new_text.push_str(doc.line_ending.as_str()); + + (line_start, line_start, new_text.chars().count()) + } else { + let indent = indent::indent_for_newline( + doc.language_config(), + doc.syntax(), + &doc.config.load().indent_heuristic, + &doc.indent_style, + doc.tab_width(), + text, + current_line, + pos, + current_line, + ); // If we are between pairs (such as brackets), we want to // insert an additional line which is indented one level @@ -4326,66 +3854,38 @@ pub mod insert { let on_auto_pair = doc .auto_pairs(cx.editor) .and_then(|pairs| pairs.get(prev)) - .is_some_and(|pair| pair.open == prev && pair.close == curr); + .map_or(false, |pair| pair.open == prev && pair.close == curr); - let local_offs = if let Some(token) = continue_comment_token { - new_text.reserve_exact(line_ending.len() + indent.len() + token.len() + 1); - new_text.push_str(line_ending); - new_text.push_str(&indent); - new_text.push_str(token); - new_text.push(' '); - new_text.chars().count() - } else if on_auto_pair { - // line where the cursor will be + let local_offs = if on_auto_pair { let inner_indent = indent.clone() + doc.indent_style.as_str(); - new_text - .reserve_exact(line_ending.len() * 2 + indent.len() + inner_indent.len()); - new_text.push_str(line_ending); + new_text.reserve_exact(2 + indent.len() + inner_indent.len()); + new_text.push_str(doc.line_ending.as_str()); new_text.push_str(&inner_indent); - - // line where the matching pair will be let local_offs = new_text.chars().count(); - new_text.push_str(line_ending); + new_text.push_str(doc.line_ending.as_str()); new_text.push_str(&indent); - local_offs } else { - new_text.reserve_exact(line_ending.len() + indent.len()); - new_text.push_str(line_ending); + new_text.reserve_exact(1 + indent.len()); + new_text.push_str(doc.line_ending.as_str()); new_text.push_str(&indent); - new_text.chars().count() }; - // Note that `first_trailing_whitespace_char` is at least `pos` so this unsigned - // subtraction cannot underflow. - chars_deleted = pos - first_trailing_whitespace_char; - - ( - first_trailing_whitespace_char, - pos, - local_offs as isize - chars_deleted as isize, - ) - } else { - // If the current line is all whitespace, insert a line ending at the beginning of - // the current line. This makes the current line empty and the new line contain the - // indentation of the old line. - new_text.push_str(line_ending); - - (line_start, line_start, new_text.chars().count() as isize) + (pos, pos, local_offs) }; let new_range = if range.cursor(text) > range.anchor { // when appending, extend the range by local_offs Range::new( - (range.anchor as isize + global_offs) as usize, - (range.head as isize + local_offs + global_offs) as usize, + range.anchor + global_offs, + range.head + local_offs + global_offs, ) } else { // when inserting, slide the range by local_offs Range::new( - (range.anchor as isize + local_offs + global_offs) as usize, - (range.head as isize + local_offs + global_offs) as usize, + range.anchor + local_offs + global_offs, + range.head + local_offs + global_offs, ) }; @@ -4393,11 +3893,9 @@ pub mod insert { // range.replace(|range| range.is_empty(), head); -> fn extend if cond true, new head pos // can be used with cx.mode to do replace or extend on most changes ranges.push(new_range); - global_offs += new_text.chars().count() as isize - chars_deleted as isize; - let tendril = Tendril::from(&new_text); - new_text.clear(); + global_offs += new_text.chars().count(); - (from, to, Some(tendril)) + (from, to, Some(new_text.into())) }); transaction = transaction.with_selection(Selection::new(ranges, selection.primary_index())); @@ -4580,11 +4078,7 @@ fn commit_undo_checkpoint(cx: &mut Context) { // Yank / Paste fn yank(cx: &mut Context) { - yank_impl( - cx.editor, - cx.register - .unwrap_or(cx.editor.config().default_yank_register), - ); + yank_impl(cx.editor, cx.register.unwrap_or('"')); exit_select_mode(cx); } @@ -4645,12 +4139,7 @@ fn yank_joined_impl(editor: &mut Editor, separator: &str, register: char) { fn yank_joined(cx: &mut Context) { let separator = doc!(cx.editor).line_ending.as_str(); - yank_joined_impl( - cx.editor, - separator, - cx.register - .unwrap_or(cx.editor.config().default_yank_register), - ); + yank_joined_impl(cx.editor, separator, cx.register.unwrap_or('"')); exit_select_mode(cx); } @@ -4695,8 +4184,6 @@ enum Paste { Cursor, } -static LINE_ENDING_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"\r\n|\r|\n").unwrap()); - fn paste_impl( values: &[String], doc: &mut Document, @@ -4709,30 +4196,26 @@ fn paste_impl( return; } - if mode == Mode::Insert { - doc.append_changes_to_history(view); - } + let repeat = std::iter::repeat( + // `values` is asserted to have at least one entry above. + values + .last() + .map(|value| Tendril::from(value.repeat(count))) + .unwrap(), + ); // if any of values ends with a line ending, it's linewise paste let linewise = values .iter() .any(|value| get_line_ending_of_str(value).is_some()); - let map_value = |value| { - let value = LINE_ENDING_REGEX.replace_all(value, doc.line_ending.as_str()); - let mut out = Tendril::from(value.as_ref()); - for _ in 1..count { - out.push_str(&value); - } - out - }; - - let repeat = std::iter::repeat( - // `values` is asserted to have at least one entry above. - map_value(values.last().unwrap()), - ); - - let mut values = values.iter().map(|value| map_value(value)).chain(repeat); + // Only compiled once. + static REGEX: Lazy<Regex> = 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().repeat(count))) + .chain(repeat); let text = doc.text(); let selection = doc.selection(view.id); @@ -4812,12 +4295,7 @@ fn paste_primary_clipboard_before(cx: &mut Context) { } fn replace_with_yanked(cx: &mut Context) { - replace_with_yanked_impl( - cx.editor, - cx.register - .unwrap_or(cx.editor.config().default_yank_register), - cx.count(), - ); + replace_with_yanked_impl(cx.editor, cx.register.unwrap_or('"'), cx.count()); exit_select_mode(cx); } @@ -4829,24 +4307,19 @@ fn replace_with_yanked_impl(editor: &mut Editor, register: char, count: usize) { else { return; }; + let values: Vec<_> = values.map(|value| value.to_string()).collect(); let scrolloff = editor.config().scrolloff; - let (view, doc) = current_ref!(editor); - let map_value = |value: &Cow<str>| { - let value = LINE_ENDING_REGEX.replace_all(value, doc.line_ending.as_str()); - let mut out = Tendril::from(value.as_ref()); - for _ in 1..count { - out.push_str(&value); - } - out - }; - let mut values_rev = values.rev().peekable(); - // `values` is asserted to have at least one entry above. - let last = values_rev.peek().unwrap(); - let repeat = std::iter::repeat(map_value(last)); - let mut values = values_rev - .rev() - .map(|value| map_value(&value)) + let (view, doc) = current!(editor); + let repeat = std::iter::repeat( + values + .last() + .map(|value| Tendril::from(&value.repeat(count))) + .unwrap(), + ); + let mut values = values + .iter() + .map(|value| Tendril::from(&value.repeat(count))) .chain(repeat); let selection = doc.selection(view.id); let transaction = Transaction::change_by_selection(doc.text(), selection, |range| { @@ -4856,9 +4329,7 @@ fn replace_with_yanked_impl(editor: &mut Editor, register: char, count: usize) { (range.from(), range.to(), None) } }); - drop(values); - let (view, doc) = current!(editor); doc.apply(&transaction, view.id); doc.append_changes_to_history(view); view.ensure_cursor_in_view(doc, scrolloff); @@ -4887,8 +4358,7 @@ fn paste(editor: &mut Editor, register: char, pos: Paste, count: usize) { fn paste_after(cx: &mut Context) { paste( cx.editor, - cx.register - .unwrap_or(cx.editor.config().default_yank_register), + cx.register.unwrap_or('"'), Paste::After, cx.count(), ); @@ -4898,8 +4368,7 @@ fn paste_after(cx: &mut Context) { fn paste_before(cx: &mut Context) { paste( cx.editor, - cx.register - .unwrap_or(cx.editor.config().default_yank_register), + cx.register.unwrap_or('"'), Paste::Before, cx.count(), ); @@ -5041,10 +4510,7 @@ fn format_selections(cx: &mut Context) { ) .unwrap(); - let edits = tokio::task::block_in_place(|| helix_lsp::block_on(future)) - .ok() - .flatten() - .unwrap_or_default(); + let edits = tokio::task::block_in_place(|| helix_lsp::block_on(future)).unwrap_or_default(); let transaction = helix_lsp::util::generate_transaction_from_edits(doc.text(), edits, offset_encoding); @@ -5058,14 +4524,6 @@ fn join_selections_impl(cx: &mut Context, select_space: bool) { let text = doc.text(); let slice = text.slice(..); - let comment_tokens = doc - .language_config() - .and_then(|config| config.comment_tokens.as_deref()) - .unwrap_or(&[]); - // Sort by length to handle Rust's /// vs // - let mut comment_tokens: Vec<&str> = comment_tokens.iter().map(|x| x.as_str()).collect(); - comment_tokens.sort_unstable_by_key(|x| std::cmp::Reverse(x.len())); - let mut changes = Vec::new(); for selection in doc.selection(view.id) { @@ -5077,31 +4535,10 @@ fn join_selections_impl(cx: &mut Context, select_space: bool) { changes.reserve(lines.len()); - let first_line_idx = slice.line_to_char(start); - let first_line_idx = skip_while(slice, first_line_idx, |ch| matches!(ch, ' ' | '\t')) - .unwrap_or(first_line_idx); - let first_line = slice.slice(first_line_idx..); - let mut current_comment_token = comment_tokens - .iter() - .find(|token| first_line.starts_with(token)); - for line in lines { let start = line_end_char_index(&slice, line); let mut end = text.line_to_char(line + 1); end = skip_while(slice, end, |ch| matches!(ch, ' ' | '\t')).unwrap_or(end); - let slice_from_end = slice.slice(end..); - if let Some(token) = comment_tokens - .iter() - .find(|token| slice_from_end.starts_with(token)) - { - if Some(token) == current_comment_token { - end += token.chars().count(); - end = skip_while(slice, end, |ch| matches!(ch, ' ' | '\t')).unwrap_or(end); - } else { - // update current token, but don't delete this one. - current_comment_token = Some(token); - } - } let separator = if end == line_end_char_index(&slice, line + 1) { // the joining line contains only space-characters => don't include a whitespace when joining @@ -5170,8 +4607,6 @@ fn keep_or_remove_selections_impl(cx: &mut Context, remove: bool) { selection::keep_or_remove_matches(text, doc.selection(view.id), ®ex, remove) { doc.set_selection(view.id, selection); - } else { - cx.editor.set_error("no selections remaining"); } }, ) @@ -5365,22 +4800,6 @@ fn rotate_selections_backward(cx: &mut Context) { rotate_selections(cx, Direction::Backward) } -fn rotate_selections_first(cx: &mut Context) { - let (view, doc) = current!(cx.editor); - let mut selection = doc.selection(view.id).clone(); - selection.set_primary_index(0); - doc.set_selection(view.id, selection); -} - -fn rotate_selections_last(cx: &mut Context) { - let (view, doc) = current!(cx.editor); - let mut selection = doc.selection(view.id).clone(); - let len = selection.len(); - selection.set_primary_index(len - 1); - doc.set_selection(view.id, selection); -} - -#[derive(Debug)] enum ReorderStrategy { RotateForward, RotateBackward, @@ -5393,50 +4812,34 @@ fn reorder_selection_contents(cx: &mut Context, strategy: ReorderStrategy) { let text = doc.text().slice(..); let selection = doc.selection(view.id); - - let mut ranges: Vec<_> = selection + let mut fragments: Vec<_> = selection .slices(text) .map(|fragment| fragment.chunks().collect()) .collect(); - let rotate_by = count.map_or(1, |count| count.get().min(ranges.len())); - - let primary_index = match strategy { - ReorderStrategy::RotateForward => { - ranges.rotate_right(rotate_by); - // Like `usize::wrapping_add`, but provide a custom range from `0` to `ranges.len()` - (selection.primary_index() + ranges.len() + rotate_by) % ranges.len() - } - ReorderStrategy::RotateBackward => { - ranges.rotate_left(rotate_by); - // Like `usize::wrapping_sub`, but provide a custom range from `0` to `ranges.len()` - (selection.primary_index() + ranges.len() - rotate_by) % ranges.len() - } - ReorderStrategy::Reverse => { - if rotate_by % 2 == 0 { - // nothing changed, if we reverse something an even - // amount of times, the output will be the same - return; - } - ranges.reverse(); - // -1 to turn 1-based len into 0-based index - (ranges.len() - 1) - selection.primary_index() - } - }; + let group = count + .map(|count| count.get()) + .unwrap_or(fragments.len()) // default to rotating everything as one group + .min(fragments.len()); + + for chunk in fragments.chunks_mut(group) { + // TODO: also modify main index + match strategy { + ReorderStrategy::RotateForward => chunk.rotate_right(1), + ReorderStrategy::RotateBackward => chunk.rotate_left(1), + ReorderStrategy::Reverse => chunk.reverse(), + }; + } let transaction = Transaction::change( doc.text(), selection .ranges() .iter() - .zip(ranges) + .zip(fragments) .map(|(range, fragment)| (range.from(), range.to(), Some(fragment))), ); - doc.set_selection( - view.id, - Selection::new(selection.ranges().into(), primary_index), - ); doc.apply(&transaction, view.id); } @@ -5631,8 +5034,6 @@ fn jump_forward(cx: &mut Context) { } doc.set_selection(view.id, selection); - // Document we switch to might not have been opened in the view before - doc.ensure_view_init(view.id); view.ensure_cursor_in_view_center(doc, config.scrolloff); }; } @@ -5653,8 +5054,6 @@ fn jump_backward(cx: &mut Context) { } doc.set_selection(view.id, selection); - // Document we switch to might not have been opened in the view before - doc.ensure_view_init(view.id); view.ensure_cursor_in_view_center(doc, config.scrolloff); }; } @@ -5716,7 +5115,7 @@ fn split(editor: &mut Editor, action: Action) { let (view, doc) = current!(editor); let id = doc.id(); let selection = doc.selection(view.id).clone(); - let offset = doc.view_offset(view.id); + let offset = view.offset; editor.switch(id, action); @@ -5725,7 +5124,7 @@ fn split(editor: &mut Editor, action: Action) { doc.set_selection(view.id, selection); // match the view scroll offset (switch doesn't handle this fully // since the selection is only matched after the split) - doc.set_view_offset(view.id, offset); + view.offset = offset; } fn hsplit(cx: &mut Context) { @@ -5771,31 +5170,24 @@ fn wonly(cx: &mut Context) { } fn select_register(cx: &mut Context) { - cx.editor.autoinfo = Some(Info::from_registers( - "Select register", - &cx.editor.registers, - )); + cx.editor.autoinfo = Some(Info::from_registers(&cx.editor.registers)); cx.on_next_key(move |cx, event| { - cx.editor.autoinfo = None; if let Some(ch) = event.char() { + cx.editor.autoinfo = None; cx.editor.selected_register = Some(ch); } }) } fn insert_register(cx: &mut Context) { - cx.editor.autoinfo = Some(Info::from_registers( - "Insert register", - &cx.editor.registers, - )); + cx.editor.autoinfo = Some(Info::from_registers(&cx.editor.registers)); cx.on_next_key(move |cx, event| { - cx.editor.autoinfo = None; if let Some(ch) = event.char() { + cx.editor.autoinfo = None; cx.register = Some(ch); paste( cx.editor, - cx.register - .unwrap_or(cx.editor.config().default_yank_register), + cx.register.unwrap_or('"'), Paste::Cursor, cx.count(), ); @@ -5803,47 +5195,6 @@ fn insert_register(cx: &mut Context) { }) } -fn copy_between_registers(cx: &mut Context) { - cx.editor.autoinfo = Some(Info::from_registers( - "Copy from register", - &cx.editor.registers, - )); - cx.on_next_key(move |cx, event| { - cx.editor.autoinfo = None; - - let Some(source) = event.char() else { - return; - }; - - let Some(values) = cx.editor.registers.read(source, cx.editor) else { - cx.editor.set_error(format!("register {source} is empty")); - return; - }; - let values: Vec<_> = values.map(|value| value.to_string()).collect(); - - cx.editor.autoinfo = Some(Info::from_registers( - "Copy into register", - &cx.editor.registers, - )); - cx.on_next_key(move |cx, event| { - cx.editor.autoinfo = None; - - let Some(dest) = event.char() else { - return; - }; - - let n_values = values.len(); - match cx.editor.registers.write(dest, values) { - Ok(_) => cx.editor.set_status(format!( - "yanked {n_values} value{} from register {source} to {dest}", - if n_values == 1 { "" } else { "s" } - )), - Err(err) => cx.editor.set_error(err.to_string()), - } - }); - }); -} - fn align_view_top(cx: &mut Context) { let (view, doc) = current!(cx.editor); align_view(doc, view, Align::Top); @@ -5868,21 +5219,14 @@ fn align_view_middle(cx: &mut Context) { return; } let doc_text = doc.text().slice(..); + let annotations = view.text_annotations(doc, None); let pos = doc.selection(view.id).primary().cursor(doc_text); - let pos = visual_offset_from_block( - doc_text, - doc.view_offset(view.id).anchor, - pos, - &text_fmt, - &view.text_annotations(doc, None), - ) - .0; + let pos = + visual_offset_from_block(doc_text, view.offset.anchor, pos, &text_fmt, &annotations).0; - let mut offset = doc.view_offset(view.id); - offset.horizontal_offset = pos + view.offset.horizontal_offset = pos .col .saturating_sub((view.inner_area(doc).width as usize) / 2); - doc.set_view_offset(view.id, offset); } fn scroll_up(cx: &mut Context) { @@ -5897,14 +5241,19 @@ fn goto_ts_object_impl(cx: &mut Context, object: &'static str, direction: Direct let count = cx.count(); let motion = move |editor: &mut Editor| { let (view, doc) = current!(editor); - let loader = editor.syn_loader.load(); - if let Some(syntax) = doc.syntax() { + if let Some((lang_config, syntax)) = doc.language_config().zip(doc.syntax()) { let text = doc.text().slice(..); let root = syntax.tree().root_node(); let selection = doc.selection(view.id).clone().transform(|range| { let new_range = movement::goto_treesitter_object( - text, range, object, direction, &root, syntax, &loader, count, + text, + range, + object, + direction, + root, + lang_config, + count, ); if editor.mode == Mode::Select { @@ -5920,7 +5269,6 @@ fn goto_ts_object_impl(cx: &mut Context, object: &'static str, direction: Direct } }); - push_jump(view, doc); doc.set_selection(view.id, selection); } else { editor.set_status("Syntax-tree is not available in current buffer"); @@ -5969,14 +5317,6 @@ fn goto_prev_test(cx: &mut Context) { goto_ts_object_impl(cx, "test", Direction::Backward) } -fn goto_next_xml_element(cx: &mut Context) { - goto_ts_object_impl(cx, "xml-element", Direction::Forward) -} - -fn goto_prev_xml_element(cx: &mut Context) { - goto_ts_object_impl(cx, "xml-element", Direction::Backward) -} - fn goto_next_entry(cx: &mut Context) { goto_ts_object_impl(cx, "entry", Direction::Forward) } @@ -6001,15 +5341,21 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) { if let Some(ch) = event.char() { let textobject = move |editor: &mut Editor| { let (view, doc) = current!(editor); - let loader = editor.syn_loader.load(); let text = doc.text().slice(..); let textobject_treesitter = |obj_name: &str, range: Range| -> Range { - let Some(syntax) = doc.syntax() else { - return 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, &loader, count, + text, + range, + objtype, + obj_name, + syntax.tree().root_node(), + lang_config, + count, ) }; @@ -6044,7 +5390,6 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) { 'c' => textobject_treesitter("comment", range), 'T' => textobject_treesitter("test", range), 'e' => textobject_treesitter("entry", range), - 'x' => textobject_treesitter("xml-element", range), 'p' => textobject::textobject_paragraph(text, range, objtype, count), 'm' => textobject::textobject_pair_surround_closest( doc.syntax(), @@ -6089,25 +5434,14 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) { ("e", "Data structure entry (tree-sitter)"), ("m", "Closest surrounding pair (tree-sitter)"), ("g", "Change"), - ("x", "(X)HTML element (tree-sitter)"), (" ", "... or any character acting as a pair"), ]; cx.editor.autoinfo = Some(Info::new(title, &help_text)); } -static SURROUND_HELP_TEXT: [(&str, &str); 6] = [ - ("m", "Nearest matching pair"), - ("( or )", "Parentheses"), - ("{ or }", "Curly braces"), - ("< or >", "Angled brackets"), - ("[ or ]", "Square brackets"), - (" ", "... or any character"), -]; - fn surround_add(cx: &mut Context) { cx.on_next_key(move |cx, event| { - cx.editor.autoinfo = None; let (view, doc) = current!(cx.editor); // surround_len is the number of new characters being added. let (open, close, surround_len) = match event.char() { @@ -6148,18 +5482,12 @@ fn surround_add(cx: &mut Context) { .with_selection(Selection::new(ranges, selection.primary_index())); doc.apply(&transaction, view.id); exit_select_mode(cx); - }); - - cx.editor.autoinfo = Some(Info::new( - "Surround selections with", - &SURROUND_HELP_TEXT[1..], - )); + }) } fn surround_replace(cx: &mut Context) { let count = cx.count(); cx.on_next_key(move |cx, event| { - cx.editor.autoinfo = None; let surround_ch = match event.char() { Some('m') => None, // m selects the closest surround pair Some(ch) => Some(ch), @@ -6186,7 +5514,6 @@ fn surround_replace(cx: &mut Context) { ); cx.on_next_key(move |cx, event| { - cx.editor.autoinfo = None; let (view, doc) = current!(cx.editor); let to = match event.char() { Some(to) => to, @@ -6214,23 +5541,12 @@ fn surround_replace(cx: &mut Context) { doc.apply(&transaction, view.id); exit_select_mode(cx); }); - - cx.editor.autoinfo = Some(Info::new( - "Replace with a pair of", - &SURROUND_HELP_TEXT[1..], - )); - }); - - cx.editor.autoinfo = Some(Info::new( - "Replace surrounding pair of", - &SURROUND_HELP_TEXT, - )); + }) } fn surround_delete(cx: &mut Context) { let count = cx.count(); cx.on_next_key(move |cx, event| { - cx.editor.autoinfo = None; let surround_ch = match event.char() { Some('m') => None, // m selects the closest surround pair Some(ch) => Some(ch), @@ -6253,9 +5569,7 @@ fn surround_delete(cx: &mut Context) { Transaction::change(doc.text(), change_pos.into_iter().map(|p| (p, p + 1, None))); doc.apply(&transaction, view.id); exit_select_mode(cx); - }); - - cx.editor.autoinfo = Some(Info::new("Delete surrounding pair of", &SURROUND_HELP_TEXT)); + }) } #[derive(Eq, PartialEq)] @@ -6267,52 +5581,64 @@ enum ShellBehavior { } fn shell_pipe(cx: &mut Context) { - shell_prompt_for_behavior(cx, "pipe:".into(), ShellBehavior::Replace); + shell_prompt(cx, "pipe:".into(), ShellBehavior::Replace); } fn shell_pipe_to(cx: &mut Context) { - shell_prompt_for_behavior(cx, "pipe-to:".into(), ShellBehavior::Ignore); + shell_prompt(cx, "pipe-to:".into(), ShellBehavior::Ignore); } fn shell_insert_output(cx: &mut Context) { - shell_prompt_for_behavior(cx, "insert-output:".into(), ShellBehavior::Insert); + shell_prompt(cx, "insert-output:".into(), ShellBehavior::Insert); } fn shell_append_output(cx: &mut Context) { - shell_prompt_for_behavior(cx, "append-output:".into(), ShellBehavior::Append); + shell_prompt(cx, "append-output:".into(), ShellBehavior::Append); } fn shell_keep_pipe(cx: &mut Context) { - shell_prompt(cx, "keep-pipe:".into(), |cx, args| { - let shell = &cx.editor.config().shell; - let (view, doc) = current!(cx.editor); - let selection = doc.selection(view.id); + ui::prompt( + cx, + "keep-pipe:".into(), + Some('|'), + ui::completers::none, + move |cx, input: &str, event: PromptEvent| { + let shell = &cx.editor.config().shell; + if event != PromptEvent::Validate { + return; + } + if input.is_empty() { + return; + } + let (view, doc) = current!(cx.editor); + let selection = doc.selection(view.id); - let mut ranges = SmallVec::with_capacity(selection.len()); - let old_index = selection.primary_index(); - let mut index: Option<usize> = None; - let text = doc.text().slice(..); + let mut ranges = SmallVec::with_capacity(selection.len()); + let old_index = selection.primary_index(); + let mut index: Option<usize> = None; + let text = doc.text().slice(..); - for (i, range) in selection.ranges().iter().enumerate() { - let fragment = range.slice(text); - if let Err(err) = shell_impl(shell, args.join(" ").as_str(), Some(fragment.into())) { - log::debug!("Shell command failed: {}", err); - } else { - ranges.push(*range); - if i >= old_index && index.is_none() { - index = Some(ranges.len() - 1); + for (i, range) in selection.ranges().iter().enumerate() { + let fragment = range.slice(text); + if let Err(err) = shell_impl(shell, input, Some(fragment.into())) { + log::debug!("Shell command failed: {}", err); + } else { + ranges.push(*range); + if i >= old_index && index.is_none() { + index = Some(ranges.len() - 1); + } } } - } - if ranges.is_empty() { - cx.editor.set_error("No selections remaining"); - return; - } + if ranges.is_empty() { + cx.editor.set_error("No selections remaining"); + return; + } - let index = index.unwrap_or_else(|| ranges.len() - 1); - doc.set_selection(view.id, Selection::new(ranges, index)); - }); + let index = index.unwrap_or_else(|| ranges.len() - 1); + doc.set_selection(view.id, Selection::new(ranges, index)); + }, + ); } fn shell_impl(shell: &[String], cmd: &str, input: Option<Rope>) -> anyhow::Result<Tendril> { @@ -6366,24 +5692,27 @@ async fn shell_impl_async( process.wait_with_output().await? }; - let output = if !output.status.success() { - if output.stderr.is_empty() { - match output.status.code() { - Some(exit_code) => bail!("Shell command failed: status {}", exit_code), - None => bail!("Shell command failed"), - } + if !output.status.success() { + if !output.stderr.is_empty() { + let err = String::from_utf8_lossy(&output.stderr).to_string(); + log::error!("Shell error: {}", err); + bail!("Shell error: {}", err); + } + match output.status.code() { + Some(exit_code) => bail!("Shell command failed: status {}", exit_code), + None => bail!("Shell command failed"), } - String::from_utf8_lossy(&output.stderr) - // Prioritize `stderr` output over `stdout` } else if !output.stderr.is_empty() { - let stderr = String::from_utf8_lossy(&output.stderr); - log::debug!("Command printed to stderr: {stderr}"); - stderr - } else { - String::from_utf8_lossy(&output.stdout) - }; + log::debug!( + "Command printed to stderr: {}", + String::from_utf8_lossy(&output.stderr).to_string() + ); + } - Ok(Tendril::from(output)) + let str = std::str::from_utf8(&output.stdout) + .map_err(|_| anyhow!("Process did not output valid UTF-8"))?; + let tendril = Tendril::from(str); + Ok(tendril) } fn shell(cx: &mut compositor::Context, cmd: &str, behavior: &ShellBehavior) { @@ -6407,20 +5736,14 @@ fn shell(cx: &mut compositor::Context, cmd: &str, behavior: &ShellBehavior) { let output = if let Some(output) = shell_output.as_ref() { output.clone() } else { - let input = range.slice(text); - match shell_impl(shell, cmd, pipe.then(|| input.into())) { - Ok(mut output) => { - if !input.ends_with("\n") && output.ends_with('\n') { - output.pop(); - if output.ends_with('\r') { - output.pop(); - } - } - + let fragment = range.slice(text); + match shell_impl(shell, cmd, pipe.then(|| fragment.into())) { + Ok(result) => { + let result = Tendril::from(result.trim_end()); if !pipe { - shell_output = Some(output.clone()); + shell_output = Some(result.clone()); } - output + result } Err(err) => { cx.editor.set_error(err.to_string()); @@ -6467,35 +5790,25 @@ fn shell(cx: &mut compositor::Context, cmd: &str, behavior: &ShellBehavior) { view.ensure_cursor_in_view(doc, config.scrolloff); } -fn shell_prompt<F>(cx: &mut Context, prompt: Cow<'static, str>, mut callback_fn: F) -where - F: FnMut(&mut compositor::Context, Args) + 'static, -{ +fn shell_prompt(cx: &mut Context, prompt: Cow<'static, str>, behavior: ShellBehavior) { ui::prompt( cx, prompt, Some('|'), - |editor, input| complete_command_args(editor, SHELL_SIGNATURE, &SHELL_COMPLETER, input, 0), - move |cx, input, event| { - if event != PromptEvent::Validate || input.is_empty() { + ui::completers::filename, + move |cx, input: &str, event: PromptEvent| { + if event != PromptEvent::Validate { return; } - match Args::parse(input, SHELL_SIGNATURE, true, |token| { - expansion::expand(cx.editor, token).map_err(|err| err.into()) - }) { - Ok(args) => callback_fn(cx, args), - Err(err) => cx.editor.set_error(err.to_string()), + if input.is_empty() { + return; } + + shell(cx, input, &behavior); }, ); } -fn shell_prompt_for_behavior(cx: &mut Context, prompt: Cow<'static, str>, behavior: ShellBehavior) { - shell_prompt(cx, prompt, move |cx, args| { - shell(cx, args.join(" ").as_str(), &behavior) - }) -} - fn suspend(_cx: &mut Context) { #[cfg(not(windows))] { @@ -6606,47 +5919,6 @@ fn increment_impl(cx: &mut Context, increment_direction: IncrementDirection) { } } -fn goto_next_tabstop(cx: &mut Context) { - goto_next_tabstop_impl(cx, Direction::Forward) -} - -fn goto_prev_tabstop(cx: &mut Context) { - goto_next_tabstop_impl(cx, Direction::Backward) -} - -fn goto_next_tabstop_impl(cx: &mut Context, direction: Direction) { - let (view, doc) = current!(cx.editor); - let view_id = view.id; - let Some(mut snippet) = doc.active_snippet.take() else { - cx.editor.set_error("no snippet is currently active"); - return; - }; - let tabstop = match direction { - Direction::Forward => Some(snippet.next_tabstop(doc.selection(view_id))), - Direction::Backward => snippet - .prev_tabstop(doc.selection(view_id)) - .map(|selection| (selection, false)), - }; - let Some((selection, last_tabstop)) = tabstop else { - return; - }; - doc.set_selection(view_id, selection); - if !last_tabstop { - doc.active_snippet = Some(snippet) - } - if cx.editor.mode() == Mode::Insert { - cx.on_next_key_fallback(|cx, key| { - if let Some(c) = key.char() { - let (view, doc) = current!(cx.editor); - if let Some(snippet) = &doc.active_snippet { - doc.apply(&snippet.delete_placeholder(doc.text()), view.id); - } - insert_char(cx, c); - } - }) - } -} - fn record_macro(cx: &mut Context) { if let Some((reg, mut keys)) = cx.editor.macro_recording.take() { // Remove the keypress which ends the recording @@ -6772,7 +6044,6 @@ fn jump_to_label(cx: &mut Context, labels: Vec<Range>, behaviour: Movement) { let alphabet = &cx.editor.config().jump_label_alphabet; let Some(i) = event .char() - .filter(|_| event.modifiers.is_empty()) .and_then(|ch| alphabet.iter().position(|&it| it == ch)) else { doc_mut!(cx.editor, &doc).remove_jump_labels(view); @@ -6789,7 +6060,6 @@ fn jump_to_label(cx: &mut Context, labels: Vec<Range>, behaviour: Movement) { let alphabet = &cx.editor.config().jump_label_alphabet; let Some(inner) = event .char() - .filter(|_| event.modifiers.is_empty()) .and_then(|ch| alphabet.iter().position(|&it| it == ch)) else { return; @@ -6825,10 +6095,6 @@ fn jump_to_word(cx: &mut Context, behaviour: Movement) { // Calculate the jump candidates: ranges for any visible words with two or // more characters. let alphabet = &cx.editor.config().jump_label_alphabet; - if alphabet.is_empty() { - return; - } - let jump_label_limit = alphabet.len() * alphabet.len(); let mut words = Vec::with_capacity(jump_label_limit); let (view, doc) = current_ref!(cx.editor); @@ -6836,7 +6102,7 @@ fn jump_to_word(cx: &mut Context, behaviour: Movement) { // This is not necessarily exact if there is virtual text like soft wrap. // It's ok though because the extra jump labels will not be rendered. - let start = text.line_to_char(text.char_to_line(doc.view_offset(view.id).anchor)); + let start = text.line_to_char(text.char_to_line(view.offset.anchor)); let end = text.line_to_char(view.estimate_last_doc_line(doc) + 1); let primary_selection = doc.selection(view.id).primary(); @@ -6845,7 +6111,7 @@ fn jump_to_word(cx: &mut Context, behaviour: Movement) { let mut cursor_rev = Range::point(cursor); if text.get_char(cursor).is_some_and(|c| !c.is_whitespace()) { let cursor_word_end = movement::move_next_word_end(text, cursor_fwd, 1); - // single grapheme words need a special case + // single grapheme words need a specical case if cursor_word_end.anchor == cursor { cursor_fwd = cursor_word_end; } @@ -6862,9 +6128,7 @@ fn jump_to_word(cx: &mut Context, behaviour: Movement) { // madeup of word characters. The latter condition is needed because // move_next_word_end simply treats a sequence of characters from // the same char class as a word so `=<` would also count as a word. - let add_label = text - .slice(..cursor_fwd.head) - .graphemes_rev() + let add_label = RevRopeGraphemes::new(text.slice(..cursor_fwd.head)) .take(2) .take_while(|g| g.chars().all(char_is_word)) .count() @@ -6890,9 +6154,7 @@ fn jump_to_word(cx: &mut Context, behaviour: Movement) { // madeup of word characters. The latter condition is needed because // move_prev_word_start simply treats a sequence of characters from // the same char class as a word so `=<` would also count as a word. - let add_label = text - .slice(cursor_rev.head..) - .graphemes() + let add_label = RopeGraphemes::new(text.slice(cursor_rev.head..)) .take(2) .take_while(|g| g.chars().all(char_is_word)) .count() @@ -6918,34 +6180,3 @@ fn jump_to_word(cx: &mut Context, behaviour: Movement) { } jump_to_label(cx, words, behaviour) } - -fn lsp_or_syntax_symbol_picker(cx: &mut Context) { - let doc = doc!(cx.editor); - - if doc - .language_servers_with_feature(LanguageServerFeature::DocumentSymbols) - .next() - .is_some() - { - lsp::symbol_picker(cx); - } else if doc.syntax().is_some() { - syntax_symbol_picker(cx); - } else { - cx.editor - .set_error("No language server supporting document symbols or syntax info available"); - } -} - -fn lsp_or_syntax_workspace_symbol_picker(cx: &mut Context) { - let doc = doc!(cx.editor); - - if doc - .language_servers_with_feature(LanguageServerFeature::WorkspaceSymbols) - .next() - .is_some() - { - lsp::workspace_symbol_picker(cx); - } else { - syntax_workspace_symbol_picker(cx); - } -} |