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.rs457
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);
- }
}