diff options
Diffstat (limited to 'helix-term/src/ui/editor.rs')
-rw-r--r-- | helix-term/src/ui/editor.rs | 408 |
1 files changed, 235 insertions, 173 deletions
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); } } |