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 | 1009 |
1 files changed, 269 insertions, 740 deletions
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 430d4430..2394f0aa 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1,6 +1,5 @@ pub(crate) mod dap; pub(crate) mod lsp; -pub(crate) mod syntax; pub(crate) mod typed; pub use dap::*; @@ -12,21 +11,16 @@ use helix_stdx::{ }; 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}, @@ -36,18 +30,17 @@ use helix_core::{ 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, @@ -150,10 +143,10 @@ impl Context<'_> { #[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 +172,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) @@ -213,7 +207,7 @@ use helix_view::{align_view, Align}; pub enum MappableCommand { Typable { name: String, - args: String, + args: Vec<String>, doc: String, }, Static { @@ -248,19 +242,16 @@ 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), @@ -402,20 +393,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 +418,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 +430,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 +440,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 +453,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 +529,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 +551,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 +585,8 @@ 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", + goto_next_tabstop, "goto next snippet placeholder", + goto_prev_tabstop, "goto next snippet placeholder", ); } @@ -649,21 +621,19 @@ 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('@') { @@ -1272,44 +1242,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 +1283,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,17 +1292,16 @@ 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 mut pos = primary.cursor(text.slice(..)); + pos = text.char_to_byte(pos); let search_start = text .line_to_byte(text.byte_to_line(pos)) - .max(text.floor_char_boundary(pos.saturating_sub(lookaround))); + .max(pos.saturating_sub(1000)); 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 + .min(pos + 1000); + let search_range = text.slice(search_start..search_end); + // we also allow paths that are next to the cursor (can be ambigous 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) @@ -1356,12 +1309,12 @@ fn goto_file_impl(cx: &mut Context, action: Action) { .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)); + let path = path.unwrap_or_else(|| primary.fragment(text.slice(..))); vec![path.into_owned()] } else { // Otherwise use each selection, trimmed. selections - .fragments(text) + .fragments(text.slice(..)) .map(|sel| sel.trim().to_owned()) .filter(|sel| !sel.is_empty()) .collect() @@ -1376,7 +1329,7 @@ fn goto_file_impl(cx: &mut Context, action: Action) { let path = path::expand(&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 +1366,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)); @@ -1724,12 +1677,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 +1710,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() @@ -2263,7 +2183,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| { @@ -2351,12 +2271,6 @@ fn search_selection_detect_word_boundaries(cx: &mut Context) { 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); @@ -2470,42 +2384,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 +2487,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 +2495,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) @@ -2902,7 +2792,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 +2954,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 +2971,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 +2982,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 +3039,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 +3254,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(), }), ); @@ -3519,12 +3356,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 +3377,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 +3439,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 +3467,94 @@ 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 continue_comment_token = doc + .language_config() + .and_then(|config| config.comment_tokens.as_ref()) + .and_then(|tokens| comment::get_comment_token(text, tokens, cursor_line)); + + let line = text.line(cursor_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.language_config(), doc.syntax(), - &config.indent_heuristic, + &doc.config.load().indent_heuristic, &doc.indent_style, doc.tab_width(), text, - above_next_new_line_num, - above_next_line_end_index, - curr_line_num, + line_num, + line_end_index, + cursor_line, ), }; let indent_len = indent.len(); let mut text = String::with_capacity(1 + indent_len); + text.push_str(doc.line_ending.as_str()); + text.push_str(&indent); - 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(' '); - } + if let Some(token) = continue_comment_token { + text.push_str(token); + text.push(' '); } let text = text.repeat(count); // calculate new selection ranges - let pos = offs + above_next_line_end_index + above_next_line_end_width; + let pos = offs + line_end_index + line_end_offset_width; let comment_len = continue_comment_token .map(|token| token.len() + 1) // `+ 1` for the extra space added .unwrap_or_default(); 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) + // pos -> beginning of reference line, + // + (i * (1+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 + (i * (1 + indent_len + comment_len)) + indent_len + comment_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 +3564,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 +3577,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 +3606,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,31 +3625,8 @@ 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)); - - 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); -} + .transform(|range| range.put_cursor(text, pos, cx.editor.mode == Mode::Select)); -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); } @@ -3892,7 +3649,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,7 +3700,6 @@ 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); @@ -3956,7 +3711,6 @@ 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); @@ -3974,13 +3728,13 @@ 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); @@ -4002,7 +3756,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,7 +3765,6 @@ 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); @@ -4041,7 +3795,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 +3850,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 +3870,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>; @@ -4197,15 +3949,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 +3962,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 { @@ -4295,22 +3986,25 @@ pub mod insert { let current_line = text.char_to_line(pos); let line_start = text.line_to_char(current_line); - let continue_comment_token = continue_comment_tokens + let mut new_text = String::new(); + + let continue_comment_token = doc + .language_config() + .and_then(|config| config.comment_tokens.as_ref()) .and_then(|tokens| comment::get_comment_token(text, tokens, current_line)); 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 first_trailing_whitespace_char = (line_start + idx + 1).min(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.language_config(), doc.syntax(), - &config.indent_heuristic, + &doc.config.load().indent_heuristic, &doc.indent_style, doc.tab_width(), text, @@ -4326,11 +4020,10 @@ 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(doc.line_ending.as_str()); new_text.push_str(&indent); new_text.push_str(token); new_text.push(' '); @@ -4338,39 +4031,37 @@ pub mod insert { } else if on_auto_pair { // line where the cursor will be 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, + // Note that `first_trailing_whitespace_char` is at least `pos` so the + // unsigned subtraction (`pos - first_trailing_whitespace_char`) cannot + // underflow. + local_offs as isize - (pos - first_trailing_whitespace_char) 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); + new_text.push_str(doc.line_ending.as_str()); (line_start, line_start, new_text.chars().count() as isize) }; @@ -4378,14 +4069,14 @@ pub mod insert { 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 as isize + local_offs) as usize + 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 as isize + local_offs) as usize + global_offs, + (range.head as isize + local_offs) as usize + global_offs, ) }; @@ -4393,11 +4084,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())); @@ -4695,8 +4384,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, @@ -4713,26 +4400,26 @@ fn paste_impl( 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); @@ -4829,24 +4516,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 +4538,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); @@ -5041,10 +4721,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); @@ -5078,7 +4755,7 @@ 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')) + 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 @@ -5365,22 +5042,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 +5054,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); } @@ -5771,26 +5416,20 @@ 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, @@ -5803,47 +5442,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); @@ -5897,14 +5495,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 +5523,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 +5571,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 +5595,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 +5644,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 +5688,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 +5736,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 +5768,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 +5795,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 +5823,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 +5835,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> { @@ -6410,7 +5990,7 @@ fn shell(cx: &mut compositor::Context, cmd: &str, behavior: &ShellBehavior) { 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') { + if !input.ends_with("\n") && !output.is_empty() && output.ends_with('\n') { output.pop(); if output.ends_with('\r') { output.pop(); @@ -6467,35 +6047,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))] { @@ -6772,7 +6342,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 +6358,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 +6393,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); @@ -6845,7 +6409,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 +6426,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 +6452,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 +6478,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); - } -} |