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.rs1587
1 files changed, 439 insertions, 1148 deletions
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
index 430d4430..44516b31 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,7 @@ 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",
+ select_register_history, "Select an item from a register's history",
);
}
@@ -629,11 +544,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 +559,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 +1033,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 +1045,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 +1126,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 +1158,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 +1199,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 +1208,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 +1265,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 +1305,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 +1362,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 +1600,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 +1633,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 +1666,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 +1682,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 {
@@ -1879,16 +1717,14 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction, sync_cursor
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 +1737,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 +1932,6 @@ fn select_regex(cx: &mut Context) {
selection::select_on_matches(text, doc.selection(view.id), &regex)
{
doc.set_selection(view.id, selection);
- } else {
- cx.editor.set_error("nothing selected");
}
},
);
@@ -2263,7 +2097,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| {
@@ -2292,7 +2126,7 @@ fn search_next_or_prev_impl(cx: &mut Context, movement: Movement, direction: Dir
.unwrap_or(cx.editor.registers.last_search_register);
let config = cx.editor.config();
let scrolloff = config.scrolloff;
- if let Some(query) = cx.editor.registers.first(register, cx.editor) {
+ if let Some(query) = cx.editor.registers.latest(register, cx.editor) {
let search_config = &config.search;
let case_insensitive = if search_config.smart_case {
!query.chars().any(char::is_uppercase)
@@ -2342,59 +2176,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<_>>()
@@ -2417,7 +2206,7 @@ fn make_search_word_bounded(cx: &mut Context) {
let register = cx
.register
.unwrap_or(cx.editor.registers.last_search_register);
- let regex = match cx.editor.registers.first(register, cx.editor) {
+ let regex = match cx.editor.registers.latest(register, cx.editor) {
Some(regex) => regex,
None => return,
};
@@ -2470,42 +2259,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 +2362,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 +2370,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)
@@ -2880,10 +2645,8 @@ fn delete_selection_impl(cx: &mut Context, op: Operation, yank: YankAction) {
if cx.register != Some('_') && matches!(yank, YankAction::Yank) {
// 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 values = selection.fragments(text).map(Cow::into_owned);
+ 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 +2665,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 +2827,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 +2844,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 +2855,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 +2912,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 +3127,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 +3136,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 +3226,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 +3247,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 +3309,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 +3337,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 +3414,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 +3427,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 +3456,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 +3475,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 +3499,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 +3550,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 +3561,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 +3578,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 +3606,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 +3615,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 +3645,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 +3700,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 +3720,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 +3786,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 +3795,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 +3808,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 +3830,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 +3864,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 +3903,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 +4088,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);
}
@@ -4634,7 +4138,7 @@ fn yank_joined_impl(editor: &mut Editor, separator: &str, register: char) {
acc
});
- match editor.registers.write(register, vec![joined]) {
+ match editor.registers.write(register, [joined]) {
Ok(_) => editor.set_status(format!(
"joined and yanked {selections} selection{} to register {register}",
if selections == 1 { "" } else { "s" }
@@ -4645,12 +4149,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);
}
@@ -4672,7 +4171,7 @@ fn yank_primary_selection_impl(editor: &mut Editor, register: char) {
let selection = doc.selection(view.id).primary().fragment(text).to_string();
- match editor.registers.write(register, vec![selection]) {
+ match editor.registers.write(register, [selection]) {
Ok(_) => editor.set_status(format!("yanked primary selection to register {register}",)),
Err(err) => editor.set_error(err.to_string()),
}
@@ -4695,8 +4194,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 +4206,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 +4305,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 +4317,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 +4339,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 +4368,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 +4378,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 +4520,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 +4534,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 +4545,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 +4617,6 @@ fn keep_or_remove_selections_impl(cx: &mut Context, remove: bool) {
selection::keep_or_remove_matches(text, doc.selection(view.id), &regex, remove)
{
doc.set_selection(view.id, selection);
- } else {
- cx.editor.set_error("no selections remaining");
}
},
)
@@ -5365,22 +4810,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 +4822,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 +5044,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 +5064,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 +5125,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 +5134,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 +5180,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 +5205,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 +5229,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 +5251,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 +5279,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 +5327,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 +5351,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 +5400,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 +5444,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 +5492,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 +5524,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 +5551,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 +5579,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 +5591,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 +5702,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 +5746,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 +5800,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,63 +5929,19 @@ 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
keys.pop();
- let s = keys
- .into_iter()
- .map(|key| {
- let s = key.to_string();
- if s.chars().count() == 1 {
- s
- } else {
- format!("<{}>", s)
- }
- })
- .collect::<String>();
- match cx.editor.registers.write(reg, vec![s]) {
+ let s = keys.into_iter().map(|key| {
+ let s = key.to_string();
+ if s.chars().count() == 1 {
+ s
+ } else {
+ format!("<{}>", s)
+ }
+ });
+ match cx.editor.registers.write(reg, s) {
Ok(_) => cx
.editor
.set_status(format!("Recorded to register [{}]", reg)),
@@ -6772,7 +6051,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 +6067,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 +6102,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 +6109,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 +6118,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 +6135,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 +6161,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()
@@ -6919,33 +6188,55 @@ 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 select_register_history(cx: &mut Context) {
+ struct HistoryEntry {
+ index: usize,
+ last_value: String,
}
-}
-fn lsp_or_syntax_workspace_symbol_picker(cx: &mut Context) {
- let doc = doc!(cx.editor);
+ // TODO: only show the registers that support selecting from history.
+ cx.editor.autoinfo = Some(Info::from_registers(&cx.editor.registers));
+ cx.on_next_key(move |cx, event| {
+ cx.editor.autoinfo = None;
+ let Some(register) = event.char() else { return };
+ let Some(history) = cx.editor.registers.history(register) else {
+ cx.editor
+ .set_error(format!("No history for register '{register}'"));
+ return;
+ };
- if doc
- .language_servers_with_feature(LanguageServerFeature::WorkspaceSymbols)
- .next()
- .is_some()
- {
- lsp::workspace_symbol_picker(cx);
- } else {
- syntax_workspace_symbol_picker(cx);
- }
+ let items = history.map(|(index, entry)| HistoryEntry {
+ index,
+ last_value: entry
+ .last()
+ .and_then(|s| s.lines().next())
+ .unwrap_or("<empty>")
+ .to_string(),
+ });
+ let columns = vec![
+ PickerColumn::new("entry", |entry: &HistoryEntry, _| {
+ entry.index.to_string().into()
+ }),
+ PickerColumn::new("contents", |entry: &HistoryEntry, _| {
+ entry.last_value.as_str().into()
+ }),
+ ];
+
+ let picker = Picker::new(
+ columns,
+ 1, // "contents"
+ items,
+ (),
+ move |cx, entry, _action| {
+ if let Err(err) = cx
+ .editor
+ .registers
+ .select_history_entry(register, entry.index)
+ {
+ cx.editor.set_error(err.to_string());
+ }
+ },
+ );
+ cx.push_layer(Box::new(overlaid(picker)));
+ })
}