Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-term/src/ui/mod.rs')
| -rw-r--r-- | helix-term/src/ui/mod.rs | 746 |
1 files changed, 268 insertions, 478 deletions
diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 58b6fc00..56888ee1 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -1,50 +1,35 @@ mod completion; -mod document; pub(crate) mod editor; +mod fuzzy_match; mod info; pub mod lsp; mod markdown; pub mod menu; pub mod overlay; -pub mod picker; +mod picker; pub mod popup; -pub mod prompt; +mod prompt; mod spinner; mod statusline; mod text; -mod text_decorations; -use crate::compositor::Compositor; -use crate::filter_picker_entry; +use crate::compositor::{Component, Compositor}; use crate::job::{self, Callback}; pub use completion::Completion; pub use editor::EditorView; -use helix_stdx::rope; -use helix_view::theme::Style; pub use markdown::Markdown; pub use menu::Menu; -pub use picker::{Column as PickerColumn, FileLocation, Picker}; +pub use picker::{FileLocation, FilePicker, Picker}; pub use popup::Popup; pub use prompt::{Prompt, PromptEvent}; pub use spinner::{ProgressSpinners, Spinner}; pub use text::Text; +use helix_core::regex::Regex; +use helix_core::regex::RegexBuilder; use helix_view::Editor; -use tui::text::{Span, Spans}; -use std::path::Path; -use std::{error::Error, path::PathBuf}; - -struct Utf8PathBuf { - path: String, - is_dir: bool, -} - -impl AsRef<str> for Utf8PathBuf { - fn as_ref(&self) -> &str { - &self.path - } -} +use std::path::PathBuf; pub fn prompt( cx: &mut crate::commands::Context, @@ -77,27 +62,12 @@ pub fn regex_prompt( prompt: std::borrow::Cow<'static, str>, history_register: Option<char>, completion_fn: impl FnMut(&Editor, &str) -> Vec<prompt::Completion> + 'static, - fun: impl Fn(&mut crate::compositor::Context, rope::Regex, PromptEvent) + 'static, -) { - raw_regex_prompt( - cx, - prompt, - history_register, - completion_fn, - move |cx, regex, _, event| fun(cx, regex, event), - ); -} -pub fn raw_regex_prompt( - cx: &mut crate::commands::Context, - prompt: std::borrow::Cow<'static, str>, - history_register: Option<char>, - completion_fn: impl FnMut(&Editor, &str) -> Vec<prompt::Completion> + 'static, - fun: impl Fn(&mut crate::compositor::Context, rope::Regex, &str, PromptEvent) + 'static, + fun: impl Fn(&mut Editor, Regex, PromptEvent) + 'static, ) { let (view, doc) = current!(cx.editor); let doc_id = view.doc; let snapshot = doc.selection(view.id).clone(); - let offset_snapshot = doc.view_offset(view.id); + let offset_snapshot = view.offset; let config = cx.editor.config(); let mut prompt = Prompt::new( @@ -109,7 +79,7 @@ pub fn raw_regex_prompt( PromptEvent::Abort => { let (view, doc) = current!(cx.editor); doc.set_selection(view.id, snapshot.clone()); - doc.set_view_offset(view.id, offset_snapshot); + view.offset = offset_snapshot; } PromptEvent::Update | PromptEvent::Validate => { // skip empty input @@ -123,13 +93,10 @@ pub fn raw_regex_prompt( false }; - match rope::RegexBuilder::new() - .syntax( - rope::Config::new() - .case_insensitive(case_insensitive) - .multi_line(true), - ) - .build(input) + match RegexBuilder::new(input) + .case_insensitive(case_insensitive) + .multi_line(true) + .build() { Ok(regex) => { let (view, doc) = current!(cx.editor); @@ -142,7 +109,7 @@ pub fn raw_regex_prompt( view.jumps.push((doc_id, snapshot.clone())); } - fun(cx, regex, input, event); + fun(cx.editor, regex, event); let (view, doc) = current!(cx.editor); view.ensure_cursor_in_view(doc, config.scrolloff); @@ -150,7 +117,7 @@ pub fn raw_regex_prompt( Err(err) => { let (view, doc) = current!(cx.editor); doc.set_selection(view.id, snapshot.clone()); - doc.set_view_offset(view.id, offset_snapshot); + view.offset = offset_snapshot; if event == PromptEvent::Validate { let callback = async move { @@ -158,12 +125,14 @@ pub fn raw_regex_prompt( move |_editor: &mut Editor, compositor: &mut Compositor| { let contents = Text::new(format!("{}", err)); let size = compositor.size(); - let popup = Popup::new("invalid-regex", contents) + let mut popup = Popup::new("invalid-regex", contents) .position(Some(helix_core::Position::new( size.height as usize - 2, // 2 = statusline + commandline 0, ))) .auto_close(true); + popup.required_size((size.width, size.height)); + compositor.replace_or_push("invalid-regex", popup); }, )); @@ -171,61 +140,30 @@ pub fn raw_regex_prompt( }; cx.jobs.callback(callback); + } else { + // Update + // TODO: mark command line as error } } } } } }, - ) - .with_language("regex", std::sync::Arc::clone(&cx.editor.syn_loader)); + ); // Calculate initial completion prompt.recalculate_completion(cx.editor); // prompt cx.push_layer(Box::new(prompt)); } -/// We want to exclude files that the editor can't handle yet -fn get_excluded_types() -> ignore::types::Types { - use ignore::types::TypesBuilder; - let mut type_builder = TypesBuilder::new(); - type_builder - .add( - "compressed", - "*.{zip,gz,bz2,zst,lzo,sz,tgz,tbz2,lz,lz4,lzma,lzo,z,Z,xz,7z,rar,cab}", - ) - .expect("Invalid type definition"); - type_builder.negate("all"); - type_builder - .build() - .expect("failed to build excluded_types") -} - -#[derive(Debug)] -pub struct FilePickerData { - root: PathBuf, - directory_style: Style, -} -type FilePicker = Picker<PathBuf, FilePickerData>; - -pub fn file_picker(editor: &Editor, root: PathBuf) -> FilePicker { - use ignore::WalkBuilder; +pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePicker<PathBuf> { + use ignore::{types::TypesBuilder, WalkBuilder}; use std::time::Instant; - let config = editor.config(); - let data = FilePickerData { - root: root.clone(), - directory_style: editor.theme.get("ui.text.directory"), - }; - let now = Instant::now(); - let dedup_symlinks = config.file_picker.deduplicate_links; - let absolute_root = root.canonicalize().unwrap_or_else(|_| root.clone()); - let mut walk_builder = WalkBuilder::new(&root); - - let mut files = walk_builder + walk_builder .hidden(config.file_picker.hidden) .parents(config.file_picker.parents) .ignore(config.file_picker.ignore) @@ -233,113 +171,57 @@ pub fn file_picker(editor: &Editor, root: PathBuf) -> FilePicker { .git_ignore(config.file_picker.git_ignore) .git_global(config.file_picker.git_global) .git_exclude(config.file_picker.git_exclude) - .sort_by_file_name(|name1, name2| name1.cmp(name2)) .max_depth(config.file_picker.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") - .types(get_excluded_types()) + // We always want to ignore the .git directory, otherwise if + // `ignore` is turned off above, we end up with a lot of noise + // in our picker. + .filter_entry(|entry| entry.file_name() != ".git"); + + // We want to exclude files that the editor can't handle yet + let mut type_builder = TypesBuilder::new(); + type_builder + .add( + "compressed", + "*.{zip,gz,bz2,zst,lzo,sz,tgz,tbz2,lz,lz4,lzma,lzo,z,Z,xz,7z,rar,cab}", + ) + .expect("Invalid type definition"); + type_builder.negate("all"); + let excluded_types = type_builder .build() - .filter_map(|entry| { - let entry = entry.ok()?; - if !entry.file_type()?.is_file() { - return None; - } + .expect("failed to build excluded_types"); + walk_builder.types(excluded_types); + + // We want files along with their modification date for sorting + let files = walk_builder.build().filter_map(|entry| { + let entry = entry.ok()?; + + // This is faster than entry.path().is_dir() since it uses cached fs::Metadata fetched by ignore/walkdir + let is_dir = entry.file_type().map_or(false, |ft| ft.is_dir()); + if is_dir { + // Will give a false positive if metadata cannot be read (eg. permission error) + None + } else { Some(entry.into_path()) - }); - log::debug!("file_picker init {:?}", Instant::now().duration_since(now)); - - let columns = [PickerColumn::new( - "path", - |item: &PathBuf, data: &FilePickerData| { - let path = item.strip_prefix(&data.root).unwrap_or(item); - let mut spans = Vec::with_capacity(3); - if let Some(dirs) = path.parent().filter(|p| !p.as_os_str().is_empty()) { - spans.extend([ - Span::styled(dirs.to_string_lossy(), data.directory_style), - Span::styled(std::path::MAIN_SEPARATOR_STR, data.directory_style), - ]); - } - let filename = path - .file_name() - .expect("normalized paths can't end in `..`") - .to_string_lossy(); - spans.push(Span::raw(filename)); - Spans::from(spans).into() - }, - )]; - let picker = Picker::new(columns, 0, [], data, move |cx, path: &PathBuf, action| { - if let Err(e) = cx.editor.open(path, action) { - let err = if let Some(err) = e.source() { - format!("{}", err) - } else { - format!("unable to open \"{}\"", path.display()) - }; - cx.editor.set_error(err); - } - }) - .with_preview(|_editor, path| Some((path.as_path().into(), None))); - let injector = picker.injector(); - let timeout = std::time::Instant::now() + std::time::Duration::from_millis(30); - - let mut hit_timeout = false; - for file in &mut files { - if injector.push(file).is_err() { - break; } - if std::time::Instant::now() >= timeout { - hit_timeout = true; - break; - } - } - if hit_timeout { - std::thread::spawn(move || { - for file in files { - if injector.push(file).is_err() { - break; - } - } - }); - } - picker -} + }); -type FileExplorer = Picker<(PathBuf, bool), (PathBuf, Style)>; + // Cap the number of files if we aren't in a git project, preventing + // hangs when using the picker in your home directory + let files: Vec<_> = if root.join(".git").exists() { + files.collect() + } else { + // const MAX: usize = 8192; + const MAX: usize = 100_000; + files.take(MAX).collect() + }; -pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result<FileExplorer, std::io::Error> { - let directory_style = editor.theme.get("ui.text.directory"); - let directory_content = directory_content(&root, editor)?; + log::debug!("file_picker init {:?}", Instant::now().duration_since(now)); - let columns = [PickerColumn::new( - "path", - |(path, is_dir): &(PathBuf, bool), (root, directory_style): &(PathBuf, Style)| { - let name = path.strip_prefix(root).unwrap_or(path).to_string_lossy(); - if *is_dir { - Span::styled(format!("{}/", name), *directory_style).into() - } else { - name.into() - } - }, - )]; - let picker = Picker::new( - columns, - 0, - directory_content, - (root, directory_style), - move |cx, (path, is_dir): &(PathBuf, bool), action| { - if *is_dir { - let new_root = helix_stdx::path::normalize(path); - let callback = Box::pin(async move { - let call: Callback = - Callback::EditorCompositor(Box::new(move |editor, compositor| { - if let Ok(picker) = file_explorer(new_root, editor) { - compositor.push(Box::new(overlay::overlaid(picker))); - } - })); - Ok(call) - }); - cx.jobs.callback(callback); - } else if let Err(e) = cx.editor.open(path, action) { + FilePicker::new( + files, + root, + move |cx, path: &PathBuf, action| { + if let Err(e) = cx.editor.open(path, action) { let err = if let Some(err) = e.source() { format!("{}", err) } else { @@ -348,83 +230,20 @@ pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result<FileExplorer, std cx.editor.set_error(err); } }, + |_editor, path| Some((path.clone().into(), None)), ) - .with_preview(|_editor, (path, _is_dir)| Some((path.as_path().into(), None))); - - Ok(picker) -} - -fn directory_content(root: &Path, editor: &Editor) -> Result<Vec<(PathBuf, bool)>, std::io::Error> { - use ignore::WalkBuilder; - - let config = editor.config(); - - let mut walk_builder = WalkBuilder::new(root); - - let mut content: Vec<(PathBuf, bool)> = walk_builder - .hidden(config.file_explorer.hidden) - .parents(config.file_explorer.parents) - .ignore(config.file_explorer.ignore) - .follow_links(config.file_explorer.follow_symlinks) - .git_ignore(config.file_explorer.git_ignore) - .git_global(config.file_explorer.git_global) - .git_exclude(config.file_explorer.git_exclude) - .max_depth(Some(1)) - .add_custom_ignore_filename(helix_loader::config_dir().join("ignore")) - .add_custom_ignore_filename(".helix/ignore") - .types(get_excluded_types()) - .build() - .filter_map(|entry| { - entry - .map(|entry| { - let is_dir = entry - .file_type() - .is_some_and(|file_type| file_type.is_dir()); - let mut path = entry.path().to_path_buf(); - if is_dir && path != root && config.file_explorer.flatten_dirs { - while let Some(single_child_directory) = get_child_if_single_dir(&path) { - path = single_child_directory; - } - } - (path, is_dir) - }) - .ok() - .filter(|entry| entry.0 != root) - }) - .collect(); - - content.sort_by(|(path1, is_dir1), (path2, is_dir2)| (!is_dir1, path1).cmp(&(!is_dir2, path2))); - - if root.parent().is_some() { - content.insert(0, (root.join(".."), true)); - } - - Ok(content) -} - -fn get_child_if_single_dir(path: &Path) -> Option<PathBuf> { - let mut entries = path.read_dir().ok()?; - let entry = entries.next()?.ok()?; - if entries.next().is_none() && entry.file_type().is_ok_and(|file_type| file_type.is_dir()) { - Some(entry.path()) - } else { - None - } } pub mod completers { - use super::Utf8PathBuf; use crate::ui::prompt::Completion; - use helix_core::command_line::{self, Tokenizer}; - use helix_core::fuzzy::fuzzy_match; - use helix_core::syntax::config::LanguageServerFeature; + use fuzzy_matcher::skim::SkimMatcherV2 as Matcher; + use fuzzy_matcher::FuzzyMatcher; use helix_view::document::SCRATCH_BUFFER_NAME; use helix_view::theme; use helix_view::{editor::Config, Editor}; use once_cell::sync::Lazy; use std::borrow::Cow; - use std::collections::BTreeSet; - use tui::text::Span; + use std::cmp::Reverse; pub type Completer = fn(&Editor, &str) -> Vec<Completion>; @@ -433,32 +252,63 @@ pub mod completers { } pub fn buffer(editor: &Editor, input: &str) -> Vec<Completion> { - let names = editor.documents.values().map(|doc| { - doc.relative_path() - .map(|p| p.display().to_string().into()) - .unwrap_or_else(|| Cow::from(SCRATCH_BUFFER_NAME)) - }); - - fuzzy_match(input, names, true) + let mut names: Vec<_> = editor + .documents + .values() + .map(|doc| { + let name = doc + .relative_path() + .map(|p| p.display().to_string()) + .unwrap_or_else(|| String::from(SCRATCH_BUFFER_NAME)); + ((0..), Cow::from(name)) + }) + .collect(); + + let matcher = Matcher::default(); + + let mut matches: Vec<_> = names .into_iter() - .map(|(name, _)| ((0..), name.into())) - .collect() + .filter_map(|(_range, name)| { + matcher.fuzzy_match(&name, input).map(|score| (name, score)) + }) + .collect(); + + matches.sort_unstable_by_key(|(_file, score)| Reverse(*score)); + names = matches.into_iter().map(|(name, _)| ((0..), name)).collect(); + + names } pub fn theme(_editor: &Editor, input: &str) -> Vec<Completion> { - let mut names = theme::Loader::read_names(&helix_loader::config_dir().join("themes")); - for rt_dir in helix_loader::runtime_dirs() { - names.extend(theme::Loader::read_names(&rt_dir.join("themes"))); - } + let mut names = theme::Loader::read_names(&helix_loader::runtime_dir().join("themes")); + names.extend(theme::Loader::read_names( + &helix_loader::config_dir().join("themes"), + )); names.push("default".into()); names.push("base16_default".into()); names.sort(); names.dedup(); - fuzzy_match(input, names, false) + let mut names: Vec<_> = names .into_iter() - .map(|(name, _)| ((0..), name.into())) - .collect() + .map(|name| ((0..), Cow::from(name))) + .collect(); + + let matcher = Matcher::default(); + + let mut matches: Vec<_> = names + .into_iter() + .filter_map(|(_range, name)| { + matcher.fuzzy_match(&name, input).map(|score| (name, score)) + }) + .collect(); + + matches.sort_unstable_by(|(name1, score1), (name2, score2)| { + (Reverse(*score1), name1).cmp(&(Reverse(*score2), name2)) + }); + names = matches.into_iter().map(|(name, _)| ((0..), name)).collect(); + + names } /// Recursive function to get all keys from this value and add them to vec @@ -477,31 +327,6 @@ pub mod completers { } } - /// Completes names of language servers which are running for the current document. - pub fn active_language_servers(editor: &Editor, input: &str) -> Vec<Completion> { - let language_servers = doc!(editor).language_servers().map(|ls| ls.name()); - - fuzzy_match(input, language_servers, false) - .into_iter() - .map(|(name, _)| ((0..), Span::raw(name.to_string()))) - .collect() - } - - /// Completes names of language servers which are configured for the language of the current - /// document. - pub fn configured_language_servers(editor: &Editor, input: &str) -> Vec<Completion> { - let language_servers = doc!(editor) - .language_config() - .into_iter() - .flat_map(|config| &config.language_servers) - .map(|ls| ls.name.as_str()); - - fuzzy_match(input, language_servers, false) - .into_iter() - .map(|(name, _)| ((0..), Span::raw(name.to_string()))) - .collect() - } - pub fn setting(_editor: &Editor, input: &str) -> Vec<Completion> { static KEYS: Lazy<Vec<String>> = Lazy::new(|| { let mut keys = Vec::new(); @@ -510,23 +335,23 @@ pub mod completers { keys }); - fuzzy_match(input, &*KEYS, false) + let matcher = Matcher::default(); + + let mut matches: Vec<_> = KEYS + .iter() + .filter_map(|name| matcher.fuzzy_match(name, input).map(|score| (name, score))) + .collect(); + + matches.sort_unstable_by_key(|(_file, score)| Reverse(*score)); + matches .into_iter() - .map(|(name, _)| ((0..), Span::raw(name))) + .map(|(name, _)| ((0..), name.into())) .collect() } pub fn filename(editor: &Editor, input: &str) -> Vec<Completion> { - filename_with_git_ignore(editor, input, true) - } - - pub fn filename_with_git_ignore( - editor: &Editor, - input: &str, - git_ignore: bool, - ) -> Vec<Completion> { - filename_impl(editor, input, git_ignore, |entry| { - let is_dir = entry.file_type().is_some_and(|entry| entry.is_dir()); + filename_impl(editor, input, |entry| { + let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir()); if is_dir { FileMatch::AcceptIncomplete @@ -537,47 +362,76 @@ pub mod completers { } pub fn language(editor: &Editor, input: &str) -> Vec<Completion> { + let matcher = Matcher::default(); + let text: String = "text".into(); - let loader = editor.syn_loader.load(); - let language_ids = loader + let language_ids = editor + .syn_loader .language_configs() .map(|config| &config.language_id) .chain(std::iter::once(&text)); - fuzzy_match(input, language_ids, false) + let mut matches: Vec<_> = language_ids + .filter_map(|language_id| { + matcher + .fuzzy_match(language_id, input) + .map(|score| (language_id, score)) + }) + .collect(); + + matches.sort_unstable_by(|(language1, score1), (language2, score2)| { + (Reverse(*score1), language1).cmp(&(Reverse(*score2), language2)) + }); + + matches .into_iter() - .map(|(name, _)| ((0..), name.to_owned().into())) + .map(|(language, _score)| ((0..), language.clone().into())) .collect() } pub fn lsp_workspace_command(editor: &Editor, input: &str) -> Vec<Completion> { - let commands = doc!(editor) - .language_servers_with_feature(LanguageServerFeature::WorkspaceCommand) - .flat_map(|ls| { - ls.capabilities() - .execute_command_provider - .iter() - .flat_map(|options| options.commands.iter()) - }); + let matcher = Matcher::default(); + + let (_, doc) = current_ref!(editor); - fuzzy_match(input, commands, false) + let language_server = match doc.language_server() { + Some(language_server) => language_server, + None => { + return vec![]; + } + }; + + let options = match &language_server.capabilities().execute_command_provider { + Some(options) => options, + None => { + return vec![]; + } + }; + + let mut matches: Vec<_> = options + .commands + .iter() + .filter_map(|command| { + matcher + .fuzzy_match(command, input) + .map(|score| (command, score)) + }) + .collect(); + + matches.sort_unstable_by(|(command1, score1), (command2, score2)| { + (Reverse(*score1), command1).cmp(&(Reverse(*score2), command2)) + }); + + matches .into_iter() - .map(|(name, _)| ((0..), name.to_owned().into())) + .map(|(command, _score)| ((0..), command.clone().into())) .collect() } pub fn directory(editor: &Editor, input: &str) -> Vec<Completion> { - directory_with_git_ignore(editor, input, true) - } - - pub fn directory_with_git_ignore( - editor: &Editor, - input: &str, - git_ignore: bool, - ) -> Vec<Completion> { - filename_impl(editor, input, git_ignore, |entry| { - let is_dir = entry.file_type().is_some_and(|entry| entry.is_dir()); + filename_impl(editor, input, |entry| { + let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir()); if is_dir { FileMatch::Accept @@ -587,6 +441,50 @@ pub mod completers { }) } + pub fn help(_editor: &Editor, input: &str) -> Vec<Completion> { + let static_cmds_path = helix_loader::runtime_dir().join("help/static-commands"); + let typable_cmds_path = helix_loader::runtime_dir().join("help/typable-commands"); + let mut items: Vec<String> = std::fs::read_dir(static_cmds_path) + .map(|entries| { + entries + .filter_map(|entry| { + let entry = entry.ok()?; + let path = entry.path(); + (path.extension()? == "md") + .then(|| path.file_stem().unwrap().to_string_lossy().into_owned()) + }) + .chain( + std::fs::read_dir(typable_cmds_path) + .map(|entries| { + entries.filter_map(|entry| { + let entry = entry.ok()?; + let path = entry.path(); + (path.extension()? == "md").then(|| { + format!(":{}", path.file_stem().unwrap().to_string_lossy()) + }) + }) + }) + .into_iter() + .flatten(), + ) + .collect() + }) + .unwrap_or_default(); + items.push("topics".to_owned()); + + let matcher = Matcher::default(); + + let mut matches: Vec<_> = items + .into_iter() + .map(Cow::from) + .filter_map(|name| matcher.fuzzy_match(&name, input).map(|score| (name, score))) + .collect(); + + matches.sort_unstable_by_key(|(_file, score)| Reverse(*score)); + + matches.into_iter().map(|(name, _)| ((0..), name)).collect() + } + #[derive(Copy, Clone, PartialEq, Eq)] enum FileMatch { /// Entry should be ignored @@ -599,12 +497,7 @@ pub mod completers { } // TODO: we could return an iter/lazy thing so it can fetch as many as it needs. - fn filename_impl<F>( - editor: &Editor, - input: &str, - git_ignore: bool, - filter_fn: F, - ) -> Vec<Completion> + fn filename_impl<F>(_editor: &Editor, input: &str, filter_fn: F) -> Vec<Completion> where F: Fn(&ignore::DirEntry) -> FileMatch, { @@ -613,30 +506,20 @@ pub mod completers { use ignore::WalkBuilder; use std::path::Path; - let is_tilde = input == "~"; - let path = helix_stdx::path::expand_tilde(Path::new(input)); + let is_tilde = input.starts_with('~') && input.len() == 1; + let path = helix_core::path::expand_tilde(Path::new(input)); let (dir, file_name) = if input.ends_with(std::path::MAIN_SEPARATOR) { (path, None) } else { - let is_period = (input.ends_with((format!("{}.", std::path::MAIN_SEPARATOR)).as_str()) - && input.len() > 2) - || input == "."; - let file_name = if is_period { - Some(String::from(".")) - } else { - path.file_name() - .and_then(|file| file.to_str().map(|path| path.to_owned())) - }; + let file_name = path + .file_name() + .and_then(|file| file.to_str().map(|path| path.to_owned())); - let path = if is_period { - path - } else { - match path.parent() { - Some(path) if !path.as_os_str().is_empty() => Cow::Borrowed(path), - // Path::new("h")'s parent is Some("")... - _ => Cow::Owned(helix_stdx::env::current_working_dir()), - } + let path = match path.parent() { + Some(path) if !path.as_os_str().is_empty() => path.to_path_buf(), + // Path::new("h")'s parent is Some("")... + _ => std::env::current_dir().expect("couldn't determine current directory"), }; (path, file_name) @@ -644,10 +527,9 @@ pub mod completers { let end = input.len()..; - let files = WalkBuilder::new(&dir) + let mut files: Vec<_> = WalkBuilder::new(&dir) .hidden(false) .follow_links(false) // We're scanning over depth 1 - .git_ignore(git_ignore) .max_depth(Some(1)) .build() .filter_map(|file| { @@ -658,7 +540,7 @@ pub mod completers { return None; } - let is_dir = entry.file_type().is_some_and(|entry| entry.is_dir()); + //let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir()); let path = entry.path(); let mut path = if is_tilde { @@ -676,135 +558,43 @@ pub mod completers { path.push(""); } - let path = path.into_os_string().into_string().ok()?; - Some(Utf8PathBuf { path, is_dir }) + let path = path.to_str()?.to_owned(); + Some((end.clone(), Cow::from(path))) }) }) // TODO: unwrap or skip - .filter(|path| !path.path.is_empty()); - - let directory_color = editor.theme.get("ui.text.directory"); - - let style_from_file = |file: Utf8PathBuf| { - if file.is_dir { - Span::styled(file.path, directory_color) - } else { - Span::raw(file.path) - } - }; + .filter(|(_, path)| !path.is_empty()) // TODO + .collect(); // if empty, return a list of dirs and files in current dir if let Some(file_name) = file_name { - let range = (input.len().saturating_sub(file_name.len()))..; - fuzzy_match(&file_name, files, true) - .into_iter() - .map(|(name, _)| (range.clone(), style_from_file(name))) - .collect() - - // TODO: complete to longest common match - } else { - let mut files: Vec<_> = files - .map(|file| (end.clone(), style_from_file(file))) - .collect(); - files.sort_unstable_by(|(_, path1), (_, path2)| path1.content.cmp(&path2.content)); - files - } - } - - pub fn register(editor: &Editor, input: &str) -> Vec<Completion> { - let iter = editor - .registers - .iter_preview() - // Exclude special registers that shouldn't be written to - .filter(|(ch, _)| !matches!(ch, '%' | '#' | '.')) - .map(|(ch, _)| ch.to_string()); - - fuzzy_match(input, iter, false) - .into_iter() - .map(|(name, _)| ((0..), name.into())) - .collect() - } + let matcher = Matcher::default(); - pub fn program(_editor: &Editor, input: &str) -> Vec<Completion> { - static PROGRAMS_IN_PATH: Lazy<BTreeSet<String>> = Lazy::new(|| { - // Go through the entire PATH and read all files into a set. - let Some(path) = std::env::var_os("PATH") else { - return Default::default(); - }; - - std::env::split_paths(&path) - .filter_map(|path| std::fs::read_dir(path).ok()) - .flatten() - .filter_map(|res| { - let entry = res.ok()?; - let metadata = entry.metadata().ok()?; - if metadata.is_file() || metadata.is_symlink() { - entry.file_name().into_string().ok() - } else { - None - } + // inefficient, but we need to calculate the scores, filter out None, then sort. + let mut matches: Vec<_> = files + .into_iter() + .filter_map(|(_range, file)| { + matcher + .fuzzy_match(&file, &file_name) + .map(|score| (file, score)) }) - .collect() - }); - - fuzzy_match(input, PROGRAMS_IN_PATH.iter(), false) - .into_iter() - .map(|(name, _)| ((0..), name.clone().into())) - .collect() - } - - /// This expects input to be a raw string of arguments, because this is what Signature's raw_after does. - pub fn repeating_filenames(editor: &Editor, input: &str) -> Vec<Completion> { - let token = match Tokenizer::new(input, false).last() { - Some(token) => token.unwrap(), - None => return filename(editor, input), - }; - - let offset = token.content_start; + .collect(); - let mut completions = filename(editor, &input[offset..]); - for completion in completions.iter_mut() { - completion.0.start += offset; - } - completions - } + let range = (input.len().saturating_sub(file_name.len()))..; - pub fn shell(editor: &Editor, input: &str) -> Vec<Completion> { - let (command, args, complete_command) = command_line::split(input); + matches.sort_unstable_by(|(file1, score1), (file2, score2)| { + (Reverse(*score1), file1).cmp(&(Reverse(*score2), file2)) + }); - if complete_command { - return program(editor, command); - } + files = matches + .into_iter() + .map(|(file, _)| (range.clone(), file)) + .collect(); - let mut completions = repeating_filenames(editor, args); - for completion in completions.iter_mut() { - // + 1 for separator between `command` and `args` - completion.0.start += command.len() + 1; + // TODO: complete to longest common match + } else { + files.sort_unstable_by(|(_, path1), (_, path2)| path1.cmp(path2)); } - completions - } -} - -#[cfg(test)] -mod tests { - use std::fs::{create_dir, File}; - - use super::*; - - #[test] - fn test_get_child_if_single_dir() { - let root = tempfile::tempdir().unwrap(); - - assert_eq!(get_child_if_single_dir(root.path()), None); - - let dir = root.path().join("dir1"); - create_dir(&dir).unwrap(); - - assert_eq!(get_child_if_single_dir(root.path()), Some(dir)); - - let file = root.path().join("file"); - File::create(file).unwrap(); - - assert_eq!(get_child_if_single_dir(root.path()), None); + files } } |