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 | 457 |
1 files changed, 71 insertions, 386 deletions
diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 58b6fc00..efa2473e 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -8,43 +8,29 @@ pub mod menu; pub mod overlay; pub 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::compositor::{Component, Compositor}; use crate::filter_picker_entry; use crate::job::{self, Callback}; -pub use completion::Completion; +pub use completion::{Completion, CompletionItem}; 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::{DynamicPicker, FileLocation, 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 +63,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 crate::compositor::Context, 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 +80,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 +94,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 +110,7 @@ pub fn raw_regex_prompt( view.jumps.push((doc_id, snapshot.clone())); } - fun(cx, regex, input, event); + fun(cx, regex, event); let (view, doc) = current!(cx.editor); view.ensure_cursor_in_view(doc, config.scrolloff); @@ -150,7 +118,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 +126,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); }, )); @@ -185,47 +155,17 @@ pub fn raw_regex_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) -> Picker<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) @@ -235,40 +175,34 @@ pub fn file_picker(editor: &Editor, root: PathBuf) -> FilePicker { .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()) + .filter_entry(move |entry| filter_picker_entry(entry, &absolute_root, dedup_symlinks)); + + walk_builder.add_custom_ignore_filename(helix_loader::config_dir().join("ignore")); + walk_builder.add_custom_ignore_filename(".helix/ignore"); + + // 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; - } - Some(entry.into_path()) - }); + .expect("failed to build excluded_types"); + walk_builder.types(excluded_types); + let mut files = walk_builder.build().filter_map(|entry| { + let entry = entry.ok()?; + if !entry.file_type()?.is_file() { + return None; + } + 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| { + let picker = Picker::new(Vec::new(), 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) @@ -278,7 +212,7 @@ pub fn file_picker(editor: &Editor, root: PathBuf) -> FilePicker { cx.editor.set_error(err); } }) - .with_preview(|_editor, path| Some((path.as_path().into(), None))); + .with_preview(|_editor, path| Some((path.clone().into(), None))); let injector = picker.injector(); let timeout = std::time::Instant::now() + std::time::Duration::from_millis(30); @@ -304,127 +238,15 @@ pub fn file_picker(editor: &Editor, root: PathBuf) -> FilePicker { picker } -type FileExplorer = Picker<(PathBuf, bool), (PathBuf, Style)>; - -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)?; - - 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) { - 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, _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 helix_core::syntax::LanguageServerFeature; 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; pub type Completer = fn(&Editor, &str) -> Vec<Completion>; @@ -441,7 +263,7 @@ pub mod completers { fuzzy_match(input, names, true) .into_iter() - .map(|(name, _)| ((0..), name.into())) + .map(|(name, _)| ((0..), name)) .collect() } @@ -477,31 +299,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(); @@ -512,7 +309,7 @@ pub mod completers { fuzzy_match(input, &*KEYS, false) .into_iter() - .map(|(name, _)| ((0..), Span::raw(name))) + .map(|(name, _)| ((0..), name.into())) .collect() } @@ -526,7 +323,7 @@ pub mod completers { 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()); + let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir()); if is_dir { FileMatch::AcceptIncomplete @@ -539,8 +336,8 @@ pub mod completers { pub fn language(editor: &Editor, input: &str) -> Vec<Completion> { 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)); @@ -552,16 +349,14 @@ pub mod completers { } pub fn lsp_workspace_command(editor: &Editor, input: &str) -> Vec<Completion> { - let commands = doc!(editor) + let Some(options) = doc!(editor) .language_servers_with_feature(LanguageServerFeature::WorkspaceCommand) - .flat_map(|ls| { - ls.capabilities() - .execute_command_provider - .iter() - .flat_map(|options| options.commands.iter()) - }); - - fuzzy_match(input, commands, false) + .find_map(|ls| ls.capabilities().execute_command_provider.as_ref()) + else { + return vec![]; + }; + + fuzzy_match(input, &options.commands, false) .into_iter() .map(|(name, _)| ((0..), name.to_owned().into())) .collect() @@ -577,7 +372,7 @@ pub mod completers { 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()); + let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir()); if is_dir { FileMatch::Accept @@ -600,7 +395,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, + _editor: &Editor, input: &str, git_ignore: bool, filter_fn: F, @@ -633,9 +428,9 @@ pub mod completers { path } else { match path.parent() { - Some(path) if !path.as_os_str().is_empty() => Cow::Borrowed(path), + Some(path) if !path.as_os_str().is_empty() => path.to_path_buf(), // Path::new("h")'s parent is Some("")... - _ => Cow::Owned(helix_stdx::env::current_working_dir()), + _ => helix_stdx::env::current_working_dir(), } }; @@ -658,7 +453,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 { @@ -677,134 +472,24 @@ pub mod completers { } let path = path.into_os_string().into_string().ok()?; - Some(Utf8PathBuf { path, is_dir }) + Some(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()); // 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))) + .map(|(name, _)| (range.clone(), 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)); + let mut files: Vec<_> = files.map(|file| (end.clone(), file)).collect(); + files.sort_unstable_by(|(_, path1), (_, path2)| path1.cmp(path2)); 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() - } - - 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 - } - }) - .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; - - let mut completions = filename(editor, &input[offset..]); - for completion in completions.iter_mut() { - completion.0.start += offset; - } - completions - } - - pub fn shell(editor: &Editor, input: &str) -> Vec<Completion> { - let (command, args, complete_command) = command_line::split(input); - - if complete_command { - return program(editor, command); - } - - 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; - } - - 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); - } } |