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 | 632 |
1 files changed, 305 insertions, 327 deletions
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 69496eb6..097c3493 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -3,6 +3,7 @@ pub(crate) mod lsp; pub(crate) mod typed; pub use dap::*; +use futures_util::FutureExt; use helix_event::status; use helix_stdx::{ path::expand_tilde, @@ -10,10 +11,7 @@ use helix_stdx::{ }; use helix_vcs::{FileChange, Hunk}; pub use lsp::*; -use tui::{ - text::Span, - widgets::{Cell, Row}, -}; +use tui::text::Span; pub use typed::*; use helix_core::{ @@ -61,8 +59,7 @@ use crate::{ compositor::{self, Component, Compositor}, filter_picker_entry, job::Callback, - keymap::ReverseKeymap, - ui::{self, menu::Item, overlay::overlaid, Picker, Popup, Prompt, PromptEvent}, + ui::{self, overlay::overlaid, Picker, PickerColumn, Popup, Prompt, PromptEvent}, }; use crate::job::{self, Jobs}; @@ -2257,216 +2254,193 @@ fn global_search(cx: &mut Context) { } } - impl ui::menu::Item for FileResult { - type Data = Option<PathBuf>; - - fn format(&self, current_path: &Self::Data) -> Row { - let relative_path = helix_stdx::path::get_relative_path(&self.path) - .to_string_lossy() - .into_owned(); - if current_path - .as_ref() - .map(|p| p == &self.path) - .unwrap_or(false) - { - format!("{} (*)", relative_path).into() - } else { - relative_path.into() - } - } + struct GlobalSearchConfig { + smart_case: bool, + file_picker_config: helix_view::editor::FilePickerConfig, } let config = cx.editor.config(); - let smart_case = config.search.smart_case; - let file_picker_config = config.file_picker.clone(); + let config = GlobalSearchConfig { + smart_case: config.search.smart_case, + file_picker_config: config.file_picker.clone(), + }; - let reg = cx.register.unwrap_or('/'); - let completions = search_completions(cx, Some(reg)); - ui::raw_regex_prompt( - cx, - "global-search:".into(), - Some(reg), - move |_editor: &Editor, input: &str| { - completions - .iter() - .filter(|comp| comp.starts_with(input)) - .map(|comp| (0.., std::borrow::Cow::Owned(comp.clone()))) - .collect() - }, - move |cx, _, input, event| { - if event != PromptEvent::Validate { - return; - } - cx.editor.registers.last_search_register = reg; + let columns = [ + PickerColumn::new("path", |item: &FileResult, _| { + let path = helix_stdx::path::get_relative_path(&item.path); + format!("{}:{}", path.to_string_lossy(), item.line_num + 1).into() + }), + PickerColumn::hidden("contents"), + ]; - let current_path = doc_mut!(cx.editor).path().cloned(); - let documents: Vec<_> = cx - .editor - .documents() - .map(|doc| (doc.path().cloned(), doc.text().to_owned())) - .collect(); + let get_files = |query: &str, + editor: &mut Editor, + config: std::sync::Arc<GlobalSearchConfig>, + injector: &ui::picker::Injector<_, _>| { + if query.is_empty() { + return async { Ok(()) }.boxed(); + } - if let Ok(matcher) = RegexMatcherBuilder::new() - .case_smart(smart_case) - .build(input) - { - let search_root = helix_stdx::env::current_working_dir(); - if !search_root.exists() { - cx.editor - .set_error("Current working directory does not exist"); - return; - } + let search_root = helix_stdx::env::current_working_dir(); + if !search_root.exists() { + return async { Err(anyhow::anyhow!("Current working directory does not exist")) } + .boxed(); + } - let (picker, injector) = Picker::stream(current_path); - - let dedup_symlinks = file_picker_config.deduplicate_links; - let absolute_root = search_root - .canonicalize() - .unwrap_or_else(|_| search_root.clone()); - let injector_ = injector.clone(); - - std::thread::spawn(move || { - let searcher = SearcherBuilder::new() - .binary_detection(BinaryDetection::quit(b'\x00')) - .build(); - - let mut walk_builder = WalkBuilder::new(search_root); - - walk_builder - .hidden(file_picker_config.hidden) - .parents(file_picker_config.parents) - .ignore(file_picker_config.ignore) - .follow_links(file_picker_config.follow_symlinks) - .git_ignore(file_picker_config.git_ignore) - .git_global(file_picker_config.git_global) - .git_exclude(file_picker_config.git_exclude) - .max_depth(file_picker_config.max_depth) - .filter_entry(move |entry| { - filter_picker_entry(entry, &absolute_root, dedup_symlinks) + let documents: Vec<_> = editor + .documents() + .map(|doc| (doc.path().cloned(), doc.text().to_owned())) + .collect(); + + let matcher = match RegexMatcherBuilder::new() + .case_smart(config.smart_case) + .build(query) + { + Ok(matcher) => { + // Clear any "Failed to compile regex" errors out of the statusline. + editor.clear_status(); + matcher + } + Err(err) => { + log::info!("Failed to compile search pattern in global search: {}", err); + return async { Err(anyhow::anyhow!("Failed to compile regex")) }.boxed(); + } + }; + + let dedup_symlinks = config.file_picker_config.deduplicate_links; + let absolute_root = search_root + .canonicalize() + .unwrap_or_else(|_| search_root.clone()); + + let injector = injector.clone(); + async move { + let searcher = SearcherBuilder::new() + .binary_detection(BinaryDetection::quit(b'\x00')) + .build(); + WalkBuilder::new(search_root) + .hidden(config.file_picker_config.hidden) + .parents(config.file_picker_config.parents) + .ignore(config.file_picker_config.ignore) + .follow_links(config.file_picker_config.follow_symlinks) + .git_ignore(config.file_picker_config.git_ignore) + .git_global(config.file_picker_config.git_global) + .git_exclude(config.file_picker_config.git_exclude) + .max_depth(config.file_picker_config.max_depth) + .filter_entry(move |entry| { + filter_picker_entry(entry, &absolute_root, dedup_symlinks) + }) + .add_custom_ignore_filename(helix_loader::config_dir().join("ignore")) + .add_custom_ignore_filename(".helix/ignore") + .build_parallel() + .run(|| { + let mut searcher = searcher.clone(); + let matcher = matcher.clone(); + let injector = injector.clone(); + let documents = &documents; + Box::new(move |entry: Result<DirEntry, ignore::Error>| -> WalkState { + let entry = match entry { + Ok(entry) => entry, + Err(_) => return WalkState::Continue, + }; + + match entry.file_type() { + Some(entry) if entry.is_file() => {} + // skip everything else + _ => return WalkState::Continue, + }; + + let mut stop = false; + let sink = sinks::UTF8(|line_num, _line_content| { + stop = injector + .push(FileResult::new(entry.path(), line_num as usize - 1)) + .is_err(); + + Ok(!stop) + }); + let doc = documents.iter().find(|&(doc_path, _)| { + doc_path + .as_ref() + .map_or(false, |doc_path| doc_path == entry.path()) }); - walk_builder - .add_custom_ignore_filename(helix_loader::config_dir().join("ignore")); - walk_builder.add_custom_ignore_filename(".helix/ignore"); - - walk_builder.build_parallel().run(|| { - let mut searcher = searcher.clone(); - let matcher = matcher.clone(); - let injector = injector_.clone(); - let documents = &documents; - Box::new(move |entry: Result<DirEntry, ignore::Error>| -> WalkState { - let entry = match entry { - Ok(entry) => entry, - Err(_) => return WalkState::Continue, - }; - - match entry.file_type() { - Some(entry) if entry.is_file() => {} - // skip everything else - _ => return WalkState::Continue, - }; - - let mut stop = false; - let sink = sinks::UTF8(|line_num, _| { - stop = injector - .push(FileResult::new(entry.path(), line_num as usize - 1)) - .is_err(); - - Ok(!stop) - }); - let doc = documents.iter().find(|&(doc_path, _)| { - doc_path - .as_ref() - .map_or(false, |doc_path| doc_path == entry.path()) - }); - - let result = if let Some((_, doc)) = doc { - // there is already a buffer for this file - // 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 continous buffer is required - // convert the rope to a string - let text = doc.to_string(); - searcher.search_slice(&matcher, text.as_bytes(), sink) - } else { - searcher.search_reader( - &matcher, - RopeReader::new(doc.slice(..)), - sink, - ) - } + let result = if let Some((_, doc)) = doc { + // there is already a buffer for this file + // 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 continous buffer is required + // convert the rope to a string + let text = doc.to_string(); + searcher.search_slice(&matcher, text.as_bytes(), sink) } else { - searcher.search_path(&matcher, entry.path(), sink) - }; - - if let Err(err) = result { - log::error!( - "Global search error: {}, {}", - entry.path().display(), - err - ); + searcher.search_reader( + &matcher, + RopeReader::new(doc.slice(..)), + sink, + ) } - if stop { - WalkState::Quit - } else { - WalkState::Continue - } - }) - }); + } else { + searcher.search_path(&matcher, entry.path(), sink) + }; + + if let Err(err) = result { + log::error!("Global search error: {}, {}", entry.path().display(), err); + } + if stop { + WalkState::Quit + } else { + WalkState::Continue + } + }) }); + Ok(()) + } + .boxed() + }; - cx.jobs.callback(async move { - let call = move |_: &mut Editor, compositor: &mut Compositor| { - let picker = Picker::with_stream( - picker, - injector, - move |cx, FileResult { path, line_num }, action| { - let doc = match cx.editor.open(path, action) { - Ok(id) => doc_mut!(cx.editor, &id), - Err(e) => { - cx.editor.set_error(format!( - "Failed to open file '{}': {}", - path.display(), - e - )); - return; - } - }; + let reg = cx.register.unwrap_or('/'); + cx.editor.registers.last_search_register = reg; + + let picker = Picker::new( + columns, + 1, // contents + [], + config, + move |cx, FileResult { path, line_num, .. }, action| { + let doc = match cx.editor.open(path, action) { + Ok(id) => doc_mut!(cx.editor, &id), + Err(e) => { + cx.editor + .set_error(format!("Failed to open file '{}': {}", path.display(), e)); + return; + } + }; - let line_num = *line_num; - let view = view_mut!(cx.editor); - let text = doc.text(); - if line_num >= text.len_lines() { - cx.editor.set_error( + let line_num = *line_num; + let view = view_mut!(cx.editor); + let text = doc.text(); + if line_num >= text.len_lines() { + cx.editor.set_error( "The line you jumped to does not exist anymore because the file has changed.", ); - return; - } - let start = text.line_to_char(line_num); - let end = text.line_to_char((line_num + 1).min(text.len_lines())); + return; + } + let start = text.line_to_char(line_num); + let end = text.line_to_char((line_num + 1).min(text.len_lines())); - doc.set_selection(view.id, Selection::single(start, end)); - if action.align_view(view, doc.id()) { - align_view(doc, view, Align::Center); - } - }, - ) - .with_preview( - |_editor, FileResult { path, line_num }| { - Some((path.clone().into(), Some((*line_num, *line_num)))) - }, - ); - compositor.push(Box::new(overlaid(picker))) - }; - Ok(Callback::EditorCompositor(Box::new(call))) - }) - } else { - // Otherwise do nothing - // log::warn!("Global Search Invalid Pattern") + doc.set_selection(view.id, Selection::single(start, end)); + if action.align_view(view, doc.id()) { + align_view(doc, view, Align::Center); } }, - ); + ) + .with_preview(|_editor, FileResult { path, line_num, .. }| { + Some((path.as_path().into(), Some((*line_num, *line_num)))) + }) + .with_history_register(Some(reg)) + .with_dynamic_query(get_files, Some(275)); + + cx.push_layer(Box::new(overlaid(picker))); } enum Extend { @@ -2894,31 +2868,6 @@ fn buffer_picker(cx: &mut Context) { focused_at: std::time::Instant, } - impl ui::menu::Item for BufferMeta { - type Data = (); - - fn format(&self, _data: &Self::Data) -> Row { - let path = self - .path - .as_deref() - .map(helix_stdx::path::get_relative_path); - let path = match path.as_deref().and_then(Path::to_str) { - Some(path) => path, - None => SCRATCH_BUFFER_NAME, - }; - - let mut flags = String::new(); - if self.is_modified { - flags.push('+'); - } - if self.is_current { - flags.push('*'); - } - - Row::new([self.id.to_string(), flags, path.to_string()]) - } - } - let new_meta = |doc: &Document| BufferMeta { id: doc.id(), path: doc.path().cloned(), @@ -2937,7 +2886,31 @@ fn buffer_picker(cx: &mut Context) { // mru items.sort_unstable_by_key(|item| std::cmp::Reverse(item.focused_at)); - let picker = Picker::new(items, (), |cx, meta, action| { + let columns = [ + PickerColumn::new("id", |meta: &BufferMeta, _| meta.id.to_string().into()), + PickerColumn::new("flags", |meta: &BufferMeta, _| { + let mut flags = String::new(); + if meta.is_modified { + flags.push('+'); + } + if meta.is_current { + flags.push('*'); + } + flags.into() + }), + PickerColumn::new("path", |meta: &BufferMeta, _| { + let path = meta + .path + .as_deref() + .map(helix_stdx::path::get_relative_path); + path.as_deref() + .and_then(Path::to_str) + .unwrap_or(SCRATCH_BUFFER_NAME) + .to_string() + .into() + }), + ]; + let picker = Picker::new(columns, 2, items, (), |cx, meta, action| { cx.editor.switch(meta.id, action); }) .with_preview(|editor, meta| { @@ -2961,33 +2934,6 @@ fn jumplist_picker(cx: &mut Context) { is_current: bool, } - impl ui::menu::Item for JumpMeta { - type Data = (); - - fn format(&self, _data: &Self::Data) -> Row { - let path = self - .path - .as_deref() - .map(helix_stdx::path::get_relative_path); - let path = match path.as_deref().and_then(Path::to_str) { - Some(path) => path, - None => SCRATCH_BUFFER_NAME, - }; - - let mut flags = Vec::new(); - if self.is_current { - flags.push("*"); - } - - let flag = if flags.is_empty() { - "".into() - } else { - format!(" ({})", flags.join("")) - }; - format!("{} {}{} {}", self.id, path, flag, self.text).into() - } - } - for (view, _) in cx.editor.tree.views_mut() { for doc_id in view.jumps.iter().map(|e| e.0).collect::<Vec<_>>().iter() { let doc = doc_mut!(cx.editor, doc_id); @@ -3014,17 +2960,43 @@ fn jumplist_picker(cx: &mut Context) { } }; + let columns = [ + ui::PickerColumn::new("id", |item: &JumpMeta, _| item.id.to_string().into()), + ui::PickerColumn::new("path", |item: &JumpMeta, _| { + let path = item + .path + .as_deref() + .map(helix_stdx::path::get_relative_path); + path.as_deref() + .and_then(Path::to_str) + .unwrap_or(SCRATCH_BUFFER_NAME) + .to_string() + .into() + }), + ui::PickerColumn::new("flags", |item: &JumpMeta, _| { + let mut flags = Vec::new(); + if item.is_current { + flags.push("*"); + } + + if flags.is_empty() { + "".into() + } else { + format!(" ({})", flags.join("")).into() + } + }), + ui::PickerColumn::new("contents", |item: &JumpMeta, _| item.text.as_str().into()), + ]; + let picker = Picker::new( - cx.editor - .tree - .views() - .flat_map(|(view, _)| { - view.jumps - .iter() - .rev() - .map(|(doc_id, selection)| new_meta(view, *doc_id, selection.clone())) - }) - .collect(), + columns, + 1, // path + cx.editor.tree.views().flat_map(|(view, _)| { + view.jumps + .iter() + .rev() + .map(|(doc_id, selection)| new_meta(view, *doc_id, selection.clone())) + }), (), |cx, meta, action| { cx.editor.switch(meta.id, action); @@ -3054,33 +3026,6 @@ fn changed_file_picker(cx: &mut Context) { style_renamed: Style, } - impl Item for FileChange { - type Data = FileChangeData; - - fn format(&self, data: &Self::Data) -> Row { - let process_path = |path: &PathBuf| { - path.strip_prefix(&data.cwd) - .unwrap_or(path) - .display() - .to_string() - }; - - let (sign, style, content) = match self { - Self::Untracked { path } => ("[+]", data.style_untracked, process_path(path)), - Self::Modified { path } => ("[~]", data.style_modified, process_path(path)), - Self::Conflict { path } => ("[x]", data.style_conflict, process_path(path)), - Self::Deleted { path } => ("[-]", data.style_deleted, process_path(path)), - Self::Renamed { from_path, to_path } => ( - "[>]", - data.style_renamed, - format!("{} -> {}", process_path(from_path), process_path(to_path)), - ), - }; - - Row::new([Cell::from(Span::styled(sign, style)), Cell::from(content)]) - } - } - let cwd = helix_stdx::env::current_working_dir(); if !cwd.exists() { cx.editor @@ -3094,8 +3039,41 @@ fn changed_file_picker(cx: &mut Context) { let deleted = cx.editor.theme.get("diff.minus"); let renamed = cx.editor.theme.get("diff.delta.moved"); + let columns = [ + PickerColumn::new("change", |change: &FileChange, data: &FileChangeData| { + match change { + FileChange::Untracked { .. } => Span::styled("+ untracked", data.style_untracked), + FileChange::Modified { .. } => Span::styled("~ modified", data.style_modified), + FileChange::Conflict { .. } => Span::styled("x conflict", data.style_conflict), + FileChange::Deleted { .. } => Span::styled("- deleted", data.style_deleted), + FileChange::Renamed { .. } => Span::styled("> renamed", data.style_renamed), + } + .into() + }), + PickerColumn::new("path", |change: &FileChange, data: &FileChangeData| { + let display_path = |path: &PathBuf| { + path.strip_prefix(&data.cwd) + .unwrap_or(path) + .display() + .to_string() + }; + match change { + FileChange::Untracked { path } => display_path(path), + FileChange::Modified { path } => display_path(path), + FileChange::Conflict { path } => display_path(path), + FileChange::Deleted { path } => display_path(path), + FileChange::Renamed { from_path, to_path } => { + format!("{} -> {}", display_path(from_path), display_path(to_path)) + } + } + .into() + }), + ]; + let picker = Picker::new( - Vec::new(), + columns, + 1, // path + [], FileChangeData { cwd: cwd.clone(), style_untracked: added, @@ -3116,7 +3094,7 @@ fn changed_file_picker(cx: &mut Context) { } }, ) - .with_preview(|_editor, meta| Some((meta.path().to_path_buf().into(), None))); + .with_preview(|_editor, meta| Some((meta.path().into(), None))); let injector = picker.injector(); cx.editor @@ -3132,35 +3110,6 @@ fn changed_file_picker(cx: &mut Context) { cx.push_layer(Box::new(overlaid(picker))); } -impl ui::menu::Item for MappableCommand { - type Data = ReverseKeymap; - - fn format(&self, keymap: &Self::Data) -> Row { - let fmt_binding = |bindings: &Vec<Vec<KeyEvent>>| -> String { - bindings.iter().fold(String::new(), |mut acc, bind| { - if !acc.is_empty() { - acc.push(' '); - } - for key in bind { - acc.push_str(&key.key_sequence_format()); - } - acc - }) - }; - - match self { - MappableCommand::Typable { doc, name, .. } => match keymap.get(name as &String) { - Some(bindings) => format!("{} ({}) [:{}]", doc, fmt_binding(bindings), name).into(), - None => format!("{} [:{}]", doc, name).into(), - }, - MappableCommand::Static { doc, name, .. } => match keymap.get(*name) { - Some(bindings) => format!("{} ({}) [{}]", doc, fmt_binding(bindings), name).into(), - None => format!("{} [{}]", doc, name).into(), - }, - } - } -} - pub fn command_palette(cx: &mut Context) { let register = cx.register; let count = cx.count; @@ -3171,16 +3120,45 @@ pub fn command_palette(cx: &mut Context) { [&cx.editor.mode] .reverse_map(); - let mut commands: Vec<MappableCommand> = MappableCommand::STATIC_COMMAND_LIST.into(); - commands.extend(typed::TYPABLE_COMMAND_LIST.iter().map(|cmd| { - MappableCommand::Typable { - name: cmd.name.to_owned(), - doc: cmd.doc.to_owned(), - args: Vec::new(), - } - })); + let commands = MappableCommand::STATIC_COMMAND_LIST.iter().cloned().chain( + typed::TYPABLE_COMMAND_LIST + .iter() + .map(|cmd| MappableCommand::Typable { + name: cmd.name.to_owned(), + args: Vec::new(), + doc: cmd.doc.to_owned(), + }), + ); + + let columns = [ + ui::PickerColumn::new("name", |item, _| match item { + MappableCommand::Typable { name, .. } => format!(":{name}").into(), + MappableCommand::Static { name, .. } => (*name).into(), + }), + ui::PickerColumn::new( + "bindings", + |item: &MappableCommand, keymap: &crate::keymap::ReverseKeymap| { + keymap + .get(item.name()) + .map(|bindings| { + bindings.iter().fold(String::new(), |mut acc, bind| { + if !acc.is_empty() { + acc.push(' '); + } + for key in bind { + acc.push_str(&key.key_sequence_format()); + } + acc + }) + }) + .unwrap_or_default() + .into() + }, + ), + ui::PickerColumn::new("doc", |item: &MappableCommand, _| item.doc().into()), + ]; - let picker = Picker::new(commands, keymap, move |cx, command, _action| { + let picker = Picker::new(columns, 0, commands, keymap, move |cx, command, _action| { let mut ctx = Context { register, count, |