use crate::{ commands, compositor::{Component, Context, EventResult}, key, keymap::{KeymapResult, KeymapResultKind, Keymaps}, ui::{Completion, ProgressSpinners}, }; use helix_core::{ coords_at_pos, graphemes::{ensure_grapheme_boundary_next, next_grapheme_boundary, prev_grapheme_boundary}, movement::Direction, syntax::{self, HighlightEvent}, unicode::segmentation::UnicodeSegmentation, unicode::width::UnicodeWidthStr, LineEnding, Position, Range, Selection, }; use helix_view::{ document::{Mode, SCRATCH_BUFFER_NAME}, editor::{Config, LineNumber}, graphics::{CursorKind, Modifier, Rect, Style}, info::Info, input::KeyEvent, keyboard::{KeyCode, KeyModifiers}, Document, Editor, Theme, View, }; use std::borrow::Cow; use crossterm::event::{Event, MouseButton, MouseEvent, MouseEventKind}; use tui::buffer::Buffer as Surface; pub struct EditorView { keymaps: Keymaps, on_next_key: Option>, last_insert: (commands::Command, Vec), pub(crate) completion: Option, spinners: ProgressSpinners, autoinfo: Option, } impl Default for EditorView { fn default() -> Self { Self::new(Keymaps::default()) } } impl EditorView { pub fn new(keymaps: Keymaps) -> Self { Self { keymaps, on_next_key: None, last_insert: (commands::Command::normal_mode, Vec::new()), completion: None, spinners: ProgressSpinners::default(), autoinfo: None, } } pub fn spinners_mut(&mut self) -> &mut ProgressSpinners { &mut self.spinners } #[allow(clippy::too_many_arguments)] pub fn render_view( &self, doc: &Document, view: &View, viewport: Rect, surface: &mut Surface, theme: &Theme, is_focused: bool, loader: &syntax::Loader, config: &helix_view::editor::Config, ) { let inner = view.inner_area(); let area = view.area; let highlights = Self::doc_syntax_highlights(doc, view.offset, inner.height, theme, loader); let highlights = syntax::merge(highlights, Self::doc_diagnostics_highlights(doc, theme)); let highlights: Box> = if is_focused { Box::new(syntax::merge( highlights, Self::doc_selection_highlights(doc, view, theme), )) } else { Box::new(highlights) }; Self::render_text_highlights(doc, view.offset, inner, surface, theme, highlights); Self::render_gutter(doc, view, view.area, surface, theme, is_focused, config); if is_focused { Self::render_focused_view_elements(view, doc, inner, theme, surface); } // if we're not at the edge of the screen, draw a right border if viewport.right() != view.area.right() { let x = area.right(); let border_style = theme.get("ui.window"); for y in area.top()..area.bottom() { surface .get_mut(x, y) .set_symbol(tui::symbols::line::VERTICAL) //.set_symbol(" ") .set_style(border_style); } } self.render_diagnostics(doc, view, inner, surface, theme); let statusline_area = view .area .clip_top(view.area.height.saturating_sub(1)) .clip_bottom(1); // -1 from bottom to remove commandline self.render_statusline(doc, view, statusline_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 doc_syntax_highlights<'doc>( doc: &'doc Document, offset: Position, height: u16, theme: &Theme, loader: &syntax::Loader, ) -> Box + 'doc> { let text = doc.text().slice(..); let last_line = std::cmp::min( // Saturating subs to make it inclusive zero indexing. (offset.row + height as usize).saturating_sub(1), doc.text().len_lines().saturating_sub(1), ); let range = { // calculate viewport byte ranges let start = text.line_to_byte(offset.row); let end = text.line_to_byte(last_line + 1); start..end }; // TODO: range doesn't actually restrict source, just highlight range let highlights = match doc.syntax() { Some(syntax) => { let scopes = theme.scopes(); syntax .highlight_iter(text.slice(..), Some(range), None, |language| { loader.language_configuration_for_injection_string(language) .and_then(|language_config| { let config = language_config.highlight_config(scopes)?; let config_ref = config.as_ref(); // SAFETY: the referenced `HighlightConfiguration` behind // the `Arc` is guaranteed to remain valid throughout the // duration of the highlight. let config_ref = unsafe { std::mem::transmute::< _, &'static syntax::HighlightConfiguration, >(config_ref) }; Some(config_ref) }) }) .map(|event| event.unwrap()) .collect() // TODO: we collect here to avoid holding the lock, fix later } None => vec![HighlightEvent::Source { start: range.start, end: range.end, }], } .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)); let end = ensure_grapheme_boundary_next(text, text.byte_to_char(end)); HighlightEvent::Source { start, end } } event => event, }); Box::new(highlights) } /// Get highlight spans for document diagnostics pub fn doc_diagnostics_highlights( doc: &Document, theme: &Theme, ) -> Vec<(usize, std::ops::Range)> { let diagnostic_scope = theme .find_scope_index("diagnostic") .or_else(|| theme.find_scope_index("ui.cursor")) .or_else(|| theme.find_scope_index("ui.selection")) .expect( "at least one of the following scopes must be defined in the theme: `diagnostic`, `ui.cursor`, or `ui.selection`", ); doc.diagnostics() .iter() .map(|diagnostic| { ( diagnostic_scope, diagnostic.range.start..diagnostic.range.end, ) }) .collect() } /// Get highlight spans for selections in a document view. pub fn doc_selection_highlights( doc: &Document, view: &View, theme: &Theme, ) -> Vec<(usize, std::ops::Range)> { let text = doc.text().slice(..); let selection = doc.selection(view.id); let primary_idx = selection.primary_index(); let selection_scope = theme .find_scope_index("ui.selection") .expect("could not find `ui.selection` scope in the theme!"); let base_cursor_scope = theme .find_scope_index("ui.cursor") .unwrap_or(selection_scope); let cursor_scope = match doc.mode() { Mode::Insert => theme.find_scope_index("ui.cursor.insert"), Mode::Select => theme.find_scope_index("ui.cursor.select"), Mode::Normal => Some(base_cursor_scope), } .unwrap_or(base_cursor_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); let mut spans: Vec<(usize, std::ops::Range)> = Vec::new(); for (i, range) in selection.iter().enumerate() { let (cursor_scope, selection_scope) = if i == primary_idx { (primary_cursor_scope, primary_selection_scope) } else { (cursor_scope, selection_scope) }; // 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 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>( doc: &Document, offset: Position, viewport: Rect, surface: &mut Surface, theme: &Theme, highlights: H, ) { let text = doc.text().slice(..); let mut spans = Vec::new(); let mut visual_x = 0u16; let mut line = 0u16; let tab_width = doc.tab_width(); let tab = " ".repeat(tab_width); let text_style = theme.get("ui.text"); 'outer: for event in highlights { match event { HighlightEvent::HighlightStart(span) => { spans.push(span); } HighlightEvent::HighlightEnd => { spans.pop(); } HighlightEvent::Source { start, end } => { // `unwrap_or_else` part is for off-the-end indices of // the rope, to allow cursor highlighting at the end // of the rope. let text = text.get_slice(start..end).unwrap_or_else(|| " ".into()); use helix_core::graphemes::{grapheme_width, RopeGraphemes}; let style = spans.iter().fold(text_style, |acc, span| { let style = theme.get(theme.scopes()[span.0].as_str()); acc.patch(style) }); for grapheme in RopeGraphemes::new(text) { let out_of_bounds = visual_x < 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 - offset.col as u16, viewport.y + line, " ", style, ); } visual_x = 0; line += 1; // TODO: with proper iter this shouldn't be necessary if line >= viewport.height { break 'outer; } } else { let grapheme = Cow::from(grapheme); let (grapheme, width) = if grapheme == "\t" { // make sure we display tab as appropriate amount of spaces (tab.as_str(), tab_width) } else { // Cow will prevent allocations if span contained in a single slice // which should really be the majority case let width = grapheme_width(&grapheme); (grapheme.as_ref(), width) }; if !out_of_bounds { // if we're offscreen just keep going until we hit a new line surface.set_string( viewport.x + visual_x - offset.col as u16, viewport.y + line, grapheme, style, ); } visual_x = visual_x.saturating_add(width as u16); } } } } } } /// Render brace match, etc (meant for the focused view only) pub fn render_focused_view_elements( view: &View, doc: &Document, viewport: Rect, theme: &Theme, surface: &mut Surface, ) { // Highlight matching braces if let Some(syntax) = doc.syntax() { let text = doc.text().slice(..); use helix_core::match_brackets; let pos = doc.selection(view.id).primary().cursor(text); let pos = match_brackets::find_matching_bracket(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.offset.col as u16 && pos.col >= view.offset.col { let style = theme.try_get("ui.cursor.match").unwrap_or_else(|| { Style::default() .add_modifier(Modifier::REVERSED) .add_modifier(Modifier::DIM) }); surface .get_mut(viewport.x + pos.col as u16, viewport.y + pos.row as u16) .set_style(style); } } } } #[allow(clippy::too_many_arguments)] pub fn render_gutter( doc: &Document, view: &View, viewport: Rect, surface: &mut Surface, theme: &Theme, is_focused: bool, config: &helix_view::editor::Config, ) { let text = doc.text().slice(..); let last_line = view.last_line(doc); // it's used inside an iterator so the collect isn't needless: // https://github.com/rust-lang/rust-clippy/issues/6164 #[allow(clippy::needless_collect)] let cursors: Vec<_> = doc .selection(view.id) .iter() .map(|range| range.cursor_line(text)) .collect(); use std::fmt::Write; fn diagnostic<'doc>( doc: &'doc Document, _view: &View, theme: &Theme, _config: &Config, _is_focused: bool, _width: usize, ) -> GutterFn<'doc> { let warning = theme.get("warning"); let error = theme.get("error"); let info = theme.get("info"); let hint = theme.get("hint"); let diagnostics = doc.diagnostics(); Box::new(move |line: usize, _selected: bool, out: &mut String| { use helix_core::diagnostic::Severity; if let Some(diagnostic) = diagnostics.iter().find(|d| d.line == line) { write!(out, "●").unwrap(); return Some(match diagnostic.severity { Some(Severity::Error) => error, Some(Severity::Warning) | None => warning, Some(Severity::Info) => info, Some(Severity::Hint) => hint, }); } None }) } fn line_number<'doc>( doc: &'doc Document, view: &View, theme: &Theme, config: &Config, is_focused: bool, width: usize, ) -> GutterFn<'doc> { let text = doc.text().slice(..); let last_line = view.last_line(doc); // 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(); let linenr = theme.get("ui.linenr"); let linenr_select: Style = theme.try_get("ui.linenr.selected").unwrap_or(linenr); let current_line = doc .text() .char_to_line(doc.selection(view.id).primary().cursor(text)); let config = config.line_number; Box::new(move |line: usize, selected: bool, out: &mut String| { if line == last_line && !draw_last { write!(out, "{:>1$}", '~', width).unwrap(); Some(linenr) } else { let line = match config { LineNumber::Absolute => line + 1, LineNumber::Relative => { if current_line == line { line + 1 } else { abs_diff(current_line, line) } } }; let style = if selected && is_focused { linenr_select } else { linenr }; write!(out, "{:>1$}", line, width).unwrap(); Some(style) } }) } type GutterFn<'doc> = Box Option