Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-term/src/ui/picker.rs')
-rw-r--r--helix-term/src/ui/picker.rs324
1 files changed, 112 insertions, 212 deletions
diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs
index 4f77f8b9..07901239 100644
--- a/helix-term/src/ui/picker.rs
+++ b/helix-term/src/ui/picker.rs
@@ -7,9 +7,8 @@ use crate::{
ctrl, key, shift,
ui::{
self,
- document::{render_document, LinePos, TextRenderer},
+ document::{render_document, LineDecoration, LinePos, TextRenderer},
picker::query::PickerQuery,
- text_decorations::DecorationManager,
EditorView,
},
};
@@ -32,7 +31,7 @@ use std::{
borrow::Cow,
collections::HashMap,
io::Read,
- path::Path,
+ path::{Path, PathBuf},
sync::{
atomic::{self, AtomicUsize},
Arc,
@@ -52,7 +51,7 @@ use helix_view::{
Document, DocumentId, Editor,
};
-use self::handlers::{DynamicQueryChange, DynamicQueryHandler, PreviewHighlightHandler};
+use self::handlers::{DynamicQueryHandler, PreviewHighlightHandler};
pub const ID: &str = "picker";
@@ -63,16 +62,30 @@ pub const MAX_FILE_SIZE_FOR_PREVIEW: u64 = 10 * 1024 * 1024;
#[derive(PartialEq, Eq, Hash)]
pub enum PathOrId<'a> {
Id(DocumentId),
- Path(&'a Path),
+ // See [PathOrId::from_path_buf]: this will eventually become `Path(&Path)`.
+ Path(Cow<'a, Path>),
+}
+
+impl<'a> PathOrId<'a> {
+ /// Creates a [PathOrId] from a PathBuf
+ ///
+ /// # Deprecated
+ /// The owned version of PathOrId will be removed in a future refactor
+ /// and replaced with `&'a Path`. See the caller of this function for
+ /// more details on its removal.
+ #[deprecated]
+ pub fn from_path_buf(path_buf: PathBuf) -> Self {
+ Self::Path(Cow::Owned(path_buf))
+ }
}
impl<'a> From<&'a Path> for PathOrId<'a> {
fn from(path: &'a Path) -> Self {
- Self::Path(path)
+ Self::Path(Cow::Borrowed(path))
}
}
-impl From<DocumentId> for PathOrId<'_> {
+impl<'a> From<DocumentId> for PathOrId<'a> {
fn from(v: DocumentId) -> Self {
Self::Id(v)
}
@@ -85,7 +98,6 @@ pub type FileLocation<'a> = (PathOrId<'a>, Option<(usize, usize)>);
pub enum CachedPreview {
Document(Box<Document>),
- Directory(Vec<(String, bool)>),
Binary,
LargeFile,
NotFound,
@@ -107,20 +119,12 @@ impl Preview<'_, '_> {
}
}
- fn dir_content(&self) -> Option<&Vec<(String, bool)>> {
- match self {
- Preview::Cached(CachedPreview::Directory(dir_content)) => Some(dir_content),
- _ => None,
- }
- }
-
/// Alternate text to show for the preview.
fn placeholder(&self) -> &str {
match *self {
Self::EditorDocument(_) => "<Invalid file location>",
Self::Cached(preview) => match preview {
CachedPreview::Document(_) => "<Invalid file location>",
- CachedPreview::Directory(_) => "<Invalid directory location>",
CachedPreview::Binary => "<Binary file>",
CachedPreview::LargeFile => "<File too large to preview>",
CachedPreview::NotFound => "<File not found>",
@@ -258,7 +262,6 @@ pub struct Picker<T: 'static + Send + Sync, D: 'static> {
widths: Vec<Constraint>,
callback_fn: PickerCallback<T>,
- default_action: Action,
pub truncate_start: bool,
/// Caches paths to documents
@@ -268,7 +271,7 @@ pub struct Picker<T: 'static + Send + Sync, D: 'static> {
file_fn: Option<FileCallback<T>>,
/// An event handler for syntax highlighting the currently previewed file.
preview_highlight_handler: Sender<Arc<Path>>,
- dynamic_query_handler: Option<Sender<DynamicQueryChange>>,
+ dynamic_query_handler: Option<Sender<Arc<str>>>,
}
impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
@@ -309,10 +312,7 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
F: Fn(&mut Context, &T, Action) + 'static,
{
let columns: Arc<[_]> = columns.into_iter().collect();
- let matcher_columns = columns
- .iter()
- .filter(|col: &&Column<T, D>| col.filter)
- .count() as u32;
+ let matcher_columns = columns.iter().filter(|col| col.filter).count() as u32;
assert!(matcher_columns > 0);
let matcher = Nucleo::new(
Config::DEFAULT,
@@ -386,7 +386,6 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
truncate_start: true,
show_preview: true,
callback_fn: Box::new(callback_fn),
- default_action: Action::Replace,
completion_height: 0,
widths,
preview_cache: HashMap::new(),
@@ -429,32 +428,17 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
self
}
- pub fn with_initial_cursor(mut self, cursor: u32) -> Self {
- self.cursor = cursor;
- self
- }
-
pub fn with_dynamic_query(
mut self,
callback: DynQueryCallback<T, D>,
debounce_ms: Option<u64>,
) -> Self {
let handler = DynamicQueryHandler::new(callback, debounce_ms).spawn();
- let event = DynamicQueryChange {
- query: self.primary_query(),
- // Treat the initial query as a paste.
- is_paste: true,
- };
- helix_event::send_blocking(&handler, event);
+ helix_event::send_blocking(&handler, self.primary_query());
self.dynamic_query_handler = Some(handler);
self
}
- pub fn with_default_action(mut self, action: Action) -> Self {
- self.default_action = action;
- self
- }
-
/// Move the cursor by a number of lines, either down (`Forward`) or up (`Backward`)
pub fn move_by(&mut self, amount: u32, direction: Direction) {
let len = self.matcher.snapshot().matched_item_count();
@@ -526,12 +510,12 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
fn prompt_handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult {
if let EventResult::Consumed(_) = self.prompt.handle_event(event, cx) {
- self.handle_prompt_change(matches!(event, Event::Paste(_)));
+ self.handle_prompt_change();
}
EventResult::Consumed(None)
}
- fn handle_prompt_change(&mut self, is_paste: bool) {
+ fn handle_prompt_change(&mut self) {
// TODO: better track how the pattern has changed
let line = self.prompt.line();
let old_query = self.query.parse(line);
@@ -572,11 +556,7 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
// If this is a dynamic picker, notify the query hook that the primary
// query might have been updated.
if let Some(handler) = &self.dynamic_query_handler {
- let event = DynamicQueryChange {
- query: self.primary_query(),
- is_paste,
- };
- helix_event::send_blocking(handler, event);
+ helix_event::send_blocking(handler, self.primary_query());
}
}
@@ -591,6 +571,7 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
match path_or_id {
PathOrId::Path(path) => {
+ let path = path.as_ref();
if let Some(doc) = editor.document_by_path(path) {
return Some((Preview::EditorDocument(doc), range));
}
@@ -600,76 +581,41 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
// retrieve the `Arc<Path>` key. The `path` in scope here is a `&Path` and
// we can cheaply clone the key for the preview highlight handler.
let (path, preview) = self.preview_cache.get_key_value(path).unwrap();
- if matches!(preview, CachedPreview::Document(doc) if doc.syntax().is_none()) {
+ if matches!(preview, CachedPreview::Document(doc) if doc.language_config().is_none())
+ {
helix_event::send_blocking(&self.preview_highlight_handler, path.clone());
}
return Some((Preview::Cached(preview), range));
}
let path: Arc<Path> = path.into();
- let preview = std::fs::metadata(&path)
- .and_then(|metadata| {
- if metadata.is_dir() {
- let files = super::directory_content(&path, editor)?;
- let file_names: Vec<_> = files
- .iter()
- .filter_map(|(file_path, is_dir)| {
- let name = file_path
- .strip_prefix(&path)
- .map(|p| Some(p.as_os_str()))
- .unwrap_or_else(|_| file_path.file_name())?
- .to_string_lossy();
- if *is_dir {
- Some((format!("{}/", name), true))
- } else {
- Some((name.into_owned(), false))
- }
- })
- .collect();
- Ok(CachedPreview::Directory(file_names))
- } else if metadata.is_file() {
- if metadata.len() > MAX_FILE_SIZE_FOR_PREVIEW {
- return Ok(CachedPreview::LargeFile);
- }
- let content_type = std::fs::File::open(&path).and_then(|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(content_type)
- })?;
- if content_type.is_binary() {
- return Ok(CachedPreview::Binary);
+ 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
}
- let mut doc = Document::open(
- &path,
- None,
- false,
- editor.config.clone(),
- editor.syn_loader.clone(),
- )
- .or(Err(std::io::Error::new(
- std::io::ErrorKind::NotFound,
- "Cannot open document",
- )))?;
- let loader = editor.syn_loader.load();
- if let Some(language_config) = doc.detect_language_config(&loader) {
- doc.language = Some(language_config);
- // Asynchronously highlight the new document
- helix_event::send_blocking(
- &self.preview_highlight_handler,
- path.clone(),
- );
- }
- Ok(CachedPreview::Document(Box::new(doc)))
- } else {
- Err(std::io::Error::new(
- std::io::ErrorKind::NotFound,
- "Neither a dir, nor a file",
- ))
- }
- })
+ _ => Document::open(&path, None, None, editor.config.clone())
+ .map(|doc| {
+ // Asynchronously highlight the new document
+ helix_event::send_blocking(
+ &self.preview_highlight_handler,
+ path.clone(),
+ );
+ CachedPreview::Document(Box::new(doc))
+ })
+ .unwrap_or(CachedPreview::NotFound),
+ },
+ )
.unwrap_or(CachedPreview::NotFound);
self.preview_cache.insert(path.clone(), preview);
Some((Preview::Cached(&self.preview_cache[&path]), range))
@@ -708,6 +654,10 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
// -- Render the input bar:
+ let area = inner.clip_left(1).with_height(1);
+ // render the prompt first since it will clear its background
+ self.prompt.render(area, surface, cx);
+
let count = format!(
"{}{}/{}",
if status.running || self.matcher.active_injectors() > 0 {
@@ -718,13 +668,6 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
snapshot.matched_item_count(),
snapshot.item_count(),
);
-
- let area = inner.clip_left(1).with_height(1);
- let line_area = area.clip_right(count.len() as u16 + 1);
-
- // render the prompt first since it will clear its background
- self.prompt.render(line_area, surface, cx);
-
surface.set_stringn(
(area.x + area.width).saturating_sub(count.len() as u16 + 1),
area.y,
@@ -846,25 +789,21 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
if self.columns.len() > 1 {
let active_column = self.query.active_column(self.prompt.position());
let header_style = cx.editor.theme.get("ui.picker.header");
- let header_column_style = cx.editor.theme.get("ui.picker.header.column");
- table = table.header(
- Row::new(self.columns.iter().map(|column| {
- if column.hidden {
- Cell::default()
+ table = table.header(Row::new(self.columns.iter().map(|column| {
+ if column.hidden {
+ Cell::default()
+ } else {
+ let style = if active_column.is_some_and(|name| Arc::ptr_eq(name, &column.name))
+ {
+ cx.editor.theme.get("ui.picker.header.active")
} else {
- let style =
- if active_column.is_some_and(|name| Arc::ptr_eq(name, &column.name)) {
- cx.editor.theme.get("ui.picker.header.column.active")
- } else {
- header_column_style
- };
+ header_style
+ };
- Cell::from(Span::styled(Cow::from(&*column.name), style))
- }
- }))
- .style(header_style),
- );
+ Cell::from(Span::styled(Cow::from(&*column.name), style))
+ }
+ })));
}
use tui::widgets::TableState;
@@ -885,7 +824,6 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
// clear area
let background = cx.editor.theme.get("ui.background");
let text = cx.editor.theme.get("ui.text");
- let directory = cx.editor.theme.get("ui.text.directory");
surface.clear_with(area, background);
const BLOCK: Block<'_> = Block::bordered();
@@ -900,29 +838,13 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
if let Some((preview, range)) = self.get_preview(cx.editor) {
let doc = match preview.document() {
Some(doc)
- if range.is_none_or(|(start, end)| {
+ if range.map_or(true, |(start, end)| {
start <= end && end <= doc.text().len_lines()
}) =>
{
doc
}
_ => {
- if let Some(dir_content) = preview.dir_content() {
- for (i, (path, is_dir)) in
- dir_content.iter().take(inner.height as usize).enumerate()
- {
- let style = if *is_dir { directory } else { text };
- surface.set_stringn(
- inner.x,
- inner.y + i as u16,
- path,
- inner.width as usize,
- style,
- );
- }
- return;
- }
-
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;
@@ -958,19 +880,22 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
}
}
- let loader = cx.editor.syn_loader.load();
-
- let syntax_highlighter =
- EditorView::doc_syntax_highlighter(doc, offset.anchor, area.height, &loader);
- let mut overlay_highlights = Vec::new();
-
- EditorView::doc_diagnostics_highlights_into(
+ let syntax_highlights = EditorView::doc_syntax_highlights(
doc,
+ offset.anchor,
+ area.height,
&cx.editor.theme,
- &mut overlay_highlights,
);
- let mut decorations = DecorationManager::default();
+ let mut overlay_highlights =
+ EditorView::empty_highlight_iter(doc, offset.anchor, area.height);
+ for spans in EditorView::doc_diagnostics_highlights(doc, &cx.editor.theme) {
+ if spans.is_empty() {
+ continue;
+ }
+ overlay_highlights = Box::new(helix_core::syntax::merge(overlay_highlights, spans));
+ }
+ let mut decorations: Vec<Box<dyn LineDecoration>> = Vec::new();
if let Some((start, end)) = range {
let style = cx
@@ -982,14 +907,14 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
if (start..=end).contains(&pos.doc_line) {
let area = Rect::new(
renderer.viewport.x,
- pos.visual_line,
+ renderer.viewport.y + pos.visual_line,
renderer.viewport.width,
1,
);
- renderer.set_style(area, style)
+ renderer.surface.set_style(area, style)
}
};
- decorations.add_decoration(draw_highlight);
+ decorations.push(Box::new(draw_highlight))
}
render_document(
@@ -999,10 +924,11 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
offset,
// TODO: compute text annotations asynchronously here (like inlay hints)
&TextAnnotations::default(),
- syntax_highlighter,
+ syntax_highlights,
overlay_highlights,
&cx.editor.theme,
- decorations,
+ &mut decorations,
+ &mut [],
);
}
}
@@ -1048,23 +974,23 @@ impl<I: 'static + Send + Sync, D: 'static + Send + Sync> Component for Picker<I,
let close_fn = |picker: &mut Self| {
// if the picker is very large don't store it as last_picker to avoid
// excessive memory consumption
- let callback: compositor::Callback =
- if picker.matcher.snapshot().item_count() > 1_000_000 {
- Box::new(|compositor: &mut Compositor, _ctx| {
- // remove the layer
- compositor.pop();
- })
- } else {
- // stop streaming in new items in the background, really we should
- // be restarting the stream somehow once the picker gets
- // reopened instead (like for an FS crawl) that would also remove the
- // need for the special case above but that is pretty tricky
- picker.version.fetch_add(1, atomic::Ordering::Relaxed);
- Box::new(|compositor: &mut Compositor, _ctx| {
- // remove the layer
- compositor.last_picker = compositor.pop();
- })
- };
+ let callback: compositor::Callback = if picker.matcher.snapshot().item_count() > 100_000
+ {
+ Box::new(|compositor: &mut Compositor, _ctx| {
+ // remove the layer
+ compositor.pop();
+ })
+ } else {
+ // stop streaming in new items in the background, really we should
+ // be restarting the stream somehow once the picker gets
+ // reopened instead (like for an FS crawl) that would also remove the
+ // need for the special case above but that is pretty tricky
+ picker.version.fetch_add(1, atomic::Ordering::Relaxed);
+ Box::new(|compositor: &mut Compositor, _ctx| {
+ // remove the layer
+ compositor.last_picker = compositor.pop();
+ })
+ };
EventResult::Consumed(Some(callback))
};
@@ -1090,7 +1016,7 @@ impl<I: 'static + Send + Sync, D: 'static + Send + Sync> Component for Picker<I,
key!(Esc) | ctrl!('c') => return close_fn(self),
alt!(Enter) => {
if let Some(option) = self.selection() {
- (self.callback_fn)(ctx, option, self.default_action);
+ (self.callback_fn)(ctx, option, Action::Load);
}
}
key!(Enter) => {
@@ -1101,29 +1027,11 @@ impl<I: 'static + Send + Sync, D: 'static + Send + Sync> Component for Picker<I,
.first_history_completion(ctx.editor)
.filter(|_| self.prompt.line().is_empty())
{
- // The percent character is used by the query language and needs to be
- // escaped with a backslash.
- let completion = if completion.contains('%') {
- completion.replace('%', "\\%")
- } else {
- completion.into_owned()
- };
- self.prompt.set_line(completion, ctx.editor);
-
- // Inserting from the history register is a paste.
- self.handle_prompt_change(true);
+ self.prompt.set_line(completion.to_string(), ctx.editor);
+ self.handle_prompt_change();
} else {
if let Some(option) = self.selection() {
- (self.callback_fn)(ctx, option, self.default_action);
- }
- if let Some(history_register) = self.prompt.history_register() {
- if let Err(err) = ctx
- .editor
- .registers
- .push(history_register, self.primary_query().to_string())
- {
- ctx.editor.set_error(err.to_string());
- }
+ (self.callback_fn)(ctx, option, Action::Replace);
}
return close_fn(self);
}
@@ -1157,15 +1065,7 @@ impl<I: 'static + Send + Sync, D: 'static + Send + Sync> Component for Picker<I,
let inner = block.inner(area);
// prompt area
- let render_preview =
- self.show_preview && self.file_fn.is_some() && area.width > MIN_AREA_WIDTH_FOR_PREVIEW;
-
- let picker_width = if render_preview {
- area.width / 2
- } else {
- area.width
- };
- let area = inner.clip_left(1).with_height(1).with_width(picker_width);
+ let area = inner.clip_left(1).with_height(1);
self.prompt.cursor(area, editor)
}