aboutsummaryrefslogtreecommitdiff
path: root/helix-term/src/ui
diff options
context:
space:
mode:
Diffstat (limited to 'helix-term/src/ui')
-rw-r--r--helix-term/src/ui/mod.rs30
-rw-r--r--helix-term/src/ui/picker.rs776
2 files changed, 380 insertions, 426 deletions
diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs
index ec328ec5..155f2435 100644
--- a/helix-term/src/ui/mod.rs
+++ b/helix-term/src/ui/mod.rs
@@ -21,7 +21,7 @@ pub use completion::{Completion, CompletionItem};
pub use editor::EditorView;
pub use markdown::Markdown;
pub use menu::Menu;
-pub use picker::{DynamicPicker, FileLocation, FilePicker, Picker};
+pub use picker::{DynamicPicker, FileLocation, Picker};
pub use popup::Popup;
pub use prompt::{Prompt, PromptEvent};
pub use spinner::{ProgressSpinners, Spinner};
@@ -158,7 +158,7 @@ pub fn regex_prompt(
cx.push_layer(Box::new(prompt));
}
-pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePicker<PathBuf> {
+pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> Picker<PathBuf> {
use ignore::{types::TypesBuilder, WalkBuilder};
use std::time::Instant;
@@ -217,21 +217,17 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi
log::debug!("file_picker init {:?}", Instant::now().duration_since(now));
- FilePicker::new(
- files,
- 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)
- } else {
- format!("unable to open \"{}\"", path.display())
- };
- cx.editor.set_error(err);
- }
- },
- |_editor, path| Some((path.clone().into(), None)),
- )
+ Picker::new(files, 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)
+ } else {
+ format!("unable to open \"{}\"", path.display())
+ };
+ cx.editor.set_error(err);
+ }
+ })
+ .with_preview(|_editor, path| Some((path.clone().into(), None)))
}
pub mod completers {
diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs
index d161f786..04ed940c 100644
--- a/helix-term/src/ui/picker.rs
+++ b/helix-term/src/ui/picker.rs
@@ -77,16 +77,6 @@ type FileCallback<T> = Box<dyn Fn(&Editor, &T) -> Option<FileLocation>>;
/// File path and range of lines (used to align and highlight lines)
pub type FileLocation = (PathOrId, Option<(usize, usize)>);
-pub struct FilePicker<T: Item> {
- picker: Picker<T>,
- pub truncate_start: bool,
- /// Caches paths to documents
- preview_cache: HashMap<PathBuf, CachedPreview>,
- read_buffer: Vec<u8>,
- /// Given an item in the picker, return the file path and line number to display.
- file_fn: FileCallback<T>,
-}
-
pub enum CachedPreview {
Document(Box<Document>),
Binary,
@@ -124,325 +114,6 @@ impl Preview<'_, '_> {
}
}
-impl<T: Item + 'static> FilePicker<T> {
- pub fn new(
- options: Vec<T>,
- editor_data: T::Data,
- callback_fn: impl Fn(&mut Context, &T, Action) + 'static,
- preview_fn: impl Fn(&Editor, &T) -> Option<FileLocation> + 'static,
- ) -> Self {
- let truncate_start = true;
- let mut picker = Picker::new(options, editor_data, callback_fn);
- picker.truncate_start = truncate_start;
-
- Self {
- picker,
- truncate_start,
- preview_cache: HashMap::new(),
- read_buffer: Vec::with_capacity(1024),
- file_fn: Box::new(preview_fn),
- }
- }
-
- pub fn truncate_start(mut self, truncate_start: bool) -> Self {
- self.truncate_start = truncate_start;
- self.picker.truncate_start = truncate_start;
- self
- }
-
- fn current_file(&self, editor: &Editor) -> Option<FileLocation> {
- self.picker
- .selection()
- .and_then(|current| (self.file_fn)(editor, current))
- .and_then(|(path_or_id, line)| path_or_id.get_canonicalized().ok().zip(Some(line)))
- }
-
- /// Get (cached) preview for a given path. If a document corresponding
- /// to the path is already open in the editor, it is used instead.
- fn get_preview<'picker, 'editor>(
- &'picker mut self,
- path_or_id: PathOrId,
- editor: &'editor Editor,
- ) -> Preview<'picker, 'editor> {
- match path_or_id {
- PathOrId::Path(path) => {
- let path = &path;
- if let Some(doc) = editor.document_by_path(path) {
- return Preview::EditorDocument(doc);
- }
-
- if self.preview_cache.contains_key(path) {
- 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, editor.config.clone())
- .map(|doc| CachedPreview::Document(Box::new(doc)))
- .unwrap_or(CachedPreview::NotFound)
- }
- },
- )
- .unwrap_or(CachedPreview::NotFound);
- self.preview_cache.insert(path.to_owned(), preview);
- Preview::Cached(&self.preview_cache[path])
- }
- PathOrId::Id(id) => {
- let doc = editor.documents.get(&id).unwrap();
- Preview::EditorDocument(doc)
- }
- }
- }
-
- fn handle_idle_timeout(&mut self, cx: &mut Context) -> EventResult {
- let Some((current_file, _)) = self.current_file(cx.editor) else {
- return EventResult::Consumed(None)
- };
-
- // Try to find a document in the cache
- let doc = match &current_file {
- PathOrId::Id(doc_id) => doc_mut!(cx.editor, doc_id),
- PathOrId::Path(path) => match self.preview_cache.get_mut(path) {
- Some(CachedPreview::Document(ref mut doc)) => doc,
- _ => return EventResult::Consumed(None),
- },
- };
-
- let mut callback: Option<compositor::Callback> = None;
-
- // Then attempt to highlight it if it has no language set
- if doc.language_config().is_none() {
- if let Some(language_config) = doc.detect_language_config(&cx.editor.syn_loader) {
- doc.language = Some(language_config.clone());
- let text = doc.text().clone();
- let loader = cx.editor.syn_loader.clone();
- let job = tokio::task::spawn_blocking(move || {
- let syntax = language_config
- .highlight_config(&loader.scopes())
- .and_then(|highlight_config| Syntax::new(&text, highlight_config, loader));
- let callback = move |editor: &mut Editor, compositor: &mut Compositor| {
- let Some(syntax) = syntax else {
- log::info!("highlighting picker item failed");
- return
- };
- let Some(Overlay { content: picker, .. }) = compositor.find::<Overlay<Self>>() else {
- log::info!("picker closed before syntax highlighting finished");
- return
- };
- // Try to find a document in the cache
- let doc = match current_file {
- PathOrId::Id(doc_id) => doc_mut!(editor, &doc_id),
- PathOrId::Path(path) => match picker.preview_cache.get_mut(&path) {
- Some(CachedPreview::Document(ref mut doc)) => doc,
- _ => return,
- },
- };
- doc.syntax = Some(syntax);
- };
- Callback::EditorCompositor(Box::new(callback))
- });
- let tmp: compositor::Callback = Box::new(move |_, ctx| {
- ctx.jobs
- .callback(job.map(|res| res.map_err(anyhow::Error::from)))
- });
- callback = Some(Box::new(tmp))
- }
- }
-
- // QUESTION: do we want to compute inlay hints in pickers too ? Probably not for now
- // but it could be interesting in the future
-
- EventResult::Consumed(callback)
- }
-}
-
-impl<T: Item + 'static> Component for FilePicker<T> {
- fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
- // +---------+ +---------+
- // |prompt | |preview |
- // +---------+ | |
- // |picker | | |
- // | | | |
- // +---------+ +---------+
-
- let render_preview = self.picker.show_preview && area.width > MIN_AREA_WIDTH_FOR_PREVIEW;
- // -- Render the frame:
- // clear area
- let background = cx.editor.theme.get("ui.background");
- let text = cx.editor.theme.get("ui.text");
- surface.clear_with(area, background);
-
- let picker_width = if render_preview {
- area.width / 2
- } else {
- area.width
- };
-
- let picker_area = area.with_width(picker_width);
- self.picker.render(picker_area, surface, cx);
-
- if !render_preview {
- return;
- }
-
- let preview_area = area.clip_left(picker_width);
-
- // don't like this but the lifetime sucks
- let block = Block::default().borders(Borders::ALL);
-
- // calculate the inner area inside the box
- let inner = block.inner(preview_area);
- // 1 column gap on either side
- let margin = Margin::horizontal(1);
- let inner = inner.inner(&margin);
- block.render(preview_area, surface);
-
- 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;
- }
- };
-
- // align to middle
- let first_line = range
- .map(|(start, end)| {
- let height = end.saturating_sub(start) + 1;
- let middle = start + (height.saturating_sub(1) / 2);
- middle.saturating_sub(inner.height as usize / 2).min(start)
- })
- .unwrap_or(0);
-
- let offset = ViewPosition {
- anchor: doc.text().line_to_char(first_line),
- horizontal_offset: 0,
- vertical_offset: 0,
- };
-
- let mut highlights = EditorView::doc_syntax_highlights(
- doc,
- offset.anchor,
- area.height,
- &cx.editor.theme,
- );
- for spans in EditorView::doc_diagnostics_highlights(doc, &cx.editor.theme) {
- if spans.is_empty() {
- continue;
- }
- highlights = Box::new(helix_core::syntax::merge(highlights, spans));
- }
- let mut decorations: Vec<Box<dyn LineDecoration>> = Vec::new();
-
- if let Some((start, end)) = range {
- let style = cx
- .editor
- .theme
- .try_get("ui.highlight")
- .unwrap_or_else(|| cx.editor.theme.get("ui.selection"));
- let draw_highlight = move |renderer: &mut TextRenderer, pos: LinePos| {
- if (start..=end).contains(&pos.doc_line) {
- let area = Rect::new(
- renderer.viewport.x,
- renderer.viewport.y + pos.visual_line,
- renderer.viewport.width,
- 1,
- );
- renderer.surface.set_style(area, style)
- }
- };
- decorations.push(Box::new(draw_highlight))
- }
-
- render_document(
- surface,
- inner,
- doc,
- offset,
- // TODO: compute text annotations asynchronously here (like inlay hints)
- &TextAnnotations::default(),
- highlights,
- &cx.editor.theme,
- &mut decorations,
- &mut [],
- );
- }
- }
-
- fn handle_event(&mut self, event: &Event, ctx: &mut Context) -> EventResult {
- if let Event::IdleTimeout = event {
- return self.handle_idle_timeout(ctx);
- }
- // TODO: keybinds for scrolling preview
- self.picker.handle_event(event, ctx)
- }
-
- fn cursor(&self, area: Rect, ctx: &Editor) -> (Option<Position>, CursorKind) {
- self.picker.cursor(area, ctx)
- }
-
- fn required_size(&mut self, (width, height): (u16, u16)) -> Option<(u16, u16)> {
- let picker_width = if width > MIN_AREA_WIDTH_FOR_PREVIEW {
- width / 2
- } else {
- width
- };
- self.picker.required_size((picker_width, height))?;
- Some((width, height))
- }
-
- fn id(&self) -> Option<&'static str> {
- Some("file-picker")
- }
-}
-
-#[derive(PartialEq, Eq, Debug)]
-struct PickerMatch {
- score: i64,
- index: usize,
- len: usize,
-}
-
-impl PickerMatch {
- fn key(&self) -> impl Ord {
- (cmp::Reverse(self.score), self.len, self.index)
- }
-}
-
-impl PartialOrd for PickerMatch {
- fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
- Some(self.cmp(other))
- }
-}
-
-impl Ord for PickerMatch {
- fn cmp(&self, other: &Self) -> Ordering {
- self.key().cmp(&other.key())
- }
-}
-
-type PickerCallback<T> = Box<dyn Fn(&mut Context, &T, Action)>;
-
pub struct Picker<T: Item> {
options: Vec<T>,
editor_data: T::Data,
@@ -457,17 +128,22 @@ pub struct Picker<T: Item> {
// pattern: String,
prompt: Prompt,
previous_pattern: (String, FuzzyQuery),
- /// Whether to truncate the start (default true)
- pub truncate_start: bool,
/// Whether to show the preview panel (default true)
show_preview: bool,
/// Constraints for tabular formatting
widths: Vec<Constraint>,
callback_fn: PickerCallback<T>,
+
+ pub truncate_start: bool,
+ /// Caches paths to documents
+ preview_cache: HashMap<PathBuf, CachedPreview>,
+ read_buffer: Vec<u8>,
+ /// Given an item in the picker, return the file path and line number to display.
+ file_fn: Option<FileCallback<T>>,
}
-impl<T: Item> Picker<T> {
+impl<T: Item + 'static> Picker<T> {
pub fn new(
options: Vec<T>,
editor_data: T::Data,
@@ -493,6 +169,9 @@ impl<T: Item> Picker<T> {
callback_fn: Box::new(callback_fn),
completion_height: 0,
widths: Vec::new(),
+ preview_cache: HashMap::new(),
+ read_buffer: Vec::with_capacity(1024),
+ file_fn: None,
};
picker.calculate_column_widths();
@@ -513,6 +192,19 @@ impl<T: Item> Picker<T> {
picker
}
+ pub fn truncate_start(mut self, truncate_start: bool) -> Self {
+ self.truncate_start = truncate_start;
+ self
+ }
+
+ pub fn with_preview(
+ mut self,
+ preview_fn: impl Fn(&Editor, &T) -> Option<FileLocation> + 'static,
+ ) -> Self {
+ self.file_fn = Some(Box::new(preview_fn));
+ self
+ }
+
pub fn set_options(&mut self, new_options: Vec<T>) {
self.options = new_options;
self.cursor = 0;
@@ -679,92 +371,127 @@ impl<T: Item> Picker<T> {
}
EventResult::Consumed(None)
}
-}
-// process:
-// - read all the files into a list, maxed out at a large value
-// - on input change:
-// - score all the names in relation to input
+ fn current_file(&self, editor: &Editor) -> Option<FileLocation> {
+ self.selection()
+ .and_then(|current| (self.file_fn.as_ref()?)(editor, current))
+ .and_then(|(path_or_id, line)| path_or_id.get_canonicalized().ok().zip(Some(line)))
+ }
-impl<T: Item + 'static> Component for Picker<T> {
- fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
- self.completion_height = viewport.1.saturating_sub(4);
- Some(viewport)
+ /// Get (cached) preview for a given path. If a document corresponding
+ /// to the path is already open in the editor, it is used instead.
+ fn get_preview<'picker, 'editor>(
+ &'picker mut self,
+ path_or_id: PathOrId,
+ editor: &'editor Editor,
+ ) -> Preview<'picker, 'editor> {
+ match path_or_id {
+ PathOrId::Path(path) => {
+ let path = &path;
+ if let Some(doc) = editor.document_by_path(path) {
+ return Preview::EditorDocument(doc);
+ }
+
+ if self.preview_cache.contains_key(path) {
+ 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, editor.config.clone())
+ .map(|doc| CachedPreview::Document(Box::new(doc)))
+ .unwrap_or(CachedPreview::NotFound)
+ }
+ },
+ )
+ .unwrap_or(CachedPreview::NotFound);
+ self.preview_cache.insert(path.to_owned(), preview);
+ Preview::Cached(&self.preview_cache[path])
+ }
+ PathOrId::Id(id) => {
+ let doc = editor.documents.get(&id).unwrap();
+ Preview::EditorDocument(doc)
+ }
+ }
}
- fn handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult {
- let key_event = match event {
- Event::Key(event) => *event,
- Event::Paste(..) => return self.prompt_handle_event(event, cx),
- Event::Resize(..) => return EventResult::Consumed(None),
- _ => return EventResult::Ignored(None),
+ fn handle_idle_timeout(&mut self, cx: &mut Context) -> EventResult {
+ let Some((current_file, _)) = self.current_file(cx.editor) else {
+ return EventResult::Consumed(None)
};
- let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _cx| {
- // remove the layer
- compositor.last_picker = compositor.pop();
- })));
+ // Try to find a document in the cache
+ let doc = match &current_file {
+ PathOrId::Id(doc_id) => doc_mut!(cx.editor, doc_id),
+ PathOrId::Path(path) => match self.preview_cache.get_mut(path) {
+ Some(CachedPreview::Document(ref mut doc)) => doc,
+ _ => return EventResult::Consumed(None),
+ },
+ };
- // So that idle timeout retriggers
- cx.editor.reset_idle_timer();
+ let mut callback: Option<compositor::Callback> = None;
- match key_event {
- shift!(Tab) | key!(Up) | ctrl!('p') => {
- self.move_by(1, Direction::Backward);
- }
- key!(Tab) | key!(Down) | ctrl!('n') => {
- self.move_by(1, Direction::Forward);
- }
- key!(PageDown) | ctrl!('d') => {
- self.page_down();
- }
- key!(PageUp) | ctrl!('u') => {
- self.page_up();
- }
- key!(Home) => {
- self.to_start();
- }
- key!(End) => {
- self.to_end();
- }
- key!(Esc) | ctrl!('c') => {
- return close_fn;
- }
- alt!(Enter) => {
- if let Some(option) = self.selection() {
- (self.callback_fn)(cx, option, Action::Load);
- }
- }
- key!(Enter) => {
- if let Some(option) = self.selection() {
- (self.callback_fn)(cx, option, Action::Replace);
- }
- return close_fn;
- }
- ctrl!('s') => {
- if let Some(option) = self.selection() {
- (self.callback_fn)(cx, option, Action::HorizontalSplit);
- }
- return close_fn;
- }
- ctrl!('v') => {
- if let Some(option) = self.selection() {
- (self.callback_fn)(cx, option, Action::VerticalSplit);
- }
- return close_fn;
- }
- ctrl!('t') => {
- self.toggle_preview();
- }
- _ => {
- self.prompt_handle_event(event, cx);
+ // Then attempt to highlight it if it has no language set
+ if doc.language_config().is_none() {
+ if let Some(language_config) = doc.detect_language_config(&cx.editor.syn_loader) {
+ doc.language = Some(language_config.clone());
+ let text = doc.text().clone();
+ let loader = cx.editor.syn_loader.clone();
+ let job = tokio::task::spawn_blocking(move || {
+ let syntax = language_config
+ .highlight_config(&loader.scopes())
+ .and_then(|highlight_config| Syntax::new(&text, highlight_config, loader));
+ let callback = move |editor: &mut Editor, compositor: &mut Compositor| {
+ let Some(syntax) = syntax else {
+ log::info!("highlighting picker item failed");
+ return
+ };
+ let Some(Overlay { content: picker, .. }) = compositor.find::<Overlay<Self>>() else {
+ log::info!("picker closed before syntax highlighting finished");
+ return
+ };
+ // Try to find a document in the cache
+ let doc = match current_file {
+ PathOrId::Id(doc_id) => doc_mut!(editor, &doc_id),
+ PathOrId::Path(path) => match picker.preview_cache.get_mut(&path) {
+ Some(CachedPreview::Document(ref mut doc)) => doc,
+ _ => return,
+ },
+ };
+ doc.syntax = Some(syntax);
+ };
+ Callback::EditorCompositor(Box::new(callback))
+ });
+ let tmp: compositor::Callback = Box::new(move |_, ctx| {
+ ctx.jobs
+ .callback(job.map(|res| res.map_err(anyhow::Error::from)))
+ });
+ callback = Some(Box::new(tmp))
}
}
- EventResult::Consumed(None)
+ // QUESTION: do we want to compute inlay hints in pickers too ? Probably not for now
+ // but it could be interesting in the future
+
+ EventResult::Consumed(callback)
}
- fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
+ fn render_picker(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
let text_style = cx.editor.theme.get("ui.text");
let selected = cx.editor.theme.get("ui.text.focus");
let highlight_style = cx.editor.theme.get("special").add_modifier(Modifier::BOLD);
@@ -930,6 +657,178 @@ impl<T: Item + 'static> Component for Picker<T> {
);
}
+ fn render_preview(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
+ // -- Render the frame:
+ // clear area
+ let background = cx.editor.theme.get("ui.background");
+ let text = cx.editor.theme.get("ui.text");
+ surface.clear_with(area, background);
+
+ // don't like this but the lifetime sucks
+ let block = Block::default().borders(Borders::ALL);
+
+ // calculate the inner area inside the box
+ let inner = block.inner(area);
+ // 1 column gap on either side
+ let margin = Margin::horizontal(1);
+ let inner = inner.inner(&margin);
+ block.render(area, surface);
+
+ 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;
+ }
+ };
+
+ // align to middle
+ let first_line = range
+ .map(|(start, end)| {
+ let height = end.saturating_sub(start) + 1;
+ let middle = start + (height.saturating_sub(1) / 2);
+ middle.saturating_sub(inner.height as usize / 2).min(start)
+ })
+ .unwrap_or(0);
+
+ let offset = ViewPosition {
+ anchor: doc.text().line_to_char(first_line),
+ horizontal_offset: 0,
+ vertical_offset: 0,
+ };
+
+ let mut highlights = EditorView::doc_syntax_highlights(
+ doc,
+ offset.anchor,
+ area.height,
+ &cx.editor.theme,
+ );
+ for spans in EditorView::doc_diagnostics_highlights(doc, &cx.editor.theme) {
+ if spans.is_empty() {
+ continue;
+ }
+ highlights = Box::new(helix_core::syntax::merge(highlights, spans));
+ }
+ let mut decorations: Vec<Box<dyn LineDecoration>> = Vec::new();
+
+ if let Some((start, end)) = range {
+ let style = cx
+ .editor
+ .theme
+ .try_get("ui.highlight")
+ .unwrap_or_else(|| cx.editor.theme.get("ui.selection"));
+ let draw_highlight = move |renderer: &mut TextRenderer, pos: LinePos| {
+ if (start..=end).contains(&pos.doc_line) {
+ let area = Rect::new(
+ renderer.viewport.x,
+ renderer.viewport.y + pos.visual_line,
+ renderer.viewport.width,
+ 1,
+ );
+ renderer.surface.set_style(area, style)
+ }
+ };
+ decorations.push(Box::new(draw_highlight))
+ }
+
+ render_document(
+ surface,
+ inner,
+ doc,
+ offset,
+ // TODO: compute text annotations asynchronously here (like inlay hints)
+ &TextAnnotations::default(),
+ highlights,
+ &cx.editor.theme,
+ &mut decorations,
+ &mut [],
+ );
+ }
+ }
+
+ fn handle_event(&mut self, event: &Event, ctx: &mut Context) -> EventResult {
+ if let Event::IdleTimeout = event {
+ return self.handle_idle_timeout(ctx);
+ }
+ // TODO: keybinds for scrolling preview
+
+ let key_event = match event {
+ Event::Key(event) => *event,
+ Event::Paste(..) => return self.prompt_handle_event(event, ctx),
+ Event::Resize(..) => return EventResult::Consumed(None),
+ _ => return EventResult::Ignored(None),
+ };
+
+ let close_fn =
+ EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _ctx| {
+ // remove the layer
+ compositor.last_picker = compositor.pop();
+ })));
+
+ // So that idle timeout retriggers
+ ctx.editor.reset_idle_timer();
+
+ match key_event {
+ shift!(Tab) | key!(Up) | ctrl!('p') => {
+ self.move_by(1, Direction::Backward);
+ }
+ key!(Tab) | key!(Down) | ctrl!('n') => {
+ self.move_by(1, Direction::Forward);
+ }
+ key!(PageDown) | ctrl!('d') => {
+ self.page_down();
+ }
+ key!(PageUp) | ctrl!('u') => {
+ self.page_up();
+ }
+ key!(Home) => {
+ self.to_start();
+ }
+ key!(End) => {
+ self.to_end();
+ }
+ key!(Esc) | ctrl!('c') => {
+ return close_fn;
+ }
+ alt!(Enter) => {
+ if let Some(option) = self.selection() {
+ (self.callback_fn)(ctx, option, Action::Load);
+ }
+ }
+ key!(Enter) => {
+ if let Some(option) = self.selection() {
+ (self.callback_fn)(ctx, option, Action::Replace);
+ }
+ return close_fn;
+ }
+ ctrl!('s') => {
+ if let Some(option) = self.selection() {
+ (self.callback_fn)(ctx, option, Action::HorizontalSplit);
+ }
+ return close_fn;
+ }
+ ctrl!('v') => {
+ if let Some(option) = self.selection() {
+ (self.callback_fn)(ctx, option, Action::VerticalSplit);
+ }
+ return close_fn;
+ }
+ ctrl!('t') => {
+ self.toggle_preview();
+ }
+ _ => {
+ self.prompt_handle_event(event, ctx);
+ }
+ }
+
+ EventResult::Consumed(None)
+ }
+
fn cursor(&self, area: Rect, editor: &Editor) -> (Option<Position>, CursorKind) {
let block = Block::default().borders(Borders::ALL);
// calculate the inner area inside the box
@@ -940,8 +839,67 @@ impl<T: Item + 'static> Component for Picker<T> {
self.prompt.cursor(area, editor)
}
+
+ fn required_size(&mut self, (width, height): (u16, u16)) -> Option<(u16, u16)> {
+ self.completion_height = height.saturating_sub(4);
+ Some((width, height))
+ }
+}
+
+impl<T: Item + 'static> Component for Picker<T> {
+ fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
+ // +---------+ +---------+
+ // |prompt | |preview |
+ // +---------+ | |
+ // |picker | | |
+ // | | | |
+ // +---------+ +---------+
+
+ let render_preview = self.show_preview && area.width > MIN_AREA_WIDTH_FOR_PREVIEW;
+
+ let picker_width = if render_preview {
+ area.width / 2
+ } else {
+ area.width
+ };
+
+ let picker_area = area.with_width(picker_width);
+ self.render_picker(picker_area, surface, cx);
+
+ if render_preview {
+ let preview_area = area.clip_left(picker_width);
+ self.render_preview(preview_area, surface, cx);
+ }
+ }
}
+#[derive(PartialEq, Eq, Debug)]
+struct PickerMatch {
+ score: i64,
+ index: usize,
+ len: usize,
+}
+
+impl PickerMatch {
+ fn key(&self) -> impl Ord {
+ (cmp::Reverse(self.score), self.len, self.index)
+ }
+}
+
+impl PartialOrd for PickerMatch {
+ fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+ Some(self.cmp(other))
+ }
+}
+
+impl Ord for PickerMatch {
+ fn cmp(&self, other: &Self) -> Ordering {
+ self.key().cmp(&other.key())
+ }
+}
+
+type PickerCallback<T> = Box<dyn Fn(&mut Context, &T, Action)>;
+
/// Returns a new list of options to replace the contents of the picker
/// when called with the current picker query,
pub type DynQueryCallback<T> =
@@ -950,7 +908,7 @@ pub type DynQueryCallback<T> =
/// A picker that updates its contents via a callback whenever the
/// query string changes. Useful for live grep, workspace symbols, etc.
pub struct DynamicPicker<T: ui::menu::Item + Send> {
- file_picker: FilePicker<T>,
+ file_picker: Picker<T>,
query_callback: DynQueryCallback<T>,
query: String,
}
@@ -958,7 +916,7 @@ pub struct DynamicPicker<T: ui::menu::Item + Send> {
impl<T: ui::menu::Item + Send> DynamicPicker<T> {
pub const ID: &'static str = "dynamic-picker";
- pub fn new(file_picker: FilePicker<T>, query_callback: DynQueryCallback<T>) -> Self {
+ pub fn new(file_picker: Picker<T>, query_callback: DynQueryCallback<T>) -> Self {
Self {
file_picker,
query_callback,
@@ -974,7 +932,7 @@ impl<T: Item + Send + 'static> Component for DynamicPicker<T> {
fn handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult {
let event_result = self.file_picker.handle_event(event, cx);
- let current_query = self.file_picker.picker.prompt.line();
+ let current_query = self.file_picker.prompt.line();
if !matches!(event, Event::IdleTimeout) || self.query == *current_query {
return event_result;
@@ -990,7 +948,7 @@ impl<T: Item + Send + 'static> Component for DynamicPicker<T> {
// Wrapping of pickers in overlay is done outside the picker code,
// so this is fragile and will break if wrapped in some other widget.
let picker = match compositor.find_id::<Overlay<DynamicPicker<T>>>(Self::ID) {
- Some(overlay) => &mut overlay.content.file_picker.picker,
+ Some(overlay) => &mut overlay.content.file_picker,
None => return,
};
picker.set_options(new_options);