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 | 689 |
1 files changed, 280 insertions, 409 deletions
diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index b25af107..fef62a29 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -1,43 +1,44 @@ use crate::{ - commands::{self, OnKeyCallback, OnKeyCallbackKind}, + commands::{self, OnKeyCallback}, compositor::{Component, Context, Event, EventResult}, events::{OnModeSwitch, PostCommand}, - handlers::completion::CompletionItem, key, keymap::{KeymapResult, Keymaps}, ui::{ - document::{render_document, LinePos, TextRenderer}, - statusline, - text_decorations::{self, Decoration, DecorationManager, InlineDiagnostics}, + document::{render_document, LinePos, TextRenderer, TranslatedPosition}, Completion, ProgressSpinners, }, }; use helix_core::{ diagnostic::NumberOrString, - graphemes::{next_grapheme_boundary, prev_grapheme_boundary}, + graphemes::{ + ensure_grapheme_boundary_next_byte, next_grapheme_boundary, prev_grapheme_boundary, + }, movement::Direction, - syntax::{self, OverlayHighlights}, + syntax::{self, HighlightEvent}, text_annotations::TextAnnotations, unicode::width::UnicodeWidthStr, visual_offset_from_block, Change, Position, Range, Selection, Transaction, }; use helix_view::{ - annotations::diagnostics::DiagnosticFilter, - document::{Mode, SCRATCH_BUFFER_NAME}, + document::{Mode, SavePoint, SCRATCH_BUFFER_NAME}, editor::{CompleteAction, CursorShapeConfig}, graphics::{Color, CursorKind, Modifier, Rect, Style}, input::{KeyEvent, MouseButton, MouseEvent, MouseEventKind}, keyboard::{KeyCode, KeyModifiers}, Document, Editor, Theme, View, }; -use std::{mem::take, num::NonZeroUsize, ops, path::PathBuf, rc::Rc}; +use std::{mem::take, num::NonZeroUsize, path::PathBuf, rc::Rc, sync::Arc}; use tui::{buffer::Buffer as Surface, text::Span}; +use super::document::LineDecoration; +use super::{completion::CompletionItem, statusline}; + pub struct EditorView { pub keymaps: Keymaps, - on_next_key: Option<(OnKeyCallback, OnKeyCallbackKind)>, + on_next_key: Option<OnKeyCallback>, pseudo_pending: Vec<KeyEvent>, pub(crate) last_insert: (commands::MappableCommand, Vec<InsertEvent>), pub(crate) completion: Option<Completion>, @@ -57,6 +58,12 @@ pub enum InsertEvent { RequestCompletion, } +impl Default for EditorView { + fn default() -> Self { + Self::new(Keymaps::default()) + } +} + impl EditorView { pub fn new(keymaps: Keymaps) -> Self { Self { @@ -87,15 +94,13 @@ impl EditorView { 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(); + let mut line_decorations: Vec<Box<dyn LineDecoration>> = Vec::new(); + let mut translated_positions: Vec<TranslatedPosition> = Vec::new(); if is_focused && config.cursorline { - decorations.add_decoration(Self::cursorline(doc, view, theme)); + line_decorations.push(Self::cursorline_decorator(doc, view, theme)) } if is_focused && config.cursorcolumn { @@ -110,51 +115,57 @@ impl EditorView { if pos.doc_line != dap_line { return; } - renderer.set_style(Rect::new(inner.x, pos.visual_line, inner.width, 1), style); + renderer.surface.set_style( + Rect::new(inner.x, inner.y + pos.visual_line, inner.width, 1), + style, + ); }; - decorations.add_decoration(line_decoration); + line_decorations.push(Box::new(line_decoration)); } - let syntax_highlighter = - Self::doc_syntax_highlighter(doc, view_offset.anchor, inner.height, &loader); - let mut overlays = Vec::new(); + let syntax_highlights = + Self::doc_syntax_highlights(doc, view.offset.anchor, inner.height, theme); - overlays.push(Self::overlay_syntax_highlights( + let mut overlay_highlights = + Self::empty_highlight_iter(doc, view.offset.anchor, inner.height); + let overlay_syntax_highlights = Self::overlay_syntax_highlights( doc, - view_offset.anchor, + view.offset.anchor, inner.height, &text_annotations, - )); + ); + if !overlay_syntax_highlights.is_empty() { + overlay_highlights = + Box::new(syntax::merge(overlay_highlights, overlay_syntax_highlights)); + } - 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); + for diagnostic in Self::doc_diagnostics_highlights(doc, theme) { + // Most of the `diagnostic` Vecs are empty most of the time. Skipping + // a merge for any empty Vec saves a significant amount of work. + if diagnostic.is_empty() { + continue; } + overlay_highlights = Box::new(syntax::merge(overlay_highlights, diagnostic)); } - 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 highlights = syntax::merge( + overlay_highlights, + Self::doc_selection_highlights( + editor.mode(), + doc, + view, + theme, + &config.cursor_shape, + self.terminal_focused, + ), + ); + let focused_view_elements = Self::highlight_focused_view_elements(view, doc, theme); + if focused_view_elements.is_empty() { + overlay_highlights = Box::new(highlights) + } else { + overlay_highlights = Box::new(syntax::merge(highlights, focused_view_elements)) } } @@ -167,46 +178,35 @@ impl EditorView { view.area, theme, is_focused & self.terminal_focused, - &mut decorations, + &mut line_decorations, ); } - Self::render_rulers(editor, doc, view, inner, surface, theme); - - 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, - }); + let cursor = doc + .selection(view.id) + .primary() + .cursor(doc.text().slice(..)); + // set the cursor_cache to out of view in case the position is not found + editor.cursor_cache.set(Some(None)); + let update_cursor_cache = + |_: &mut TextRenderer, pos| editor.cursor_cache.set(Some(Some(pos))); + translated_positions.push((cursor, Box::new(update_cursor_cache))); } - 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, + view.offset, &text_annotations, - syntax_highlighter, - overlays, + syntax_highlights, + overlay_highlights, theme, - decorations, + &mut line_decorations, + &mut translated_positions, ); + Self::render_rulers(editor, doc, view, inner, surface, theme); // if we're not at the edge of the screen, draw a right border if viewport.right() != view.area.right() { @@ -220,11 +220,7 @@ impl EditorView { } } - 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 @@ -255,13 +251,11 @@ impl EditorView { .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_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)) @@ -282,181 +276,142 @@ impl EditorView { start..end } - /// Get the syntax highlighter 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, + pub fn empty_highlight_iter( + doc: &Document, anchor: usize, height: u16, - loader: &'editor syntax::Loader, - ) -> Option<syntax::Highlighter<'editor>> { - let syntax = doc.syntax()?; + ) -> Box<dyn Iterator<Item = HighlightEvent>> { 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 highlighter = syntax.highlighter(text, loader, range); - Some(highlighter) + // Calculate viewport byte ranges: + // Saturating subs to make it inclusive zero indexing. + let range = Self::viewport_byte_range(text, row, height); + Box::new( + [HighlightEvent::Source { + start: text.byte_to_char(range.start), + end: text.byte_to_char(range.end), + }] + .into_iter(), + ) } - pub fn overlay_syntax_highlights( - doc: &Document, + /// 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_highlights<'doc>( + doc: &'doc Document, anchor: usize, height: u16, - text_annotations: &TextAnnotations, - ) -> OverlayHighlights { + _theme: &Theme, + ) -> Box<dyn Iterator<Item = HighlightEvent> + 'doc> { let text = doc.text().slice(..); let row = text.char_to_line(anchor.min(text.len_chars())); - let mut range = Self::viewport_byte_range(text, row, height); - range = text.byte_to_char(range.start)..text.byte_to_char(range.end); + let range = Self::viewport_byte_range(text, row, height); - text_annotations.collect_overlay_highlights(range) + match doc.syntax() { + Some(syntax) => { + let iter = syntax + // TODO: range doesn't actually restrict source, just highlight range + .highlight_iter(text.slice(..), Some(range), None) + .map(|event| event.unwrap()) + .map(move |event| match event { + // TODO: use byte slices directly + // convert byte offsets to char offset + HighlightEvent::Source { start, end } => { + let start = + text.byte_to_char(ensure_grapheme_boundary_next_byte(text, start)); + let end = + text.byte_to_char(ensure_grapheme_boundary_next_byte(text, end)); + HighlightEvent::Source { start, end } + } + event => event, + }); + + Box::new(iter) + } + None => Box::new( + [HighlightEvent::Source { + start: text.byte_to_char(range.start), + end: text.byte_to_char(range.end), + }] + .into_iter(), + ), + } } - pub fn doc_rainbow_highlights( + pub fn overlay_syntax_highlights( doc: &Document, anchor: usize, height: u16, - theme: &Theme, - loader: &syntax::Loader, - ) -> Option<OverlayHighlights> { - let syntax = doc.syntax()?; + text_annotations: &TextAnnotations, + ) -> Vec<(usize, std::ops::Range<usize>)> { 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)) + let range = Self::viewport_byte_range(text, row, height); + + text_annotations.collect_overlay_highlights(range) } /// 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}; + ) -> [Vec<(usize, std::ops::Range<usize>)>; 5] { + use helix_core::diagnostic::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`", - ) + .find_scope_index_exact(scope) + // get one of the themes below as fallback values + .or_else(|| theme.find_scope_index_exact("diagnostic")) + .or_else(|| theme.find_scope_index_exact("ui.cursor")) + .or_else(|| theme.find_scope_index_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"); + // basically just queries the theme color defined in the config + let hint = get_scope_of("diagnostic.hint"); + let info = get_scope_of("diagnostic.info"); + let warning = get_scope_of("diagnostic.warning"); + let error = get_scope_of("diagnostic.error"); + let r#default = get_scope_of("diagnostic"); // this is a bit redundant but should be fine - let mut default_vec = Vec::new(); + let mut default_vec: Vec<(usize, std::ops::Range<usize>)> = 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| { + for diagnostic in doc.diagnostics() { + // Separate diagnostics into different Vecs by severity. + let (vec, scope) = match diagnostic.severity { + Some(Severity::Info) => (&mut info_vec, info), + Some(Severity::Hint) => (&mut hint_vec, hint), + Some(Severity::Warning) => (&mut warning_vec, warning), + Some(Severity::Error) => (&mut error_vec, error), + _ => (&mut default_vec, r#default), + }; + // 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 => { + Some((_, range)) if diagnostic.range.start <= 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, - }; - - // 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) - ) - { - 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) - } - } + debug_assert!(range.start <= diagnostic.range.start); + range.end = diagnostic.range.end.max(range.end) } + _ => vec.push((scope, diagnostic.range.start..diagnostic.range.end)), } } - 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, - }, - ]); + [default_vec, info_vec, hint_vec, warning_vec, error_vec] } /// Get highlight spans for selections in a document view. @@ -467,7 +422,7 @@ impl EditorView { 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(); @@ -476,34 +431,34 @@ impl EditorView { let cursor_is_block = cursorkind == CursorKind::Block; let selection_scope = theme - .find_highlight_exact("ui.selection") + .find_scope_index_exact("ui.selection") .expect("could not find `ui.selection` scope in the theme!"); let primary_selection_scope = theme - .find_highlight_exact("ui.selection.primary") + .find_scope_index_exact("ui.selection.primary") .unwrap_or(selection_scope); let base_cursor_scope = theme - .find_highlight_exact("ui.cursor") + .find_scope_index_exact("ui.cursor") .unwrap_or(selection_scope); let base_primary_cursor_scope = theme - .find_highlight("ui.cursor.primary") + .find_scope_index("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"), + Mode::Insert => theme.find_scope_index_exact("ui.cursor.insert"), + Mode::Select => theme.find_scope_index_exact("ui.cursor.select"), + Mode::Normal => theme.find_scope_index_exact("ui.cursor.normal"), } .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"), + Mode::Insert => theme.find_scope_index_exact("ui.cursor.primary.insert"), + Mode::Select => theme.find_scope_index_exact("ui.cursor.primary.select"), + Mode::Normal => theme.find_scope_index_exact("ui.cursor.primary.normal"), } .unwrap_or(base_primary_cursor_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 { @@ -538,7 +493,7 @@ impl EditorView { }; 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 + // skip primary cursor if terminal is unfocused - crossterm cursor is used in that case if !selection_is_primary || (cursor_is_block && is_terminal_focused) { spans.push((cursor_scope, cursor_start..range.head)); } @@ -546,7 +501,7 @@ impl EditorView { // 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 + // skip primary cursor if terminal is unfocused - crossterm cursor is used in that case if !selection_is_primary || (cursor_is_block && is_terminal_focused) { spans.push((cursor_scope, range.head..cursor_end)); } @@ -563,7 +518,7 @@ impl EditorView { } } - OverlayHighlights::Heterogenous { highlights: spans } + spans } /// Render brace match, etc (meant for the focused view only) @@ -571,24 +526,23 @@ impl EditorView { view: &View, doc: &Document, theme: &Theme, - ) -> Option<OverlayHighlights> { + ) -> Vec<(usize, std::ops::Range<usize>)> { // Highlight matching braces - let syntax = doc.syntax()?; - let highlight = theme.find_highlight_exact("ui.cursor.match")?; - 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)) - } + 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); - 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)); + if let Some(pos) = + match_brackets::find_matching_bracket(syntax, doc.text().slice(..), pos) + { + // ensure col is on screen + if let Some(highlight) = theme.find_scope_index_exact("ui.cursor.match") { + return vec![(highlight, pos..pos + 1)]; + } + } } - Some(OverlayHighlights::Homogeneous { highlight, ranges }) + Vec::new() } /// Render bufferline at the top @@ -651,7 +605,7 @@ impl EditorView { viewport: Rect, theme: &Theme, is_focused: bool, - decoration_manager: &mut DecorationManager<'d>, + line_decorations: &mut Vec<Box<(dyn LineDecoration + 'd)>>, ) { let text = doc.text().slice(..); let cursors: Rc<[_]> = doc @@ -677,7 +631,7 @@ impl EditorView { // TODO handle softwrap in gutters let selected = cursors.contains(&pos.doc_line); let x = viewport.x + offset; - let y = pos.visual_line; + let y = viewport.y + pos.visual_line; let gutter_style = match (selected, pos.first_visual_line) { (false, true) => gutter_style, @@ -689,9 +643,11 @@ impl EditorView { 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)); + renderer + .surface + .set_stringn(x, y, &text, width, gutter_style.patch(style)); } else { - renderer.set_style( + renderer.surface.set_style( Rect { x, y, @@ -703,7 +659,7 @@ impl EditorView { } text.clear(); }; - decoration_manager.add_decoration(gutter_decoration); + line_decorations.push(Box::new(gutter_decoration)); offset += width as u16; } @@ -760,8 +716,7 @@ impl EditorView { } } - let text = Text::from(lines); - let paragraph = Paragraph::new(&text) + let paragraph = Paragraph::new(lines) .alignment(Alignment::Right) .wrap(Wrap { trim: true }); let width = 100.min(viewport.width); @@ -773,7 +728,11 @@ impl EditorView { } /// Apply the highlighting on the lines where a cursor is active - pub fn cursorline(doc: &Document, view: &View, theme: &Theme) -> impl Decoration { + pub fn cursorline_decorator( + doc: &Document, + view: &View, + theme: &Theme, + ) -> Box<dyn LineDecoration> { 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); @@ -794,14 +753,16 @@ impl EditorView { 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); + let line_decoration = move |renderer: &mut TextRenderer, pos: LinePos| { + let area = Rect::new(viewport.x, viewport.y + pos.visual_line, viewport.width, 1); if primary_line == pos.doc_line { - renderer.set_style(area, primary_style); + renderer.surface.set_style(area, primary_style); } else if secondary_lines.binary_search(&pos.doc_line).is_ok() { - renderer.set_style(area, secondary_style); + renderer.surface.set_style(area, secondary_style); } - } + }; + + Box::new(line_decoration) } /// Apply the highlighting on the columns where a cursor is active @@ -829,7 +790,6 @@ impl EditorView { let inner_area = view.inner_area(doc); 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() { @@ -840,11 +800,11 @@ impl EditorView { 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 + 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, + inner_area.x + (col - view.offset.horizontal_offset) as u16, view.area.y, 1, view.area.height, @@ -917,10 +877,8 @@ impl EditorView { 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) - } + if let Some(ch) = event.char() { + commands::insert::insert_char(cx, ch) } } KeymapResult::Cancelled(pending) => { @@ -944,19 +902,11 @@ 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) => { + // count handling + (key!(i @ '0'), Some(_)) | (key!(i @ '1'..='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() => { @@ -1016,10 +966,7 @@ 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 { @@ -1033,11 +980,12 @@ impl EditorView { pub fn set_completion( &mut self, editor: &mut Editor, + savepoint: Arc<SavePoint>, items: Vec<CompletionItem>, trigger_offset: usize, size: Rect, ) -> Option<Rect> { - let mut completion = Completion::new(editor, items, trigger_offset); + let mut completion = Completion::new(editor, savepoint, items, trigger_offset); if completion.is_empty() { // skip if we got no completion results @@ -1049,81 +997,54 @@ impl EditorView { 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> { + pub fn clear_completion(&mut self, editor: &mut Editor) { 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); - } - })) - } + } => self.last_insert.1.push(InsertEvent::CompletionApply { + trigger_offset, + changes, + }), 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); + if let Some(completion) = &mut self.completion { + return if completion.ensure_item_resolved(cx) { + EventResult::Consumed(None) + } else { + EventResult::Ignored(None) + }; + } + 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, cxt: &mut commands::Context, ) -> EventResult { if event.kind != MouseEventKind::Moved { - self.handle_non_key_input(cxt) + cxt.editor.reset_idle_timer(); } let config = cxt.editor.config(); @@ -1159,23 +1080,12 @@ impl EditorView { let editor = &mut cxt.editor; if let Some((pos, view_id)) = pos_and_view(editor, row, column, true) { - editor.focus(view_id); - let prev_view_id = view!(editor).id; let doc = doc_mut!(editor, &view!(editor, view_id).doc); if modifiers == 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)); } @@ -1184,6 +1094,7 @@ impl EditorView { self.clear_completion(editor); } + editor.focus(view_id); editor.ensure_cursor_in_view(view_id); return EventResult::Consumed(None); @@ -1243,7 +1154,7 @@ impl EditorView { } let offset = config.scroll_lines.unsigned_abs(); - commands::scroll(cxt, offset, direction, false); + commands::scroll(cxt, offset, direction); cxt.editor.tree.focus = current_view; cxt.editor.ensure_cursor_in_view(current_view); @@ -1258,51 +1169,40 @@ impl EditorView { let (view, doc) = current!(cxt.editor); - 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 doc + .selection(view.id) + .primary() + .slice(doc.text().slice(..)) + .len_chars() + <= 1 + { + return EventResult::Ignored(None); } + + commands::MappableCommand::yank_main_selection_to_primary_clipboard.execute(cxt); + + EventResult::Consumed(None) } MouseEventKind::Up(MouseButton::Right) => { - if let Some((pos, view_id)) = gutter_coords_and_view(cxt.editor, row, column) { + if let Some((coords, 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); - - 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), - }; + let (view, doc) = current!(cxt.editor); + if let Some(pos) = + view.pos_at_visual_coords(doc, coords.row as u16, coords.col as u16, true) + { + doc.set_selection(view_id, Selection::point(pos)); + if modifiers == KeyModifiers::ALT { + commands::MappableCommand::dap_edit_log.execute(cxt); + } else { + commands::MappableCommand::dap_edit_condition.execute(cxt); } - } - cxt.editor.ensure_cursor_in_view(view_id); - return EventResult::Consumed(None); + return EventResult::Consumed(None); + } } + EventResult::Ignored(None) } @@ -1334,24 +1234,6 @@ impl EditorView { _ => 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 - } - } } impl Component for EditorView { @@ -1371,7 +1253,6 @@ impl Component for EditorView { 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; @@ -1403,7 +1284,10 @@ impl Component for EditorView { let mode = cx.editor.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 cx, key); + } else { match mode { Mode::Insert => { // let completion swallow the event if necessary @@ -1434,15 +1318,7 @@ impl Component for EditorView { 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.clear_completion(cx.editor); } } } @@ -1461,8 +1337,8 @@ impl Component for EditorView { 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(), + Some(_) => self.pseudo_pending.push(key), + None => self.pseudo_pending.clear(), } // appease borrowck @@ -1506,13 +1382,8 @@ impl Component for EditorView { 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) { + if context.editor.config().auto_save { + if let Err(e) = commands::typed::write_all_impl(context, false, false) { context.editor.set_error(format!("{}", e)); } } @@ -1631,7 +1502,7 @@ impl Component for EditorView { if self.terminal_focused { (pos, CursorKind::Hidden) } else { - // use terminal cursor when terminal loses focus + // use crossterm cursor when terminal loses focus (pos, CursorKind::Underline) } } |