diff --git a/book/src/themes.md b/book/src/themes.md index 40e12330e425..1d0ba151773d 100644 --- a/book/src/themes.md +++ b/book/src/themes.md @@ -305,6 +305,7 @@ These scopes are used for theming the editor interface: | `ui.text.focus` | The currently selected line in the picker | | `ui.text.inactive` | Same as `ui.text` but when the text is inactive (e.g. suggestions) | | `ui.text.info` | The key: command text in `ui.popup.info` boxes | +| `ui.text.directory` | Directory names in prompt completion | | `ui.virtual.ruler` | Ruler columns (see the [`editor.rulers` config][editor-section]) | | `ui.virtual.whitespace` | Visible whitespace characters | | `ui.virtual.indent-guide` | Vertical indent width guides | diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index d08148362e25..9c71f3e4e79b 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2164,7 +2164,7 @@ fn searcher(cx: &mut Context, direction: Direction) { completions .iter() .filter(|comp| comp.starts_with(input)) - .map(|comp| (0.., std::borrow::Cow::Owned(comp.clone()))) + .map(|comp| (0.., comp.clone().into())) .collect() }, move |cx, regex, event| { diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 49864abb683e..3d8655727263 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -3197,8 +3197,8 @@ pub(super) fn command_mode(cx: &mut Context) { { completer(editor, word) .into_iter() - .map(|(range, file)| { - let file = shellwords::escape(file); + .map(|(range, mut file)| { + file.content = shellwords::escape(file.content); // offset ranges to input let offset = input.len() - word_len; diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index ab9b5392bace..6ee49fa57ddd 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -32,6 +32,17 @@ use helix_view::Editor; use std::{error::Error, path::PathBuf}; +struct Utf8PathBuf { + path: String, + is_dir: bool, +} + +impl AsRef for Utf8PathBuf { + fn as_ref(&self) -> &str { + &self.path + } +} + pub fn prompt( cx: &mut crate::commands::Context, prompt: std::borrow::Cow<'static, str>, @@ -266,6 +277,7 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi } pub mod completers { + use super::Utf8PathBuf; use crate::ui::prompt::Completion; use helix_core::fuzzy::fuzzy_match; use helix_core::syntax::LanguageServerFeature; @@ -274,6 +286,7 @@ pub mod completers { use helix_view::{editor::Config, Editor}; use once_cell::sync::Lazy; use std::borrow::Cow; + use tui::text::Span; pub type Completer = fn(&Editor, &str) -> Vec; @@ -290,7 +303,7 @@ pub mod completers { fuzzy_match(input, names, true) .into_iter() - .map(|(name, _)| ((0..), name)) + .map(|(name, _)| ((0..), name.into())) .collect() } @@ -336,7 +349,7 @@ pub mod completers { fuzzy_match(input, &*KEYS, false) .into_iter() - .map(|(name, _)| ((0..), name.into())) + .map(|(name, _)| ((0..), Span::raw(name))) .collect() } @@ -424,7 +437,7 @@ pub mod completers { // TODO: we could return an iter/lazy thing so it can fetch as many as it needs. fn filename_impl( - _editor: &Editor, + editor: &Editor, input: &str, git_ignore: bool, filter_fn: F, @@ -482,7 +495,7 @@ pub mod completers { return None; } - //let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir()); + let is_dir = entry.file_type().is_some_and(|entry| entry.is_dir()); let path = entry.path(); let mut path = if is_tilde { @@ -501,23 +514,35 @@ pub mod completers { } let path = path.into_os_string().into_string().ok()?; - Some(Cow::from(path)) + Some(Utf8PathBuf { path, is_dir }) }) }) // TODO: unwrap or skip - .filter(|path| !path.is_empty()); + .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) + } + }; // 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(), name)) + .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(), file)).collect(); - files.sort_unstable_by(|(_, path1), (_, path2)| path1.cmp(path2)); + 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 } } diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index 1e443ce7ff0e..2f4a6010a44d 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -8,6 +8,7 @@ use helix_view::keyboard::KeyCode; use std::sync::Arc; use std::{borrow::Cow, ops::RangeFrom}; use tui::buffer::Buffer as Surface; +use tui::text::Span; use tui::widgets::{Block, Widget}; use helix_core::{ @@ -19,7 +20,8 @@ use helix_view::{ }; type PromptCharHandler = Box; -pub type Completion = (RangeFrom, Cow<'static, str>); + +pub type Completion = (RangeFrom, Span<'static>); type CompletionFn = Box Vec>; type CallbackFn = Box; pub type DocFn = Box Option>>; @@ -382,7 +384,7 @@ impl Prompt { let (range, item) = &self.completion[index]; - self.line.replace_range(range.clone(), item); + self.line.replace_range(range.clone(), &item.content); self.move_end(); } @@ -407,7 +409,7 @@ impl Prompt { let max_len = self .completion .iter() - .map(|(_, completion)| completion.len() as u16) + .map(|(_, completion)| completion.content.len() as u16) .max() .unwrap_or(BASE_WIDTH) .max(BASE_WIDTH); @@ -446,18 +448,22 @@ impl Prompt { for (i, (_range, completion)) in self.completion.iter().enumerate().skip(offset).take(items) { - let color = if Some(i) == self.selection { - selected_color // TODO: just invert bg + let is_selected = Some(i) == self.selection; + + let completion_item_style = if is_selected { + selected_color } else { - completion_color + completion_color.patch(completion.style) }; + surface.set_stringn( area.x + col * (1 + col_width), area.y + row, - completion, + &completion.content, col_width.saturating_sub(1) as usize, - color, + completion_item_style, ); + row += 1; if row > area.height - 1 { row = 0; diff --git a/runtime/themes/catppuccin_mocha.toml b/runtime/themes/catppuccin_mocha.toml index 76e65c62f7be..a96b46df5085 100644 --- a/runtime/themes/catppuccin_mocha.toml +++ b/runtime/themes/catppuccin_mocha.toml @@ -86,6 +86,7 @@ "ui.text" = "text" "ui.text.focus" = { fg = "text", bg = "surface0", modifiers = ["bold"] } "ui.text.inactive" = { fg = "overlay1" } +"ui.text.directory" = { fg = "blue" } "ui.virtual" = "overlay0" "ui.virtual.ruler" = { bg = "surface0" } diff --git a/runtime/themes/dark_plus.toml b/runtime/themes/dark_plus.toml index 813eebf3c252..7f57723c5f71 100644 --- a/runtime/themes/dark_plus.toml +++ b/runtime/themes/dark_plus.toml @@ -72,6 +72,7 @@ "ui.bufferline.background" = { bg = "background" } "ui.text" = { fg = "text" } "ui.text.focus" = { fg = "white" } +"ui.text.directory" = { fg = "blue3" } "ui.virtual.whitespace" = { fg = "#3e3e3d" } "ui.virtual.ruler" = { bg = "borders" } "ui.virtual.indent-guide" = { fg = "dark_gray4" } diff --git a/runtime/themes/dracula.toml b/runtime/themes/dracula.toml index c1a9a12bc84a..bb4a38d5e22a 100644 --- a/runtime/themes/dracula.toml +++ b/runtime/themes/dracula.toml @@ -118,6 +118,7 @@ "ui.statusline.select" = { fg = "black", bg = "cyan", modifiers = ["bold"] } "ui.text" = { fg = "foreground" } "ui.text.focus" = { fg = "cyan" } +"ui.text.directory" = { fg = "cyan" } "ui.virtual.indent-guide" = { fg = "indent" } "ui.virtual.inlay-hint" = { fg = "cyan" } "ui.virtual.inlay-hint.parameter" = { fg = "cyan", modifiers = ["italic", "dim"] } diff --git a/runtime/themes/dracula_at_night.toml b/runtime/themes/dracula_at_night.toml index 55f2615398c1..ac75199d9d56 100644 --- a/runtime/themes/dracula_at_night.toml +++ b/runtime/themes/dracula_at_night.toml @@ -42,6 +42,7 @@ "ui.statusline.select" = { fg = "background_dark", bg = "purple" } "ui.text" = { fg = "foreground" } "ui.text.focus" = { fg = "cyan" } +"ui.text.directory" = { fg = "cyan" } "ui.window" = { fg = "foreground" } "ui.virtual.jump-label" = { fg = "pink", modifiers = ["bold"] } "ui.virtual.ruler" = { bg = "background_dark" } diff --git a/runtime/themes/github_dark.toml b/runtime/themes/github_dark.toml index e98032108e26..c0f89cc815c9 100644 --- a/runtime/themes/github_dark.toml +++ b/runtime/themes/github_dark.toml @@ -59,6 +59,7 @@ label = "scale.red.3" "ui.text" = { fg = "fg.muted" } "ui.text.focus" = { fg = "fg.default" } "ui.text.inactive" = "fg.subtle" +"ui.text.directory" = { fg = "scale.blue.2" } "ui.virtual" = { fg = "scale.gray.6" } "ui.virtual.ruler" = { bg = "canvas.subtle" } "ui.virtual.jump-label" = { fg = "scale.red.2", modifiers = ["bold"] } diff --git a/runtime/themes/github_light.toml b/runtime/themes/github_light.toml index cd3f1e5d16cd..6fa79c7d63c6 100644 --- a/runtime/themes/github_light.toml +++ b/runtime/themes/github_light.toml @@ -59,6 +59,7 @@ label = "scale.red.5" "ui.text" = { fg = "fg.muted" } "ui.text.focus" = { fg = "fg.default" } "ui.text.inactive" = "fg.subtle" +"ui.text.directory" = { fg = "scale.blue.4" } "ui.virtual" = { fg = "scale.gray.2" } "ui.virtual.ruler" = { bg = "canvas.subtle" } diff --git a/runtime/themes/gruvbox.toml b/runtime/themes/gruvbox.toml index e91b6f32284b..933b63125f94 100644 --- a/runtime/themes/gruvbox.toml +++ b/runtime/themes/gruvbox.toml @@ -106,6 +106,7 @@ "ui.statusline.select" = { fg = "bg1", bg = "orange1", modifiers = ["bold"] } "ui.text" = { fg = "fg1" } +"ui.text.directory" = { fg = "blue1" } "ui.virtual.inlay-hint" = { fg = "gray" } "ui.virtual.jump-label" = { fg = "purple0", modifiers = ["bold"] } "ui.virtual.ruler" = { bg = "bg1" } diff --git a/runtime/themes/material_deep_ocean.toml b/runtime/themes/material_deep_ocean.toml index 8b1e3c86144d..76041d07d345 100644 --- a/runtime/themes/material_deep_ocean.toml +++ b/runtime/themes/material_deep_ocean.toml @@ -60,6 +60,7 @@ "ui.background" = { bg = "bg", fg = "text" } "ui.text" = { fg = "text" } +"ui.text.directory" = { fg = "blue" } "ui.statusline" = { bg = "bg", fg = "text" } "ui.statusline.inactive" = { bg = "bg", fg = "disabled" } diff --git a/runtime/themes/rose_pine.toml b/runtime/themes/rose_pine.toml index 9a5ba450e92e..ae88bdc5441b 100644 --- a/runtime/themes/rose_pine.toml +++ b/runtime/themes/rose_pine.toml @@ -36,6 +36,7 @@ "ui.text" = { fg = "text" } "ui.text.focus" = { bg = "overlay" } "ui.text.info" = { fg = "subtle" } +"ui.text.directory" = { fg = "iris" } "ui.virtual.jump-label" = { fg = "love", modifiers = ["bold"] } "ui.virtual.ruler" = { bg = "overlay" } diff --git a/runtime/themes/tokyonight.toml b/runtime/themes/tokyonight.toml index 4e53e03b85f5..08e7ce3aec33 100644 --- a/runtime/themes/tokyonight.toml +++ b/runtime/themes/tokyonight.toml @@ -89,6 +89,7 @@ hint = { fg = "hint" } "ui.text.focus" = { bg = "bg-focus" } "ui.text.inactive" = { fg = "comment", modifiers = ["italic"] } "ui.text.info" = { bg = "bg-menu", fg = "fg" } +"ui.text.directory" = { fg = "cyan" } "ui.virtual.ruler" = { bg = "fg-gutter" } "ui.virtual.whitespace" = { fg = "fg-gutter" } "ui.virtual.inlay-hint" = { bg = "bg-inlay", fg = "teal" } diff --git a/theme.toml b/theme.toml index c1e5883d0bec..ccb2ec795445 100644 --- a/theme.toml +++ b/theme.toml @@ -55,6 +55,7 @@ label = "honey" "ui.text" = { fg = "lavender" } "ui.text.focus" = { fg = "white" } "ui.text.inactive" = "sirocco" +"ui.text.directory" = { fg = "lilac" } "ui.virtual" = { fg = "comet" } "ui.virtual.ruler" = { bg = "bossanova" } "ui.virtual.jump-label" = { fg = "apricot", modifiers = ["bold"] }