From 7a1dbedb0551f68a157037e3a8b4365b2b3878b2 Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Fri, 30 Jul 2021 17:53:01 +0530 Subject: [PATCH 01/20] Add preview pane for fuzzy finder --- helix-term/src/commands.rs | 8 +- helix-term/src/ui/mod.rs | 5 + helix-term/src/ui/picker.rs | 223 +++++++++++++++++++++++++++++++++--- 3 files changed, 221 insertions(+), 15 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 7403f5b2616c..645a9626bc95 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -101,13 +101,13 @@ impl<'a> Context<'a> { } } -enum Align { +pub enum Align { Top, Center, Bottom, } -fn align_view(doc: &Document, view: &mut View, align: Align) { +pub fn align_view(doc: &Document, view: &mut View, align: Align) { let pos = doc .selection(view.id) .primary() @@ -2061,6 +2061,7 @@ fn buffer_picker(cx: &mut Context) { |editor: &mut Editor, (id, _path): &(DocumentId, Option), _action| { editor.switch(*id, Action::Replace); }, + |symbol| None, ); cx.push_layer(Box::new(picker)); } @@ -2128,6 +2129,7 @@ fn symbol_picker(cx: &mut Context) { align_view(doc, view, Align::Center); } }, + |symbol| None, ); compositor.push(Box::new(picker)) } @@ -2178,6 +2180,7 @@ pub fn code_action(cx: &mut Context) { } } }, + |symbol| None, ); compositor.push(Box::new(picker)) } @@ -2515,6 +2518,7 @@ fn goto_impl( move |editor: &mut Editor, location, action| { jump_to(editor, location, offset_encoding, action) }, + |symbol| None, ); compositor.push(Box::new(picker)); } diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 9e71cfe73b92..290e65ac90e4 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -11,6 +11,7 @@ mod text; pub use completion::Completion; pub use editor::EditorView; +use helix_core::Selection; pub use markdown::Markdown; pub use menu::Menu; pub use picker::Picker; @@ -124,6 +125,10 @@ pub fn file_picker(root: PathBuf) -> Picker { .open(path.into(), action) .expect("editor.open failed"); }, + |path| { + // FIXME: directories are creeping up in filepicker + Some((path.clone(), Selection::point(0))) + }, ) } diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 0b67cd9c6399..389379e5d141 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -1,4 +1,7 @@ -use crate::compositor::{Component, Compositor, Context, EventResult}; +use crate::{ + commands::{self, Align}, + compositor::{Component, Compositor, Context, EventResult}, +}; use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; use tui::{ buffer::Buffer as Surface, @@ -8,14 +11,15 @@ use tui::{ use fuzzy_matcher::skim::SkimMatcherV2 as Matcher; use fuzzy_matcher::FuzzyMatcher; -use std::borrow::Cow; +use std::{borrow::Cow, path::PathBuf}; use crate::ui::{Prompt, PromptEvent}; -use helix_core::Position; +use helix_core::{Position, Selection}; use helix_view::{ + document::canonicalize_path, editor::Action, graphics::{Color, CursorKind, Rect, Style}, - Editor, + Document, Editor, View, }; pub struct Picker { @@ -33,6 +37,7 @@ pub struct Picker { format_fn: Box Cow>, callback_fn: Box, + preview_fn: Box Option<(PathBuf, Selection)>>, } impl Picker { @@ -40,6 +45,7 @@ impl Picker { options: Vec, format_fn: impl Fn(&T) -> Cow + 'static, callback_fn: impl Fn(&mut Editor, &T, Action) + 'static, + preview_fn: impl Fn(&T) -> Option<(PathBuf, Selection)> + 'static, ) -> Self { let prompt = Prompt::new( "".to_string(), @@ -59,6 +65,7 @@ impl Picker { prompt, format_fn: Box::new(format_fn), callback_fn: Box::new(callback_fn), + preview_fn: Box::new(preview_fn), }; // TODO: scoring on empty input should just use a fastpath @@ -67,6 +74,161 @@ impl Picker { picker } + // TODO: Copied from EditorView::render_buffer, reuse + #[allow(clippy::too_many_arguments)] + fn render_buffer( + &self, + doc: &Document, + view: &helix_view::View, + viewport: Rect, + surface: &mut Surface, + theme: &helix_view::Theme, + loader: &helix_core::syntax::Loader, + ) { + let text = doc.text().slice(..); + + let last_line = view.last_line(doc); + + let range = { + // calculate viewport byte ranges + let start = text.line_to_byte(view.first_line); + let end = text.line_to_byte(last_line + 1); + + start..end + }; + + // TODO: range doesn't actually restrict source, just highlight range + let highlights: Vec<_> = match doc.syntax() { + Some(syntax) => { + let scopes = theme.scopes(); + syntax + .highlight_iter(text.slice(..), Some(range), None, |language| { + loader + .language_config_for_scope(&format!("source.{}", language)) + .and_then(|language_config| { + let config = language_config.highlight_config(scopes)?; + let config_ref = config.as_ref(); + // SAFETY: the referenced `HighlightConfiguration` behind + // the `Arc` is guaranteed to remain valid throughout the + // duration of the highlight. + let config_ref = unsafe { + std::mem::transmute::< + _, + &'static helix_core::syntax::HighlightConfiguration, + >(config_ref) + }; + Some(config_ref) + }) + }) + .collect() // TODO: we collect here to avoid holding the lock, fix later + } + None => vec![Ok(helix_core::syntax::HighlightEvent::Source { + start: range.start, + end: range.end, + })], + }; + let mut spans = Vec::new(); + let mut visual_x = 0u16; + let mut line = 0u16; + let tab_width = doc.tab_width(); + let tab = " ".repeat(tab_width); + + let highlights = highlights.into_iter().map(|event| match event.unwrap() { + // convert byte offsets to char offset + helix_core::syntax::HighlightEvent::Source { start, end } => { + let start = helix_core::graphemes::ensure_grapheme_boundary_next( + text, + text.byte_to_char(start), + ); + let end = helix_core::graphemes::ensure_grapheme_boundary_next( + text, + text.byte_to_char(end), + ); + helix_core::syntax::HighlightEvent::Source { start, end } + } + event => event, + }); + + // let selections = doc.selection(view.id); + // let primary_idx = selections.primary_index(); + // let selection_scope = theme + // .find_scope_index("ui.selection") + // .expect("no selection scope found!"); + + 'outer: for event in highlights { + match event { + helix_core::syntax::HighlightEvent::HighlightStart(span) => { + spans.push(span); + } + helix_core::syntax::HighlightEvent::HighlightEnd => { + spans.pop(); + } + helix_core::syntax::HighlightEvent::Source { start, end } => { + // `unwrap_or_else` part is for off-the-end indices of + // the rope, to allow cursor highlighting at the end + // of the rope. + let text = text.get_slice(start..end).unwrap_or_else(|| " ".into()); + + use helix_core::graphemes::{grapheme_width, RopeGraphemes}; + + let style = spans.iter().fold(theme.get("ui.text"), |acc, span| { + let style = theme.get(theme.scopes()[span.0].as_str()); + acc.patch(style) + }); + + for grapheme in RopeGraphemes::new(text) { + let out_of_bounds = visual_x < view.first_col as u16 + || visual_x >= viewport.width + view.first_col as u16; + + if helix_core::LineEnding::from_rope_slice(&grapheme).is_some() { + if !out_of_bounds { + // we still want to render an empty cell with the style + surface.set_string( + viewport.x + visual_x - view.first_col as u16, + viewport.y + line, + " ", + style, + ); + } + + visual_x = 0; + line += 1; + + // TODO: with proper iter this shouldn't be necessary + if line >= viewport.height { + break 'outer; + } + } else { + let grapheme = Cow::from(grapheme); + + let (grapheme, width) = if grapheme == "\t" { + // make sure we display tab as appropriate amount of spaces + (tab.as_str(), tab_width) + } else { + // Cow will prevent allocations if span contained in a single slice + // which should really be the majority case + let width = grapheme_width(&grapheme); + (grapheme.as_ref(), width) + }; + + if !out_of_bounds { + // if we're offscreen just keep going until we hit a new line + surface.set_string( + viewport.x + visual_x - view.first_col as u16, + viewport.y + line, + grapheme, + style, + ); + } + + visual_x = visual_x.saturating_add(width as u16); + } + } + } + } + } + } + pub fn score(&mut self) { // need to borrow via pattern match otherwise it complains about simultaneous borrow let Self { @@ -263,21 +425,56 @@ impl Component for Picker { self.prompt.render(area, surface, cx); // -- Separator - let style = Style::default().fg(Color::Rgb(90, 89, 119)); - let symbols = BorderType::line_symbols(BorderType::Plain); + let sep_style = Style::default().fg(Color::Rgb(90, 89, 119)); + let borders = BorderType::line_symbols(BorderType::Plain); for x in inner.left()..inner.right() { surface .get_mut(x, inner.y + 1) - .set_symbol(symbols.horizontal) - .set_style(style); + .set_symbol(borders.horizontal) + .set_style(sep_style); } // -- Render the contents: + // subtract the area of the prompt (-2) and current item marker " > " (-3) + let inner = Rect::new(inner.x + 3, inner.y + 2, inner.width - 3, inner.height - 2); + let mut item_width = inner.width; + + if let Some((path, selection)) = self + .selection() + .and_then(|current| (self.preview_fn)(current)) + .and_then(|(path, selection)| canonicalize_path(&path).ok().zip(Some(selection))) + { + item_width = inner.width * 40 / 100; + + let (theme, loader) = (&cx.editor.theme, &cx.editor.syn_loader); + let mut doc = Document::open(path, None, Some(theme), Some(loader)).unwrap(); + let mut view = View::new(doc.id()); + + doc.set_selection(view.id, selection); + commands::align_view(&doc, &mut view, Align::Center); + + for y in inner.top()..inner.bottom() { + surface + .get_mut(inner.x + item_width, y) + .set_symbol(borders.vertical) + .set_style(sep_style); + } + + let viewport = Rect::new( + inner.x + item_width + 1, // 1 for sep + inner.y, + inner.width * 60 / 100, + inner.height, + ); + // FIXME: the last line will not be highlighted because of a -1 in View::last_line + view.area = viewport; + self.render_buffer(&doc, &view, viewport, surface, theme, loader); + } let style = cx.editor.theme.get("ui.text"); let selected = Style::default().fg(Color::Rgb(255, 255, 255)); - let rows = inner.height - 2; // -1 for search bar + let rows = inner.height; let offset = self.cursor / (rows as usize) * (rows as usize); let files = self.matches.iter().skip(offset).map(|(index, _score)| { @@ -286,14 +483,14 @@ impl Component for Picker { for (i, (_index, option)) in files.take(rows as usize).enumerate() { if i == (self.cursor - offset) { - surface.set_string(inner.x + 1, inner.y + 2 + i as u16, ">", selected); + surface.set_string(inner.x - 2, inner.y + i as u16, ">", selected); } surface.set_string_truncated( - inner.x + 3, - inner.y + 2 + i as u16, + inner.x, + inner.y + i as u16, (self.format_fn)(option), - (inner.width as usize).saturating_sub(3), // account for the " > " + item_width as usize, if i == (self.cursor - offset) { selected } else { From ab41e4519c8ce20f5e4f7fbe9d689f2848279828 Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Sat, 31 Jul 2021 11:57:52 +0530 Subject: [PATCH 02/20] Fix picker preview lag by caching --- helix-core/src/selection.rs | 8 +++- helix-term/src/ui/mod.rs | 4 +- helix-term/src/ui/picker.rs | 73 ++++++++++++++++++++++++++++--------- helix-view/src/view.rs | 4 +- 4 files changed, 67 insertions(+), 22 deletions(-) diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs index a3ea2ed42524..6cca0775507e 100644 --- a/helix-core/src/selection.rs +++ b/helix-core/src/selection.rs @@ -46,7 +46,7 @@ use std::borrow::Cow; /// single grapheme inward from the range's edge. There are a /// variety of helper methods on `Range` for working in terms of /// that block cursor, all of which have `cursor` in their name. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct Range { /// The anchor of the range: the side that doesn't move when extending. pub anchor: usize, @@ -497,6 +497,12 @@ impl Selection { } } +impl From for Selection { + fn from(range: Range) -> Self { + Self::single(range.anchor, range.head) + } +} + impl<'a> IntoIterator for &'a Selection { type Item = &'a Range; type IntoIter = std::slice::Iter<'a, Range>; diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 290e65ac90e4..fbb72d2d700a 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -11,7 +11,7 @@ mod text; pub use completion::Completion; pub use editor::EditorView; -use helix_core::Selection; +use helix_core::Range; pub use markdown::Markdown; pub use menu::Menu; pub use picker::Picker; @@ -127,7 +127,7 @@ pub fn file_picker(root: PathBuf) -> Picker { }, |path| { // FIXME: directories are creeping up in filepicker - Some((path.clone(), Selection::point(0))) + Some((path.clone(), Range::point(0))) }, ) } diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 389379e5d141..6049e64e9033 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -11,10 +11,10 @@ use tui::{ use fuzzy_matcher::skim::SkimMatcherV2 as Matcher; use fuzzy_matcher::FuzzyMatcher; -use std::{borrow::Cow, path::PathBuf}; +use std::{borrow::Cow, collections::HashMap, path::PathBuf}; use crate::ui::{Prompt, PromptEvent}; -use helix_core::{Position, Selection}; +use helix_core::{Position, Range, Selection}; use helix_view::{ document::canonicalize_path, editor::Action, @@ -34,10 +34,11 @@ pub struct Picker { cursor: usize, // pattern: String, prompt: Prompt, + preview_cache: HashMap<(PathBuf, Range), (Document, View)>, format_fn: Box Cow>, callback_fn: Box, - preview_fn: Box Option<(PathBuf, Selection)>>, + preview_fn: Box Option<(PathBuf, Range)>>, } impl Picker { @@ -45,7 +46,7 @@ impl Picker { options: Vec, format_fn: impl Fn(&T) -> Cow + 'static, callback_fn: impl Fn(&mut Editor, &T, Action) + 'static, - preview_fn: impl Fn(&T) -> Option<(PathBuf, Selection)> + 'static, + preview_fn: impl Fn(&T) -> Option<(PathBuf, Range)> + 'static, ) -> Self { let prompt = Prompt::new( "".to_string(), @@ -63,6 +64,7 @@ impl Picker { filters: Vec::new(), cursor: 0, prompt, + preview_cache: HashMap::new(), format_fn: Box::new(format_fn), callback_fn: Box::new(callback_fn), preview_fn: Box::new(preview_fn), @@ -280,6 +282,30 @@ impl Picker { } } + fn calculate_preview( + &mut self, + theme: &helix_view::Theme, + loader: &helix_core::syntax::Loader, + ) { + if let Some((path, range)) = self + .selection() + .and_then(|current| (self.preview_fn)(current)) + .and_then(|(path, range)| canonicalize_path(&path).ok().zip(Some(range))) + { + let &mut (ref mut doc, ref mut view) = self + .preview_cache + .entry((path.clone(), range)) + .or_insert_with(|| { + let doc = Document::open(path, None, Some(theme), Some(loader)).unwrap(); + let view = View::new(doc.id()); + (doc, view) + }); + + doc.set_selection(view.id, Selection::from(range)); + commands::align_view(doc, view, Align::Center); + } + } + pub fn selection(&self) -> Option<&T> { self.matches .get(self.cursor) @@ -336,7 +362,10 @@ impl Component for Picker { | KeyEvent { code: KeyCode::Char('p'), modifiers: KeyModifiers::CONTROL, - } => self.move_up(), + } => { + self.move_up(); + self.calculate_preview(&cx.editor.theme, &cx.editor.syn_loader); + } KeyEvent { code: KeyCode::Down, .. @@ -347,7 +376,10 @@ impl Component for Picker { | KeyEvent { code: KeyCode::Char('n'), modifiers: KeyModifiers::CONTROL, - } => self.move_down(), + } => { + self.move_down(); + self.calculate_preview(&cx.editor.theme, &cx.editor.syn_loader); + } KeyEvent { code: KeyCode::Esc, .. } @@ -355,6 +387,7 @@ impl Component for Picker { code: KeyCode::Char('c'), modifiers: KeyModifiers::CONTROL, } => { + self.preview_cache.clear(); return close_fn; } KeyEvent { @@ -364,6 +397,7 @@ impl Component for Picker { if let Some(option) = self.selection() { (self.callback_fn)(&mut cx.editor, option, Action::Replace); } + self.preview_cache.clear(); return close_fn; } KeyEvent { @@ -382,6 +416,7 @@ impl Component for Picker { if let Some(option) = self.selection() { (self.callback_fn)(&mut cx.editor, option, Action::VerticalSplit); } + self.preview_cache.clear(); return close_fn; } KeyEvent { @@ -389,11 +424,13 @@ impl Component for Picker { modifiers: KeyModifiers::CONTROL, } => { self.save_filter(); + self.calculate_preview(&cx.editor.theme, &cx.editor.syn_loader); } _ => { if let EventResult::Consumed(_) = self.prompt.handle_event(event, cx) { // TODO: recalculate only if pattern changed self.score(); + self.calculate_preview(&cx.editor.theme, &cx.editor.syn_loader); } } } @@ -439,20 +476,14 @@ impl Component for Picker { let inner = Rect::new(inner.x + 3, inner.y + 2, inner.width - 3, inner.height - 2); let mut item_width = inner.width; - if let Some((path, selection)) = self + if let Some((doc, view)) = self .selection() .and_then(|current| (self.preview_fn)(current)) - .and_then(|(path, selection)| canonicalize_path(&path).ok().zip(Some(selection))) + .and_then(|(path, range)| canonicalize_path(&path).ok().zip(Some(range))) + .and_then(|(path, range)| self.preview_cache.get(&(path, range))) { item_width = inner.width * 40 / 100; - let (theme, loader) = (&cx.editor.theme, &cx.editor.syn_loader); - let mut doc = Document::open(path, None, Some(theme), Some(loader)).unwrap(); - let mut view = View::new(doc.id()); - - doc.set_selection(view.id, selection); - commands::align_view(&doc, &mut view, Align::Center); - for y in inner.top()..inner.bottom() { surface .get_mut(inner.x + item_width, y) @@ -466,9 +497,17 @@ impl Component for Picker { inner.width * 60 / 100, inner.height, ); - // FIXME: the last line will not be highlighted because of a -1 in View::last_line + // FIXME: last line will not be highlighted because of a -1 in View::last_line + let mut view = view.clone(); view.area = viewport; - self.render_buffer(&doc, &view, viewport, surface, theme, loader); + self.render_buffer( + doc, + &view, + viewport, + surface, + &cx.editor.theme, + &cx.editor.syn_loader, + ); } let style = cx.editor.theme.get("ui.text"); diff --git a/helix-view/src/view.rs b/helix-view/src/view.rs index 1b350172a346..030ce6684447 100644 --- a/helix-view/src/view.rs +++ b/helix-view/src/view.rs @@ -12,7 +12,7 @@ pub const PADDING: usize = 5; type Jump = (DocumentId, Selection); -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct JumpList { jumps: Vec, current: usize, @@ -59,7 +59,7 @@ impl JumpList { } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct View { pub id: ViewId, pub doc: DocumentId, From c62d5aa5ef1ec8f1e5de87cb33fa96abb0b9e093 Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Sat, 31 Jul 2021 12:48:51 +0530 Subject: [PATCH 03/20] Add picker preview for document symbols --- helix-term/src/commands.rs | 19 ++++++++++++------- helix-term/src/ui/mod.rs | 2 +- helix-term/src/ui/picker.rs | 28 ++++++++++++++-------------- 3 files changed, 27 insertions(+), 22 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 645a9626bc95..d24211c138f9 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2061,7 +2061,7 @@ fn buffer_picker(cx: &mut Context) { |editor: &mut Editor, (id, _path): &(DocumentId, Option), _action| { editor.switch(*id, Action::Replace); }, - |symbol| None, + |editor, symbol| None, ); cx.push_layer(Box::new(picker)); } @@ -2125,11 +2125,17 @@ fn symbol_picker(cx: &mut Context) { if let Some(range) = lsp_range_to_range(doc.text(), symbol.location.range, offset_encoding) { - doc.set_selection(view.id, Selection::single(range.to(), range.from())); + doc.set_selection(view.id, Selection::from(range)); align_view(doc, view, Align::Center); } }, - |symbol| None, + move |editor, symbol| { + let view = editor.tree.get(editor.tree.focus); + let doc = &editor.documents[view.doc]; + let range = + lsp_range_to_range(doc.text(), symbol.location.range, offset_encoding); + doc.path().cloned().zip(range) + }, ); compositor.push(Box::new(picker)) } @@ -2180,7 +2186,7 @@ pub fn code_action(cx: &mut Context) { } } }, - |symbol| None, + |editor, symbol| None, ); compositor.push(Box::new(picker)) } @@ -2518,7 +2524,7 @@ fn goto_impl( move |editor: &mut Editor, location, action| { jump_to(editor, location, offset_encoding, action) }, - |symbol| None, + |editor, symbol| None, ); compositor.push(Box::new(picker)); } @@ -3476,8 +3482,7 @@ fn keep_primary_selection(cx: &mut Context) { let (view, doc) = current!(cx.editor); let range = doc.selection(view.id).primary(); - let selection = Selection::single(range.anchor, range.head); - doc.set_selection(view.id, selection); + doc.set_selection(view.id, Selection::from(range)); } fn completion(cx: &mut Context) { diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index fbb72d2d700a..4ff4aa601818 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -125,7 +125,7 @@ pub fn file_picker(root: PathBuf) -> Picker { .open(path.into(), action) .expect("editor.open failed"); }, - |path| { + |_editor, path| { // FIXME: directories are creeping up in filepicker Some((path.clone(), Range::point(0))) }, diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 6049e64e9033..bc0d7b5c7795 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -38,7 +38,7 @@ pub struct Picker { format_fn: Box Cow>, callback_fn: Box, - preview_fn: Box Option<(PathBuf, Range)>>, + preview_fn: Box Option<(PathBuf, Range)>>, } impl Picker { @@ -46,7 +46,7 @@ impl Picker { options: Vec, format_fn: impl Fn(&T) -> Cow + 'static, callback_fn: impl Fn(&mut Editor, &T, Action) + 'static, - preview_fn: impl Fn(&T) -> Option<(PathBuf, Range)> + 'static, + preview_fn: impl Fn(&Editor, &T) -> Option<(PathBuf, Range)> + 'static, ) -> Self { let prompt = Prompt::new( "".to_string(), @@ -282,21 +282,21 @@ impl Picker { } } - fn calculate_preview( - &mut self, - theme: &helix_view::Theme, - loader: &helix_core::syntax::Loader, - ) { + fn calculate_preview(&mut self, editor: &Editor) { if let Some((path, range)) = self .selection() - .and_then(|current| (self.preview_fn)(current)) + .and_then(|current| (self.preview_fn)(editor, current)) .and_then(|(path, range)| canonicalize_path(&path).ok().zip(Some(range))) { + // TODO: store only the doc as key and map of range -> view as value ? + // will improve perfomance for pickers where most items are from same file (symbol, goto) let &mut (ref mut doc, ref mut view) = self .preview_cache .entry((path.clone(), range)) .or_insert_with(|| { - let doc = Document::open(path, None, Some(theme), Some(loader)).unwrap(); + let doc = + Document::open(path, None, Some(&editor.theme), Some(&editor.syn_loader)) + .unwrap(); let view = View::new(doc.id()); (doc, view) }); @@ -364,7 +364,7 @@ impl Component for Picker { modifiers: KeyModifiers::CONTROL, } => { self.move_up(); - self.calculate_preview(&cx.editor.theme, &cx.editor.syn_loader); + self.calculate_preview(cx.editor); } KeyEvent { code: KeyCode::Down, @@ -378,7 +378,7 @@ impl Component for Picker { modifiers: KeyModifiers::CONTROL, } => { self.move_down(); - self.calculate_preview(&cx.editor.theme, &cx.editor.syn_loader); + self.calculate_preview(cx.editor); } KeyEvent { code: KeyCode::Esc, .. @@ -424,13 +424,13 @@ impl Component for Picker { modifiers: KeyModifiers::CONTROL, } => { self.save_filter(); - self.calculate_preview(&cx.editor.theme, &cx.editor.syn_loader); + self.calculate_preview(cx.editor); } _ => { if let EventResult::Consumed(_) = self.prompt.handle_event(event, cx) { // TODO: recalculate only if pattern changed self.score(); - self.calculate_preview(&cx.editor.theme, &cx.editor.syn_loader); + self.calculate_preview(cx.editor); } } } @@ -478,7 +478,7 @@ impl Component for Picker { if let Some((doc, view)) = self .selection() - .and_then(|current| (self.preview_fn)(current)) + .and_then(|current| (self.preview_fn)(cx.editor, current)) .and_then(|(path, range)| canonicalize_path(&path).ok().zip(Some(range))) .and_then(|(path, range)| self.preview_cache.get(&(path, range))) { From 5325eea28530710a34d94a46c04c3376e7289525 Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Sat, 31 Jul 2021 16:30:44 +0530 Subject: [PATCH 04/20] Cache picker preview per document instead of view --- helix-term/src/commands.rs | 13 +++++++++---- helix-term/src/ui/picker.rs | 23 ++++++++++++++--------- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index d24211c138f9..58972cc3482d 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2132,9 +2132,14 @@ fn symbol_picker(cx: &mut Context) { move |editor, symbol| { let view = editor.tree.get(editor.tree.focus); let doc = &editor.documents[view.doc]; - let range = - lsp_range_to_range(doc.text(), symbol.location.range, offset_encoding); - doc.path().cloned().zip(range) + // let range = + // lsp_range_to_range(doc.text(), symbol.location.range, offset_encoding); + // Calculating the exact range is expensive, so use line number only + let pos = doc + .text() + .line_to_char(symbol.location.range.start.line as usize); + let range = Range::point(pos); + doc.path().cloned().zip(Some(range)) }, ); compositor.push(Box::new(picker)) @@ -2186,7 +2191,7 @@ pub fn code_action(cx: &mut Context) { } } }, - |editor, symbol| None, + |_editor, _action| None, ); compositor.push(Box::new(picker)) } diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index bc0d7b5c7795..b86804da0947 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -14,7 +14,7 @@ use fuzzy_matcher::FuzzyMatcher; use std::{borrow::Cow, collections::HashMap, path::PathBuf}; use crate::ui::{Prompt, PromptEvent}; -use helix_core::{Position, Range, Selection}; +use helix_core::{hashmap, Position, Range, Selection}; use helix_view::{ document::canonicalize_path, editor::Action, @@ -34,7 +34,7 @@ pub struct Picker { cursor: usize, // pattern: String, prompt: Prompt, - preview_cache: HashMap<(PathBuf, Range), (Document, View)>, + preview_cache: HashMap)>, format_fn: Box Cow>, callback_fn: Box, @@ -290,19 +290,20 @@ impl Picker { { // TODO: store only the doc as key and map of range -> view as value ? // will improve perfomance for pickers where most items are from same file (symbol, goto) - let &mut (ref mut doc, ref mut view) = self - .preview_cache - .entry((path.clone(), range)) - .or_insert_with(|| { + let &mut (ref mut doc, ref mut range_map) = + self.preview_cache.entry(path.clone()).or_insert_with(|| { let doc = Document::open(path, None, Some(&editor.theme), Some(&editor.syn_loader)) .unwrap(); let view = View::new(doc.id()); - (doc, view) + (doc, hashmap!(range => view)) }); + let view = range_map + .entry(range) + .or_insert_with(|| View::new(doc.id())); doc.set_selection(view.id, Selection::from(range)); - commands::align_view(doc, view, Align::Center); + commands::align_view(doc, view, Align::Top); } } @@ -480,7 +481,11 @@ impl Component for Picker { .selection() .and_then(|current| (self.preview_fn)(cx.editor, current)) .and_then(|(path, range)| canonicalize_path(&path).ok().zip(Some(range))) - .and_then(|(path, range)| self.preview_cache.get(&(path, range))) + .and_then(|(path, range)| { + self.preview_cache + .get(&path) + .and_then(|(doc, range_map)| Some((doc, range_map.get(&range)?))) + }) { item_width = inner.width * 40 / 100; From 6df7cfa94b193c3205755c34f9fed3c5c3176b72 Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Sat, 31 Jul 2021 18:25:11 +0530 Subject: [PATCH 05/20] Use line instead of range for preview doc --- helix-term/src/commands.rs | 13 +++++++------ helix-term/src/ui/mod.rs | 3 +-- helix-term/src/ui/picker.rs | 25 +++++++++++++------------ 3 files changed, 21 insertions(+), 20 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 58972cc3482d..0a002bf29575 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2135,11 +2135,9 @@ fn symbol_picker(cx: &mut Context) { // let range = // lsp_range_to_range(doc.text(), symbol.location.range, offset_encoding); // Calculating the exact range is expensive, so use line number only - let pos = doc - .text() - .line_to_char(symbol.location.range.start.line as usize); - let range = Range::point(pos); - doc.path().cloned().zip(Some(range)) + doc.path() + .cloned() + .zip(Some(symbol.location.range.start.line as usize)) }, ); compositor.push(Box::new(picker)) @@ -2529,7 +2527,10 @@ fn goto_impl( move |editor: &mut Editor, location, action| { jump_to(editor, location, offset_encoding, action) }, - |editor, symbol| None, + |_editor, location| { + let path = location.uri.to_file_path().unwrap(); + Some((path, location.range.start.line as usize)) + }, ); compositor.push(Box::new(picker)); } diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 4ff4aa601818..c5c937f21e4b 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -11,7 +11,6 @@ mod text; pub use completion::Completion; pub use editor::EditorView; -use helix_core::Range; pub use markdown::Markdown; pub use menu::Menu; pub use picker::Picker; @@ -127,7 +126,7 @@ pub fn file_picker(root: PathBuf) -> Picker { }, |_editor, path| { // FIXME: directories are creeping up in filepicker - Some((path.clone(), Range::point(0))) + Some((path.clone(), 0)) }, ) } diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index b86804da0947..daad48bf9a2c 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -34,11 +34,12 @@ pub struct Picker { cursor: usize, // pattern: String, prompt: Prompt, - preview_cache: HashMap)>, + // Caches paths to docs to line number to view + preview_cache: HashMap)>, format_fn: Box Cow>, callback_fn: Box, - preview_fn: Box Option<(PathBuf, Range)>>, + preview_fn: Box Option<(PathBuf, usize)>>, } impl Picker { @@ -46,7 +47,7 @@ impl Picker { options: Vec, format_fn: impl Fn(&T) -> Cow + 'static, callback_fn: impl Fn(&mut Editor, &T, Action) + 'static, - preview_fn: impl Fn(&Editor, &T) -> Option<(PathBuf, Range)> + 'static, + preview_fn: impl Fn(&Editor, &T) -> Option<(PathBuf, usize)> + 'static, ) -> Self { let prompt = Prompt::new( "".to_string(), @@ -283,7 +284,7 @@ impl Picker { } fn calculate_preview(&mut self, editor: &Editor) { - if let Some((path, range)) = self + if let Some((path, line)) = self .selection() .and_then(|current| (self.preview_fn)(editor, current)) .and_then(|(path, range)| canonicalize_path(&path).ok().zip(Some(range))) @@ -296,14 +297,14 @@ impl Picker { Document::open(path, None, Some(&editor.theme), Some(&editor.syn_loader)) .unwrap(); let view = View::new(doc.id()); - (doc, hashmap!(range => view)) + (doc, hashmap!(line => view)) }); - let view = range_map - .entry(range) - .or_insert_with(|| View::new(doc.id())); + let view = range_map.entry(line).or_insert_with(|| View::new(doc.id())); + let range = Range::point(doc.text().line_to_char(line)); doc.set_selection(view.id, Selection::from(range)); - commands::align_view(doc, view, Align::Top); + // FIXME: gets aligned top instead of center + commands::align_view(doc, view, Align::Center); } } @@ -480,11 +481,11 @@ impl Component for Picker { if let Some((doc, view)) = self .selection() .and_then(|current| (self.preview_fn)(cx.editor, current)) - .and_then(|(path, range)| canonicalize_path(&path).ok().zip(Some(range))) - .and_then(|(path, range)| { + .and_then(|(path, line)| canonicalize_path(&path).ok().zip(Some(line))) + .and_then(|(path, line)| { self.preview_cache .get(&path) - .and_then(|(doc, range_map)| Some((doc, range_map.get(&range)?))) + .and_then(|(doc, range_map)| Some((doc, range_map.get(&line)?))) }) { item_width = inner.width * 40 / 100; From 2ed40c256ff7a77a92bed43125ca9f85cb3eb403 Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Sat, 31 Jul 2021 19:42:58 +0530 Subject: [PATCH 06/20] Add picker preview for buffer picker --- helix-term/src/commands.rs | 10 +++++++++- helix-term/src/ui/picker.rs | 1 + helix-view/src/document.rs | 4 ++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 0a002bf29575..ecf8d69fa6fc 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2061,7 +2061,15 @@ fn buffer_picker(cx: &mut Context) { |editor: &mut Editor, (id, _path): &(DocumentId, Option), _action| { editor.switch(*id, Action::Replace); }, - |editor, symbol| None, + |editor, (id, path)| { + let doc = &editor.documents.get(*id)?; + let &view_id = doc.selections().keys().next()?; + let line = doc + .selection(view_id) + .primary() + .cursor_line(doc.text().slice(..)); + Some((path.clone()?, line)) + }, ); cx.push_layer(Box::new(picker)); } diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index daad48bf9a2c..25a3c37c78a0 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -39,6 +39,7 @@ pub struct Picker { format_fn: Box Cow>, callback_fn: Box, + #[allow(clippy::type_complexity)] preview_fn: Box Option<(PathBuf, usize)>>, } diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index c02d66563312..ecd481e2aa5f 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -901,6 +901,10 @@ impl Document { &self.selections[&view_id] } + pub fn selections(&self) -> &HashMap { + &self.selections + } + pub fn relative_path(&self) -> Option { let cwdir = std::env::current_dir().expect("couldn't determine current directory"); From 5785258d18b3b3fb6b52c081355d9bac3454808d Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Sat, 31 Jul 2021 20:29:26 +0530 Subject: [PATCH 07/20] Fix render bug and refactor picker --- helix-term/src/compositor.rs | 7 +++++-- helix-term/src/ui/picker.rs | 8 ++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs index c2cfa3a72954..d9dcc94eaa53 100644 --- a/helix-term/src/compositor.rs +++ b/helix-term/src/compositor.rs @@ -53,6 +53,8 @@ pub trait Component: Any + AnyComponent { (None, CursorKind::Hidden) } + fn prepare_for_render(&mut self, _ctx: &Context) {} + /// May be used by the parent component to compute the child area. /// viewport is the maximum allowed area, and the child should stay within those bounds. fn required_size(&mut self, _viewport: (u16, u16)) -> Option<(u16, u16)> { @@ -137,8 +139,9 @@ impl Compositor { let area = *surface.area(); - for layer in &self.layers { - layer.render(area, surface, cx) + for layer in &mut self.layers { + layer.prepare_for_render(cx); + layer.render(area, surface, cx); } let (pos, kind) = self.cursor(area, cx.editor); diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 25a3c37c78a0..e5db5198861a 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -367,7 +367,6 @@ impl Component for Picker { modifiers: KeyModifiers::CONTROL, } => { self.move_up(); - self.calculate_preview(cx.editor); } KeyEvent { code: KeyCode::Down, @@ -381,7 +380,6 @@ impl Component for Picker { modifiers: KeyModifiers::CONTROL, } => { self.move_down(); - self.calculate_preview(cx.editor); } KeyEvent { code: KeyCode::Esc, .. @@ -427,13 +425,11 @@ impl Component for Picker { modifiers: KeyModifiers::CONTROL, } => { self.save_filter(); - self.calculate_preview(cx.editor); } _ => { if let EventResult::Consumed(_) = self.prompt.handle_event(event, cx) { // TODO: recalculate only if pattern changed self.score(); - self.calculate_preview(cx.editor); } } } @@ -441,6 +437,10 @@ impl Component for Picker { EventResult::Consumed(None) } + fn prepare_for_render(&mut self, cx: &Context) { + self.calculate_preview(cx.editor) + } + fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) { let area = inner_rect(area); From 42ebb1b6a21382e278c31c06be6d6b5a91cbda3e Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Sun, 1 Aug 2021 10:24:41 +0530 Subject: [PATCH 08/20] Refactor picker preview rendering --- helix-term/src/ui/editor.rs | 146 +++++++++++++++++++++----------- helix-term/src/ui/picker.rs | 161 +----------------------------------- 2 files changed, 102 insertions(+), 205 deletions(-) diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index a2b169ed4198..f10288f8da12 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -104,9 +104,10 @@ impl EditorView { self.render_statusline(doc, view, area, surface, theme, is_focused); } + /// Render a document into a Rect with syntax highlighting, + /// diagnostics, matching brackets and selections. #[allow(clippy::too_many_arguments)] - pub fn render_buffer( - &self, + pub fn render_doc( doc: &Document, view: &View, viewport: Rect, @@ -116,9 +117,7 @@ impl EditorView { loader: &syntax::Loader, ) { let text = doc.text().slice(..); - let last_line = view.last_line(doc); - let range = { // calculate viewport byte ranges let start = text.line_to_byte(view.first_line); @@ -128,7 +127,7 @@ impl EditorView { }; // TODO: range doesn't actually restrict source, just highlight range - let highlights: Vec<_> = match doc.syntax() { + let highlights = match doc.syntax() { Some(syntax) => { let scopes = theme.scopes(); syntax @@ -150,20 +149,16 @@ impl EditorView { Some(config_ref) }) }) + .map(|event| event.unwrap()) .collect() // TODO: we collect here to avoid holding the lock, fix later } - None => vec![Ok(HighlightEvent::Source { + None => vec![HighlightEvent::Source { start: range.start, end: range.end, - })], - }; - let mut spans = Vec::new(); - let mut visual_x = 0u16; - let mut line = 0u16; - let tab_width = doc.tab_width(); - let tab = " ".repeat(tab_width); - - let highlights = highlights.into_iter().map(|event| match event.unwrap() { + }], + } + .into_iter() + .map(|event| match event { // convert byte offsets to char offset HighlightEvent::Source { start, end } => { let start = ensure_grapheme_boundary_next(text, text.byte_to_char(start)); @@ -250,6 +245,12 @@ impl EditorView { .collect(), )); + let mut spans = Vec::new(); + let mut visual_x = 0u16; + let mut line = 0u16; + let tab_width = doc.tab_width(); + let tab = " ".repeat(tab_width); + 'outer: for event in highlights { match event { HighlightEvent::HighlightStart(span) => { @@ -323,7 +324,72 @@ impl EditorView { } } - // render gutters + if is_focused { + let screen = { + let start = text.line_to_char(view.first_line); + let end = text.line_to_char(last_line + 1) + 1; // +1 for cursor at end of text. + Range::new(start, end) + }; + + let selection = doc.selection(view.id); + + for selection in selection.iter().filter(|range| range.overlaps(&screen)) { + let head = view.screen_coords_at_pos( + doc, + text, + if selection.head > selection.anchor { + selection.head - 1 + } else { + selection.head + }, + ); + if head.is_some() { + // TODO: set cursor position for IME + if let Some(syntax) = doc.syntax() { + use helix_core::match_brackets; + let pos = doc + .selection(view.id) + .primary() + .cursor(doc.text().slice(..)); + let pos = match_brackets::find(syntax, doc.text(), pos) + .and_then(|pos| view.screen_coords_at_pos(doc, text, pos)); + + if let Some(pos) = pos { + // ensure col is on screen + if (pos.col as u16) < viewport.width + view.first_col as u16 + && pos.col >= view.first_col + { + let style = theme.try_get("ui.cursor.match").unwrap_or_else(|| { + Style::default() + .add_modifier(Modifier::REVERSED) + .add_modifier(Modifier::DIM) + }); + + surface + .get_mut( + viewport.x + pos.col as u16, + viewport.y + pos.row as u16, + ) + .set_style(style); + } + } + } + } + } + } + } + + #[allow(clippy::too_many_arguments)] + pub fn render_gutter( + doc: &Document, + view: &View, + viewport: Rect, + surface: &mut Surface, + theme: &Theme, + is_focused: bool, + ) { + let text = doc.text().slice(..); + let last_line = view.last_line(doc); let linenr: Style = theme.get("ui.linenr"); let warning: Style = theme.get("warning"); @@ -368,7 +434,7 @@ impl EditorView { ); } - // render selections and selected linenr(s) + // render selected linenr(s) let linenr_select: Style = theme .try_get("ui.linenr.selected") .unwrap_or_else(|| theme.get("ui.linenr")); @@ -407,42 +473,26 @@ impl EditorView { 5, linenr_select, ); - - // TODO: set cursor position for IME - if let Some(syntax) = doc.syntax() { - use helix_core::match_brackets; - let pos = doc - .selection(view.id) - .primary() - .cursor(doc.text().slice(..)); - let pos = match_brackets::find(syntax, doc.text(), pos) - .and_then(|pos| view.screen_coords_at_pos(doc, text, pos)); - - if let Some(pos) = pos { - // ensure col is on screen - if (pos.col as u16) < viewport.width + view.first_col as u16 - && pos.col >= view.first_col - { - let style = theme.try_get("ui.cursor.match").unwrap_or_else(|| { - Style::default() - .add_modifier(Modifier::REVERSED) - .add_modifier(Modifier::DIM) - }); - - surface - .get_mut( - viewport.x + pos.col as u16, - viewport.y + pos.row as u16, - ) - .set_style(style); - } - } - } } } } } + #[allow(clippy::too_many_arguments)] + pub fn render_buffer( + &self, + doc: &Document, + view: &View, + viewport: Rect, + surface: &mut Surface, + theme: &Theme, + is_focused: bool, + loader: &syntax::Loader, + ) { + Self::render_doc(doc, view, viewport, surface, theme, is_focused, loader); + Self::render_gutter(doc, view, viewport, surface, theme, is_focused); + } + pub fn render_diagnostics( &self, doc: &Document, diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index e5db5198861a..d1d0a97869cf 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -1,6 +1,7 @@ use crate::{ commands::{self, Align}, compositor::{Component, Compositor, Context, EventResult}, + ui::EditorView, }; use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; use tui::{ @@ -78,161 +79,6 @@ impl Picker { picker } - // TODO: Copied from EditorView::render_buffer, reuse - #[allow(clippy::too_many_arguments)] - fn render_buffer( - &self, - doc: &Document, - view: &helix_view::View, - viewport: Rect, - surface: &mut Surface, - theme: &helix_view::Theme, - loader: &helix_core::syntax::Loader, - ) { - let text = doc.text().slice(..); - - let last_line = view.last_line(doc); - - let range = { - // calculate viewport byte ranges - let start = text.line_to_byte(view.first_line); - let end = text.line_to_byte(last_line + 1); - - start..end - }; - - // TODO: range doesn't actually restrict source, just highlight range - let highlights: Vec<_> = match doc.syntax() { - Some(syntax) => { - let scopes = theme.scopes(); - syntax - .highlight_iter(text.slice(..), Some(range), None, |language| { - loader - .language_config_for_scope(&format!("source.{}", language)) - .and_then(|language_config| { - let config = language_config.highlight_config(scopes)?; - let config_ref = config.as_ref(); - // SAFETY: the referenced `HighlightConfiguration` behind - // the `Arc` is guaranteed to remain valid throughout the - // duration of the highlight. - let config_ref = unsafe { - std::mem::transmute::< - _, - &'static helix_core::syntax::HighlightConfiguration, - >(config_ref) - }; - Some(config_ref) - }) - }) - .collect() // TODO: we collect here to avoid holding the lock, fix later - } - None => vec![Ok(helix_core::syntax::HighlightEvent::Source { - start: range.start, - end: range.end, - })], - }; - let mut spans = Vec::new(); - let mut visual_x = 0u16; - let mut line = 0u16; - let tab_width = doc.tab_width(); - let tab = " ".repeat(tab_width); - - let highlights = highlights.into_iter().map(|event| match event.unwrap() { - // convert byte offsets to char offset - helix_core::syntax::HighlightEvent::Source { start, end } => { - let start = helix_core::graphemes::ensure_grapheme_boundary_next( - text, - text.byte_to_char(start), - ); - let end = helix_core::graphemes::ensure_grapheme_boundary_next( - text, - text.byte_to_char(end), - ); - helix_core::syntax::HighlightEvent::Source { start, end } - } - event => event, - }); - - // let selections = doc.selection(view.id); - // let primary_idx = selections.primary_index(); - // let selection_scope = theme - // .find_scope_index("ui.selection") - // .expect("no selection scope found!"); - - 'outer: for event in highlights { - match event { - helix_core::syntax::HighlightEvent::HighlightStart(span) => { - spans.push(span); - } - helix_core::syntax::HighlightEvent::HighlightEnd => { - spans.pop(); - } - helix_core::syntax::HighlightEvent::Source { start, end } => { - // `unwrap_or_else` part is for off-the-end indices of - // the rope, to allow cursor highlighting at the end - // of the rope. - let text = text.get_slice(start..end).unwrap_or_else(|| " ".into()); - - use helix_core::graphemes::{grapheme_width, RopeGraphemes}; - - let style = spans.iter().fold(theme.get("ui.text"), |acc, span| { - let style = theme.get(theme.scopes()[span.0].as_str()); - acc.patch(style) - }); - - for grapheme in RopeGraphemes::new(text) { - let out_of_bounds = visual_x < view.first_col as u16 - || visual_x >= viewport.width + view.first_col as u16; - - if helix_core::LineEnding::from_rope_slice(&grapheme).is_some() { - if !out_of_bounds { - // we still want to render an empty cell with the style - surface.set_string( - viewport.x + visual_x - view.first_col as u16, - viewport.y + line, - " ", - style, - ); - } - - visual_x = 0; - line += 1; - - // TODO: with proper iter this shouldn't be necessary - if line >= viewport.height { - break 'outer; - } - } else { - let grapheme = Cow::from(grapheme); - - let (grapheme, width) = if grapheme == "\t" { - // make sure we display tab as appropriate amount of spaces - (tab.as_str(), tab_width) - } else { - // Cow will prevent allocations if span contained in a single slice - // which should really be the majority case - let width = grapheme_width(&grapheme); - (grapheme.as_ref(), width) - }; - - if !out_of_bounds { - // if we're offscreen just keep going until we hit a new line - surface.set_string( - viewport.x + visual_x - view.first_col as u16, - viewport.y + line, - grapheme, - style, - ); - } - - visual_x = visual_x.saturating_add(width as u16); - } - } - } - } - } - } - pub fn score(&mut self) { // need to borrow via pattern match otherwise it complains about simultaneous borrow let Self { @@ -288,7 +134,7 @@ impl Picker { if let Some((path, line)) = self .selection() .and_then(|current| (self.preview_fn)(editor, current)) - .and_then(|(path, range)| canonicalize_path(&path).ok().zip(Some(range))) + .and_then(|(path, line)| canonicalize_path(&path).ok().zip(Some(line))) { // TODO: store only the doc as key and map of range -> view as value ? // will improve perfomance for pickers where most items are from same file (symbol, goto) @@ -507,12 +353,13 @@ impl Component for Picker { // FIXME: last line will not be highlighted because of a -1 in View::last_line let mut view = view.clone(); view.area = viewport; - self.render_buffer( + EditorView::render_doc( doc, &view, viewport, surface, &cx.editor.theme, + true, // is_focused &cx.editor.syn_loader, ); } From bd5c7b5f4b82338d1919262a5eec632213d21536 Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Sun, 1 Aug 2021 20:57:18 +0530 Subject: [PATCH 09/20] Split picker and preview and compose The current selected item is cloned on every event, which is undesirable --- helix-term/src/commands.rs | 10 +- helix-term/src/ui/mod.rs | 6 +- helix-term/src/ui/picker.rs | 218 ++++++++++++++++++++++-------------- 3 files changed, 143 insertions(+), 91 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index ecf8d69fa6fc..99bf8d9d1bae 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -31,7 +31,7 @@ use movement::Movement; use crate::{ compositor::{self, Component, Compositor}, - ui::{self, Picker, Popup, Prompt, PromptEvent}, + ui::{self, FilePicker, Popup, Prompt, PromptEvent}, }; use crate::job::{self, Job, Jobs}; @@ -2039,7 +2039,7 @@ fn file_picker(cx: &mut Context) { fn buffer_picker(cx: &mut Context) { let current = view!(cx.editor).doc; - let picker = Picker::new( + let picker = FilePicker::new( cx.editor .documents .iter() @@ -2123,7 +2123,7 @@ fn symbol_picker(cx: &mut Context) { } }; - let picker = Picker::new( + let picker = FilePicker::new( symbols, |symbol| (&symbol.name).into(), move |editor: &mut Editor, symbol, _action| { @@ -2177,7 +2177,7 @@ pub fn code_action(cx: &mut Context) { compositor: &mut Compositor, response: Option| { if let Some(actions) = response { - let picker = Picker::new( + let picker = FilePicker::new( actions, |action| match action { lsp::CodeActionOrCommand::CodeAction(action) => { @@ -2525,7 +2525,7 @@ fn goto_impl( editor.set_error("No definition found.".to_string()); } _locations => { - let picker = ui::Picker::new( + let picker = FilePicker::new( locations, |location| { let file = location.uri.as_str(); diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index c5c937f21e4b..704f1e3c69c9 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -13,7 +13,7 @@ pub use completion::Completion; pub use editor::EditorView; pub use markdown::Markdown; pub use menu::Menu; -pub use picker::Picker; +pub use picker::FilePicker; pub use popup::Popup; pub use prompt::{Prompt, PromptEvent}; pub use spinner::{ProgressSpinners, Spinner}; @@ -73,7 +73,7 @@ pub fn regex_prompt( ) } -pub fn file_picker(root: PathBuf) -> Picker { +pub fn file_picker(root: PathBuf) -> FilePicker { use ignore::Walk; use std::time; let files = Walk::new(root.clone()).filter_map(|entry| match entry { @@ -109,7 +109,7 @@ pub fn file_picker(root: PathBuf) -> Picker { let files = files.into_iter().map(|(path, _)| path).collect(); - Picker::new( + FilePicker::new( files, move |path: &PathBuf| { // format_fn diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index d1d0a97869cf..24706983b4d1 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -11,6 +11,7 @@ use tui::{ use fuzzy_matcher::skim::SkimMatcherV2 as Matcher; use fuzzy_matcher::FuzzyMatcher; +use tui::widgets::Widget; use std::{borrow::Cow, collections::HashMap, path::PathBuf}; @@ -23,6 +24,139 @@ use helix_view::{ Document, Editor, View, }; +pub struct FilePicker { + picker: Picker, + preview: Preview, +} + +impl FilePicker { + pub fn new( + options: Vec, + format_fn: impl Fn(&T) -> Cow + 'static, + callback_fn: impl Fn(&mut Editor, &T, Action) + 'static, + preview_fn: impl Fn(&Editor, &T) -> Option<(PathBuf, usize)> + 'static, + ) -> Self { + Self { + picker: Picker::new(options, format_fn, callback_fn), + preview: Preview::new(preview_fn), + } + } +} + +impl Component for FilePicker { + fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) { + let area = inner_rect(area); + let picker_area = Rect::new(area.x, area.y, area.width / 2, area.height); + let preview_area = Rect::new( + area.x + picker_area.width, + area.y, + area.width / 2, + area.height, + ); + self.picker.render(picker_area, surface, cx); + self.preview.render(preview_area, surface, cx); + } + + fn prepare_for_render(&mut self, cx: &Context) { + self.preview.current = self.picker.selection().cloned(); + self.preview.calculate_preview(cx.editor); + } + + fn handle_event(&mut self, event: Event, ctx: &mut Context) -> EventResult { + let result = self.picker.handle_event(event, ctx); + self.preview.current = self.picker.selection().cloned(); + result + } + + fn cursor(&self, area: Rect, ctx: &Editor) -> (Option, CursorKind) { + self.picker.cursor(area, ctx) + } +} + +pub struct Preview { + pub current: Option, + // Caches paths to docs to line number to view + cache: HashMap)>, + #[allow(clippy::type_complexity)] + preview_fn: Box Option<(PathBuf, usize)>>, +} + +impl Preview { + fn new(preview_fn: impl Fn(&Editor, &T) -> Option<(PathBuf, usize)> + 'static) -> Self { + Self { + current: None, + cache: HashMap::new(), + preview_fn: Box::new(preview_fn), + } + } + + fn calculate_preview(&mut self, editor: &Editor) { + if let Some((path, line)) = self + .current + .as_ref() + .and_then(|current| (self.preview_fn)(editor, current)) + .and_then(|(path, line)| canonicalize_path(&path).ok().zip(Some(line))) + { + let &mut (ref mut doc, ref mut range_map) = + self.cache.entry(path.clone()).or_insert_with(|| { + let doc = + Document::open(path, None, Some(&editor.theme), Some(&editor.syn_loader)) + .unwrap(); + let view = View::new(doc.id()); + (doc, hashmap!(line => view)) + }); + let view = range_map.entry(line).or_insert_with(|| View::new(doc.id())); + + let range = Range::point(doc.text().line_to_char(line)); + doc.set_selection(view.id, Selection::from(range)); + // FIXME: gets aligned top instead of center + commands::align_view(doc, view, Align::Center); + } + } +} + +impl Component for Preview { + fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) { + // -- Render the frame: + // clear area + let background = cx.editor.theme.get("ui.background"); + 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); + + block.render(area, surface); + + if let Some((doc, view)) = self + .current + .as_ref() + .and_then(|current| (self.preview_fn)(cx.editor, current)) + .and_then(|(path, line)| canonicalize_path(&path).ok().zip(Some(line))) + .and_then(|(path, line)| { + self.cache + .get(&path) + .and_then(|(doc, range_map)| Some((doc, range_map.get(&line)?))) + }) + { + // FIXME: last line will not be highlighted because of a -1 in View::last_line + let mut view = view.clone(); + view.area = inner; + EditorView::render_doc( + doc, + &view, + inner, + surface, + &cx.editor.theme, + true, // is_focused + &cx.editor.syn_loader, + ); + } + } +} + pub struct Picker { options: Vec, // filter: String, @@ -35,13 +169,9 @@ pub struct Picker { cursor: usize, // pattern: String, prompt: Prompt, - // Caches paths to docs to line number to view - preview_cache: HashMap)>, format_fn: Box Cow>, callback_fn: Box, - #[allow(clippy::type_complexity)] - preview_fn: Box Option<(PathBuf, usize)>>, } impl Picker { @@ -49,7 +179,6 @@ impl Picker { options: Vec, format_fn: impl Fn(&T) -> Cow + 'static, callback_fn: impl Fn(&mut Editor, &T, Action) + 'static, - preview_fn: impl Fn(&Editor, &T) -> Option<(PathBuf, usize)> + 'static, ) -> Self { let prompt = Prompt::new( "".to_string(), @@ -67,10 +196,8 @@ impl Picker { filters: Vec::new(), cursor: 0, prompt, - preview_cache: HashMap::new(), format_fn: Box::new(format_fn), callback_fn: Box::new(callback_fn), - preview_fn: Box::new(preview_fn), }; // TODO: scoring on empty input should just use a fastpath @@ -130,31 +257,6 @@ impl Picker { } } - fn calculate_preview(&mut self, editor: &Editor) { - if let Some((path, line)) = self - .selection() - .and_then(|current| (self.preview_fn)(editor, current)) - .and_then(|(path, line)| canonicalize_path(&path).ok().zip(Some(line))) - { - // TODO: store only the doc as key and map of range -> view as value ? - // will improve perfomance for pickers where most items are from same file (symbol, goto) - let &mut (ref mut doc, ref mut range_map) = - self.preview_cache.entry(path.clone()).or_insert_with(|| { - let doc = - Document::open(path, None, Some(&editor.theme), Some(&editor.syn_loader)) - .unwrap(); - let view = View::new(doc.id()); - (doc, hashmap!(line => view)) - }); - let view = range_map.entry(line).or_insert_with(|| View::new(doc.id())); - - let range = Range::point(doc.text().line_to_char(line)); - doc.set_selection(view.id, Selection::from(range)); - // FIXME: gets aligned top instead of center - commands::align_view(doc, view, Align::Center); - } - } - pub fn selection(&self) -> Option<&T> { self.matches .get(self.cursor) @@ -234,7 +336,6 @@ impl Component for Picker { code: KeyCode::Char('c'), modifiers: KeyModifiers::CONTROL, } => { - self.preview_cache.clear(); return close_fn; } KeyEvent { @@ -244,7 +345,6 @@ impl Component for Picker { if let Some(option) = self.selection() { (self.callback_fn)(&mut cx.editor, option, Action::Replace); } - self.preview_cache.clear(); return close_fn; } KeyEvent { @@ -263,7 +363,6 @@ impl Component for Picker { if let Some(option) = self.selection() { (self.callback_fn)(&mut cx.editor, option, Action::VerticalSplit); } - self.preview_cache.clear(); return close_fn; } KeyEvent { @@ -283,20 +382,13 @@ impl Component for Picker { EventResult::Consumed(None) } - fn prepare_for_render(&mut self, cx: &Context) { - self.calculate_preview(cx.editor) - } - fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) { - let area = inner_rect(area); - // -- Render the frame: // clear area let background = cx.editor.theme.get("ui.background"); surface.clear_with(area, background); - use tui::widgets::Widget; // don't like this but the lifetime sucks let block = Block::default().borders(Borders::ALL); @@ -323,46 +415,6 @@ impl Component for Picker { // -- Render the contents: // subtract the area of the prompt (-2) and current item marker " > " (-3) let inner = Rect::new(inner.x + 3, inner.y + 2, inner.width - 3, inner.height - 2); - let mut item_width = inner.width; - - if let Some((doc, view)) = self - .selection() - .and_then(|current| (self.preview_fn)(cx.editor, current)) - .and_then(|(path, line)| canonicalize_path(&path).ok().zip(Some(line))) - .and_then(|(path, line)| { - self.preview_cache - .get(&path) - .and_then(|(doc, range_map)| Some((doc, range_map.get(&line)?))) - }) - { - item_width = inner.width * 40 / 100; - - for y in inner.top()..inner.bottom() { - surface - .get_mut(inner.x + item_width, y) - .set_symbol(borders.vertical) - .set_style(sep_style); - } - - let viewport = Rect::new( - inner.x + item_width + 1, // 1 for sep - inner.y, - inner.width * 60 / 100, - inner.height, - ); - // FIXME: last line will not be highlighted because of a -1 in View::last_line - let mut view = view.clone(); - view.area = viewport; - EditorView::render_doc( - doc, - &view, - viewport, - surface, - &cx.editor.theme, - true, // is_focused - &cx.editor.syn_loader, - ); - } let style = cx.editor.theme.get("ui.text"); let selected = Style::default().fg(Color::Rgb(255, 255, 255)); @@ -383,7 +435,7 @@ impl Component for Picker { inner.x, inner.y + i as u16, (self.format_fn)(option), - item_width as usize, + inner.width as usize, if i == (self.cursor - offset) { selected } else { From 29b0757c0df2cd21f2a90bfdf0e4d9eeaf8497a1 Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Mon, 2 Aug 2021 14:59:52 +0530 Subject: [PATCH 10/20] Refactor out clones in previewed picker --- helix-term/src/commands.rs | 10 ++-- helix-term/src/ui/mod.rs | 6 +-- helix-term/src/ui/picker.rs | 100 ++++++++++++++---------------------- 3 files changed, 47 insertions(+), 69 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 99bf8d9d1bae..55ab337c017f 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -31,7 +31,7 @@ use movement::Movement; use crate::{ compositor::{self, Component, Compositor}, - ui::{self, FilePicker, Popup, Prompt, PromptEvent}, + ui::{self, Popup, PreviewedPicker, Prompt, PromptEvent}, }; use crate::job::{self, Job, Jobs}; @@ -2039,7 +2039,7 @@ fn file_picker(cx: &mut Context) { fn buffer_picker(cx: &mut Context) { let current = view!(cx.editor).doc; - let picker = FilePicker::new( + let picker = PreviewedPicker::new( cx.editor .documents .iter() @@ -2123,7 +2123,7 @@ fn symbol_picker(cx: &mut Context) { } }; - let picker = FilePicker::new( + let picker = PreviewedPicker::new( symbols, |symbol| (&symbol.name).into(), move |editor: &mut Editor, symbol, _action| { @@ -2177,7 +2177,7 @@ pub fn code_action(cx: &mut Context) { compositor: &mut Compositor, response: Option| { if let Some(actions) = response { - let picker = FilePicker::new( + let picker = PreviewedPicker::new( actions, |action| match action { lsp::CodeActionOrCommand::CodeAction(action) => { @@ -2525,7 +2525,7 @@ fn goto_impl( editor.set_error("No definition found.".to_string()); } _locations => { - let picker = FilePicker::new( + let picker = PreviewedPicker::new( locations, |location| { let file = location.uri.as_str(); diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 704f1e3c69c9..3b0e619f7f58 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -13,7 +13,7 @@ pub use completion::Completion; pub use editor::EditorView; pub use markdown::Markdown; pub use menu::Menu; -pub use picker::FilePicker; +pub use picker::PreviewedPicker; pub use popup::Popup; pub use prompt::{Prompt, PromptEvent}; pub use spinner::{ProgressSpinners, Spinner}; @@ -73,7 +73,7 @@ pub fn regex_prompt( ) } -pub fn file_picker(root: PathBuf) -> FilePicker { +pub fn file_picker(root: PathBuf) -> PreviewedPicker { use ignore::Walk; use std::time; let files = Walk::new(root.clone()).filter_map(|entry| match entry { @@ -109,7 +109,7 @@ pub fn file_picker(root: PathBuf) -> FilePicker { let files = files.into_iter().map(|(path, _)| path).collect(); - FilePicker::new( + PreviewedPicker::new( files, move |path: &PathBuf| { // format_fn diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 24706983b4d1..d58064459f35 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -24,12 +24,15 @@ use helix_view::{ Document, Editor, View, }; -pub struct FilePicker { +pub struct PreviewedPicker { picker: Picker, - preview: Preview, + // Caches paths to docs to line number to view + preview_cache: HashMap)>, + #[allow(clippy::type_complexity)] + preview_fn: Box Option<(PathBuf, usize)>>, } -impl FilePicker { +impl PreviewedPicker { pub fn new( options: Vec, format_fn: impl Fn(&T) -> Cow + 'static, @@ -38,67 +41,20 @@ impl FilePicker { ) -> Self { Self { picker: Picker::new(options, format_fn, callback_fn), - preview: Preview::new(preview_fn), - } - } -} - -impl Component for FilePicker { - fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) { - let area = inner_rect(area); - let picker_area = Rect::new(area.x, area.y, area.width / 2, area.height); - let preview_area = Rect::new( - area.x + picker_area.width, - area.y, - area.width / 2, - area.height, - ); - self.picker.render(picker_area, surface, cx); - self.preview.render(preview_area, surface, cx); - } - - fn prepare_for_render(&mut self, cx: &Context) { - self.preview.current = self.picker.selection().cloned(); - self.preview.calculate_preview(cx.editor); - } - - fn handle_event(&mut self, event: Event, ctx: &mut Context) -> EventResult { - let result = self.picker.handle_event(event, ctx); - self.preview.current = self.picker.selection().cloned(); - result - } - - fn cursor(&self, area: Rect, ctx: &Editor) -> (Option, CursorKind) { - self.picker.cursor(area, ctx) - } -} - -pub struct Preview { - pub current: Option, - // Caches paths to docs to line number to view - cache: HashMap)>, - #[allow(clippy::type_complexity)] - preview_fn: Box Option<(PathBuf, usize)>>, -} - -impl Preview { - fn new(preview_fn: impl Fn(&Editor, &T) -> Option<(PathBuf, usize)> + 'static) -> Self { - Self { - current: None, - cache: HashMap::new(), + preview_cache: HashMap::new(), preview_fn: Box::new(preview_fn), } } fn calculate_preview(&mut self, editor: &Editor) { if let Some((path, line)) = self - .current - .as_ref() + .picker + .selection() .and_then(|current| (self.preview_fn)(editor, current)) .and_then(|(path, line)| canonicalize_path(&path).ok().zip(Some(line))) { let &mut (ref mut doc, ref mut range_map) = - self.cache.entry(path.clone()).or_insert_with(|| { + self.preview_cache.entry(path.clone()).or_insert_with(|| { let doc = Document::open(path, None, Some(&editor.theme), Some(&editor.syn_loader)) .unwrap(); @@ -115,28 +71,38 @@ impl Preview { } } -impl Component for Preview { +impl Component for PreviewedPicker { fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) { + let area = inner_rect(area); // -- Render the frame: // clear area let background = cx.editor.theme.get("ui.background"); surface.clear_with(area, background); + let picker_area = Rect::new(area.x, area.y, area.width / 2, area.height); + let preview_area = Rect::new( + area.x + picker_area.width, + area.y, + area.width / 2, + area.height, + ); + self.picker.render(picker_area, surface, cx); + // 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); + let inner = block.inner(preview_area); - block.render(area, surface); + block.render(preview_area, surface); if let Some((doc, view)) = self - .current - .as_ref() + .picker + .selection() .and_then(|current| (self.preview_fn)(cx.editor, current)) .and_then(|(path, line)| canonicalize_path(&path).ok().zip(Some(line))) .and_then(|(path, line)| { - self.cache + self.preview_cache .get(&path) .and_then(|(doc, range_map)| Some((doc, range_map.get(&line)?))) }) @@ -155,6 +121,19 @@ impl Component for Preview { ); } } + + fn prepare_for_render(&mut self, cx: &Context) { + self.calculate_preview(cx.editor); + } + + fn handle_event(&mut self, event: Event, ctx: &mut Context) -> EventResult { + // TODO: keybinds for scrolling preview + self.picker.handle_event(event, ctx) + } + + fn cursor(&self, area: Rect, ctx: &Editor) -> (Option, CursorKind) { + self.picker.cursor(area, ctx) + } } pub struct Picker { @@ -384,7 +363,6 @@ impl Component for Picker { fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) { // -- Render the frame: - // clear area let background = cx.editor.theme.get("ui.background"); surface.clear_with(area, background); From 3ab2823b99db65a94f7a093d1a1c2232922906f0 Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Tue, 3 Aug 2021 11:49:07 +0530 Subject: [PATCH 11/20] Retrieve doc from editor if possible in filepicker --- helix-term/src/commands.rs | 10 ++-- helix-term/src/ui/mod.rs | 6 +- helix-term/src/ui/picker.rs | 106 +++++++++++++++++++++--------------- helix-view/src/document.rs | 9 +-- helix-view/src/editor.rs | 11 +++- 5 files changed, 85 insertions(+), 57 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 55ab337c017f..99bf8d9d1bae 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -31,7 +31,7 @@ use movement::Movement; use crate::{ compositor::{self, Component, Compositor}, - ui::{self, Popup, PreviewedPicker, Prompt, PromptEvent}, + ui::{self, FilePicker, Popup, Prompt, PromptEvent}, }; use crate::job::{self, Job, Jobs}; @@ -2039,7 +2039,7 @@ fn file_picker(cx: &mut Context) { fn buffer_picker(cx: &mut Context) { let current = view!(cx.editor).doc; - let picker = PreviewedPicker::new( + let picker = FilePicker::new( cx.editor .documents .iter() @@ -2123,7 +2123,7 @@ fn symbol_picker(cx: &mut Context) { } }; - let picker = PreviewedPicker::new( + let picker = FilePicker::new( symbols, |symbol| (&symbol.name).into(), move |editor: &mut Editor, symbol, _action| { @@ -2177,7 +2177,7 @@ pub fn code_action(cx: &mut Context) { compositor: &mut Compositor, response: Option| { if let Some(actions) = response { - let picker = PreviewedPicker::new( + let picker = FilePicker::new( actions, |action| match action { lsp::CodeActionOrCommand::CodeAction(action) => { @@ -2525,7 +2525,7 @@ fn goto_impl( editor.set_error("No definition found.".to_string()); } _locations => { - let picker = PreviewedPicker::new( + let picker = FilePicker::new( locations, |location| { let file = location.uri.as_str(); diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 3b0e619f7f58..704f1e3c69c9 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -13,7 +13,7 @@ pub use completion::Completion; pub use editor::EditorView; pub use markdown::Markdown; pub use menu::Menu; -pub use picker::PreviewedPicker; +pub use picker::FilePicker; pub use popup::Popup; pub use prompt::{Prompt, PromptEvent}; pub use spinner::{ProgressSpinners, Spinner}; @@ -73,7 +73,7 @@ pub fn regex_prompt( ) } -pub fn file_picker(root: PathBuf) -> PreviewedPicker { +pub fn file_picker(root: PathBuf) -> FilePicker { use ignore::Walk; use std::time; let files = Walk::new(root.clone()).filter_map(|entry| match entry { @@ -109,7 +109,7 @@ pub fn file_picker(root: PathBuf) -> PreviewedPicker { let files = files.into_iter().map(|(path, _)| path).collect(); - PreviewedPicker::new( + FilePicker::new( files, move |path: &PathBuf| { // format_fn diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index d58064459f35..368eb60255b2 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -1,5 +1,4 @@ use crate::{ - commands::{self, Align}, compositor::{Component, Compositor, Context, EventResult}, ui::EditorView, }; @@ -16,63 +15,72 @@ use tui::widgets::Widget; use std::{borrow::Cow, collections::HashMap, path::PathBuf}; use crate::ui::{Prompt, PromptEvent}; -use helix_core::{hashmap, Position, Range, Selection}; +use helix_core::{Position, Selection}; use helix_view::{ document::canonicalize_path, editor::Action, graphics::{Color, CursorKind, Rect, Style}, - Document, Editor, View, + Document, Editor, View, ViewId, }; -pub struct PreviewedPicker { +/// File path and line number +type FileLocation = (PathBuf, usize); + +pub struct FilePicker { picker: Picker, - // Caches paths to docs to line number to view - preview_cache: HashMap)>, - #[allow(clippy::type_complexity)] - preview_fn: Box Option<(PathBuf, usize)>>, + /// Caches paths to documents + preview_cache: HashMap, + /// Given an item in the picker, return the file path and line number to display. + file_fn: Box Option>, + // A view id to be shared by all documents in the cache. Mostly a hack since a doc + // requires at least one selection. + _preview_view_id: ViewId, } -impl PreviewedPicker { +impl FilePicker { pub fn new( options: Vec, format_fn: impl Fn(&T) -> Cow + 'static, callback_fn: impl Fn(&mut Editor, &T, Action) + 'static, - preview_fn: impl Fn(&Editor, &T) -> Option<(PathBuf, usize)> + 'static, + preview_fn: impl Fn(&Editor, &T) -> Option + 'static, ) -> Self { Self { picker: Picker::new(options, format_fn, callback_fn), preview_cache: HashMap::new(), - preview_fn: Box::new(preview_fn), + file_fn: Box::new(preview_fn), + _preview_view_id: ViewId::default(), } } - fn calculate_preview(&mut self, editor: &Editor) { - if let Some((path, line)) = self - .picker + fn current_file(&self, editor: &Editor) -> Option { + self.picker .selection() - .and_then(|current| (self.preview_fn)(editor, current)) + .and_then(|current| (self.file_fn)(editor, current)) .and_then(|(path, line)| canonicalize_path(&path).ok().zip(Some(line))) - { - let &mut (ref mut doc, ref mut range_map) = - self.preview_cache.entry(path.clone()).or_insert_with(|| { - let doc = - Document::open(path, None, Some(&editor.theme), Some(&editor.syn_loader)) - .unwrap(); - let view = View::new(doc.id()); - (doc, hashmap!(line => view)) - }); - let view = range_map.entry(line).or_insert_with(|| View::new(doc.id())); - - let range = Range::point(doc.text().line_to_char(line)); - doc.set_selection(view.id, Selection::from(range)); - // FIXME: gets aligned top instead of center - commands::align_view(doc, view, Align::Center); + } + + fn calculate_preview(&mut self, editor: &Editor) { + if let Some((path, _line)) = self.current_file(editor) { + if !self.preview_cache.contains_key(&path) && editor.document_by_path(&path).is_none() { + let mut doc = + Document::open(&path, None, Some(&editor.theme), Some(&editor.syn_loader)) + .unwrap(); + // HACK: a doc needs atleast one selection + doc.set_selection(self._preview_view_id, Selection::point(0)); + self.preview_cache.insert(path, doc); + } } } } -impl Component for PreviewedPicker { +impl Component for FilePicker { fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) { + // |---------| |---------| + // |prompt | |preview | + // |---------| | | + // |picker | | | + // | | | | + // |---------| |---------| let area = inner_rect(area); // -- Render the frame: // clear area @@ -96,19 +104,23 @@ impl Component for PreviewedPicker { block.render(preview_area, surface); - if let Some((doc, view)) = self - .picker - .selection() - .and_then(|current| (self.preview_fn)(cx.editor, current)) - .and_then(|(path, line)| canonicalize_path(&path).ok().zip(Some(line))) - .and_then(|(path, line)| { - self.preview_cache - .get(&path) - .and_then(|(doc, range_map)| Some((doc, range_map.get(&line)?))) - }) - { + if let Some((doc, line)) = self.current_file(cx.editor).and_then(|(path, line)| { + cx.editor + .document_by_path(&path) + .or_else(|| self.preview_cache.get(&path)) + .zip(Some(line)) + }) { // FIXME: last line will not be highlighted because of a -1 in View::last_line - let mut view = view.clone(); + let mut view = View::new(doc.id()); + view.id = if doc.selections().contains_key(&self._preview_view_id) { + self._preview_view_id // doc from cache + } else { + // Any view will do since we do not depend on doc selections for highlighting + *doc.selections().keys().next().unwrap() // doc from editor + }; + view.first_col = 0; + // align to middle + view.first_line = line.saturating_sub(inner.height as usize / 2); view.area = inner; EditorView::render_doc( doc, @@ -116,9 +128,15 @@ impl Component for PreviewedPicker { inner, surface, &cx.editor.theme, - true, // is_focused + false, // is_focused &cx.editor.syn_loader, ); + // highlight the line + for x in inner.left()..inner.right() { + surface + .get_mut(x, inner.y + line.saturating_sub(view.first_line) as u16) + .set_style(cx.editor.theme.get("ui.selection.primary")); + } } } diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index ecd481e2aa5f..8c9552cba742 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -431,15 +431,16 @@ impl Document { // TODO: async fn? /// Create a new document from `path`. Encoding is auto-detected, but it can be manually /// overwritten with the `encoding` parameter. - pub fn open( - path: PathBuf, + pub fn open>( + path: P, encoding: Option<&'static encoding_rs::Encoding>, theme: Option<&Theme>, config_loader: Option<&syntax::Loader>, ) -> Result { + let path = path.as_ref(); let (rope, encoding) = if path.exists() { let mut file = - std::fs::File::open(&path).context(format!("unable to open {:?}", path))?; + std::fs::File::open(path).context(format!("unable to open {:?}", path))?; from_reader(&mut file, encoding)? } else { let encoding = encoding.unwrap_or(encoding_rs::UTF_8); @@ -449,7 +450,7 @@ impl Document { let mut doc = Self::from(rope, Some(encoding)); // set the path and try detecting the language - doc.set_path(&path)?; + doc.set_path(path)?; if let Some(loader) = config_loader { doc.detect_language(theme, loader); } diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 7e8548e73180..7fe89ec1b51c 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -7,7 +7,11 @@ use crate::{ }; use futures_util::future; -use std::{path::PathBuf, sync::Arc, time::Duration}; +use std::{ + path::{Path, PathBuf}, + sync::Arc, + time::Duration, +}; use slotmap::SlotMap; @@ -286,6 +290,11 @@ impl Editor { self.documents.iter_mut().map(|(_id, doc)| doc) } + pub fn document_by_path>(&self, path: P) -> Option<&Document> { + self.documents() + .find(|doc| doc.path().map(|p| p == path.as_ref()).unwrap_or(false)) + } + // pub fn current_document(&self) -> Document { // let id = self.view().doc; // let doc = &mut editor.documents[id]; From a524cd8d0aacf7adc67b5803b19d88b38f75e59f Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Fri, 6 Aug 2021 18:40:14 +0530 Subject: [PATCH 12/20] Disable syntax highlight for picker preview Files already loaded in memory have syntax highlighting enabled --- helix-term/src/ui/picker.rs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 368eb60255b2..0a0e748ea2d4 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -62,9 +62,8 @@ impl FilePicker { fn calculate_preview(&mut self, editor: &Editor) { if let Some((path, _line)) = self.current_file(editor) { if !self.preview_cache.contains_key(&path) && editor.document_by_path(&path).is_none() { - let mut doc = - Document::open(&path, None, Some(&editor.theme), Some(&editor.syn_loader)) - .unwrap(); + // TODO: enable syntax highlighting; blocked by async rendering + let mut doc = Document::open(&path, None, Some(&editor.theme), None).unwrap(); // HACK: a doc needs atleast one selection doc.set_selection(self._preview_view_id, Selection::point(0)); self.preview_cache.insert(path, doc); @@ -75,12 +74,12 @@ impl FilePicker { impl Component for FilePicker { fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) { - // |---------| |---------| - // |prompt | |preview | - // |---------| | | - // |picker | | | - // | | | | - // |---------| |---------| + // +---------+ +---------+ + // |prompt | |preview | + // +---------+ | | + // |picker | | | + // | | | | + // +---------+ +---------+ let area = inner_rect(area); // -- Render the frame: // clear area From f902c5f38b0f60b6a095f8cb2b8fd0e7aa1e9326 Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Fri, 6 Aug 2021 19:51:33 +0530 Subject: [PATCH 13/20] Ignore directory symlinks in file picker --- helix-term/src/ui/mod.rs | 35 ++++++++++++++++------------------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 704f1e3c69c9..77b6637863eb 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -76,26 +76,23 @@ pub fn regex_prompt( pub fn file_picker(root: PathBuf) -> FilePicker { use ignore::Walk; use std::time; - let files = Walk::new(root.clone()).filter_map(|entry| match entry { - Ok(entry) => { - // filter dirs, but we might need special handling for symlinks! - if !entry.file_type().map_or(false, |entry| entry.is_dir()) { - let time = if let Ok(metadata) = entry.metadata() { - metadata - .accessed() - .or_else(|_| metadata.modified()) - .or_else(|_| metadata.created()) - .unwrap_or(time::UNIX_EPOCH) - } else { - time::UNIX_EPOCH - }; - - Some((entry.into_path(), time)) - } else { - None - } + let files = Walk::new(root.clone()).filter_map(|entry| { + let entry = entry.ok()?; + // Path::is_dir() traverses symlinks, so we use it over DirEntry::is_dir + if entry.path().is_dir() { + // Will give a false positive if metadata cannot be read (eg. permission error) + return None; } - Err(_err) => None, + + let time = entry.metadata().map_or(time::UNIX_EPOCH, |metadata| { + metadata + .accessed() + .or_else(|_| metadata.modified()) + .or_else(|_| metadata.created()) + .unwrap_or(time::UNIX_EPOCH) + }); + + Some((entry.into_path(), time)) }); let mut files: Vec<_> = if root.join(".git").is_dir() { From 820b85ee249da9a072726572ab13d52c522e12ec Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Fri, 6 Aug 2021 23:25:56 +0530 Subject: [PATCH 14/20] Cleanup unnecessary pubs and derives --- helix-core/src/selection.rs | 2 +- helix-term/src/commands.rs | 7 ++----- helix-term/src/ui/editor.rs | 2 +- helix-view/src/view.rs | 2 +- 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs index 6cca0775507e..e39c5d3fe70a 100644 --- a/helix-core/src/selection.rs +++ b/helix-core/src/selection.rs @@ -46,7 +46,7 @@ use std::borrow::Cow; /// single grapheme inward from the range's edge. There are a /// variety of helper methods on `Range` for working in terms of /// that block cursor, all of which have `cursor` in their name. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Range { /// The anchor of the range: the side that doesn't move when extending. pub anchor: usize, diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 99bf8d9d1bae..a04546772552 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -101,13 +101,13 @@ impl<'a> Context<'a> { } } -pub enum Align { +enum Align { Top, Center, Bottom, } -pub fn align_view(doc: &Document, view: &mut View, align: Align) { +fn align_view(doc: &Document, view: &mut View, align: Align) { let pos = doc .selection(view.id) .primary() @@ -2140,9 +2140,6 @@ fn symbol_picker(cx: &mut Context) { move |editor, symbol| { let view = editor.tree.get(editor.tree.focus); let doc = &editor.documents[view.doc]; - // let range = - // lsp_range_to_range(doc.text(), symbol.location.range, offset_encoding); - // Calculating the exact range is expensive, so use line number only doc.path() .cloned() .zip(Some(symbol.location.range.start.line as usize)) diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index f10288f8da12..95e0ca17366e 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -33,7 +33,7 @@ pub struct EditorView { last_insert: (commands::Command, Vec), completion: Option, spinners: ProgressSpinners, - pub autoinfo: Option, + autoinfo: Option, } const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter diff --git a/helix-view/src/view.rs b/helix-view/src/view.rs index 030ce6684447..990f5484f2b1 100644 --- a/helix-view/src/view.rs +++ b/helix-view/src/view.rs @@ -59,7 +59,7 @@ impl JumpList { } } -#[derive(Debug, Clone)] +#[derive(Debug)] pub struct View { pub id: ViewId, pub doc: DocumentId, From a2e8a33d204262ed65641d338431c4d5c2cfc6ae Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Sat, 7 Aug 2021 11:29:10 +0530 Subject: [PATCH 15/20] Remove unnecessary highlight from file picker --- helix-term/src/commands.rs | 10 +++++----- helix-term/src/ui/mod.rs | 2 +- helix-term/src/ui/picker.rs | 18 ++++++++++-------- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index a04546772552..27b18be6fd8d 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2068,7 +2068,7 @@ fn buffer_picker(cx: &mut Context) { .selection(view_id) .primary() .cursor_line(doc.text().slice(..)); - Some((path.clone()?, line)) + Some((path.clone()?, Some(line))) }, ); cx.push_layer(Box::new(picker)); @@ -2140,9 +2140,8 @@ fn symbol_picker(cx: &mut Context) { move |editor, symbol| { let view = editor.tree.get(editor.tree.focus); let doc = &editor.documents[view.doc]; - doc.path() - .cloned() - .zip(Some(symbol.location.range.start.line as usize)) + let line = Some(symbol.location.range.start.line as usize); + doc.path().cloned().zip(Some(line)) }, ); compositor.push(Box::new(picker)) @@ -2534,7 +2533,8 @@ fn goto_impl( }, |_editor, location| { let path = location.uri.to_file_path().unwrap(); - Some((path, location.range.start.line as usize)) + let line = Some(location.range.start.line as usize); + Some((path, line)) }, ); compositor.push(Box::new(picker)); diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 77b6637863eb..0642bab69e3e 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -123,7 +123,7 @@ pub fn file_picker(root: PathBuf) -> FilePicker { }, |_editor, path| { // FIXME: directories are creeping up in filepicker - Some((path.clone(), 0)) + Some((path.clone(), None)) }, ) } diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 0a0e748ea2d4..6c7674a042d6 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -23,8 +23,8 @@ use helix_view::{ Document, Editor, View, ViewId, }; -/// File path and line number -type FileLocation = (PathBuf, usize); +/// File path and line number (used to align and highlight a line) +type FileLocation = (PathBuf, Option); pub struct FilePicker { picker: Picker, @@ -64,7 +64,7 @@ impl FilePicker { if !self.preview_cache.contains_key(&path) && editor.document_by_path(&path).is_none() { // TODO: enable syntax highlighting; blocked by async rendering let mut doc = Document::open(&path, None, Some(&editor.theme), None).unwrap(); - // HACK: a doc needs atleast one selection + // HACK: a doc needs atleast one selection, but we do our own line highlighting doc.set_selection(self._preview_view_id, Selection::point(0)); self.preview_cache.insert(path, doc); } @@ -119,7 +119,7 @@ impl Component for FilePicker { }; view.first_col = 0; // align to middle - view.first_line = line.saturating_sub(inner.height as usize / 2); + view.first_line = line.unwrap_or(0).saturating_sub(inner.height as usize / 2); view.area = inner; EditorView::render_doc( doc, @@ -131,10 +131,12 @@ impl Component for FilePicker { &cx.editor.syn_loader, ); // highlight the line - for x in inner.left()..inner.right() { - surface - .get_mut(x, inner.y + line.saturating_sub(view.first_line) as u16) - .set_style(cx.editor.theme.get("ui.selection.primary")); + if let Some(line) = line { + for x in inner.left()..inner.right() { + surface + .get_mut(x, inner.y + line.saturating_sub(view.first_line) as u16) + .set_style(cx.editor.theme.get("ui.selection.primary")); + } } } } From 805c0e00bd1ae85c860414208b47c884957dab95 Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Mon, 9 Aug 2021 11:31:42 +0530 Subject: [PATCH 16/20] Reorganize buffer rendering --- helix-term/src/ui/editor.rs | 382 ++++++++++++++++++------------------ helix-term/src/ui/picker.rs | 48 +++-- 2 files changed, 217 insertions(+), 213 deletions(-) diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 95e0ca17366e..f1dd0a01ea01 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -77,8 +77,26 @@ impl EditorView { view.area.width - OFFSET, view.area.height.saturating_sub(1), ); // - 1 for statusline + let offset = Position::new(view.first_line, view.first_col); + let last_line = view.last_line(doc); + + let highlights = Self::doc_syntax_highlights(doc, offset, last_line, theme, loader); + let highlights = syntax::merge(highlights, Self::doc_diagnostics_highlights(doc, theme)); + let highlights: Box> = if is_focused { + Box::new(syntax::merge( + highlights, + Self::doc_selection_highlights(doc, view, theme), + )) + } else { + Box::new(highlights) + }; - self.render_buffer(doc, view, area, surface, theme, is_focused, loader); + Self::render_text_highlights(doc, offset, area, surface, theme, highlights); + Self::render_gutter(doc, view, area, surface, theme); + + if is_focused { + Self::render_focused_view_elements(view, doc, area, theme, surface); + } // if we're not at the edge of the screen, draw a right border if viewport.right() != view.area.right() { @@ -93,7 +111,7 @@ impl EditorView { } } - self.render_diagnostics(doc, view, area, surface, theme, is_focused); + self.render_diagnostics(doc, view, area, surface, theme); let area = Rect::new( view.area.x, @@ -104,23 +122,21 @@ impl EditorView { self.render_statusline(doc, view, area, surface, theme, is_focused); } - /// Render a document into a Rect with syntax highlighting, - /// diagnostics, matching brackets and selections. + /// Get syntax highlights for a document in a view represented by the first line + /// and column (`offset`) and the last line. This is done instead of using a view + /// directly to enable rendering syntax highlighted docs anywhere (eg. picker preview) #[allow(clippy::too_many_arguments)] - pub fn render_doc( - doc: &Document, - view: &View, - viewport: Rect, - surface: &mut Surface, + pub fn doc_syntax_highlights<'doc>( + doc: &'doc Document, + offset: Position, + last_line: usize, theme: &Theme, - is_focused: bool, loader: &syntax::Loader, - ) { + ) -> Box + 'doc> { let text = doc.text().slice(..); - let last_line = view.last_line(doc); let range = { // calculate viewport byte ranges - let start = text.line_to_byte(view.first_line); + let start = text.line_to_byte(offset.row); let end = text.line_to_byte(last_line + 1); start..end @@ -158,7 +174,7 @@ impl EditorView { }], } .into_iter() - .map(|event| match event { + .map(move |event| match event { // convert byte offsets to char offset HighlightEvent::Source { start, end } => { let start = ensure_grapheme_boundary_next(text, text.byte_to_char(start)); @@ -168,13 +184,44 @@ impl EditorView { event => event, }); - let selections = doc.selection(view.id); - let primary_idx = selections.primary_index(); + Box::new(highlights) + } + + /// Get highlight spans for document diagnostics + pub fn doc_diagnostics_highlights( + doc: &Document, + theme: &Theme, + ) -> Vec<(usize, std::ops::Range)> { + let diagnostic_scope = theme + .find_scope_index("diagnostic") + .or_else(|| theme.find_scope_index("ui.cursor")) + .or_else(|| theme.find_scope_index("ui.selection")) + .expect("no selection scope found!"); + + doc.diagnostics() + .iter() + .map(|diagnostic| { + ( + diagnostic_scope, + diagnostic.range.start..diagnostic.range.end, + ) + }) + .collect() + } + + /// Get highlight spans for selections in a document view. + pub fn doc_selection_highlights( + doc: &Document, + view: &View, + theme: &Theme, + ) -> Vec<(usize, std::ops::Range)> { + let text = doc.text().slice(..); + let selection = doc.selection(view.id); + let primary_idx = selection.primary_index(); let selection_scope = theme .find_scope_index("ui.selection") .expect("no selection scope found!"); - let base_cursor_scope = theme .find_scope_index("ui.cursor") .unwrap_or(selection_scope); @@ -186,64 +233,53 @@ impl EditorView { } .unwrap_or(base_cursor_scope); - let highlights: Box> = if is_focused { - // TODO: primary + insert mode patching: - // (ui.cursor.primary).patch(mode).unwrap_or(cursor) - let primary_cursor_scope = theme - .find_scope_index("ui.cursor.primary") - .unwrap_or(cursor_scope); - let primary_selection_scope = theme - .find_scope_index("ui.selection.primary") - .unwrap_or(selection_scope); - - // inject selections as highlight scopes - let mut spans: Vec<(usize, std::ops::Range)> = Vec::new(); - for (i, range) in selections.iter().enumerate() { - let (cursor_scope, selection_scope) = if i == primary_idx { - (primary_cursor_scope, primary_selection_scope) - } else { - (cursor_scope, selection_scope) - }; + let primary_cursor_scope = theme + .find_scope_index("ui.cursor.primary") + .unwrap_or(cursor_scope); + let primary_selection_scope = theme + .find_scope_index("ui.selection.primary") + .unwrap_or(selection_scope); - // Special-case: cursor at end of the rope. - if range.head == range.anchor && range.head == text.len_chars() { - spans.push((cursor_scope, range.head..range.head + 1)); - continue; - } + let mut spans: Vec<(usize, std::ops::Range)> = Vec::new(); + for (i, range) in selection.iter().enumerate() { + let (cursor_scope, selection_scope) = if i == primary_idx { + (primary_cursor_scope, primary_selection_scope) + } else { + (cursor_scope, selection_scope) + }; - let range = range.min_width_1(text); - if range.head > range.anchor { - // Standard case. - let cursor_start = prev_grapheme_boundary(text, range.head); - spans.push((selection_scope, range.anchor..cursor_start)); - spans.push((cursor_scope, cursor_start..range.head)); - } else { - // Reverse case. - let cursor_end = next_grapheme_boundary(text, range.head); - spans.push((cursor_scope, range.head..cursor_end)); - spans.push((selection_scope, cursor_end..range.anchor)); - } + // Special-case: cursor at end of the rope. + if range.head == range.anchor && range.head == text.len_chars() { + spans.push((cursor_scope, range.head..range.head + 1)); + continue; } - Box::new(syntax::merge(highlights, spans)) - } else { - Box::new(highlights) - }; + let range = range.min_width_1(text); + if range.head > range.anchor { + // Standard case. + let cursor_start = prev_grapheme_boundary(text, range.head); + spans.push((selection_scope, range.anchor..cursor_start)); + spans.push((cursor_scope, cursor_start..range.head)); + } else { + // Reverse case. + let cursor_end = next_grapheme_boundary(text, range.head); + spans.push((cursor_scope, range.head..cursor_end)); + spans.push((selection_scope, cursor_end..range.anchor)); + } + } + + spans + } - // diagnostic injection - let diagnostic_scope = theme.find_scope_index("diagnostic").unwrap_or(cursor_scope); - let highlights = Box::new(syntax::merge( - highlights, - doc.diagnostics() - .iter() - .map(|diagnostic| { - ( - diagnostic_scope, - diagnostic.range.start..diagnostic.range.end, - ) - }) - .collect(), - )); + pub fn render_text_highlights>( + doc: &Document, + offset: Position, + viewport: Rect, + surface: &mut Surface, + theme: &Theme, + highlights: H, + ) { + let text = doc.text().slice(..); let mut spans = Vec::new(); let mut visual_x = 0u16; @@ -273,14 +309,14 @@ impl EditorView { }); for grapheme in RopeGraphemes::new(text) { - let out_of_bounds = visual_x < view.first_col as u16 - || visual_x >= viewport.width + view.first_col as u16; + let out_of_bounds = visual_x < offset.col as u16 + || visual_x >= viewport.width + offset.col as u16; if LineEnding::from_rope_slice(&grapheme).is_some() { if !out_of_bounds { // we still want to render an empty cell with the style surface.set_string( - viewport.x + visual_x - view.first_col as u16, + viewport.x + visual_x - offset.col as u16, viewport.y + line, " ", style, @@ -310,7 +346,7 @@ impl EditorView { if !out_of_bounds { // if we're offscreen just keep going until we hit a new line surface.set_string( - viewport.x + visual_x - view.first_col as u16, + viewport.x + visual_x - offset.col as u16, viewport.y + line, grapheme, style, @@ -323,55 +359,85 @@ impl EditorView { } } } + } - if is_focused { - let screen = { - let start = text.line_to_char(view.first_line); - let end = text.line_to_char(last_line + 1) + 1; // +1 for cursor at end of text. - Range::new(start, end) - }; + /// Render brace match, selected line numbers, etc (meant for the focused view only) + pub fn render_focused_view_elements( + view: &View, + doc: &Document, + viewport: Rect, + theme: &Theme, + surface: &mut Surface, + ) { + let text = doc.text().slice(..); + let selection = doc.selection(view.id); + let last_line = view.last_line(doc); + let screen = { + let start = text.line_to_char(view.first_line); + let end = text.line_to_char(last_line + 1) + 1; // +1 for cursor at end of text. + Range::new(start, end) + }; - let selection = doc.selection(view.id); + // render selected linenr(s) + let linenr_select: Style = theme + .try_get("ui.linenr.selected") + .unwrap_or_else(|| theme.get("ui.linenr")); - for selection in selection.iter().filter(|range| range.overlaps(&screen)) { - let head = view.screen_coords_at_pos( - doc, - text, - if selection.head > selection.anchor { - selection.head - 1 - } else { - selection.head - }, + // Whether to draw the line number for the last line of the + // document or not. We only draw it if it's not an empty line. + let draw_last = text.line_to_byte(last_line) < text.len_bytes(); + + for selection in selection.iter().filter(|range| range.overlaps(&screen)) { + let head = view.screen_coords_at_pos( + doc, + text, + if selection.head > selection.anchor { + selection.head - 1 + } else { + selection.head + }, + ); + if let Some(head) = head { + // Highlight line number for selected lines. + let line_number = view.first_line + head.row; + let line_number_text = if line_number == last_line && !draw_last { + " ~".into() + } else { + format!("{:>5}", line_number + 1) + }; + surface.set_stringn( + viewport.x - OFFSET + 1, + viewport.y + head.row as u16, + line_number_text, + 5, + linenr_select, ); - if head.is_some() { - // TODO: set cursor position for IME - if let Some(syntax) = doc.syntax() { - use helix_core::match_brackets; - let pos = doc - .selection(view.id) - .primary() - .cursor(doc.text().slice(..)); - let pos = match_brackets::find(syntax, doc.text(), pos) - .and_then(|pos| view.screen_coords_at_pos(doc, text, pos)); - - if let Some(pos) = pos { - // ensure col is on screen - if (pos.col as u16) < viewport.width + view.first_col as u16 - && pos.col >= view.first_col - { - let style = theme.try_get("ui.cursor.match").unwrap_or_else(|| { - Style::default() - .add_modifier(Modifier::REVERSED) - .add_modifier(Modifier::DIM) - }); - - surface - .get_mut( - viewport.x + pos.col as u16, - viewport.y + pos.row as u16, - ) - .set_style(style); - } + + // Highlight matching braces + // TODO: set cursor position for IME + if let Some(syntax) = doc.syntax() { + use helix_core::match_brackets; + let pos = doc + .selection(view.id) + .primary() + .cursor(doc.text().slice(..)); + let pos = match_brackets::find(syntax, doc.text(), pos) + .and_then(|pos| view.screen_coords_at_pos(doc, text, pos)); + + if let Some(pos) = pos { + // ensure col is on screen + if (pos.col as u16) < viewport.width + view.first_col as u16 + && pos.col >= view.first_col + { + let style = theme.try_get("ui.cursor.match").unwrap_or_else(|| { + Style::default() + .add_modifier(Modifier::REVERSED) + .add_modifier(Modifier::DIM) + }); + + surface + .get_mut(viewport.x + pos.col as u16, viewport.y + pos.row as u16) + .set_style(style); } } } @@ -386,16 +452,15 @@ impl EditorView { viewport: Rect, surface: &mut Surface, theme: &Theme, - is_focused: bool, ) { let text = doc.text().slice(..); let last_line = view.last_line(doc); - let linenr: Style = theme.get("ui.linenr"); - let warning: Style = theme.get("warning"); - let error: Style = theme.get("error"); - let info: Style = theme.get("info"); - let hint: Style = theme.get("hint"); + let linenr = theme.get("ui.linenr"); + let warning = theme.get("warning"); + let error = theme.get("error"); + let info = theme.get("info"); + let hint = theme.get("hint"); // Whether to draw the line number for the last line of the // document or not. We only draw it if it's not an empty line. @@ -433,64 +498,6 @@ impl EditorView { linenr, ); } - - // render selected linenr(s) - let linenr_select: Style = theme - .try_get("ui.linenr.selected") - .unwrap_or_else(|| theme.get("ui.linenr")); - - if is_focused { - let screen = { - let start = text.line_to_char(view.first_line); - let end = text.line_to_char(last_line + 1) + 1; // +1 for cursor at end of text. - Range::new(start, end) - }; - - let selection = doc.selection(view.id); - - for selection in selection.iter().filter(|range| range.overlaps(&screen)) { - let head = view.screen_coords_at_pos( - doc, - text, - if selection.head > selection.anchor { - selection.head - 1 - } else { - selection.head - }, - ); - if let Some(head) = head { - // Draw line number for selected lines. - let line_number = view.first_line + head.row; - let line_number_text = if line_number == last_line && !draw_last { - " ~".into() - } else { - format!("{:>5}", line_number + 1) - }; - surface.set_stringn( - viewport.x + 1 - OFFSET, - viewport.y + head.row as u16, - line_number_text, - 5, - linenr_select, - ); - } - } - } - } - - #[allow(clippy::too_many_arguments)] - pub fn render_buffer( - &self, - doc: &Document, - view: &View, - viewport: Rect, - surface: &mut Surface, - theme: &Theme, - is_focused: bool, - loader: &syntax::Loader, - ) { - Self::render_doc(doc, view, viewport, surface, theme, is_focused, loader); - Self::render_gutter(doc, view, viewport, surface, theme, is_focused); } pub fn render_diagnostics( @@ -500,7 +507,6 @@ impl EditorView { viewport: Rect, surface: &mut Surface, theme: &Theme, - _is_focused: bool, ) { use helix_core::diagnostic::Severity; use tui::{ @@ -518,10 +524,10 @@ impl EditorView { diagnostic.range.start <= cursor && diagnostic.range.end >= cursor }); - let warning: Style = theme.get("warning"); - let error: Style = theme.get("error"); - let info: Style = theme.get("info"); - let hint: Style = theme.get("hint"); + let warning = theme.get("warning"); + let error = theme.get("error"); + let info = theme.get("info"); + let hint = theme.get("hint"); // Vec::with_capacity(diagnostics.len()); // rough estimate let mut lines = Vec::new(); diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 6c7674a042d6..e445b0aac49d 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -15,12 +15,12 @@ use tui::widgets::Widget; use std::{borrow::Cow, collections::HashMap, path::PathBuf}; use crate::ui::{Prompt, PromptEvent}; -use helix_core::{Position, Selection}; +use helix_core::Position; use helix_view::{ document::canonicalize_path, editor::Action, graphics::{Color, CursorKind, Rect, Style}, - Document, Editor, View, ViewId, + Document, Editor, }; /// File path and line number (used to align and highlight a line) @@ -32,9 +32,6 @@ pub struct FilePicker { preview_cache: HashMap, /// Given an item in the picker, return the file path and line number to display. file_fn: Box Option>, - // A view id to be shared by all documents in the cache. Mostly a hack since a doc - // requires at least one selection. - _preview_view_id: ViewId, } impl FilePicker { @@ -48,7 +45,6 @@ impl FilePicker { picker: Picker::new(options, format_fn, callback_fn), preview_cache: HashMap::new(), file_fn: Box::new(preview_fn), - _preview_view_id: ViewId::default(), } } @@ -63,9 +59,7 @@ impl FilePicker { if let Some((path, _line)) = self.current_file(editor) { if !self.preview_cache.contains_key(&path) && editor.document_by_path(&path).is_none() { // TODO: enable syntax highlighting; blocked by async rendering - let mut doc = Document::open(&path, None, Some(&editor.theme), None).unwrap(); - // HACK: a doc needs atleast one selection, but we do our own line highlighting - doc.set_selection(self._preview_view_id, Selection::point(0)); + let doc = Document::open(&path, None, Some(&editor.theme), None).unwrap(); self.preview_cache.insert(path, doc); } } @@ -109,32 +103,36 @@ impl Component for FilePicker { .or_else(|| self.preview_cache.get(&path)) .zip(Some(line)) }) { - // FIXME: last line will not be highlighted because of a -1 in View::last_line - let mut view = View::new(doc.id()); - view.id = if doc.selections().contains_key(&self._preview_view_id) { - self._preview_view_id // doc from cache - } else { - // Any view will do since we do not depend on doc selections for highlighting - *doc.selections().keys().next().unwrap() // doc from editor - }; - view.first_col = 0; // align to middle - view.first_line = line.unwrap_or(0).saturating_sub(inner.height as usize / 2); - view.area = inner; - EditorView::render_doc( + let first_line = line.unwrap_or(0).saturating_sub(inner.height as usize / 2); + let last_line = std::cmp::min( + // Saturating subs to make it inclusive zero indexing. + (first_line + area.height as usize).saturating_sub(1), + doc.text().len_lines().saturating_sub(1), + ); + let offset = Position::new(first_line, 0); + + let highlights = EditorView::doc_syntax_highlights( + doc, + offset, + last_line, + &cx.editor.theme, + &cx.editor.syn_loader, + ); + EditorView::render_text_highlights( doc, - &view, + offset, inner, surface, &cx.editor.theme, - false, // is_focused - &cx.editor.syn_loader, + highlights, ); + // highlight the line if let Some(line) = line { for x in inner.left()..inner.right() { surface - .get_mut(x, inner.y + line.saturating_sub(view.first_line) as u16) + .get_mut(x, inner.y + line.saturating_sub(first_line) as u16) .set_style(cx.editor.theme.get("ui.selection.primary")); } } From ecc857bee7c08f30c5a47ddeb50445384dabc70c Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Tue, 10 Aug 2021 15:16:34 +0530 Subject: [PATCH 17/20] Use normal picker for code actions --- helix-term/src/commands.rs | 6 +++--- helix-term/src/ui/mod.rs | 2 +- helix-term/src/ui/picker.rs | 12 +++++++++++- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 3010a9ce6ddc..7f714c4d92ae 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -27,7 +27,7 @@ use movement::Movement; use crate::{ compositor::{self, Component, Compositor}, - ui::{self, FilePicker, Popup, Prompt, PromptEvent}, + ui::{self, FilePicker, Picker, Popup, Prompt, PromptEvent}, }; use crate::job::{self, Job, Jobs}; @@ -2251,7 +2251,8 @@ pub fn code_action(cx: &mut Context) { compositor: &mut Compositor, response: Option| { if let Some(actions) = response { - let picker = FilePicker::new( + let picker = Picker::new( + true, actions, |action| match action { lsp::CodeActionOrCommand::CodeAction(action) => { @@ -2271,7 +2272,6 @@ pub fn code_action(cx: &mut Context) { } } }, - |_editor, _action| None, ); compositor.push(Box::new(picker)) } diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 8a993e09d7e7..f4b990e3f3c1 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -13,7 +13,7 @@ pub use completion::Completion; pub use editor::EditorView; pub use markdown::Markdown; pub use menu::Menu; -pub use picker::FilePicker; +pub use picker::{FilePicker, Picker}; pub use popup::Popup; pub use prompt::{Prompt, PromptEvent}; pub use spinner::{ProgressSpinners, Spinner}; diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index e445b0aac49d..60a351419521 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -42,7 +42,7 @@ impl FilePicker { preview_fn: impl Fn(&Editor, &T) -> Option + 'static, ) -> Self { Self { - picker: Picker::new(options, format_fn, callback_fn), + picker: Picker::new(false, options, format_fn, callback_fn), preview_cache: HashMap::new(), file_fn: Box::new(preview_fn), } @@ -165,6 +165,8 @@ pub struct Picker { cursor: usize, // pattern: String, prompt: Prompt, + /// Whether to render in the middle of the area + render_centered: bool, format_fn: Box Cow>, callback_fn: Box, @@ -172,6 +174,7 @@ pub struct Picker { impl Picker { pub fn new( + render_centered: bool, options: Vec, format_fn: impl Fn(&T) -> Cow + 'static, callback_fn: impl Fn(&mut Editor, &T, Action) + 'static, @@ -192,6 +195,7 @@ impl Picker { filters: Vec::new(), cursor: 0, prompt, + render_centered, format_fn: Box::new(format_fn), callback_fn: Box::new(callback_fn), }; @@ -379,6 +383,12 @@ impl Component for Picker { } fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) { + let area = if self.render_centered { + inner_rect(area) + } else { + area + }; + // -- Render the frame: // clear area let background = cx.editor.theme.get("ui.background"); From 014cdd1377c5ef92a0f99a0ea33b05e7d83d19d8 Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Wed, 11 Aug 2021 21:47:55 +0530 Subject: [PATCH 18/20] Remove unnecessary generics and trait impls --- helix-core/src/selection.rs | 6 ------ helix-term/src/commands.rs | 11 +++++------ helix-term/src/ui/mod.rs | 5 +---- helix-view/src/document.rs | 5 ++--- helix-view/src/editor.rs | 2 +- 5 files changed, 9 insertions(+), 20 deletions(-) diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs index e39c5d3fe70a..a3ea2ed42524 100644 --- a/helix-core/src/selection.rs +++ b/helix-core/src/selection.rs @@ -497,12 +497,6 @@ impl Selection { } } -impl From for Selection { - fn from(range: Range) -> Self { - Self::single(range.anchor, range.head) - } -} - impl<'a> IntoIterator for &'a Selection { type Item = &'a Range; type IntoIter = std::slice::Iter<'a, Range>; diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 7f714c4d92ae..c7ce79d8311a 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2211,15 +2211,14 @@ fn symbol_picker(cx: &mut Context) { if let Some(range) = lsp_range_to_range(doc.text(), symbol.location.range, offset_encoding) { - doc.set_selection(view.id, Selection::from(range)); + doc.set_selection(view.id, Selection::single(range.anchor, range.head)); align_view(doc, view, Align::Center); } }, - move |editor, symbol| { - let view = editor.tree.get(editor.tree.focus); - let doc = &editor.documents[view.doc]; + move |_editor, symbol| { + let path = symbol.location.uri.to_file_path().unwrap(); let line = Some(symbol.location.range.start.line as usize); - doc.path().cloned().zip(Some(line)) + Some((path, line)) }, ); compositor.push(Box::new(picker)) @@ -3606,7 +3605,7 @@ fn keep_primary_selection(cx: &mut Context) { let (view, doc) = current!(cx.editor); let range = doc.selection(view.id).primary(); - doc.set_selection(view.id, Selection::from(range)); + doc.set_selection(view.id, Selection::single(range.anchor, range.head)); } fn completion(cx: &mut Context) { diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index f4b990e3f3c1..d1af0e481d45 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -121,10 +121,7 @@ pub fn file_picker(root: PathBuf) -> FilePicker { .open(path.into(), action) .expect("editor.open failed"); }, - |_editor, path| { - // FIXME: directories are creeping up in filepicker - Some((path.clone(), None)) - }, + |_editor, path| Some((path.clone(), None)), ) } diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 69cdcbe0e675..8730bef2246a 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -431,13 +431,12 @@ impl Document { // TODO: async fn? /// Create a new document from `path`. Encoding is auto-detected, but it can be manually /// overwritten with the `encoding` parameter. - pub fn open>( - path: P, + pub fn open( + path: &Path, encoding: Option<&'static encoding_rs::Encoding>, theme: Option<&Theme>, config_loader: Option<&syntax::Loader>, ) -> Result { - let path = path.as_ref(); let (rope, encoding) = if path.exists() { let mut file = std::fs::File::open(path).context(format!("unable to open {:?}", path))?; diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 567a16f35693..1c4e50d74ca0 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -220,7 +220,7 @@ impl Editor { let id = if let Some(id) = id { id } else { - let mut doc = Document::open(path, None, Some(&self.theme), Some(&self.syn_loader))?; + let mut doc = Document::open(&path, None, Some(&self.theme), Some(&self.syn_loader))?; // try to find a language server based on the language name let language_server = doc From bbd5071df188fc68c6782efa3abd36791c59e07e Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Thu, 12 Aug 2021 10:42:54 +0530 Subject: [PATCH 19/20] Remove prepare_for_render and make render mutable --- helix-term/src/compositor.rs | 5 +---- helix-term/src/ui/completion.rs | 4 ++-- helix-term/src/ui/editor.rs | 18 ++++++++++++------ helix-term/src/ui/info.rs | 2 +- helix-term/src/ui/markdown.rs | 2 +- helix-term/src/ui/menu.rs | 2 +- helix-term/src/ui/picker.rs | 16 ++++------------ helix-term/src/ui/popup.rs | 2 +- helix-term/src/ui/prompt.rs | 4 ++-- helix-term/src/ui/text.rs | 2 +- 10 files changed, 26 insertions(+), 31 deletions(-) diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs index 6e8aa0b2ccdc..36e54eded1fb 100644 --- a/helix-term/src/compositor.rs +++ b/helix-term/src/compositor.rs @@ -46,15 +46,13 @@ pub trait Component: Any + AnyComponent { } /// Render the component onto the provided surface. - fn render(&self, area: Rect, frame: &mut Surface, ctx: &mut Context); + fn render(&mut self, area: Rect, frame: &mut Surface, ctx: &mut Context); /// Get cursor position and cursor kind. fn cursor(&self, _area: Rect, _ctx: &Editor) -> (Option, CursorKind) { (None, CursorKind::Hidden) } - fn prepare_for_render(&mut self, _ctx: &Context) {} - /// May be used by the parent component to compute the child area. /// viewport is the maximum allowed area, and the child should stay within those bounds. fn required_size(&mut self, _viewport: (u16, u16)) -> Option<(u16, u16)> { @@ -155,7 +153,6 @@ impl Compositor { let area = *surface.area(); for layer in &mut self.layers { - layer.prepare_for_render(cx); layer.render(area, surface, cx); } diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index 2725d53debd6..12f69baf75a1 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -206,7 +206,7 @@ impl Component for Completion { self.popup.required_size(viewport) } - fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) { + fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { self.popup.render(area, surface, cx); // if we have a selection, render a markdown popup on top/below with info @@ -228,7 +228,7 @@ impl Component for Completion { let cursor_pos = (helix_core::coords_at_pos(doc.text().slice(..), cursor_pos).row - view.first_line) as u16; - let doc = match &option.documentation { + let mut doc = match &option.documentation { Some(lsp::Documentation::String(contents)) | Some(lsp::Documentation::MarkupContent(lsp::MarkupContent { kind: lsp::MarkupKind::PlainText, diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 6963436b2af5..6664029f8641 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -78,9 +78,9 @@ impl EditorView { view.area.height.saturating_sub(1), ); // - 1 for statusline let offset = Position::new(view.first_line, view.first_col); - let last_line = view.last_line(doc); + let height = view.area.height.saturating_sub(1); // - 1 for statusline - let highlights = Self::doc_syntax_highlights(doc, offset, last_line, theme, loader); + let highlights = Self::doc_syntax_highlights(doc, offset, height, theme, loader); let highlights = syntax::merge(highlights, Self::doc_diagnostics_highlights(doc, theme)); let highlights: Box> = if is_focused { Box::new(syntax::merge( @@ -129,11 +129,17 @@ impl EditorView { pub fn doc_syntax_highlights<'doc>( doc: &'doc Document, offset: Position, - last_line: usize, + height: u16, theme: &Theme, loader: &syntax::Loader, ) -> Box + 'doc> { let text = doc.text().slice(..); + let last_line = std::cmp::min( + // Saturating subs to make it inclusive zero indexing. + (offset.row + height as usize).saturating_sub(1), + doc.text().len_lines().saturating_sub(1), + ); + let range = { // calculate viewport byte ranges let start = text.line_to_byte(offset.row); @@ -917,7 +923,7 @@ impl Component for EditorView { } } - fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) { + fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { // clear with background color surface.set_style(area, cx.editor.theme.get("ui.background")); @@ -939,7 +945,7 @@ impl Component for EditorView { ); } - if let Some(ref info) = self.autoinfo { + if let Some(ref mut info) = self.autoinfo { info.render(area, surface, cx); } @@ -986,7 +992,7 @@ impl Component for EditorView { ); } - if let Some(completion) = &self.completion { + if let Some(completion) = self.completion.as_mut() { completion.render(area, surface, cx); } } diff --git a/helix-term/src/ui/info.rs b/helix-term/src/ui/info.rs index 6e810b86617c..0f14260e3622 100644 --- a/helix-term/src/ui/info.rs +++ b/helix-term/src/ui/info.rs @@ -5,7 +5,7 @@ use tui::buffer::Buffer as Surface; use tui::widgets::{Block, Borders, Widget}; impl Component for Info { - fn render(&self, viewport: Rect, surface: &mut Surface, cx: &mut Context) { + fn render(&mut self, viewport: Rect, surface: &mut Surface, cx: &mut Context) { let style = cx.editor.theme.get("ui.popup"); // Calculate the area of the terminal to modify. Because we want to diff --git a/helix-term/src/ui/markdown.rs b/helix-term/src/ui/markdown.rs index 6c79ca67160d..b7e29f210944 100644 --- a/helix-term/src/ui/markdown.rs +++ b/helix-term/src/ui/markdown.rs @@ -198,7 +198,7 @@ fn parse<'a>( Text::from(lines) } impl Component for Markdown { - fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) { + fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { use tui::widgets::{Paragraph, Widget, Wrap}; let text = parse(&self.contents, Some(&cx.editor.theme), &self.config_loader); diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs index 1e1c5427282d..7dcdb2a2d90e 100644 --- a/helix-term/src/ui/menu.rs +++ b/helix-term/src/ui/menu.rs @@ -263,7 +263,7 @@ impl Component for Menu { // TODO: required size should re-trigger when we filter items so we can draw a smaller menu - fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) { + fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { let style = cx.editor.theme.get("ui.text"); let selected = cx.editor.theme.get("ui.menu.selected"); diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 60a351419521..4a49a3973a15 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -67,13 +67,14 @@ impl FilePicker { } impl Component for FilePicker { - fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) { + fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { // +---------+ +---------+ // |prompt | |preview | // +---------+ | | // |picker | | | // | | | | // +---------+ +---------+ + self.calculate_preview(cx.editor); let area = inner_rect(area); // -- Render the frame: // clear area @@ -105,17 +106,12 @@ impl Component for FilePicker { }) { // align to middle let first_line = line.unwrap_or(0).saturating_sub(inner.height as usize / 2); - let last_line = std::cmp::min( - // Saturating subs to make it inclusive zero indexing. - (first_line + area.height as usize).saturating_sub(1), - doc.text().len_lines().saturating_sub(1), - ); let offset = Position::new(first_line, 0); let highlights = EditorView::doc_syntax_highlights( doc, offset, - last_line, + area.height, &cx.editor.theme, &cx.editor.syn_loader, ); @@ -139,10 +135,6 @@ impl Component for FilePicker { } } - fn prepare_for_render(&mut self, cx: &Context) { - self.calculate_preview(cx.editor); - } - fn handle_event(&mut self, event: Event, ctx: &mut Context) -> EventResult { // TODO: keybinds for scrolling preview self.picker.handle_event(event, ctx) @@ -382,7 +374,7 @@ impl Component for Picker { EventResult::Consumed(None) } - fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) { + fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { let area = if self.render_centered { inner_rect(area) } else { diff --git a/helix-term/src/ui/popup.rs b/helix-term/src/ui/popup.rs index 29ffb4ad5daf..e31d4d7bb4bb 100644 --- a/helix-term/src/ui/popup.rs +++ b/helix-term/src/ui/popup.rs @@ -105,7 +105,7 @@ impl Component for Popup { Some(self.size) } - fn render(&self, viewport: Rect, surface: &mut Surface, cx: &mut Context) { + fn render(&mut self, viewport: Rect, surface: &mut Surface, cx: &mut Context) { cx.scroll = Some(self.scroll); let position = self diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index 57daef3a689d..8ec3674e5679 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -352,7 +352,7 @@ impl Prompt { } if let Some(doc) = (self.doc_fn)(&self.line) { - let text = ui::Text::new(doc.to_string()); + let mut text = ui::Text::new(doc.to_string()); let viewport = area; let area = viewport.intersection(Rect::new( @@ -546,7 +546,7 @@ impl Component for Prompt { EventResult::Consumed(None) } - fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) { + fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { self.render_prompt(area, surface, cx) } diff --git a/helix-term/src/ui/text.rs b/helix-term/src/ui/text.rs index 249cf89ed01a..65a75a4af2d5 100644 --- a/helix-term/src/ui/text.rs +++ b/helix-term/src/ui/text.rs @@ -13,7 +13,7 @@ impl Text { } } impl Component for Text { - fn render(&self, area: Rect, surface: &mut Surface, _cx: &mut Context) { + fn render(&mut self, area: Rect, surface: &mut Surface, _cx: &mut Context) { use tui::widgets::{Paragraph, Widget, Wrap}; let contents = tui::text::Text::from(self.contents.clone()); From a8d58326a4c2fd5418157c333ba020f185b60002 Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Thu, 12 Aug 2021 11:06:20 +0530 Subject: [PATCH 20/20] Skip picker preview if screen small, less padding --- helix-term/src/ui/picker.rs | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 4a49a3973a15..9c6b328f4fe1 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -23,6 +23,8 @@ use helix_view::{ Document, Editor, }; +pub const MIN_SCREEN_WIDTH_FOR_PREVIEW: u16 = 80; + /// File path and line number (used to align and highlight a line) type FileLocation = (PathBuf, Option); @@ -75,26 +77,36 @@ impl Component for FilePicker { // | | | | // +---------+ +---------+ self.calculate_preview(cx.editor); + let render_preview = area.width > MIN_SCREEN_WIDTH_FOR_PREVIEW; let area = inner_rect(area); // -- Render the frame: // clear area let background = cx.editor.theme.get("ui.background"); surface.clear_with(area, background); - let picker_area = Rect::new(area.x, area.y, area.width / 2, area.height); - let preview_area = Rect::new( - area.x + picker_area.width, - area.y, - area.width / 2, - area.height, - ); + let picker_width = if render_preview { + area.width / 2 + } else { + area.width + }; + + let picker_area = Rect::new(area.x, area.y, picker_width, area.height); self.picker.render(picker_area, surface, cx); + if !render_preview { + return; + } + + let preview_area = Rect::new(area.x + picker_width, area.y, area.width / 2, area.height); + // 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); + let mut inner = block.inner(preview_area); + // 1 column gap on either side + inner.x += 1; + inner.width = inner.width.saturating_sub(2); block.render(preview_area, surface); @@ -270,8 +282,8 @@ impl Picker { // - score all the names in relation to input fn inner_rect(area: Rect) -> Rect { - let padding_vertical = area.height * 20 / 100; - let padding_horizontal = area.width * 20 / 100; + let padding_vertical = area.height * 10 / 100; + let padding_horizontal = area.width * 10 / 100; Rect::new( area.x + padding_horizontal,