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.rs | 324 |
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) } |