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.rs746
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
}
}