Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-term/src/ui/editor.rs')
| -rw-r--r-- | helix-term/src/ui/editor.rs | 1845 |
1 files changed, 661 insertions, 1184 deletions
diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index b25af107..5dc9f8eb 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -1,60 +1,47 @@ use crate::{ - commands::{self, OnKeyCallback, OnKeyCallbackKind}, - compositor::{Component, Context, Event, EventResult}, - events::{OnModeSwitch, PostCommand}, - handlers::completion::CompletionItem, + commands, + compositor::{Component, Context, EventResult}, key, - keymap::{KeymapResult, Keymaps}, - ui::{ - document::{render_document, LinePos, TextRenderer}, - statusline, - text_decorations::{self, Decoration, DecorationManager, InlineDiagnostics}, - Completion, ProgressSpinners, - }, + keymap::{KeymapResult, KeymapResultKind, Keymaps}, + ui::{Completion, ProgressSpinners}, }; use helix_core::{ - diagnostic::NumberOrString, - graphemes::{next_grapheme_boundary, prev_grapheme_boundary}, + coords_at_pos, + graphemes::{ensure_grapheme_boundary_next, next_grapheme_boundary, prev_grapheme_boundary}, movement::Direction, - syntax::{self, OverlayHighlights}, - text_annotations::TextAnnotations, + syntax::{self, HighlightEvent}, + unicode::segmentation::UnicodeSegmentation, unicode::width::UnicodeWidthStr, - visual_offset_from_block, Change, Position, Range, Selection, Transaction, + LineEnding, Position, Range, Selection, }; use helix_view::{ - annotations::diagnostics::DiagnosticFilter, - document::{Mode, SCRATCH_BUFFER_NAME}, - editor::{CompleteAction, CursorShapeConfig}, - graphics::{Color, CursorKind, Modifier, Rect, Style}, - input::{KeyEvent, MouseButton, MouseEvent, MouseEventKind}, + document::Mode, + editor::LineNumber, + graphics::{CursorKind, Modifier, Rect, Style}, + info::Info, + input::KeyEvent, keyboard::{KeyCode, KeyModifiers}, Document, Editor, Theme, View, }; -use std::{mem::take, num::NonZeroUsize, ops, path::PathBuf, rc::Rc}; +use std::borrow::Cow; -use tui::{buffer::Buffer as Surface, text::Span}; +use crossterm::event::{Event, MouseButton, MouseEvent, MouseEventKind}; +use tui::buffer::Buffer as Surface; pub struct EditorView { - pub keymaps: Keymaps, - on_next_key: Option<(OnKeyCallback, OnKeyCallbackKind)>, - pseudo_pending: Vec<KeyEvent>, - pub(crate) last_insert: (commands::MappableCommand, Vec<InsertEvent>), + keymaps: Keymaps, + on_next_key: Option<Box<dyn FnOnce(&mut commands::Context, KeyEvent)>>, + last_insert: (commands::Command, Vec<KeyEvent>), pub(crate) completion: Option<Completion>, spinners: ProgressSpinners, - /// Tracks if the terminal window is focused by reaction to terminal focus events - terminal_focused: bool, + autoinfo: Option<Info>, } -#[derive(Debug, Clone)] -pub enum InsertEvent { - Key(KeyEvent), - CompletionApply { - trigger_offset: usize, - changes: Vec<Change>, - }, - TriggerCompletion, - RequestCompletion, +impl Default for EditorView { + fn default() -> Self { + Self::new(Keymaps::default()) + } } impl EditorView { @@ -62,11 +49,10 @@ impl EditorView { Self { keymaps, on_next_key: None, - pseudo_pending: Vec::new(), - last_insert: (commands::MappableCommand::normal_mode, Vec::new()), + last_insert: (commands::Command::normal_mode, Vec::new()), completion: None, spinners: ProgressSpinners::default(), - terminal_focused: true, + autoinfo: None, } } @@ -74,439 +60,189 @@ impl EditorView { &mut self.spinners } + #[allow(clippy::too_many_arguments)] pub fn render_view( &self, - editor: &Editor, 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(doc); + let inner = view.inner_area(); let area = view.area; - let theme = &editor.theme; - let config = editor.config(); - let loader = editor.syn_loader.load(); - - let view_offset = doc.view_offset(view.id); - let text_annotations = view.text_annotations(doc, Some(theme)); - let mut decorations = DecorationManager::default(); - - if is_focused && config.cursorline { - decorations.add_decoration(Self::cursorline(doc, view, theme)); - } - - if is_focused && config.cursorcolumn { - Self::highlight_cursorcolumn(doc, view, surface, theme, inner, &text_annotations); - } - - // Set DAP highlights, if needed. - if let Some(frame) = editor.current_stack_frame() { - let dap_line = frame.line.saturating_sub(1); - let style = theme.get("ui.highlight.frameline"); - let line_decoration = move |renderer: &mut TextRenderer, pos: LinePos| { - if pos.doc_line != dap_line { - return; - } - renderer.set_style(Rect::new(inner.x, pos.visual_line, inner.width, 1), style); - }; - - decorations.add_decoration(line_decoration); - } - - let syntax_highlighter = - Self::doc_syntax_highlighter(doc, view_offset.anchor, inner.height, &loader); - let mut overlays = Vec::new(); - - overlays.push(Self::overlay_syntax_highlights( - doc, - view_offset.anchor, - inner.height, - &text_annotations, - )); - - if doc - .language_config() - .and_then(|config| config.rainbow_brackets) - .unwrap_or(config.rainbow_brackets) - { - if let Some(overlay) = - Self::doc_rainbow_highlights(doc, view_offset.anchor, inner.height, theme, &loader) - { - overlays.push(overlay); - } - } - - Self::doc_diagnostics_highlights_into(doc, theme, &mut overlays); - - if is_focused { - if let Some(tabstops) = Self::tabstop_highlights(doc, theme) { - overlays.push(tabstops); - } - overlays.push(Self::doc_selection_highlights( - editor.mode(), - doc, - view, - theme, - &config.cursor_shape, - self.terminal_focused, - )); - if let Some(overlay) = Self::highlight_focused_view_elements(view, doc, theme) { - overlays.push(overlay); - } - } - - let gutter_overflow = view.gutter_offset(doc) == 0; - if !gutter_overflow { - Self::render_gutter( - editor, - doc, - view, - view.area, - theme, - is_focused & self.terminal_focused, - &mut decorations, - ); - } + 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<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_rulers(editor, doc, view, inner, surface, theme); + Self::render_text_highlights(doc, view.offset, inner, surface, theme, highlights); + Self::render_gutter(doc, view, view.area, surface, theme, is_focused, config); - let primary_cursor = doc - .selection(view.id) - .primary() - .cursor(doc.text().slice(..)); if is_focused { - decorations.add_decoration(text_decorations::Cursor { - cache: &editor.cursor_cache, - primary_cursor, - }); + Self::render_focused_view_elements(view, doc, inner, theme, surface); } - let width = view.inner_width(doc); - let config = doc.config.load(); - let enable_cursor_line = view - .diagnostics_handler - .show_cursorline_diagnostics(doc, view.id); - let inline_diagnostic_config = config.inline_diagnostics.prepare(width, enable_cursor_line); - decorations.add_decoration(InlineDiagnostics::new( - doc, - theme, - primary_cursor, - inline_diagnostic_config, - config.end_of_line_diagnostics, - )); - render_document( - surface, - inner, - doc, - view_offset, - &text_annotations, - syntax_highlighter, - overlays, - theme, - decorations, - ); // 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[(x, y)] + surface + .get_mut(x, y) .set_symbol(tui::symbols::line::VERTICAL) //.set_symbol(" ") .set_style(border_style); } } - if config.inline_diagnostics.disabled() - && config.end_of_line_diagnostics == DiagnosticFilter::Disable - { - Self::render_diagnostics(doc, view, inner, surface, theme); - } + 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 - - let mut context = - statusline::RenderContext::new(editor, doc, view, is_focused, &self.spinners); - - statusline::render(&mut context, statusline_area, surface); - } - - pub fn render_rulers( - editor: &Editor, - doc: &Document, - view: &View, - viewport: Rect, - surface: &mut Surface, - theme: &Theme, - ) { - let editor_rulers = &editor.config().rulers; - let ruler_theme = theme - .try_get("ui.virtual.ruler") - .unwrap_or_else(|| Style::default().bg(Color::Red)); - - let rulers = doc - .language_config() - .and_then(|config| config.rulers.as_ref()) - .unwrap_or(editor_rulers); - - let view_offset = doc.view_offset(view.id); - - rulers - .iter() - // View might be horizontally scrolled, convert from absolute distance - // from the 1st column to relative distance from left of viewport - .filter_map(|ruler| ruler.checked_sub(1 + view_offset.horizontal_offset as u16)) - .filter(|ruler| ruler < &viewport.width) - .map(|ruler| viewport.clip_left(ruler).with_width(1)) - .for_each(|area| surface.set_style(area, ruler_theme)) + self.render_statusline(doc, view, statusline_area, surface, theme, is_focused); } - fn viewport_byte_range( - text: helix_core::RopeSlice, - row: usize, - height: u16, - ) -> std::ops::Range<usize> { - // Calculate viewport byte ranges: - // Saturating subs to make it inclusive zero indexing. - let last_line = text.len_lines().saturating_sub(1); - let last_visible_line = (row + height as usize).saturating_sub(1).min(last_line); - let start = text.line_to_byte(row.min(last_line)); - let end = text.line_to_byte(last_visible_line + 1); - - start..end - } - - /// Get the syntax highlighter for a document in a view represented by the first line + /// 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) - pub fn doc_syntax_highlighter<'editor>( - doc: &'editor Document, - anchor: usize, + #[allow(clippy::too_many_arguments)] + pub fn doc_syntax_highlights<'doc>( + doc: &'doc Document, + offset: Position, height: u16, - loader: &'editor syntax::Loader, - ) -> Option<syntax::Highlighter<'editor>> { - let syntax = doc.syntax()?; + theme: &Theme, + loader: &syntax::Loader, + ) -> Box<dyn Iterator<Item = HighlightEvent> + 'doc> { let text = doc.text().slice(..); - let row = text.char_to_line(anchor.min(text.len_chars())); - let range = Self::viewport_byte_range(text, row, height); - let range = range.start as u32..range.end as u32; + 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 highlighter = syntax.highlighter(text, loader, range); - Some(highlighter) - } + let range = { + // calculate viewport byte ranges + let start = text.line_to_byte(offset.row); + let end = text.line_to_byte(last_line + 1); - pub fn overlay_syntax_highlights( - doc: &Document, - anchor: usize, - height: u16, - text_annotations: &TextAnnotations, - ) -> OverlayHighlights { - let text = doc.text().slice(..); - let row = text.char_to_line(anchor.min(text.len_chars())); + start..end + }; - let mut range = Self::viewport_byte_range(text, row, height); - range = text.byte_to_char(range.start)..text.byte_to_char(range.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, + }); - text_annotations.collect_overlay_highlights(range) - } - - pub fn doc_rainbow_highlights( - doc: &Document, - anchor: usize, - height: u16, - theme: &Theme, - loader: &syntax::Loader, - ) -> Option<OverlayHighlights> { - let syntax = doc.syntax()?; - let text = doc.text().slice(..); - let row = text.char_to_line(anchor.min(text.len_chars())); - let visible_range = Self::viewport_byte_range(text, row, height); - let start = syntax::child_for_byte_range( - &syntax.tree().root_node(), - visible_range.start as u32..visible_range.end as u32, - ) - .map_or(visible_range.start as u32, |node| node.start_byte()); - let range = start..visible_range.end as u32; - - Some(syntax.rainbow_highlights(text, theme.rainbow_length(), loader, range)) + Box::new(highlights) } /// Get highlight spans for document diagnostics - pub fn doc_diagnostics_highlights_into( + pub fn doc_diagnostics_highlights( doc: &Document, theme: &Theme, - overlay_highlights: &mut Vec<OverlayHighlights>, - ) { - use helix_core::diagnostic::{DiagnosticTag, Range, Severity}; - let get_scope_of = |scope| { - theme - .find_highlight_exact(scope) - // get one of the themes below as fallback values - .or_else(|| theme.find_highlight_exact("diagnostic")) - .or_else(|| theme.find_highlight_exact("ui.cursor")) - .or_else(|| theme.find_highlight_exact("ui.selection")) - .expect( - "at least one of the following scopes must be defined in the theme: `diagnostic`, `ui.cursor`, or `ui.selection`", - ) - }; - - // Diagnostic tags - let unnecessary = theme.find_highlight_exact("diagnostic.unnecessary"); - let deprecated = theme.find_highlight_exact("diagnostic.deprecated"); - - let mut default_vec = Vec::new(); - let mut info_vec = Vec::new(); - let mut hint_vec = Vec::new(); - let mut warning_vec = Vec::new(); - let mut error_vec = Vec::new(); - let mut unnecessary_vec = Vec::new(); - let mut deprecated_vec = Vec::new(); - - let push_diagnostic = |vec: &mut Vec<ops::Range<usize>>, range: Range| { - // If any diagnostic overlaps ranges with the prior diagnostic, - // merge the two together. Otherwise push a new span. - match vec.last_mut() { - Some(existing_range) if range.start <= existing_range.end => { - // This branch merges overlapping diagnostics, assuming that the current - // diagnostic starts on range.start or later. If this assertion fails, - // we will discard some part of `diagnostic`. This implies that - // `doc.diagnostics()` is not sorted by `diagnostic.range`. - debug_assert!(existing_range.start <= range.start); - existing_range.end = range.end.max(existing_range.end) - } - _ => vec.push(range.start..range.end), - } - }; - - for diagnostic in doc.diagnostics() { - // Separate diagnostics into different Vecs by severity. - let vec = match diagnostic.severity { - Some(Severity::Info) => &mut info_vec, - Some(Severity::Hint) => &mut hint_vec, - Some(Severity::Warning) => &mut warning_vec, - Some(Severity::Error) => &mut error_vec, - _ => &mut default_vec, - }; + ) -> 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( + "at least one of the following scopes must be defined in the theme: `diagnostic`, `ui.cursor`, or `ui.selection`", + ); - // If the diagnostic has tags and a non-warning/error severity, skip rendering - // the diagnostic as info/hint/default and only render it as unnecessary/deprecated - // instead. For warning/error diagnostics, render both the severity highlight and - // the tag highlight. - if diagnostic.tags.is_empty() - || matches!( - diagnostic.severity, - Some(Severity::Warning | Severity::Error) + doc.diagnostics() + .iter() + .map(|diagnostic| { + ( + diagnostic_scope, + diagnostic.range.start..diagnostic.range.end, ) - { - push_diagnostic(vec, diagnostic.range); - } - - for tag in &diagnostic.tags { - match tag { - DiagnosticTag::Unnecessary => { - if unnecessary.is_some() { - push_diagnostic(&mut unnecessary_vec, diagnostic.range) - } - } - DiagnosticTag::Deprecated => { - if deprecated.is_some() { - push_diagnostic(&mut deprecated_vec, diagnostic.range) - } - } - } - } - } - - overlay_highlights.push(OverlayHighlights::Homogeneous { - highlight: get_scope_of("diagnostic"), - ranges: default_vec, - }); - if let Some(highlight) = unnecessary { - overlay_highlights.push(OverlayHighlights::Homogeneous { - highlight, - ranges: unnecessary_vec, - }); - } - if let Some(highlight) = deprecated { - overlay_highlights.push(OverlayHighlights::Homogeneous { - highlight, - ranges: deprecated_vec, - }); - } - overlay_highlights.extend([ - OverlayHighlights::Homogeneous { - highlight: get_scope_of("diagnostic.info"), - ranges: info_vec, - }, - OverlayHighlights::Homogeneous { - highlight: get_scope_of("diagnostic.hint"), - ranges: hint_vec, - }, - OverlayHighlights::Homogeneous { - highlight: get_scope_of("diagnostic.warning"), - ranges: warning_vec, - }, - OverlayHighlights::Homogeneous { - highlight: get_scope_of("diagnostic.error"), - ranges: error_vec, - }, - ]); + }) + .collect() } /// Get highlight spans for selections in a document view. pub fn doc_selection_highlights( - mode: Mode, doc: &Document, view: &View, theme: &Theme, - cursor_shape_config: &CursorShapeConfig, - is_terminal_focused: bool, - ) -> OverlayHighlights { + ) -> Vec<(usize, std::ops::Range<usize>)> { let text = doc.text().slice(..); let selection = doc.selection(view.id); let primary_idx = selection.primary_index(); - let cursorkind = cursor_shape_config.from_mode(mode); - let cursor_is_block = cursorkind == CursorKind::Block; - let selection_scope = theme - .find_highlight_exact("ui.selection") + .find_scope_index("ui.selection") .expect("could not find `ui.selection` scope in the theme!"); - let primary_selection_scope = theme - .find_highlight_exact("ui.selection.primary") - .unwrap_or(selection_scope); - let base_cursor_scope = theme - .find_highlight_exact("ui.cursor") + .find_scope_index("ui.cursor") .unwrap_or(selection_scope); - let base_primary_cursor_scope = theme - .find_highlight("ui.cursor.primary") - .unwrap_or(base_cursor_scope); - - let cursor_scope = match mode { - Mode::Insert => theme.find_highlight_exact("ui.cursor.insert"), - Mode::Select => theme.find_highlight_exact("ui.cursor.select"), - Mode::Normal => theme.find_highlight_exact("ui.cursor.normal"), + + 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 = match mode { - Mode::Insert => theme.find_highlight_exact("ui.cursor.primary.insert"), - Mode::Select => theme.find_highlight_exact("ui.cursor.primary.select"), - Mode::Normal => theme.find_highlight_exact("ui.cursor.primary.normal"), - } - .unwrap_or(base_primary_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::new(); + let mut spans: Vec<(usize, std::ops::Range<usize>)> = Vec::new(); for (i, range) in selection.iter().enumerate() { - let selection_is_primary = i == primary_idx; - let (cursor_scope, selection_scope) = if selection_is_primary { + let (cursor_scope, selection_scope) = if i == primary_idx { (primary_cursor_scope, primary_selection_scope) } else { (cursor_scope, selection_scope) @@ -514,14 +250,7 @@ impl EditorView { // Special-case: cursor at end of the rope. if range.head == range.anchor && range.head == text.len_chars() { - if !selection_is_primary || (cursor_is_block && is_terminal_focused) { - // Bar and underline cursors are drawn by the terminal - // BUG: If the editor area loses focus while having a bar or - // underline cursor (eg. when a regex prompt has focus) then - // the primary cursor will be invisible. This doesn't happen - // with block cursors since we manually draw *all* cursors. - spans.push((cursor_scope, range.head..range.head + 1)); - } + spans.push((cursor_scope, range.head..range.head + 1)); continue; } @@ -529,187 +258,235 @@ impl EditorView { if range.head > range.anchor { // Standard case. let cursor_start = prev_grapheme_boundary(text, range.head); - // non block cursors look like they exclude the cursor - let selection_end = - if selection_is_primary && !cursor_is_block && mode != Mode::Insert { - range.head - } else { - cursor_start - }; - spans.push((selection_scope, range.anchor..selection_end)); - // add block cursors - // skip primary cursor if terminal is unfocused - terminal cursor is used in that case - if !selection_is_primary || (cursor_is_block && is_terminal_focused) { - spans.push((cursor_scope, cursor_start..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); - // add block cursors - // skip primary cursor if terminal is unfocused - terminal cursor is used in that case - if !selection_is_primary || (cursor_is_block && is_terminal_focused) { - spans.push((cursor_scope, range.head..cursor_end)); - } - // non block cursors look like they exclude the cursor - let selection_start = if selection_is_primary - && !cursor_is_block - && !(mode == Mode::Insert && cursor_end == range.anchor) - { - range.head - } else { - cursor_end - }; - spans.push((selection_scope, selection_start..range.anchor)); + spans.push((cursor_scope, range.head..cursor_end)); + spans.push((selection_scope, cursor_end..range.anchor)); } } - OverlayHighlights::Heterogenous { highlights: spans } + spans } - /// Render brace match, etc (meant for the focused view only) - pub fn highlight_focused_view_elements( - view: &View, + pub fn render_text_highlights<H: Iterator<Item = HighlightEvent>>( doc: &Document, + offset: Position, + viewport: Rect, + surface: &mut Surface, theme: &Theme, - ) -> Option<OverlayHighlights> { - // Highlight matching braces - let syntax = doc.syntax()?; - let highlight = theme.find_highlight_exact("ui.cursor.match")?; + highlights: H, + ) { let text = doc.text().slice(..); - let pos = doc.selection(view.id).primary().cursor(text); - let pos = helix_core::match_brackets::find_matching_bracket(syntax, text, pos)?; - Some(OverlayHighlights::single(highlight, pos..pos + 1)) - } - pub fn tabstop_highlights(doc: &Document, theme: &Theme) -> Option<OverlayHighlights> { - let snippet = doc.active_snippet.as_ref()?; - let highlight = theme.find_highlight_exact("tabstop")?; - let mut ranges = Vec::new(); - for tabstop in snippet.tabstops() { - ranges.extend(tabstop.ranges.iter().map(|range| range.start..range.end)); - } - Some(OverlayHighlights::Homogeneous { highlight, ranges }) - } + 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); - /// Render bufferline at the top - pub fn render_bufferline(editor: &Editor, viewport: Rect, surface: &mut Surface) { - let scratch = PathBuf::from(SCRATCH_BUFFER_NAME); // default filename to use for scratch buffer - surface.clear_with( - viewport, - editor - .theme - .try_get("ui.bufferline.background") - .unwrap_or_else(|| editor.theme.get("ui.statusline")), - ); + let text_style = theme.get("ui.text"); - let bufferline_active = editor - .theme - .try_get("ui.bufferline.active") - .unwrap_or_else(|| editor.theme.get("ui.statusline.active")); - - let bufferline_inactive = editor - .theme - .try_get("ui.bufferline") - .unwrap_or_else(|| editor.theme.get("ui.statusline.inactive")); - - let mut x = viewport.x; - let current_doc = view!(editor).doc; - - for doc in editor.documents() { - let fname = doc - .path() - .unwrap_or(&scratch) - .file_name() - .unwrap_or_default() - .to_str() - .unwrap_or_default(); - - let style = if current_doc == doc.id() { - bufferline_active - } else { - bufferline_inactive - }; + '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; - let text = format!(" {}{} ", fname, if doc.is_modified() { "[+]" } else { "" }); - let used_width = viewport.x.saturating_sub(x); - let rem_width = surface.area.width.saturating_sub(used_width); + // 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) + }; - x = surface - .set_stringn(x, viewport.y, text, rem_width as usize, style) - .0; + 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, + ); + } - if x >= surface.area.right() { - break; + 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(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); + } } } } - pub fn render_gutter<'d>( - editor: &'d Editor, - doc: &'d Document, + #[allow(clippy::too_many_arguments)] + pub fn render_gutter( + doc: &Document, view: &View, viewport: Rect, + surface: &mut Surface, theme: &Theme, is_focused: bool, - decoration_manager: &mut DecorationManager<'d>, + config: &helix_view::editor::Config, ) { let text = doc.text().slice(..); - let cursors: Rc<[_]> = doc + let last_line = view.last_line(doc); + + let linenr = theme.get("ui.linenr"); + let linenr_select: Style = theme.try_get("ui.linenr.selected").unwrap_or(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(); + + let current_line = doc + .text() + .char_to_line(doc.selection(view.id).primary().cursor(text)); + + // 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(); - let mut offset = 0; - - let gutter_style = theme.get("ui.gutter"); - let gutter_selected_style = theme.get("ui.gutter.selected"); - let gutter_style_virtual = theme.get("ui.gutter.virtual"); - let gutter_selected_style_virtual = theme.get("ui.gutter.selected.virtual"); - - for gutter_type in view.gutters() { - let mut gutter = gutter_type.style(editor, doc, view, theme, is_focused); - let width = gutter_type.width(view, doc); - // avoid lots of small allocations by reusing a text buffer for each line - let mut text = String::with_capacity(width); - let cursors = cursors.clone(); - let gutter_decoration = move |renderer: &mut TextRenderer, pos: LinePos| { - // TODO handle softwrap in gutters - let selected = cursors.contains(&pos.doc_line); - let x = viewport.x + offset; - let y = pos.visual_line; - - let gutter_style = match (selected, pos.first_visual_line) { - (false, true) => gutter_style, - (true, true) => gutter_selected_style, - (false, false) => gutter_style_virtual, - (true, false) => gutter_selected_style_virtual, - }; + for (i, line) in (view.offset.row..(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, + 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, + }, + ); + } - if let Some(style) = - gutter(pos.doc_line, selected, pos.first_visual_line, &mut text) - { - renderer.set_stringn(x, y, &text, width, gutter_style.patch(style)); - } else { - renderer.set_style( - Rect { - x, - y, - width: width as u16, - height: 1, - }, - gutter_style, - ); - } - text.clear(); - }; - decoration_manager.add_decoration(gutter_decoration); + let selected = cursors.contains(&line); - offset += width as u16; + let text = if line == last_line && !draw_last { + " ~".into() + } else { + let line = match config.line_number { + LineNumber::Absolute => line + 1, + LineNumber::Relative => { + if current_line == line { + line + 1 + } else { + abs_diff(current_line, line) + } + } + }; + format!("{:>5}", line) + }; + surface.set_stringn( + viewport.x + 1, + viewport.y + i as u16, + text, + 5, + if selected && is_focused { + linenr_select + } else { + linenr + }, + ); } } pub fn render_diagnostics( + &self, doc: &Document, view: &View, viewport: Rect, @@ -720,7 +497,7 @@ impl EditorView { use tui::{ layout::Alignment, text::Text, - widgets::{Paragraph, Widget, Wrap}, + widgets::{Paragraph, Widget}, }; let cursor = doc @@ -737,34 +514,23 @@ impl EditorView { let info = theme.get("info"); let hint = theme.get("hint"); + // Vec::with_capacity(diagnostics.len()); // rough estimate let mut lines = Vec::new(); - let background_style = theme.get("ui.background"); for diagnostic in diagnostics { - let style = Style::reset() - .patch(background_style) - .patch(match diagnostic.severity { + let text = Text::styled( + &diagnostic.message, + match diagnostic.severity { Some(Severity::Error) => error, Some(Severity::Warning) | None => warning, Some(Severity::Info) => info, Some(Severity::Hint) => hint, - }); - let text = Text::styled(&diagnostic.message, style); + }, + ); lines.extend(text.lines); - let code = diagnostic.code.as_ref().map(|x| match x { - NumberOrString::Number(n) => format!("({n})"), - NumberOrString::String(s) => format!("({s})"), - }); - if let Some(code) = code { - let span = Span::styled(code, style); - lines.push(span.into()); - } } - let text = Text::from(lines); - let paragraph = Paragraph::new(&text) - .alignment(Alignment::Right) - .wrap(Wrap { trim: true }); - let width = 100.min(viewport.width); + let paragraph = Paragraph::new(lines).alignment(Alignment::Right); + let width = 80.min(viewport.width); let height = 15.min(viewport.height); paragraph.render( Rect::new(viewport.right() - width, viewport.y + 1, width, height), @@ -772,95 +538,119 @@ impl EditorView { ); } - /// Apply the highlighting on the lines where a cursor is active - pub fn cursorline(doc: &Document, view: &View, theme: &Theme) -> impl Decoration { - let text = doc.text().slice(..); - // TODO only highlight the visual line that contains the cursor instead of the full visual line - let primary_line = doc.selection(view.id).primary().cursor_line(text); - - // The secondary_lines do contain the primary_line, it doesn't matter - // as the else-if clause in the loop later won't test for the - // secondary_lines if primary_line == line. - // It's used inside a loop so the collect isn't needless: - // https://github.com/rust-lang/rust-clippy/issues/6164 - #[allow(clippy::needless_collect)] - let secondary_lines: Vec<_> = doc - .selection(view.id) - .iter() - .map(|range| range.cursor_line(text)) - .collect(); - - let primary_style = theme.get("ui.cursorline.primary"); - let secondary_style = theme.get("ui.cursorline.secondary"); - let viewport = view.area; - - move |renderer: &mut TextRenderer, pos: LinePos| { - let area = Rect::new(viewport.x, pos.visual_line, viewport.width, 1); - if primary_line == pos.doc_line { - renderer.set_style(area, primary_style); - } else if secondary_lines.binary_search(&pos.doc_line).is_ok() { - renderer.set_style(area, secondary_style); - } - } - } - - /// Apply the highlighting on the columns where a cursor is active - pub fn highlight_cursorcolumn( + #[allow(clippy::too_many_arguments)] + pub fn render_statusline( + &self, doc: &Document, view: &View, + viewport: Rect, surface: &mut Surface, theme: &Theme, - viewport: Rect, - text_annotations: &TextAnnotations, + is_focused: bool, ) { - let text = doc.text().slice(..); + //------------------------------- + // Left side of the status line. + //------------------------------- + + let mode = match doc.mode() { + Mode::Insert => "INS", + Mode::Select => "SEL", + Mode::Normal => "NOR", + }; + let progress = doc + .language_server() + .and_then(|srv| { + self.spinners + .get(srv.id()) + .and_then(|spinner| spinner.frame()) + }) + .unwrap_or(""); - // Manual fallback behaviour: - // ui.cursorcolumn.{p/s} -> ui.cursorcolumn -> ui.cursorline.{p/s} - let primary_style = theme - .try_get_exact("ui.cursorcolumn.primary") - .or_else(|| theme.try_get_exact("ui.cursorcolumn")) - .unwrap_or_else(|| theme.get("ui.cursorline.primary")); - let secondary_style = theme - .try_get_exact("ui.cursorcolumn.secondary") - .or_else(|| theme.try_get_exact("ui.cursorcolumn")) - .unwrap_or_else(|| theme.get("ui.cursorline.secondary")); + let style = if is_focused { + theme.get("ui.statusline") + } else { + theme.get("ui.statusline.inactive") + }; + // statusline + surface.set_style(viewport.with_height(1), style); + if is_focused { + surface.set_string(viewport.x + 1, viewport.y, mode, style); + + // TODO: put this in a better place and possibly cache + let _ = crossterm::execute!( + std::io::stdout(), + crossterm::terminal::SetTitle(format!( + "{} - Helix", + doc.relative_path() + .as_deref() + .unwrap_or(std::path::Path::new("[scratch]")) + .to_str() + .unwrap() + )) + ); + } + surface.set_string(viewport.x + 5, viewport.y, progress, style); - let inner_area = view.inner_area(doc); + if let Some(path) = doc.relative_path() { + let path = path.to_string_lossy(); - let selection = doc.selection(view.id); - let view_offset = doc.view_offset(view.id); - let primary = selection.primary(); - let text_format = doc.text_format(viewport.width, None); - for range in selection.iter() { - let is_primary = primary == *range; - let cursor = range.cursor(text); - - let Position { col, .. } = - visual_offset_from_block(text, cursor, cursor, &text_format, text_annotations).0; - - // if the cursor is horizontally in the view - if col >= view_offset.horizontal_offset - && inner_area.width > (col - view_offset.horizontal_offset) as u16 - { - let area = Rect::new( - inner_area.x + (col - view_offset.horizontal_offset) as u16, - view.area.y, - 1, - view.area.height, - ); - if is_primary { - surface.set_style(area, primary_style) - } else { - surface.set_style(area, secondary_style) - } - } + let title = format!("{}{}", path, if doc.is_modified() { "[+]" } else { "" }); + surface.set_stringn( + viewport.x + 8, + viewport.y, + title, + viewport.width.saturating_sub(6) as usize, + style, + ); } + + //------------------------------- + // Right side of the status line. + //------------------------------- + + // Compute the individual info strings. + let diag_count = format!("{}", doc.diagnostics().len()); + // let indent_info = match doc.indent_style { + // IndentStyle::Tabs => "tabs", + // IndentStyle::Spaces(1) => "spaces:1", + // IndentStyle::Spaces(2) => "spaces:2", + // IndentStyle::Spaces(3) => "spaces:3", + // IndentStyle::Spaces(4) => "spaces:4", + // IndentStyle::Spaces(5) => "spaces:5", + // IndentStyle::Spaces(6) => "spaces:6", + // IndentStyle::Spaces(7) => "spaces:7", + // IndentStyle::Spaces(8) => "spaces:8", + // _ => "indent:ERROR", + // }; + let position_info = { + let pos = coords_at_pos( + doc.text().slice(..), + doc.selection(view.id) + .primary() + .cursor(doc.text().slice(..)), + ); + format!("{}:{}", pos.row + 1, pos.col + 1) // convert to 1-indexing + }; + + // Render them to the status line together. + let right_side_text = format!( + "{} {} ", + &diag_count[..diag_count.len().min(4)], + // indent_info, + position_info + ); + let text_len = right_side_text.len() as u16; + surface.set_string( + viewport.x + viewport.width.saturating_sub(text_len), + viewport.y, + right_side_text, + style, + ); } /// Handle events by looking them up in `self.keymaps`. Returns None /// if event was handled (a command was executed or a subkeymap was - /// activated). Only KeymapResult::{NotFound, Cancelled} is returned + /// activated). Only KeymapResultKind::{NotFound, Cancelled} is returned /// otherwise. fn handle_keymap_event( &mut self, @@ -868,68 +658,33 @@ impl EditorView { cxt: &mut commands::Context, event: KeyEvent, ) -> Option<KeymapResult> { - let mut last_mode = mode; - self.pseudo_pending.extend(self.keymaps.pending()); - let key_result = self.keymaps.get(mode, event); - cxt.editor.autoinfo = self.keymaps.sticky().map(|node| node.infobox()); - - let mut execute_command = |command: &commands::MappableCommand| { - command.execute(cxt); - helix_event::dispatch(PostCommand { command, cx: cxt }); - - let current_mode = cxt.editor.mode(); - if current_mode != last_mode { - helix_event::dispatch(OnModeSwitch { - old_mode: last_mode, - new_mode: current_mode, - cx: cxt, - }); - - // HAXX: if we just entered insert mode from normal, clear key buf - // and record the command that got us into this mode. - if current_mode == Mode::Insert { - // how we entered insert mode is important, and we should track that so - // we can repeat the side effect. - self.last_insert.0 = command.clone(); - self.last_insert.1.clear(); - } - } - - last_mode = current_mode; - }; - - match &key_result { - KeymapResult::Matched(command) => { - execute_command(command); - } - KeymapResult::Pending(node) => cxt.editor.autoinfo = Some(node.infobox()), - KeymapResult::MatchedSequence(commands) => { - for command in commands { - execute_command(command); - } - } - KeymapResult::NotFound | KeymapResult::Cancelled(_) => return Some(key_result), + self.autoinfo = None; + let key_result = self.keymaps.get_mut(&mode).unwrap().get(event); + self.autoinfo = key_result.sticky.map(|node| node.infobox()); + + match &key_result.kind { + KeymapResultKind::Matched(command) => command.execute(cxt), + KeymapResultKind::Pending(node) => self.autoinfo = Some(node.infobox()), + KeymapResultKind::NotFound | KeymapResultKind::Cancelled(_) => return Some(key_result), } None } fn insert_mode(&mut self, cx: &mut commands::Context, event: KeyEvent) { if let Some(keyresult) = self.handle_keymap_event(Mode::Insert, cx, event) { - match keyresult { - KeymapResult::NotFound => { - if !self.on_next_key(OnKeyCallbackKind::Fallback, cx, event) { - if let Some(ch) = event.char() { - commands::insert::insert_char(cx, ch) - } + match keyresult.kind { + KeymapResultKind::NotFound => { + if let Some(ch) = event.char() { + commands::insert::insert_char(cx, ch) } } - KeymapResult::Cancelled(pending) => { + KeymapResultKind::Cancelled(pending) => { for ev in pending { match ev.char() { Some(ch) => commands::insert::insert_char(cx, ch), None => { - if let KeymapResult::Matched(command) = - self.keymaps.get(Mode::Insert, ev) + if let KeymapResultKind::Matched(command) = + self.keymaps.get_mut(&Mode::Insert).unwrap().get(ev).kind { command.execute(cx); } @@ -943,68 +698,21 @@ impl EditorView { } fn command_mode(&mut self, mode: Mode, cxt: &mut commands::Context, event: KeyEvent) { - match (event, cxt.editor.count) { - // If the count is already started and the input is a number, always continue the count. - (key!(i @ '0'..='9'), Some(count)) => { - let i = i.to_digit(10).unwrap() as usize; - let count = count.get() * 10 + i; - if count > 100_000_000 { - return; - } - cxt.editor.count = NonZeroUsize::new(count); - } - // A non-zero digit will start the count if that number isn't used by a keymap. - (key!(i @ '1'..='9'), None) if !self.keymaps.contains_key(mode, event) => { + match event { + // count handling + key!(i @ '0'..='9') => { let i = i.to_digit(10).unwrap() as usize; - cxt.editor.count = NonZeroUsize::new(i); + cxt.editor.count = + std::num::NonZeroUsize::new(cxt.editor.count.map_or(i, |c| c.get() * 10 + i)); } // special handling for repeat operator - (key!('.'), _) if self.keymaps.pending().is_empty() => { - for _ in 0..cxt.editor.count.map_or(1, NonZeroUsize::into) { - // first execute whatever put us into insert mode - self.last_insert.0.execute(cxt); - let mut last_savepoint = None; - let mut last_request_savepoint = None; - // then replay the inputs - for key in self.last_insert.1.clone() { - match key { - InsertEvent::Key(key) => self.insert_mode(cxt, key), - InsertEvent::CompletionApply { - trigger_offset, - changes, - } => { - let (view, doc) = current!(cxt.editor); - - if let Some(last_savepoint) = last_savepoint.as_deref() { - doc.restore(view, last_savepoint, true); - } - - let text = doc.text().slice(..); - let cursor = doc.selection(view.id).primary().cursor(text); - - let shift_position = |pos: usize| -> usize { - (pos + cursor).saturating_sub(trigger_offset) - }; - - let tx = Transaction::change( - doc.text(), - changes.iter().cloned().map(|(start, end, t)| { - (shift_position(start), shift_position(end), t) - }), - ); - doc.apply(&tx, view.id); - } - InsertEvent::TriggerCompletion => { - last_savepoint = take(&mut last_request_savepoint); - } - InsertEvent::RequestCompletion => { - let (view, doc) = current!(cxt.editor); - last_request_savepoint = Some(doc.savepoint(view)); - } - } - } + key!('.') => { + // first execute whatever put us into insert mode + self.last_insert.0.execute(cxt); + // then replay the inputs + for &key in &self.last_insert.1.clone() { + self.insert_mode(cxt, key) } - cxt.editor.count = None; } _ => { // set the count @@ -1016,219 +724,102 @@ impl EditorView { // set the register cxt.register = cxt.editor.selected_register.take(); - let res = self.handle_keymap_event(mode, cxt, event); - if matches!(&res, Some(KeymapResult::NotFound)) { - self.on_next_key(OnKeyCallbackKind::Fallback, cxt, event); - } + self.handle_keymap_event(mode, cxt, event); if self.keymaps.pending().is_empty() { cxt.editor.count = None - } else { - cxt.editor.selected_register = cxt.register.take(); } } } } - #[allow(clippy::too_many_arguments)] pub fn set_completion( &mut self, - editor: &mut Editor, - items: Vec<CompletionItem>, + editor: &Editor, + items: Vec<helix_lsp::lsp::CompletionItem>, + offset_encoding: helix_lsp::OffsetEncoding, + start_offset: usize, trigger_offset: usize, size: Rect, - ) -> Option<Rect> { - let mut completion = Completion::new(editor, items, trigger_offset); + ) { + let mut completion = + Completion::new(editor, items, offset_encoding, start_offset, trigger_offset); if completion.is_empty() { // skip if we got no completion results - return None; + return; } - let area = completion.area(size, editor); - editor.last_completion = Some(CompleteAction::Triggered); - self.last_insert.1.push(InsertEvent::TriggerCompletion); - // TODO : propagate required size on resize to completion too + completion.required_size((size.width, size.height)); self.completion = Some(completion); - Some(area) - } - - pub fn clear_completion(&mut self, editor: &mut Editor) -> Option<OnKeyCallback> { - self.completion = None; - let mut on_next_key: Option<OnKeyCallback> = None; - editor.handlers.completions.request_controller.restart(); - editor.handlers.completions.active_completions.clear(); - if let Some(last_completion) = editor.last_completion.take() { - match last_completion { - CompleteAction::Triggered => (), - CompleteAction::Applied { - trigger_offset, - changes, - placeholder, - } => { - self.last_insert.1.push(InsertEvent::CompletionApply { - trigger_offset, - changes, - }); - on_next_key = placeholder.then_some(Box::new(|cx, key| { - if let Some(c) = key.char() { - let (view, doc) = current!(cx.editor); - if let Some(snippet) = &doc.active_snippet { - doc.apply(&snippet.delete_placeholder(doc.text()), view.id); - } - commands::insert::insert_char(cx, c); - } - })) - } - CompleteAction::Selected { savepoint } => { - let (view, doc) = current!(editor); - doc.restore(view, &savepoint, false); - } - } - } - on_next_key - } - - pub fn handle_idle_timeout(&mut self, cx: &mut commands::Context) -> EventResult { - commands::compute_inlay_hints_for_all_views(cx.editor, cx.jobs); - - EventResult::Ignored(None) } } impl EditorView { - /// must be called whenever the editor processed input that - /// is not a `KeyEvent`. In these cases any pending keys/on next - /// key callbacks must be canceled. - fn handle_non_key_input(&mut self, cxt: &mut commands::Context) { - cxt.editor.status_msg = None; - cxt.editor.reset_idle_timer(); - // HACKS: create a fake key event that will never trigger any actual map - // and therefore simply acts as "dismiss" - let null_key_event = KeyEvent { - code: KeyCode::Null, - modifiers: KeyModifiers::empty(), - }; - // dismiss any pending keys - if let Some((on_next_key, _)) = self.on_next_key.take() { - on_next_key(cxt, null_key_event); - } - self.handle_keymap_event(cxt.editor.mode, cxt, null_key_event); - self.pseudo_pending.clear(); - } - fn handle_mouse_event( &mut self, - event: &MouseEvent, + event: MouseEvent, cxt: &mut commands::Context, ) -> EventResult { - if event.kind != MouseEventKind::Moved { - self.handle_non_key_input(cxt) - } - - let config = cxt.editor.config(); - let MouseEvent { - kind, - row, - column, - modifiers, - .. - } = *event; - - let pos_and_view = |editor: &Editor, row, column, ignore_virtual_text| { - editor.tree.views().find_map(|(view, _focus)| { - view.pos_at_screen_coords( - &editor.documents[&view.doc], - row, - column, - ignore_virtual_text, - ) - .map(|pos| (pos, view.id)) - }) - }; - - let gutter_coords_and_view = |editor: &Editor, row, column| { - editor.tree.views().find_map(|(view, _focus)| { - view.gutter_coords_at_screen_coords(row, column) - .map(|coords| (coords, view.id)) - }) - }; - - match kind { - MouseEventKind::Down(MouseButton::Left) => { + match event { + MouseEvent { + kind: MouseEventKind::Down(MouseButton::Left), + row, + column, + modifiers, + .. + } => { let editor = &mut cxt.editor; - if let Some((pos, view_id)) = pos_and_view(editor, row, column, true) { - editor.focus(view_id); + let result = editor.tree.views().find_map(|(view, _focus)| { + view.pos_at_screen_coords(&editor.documents[view.doc], row, column) + .map(|pos| (pos, view.id)) + }); - let prev_view_id = view!(editor).id; - let doc = doc_mut!(editor, &view!(editor, view_id).doc); + if let Some((pos, view_id)) = result { + let doc = &mut editor.documents[editor.tree.get(view_id).doc]; - if modifiers == KeyModifiers::ALT { + if modifiers == crossterm::event::KeyModifiers::ALT { let selection = doc.selection(view_id).clone(); doc.set_selection(view_id, selection.push(Range::point(pos))); - } else if editor.mode == Mode::Select { - // Discards non-primary selections for consistent UX with normal mode - let primary = doc.selection(view_id).primary().put_cursor( - doc.text().slice(..), - pos, - true, - ); - editor.mouse_down_range = Some(primary); - doc.set_selection(view_id, Selection::single(primary.anchor, primary.head)); } else { doc.set_selection(view_id, Selection::point(pos)); } - if view_id != prev_view_id { - self.clear_completion(editor); - } - - editor.ensure_cursor_in_view(view_id); + editor.tree.focus = view_id; return EventResult::Consumed(None); } - if let Some((coords, view_id)) = gutter_coords_and_view(editor, row, column) { - editor.focus(view_id); - - let (view, doc) = current!(cxt.editor); - - let path = match doc.path() { - Some(path) => path.clone(), - None => return EventResult::Ignored(None), - }; - - if let Some(char_idx) = - view.pos_at_visual_coords(doc, coords.row as u16, coords.col as u16, true) - { - let line = doc.text().char_to_line(char_idx); - commands::dap_toggle_breakpoint_impl(cxt, path, line); - return EventResult::Consumed(None); - } - } - - EventResult::Ignored(None) + EventResult::Ignored } - MouseEventKind::Drag(MouseButton::Left) => { + MouseEvent { + kind: MouseEventKind::Drag(MouseButton::Left), + row, + column, + .. + } => { let (view, doc) = current!(cxt.editor); - let pos = match view.pos_at_screen_coords(doc, row, column, true) { + let pos = match view.pos_at_screen_coords(doc, row, column) { Some(pos) => pos, - None => return EventResult::Ignored(None), + None => return EventResult::Ignored, }; let mut selection = doc.selection(view.id).clone(); let primary = selection.primary_mut(); - *primary = primary.put_cursor(doc.text().slice(..), pos, true); + *primary = Range::new(primary.anchor, pos); doc.set_selection(view.id, selection); - let view_id = view.id; - cxt.editor.ensure_cursor_in_view(view_id); EventResult::Consumed(None) } - MouseEventKind::ScrollUp | MouseEventKind::ScrollDown => { + MouseEvent { + kind: MouseEventKind::ScrollUp | MouseEventKind::ScrollDown, + row, + column, + .. + } => { let current_view = cxt.editor.tree.focus; let direction = match event.kind { @@ -1237,327 +828,227 @@ impl EditorView { _ => unreachable!(), }; - match pos_and_view(cxt.editor, row, column, false) { - Some((_, view_id)) => cxt.editor.tree.focus = view_id, - None => return EventResult::Ignored(None), + let result = cxt.editor.tree.views().find_map(|(view, _focus)| { + view.pos_at_screen_coords(&cxt.editor.documents[view.doc], row, column) + .map(|_| view.id) + }); + + match result { + Some(view_id) => cxt.editor.tree.focus = view_id, + None => return EventResult::Ignored, } - let offset = config.scroll_lines.unsigned_abs(); - commands::scroll(cxt, offset, direction, false); + let offset = cxt.editor.config.scroll_lines.abs() as usize; + commands::scroll(cxt, offset, direction); cxt.editor.tree.focus = current_view; - cxt.editor.ensure_cursor_in_view(current_view); EventResult::Consumed(None) } - MouseEventKind::Up(MouseButton::Left) => { - if !config.middle_click_paste { - return EventResult::Ignored(None); + MouseEvent { + kind: MouseEventKind::Up(MouseButton::Left), + .. + } => { + if !cxt.editor.config.middle_click_paste { + return EventResult::Ignored; } let (view, doc) = current!(cxt.editor); + let range = doc.selection(view.id).primary(); - let should_yank = match cxt.editor.mouse_down_range.take() { - Some(down_range) => doc.selection(view.id).primary() != down_range, - None => { - // This should not happen under normal cases. We fall back to the original - // behavior of yanking on non-single-char selections. - doc.selection(view.id) - .primary() - .slice(doc.text().slice(..)) - .len_chars() - > 1 - } - }; - - if should_yank { - commands::MappableCommand::yank_main_selection_to_primary_clipboard - .execute(cxt); - EventResult::Consumed(None) - } else { - EventResult::Ignored(None) + if range.to() - range.from() <= 1 { + return EventResult::Ignored; } - } - - MouseEventKind::Up(MouseButton::Right) => { - if let Some((pos, view_id)) = gutter_coords_and_view(cxt.editor, row, column) { - cxt.editor.focus(view_id); - if let Some((pos, _)) = pos_and_view(cxt.editor, row, column, true) { - doc_mut!(cxt.editor).set_selection(view_id, Selection::point(pos)); - } else { - let (view, doc) = current!(cxt.editor); + commands::Command::yank_main_selection_to_primary_clipboard.execute(cxt); - if let Some(pos) = view.pos_at_visual_coords(doc, pos.row as u16, 0, true) { - doc.set_selection(view_id, Selection::point(pos)); - match modifiers { - KeyModifiers::ALT => { - commands::MappableCommand::dap_edit_log.execute(cxt) - } - _ => commands::MappableCommand::dap_edit_condition.execute(cxt), - }; - } - } - - cxt.editor.ensure_cursor_in_view(view_id); - return EventResult::Consumed(None); - } - EventResult::Ignored(None) + EventResult::Consumed(None) } - MouseEventKind::Up(MouseButton::Middle) => { + MouseEvent { + kind: MouseEventKind::Up(MouseButton::Middle), + row, + column, + modifiers, + .. + } => { let editor = &mut cxt.editor; - if !config.middle_click_paste { - return EventResult::Ignored(None); + if !editor.config.middle_click_paste { + return EventResult::Ignored; } - if modifiers == KeyModifiers::ALT { - commands::MappableCommand::replace_selections_with_primary_clipboard - .execute(cxt); + if modifiers == crossterm::event::KeyModifiers::ALT { + commands::Command::replace_selections_with_primary_clipboard.execute(cxt); return EventResult::Consumed(None); } - if let Some((pos, view_id)) = pos_and_view(editor, row, column, true) { - let doc = doc_mut!(editor, &view!(editor, view_id).doc); - doc.set_selection(view_id, Selection::point(pos)); - cxt.editor.focus(view_id); - commands::MappableCommand::paste_primary_clipboard_before.execute(cxt); + let result = editor.tree.views().find_map(|(view, _focus)| { + view.pos_at_screen_coords(&editor.documents[view.doc], row, column) + .map(|pos| (pos, view.id)) + }); + if let Some((pos, view_id)) = result { + let doc = &mut editor.documents[editor.tree.get(view_id).doc]; + doc.set_selection(view_id, Selection::point(pos)); + editor.tree.focus = view_id; + commands::Command::paste_primary_clipboard_before.execute(cxt); return EventResult::Consumed(None); } - EventResult::Ignored(None) + EventResult::Ignored } - _ => EventResult::Ignored(None), - } - } - fn on_next_key( - &mut self, - kind: OnKeyCallbackKind, - ctx: &mut commands::Context, - event: KeyEvent, - ) -> bool { - if let Some((on_next_key, kind_)) = self.on_next_key.take() { - if kind == kind_ { - on_next_key(ctx, event); - true - } else { - self.on_next_key = Some((on_next_key, kind_)); - false - } - } else { - false + _ => EventResult::Ignored, } } } impl Component for EditorView { - fn handle_event( - &mut self, - event: &Event, - context: &mut crate::compositor::Context, - ) -> EventResult { - let mut cx = commands::Context { - editor: context.editor, + fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult { + let mut cxt = commands::Context { + editor: &mut cx.editor, count: None, register: None, - callback: Vec::new(), + callback: None, on_next_key_callback: None, - jobs: context.jobs, + jobs: cx.jobs, }; match event { - Event::Paste(contents) => { - self.handle_non_key_input(&mut cx); - cx.count = cx.editor.count; - commands::paste_bracketed_value(&mut cx, contents.clone()); - cx.editor.count = None; - - let config = cx.editor.config(); - let mode = cx.editor.mode(); - let (view, doc) = current!(cx.editor); - view.ensure_cursor_in_view(doc, config.scrolloff); - - // Store a history state if not in insert mode. Otherwise wait till we exit insert - // to include any edits to the paste in the history state. - if mode != Mode::Insert { - doc.append_changes_to_history(view); - } - - EventResult::Consumed(None) - } Event::Resize(_width, _height) => { // Ignore this event, we handle resizing just before rendering to screen. // Handling it here but not re-rendering will cause flashing EventResult::Consumed(None) } - Event::Key(mut key) => { - cx.editor.reset_idle_timer(); + Event::Key(key) => { + cxt.editor.reset_idle_timer(); + let mut key = KeyEvent::from(key); canonicalize_key(&mut key); - // clear status - cx.editor.status_msg = None; + cxt.editor.status_msg = None; - let mode = cx.editor.mode(); + let (_, doc) = current!(cxt.editor); + let mode = doc.mode(); - if !self.on_next_key(OnKeyCallbackKind::PseudoPending, &mut cx, key) { + if let Some(on_next_key) = self.on_next_key.take() { + // if there's a command waiting input, do that first + on_next_key(&mut cxt, key); + } else { match mode { Mode::Insert => { + // record last_insert key + self.last_insert.1.push(key); + // let completion swallow the event if necessary let mut consumed = false; if let Some(completion) = &mut self.completion { - let res = { - // use a fake context here - let mut cx = Context { - editor: cx.editor, - jobs: cx.jobs, - scroll: None, - }; - - if let EventResult::Consumed(callback) = - completion.handle_event(event, &mut cx) - { - consumed = true; - Some(callback) - } else if let EventResult::Consumed(callback) = - completion.handle_event(&Event::Key(key!(Enter)), &mut cx) - { - Some(callback) - } else { - None - } + // use a fake context here + let mut cx = Context { + editor: cxt.editor, + jobs: cxt.jobs, + scroll: None, }; + let res = completion.handle_event(event, &mut cx); + + if let EventResult::Consumed(callback) = res { + consumed = true; - if let Some(callback) = res { if callback.is_some() { // assume close_fn - if let Some(cb) = self.clear_completion(cx.editor) { - if consumed { - cx.on_next_key_callback = - Some((cb, OnKeyCallbackKind::Fallback)) - } else { - self.on_next_key = - Some((cb, OnKeyCallbackKind::Fallback)); - } - } + self.completion = None; + cxt.editor.clear_idle_timer(); // don't retrigger } } } // if completion didn't take the event, we pass it onto commands if !consumed { - self.insert_mode(&mut cx, key); - - // record last_insert key - self.last_insert.1.push(InsertEvent::Key(key)); + self.insert_mode(&mut cxt, key); + + // lastly we recalculate completion + if let Some(completion) = &mut self.completion { + completion.update(&mut cxt); + if completion.is_empty() { + self.completion = None; + cxt.editor.clear_idle_timer(); // don't retrigger + } + } } } - mode => self.command_mode(mode, &mut cx, key), + mode => self.command_mode(mode, &mut cxt, key), } } - self.on_next_key = cx.on_next_key_callback.take(); - match self.on_next_key { - Some((_, OnKeyCallbackKind::PseudoPending)) => self.pseudo_pending.push(key), - _ => self.pseudo_pending.clear(), - } - + self.on_next_key = cxt.on_next_key_callback.take(); // appease borrowck - let callbacks = take(&mut cx.callback); + let callback = cxt.callback.take(); // if the command consumed the last view, skip the render. // on the next loop cycle the Application will then terminate. - if cx.editor.should_close() { - return EventResult::Ignored(None); + if cxt.editor.should_close() { + return EventResult::Ignored; } - let config = cx.editor.config(); - let mode = cx.editor.mode(); - let (view, doc) = current!(cx.editor); - - view.ensure_cursor_in_view(doc, config.scrolloff); - - // Store a history state if not in insert mode. This also takes care of - // committing changes when leaving insert mode. - if mode != Mode::Insert { - doc.append_changes_to_history(view); + let (view, doc) = current!(cxt.editor); + view.ensure_cursor_in_view(doc, cxt.editor.config.scrolloff); + + // mode transitions + match (mode, doc.mode()) { + (Mode::Normal, Mode::Insert) => { + // HAXX: if we just entered insert mode from normal, clear key buf + // and record the command that got us into this mode. + + // how we entered insert mode is important, and we should track that so + // we can repeat the side effect. + + self.last_insert.0 = + match self.keymaps.get_mut(&mode).unwrap().get(key).kind { + KeymapResultKind::Matched(command) => command, + // FIXME: insert mode can only be entered through single KeyCodes + _ => unimplemented!(), + }; + self.last_insert.1.clear(); + } + (Mode::Insert, Mode::Normal) => { + // if exiting insert mode, remove completion + self.completion = None; + } + _ => (), } - let callback = if callbacks.is_empty() { - None - } else { - let callback: crate::compositor::Callback = Box::new(move |compositor, cx| { - for callback in callbacks { - callback(compositor, cx) - } - }); - Some(callback) - }; EventResult::Consumed(callback) } - Event::Mouse(event) => self.handle_mouse_event(event, &mut cx), - Event::IdleTimeout => self.handle_idle_timeout(&mut cx), - Event::FocusGained => { - self.terminal_focused = true; - EventResult::Consumed(None) - } - Event::FocusLost => { - if context.editor.config().auto_save.focus_lost { - let options = commands::WriteAllOptions { - force: false, - write_scratch: false, - auto_format: false, - }; - if let Err(e) = commands::typed::write_all_impl(context, options) { - context.editor.set_error(format!("{}", e)); - } - } - self.terminal_focused = false; - EventResult::Consumed(None) - } + Event::Mouse(event) => self.handle_mouse_event(event, &mut cxt), } } 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")); - let config = cx.editor.config(); - - // check if bufferline should be rendered - use helix_view::editor::BufferLine; - let use_bufferline = match config.bufferline { - BufferLine::Always => true, - BufferLine::Multiple if cx.editor.documents.len() > 1 => true, - _ => false, - }; - - // -1 for commandline and -1 for bufferline - let mut editor_area = area.clip_bottom(1); - if use_bufferline { - editor_area = editor_area.clip_top(1); - } // if the terminal size suddenly changed, we need to trigger a resize - cx.editor.resize(editor_area); - - if use_bufferline { - Self::render_bufferline(cx.editor, area.with_height(1), surface); - } + cx.editor.resize(area.clip_bottom(1)); // -1 from bottom for commandline for (view, is_focused) in cx.editor.tree.views() { let doc = cx.editor.document(view.doc).unwrap(); - self.render_view(cx.editor, doc, view, area, surface, is_focused); + let loader = &cx.editor.syn_loader; + self.render_view( + doc, + view, + area, + surface, + &cx.editor.theme, + is_focused, + loader, + &cx.editor.config, + ); } - if config.auto_info { - if let Some(mut info) = cx.editor.autoinfo.take() { - info.render(area, surface, cx); - cx.editor.autoinfo = Some(info) - } + if let Some(ref mut info) = self.autoinfo { + info.render(area, surface, cx); } let key_width = 15u16; // for showing pending keys @@ -1587,36 +1078,20 @@ impl Component for EditorView { disp.push_str(&count.to_string()) } for key in self.keymaps.pending() { - disp.push_str(&key.key_sequence_format()); - } - for key in &self.pseudo_pending { - disp.push_str(&key.key_sequence_format()); + let s = key.to_string(); + if s.graphemes(true).count() > 1 { + disp.push_str(&format!("<{}>", s)); + } else { + disp.push_str(&s); + } } - let style = cx.editor.theme.get("ui.text"); - let macro_width = if cx.editor.macro_recording.is_some() { - 3 - } else { - 0 - }; surface.set_string( - area.x + area.width.saturating_sub(key_width + macro_width), + area.x + area.width.saturating_sub(key_width), area.y + area.height.saturating_sub(1), disp.get(disp.len().saturating_sub(key_width as usize)..) .unwrap_or(&disp), - style, + cx.editor.theme.get("ui.text"), ); - if let Some((reg, _)) = cx.editor.macro_recording { - let disp = format!("[{}]", reg); - let style = style - .fg(helix_view::graphics::Color::Yellow) - .add_modifier(Modifier::BOLD); - surface.set_string( - area.x + area.width.saturating_sub(3), - area.y + area.height.saturating_sub(1), - &disp, - style, - ); - } } if let Some(completion) = self.completion.as_mut() { @@ -1625,18 +1100,11 @@ impl Component for EditorView { } fn cursor(&self, _area: Rect, editor: &Editor) -> (Option<Position>, CursorKind) { - match editor.cursor() { - // all block cursors are drawn manually - (pos, CursorKind::Block) => { - if self.terminal_focused { - (pos, CursorKind::Hidden) - } else { - // use terminal cursor when terminal loses focus - (pos, CursorKind::Underline) - } - } - cursor => cursor, - } + // match view.doc.mode() { + // Mode::Insert => write!(stdout, "\x1B[6 q"), + // mode => write!(stdout, "\x1B[2 q"), + // }; + editor.cursor() } } @@ -1649,3 +1117,12 @@ fn canonicalize_key(key: &mut KeyEvent) { key.modifiers.remove(KeyModifiers::SHIFT) } } + +#[inline] +fn abs_diff(a: usize, b: usize) -> usize { + if a > b { + a - b + } else { + b - a + } +} |