aboutsummaryrefslogtreecommitdiff
path: root/helix-term/src
diff options
context:
space:
mode:
Diffstat (limited to 'helix-term/src')
-rw-r--r--helix-term/src/commands.rs33
-rw-r--r--helix-term/src/compositor.rs6
-rw-r--r--helix-term/src/ui/completion.rs4
-rw-r--r--helix-term/src/ui/editor.rs408
-rw-r--r--helix-term/src/ui/info.rs2
-rw-r--r--helix-term/src/ui/markdown.rs2
-rw-r--r--helix-term/src/ui/menu.rs2
-rw-r--r--helix-term/src/ui/mod.rs42
-rw-r--r--helix-term/src/ui/picker.rs191
-rw-r--r--helix-term/src/ui/popup.rs2
-rw-r--r--helix-term/src/ui/prompt.rs4
-rw-r--r--helix-term/src/ui/text.rs2
12 files changed, 464 insertions, 234 deletions
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
index 2b1b859b..0d118dbc 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, Picker, Popup, Prompt, PromptEvent},
+ ui::{self, FilePicker, Picker, Popup, Prompt, PromptEvent},
};
use crate::job::{self, Job, Jobs};
@@ -2212,7 +2212,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()
@@ -2234,6 +2234,15 @@ fn buffer_picker(cx: &mut Context) {
|editor: &mut Editor, (id, _path): &(DocumentId, Option<PathBuf>), _action| {
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));
}
@@ -2287,7 +2296,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| {
@@ -2297,10 +2306,15 @@ 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::single(range.anchor, range.head));
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))
}
@@ -2332,6 +2346,7 @@ pub fn code_action(cx: &mut Context) {
response: Option<lsp::CodeActionResponse>| {
if let Some(actions) = response {
let picker = Picker::new(
+ true,
actions,
|action| match action {
lsp::CodeActionOrCommand::CodeAction(action) => {
@@ -2703,7 +2718,7 @@ fn goto_impl(
editor.set_error("No definition found.".to_string());
}
_locations => {
- let picker = ui::Picker::new(
+ let picker = FilePicker::new(
locations,
move |location| {
let file: Cow<'_, str> = (location.uri.scheme() == "file")
@@ -2728,6 +2743,11 @@ fn goto_impl(
move |editor: &mut Editor, location, 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));
}
@@ -3729,8 +3749,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::single(range.anchor, range.head));
}
fn completion(cx: &mut Context) {
diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs
index 628c4e13..36e54ede 100644
--- a/helix-term/src/compositor.rs
+++ b/helix-term/src/compositor.rs
@@ -46,7 +46,7 @@ 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<Position>, CursorKind) {
@@ -152,8 +152,8 @@ impl Compositor {
let area = *surface.area();
- for layer in &self.layers {
- layer.render(area, surface, cx)
+ for layer in &mut self.layers {
+ layer.render(area, surface, cx);
}
let (pos, kind) = self.cursor(area, cx.editor);
diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs
index 6d737652..4e01ce1c 100644
--- a/helix-term/src/ui/completion.rs
+++ b/helix-term/src/ui/completion.rs
@@ -241,7 +241,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
@@ -263,7 +263,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 96a4afe8..8c46eef9 100644
--- a/helix-term/src/ui/editor.rs
+++ b/helix-term/src/ui/editor.rs
@@ -34,7 +34,7 @@ pub struct EditorView {
last_insert: (commands::Command, Vec<KeyEvent>),
completion: Option<Completion>,
spinners: ProgressSpinners,
- pub autoinfo: Option<Info>,
+ autoinfo: Option<Info>,
}
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.height.saturating_sub(1),
); // - 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 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(
view.area.x,
@@ -105,31 +123,34 @@ impl EditorView {
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)]
- pub fn render_buffer(
- &self,
- doc: &Document,
- view: &View,
- viewport: Rect,
- surface: &mut Surface,
+ pub fn doc_syntax_highlights<'doc>(
+ doc: &'doc Document,
+ offset: Position,
+ height: u16,
theme: &Theme,
- is_focused: bool,
loader: &syntax::Loader,
- ) {
+ ) -> Box<dyn Iterator<Item = HighlightEvent> + 'doc> {
let text = doc.text().slice(..);
-
- let last_line = view.last_line(doc);
+ 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(view.first_line);
+ let start = text.line_to_byte(offset.row);
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() {
+ let highlights = match doc.syntax() {
Some(syntax) => {
let scopes = theme.scopes();
syntax
@@ -151,20 +172,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(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));
@@ -174,13 +191,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<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
.find_scope_index("ui.selection")
.expect("no selection scope found!");
-
let base_cursor_scope = theme
.find_scope_index("ui.cursor")
.unwrap_or(selection_scope);
@@ -192,64 +240,59 @@ impl EditorView {
}
.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
- .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<usize>)> = 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<usize>)> = 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
+ }
+
+ pub fn render_text_highlights<H: Iterator<Item = HighlightEvent>>(
+ doc: &Document,
+ offset: Position,
+ viewport: Rect,
+ surface: &mut Surface,
+ theme: &Theme,
+ highlights: H,
+ ) {
+ let text = doc.text().slice(..);
- // 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(),
- ));
+ 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 {
@@ -273,14 +316,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 +353,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,14 +366,108 @@ impl EditorView {
}
}
}
+ }
+
+ /// 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)
+ };
+
+ // 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)) {
+ 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 - GUTTER_OFFSET + 1,
+ viewport.y + head.row as u16,
+ line_number_text,
+ 5,
+ linenr_select,
+ );
+
+ // 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);
+ }
+ }
+ }
+ }
+ }
+ }
- // render gutters
+ #[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: 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.
@@ -368,80 +505,6 @@ impl EditorView {
linenr,
);
}
-
- // render selections and 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 - GUTTER_OFFSET,
- viewport.y + head.row as u16,
- line_number_text,
- 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);
- }
- }
- }
- }
- }
- }
}
pub fn render_diagnostics(
@@ -451,7 +514,6 @@ impl EditorView {
viewport: Rect,
surface: &mut Surface,
theme: &Theme,
- _is_focused: bool,
) {
use helix_core::diagnostic::Severity;
use tui::{
@@ -469,10 +531,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();
@@ -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
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);
}
@@ -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);
}
}
diff --git a/helix-term/src/ui/info.rs b/helix-term/src/ui/info.rs
index 6e810b86..0f14260e 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 6c79ca67..b7e29f21 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 26eff1d8..3e63db35 100644
--- a/helix-term/src/ui/menu.rs
+++ b/helix-term/src/ui/menu.rs
@@ -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
- 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/mod.rs b/helix-term/src/ui/mod.rs
index f68ad0a7..d1af0e48 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, Picker};
pub use popup::Popup;
pub use prompt::{Prompt, PromptEvent};
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 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() {
@@ -109,7 +106,7 @@ pub fn file_picker(root: PathBuf) -> Picker<PathBuf> {
let files = files.into_iter().map(|(path, _)| path).collect();
- Picker::new(
+ FilePicker::new(
files,
move |path: &PathBuf| {
// format_fn
@@ -124,6 +121,7 @@ pub fn file_picker(root: PathBuf) -> Picker<PathBuf> {
.open(path.into(), action)
.expect("editor.open failed");
},
+ |_editor, path| Some((path.clone(), None)),
)
}
diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs
index 0b67cd9c..9c6b328f 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::{
+ compositor::{Component, Compositor, Context, EventResult},
+ ui::EditorView,
+};
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
use tui::{
buffer::Buffer as Surface,
@@ -7,17 +10,153 @@ use tui::{
use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
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 helix_core::Position;
use helix_view::{
+ document::canonicalize_path,
editor::Action,
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> {
options: Vec<T>,
// filter: String,
@@ -30,6 +169,8 @@ pub struct Picker<T> {
cursor: usize,
// pattern: String,
prompt: Prompt,
+ /// Whether to render in the middle of the area
+ render_centered: bool,
format_fn: Box<dyn Fn(&T) -> Cow<str>>,
callback_fn: Box<dyn Fn(&mut Editor, &T, Action)>,
@@ -37,6 +178,7 @@ pub struct Picker<T> {
impl<T> Picker<T> {
pub fn new(
+ render_centered: bool,
options: Vec<T>,
format_fn: impl Fn(&T) -> Cow<str> + 'static,
callback_fn: impl Fn(&mut Editor, &T, Action) + 'static,
@@ -57,6 +199,7 @@ impl<T> Picker<T> {
filters: Vec::new(),
cursor: 0,
prompt,
+ render_centered,
format_fn: Box::new(format_fn),
callback_fn: Box::new(callback_fn),
};
@@ -139,8 +282,8 @@ impl<T> Picker<T> {
// - 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,
@@ -174,7 +317,9 @@ impl<T: 'static> Component for Picker<T> {
| KeyEvent {
code: KeyCode::Char('p'),
modifiers: KeyModifiers::CONTROL,
- } => self.move_up(),
+ } => {
+ self.move_up();
+ }
KeyEvent {
code: KeyCode::Down,
..
@@ -185,7 +330,9 @@ impl<T: 'static> Component for Picker<T> {
| KeyEvent {
code: KeyCode::Char('n'),
modifiers: KeyModifiers::CONTROL,
- } => self.move_down(),
+ } => {
+ self.move_down();
+ }
KeyEvent {
code: KeyCode::Esc, ..
}
@@ -239,16 +386,18 @@ impl<T: 'static> Component for Picker<T> {
EventResult::Consumed(None)
}
- fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
- let area = inner_rect(area);
+ fn render(&mut 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");
surface.clear_with(area, background);
- use tui::widgets::Widget;
// don't like this but the lifetime sucks
let block = Block::default().borders(Borders::ALL);
@@ -263,21 +412,23 @@ impl<T: 'static> Component for Picker<T> {
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 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 +437,14 @@ impl<T: 'static> Component for Picker<T> {
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 " > "
+ inner.width as usize,
if i == (self.cursor - offset) {
selected
} else {
diff --git a/helix-term/src/ui/popup.rs b/helix-term/src/ui/popup.rs
index 29ffb4ad..e31d4d7b 100644
--- a/helix-term/src/ui/popup.rs
+++ b/helix-term/src/ui/popup.rs
@@ -105,7 +105,7 @@ impl<T: Component> Component for Popup<T> {
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 57daef3a..8ec3674e 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 249cf89e..65a75a4a 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());