From c925fbd9f837a54caadd8014f91651859a009bbb Mon Sep 17 00:00:00 2001 From: Ivan Tham Date: Wed, 28 Sep 2022 23:03:33 +0800 Subject: [PATCH] Find file (space .) Based on doom emacs find-file but also wanted to show an alternative to file explorer helix-editor#2377. --- Cargo.lock | 12 +- book/src/keymap.md | 1 + helix-term/Cargo.toml | 2 + helix-term/src/commands.rs | 11 + helix-term/src/keymap/default.rs | 1 + helix-term/src/ui/mod.rs | 7 +- helix-term/src/ui/picker.rs | 525 ++++++++++++++++++++++++++++--- helix-term/src/ui/prompt.rs | 17 +- 8 files changed, 524 insertions(+), 52 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e5edcaac602f..cac7e670ffbc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -482,7 +482,9 @@ dependencies = [ "helix-view", "ignore", "indoc", + "libc", "log", + "number_prefix", "once_cell", "pulldown-cmark", "serde", @@ -626,9 +628,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.132" +version = "0.2.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" +checksum = "c0f80d65747a3e43d1596c7c5492d95d5edddaabd45a7fcdb02b95f644164966" [[package]] name = "libloading" @@ -728,6 +730,12 @@ dependencies = [ "libc", ] +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + [[package]] name = "once_cell" version = "1.15.0" diff --git a/book/src/keymap.md b/book/src/keymap.md index 6d90d802d41d..1d2c377f4822 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -258,6 +258,7 @@ This layer is a kludge of mappings, mostly pickers. | ----- | ----------- | ------- | | `f` | Open file picker | `file_picker` | | `F` | Open file picker at current working directory | `file_picker_in_current_directory` | +| `.` | Open find file picker | `find_file_picker` | | `b` | Open buffer picker | `buffer_picker` | | `j` | Open jumplist picker | `jumplist_picker` | | `k` | Show documentation for item under cursor in a [popup](#popup) (**LSP**) | `hover` | diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index ac50b610709e..1fa819e393bd 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -66,6 +66,8 @@ serde = { version = "1.0", features = ["derive"] } # ripgrep for global search grep-regex = "0.1.10" grep-searcher = "0.1.10" +libc = "0.2.133" +number_prefix = "0.4.0" [target.'cfg(not(windows))'.dependencies] # https://github.com/vorner/signal-hook/issues/100 signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] } diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index c87ad0ca229c..590e096684a0 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -264,6 +264,7 @@ impl MappableCommand { command_mode, "Enter command mode", file_picker, "Open file picker", file_picker_in_current_directory, "Open file picker at current working directory", + find_file_picker, "Open find file picker", code_action, "Perform code action", buffer_picker, "Open buffer picker", jumplist_picker, "Open jumplist picker", @@ -2238,6 +2239,16 @@ fn file_picker_in_current_directory(cx: &mut Context) { cx.push_layer(Box::new(overlayed(picker))); } +fn find_file_picker(cx: &mut Context) { + let doc = doc!(cx.editor); + let doc_parent = doc.path().and_then(|p| p.parent()).map(|p| p.to_owned()); + let cwd = doc_parent + .or_else(|| std::env::current_dir().ok()) + .unwrap_or_else(|| PathBuf::from("./")); + let picker = ui::find_file_picker(cwd); + cx.push_layer(Box::new(overlayed(picker))); +} + fn buffer_picker(cx: &mut Context) { let current = view!(cx.editor).doc; diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs index f07d4028ccde..c710c756f269 100644 --- a/helix-term/src/keymap/default.rs +++ b/helix-term/src/keymap/default.rs @@ -204,6 +204,7 @@ pub fn default() -> HashMap { "space" => { "Space" "f" => file_picker, "F" => file_picker_in_current_directory, + "." => find_file_picker, "b" => buffer_picker, "j" => jumplist_picker, "s" => symbol_picker, diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 60ad3b2478cc..9da30bfb6579 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -16,7 +16,7 @@ pub use completion::Completion; pub use editor::EditorView; pub use markdown::Markdown; pub use menu::Menu; -pub use picker::{FileLocation, FilePicker, Picker}; +pub use picker::{FileLocation, FilePicker, FindFilePicker, Picker}; pub use popup::Popup; pub use prompt::{Prompt, PromptEvent}; pub use spinner::{ProgressSpinners, Spinner}; @@ -200,6 +200,11 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi ) } +/// Based on find-file on doom emacs (SPC . or SPC f f). +pub fn find_file_picker(dir: PathBuf) -> FindFilePicker { + FindFilePicker::new(dir) +} + pub mod completers { use crate::ui::prompt::Completion; use fuzzy_matcher::skim::SkimMatcherV2 as Matcher; diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index a56455d7d569..54e7ee15d88f 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -12,15 +12,18 @@ use fuzzy_matcher::skim::SkimMatcherV2 as Matcher; use fuzzy_matcher::FuzzyMatcher; use tui::widgets::Widget; +use std::borrow::Cow; use std::time::Instant; use std::{ cmp::Reverse, collections::HashMap, + fs, io::Read, path::{Path, PathBuf}, }; -use crate::ui::{Prompt, PromptEvent}; +use super::menu::Item; +use crate::ui::{overlay, Prompt, PromptEvent}; use helix_core::{movement::Direction, Position}; use helix_view::{ editor::Action, @@ -28,29 +31,273 @@ use helix_view::{ Document, Editor, }; -use super::menu::Item; +// based on exa but not sure where to put this +/// More readable aliases for the permission bits exposed by libc. +#[cfg(unix)] +mod modes { + // The `libc::mode_t` type’s actual type varies, but the value returned + // from `metadata.permissions().mode()` is always `u32`. + pub type Mode = u32; + + pub const USER_READ: Mode = libc::S_IRUSR as Mode; + pub const USER_WRITE: Mode = libc::S_IWUSR as Mode; + pub const USER_EXECUTE: Mode = libc::S_IXUSR as Mode; + + pub const GROUP_READ: Mode = libc::S_IRGRP as Mode; + pub const GROUP_WRITE: Mode = libc::S_IWGRP as Mode; + pub const GROUP_EXECUTE: Mode = libc::S_IXGRP as Mode; + + pub const OTHER_READ: Mode = libc::S_IROTH as Mode; + pub const OTHER_WRITE: Mode = libc::S_IWOTH as Mode; + pub const OTHER_EXECUTE: Mode = libc::S_IXOTH as Mode; + + pub const STICKY: Mode = libc::S_ISVTX as Mode; + pub const SETGID: Mode = libc::S_ISGID as Mode; + pub const SETUID: Mode = libc::S_ISUID as Mode; +} -pub const MIN_AREA_WIDTH_FOR_PREVIEW: u16 = 72; -/// Biggest file size to preview in bytes -pub const MAX_FILE_SIZE_FOR_PREVIEW: u64 = 10 * 1024 * 1024; +#[cfg(not(unix))] +mod modes {} + +// based on exa but not sure where to put this +mod fields { + use super::modes; + use std::fmt; + use std::fs::Metadata; + #[cfg(unix)] + use std::os::unix::fs::{FileTypeExt, MetadataExt}; + + /// The file’s base type, which gets displayed in the very first column of the + /// details output. + /// + /// This type is set entirely by the filesystem, rather than relying on a + /// file’s contents. So “link” is a type, but “image” is just a type of + /// regular file. (See the `filetype` module for those checks.) + /// + /// Its ordering is used when sorting by type. + #[derive(PartialEq, Eq, PartialOrd, Ord, Copy, Clone)] + pub enum Type { + Directory, + File, + Link, + Pipe, + Socket, + CharDevice, + BlockDevice, + Special, + } -/// File path and range of lines (used to align and highlight lines) -pub type FileLocation = (PathBuf, Option<(usize, usize)>); + #[cfg(unix)] + pub fn filetype(metadata: &Metadata) -> Type { + let filetype = metadata.file_type(); + if metadata.is_file() { + Type::File + } else if metadata.is_dir() { + Type::Directory + } else if filetype.is_fifo() { + Type::Pipe + } else if filetype.is_symlink() { + Type::Link + } else if filetype.is_char_device() { + Type::CharDevice + } else if filetype.is_block_device() { + Type::BlockDevice + } else if filetype.is_socket() { + Type::Socket + } else { + Type::Special + } + } -pub struct FilePicker { - picker: Picker, - pub truncate_start: bool, - /// Caches paths to documents - preview_cache: HashMap, - read_buffer: Vec, - /// Given an item in the picker, return the file path and line number to display. - file_fn: Box Option>, + #[cfg(not(unix))] + pub fn filetype(metadata: &Metadata) -> Type { + unreachable!() + } + + impl fmt::Display for Type { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}", + match self { + Type::Directory => 'd', + Type::File => '.', + Type::Link => 'l', + Type::Pipe => '|', + Type::Socket => 's', + Type::CharDevice => 'c', + Type::BlockDevice => 'b', + Type::Special => '?', + } + ) + } + } + + /// The file’s Unix permission bitfield, with one entry per bit. + #[derive(Copy, Clone)] + pub struct Permissions { + pub user_read: bool, + pub user_write: bool, + pub user_execute: bool, + + pub group_read: bool, + pub group_write: bool, + pub group_execute: bool, + + pub other_read: bool, + pub other_write: bool, + pub other_execute: bool, + + pub sticky: bool, + pub setgid: bool, + pub setuid: bool, + } + + #[cfg(unix)] + pub fn permissions(metadata: &Metadata) -> Permissions { + let bits = metadata.mode(); + let has_bit = |bit| bits & bit == bit; + Permissions { + user_read: has_bit(modes::USER_READ), + user_write: has_bit(modes::USER_WRITE), + user_execute: has_bit(modes::USER_EXECUTE), + + group_read: has_bit(modes::GROUP_READ), + group_write: has_bit(modes::GROUP_WRITE), + group_execute: has_bit(modes::GROUP_EXECUTE), + + other_read: has_bit(modes::OTHER_READ), + other_write: has_bit(modes::OTHER_WRITE), + other_execute: has_bit(modes::OTHER_EXECUTE), + + sticky: has_bit(modes::STICKY), + setgid: has_bit(modes::SETGID), + setuid: has_bit(modes::SETUID), + } + } + + #[cfg(not(unix))] + pub fn permissions(metadata: &Metadata) -> Permissions { + unreachable!() + } + + impl fmt::Display for Permissions { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let bit = |bit, char| if bit { char } else { "-" }; + write!( + f, + "{}{}{}{}{}{}{}{}{}", + bit(self.user_read, "r"), + bit(self.user_write, "w"), + bit(self.user_execute, "x"), + bit(self.group_read, "r"), + bit(self.group_write, "w"), + bit(self.group_execute, "x"), + bit(self.other_read, "r"), + bit(self.other_write, "w"), + bit(self.other_execute, "x"), + ) + } + } + + /// A file’s size, in bytes. This is usually formatted by the `number_prefix` + /// crate into something human-readable. + #[derive(Copy, Clone)] + pub enum Size { + /// This file has a defined size. + Some(u64), + + /// This file has no size, or has a size but we aren’t interested in it. + /// + /// Under Unix, directory entries that aren’t regular files will still + /// have a file size. For example, a directory will just contain a list of + /// its files as its “contents” and will be specially flagged as being a + /// directory, rather than a file. However, seeing the “file size” of this + /// data is rarely useful — I can’t think of a time when I’ve seen it and + /// learnt something. So we discard it and just output “-” instead. + /// + /// See this answer for more: http://unix.stackexchange.com/a/68266 + None, + + /// This file is a block or character device, so instead of a size, print + /// out the file’s major and minor device IDs. + /// + /// This is what ls does as well. Without it, the devices will just have + /// file sizes of zero. + DeviceIDs(DeviceIDs), + } + + /// The major and minor device IDs that gets displayed for device files. + /// + /// You can see what these device numbers mean: + /// - + /// - + #[derive(Copy, Clone)] + pub struct DeviceIDs { + pub major: u8, + pub minor: u8, + } + + /// This file’s size, if it’s a regular file. + /// + /// For directories, no size is given. Although they do have a size on + /// some filesystems, I’ve never looked at one of those numbers and gained + /// any information from it. So it’s going to be hidden instead. + /// + /// Block and character devices return their device IDs, because they + /// usually just have a file size of zero. + #[cfg(unix)] + pub fn size(metadata: &Metadata) -> Size { + let filetype = metadata.file_type(); + if metadata.is_dir() { + Size::None + } else if filetype.is_char_device() || filetype.is_block_device() { + let device_ids = metadata.rdev().to_be_bytes(); + + // In C-land, getting the major and minor device IDs is done with + // preprocessor macros called `major` and `minor` that depend on + // the size of `dev_t`, but we just take the second-to-last and + // last bytes. + Size::DeviceIDs(DeviceIDs { + major: device_ids[6], + minor: device_ids[7], + }) + } else { + Size::Some(metadata.len()) + } + } + + #[cfg(not(unix))] + pub fn size(metadata: &Metadata) -> Size { + unreachable!() + } + + impl fmt::Display for Size { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use number_prefix::NumberPrefix; + match self { + Size::Some(s) => match NumberPrefix::decimal(*s as f64) { + NumberPrefix::Standalone(n) => write!(f, "{}", n), + NumberPrefix::Prefixed(p, n) => write!(f, "{:.1}{}", n, p), + }, + Size::None => write!(f, "-"), + Size::DeviceIDs(DeviceIDs { major, minor }) => { + write!(f, "{},{}", major, minor) + } + } + } + } } +pub const MIN_AREA_WIDTH_FOR_PREVIEW: u16 = 72; +/// Biggest file size to preview in bytes +pub const MAX_FILE_SIZE_FOR_PREVIEW: u64 = 10 * 1024 * 1024; + pub enum CachedPreview { Document(Box), Binary, LargeFile, + Directory(Vec), NotFound, } @@ -70,6 +317,13 @@ impl Preview<'_, '_> { } } + fn directory(&self) -> Option<&Vec> { + match self { + Preview::Cached(CachedPreview::Directory(preview)) => Some(preview), + _ => None, + } + } + /// Alternate text to show for the preview. fn placeholder(&self) -> &str { match *self { @@ -78,12 +332,151 @@ impl Preview<'_, '_> { CachedPreview::Document(_) => "", CachedPreview::Binary => "", CachedPreview::LargeFile => "", + CachedPreview::Directory(_) => "", CachedPreview::NotFound => "", }, } } } +/// File path and range of lines (used to align and highlight lines) +pub type FileLocation = (PathBuf, Option<(usize, usize)>); + +// Specialized version of [`FilePicker`] with some custom support to allow directory navigation. +pub struct FindFilePicker { + picker: FilePicker, + dir: PathBuf, +} + +impl FindFilePicker { + pub fn new(dir: PathBuf) -> FindFilePicker { + // switch to Result::flatten later + let files: Vec<_> = match fs::read_dir(&dir) { + Ok(dir) => dir + .flat_map(|entry| entry.map(|entry| entry.path())) + .collect(), + Err(_) => Vec::new(), + }; + let dir1 = dir.clone(); + let mut picker = FilePicker::new( + files, + dir1, + // TODO: add format_fn + // TODO: prevent this from running within score function that skews + // the score, and only calculate it once during initialization + // move |path| { + // let suffix = if path.is_dir() { "/" } else { "" }; + // let metadata = path.symlink_metadata().unwrap(); + // let path = path.strip_prefix(&dir1).unwrap_or(path).to_string_lossy(); + // if cfg!(unix) { + // let filetype = fields::filetype(&metadata); + // let permissions = fields::permissions(&metadata); + // let size = format!("{}", fields::size(&metadata)); + // Cow::Owned(format!( + // "{:<22} {}{} {:>6}", + // path + suffix, // TODO this should check for size and handle truncation + // filetype, + // permissions, + // size, + // // TODO add absolute/relative time? may need to handle truncation + // )) + // } else { + // path + suffix + // } + // }, + |_cx, _path, _action| {}, // we use custom callback_fn + |_editor, path| Some((path.clone(), None)), + ); + // TODO: truncate prompt dir if prompt area too small and current dir too long + let current_dir = std::env::current_dir().expect("couldn't determine current directory"); + let dir1 = dir.clone(); + let prompt = dir1 + .strip_prefix(current_dir) + .unwrap_or(&dir1) + .to_string_lossy() + .into_owned(); + let prompt = match prompt.as_str() { + "/" => "/".to_owned(), + "" => "./".to_owned(), + _ => prompt + "/", + }; + *picker.picker.prompt.prompt_mut() = Cow::Owned(prompt); + picker.picker.prompt.prompt_style_fn = Box::new(|theme| Some(theme.get("blue"))); + FindFilePicker { picker, dir } + } +} + +impl Component for FindFilePicker { + fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { + self.picker.render(area, surface, cx); + } + + fn handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult { + let key = match event { + Event::Key(key) => *key, + // TODO we may want to handle mouse event + Event::Mouse(_) => return EventResult::Ignored(None), + Event::Paste(_) | Event::Resize(_, _) | Event::FocusGained | Event::FocusLost => { + return EventResult::Ignored(None) + } + }; + + let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _| { + // remove the layer + compositor.last_picker = compositor.pop(); + }))); + + let findfile_fn = |path: &Path| { + let picker = FindFilePicker::new(path.to_path_buf()); + EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _| { + // remove the layer + compositor.last_picker = compositor.pop(); + compositor.push(Box::new(overlay::overlayed(picker))); + }))) + }; + + // different from FilePicker callback_fn as in second option is an + // Option and returns EventResult + let callback_fn = + move |cx: &mut Context, picker: &FilePicker, dir: &PathBuf, action| { + if let Some(path) = picker.picker.selection() { + if path.is_dir() { + return findfile_fn(path); + } else { + cx.editor.open(path, action).expect("editor.open failed"); + } + } else { + let filename = picker.picker.prompt.line(); + cx.editor + .open(&dir.join(filename), action) + .expect("editor.open failed"); + } + close_fn + }; + + match key { + ctrl!('h') | key!(Backspace) if self.picker.picker.prompt.line().is_empty() => { + let parent = self.dir.parent().unwrap_or(&self.dir); + findfile_fn(parent) + } + key!(Enter) => (callback_fn)(cx, &self.picker, &self.dir, Action::Replace), + ctrl!('s') => (callback_fn)(cx, &self.picker, &self.dir, Action::HorizontalSplit), + ctrl!('v') => (callback_fn)(cx, &self.picker, &self.dir, Action::VerticalSplit), + _ => self.picker.handle_event(event, cx), + } + } +} + +pub struct FilePicker { + picker: Picker, + pub truncate_start: bool, + /// Caches paths to documents + preview_cache: HashMap, + read_buffer: Vec, + /// Given an item in the picker, return the file path and line number to display. + file_fn: Box Option>, +} + impl FilePicker { pub fn new( options: Vec, @@ -136,31 +529,70 @@ impl FilePicker { return Preview::Cached(&self.preview_cache[path]); } - let data = std::fs::File::open(path).and_then(|file| { - let metadata = file.metadata()?; - // Read up to 1kb to detect the content type - let n = file.take(1024).read_to_end(&mut self.read_buffer)?; - let content_type = content_inspector::inspect(&self.read_buffer[..n]); - self.read_buffer.clear(); - Ok((metadata, content_type)) - }); - let preview = data - .map( - |(metadata, content_type)| match (metadata.len(), content_type) { - (_, content_inspector::ContentType::BINARY) => CachedPreview::Binary, - (size, _) if size > MAX_FILE_SIZE_FOR_PREVIEW => CachedPreview::LargeFile, - _ => { - // TODO: enable syntax highlighting; blocked by async rendering - Document::open(path, None, None) - .map(|doc| CachedPreview::Document(Box::new(doc))) - .unwrap_or(CachedPreview::NotFound) - } - }, - ) + let preview = std::fs::File::open(path) + .and_then(|file| { + let metadata = file.metadata()?; + if metadata.is_file() { + // Read up to 1kb to detect the content type + let n = file.take(1024).read_to_end(&mut self.read_buffer)?; + let content_type = content_inspector::inspect(&self.read_buffer[..n]); + self.read_buffer.clear(); + Ok(match (metadata.len(), content_type) { + (_, content_inspector::ContentType::BINARY) => CachedPreview::Binary, + (size, _) if size > MAX_FILE_SIZE_FOR_PREVIEW => CachedPreview::LargeFile, + _ => { + // TODO: enable syntax highlighting; blocked by async rendering + Document::open(path, None, None) + .map(|doc| CachedPreview::Document(Box::new(doc))) + .unwrap_or(CachedPreview::NotFound) + } + }) + } else { + // metadata.is_dir + Ok(CachedPreview::Directory(self.preview_dir(path))) + } + }) .unwrap_or(CachedPreview::NotFound); self.preview_cache.insert(path.to_owned(), preview); Preview::Cached(&self.preview_cache[path]) } + + fn preview_dir(&self, dir: &Path) -> Vec { + // TODO refactor this with directory list to directory.rs in view later + match fs::read_dir(&dir) { + Ok(entries) => entries + .flat_map(|res| { + res.map(|entry| { + let path = entry.path(); + let suffix = if path.is_dir() { "/" } else { "" }; + let metadata = fs::metadata(&*path); + let path = path.strip_prefix(&dir).unwrap_or(&path).to_string_lossy(); + if cfg!(unix) { + if let Ok(metadata) = metadata { + let filetype = fields::filetype(&metadata); + let permissions = fields::permissions(&metadata); + let size = format!("{}", fields::size(&metadata)); + format!( + "{:<22} {}{} {:>6}", + path + suffix, // TODO this should check for size and handle truncation + filetype, + permissions, + size, + // TODO add absolute/relative time? may need to handle truncation + ) + } else { + // metadata fails in cases like broken soft link + (path + suffix).to_string() + } + } else { + (path + suffix).to_string() + } + }) + }) + .collect(), + Err(_) => vec!["".to_owned()], + } + } } impl Component for FilePicker { @@ -206,15 +638,20 @@ impl Component for FilePicker { if let Some((path, range)) = self.current_file(cx.editor) { let preview = self.get_preview(&path, cx.editor); - let doc = match preview.document() { - Some(doc) => doc, - None => { - let alt_text = preview.placeholder(); - let x = inner.x + inner.width.saturating_sub(alt_text.len() as u16) / 2; - let y = inner.y + inner.height / 2; - surface.set_stringn(x, y, alt_text, inner.width as usize, text); - return; + let doc = if let Some(doc) = preview.document() { + doc + } else if let Some(output) = preview.directory() { + for (n, line) in output.iter().take(inner.height as usize).enumerate() { + let y = inner.y + n as u16; + surface.set_stringn(inner.x, y, line, inner.width as usize, text); } + return; + } else { + let alt_text = preview.placeholder(); + let x = inner.x + inner.width.saturating_sub(alt_text.len() as u16) / 2; + let y = inner.y + inner.height / 2; + surface.set_stringn(x, y, alt_text, inner.width as usize, text); + return; }; // align to middle diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index db3bd62d7589..34c3f2569bad 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -10,8 +10,8 @@ use helix_core::{ unicode::segmentation::GraphemeCursor, unicode::width::UnicodeWidthStr, Position, }; use helix_view::{ - graphics::{CursorKind, Margin, Rect}, - Editor, + graphics::{CursorKind, Margin, Rect, Style}, + Editor, Theme, }; pub type Completion = (RangeFrom, Cow<'static, str>); @@ -28,6 +28,7 @@ pub struct Prompt { completion_fn: Box Vec>, callback_fn: Box, pub doc_fn: Box Option>>, + pub prompt_style_fn: Box Option