Show file preview in split pane in fuzzy finder (#534)

* Add preview pane for fuzzy finder

* Fix picker preview lag by caching

* Add picker preview for document symbols

* Cache picker preview per document instead of view

* Use line instead of range for preview doc

* Add picker preview for buffer picker

* Fix render bug and refactor picker

* Refactor picker preview rendering

* Split picker and preview and compose

The current selected item is cloned on every event, which is
undesirable

* Refactor out clones in previewed picker

* Retrieve doc from editor if possible in filepicker

* Disable syntax highlight for picker preview

Files already loaded in memory have syntax highlighting enabled

* Ignore directory symlinks in file picker

* Cleanup unnecessary pubs and derives

* Remove unnecessary highlight from file picker

* Reorganize buffer rendering

* Use normal picker for code actions

* Remove unnecessary generics and trait impls

* Remove prepare_for_render and make render mutable

* Skip picker preview if screen small, less padding
pull/579/head
Gokul Soumya 3 years ago committed by GitHub
parent 7d51805e94
commit d84f8b5fde
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -27,7 +27,7 @@ use movement::Movement;
use crate::{ use crate::{
compositor::{self, Component, Compositor}, compositor::{self, Component, Compositor},
ui::{self, Picker, Popup, Prompt, PromptEvent}, ui::{self, FilePicker, Picker, Popup, Prompt, PromptEvent},
}; };
use crate::job::{self, Job, Jobs}; use crate::job::{self, Job, Jobs};
@ -2212,7 +2212,7 @@ fn file_picker(cx: &mut Context) {
fn buffer_picker(cx: &mut Context) { fn buffer_picker(cx: &mut Context) {
let current = view!(cx.editor).doc; let current = view!(cx.editor).doc;
let picker = Picker::new( let picker = FilePicker::new(
cx.editor cx.editor
.documents .documents
.iter() .iter()
@ -2234,6 +2234,15 @@ fn buffer_picker(cx: &mut Context) {
|editor: &mut Editor, (id, _path): &(DocumentId, Option<PathBuf>), _action| { |editor: &mut Editor, (id, _path): &(DocumentId, Option<PathBuf>), _action| {
editor.switch(*id, Action::Replace); editor.switch(*id, Action::Replace);
}, },
|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()?, Some(line)))
},
); );
cx.push_layer(Box::new(picker)); cx.push_layer(Box::new(picker));
} }
@ -2287,7 +2296,7 @@ fn symbol_picker(cx: &mut Context) {
} }
}; };
let picker = Picker::new( let picker = FilePicker::new(
symbols, symbols,
|symbol| (&symbol.name).into(), |symbol| (&symbol.name).into(),
move |editor: &mut Editor, symbol, _action| { move |editor: &mut Editor, symbol, _action| {
@ -2297,10 +2306,15 @@ fn symbol_picker(cx: &mut Context) {
if let Some(range) = if let Some(range) =
lsp_range_to_range(doc.text(), symbol.location.range, offset_encoding) 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::single(range.anchor, range.head));
align_view(doc, view, Align::Center); align_view(doc, view, Align::Center);
} }
}, },
move |_editor, symbol| {
let path = symbol.location.uri.to_file_path().unwrap();
let line = Some(symbol.location.range.start.line as usize);
Some((path, line))
},
); );
compositor.push(Box::new(picker)) compositor.push(Box::new(picker))
} }
@ -2332,6 +2346,7 @@ pub fn code_action(cx: &mut Context) {
response: Option<lsp::CodeActionResponse>| { response: Option<lsp::CodeActionResponse>| {
if let Some(actions) = response { if let Some(actions) = response {
let picker = Picker::new( let picker = Picker::new(
true,
actions, actions,
|action| match action { |action| match action {
lsp::CodeActionOrCommand::CodeAction(action) => { lsp::CodeActionOrCommand::CodeAction(action) => {
@ -2703,7 +2718,7 @@ fn goto_impl(
editor.set_error("No definition found.".to_string()); editor.set_error("No definition found.".to_string());
} }
_locations => { _locations => {
let picker = ui::Picker::new( let picker = FilePicker::new(
locations, locations,
move |location| { move |location| {
let file: Cow<'_, str> = (location.uri.scheme() == "file") let file: Cow<'_, str> = (location.uri.scheme() == "file")
@ -2728,6 +2743,11 @@ fn goto_impl(
move |editor: &mut Editor, location, action| { move |editor: &mut Editor, location, action| {
jump_to(editor, location, offset_encoding, action) jump_to(editor, location, offset_encoding, action)
}, },
|_editor, location| {
let path = location.uri.to_file_path().unwrap();
let line = Some(location.range.start.line as usize);
Some((path, line))
},
); );
compositor.push(Box::new(picker)); compositor.push(Box::new(picker));
} }
@ -3729,8 +3749,7 @@ fn keep_primary_selection(cx: &mut Context) {
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
let range = doc.selection(view.id).primary(); let range = doc.selection(view.id).primary();
let selection = Selection::single(range.anchor, range.head); doc.set_selection(view.id, Selection::single(range.anchor, range.head));
doc.set_selection(view.id, selection);
} }
fn completion(cx: &mut Context) { fn completion(cx: &mut Context) {

@ -46,7 +46,7 @@ pub trait Component: Any + AnyComponent {
} }
/// Render the component onto the provided surface. /// 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. /// Get cursor position and cursor kind.
fn cursor(&self, _area: Rect, _ctx: &Editor) -> (Option<Position>, CursorKind) { fn cursor(&self, _area: Rect, _ctx: &Editor) -> (Option<Position>, CursorKind) {
@ -152,8 +152,8 @@ impl Compositor {
let area = *surface.area(); let area = *surface.area();
for layer in &self.layers { for layer in &mut self.layers {
layer.render(area, surface, cx) layer.render(area, surface, cx);
} }
let (pos, kind) = self.cursor(area, cx.editor); let (pos, kind) = self.cursor(area, cx.editor);

@ -241,7 +241,7 @@ impl Component for Completion {
self.popup.required_size(viewport) 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); self.popup.render(area, surface, cx);
// if we have a selection, render a markdown popup on top/below with info // if we have a selection, render a markdown popup on top/below with info
@ -263,7 +263,7 @@ impl Component for Completion {
let cursor_pos = (helix_core::coords_at_pos(doc.text().slice(..), cursor_pos).row let cursor_pos = (helix_core::coords_at_pos(doc.text().slice(..), cursor_pos).row
- view.first_line) as u16; - view.first_line) as u16;
let doc = match &option.documentation { let mut doc = match &option.documentation {
Some(lsp::Documentation::String(contents)) Some(lsp::Documentation::String(contents))
| Some(lsp::Documentation::MarkupContent(lsp::MarkupContent { | Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
kind: lsp::MarkupKind::PlainText, kind: lsp::MarkupKind::PlainText,

@ -34,7 +34,7 @@ pub struct EditorView {
last_insert: (commands::Command, Vec<KeyEvent>), last_insert: (commands::Command, Vec<KeyEvent>),
completion: Option<Completion>, completion: Option<Completion>,
spinners: ProgressSpinners, spinners: ProgressSpinners,
pub autoinfo: Option<Info>, autoinfo: Option<Info>,
} }
pub const GUTTER_OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter pub const GUTTER_OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
@ -78,8 +78,26 @@ impl EditorView {
view.area.width - GUTTER_OFFSET, view.area.width - GUTTER_OFFSET,
view.area.height.saturating_sub(1), view.area.height.saturating_sub(1),
); // - 1 for statusline ); // - 1 for statusline
let offset = Position::new(view.first_line, view.first_col);
let height = view.area.height.saturating_sub(1); // - 1 for statusline
self.render_buffer(doc, view, area, surface, theme, is_focused, 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<dyn Iterator<Item = HighlightEvent>> = if is_focused {
Box::new(syntax::merge(
highlights,
Self::doc_selection_highlights(doc, view, theme),
))
} else {
Box::new(highlights)
};
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 we're not at the edge of the screen, draw a right border
if viewport.right() != view.area.right() { if viewport.right() != view.area.right() {
@ -94,7 +112,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( let area = Rect::new(
view.area.x, view.area.x,
@ -105,31 +123,34 @@ impl EditorView {
self.render_statusline(doc, view, area, surface, theme, is_focused); self.render_statusline(doc, view, area, surface, theme, is_focused);
} }
/// 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)] #[allow(clippy::too_many_arguments)]
pub fn render_buffer( pub fn doc_syntax_highlights<'doc>(
&self, doc: &'doc Document,
doc: &Document, offset: Position,
view: &View, height: u16,
viewport: Rect,
surface: &mut Surface,
theme: &Theme, theme: &Theme,
is_focused: bool,
loader: &syntax::Loader, loader: &syntax::Loader,
) { ) -> Box<dyn Iterator<Item = HighlightEvent> + 'doc> {
let text = doc.text().slice(..); let text = doc.text().slice(..);
let last_line = std::cmp::min(
let last_line = view.last_line(doc); // 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 = { let range = {
// calculate viewport byte ranges // 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); let end = text.line_to_byte(last_line + 1);
start..end start..end
}; };
// TODO: range doesn't actually restrict source, just highlight range // TODO: range doesn't actually restrict source, just highlight range
let highlights: Vec<_> = match doc.syntax() { let highlights = match doc.syntax() {
Some(syntax) => { Some(syntax) => {
let scopes = theme.scopes(); let scopes = theme.scopes();
syntax syntax
@ -151,20 +172,16 @@ impl EditorView {
Some(config_ref) Some(config_ref)
}) })
}) })
.map(|event| event.unwrap())
.collect() // TODO: we collect here to avoid holding the lock, fix later .collect() // TODO: we collect here to avoid holding the lock, fix later
} }
None => vec![Ok(HighlightEvent::Source { None => vec![HighlightEvent::Source {
start: range.start, start: range.start,
end: range.end, end: range.end,
})], }],
}; }
let mut spans = Vec::new(); .into_iter()
let mut visual_x = 0u16; .map(move |event| match event {
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 // convert byte offsets to char offset
HighlightEvent::Source { start, end } => { HighlightEvent::Source { start, end } => {
let start = ensure_grapheme_boundary_next(text, text.byte_to_char(start)); let start = ensure_grapheme_boundary_next(text, text.byte_to_char(start));
@ -174,13 +191,44 @@ impl EditorView {
event => event, event => event,
}); });
let selections = doc.selection(view.id); Box::new(highlights)
let primary_idx = selections.primary_index(); }
/// Get highlight spans for document diagnostics
pub fn doc_diagnostics_highlights(
doc: &Document,
theme: &Theme,
) -> Vec<(usize, std::ops::Range<usize>)> {
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<usize>)> {
let text = doc.text().slice(..);
let selection = doc.selection(view.id);
let primary_idx = selection.primary_index();
let selection_scope = theme let selection_scope = theme
.find_scope_index("ui.selection") .find_scope_index("ui.selection")
.expect("no selection scope found!"); .expect("no selection scope found!");
let base_cursor_scope = theme let base_cursor_scope = theme
.find_scope_index("ui.cursor") .find_scope_index("ui.cursor")
.unwrap_or(selection_scope); .unwrap_or(selection_scope);
@ -192,9 +240,6 @@ impl EditorView {
} }
.unwrap_or(base_cursor_scope); .unwrap_or(base_cursor_scope);
let highlights: Box<dyn Iterator<Item = HighlightEvent>> = if is_focused {
// TODO: primary + insert mode patching:
// (ui.cursor.primary).patch(mode).unwrap_or(cursor)
let primary_cursor_scope = theme let primary_cursor_scope = theme
.find_scope_index("ui.cursor.primary") .find_scope_index("ui.cursor.primary")
.unwrap_or(cursor_scope); .unwrap_or(cursor_scope);
@ -202,9 +247,8 @@ impl EditorView {
.find_scope_index("ui.selection.primary") .find_scope_index("ui.selection.primary")
.unwrap_or(selection_scope); .unwrap_or(selection_scope);
// inject selections as highlight scopes
let mut spans: Vec<(usize, std::ops::Range<usize>)> = Vec::new(); let mut spans: Vec<(usize, std::ops::Range<usize>)> = Vec::new();
for (i, range) in selections.iter().enumerate() { for (i, range) in selection.iter().enumerate() {
let (cursor_scope, selection_scope) = if i == primary_idx { let (cursor_scope, selection_scope) = if i == primary_idx {
(primary_cursor_scope, primary_selection_scope) (primary_cursor_scope, primary_selection_scope)
} else { } else {
@ -231,25 +275,24 @@ impl EditorView {
} }
} }
Box::new(syntax::merge(highlights, spans)) spans
} else { }
Box::new(highlights)
};
// diagnostic injection pub fn render_text_highlights<H: Iterator<Item = HighlightEvent>>(
let diagnostic_scope = theme.find_scope_index("diagnostic").unwrap_or(cursor_scope); doc: &Document,
let highlights = Box::new(syntax::merge( offset: Position,
highlights, viewport: Rect,
doc.diagnostics() surface: &mut Surface,
.iter() theme: &Theme,
.map(|diagnostic| { highlights: H,
( ) {
diagnostic_scope, let text = doc.text().slice(..);
diagnostic.range.start..diagnostic.range.end,
) let mut spans = Vec::new();
}) let mut visual_x = 0u16;
.collect(), let mut line = 0u16;
)); let tab_width = doc.tab_width();
let tab = " ".repeat(tab_width);
'outer: for event in highlights { 'outer: for event in highlights {
match event { match event {
@ -273,14 +316,14 @@ impl EditorView {
}); });
for grapheme in RopeGraphemes::new(text) { for grapheme in RopeGraphemes::new(text) {
let out_of_bounds = visual_x < view.first_col as u16 let out_of_bounds = visual_x < offset.col as u16
|| visual_x >= viewport.width + view.first_col as u16; || visual_x >= viewport.width + offset.col as u16;
if LineEnding::from_rope_slice(&grapheme).is_some() { if LineEnding::from_rope_slice(&grapheme).is_some() {
if !out_of_bounds { if !out_of_bounds {
// we still want to render an empty cell with the style // we still want to render an empty cell with the style
surface.set_string( surface.set_string(
viewport.x + visual_x - view.first_col as u16, viewport.x + visual_x - offset.col as u16,
viewport.y + line, viewport.y + line,
" ", " ",
style, style,
@ -310,7 +353,7 @@ impl EditorView {
if !out_of_bounds { if !out_of_bounds {
// if we're offscreen just keep going until we hit a new line // if we're offscreen just keep going until we hit a new line
surface.set_string( surface.set_string(
viewport.x + visual_x - view.first_col as u16, viewport.x + visual_x - offset.col as u16,
viewport.y + line, viewport.y + line,
grapheme, grapheme,
style, style,
@ -323,65 +366,33 @@ impl EditorView {
} }
} }
} }
// render gutters
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");
// 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 (i, line) in (view.first_line..(last_line + 1)).enumerate() {
use helix_core::diagnostic::Severity;
if let Some(diagnostic) = doc.diagnostics().iter().find(|d| d.line == line) {
surface.set_stringn(
viewport.x - GUTTER_OFFSET,
viewport.y + i as u16,
"●",
1,
match diagnostic.severity {
Some(Severity::Error) => error,
Some(Severity::Warning) | None => warning,
Some(Severity::Info) => info,
Some(Severity::Hint) => hint,
},
);
}
// Line numbers having selections are rendered
// differently, further below.
let line_number_text = if line == last_line && !draw_last {
" ~".into()
} else {
format!("{:>5}", line + 1)
};
surface.set_stringn(
viewport.x + 1 - GUTTER_OFFSET,
viewport.y + i as u16,
line_number_text,
5,
linenr,
);
} }
// render selections and selected linenr(s) /// Render brace match, selected line numbers, etc (meant for the focused view only)
let linenr_select: Style = theme pub fn render_focused_view_elements(
.try_get("ui.linenr.selected") view: &View,
.unwrap_or_else(|| theme.get("ui.linenr")); doc: &Document,
viewport: Rect,
if is_focused { 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 screen = {
let start = text.line_to_char(view.first_line); 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. let end = text.line_to_char(last_line + 1) + 1; // +1 for cursor at end of text.
Range::new(start, end) 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"));
// 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)) { for selection in selection.iter().filter(|range| range.overlaps(&screen)) {
let head = view.screen_coords_at_pos( let head = view.screen_coords_at_pos(
@ -394,7 +405,7 @@ impl EditorView {
}, },
); );
if let Some(head) = head { if let Some(head) = head {
// Draw line number for selected lines. // Highlight line number for selected lines.
let line_number = view.first_line + head.row; let line_number = view.first_line + head.row;
let line_number_text = if line_number == last_line && !draw_last { let line_number_text = if line_number == last_line && !draw_last {
" ~".into() " ~".into()
@ -402,13 +413,14 @@ impl EditorView {
format!("{:>5}", line_number + 1) format!("{:>5}", line_number + 1)
}; };
surface.set_stringn( surface.set_stringn(
viewport.x + 1 - GUTTER_OFFSET, viewport.x - GUTTER_OFFSET + 1,
viewport.y + head.row as u16, viewport.y + head.row as u16,
line_number_text, line_number_text,
5, 5,
linenr_select, linenr_select,
); );
// Highlight matching braces
// TODO: set cursor position for IME // TODO: set cursor position for IME
if let Some(syntax) = doc.syntax() { if let Some(syntax) = doc.syntax() {
use helix_core::match_brackets; use helix_core::match_brackets;
@ -431,10 +443,7 @@ impl EditorView {
}); });
surface surface
.get_mut( .get_mut(viewport.x + pos.col as u16, viewport.y + pos.row as u16)
viewport.x + pos.col as u16,
viewport.y + pos.row as u16,
)
.set_style(style); .set_style(style);
} }
} }
@ -442,6 +451,60 @@ impl EditorView {
} }
} }
} }
#[allow(clippy::too_many_arguments)]
pub fn render_gutter(
doc: &Document,
view: &View,
viewport: Rect,
surface: &mut Surface,
theme: &Theme,
) {
let text = doc.text().slice(..);
let last_line = view.last_line(doc);
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.
let draw_last = text.line_to_byte(last_line) < text.len_bytes();
for (i, line) in (view.first_line..(last_line + 1)).enumerate() {
use helix_core::diagnostic::Severity;
if let Some(diagnostic) = doc.diagnostics().iter().find(|d| d.line == line) {
surface.set_stringn(
viewport.x - GUTTER_OFFSET,
viewport.y + i as u16,
"●",
1,
match diagnostic.severity {
Some(Severity::Error) => error,
Some(Severity::Warning) | None => warning,
Some(Severity::Info) => info,
Some(Severity::Hint) => hint,
},
);
}
// Line numbers having selections are rendered
// differently, further below.
let line_number_text = if line == last_line && !draw_last {
" ~".into()
} else {
format!("{:>5}", line + 1)
};
surface.set_stringn(
viewport.x + 1 - GUTTER_OFFSET,
viewport.y + i as u16,
line_number_text,
5,
linenr,
);
}
} }
pub fn render_diagnostics( pub fn render_diagnostics(
@ -451,7 +514,6 @@ impl EditorView {
viewport: Rect, viewport: Rect,
surface: &mut Surface, surface: &mut Surface,
theme: &Theme, theme: &Theme,
_is_focused: bool,
) { ) {
use helix_core::diagnostic::Severity; use helix_core::diagnostic::Severity;
use tui::{ use tui::{
@ -469,10 +531,10 @@ impl EditorView {
diagnostic.range.start <= cursor && diagnostic.range.end >= cursor diagnostic.range.start <= cursor && diagnostic.range.end >= cursor
}); });
let warning: Style = theme.get("warning"); let warning = theme.get("warning");
let error: Style = theme.get("error"); let error = theme.get("error");
let info: Style = theme.get("info"); let info = theme.get("info");
let hint: Style = theme.get("hint"); let hint = theme.get("hint");
// Vec::with_capacity(diagnostics.len()); // rough estimate // Vec::with_capacity(diagnostics.len()); // rough estimate
let mut lines = Vec::new(); let mut lines = Vec::new();
@ -961,7 +1023,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 // clear with background color
surface.set_style(area, cx.editor.theme.get("ui.background")); surface.set_style(area, cx.editor.theme.get("ui.background"));
@ -983,7 +1045,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); info.render(area, surface, cx);
} }
@ -1030,7 +1092,7 @@ impl Component for EditorView {
); );
} }
if let Some(completion) = &self.completion { if let Some(completion) = self.completion.as_mut() {
completion.render(area, surface, cx); completion.render(area, surface, cx);
} }
} }

@ -5,7 +5,7 @@ use tui::buffer::Buffer as Surface;
use tui::widgets::{Block, Borders, Widget}; use tui::widgets::{Block, Borders, Widget};
impl Component for Info { 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"); let style = cx.editor.theme.get("ui.popup");
// Calculate the area of the terminal to modify. Because we want to // Calculate the area of the terminal to modify. Because we want to

@ -198,7 +198,7 @@ fn parse<'a>(
Text::from(lines) Text::from(lines)
} }
impl Component for Markdown { 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}; use tui::widgets::{Paragraph, Widget, Wrap};
let text = parse(&self.contents, Some(&cx.editor.theme), &self.config_loader); let text = parse(&self.contents, Some(&cx.editor.theme), &self.config_loader);

@ -258,7 +258,7 @@ impl<T: Item + 'static> Component for Menu<T> {
// TODO: required size should re-trigger when we filter items so we can draw a smaller 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 style = cx.editor.theme.get("ui.text");
let selected = cx.editor.theme.get("ui.menu.selected"); let selected = cx.editor.theme.get("ui.menu.selected");

@ -13,7 +13,7 @@ pub use completion::Completion;
pub use editor::EditorView; pub use editor::EditorView;
pub use markdown::Markdown; pub use markdown::Markdown;
pub use menu::Menu; pub use menu::Menu;
pub use picker::Picker; pub use picker::{FilePicker, Picker};
pub use popup::Popup; pub use popup::Popup;
pub use prompt::{Prompt, PromptEvent}; pub use prompt::{Prompt, PromptEvent};
pub use spinner::{ProgressSpinners, Spinner}; pub use spinner::{ProgressSpinners, Spinner};
@ -73,29 +73,26 @@ pub fn regex_prompt(
) )
} }
pub fn file_picker(root: PathBuf) -> Picker<PathBuf> { pub fn file_picker(root: PathBuf) -> FilePicker<PathBuf> {
use ignore::Walk; use ignore::Walk;
use std::time; use std::time;
let files = Walk::new(root.clone()).filter_map(|entry| match entry { let files = Walk::new(root.clone()).filter_map(|entry| {
Ok(entry) => { let entry = entry.ok()?;
// filter dirs, but we might need special handling for symlinks! // Path::is_dir() traverses symlinks, so we use it over DirEntry::is_dir
if !entry.file_type().map_or(false, |entry| entry.is_dir()) { if entry.path().is_dir() {
let time = if let Ok(metadata) = entry.metadata() { // Will give a false positive if metadata cannot be read (eg. permission error)
return None;
}
let time = entry.metadata().map_or(time::UNIX_EPOCH, |metadata| {
metadata metadata
.accessed() .accessed()
.or_else(|_| metadata.modified()) .or_else(|_| metadata.modified())
.or_else(|_| metadata.created()) .or_else(|_| metadata.created())
.unwrap_or(time::UNIX_EPOCH) .unwrap_or(time::UNIX_EPOCH)
} else { });
time::UNIX_EPOCH
};
Some((entry.into_path(), time)) Some((entry.into_path(), time))
} else {
None
}
}
Err(_err) => None,
}); });
let mut files: Vec<_> = if root.join(".git").is_dir() { let mut files: Vec<_> = if root.join(".git").is_dir() {
@ -109,7 +106,7 @@ pub fn file_picker(root: PathBuf) -> Picker<PathBuf> {
let files = files.into_iter().map(|(path, _)| path).collect(); let files = files.into_iter().map(|(path, _)| path).collect();
Picker::new( FilePicker::new(
files, files,
move |path: &PathBuf| { move |path: &PathBuf| {
// format_fn // format_fn
@ -124,6 +121,7 @@ pub fn file_picker(root: PathBuf) -> Picker<PathBuf> {
.open(path.into(), action) .open(path.into(), action)
.expect("editor.open failed"); .expect("editor.open failed");
}, },
|_editor, path| Some((path.clone(), None)),
) )
} }

@ -1,4 +1,7 @@
use crate::compositor::{Component, Compositor, Context, EventResult}; use crate::{
compositor::{Component, Compositor, Context, EventResult},
ui::EditorView,
};
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
use tui::{ use tui::{
buffer::Buffer as Surface, buffer::Buffer as Surface,
@ -7,17 +10,153 @@ use tui::{
use fuzzy_matcher::skim::SkimMatcherV2 as Matcher; use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
use fuzzy_matcher::FuzzyMatcher; use fuzzy_matcher::FuzzyMatcher;
use tui::widgets::Widget;
use std::borrow::Cow; use std::{borrow::Cow, collections::HashMap, path::PathBuf};
use crate::ui::{Prompt, PromptEvent}; use crate::ui::{Prompt, PromptEvent};
use helix_core::Position; use helix_core::Position;
use helix_view::{ use helix_view::{
document::canonicalize_path,
editor::Action, editor::Action,
graphics::{Color, CursorKind, Rect, Style}, graphics::{Color, CursorKind, Rect, Style},
Editor, 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<usize>);
pub struct FilePicker<T> {
picker: Picker<T>,
/// Caches paths to documents
preview_cache: HashMap<PathBuf, Document>,
/// Given an item in the picker, return the file path and line number to display.
file_fn: Box<dyn Fn(&Editor, &T) -> Option<FileLocation>>,
}
impl<T> FilePicker<T> {
pub fn new(
options: Vec<T>,
format_fn: impl Fn(&T) -> Cow<str> + 'static,
callback_fn: impl Fn(&mut Editor, &T, Action) + 'static,
preview_fn: impl Fn(&Editor, &T) -> Option<FileLocation> + 'static,
) -> Self {
Self {
picker: Picker::new(false, options, format_fn, callback_fn),
preview_cache: HashMap::new(),
file_fn: Box::new(preview_fn),
}
}
fn current_file(&self, editor: &Editor) -> Option<FileLocation> {
self.picker
.selection()
.and_then(|current| (self.file_fn)(editor, current))
.and_then(|(path, line)| canonicalize_path(&path).ok().zip(Some(line)))
}
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() {
// TODO: enable syntax highlighting; blocked by async rendering
let doc = Document::open(&path, None, Some(&editor.theme), None).unwrap();
self.preview_cache.insert(path, doc);
}
}
}
}
impl<T: 'static> Component for FilePicker<T> {
fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
// +---------+ +---------+
// |prompt | |preview |
// +---------+ | |
// |picker | | |
// | | | |
// +---------+ +---------+
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_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 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);
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))
}) {
// align to middle
let first_line = line.unwrap_or(0).saturating_sub(inner.height as usize / 2);
let offset = Position::new(first_line, 0);
let highlights = EditorView::doc_syntax_highlights(
doc,
offset,
area.height,
&cx.editor.theme,
&cx.editor.syn_loader,
);
EditorView::render_text_highlights(
doc,
offset,
inner,
surface,
&cx.editor.theme,
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(first_line) as u16)
.set_style(cx.editor.theme.get("ui.selection.primary"));
}
}
}
}
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<Position>, CursorKind) {
self.picker.cursor(area, ctx)
}
}
pub struct Picker<T> { pub struct Picker<T> {
options: Vec<T>, options: Vec<T>,
// filter: String, // filter: String,
@ -30,6 +169,8 @@ pub struct Picker<T> {
cursor: usize, cursor: usize,
// pattern: String, // pattern: String,
prompt: Prompt, prompt: Prompt,
/// Whether to render in the middle of the area
render_centered: bool,
format_fn: Box<dyn Fn(&T) -> Cow<str>>, format_fn: Box<dyn Fn(&T) -> Cow<str>>,
callback_fn: Box<dyn Fn(&mut Editor, &T, Action)>, callback_fn: Box<dyn Fn(&mut Editor, &T, Action)>,
@ -37,6 +178,7 @@ pub struct Picker<T> {
impl<T> Picker<T> { impl<T> Picker<T> {
pub fn new( pub fn new(
render_centered: bool,
options: Vec<T>, options: Vec<T>,
format_fn: impl Fn(&T) -> Cow<str> + 'static, format_fn: impl Fn(&T) -> Cow<str> + 'static,
callback_fn: impl Fn(&mut Editor, &T, Action) + 'static, callback_fn: impl Fn(&mut Editor, &T, Action) + 'static,
@ -57,6 +199,7 @@ impl<T> Picker<T> {
filters: Vec::new(), filters: Vec::new(),
cursor: 0, cursor: 0,
prompt, prompt,
render_centered,
format_fn: Box::new(format_fn), format_fn: Box::new(format_fn),
callback_fn: Box::new(callback_fn), callback_fn: Box::new(callback_fn),
}; };
@ -139,8 +282,8 @@ impl<T> Picker<T> {
// - score all the names in relation to input // - score all the names in relation to input
fn inner_rect(area: Rect) -> Rect { fn inner_rect(area: Rect) -> Rect {
let padding_vertical = area.height * 20 / 100; let padding_vertical = area.height * 10 / 100;
let padding_horizontal = area.width * 20 / 100; let padding_horizontal = area.width * 10 / 100;
Rect::new( Rect::new(
area.x + padding_horizontal, area.x + padding_horizontal,
@ -174,7 +317,9 @@ impl<T: 'static> Component for Picker<T> {
| KeyEvent { | KeyEvent {
code: KeyCode::Char('p'), code: KeyCode::Char('p'),
modifiers: KeyModifiers::CONTROL, modifiers: KeyModifiers::CONTROL,
} => self.move_up(), } => {
self.move_up();
}
KeyEvent { KeyEvent {
code: KeyCode::Down, code: KeyCode::Down,
.. ..
@ -185,7 +330,9 @@ impl<T: 'static> Component for Picker<T> {
| KeyEvent { | KeyEvent {
code: KeyCode::Char('n'), code: KeyCode::Char('n'),
modifiers: KeyModifiers::CONTROL, modifiers: KeyModifiers::CONTROL,
} => self.move_down(), } => {
self.move_down();
}
KeyEvent { KeyEvent {
code: KeyCode::Esc, .. code: KeyCode::Esc, ..
} }
@ -239,16 +386,18 @@ impl<T: 'static> Component for Picker<T> {
EventResult::Consumed(None) 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 = inner_rect(area); let area = if self.render_centered {
inner_rect(area)
} else {
area
};
// -- Render the frame: // -- Render the frame:
// clear area // clear area
let background = cx.editor.theme.get("ui.background"); let background = cx.editor.theme.get("ui.background");
surface.clear_with(area, background); surface.clear_with(area, background);
use tui::widgets::Widget;
// don't like this but the lifetime sucks // don't like this but the lifetime sucks
let block = Block::default().borders(Borders::ALL); let block = Block::default().borders(Borders::ALL);
@ -263,21 +412,23 @@ impl<T: 'static> Component for Picker<T> {
self.prompt.render(area, surface, cx); self.prompt.render(area, surface, cx);
// -- Separator // -- Separator
let style = Style::default().fg(Color::Rgb(90, 89, 119)); let sep_style = Style::default().fg(Color::Rgb(90, 89, 119));
let symbols = BorderType::line_symbols(BorderType::Plain); let borders = BorderType::line_symbols(BorderType::Plain);
for x in inner.left()..inner.right() { for x in inner.left()..inner.right() {
surface surface
.get_mut(x, inner.y + 1) .get_mut(x, inner.y + 1)
.set_symbol(symbols.horizontal) .set_symbol(borders.horizontal)
.set_style(style); .set_style(sep_style);
} }
// -- Render the contents: // -- 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 style = cx.editor.theme.get("ui.text"); let style = cx.editor.theme.get("ui.text");
let selected = Style::default().fg(Color::Rgb(255, 255, 255)); 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 offset = self.cursor / (rows as usize) * (rows as usize);
let files = self.matches.iter().skip(offset).map(|(index, _score)| { let files = self.matches.iter().skip(offset).map(|(index, _score)| {
@ -286,14 +437,14 @@ impl<T: 'static> Component for Picker<T> {
for (i, (_index, option)) in files.take(rows as usize).enumerate() { for (i, (_index, option)) in files.take(rows as usize).enumerate() {
if i == (self.cursor - offset) { 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( surface.set_string_truncated(
inner.x + 3, inner.x,
inner.y + 2 + i as u16, inner.y + i as u16,
(self.format_fn)(option), (self.format_fn)(option),
(inner.width as usize).saturating_sub(3), // account for the " > " inner.width as usize,
if i == (self.cursor - offset) { if i == (self.cursor - offset) {
selected selected
} else { } else {

@ -105,7 +105,7 @@ impl<T: Component> Component for Popup<T> {
Some(self.size) 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); cx.scroll = Some(self.scroll);
let position = self let position = self

@ -352,7 +352,7 @@ impl Prompt {
} }
if let Some(doc) = (self.doc_fn)(&self.line) { 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 viewport = area;
let area = viewport.intersection(Rect::new( let area = viewport.intersection(Rect::new(
@ -546,7 +546,7 @@ impl Component for Prompt {
EventResult::Consumed(None) 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) self.render_prompt(area, surface, cx)
} }

@ -13,7 +13,7 @@ impl Text {
} }
} }
impl Component for 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}; use tui::widgets::{Paragraph, Widget, Wrap};
let contents = tui::text::Text::from(self.contents.clone()); let contents = tui::text::Text::from(self.contents.clone());

@ -432,14 +432,14 @@ impl Document {
/// Create a new document from `path`. Encoding is auto-detected, but it can be manually /// Create a new document from `path`. Encoding is auto-detected, but it can be manually
/// overwritten with the `encoding` parameter. /// overwritten with the `encoding` parameter.
pub fn open( pub fn open(
path: PathBuf, path: &Path,
encoding: Option<&'static encoding_rs::Encoding>, encoding: Option<&'static encoding_rs::Encoding>,
theme: Option<&Theme>, theme: Option<&Theme>,
config_loader: Option<&syntax::Loader>, config_loader: Option<&syntax::Loader>,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
let (rope, encoding) = if path.exists() { let (rope, encoding) = if path.exists() {
let mut file = 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)? from_reader(&mut file, encoding)?
} else { } else {
let encoding = encoding.unwrap_or(encoding_rs::UTF_8); let encoding = encoding.unwrap_or(encoding_rs::UTF_8);
@ -449,7 +449,7 @@ impl Document {
let mut doc = Self::from(rope, Some(encoding)); let mut doc = Self::from(rope, Some(encoding));
// set the path and try detecting the language // set the path and try detecting the language
doc.set_path(&path)?; doc.set_path(path)?;
if let Some(loader) = config_loader { if let Some(loader) = config_loader {
doc.detect_language(theme, loader); doc.detect_language(theme, loader);
} }
@ -904,6 +904,10 @@ impl Document {
&self.selections[&view_id] &self.selections[&view_id]
} }
pub fn selections(&self) -> &HashMap<ViewId, Selection> {
&self.selections
}
pub fn relative_path(&self) -> Option<PathBuf> { pub fn relative_path(&self) -> Option<PathBuf> {
let cwdir = std::env::current_dir().expect("couldn't determine current directory"); let cwdir = std::env::current_dir().expect("couldn't determine current directory");

@ -7,7 +7,11 @@ use crate::{
}; };
use futures_util::future; use futures_util::future;
use std::{path::PathBuf, sync::Arc, time::Duration}; use std::{
path::{Path, PathBuf},
sync::Arc,
time::Duration,
};
use slotmap::SlotMap; use slotmap::SlotMap;
@ -222,7 +226,7 @@ impl Editor {
let id = if let Some(id) = id { let id = if let Some(id) = id {
id id
} else { } 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 // try to find a language server based on the language name
let language_server = doc let language_server = doc
@ -316,6 +320,11 @@ impl Editor {
self.documents.iter_mut().map(|(_id, doc)| doc) self.documents.iter_mut().map(|(_id, doc)| doc)
} }
pub fn document_by_path<P: AsRef<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 { // pub fn current_document(&self) -> Document {
// let id = self.view().doc; // let id = self.view().doc;
// let doc = &mut editor.documents[id]; // let doc = &mut editor.documents[id];

@ -10,7 +10,7 @@ use helix_core::{
type Jump = (DocumentId, Selection); type Jump = (DocumentId, Selection);
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct JumpList { pub struct JumpList {
jumps: Vec<Jump>, jumps: Vec<Jump>,
current: usize, current: usize,

Loading…
Cancel
Save