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.rs632
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,