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 | 1320 |
1 files changed, 615 insertions, 705 deletions
diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index b25af107..fc201853 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -1,29 +1,23 @@ use crate::{ - commands::{self, OnKeyCallback, OnKeyCallbackKind}, + commands, compositor::{Component, Context, Event, EventResult}, - events::{OnModeSwitch, PostCommand}, - handlers::completion::CompletionItem, + job::{self, Callback}, key, keymap::{KeymapResult, Keymaps}, - ui::{ - document::{render_document, LinePos, TextRenderer}, - statusline, - text_decorations::{self, Decoration, DecorationManager, InlineDiagnostics}, - Completion, ProgressSpinners, - }, + ui::{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}, - text_annotations::TextAnnotations, + syntax::{self, HighlightEvent}, unicode::width::UnicodeWidthStr, - visual_offset_from_block, Change, Position, Range, Selection, Transaction, + visual_coords_at_pos, LineEnding, Position, Range, Selection, Transaction, }; use helix_view::{ - annotations::diagnostics::DiagnosticFilter, + apply_transaction, document::{Mode, SCRATCH_BUFFER_NAME}, editor::{CompleteAction, CursorShapeConfig}, graphics::{Color, CursorKind, Modifier, Rect, Style}, @@ -31,30 +25,33 @@ use helix_view::{ keyboard::{KeyCode, KeyModifiers}, Document, Editor, Theme, View, }; -use std::{mem::take, num::NonZeroUsize, ops, path::PathBuf, rc::Rc}; +use std::{borrow::Cow, cmp::min, num::NonZeroUsize, path::PathBuf}; + +use tui::buffer::Buffer as Surface; -use tui::{buffer::Buffer as Surface, text::Span}; +use super::lsp::SignatureHelp; +use super::statusline; pub struct EditorView { pub keymaps: Keymaps, - on_next_key: Option<(OnKeyCallback, OnKeyCallbackKind)>, + on_next_key: Option<Box<dyn FnOnce(&mut commands::Context, KeyEvent)>>, pseudo_pending: Vec<KeyEvent>, - pub(crate) last_insert: (commands::MappableCommand, Vec<InsertEvent>), + last_insert: (commands::MappableCommand, Vec<InsertEvent>), pub(crate) completion: Option<Completion>, spinners: ProgressSpinners, - /// Tracks if the terminal window is focused by reaction to terminal focus events - terminal_focused: bool, } #[derive(Debug, Clone)] pub enum InsertEvent { Key(KeyEvent), - CompletionApply { - trigger_offset: usize, - changes: Vec<Change>, - }, + CompletionApply(CompleteAction), TriggerCompletion, - RequestCompletion, +} + +impl Default for EditorView { + fn default() -> Self { + Self::new(Keymaps::default()) + } } impl EditorView { @@ -66,7 +63,6 @@ impl EditorView { last_insert: (commands::MappableCommand::normal_mode, Vec::new()), completion: None, spinners: ProgressSpinners::default(), - terminal_focused: true, } } @@ -87,126 +83,79 @@ 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(); + // DAP: Highlight current stack frame position + let stack_frame = editor.debugger.as_ref().and_then(|debugger| { + if let (Some(frame), Some(thread_id)) = (debugger.active_frame, debugger.thread_id) { + debugger + .stack_frames + .get(&thread_id) + .and_then(|bt| bt.get(frame)) + } else { + None + } + }); + if let Some(frame) = stack_frame { + if doc.path().is_some() + && frame + .source + .as_ref() + .and_then(|source| source.path.as_ref()) + == doc.path() + { + let line = frame.line - 1; // convert to 0-indexing + if line >= view.offset.row && line < view.offset.row + area.height as usize { + surface.set_style( + Rect::new( + area.x, + area.y + (line - view.offset.row) as u16, + area.width, + 1, + ), + theme.get("ui.highlight"), + ); + } + } + } if is_focused && config.cursorline { - decorations.add_decoration(Self::cursorline(doc, view, theme)); + Self::highlight_cursorline(doc, view, surface, theme); } - if is_focused && config.cursorcolumn { - Self::highlight_cursorcolumn(doc, view, surface, theme, inner, &text_annotations); + Self::highlight_cursorcolumn(doc, view, surface, theme); } - // 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 mut highlights = Self::doc_syntax_highlights(doc, view.offset, inner.height, theme); + 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; } + highlights = Box::new(syntax::merge(highlights, diagnostic)); } + let highlights: Box<dyn Iterator<Item = HighlightEvent>> = if is_focused { + Box::new(syntax::merge( + highlights, + Self::doc_selection_highlights( + editor.mode(), + doc, + view, + theme, + &config.cursor_shape, + ), + )) + } else { + Box::new(highlights) + }; - 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, - ); - } - + Self::render_text_highlights(doc, view.offset, inner, surface, theme, highlights, &config); + Self::render_gutter(editor, doc, view, view.area, surface, theme, is_focused); 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, - }); + 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() { @@ -220,11 +169,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,208 +200,128 @@ 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.col 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)) } - 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, - height: u16, - loader: &'editor syntax::Loader, - ) -> Option<syntax::Highlighter<'editor>> { - let syntax = doc.syntax()?; - 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) - } - - pub fn overlay_syntax_highlights( - doc: &Document, - anchor: usize, + pub fn doc_syntax_highlights<'doc>( + doc: &'doc Document, + offset: Position, 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 = { + // Calculate viewport byte ranges: + // Saturating subs to make it inclusive zero indexing. + let last_line = doc.text().len_lines().saturating_sub(1); + let last_visible_line = (offset.row + height as usize) + .saturating_sub(1) + .min(last_line); + let start = text.line_to_byte(offset.row.min(last_line)); + let end = text.line_to_byte(last_visible_line + 1); + + start..end + }; - 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, + }); - 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(iter) + } + None => Box::new( + [HighlightEvent::Source { + start: text.byte_to_char(range.start), + end: text.byte_to_char(range.end), + }] + .into_iter(), + ), + } } /// 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(scope) + // get one of the themes below as fallback values + .or_else(|| 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`", + ) }; - // 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. @@ -466,8 +331,7 @@ impl EditorView { 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(); @@ -476,34 +340,27 @@ impl EditorView { 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"), + 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 { @@ -514,7 +371,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) { + if !selection_is_primary || cursor_is_block { // 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 @@ -529,66 +386,265 @@ 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((selection_scope, range.anchor..cursor_start)); + if !selection_is_primary || cursor_is_block { 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) { + if !selection_is_primary || cursor_is_block { 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((selection_scope, cursor_end..range.anchor)); } } - OverlayHighlights::Heterogenous { highlights: spans } + spans + } + + pub fn render_text_highlights<H: Iterator<Item = HighlightEvent>>( + doc: &Document, + offset: Position, + viewport: Rect, + surface: &mut Surface, + theme: &Theme, + highlights: H, + config: &helix_view::editor::Config, + ) { + let whitespace = &config.whitespace; + use helix_view::editor::WhitespaceRenderValue; + + // It's slightly more efficient to produce a full RopeSlice from the Rope, then slice that a bunch + // of times than it is to always call Rope::slice/get_slice (it will internally always hit RSEnum::Light). + let text = doc.text().slice(..); + + let characters = &whitespace.characters; + + let mut spans = Vec::new(); + let mut visual_x = 0usize; + let mut line = 0u16; + let tab_width = doc.tab_width(); + let tab = if whitespace.render.tab() == WhitespaceRenderValue::All { + std::iter::once(characters.tab) + .chain(std::iter::repeat(characters.tabpad).take(tab_width - 1)) + .collect() + } else { + " ".repeat(tab_width) + }; + let space = characters.space.to_string(); + let nbsp = characters.nbsp.to_string(); + let newline = if whitespace.render.newline() == WhitespaceRenderValue::All { + characters.newline.to_string() + } else { + " ".to_string() + }; + let indent_guide_char = config.indent_guides.character.to_string(); + + let text_style = theme.get("ui.text"); + let whitespace_style = theme.get("ui.virtual.whitespace"); + + let mut is_in_indent_area = true; + let mut last_line_indent_level = 0; + + // use whitespace style as fallback for indent-guide + let indent_guide_style = text_style.patch( + theme + .try_get("ui.virtual.indent-guide") + .unwrap_or_else(|| theme.get("ui.virtual.whitespace")), + ); + + let draw_indent_guides = |indent_level, line, surface: &mut Surface| { + if !config.indent_guides.render { + return; + } + + let starting_indent = + (offset.col / tab_width) + config.indent_guides.skip_levels as usize; + + // Don't draw indent guides outside of view + let end_indent = min( + indent_level, + // Add tab_width - 1 to round up, since the first visible + // indent might be a bit after offset.col + offset.col + viewport.width as usize + (tab_width - 1), + ) / tab_width; + + for i in starting_indent..end_indent { + let x = (viewport.x as usize + (i * tab_width) - offset.col) as u16; + let y = viewport.y + line; + debug_assert!(surface.in_bounds(x, y)); + surface.set_string(x, y, &indent_guide_char, indent_guide_style); + } + }; + + 'outer: for event in highlights { + match event { + HighlightEvent::HighlightStart(span) => { + spans.push(span); + } + HighlightEvent::HighlightEnd => { + spans.pop(); + } + HighlightEvent::Source { start, end } => { + let is_trailing_cursor = text.len_chars() < 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()); + let style = spans + .iter() + .fold(text_style, |acc, span| acc.patch(theme.highlight(span.0))); + + let space = if whitespace.render.space() == WhitespaceRenderValue::All + && !is_trailing_cursor + { + &space + } else { + " " + }; + + let nbsp = if whitespace.render.nbsp() == WhitespaceRenderValue::All + && text.len_chars() < end + { +   + } else { + " " + }; + + use helix_core::graphemes::{grapheme_width, RopeGraphemes}; + + for grapheme in RopeGraphemes::new(text) { + let out_of_bounds = offset.col > visual_x + || visual_x >= viewport.width as usize + offset.col; + + 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 as usize + visual_x - offset.col) as u16, + viewport.y + line, + &newline, + style.patch(whitespace_style), + ); + } + + draw_indent_guides(last_line_indent_level, line, surface); + + visual_x = 0; + line += 1; + is_in_indent_area = true; + + // TODO: with proper iter this shouldn't be necessary + if line >= viewport.height { + break 'outer; + } + } else { + let grapheme = Cow::from(grapheme); + let is_whitespace; + + let (display_grapheme, width) = if grapheme == "\t" { + is_whitespace = true; + // make sure we display tab as appropriate amount of spaces + let visual_tab_width = tab_width - (visual_x % tab_width); + let grapheme_tab_width = + helix_core::str_utils::char_to_byte_idx(&tab, visual_tab_width); + + (&tab[..grapheme_tab_width], visual_tab_width) + } else if grapheme == " " { + is_whitespace = true; + (space, 1) + } else if grapheme == "\u{00A0}" { + is_whitespace = true; + (nbsp, 1) + } else { + is_whitespace = false; + // 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) + }; + + let cut_off_start = offset.col.saturating_sub(visual_x); + + if !out_of_bounds { + // if we're offscreen just keep going until we hit a new line + surface.set_string( + (viewport.x as usize + visual_x - offset.col) as u16, + viewport.y + line, + display_grapheme, + if is_whitespace { + style.patch(whitespace_style) + } else { + style + }, + ); + } else if cut_off_start != 0 && cut_off_start < width { + // partially on screen + let rect = Rect::new( + viewport.x, + viewport.y + line, + (width - cut_off_start) as u16, + 1, + ); + surface.set_style( + rect, + if is_whitespace { + style.patch(whitespace_style) + } else { + style + }, + ); + } + + if is_in_indent_area && !(grapheme == " " || grapheme == "\t") { + draw_indent_guides(visual_x, line, surface); + is_in_indent_area = false; + last_line_indent_level = visual_x; + } + + visual_x = visual_x.saturating_add(width); + } + } + } + } + } } /// Render brace match, etc (meant for the focused view only) - pub fn highlight_focused_view_elements( + pub fn render_focused_view_elements( view: &View, doc: &Document, + viewport: Rect, theme: &Theme, - ) -> Option<OverlayHighlights> { + surface: &mut Surface, + ) { // 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); + + let pos = match_brackets::find_matching_bracket(syntax, doc.text(), pos) + .and_then(|pos| view.screen_coords_at_pos(doc, text, pos)); + + if let Some(pos) = pos { + // ensure col is on screen + if (pos.col as u16) < viewport.width + view.offset.col as u16 + && pos.col >= view.offset.col + { + let style = theme.try_get("ui.cursor.match").unwrap_or_else(|| { + Style::default() + .add_modifier(Modifier::REVERSED) + .add_modifier(Modifier::DIM) + }); - 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)); + surface[(viewport.x + pos.col as u16, viewport.y + pos.row as u16)] + .set_style(style); + } + } } - Some(OverlayHighlights::Homogeneous { highlight, ranges }) } /// Render bufferline at the top @@ -644,17 +700,22 @@ impl EditorView { } } - pub fn render_gutter<'d>( - editor: &'d Editor, - doc: &'d Document, + pub fn render_gutter( + editor: &Editor, + doc: &Document, view: &View, viewport: Rect, + surface: &mut Surface, theme: &Theme, is_focused: bool, - decoration_manager: &mut DecorationManager<'d>, ) { let text = doc.text().slice(..); - let cursors: Rc<[_]> = doc + let last_line = view.last_line(doc); + + // it's used inside an iterator so the collect isn't needless: + // https://github.com/rust-lang/rust-clippy/issues/6164 + #[allow(clippy::needless_collect)] + let cursors: Vec<_> = doc .selection(view.id) .iter() .map(|range| range.cursor_line(text)) @@ -664,34 +725,29 @@ impl EditorView { 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"); + + // avoid lots of small allocations by reusing a text buffer for each line + let mut text = String::with_capacity(8); 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); + text.reserve(width); // ensure there's enough space for the gutter + for (i, line) in (view.offset.row..(last_line + 1)).enumerate() { + let selected = cursors.contains(&line); let x = viewport.x + offset; - let y = pos.visual_line; + let y = viewport.y + i as u16; - 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, + let gutter_style = if selected { + gutter_selected_style + } else { + gutter_style }; - 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)); + if let Some(style) = gutter(line, selected, &mut text) { + surface.set_stringn(x, y, &text, width, gutter_style.patch(style)); } else { - renderer.set_style( + surface.set_style( Rect { x, y, @@ -702,8 +758,7 @@ impl EditorView { ); } text.clear(); - }; - decoration_manager.add_decoration(gutter_decoration); + } offset += width as u16; } @@ -750,18 +805,9 @@ impl EditorView { }); 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) + let paragraph = Paragraph::new(lines) .alignment(Alignment::Right) .wrap(Wrap { trim: true }); let width = 100.min(viewport.width); @@ -773,9 +819,10 @@ 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 highlight_cursorline(doc: &Document, view: &View, surface: &mut Surface, theme: &Theme) { let text = doc.text().slice(..); - // TODO only highlight the visual line that contains the cursor instead of the full visual line + let last_line = view.last_line(doc); + let primary_line = doc.selection(view.id).primary().cursor_line(text); // The secondary_lines do contain the primary_line, it doesn't matter @@ -792,14 +839,18 @@ impl EditorView { 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); + + for line in view.offset.row..(last_line + 1) { + let area = Rect::new( + view.area.x, + view.area.y + (line - view.offset.row) as u16, + view.area.width, + 1, + ); + if primary_line == line { + surface.set_style(area, primary_style); + } else if secondary_lines.binary_search(&line).is_ok() { + surface.set_style(area, secondary_style); } } } @@ -810,8 +861,6 @@ impl EditorView { view: &View, surface: &mut Surface, theme: &Theme, - viewport: Rect, - text_annotations: &TextAnnotations, ) { let text = doc.text().slice(..); @@ -827,24 +876,19 @@ impl EditorView { .unwrap_or_else(|| theme.get("ui.cursorline.secondary")); let inner_area = view.inner_area(doc); + let offset = view.offset.col; 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; + let Position { row: _, col } = + visual_coords_at_pos(text, range.cursor(text), doc.tab_width()); // 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 >= offset && inner_area.width > (col - offset) as u16 { let area = Rect::new( - inner_area.x + (col - view_offset.horizontal_offset) as u16, + inner_area.x + (col - offset) as u16, view.area.y, 1, view.area.height, @@ -875,26 +919,34 @@ impl EditorView { 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, - }); + match (last_mode, current_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. - // 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(); + + commands::signature_help_impl(cxt, commands::SignatureHelpInvoked::Automatic); + } + (Mode::Insert, Mode::Normal) => { + // if exiting insert mode, remove completion + self.completion = None; + + // TODO: Use an on_mode_change hook to remove signature help + cxt.jobs.callback(async { + let call: job::Callback = + Callback::EditorCompositor(Box::new(|_editor, compositor| { + compositor.remove(SignatureHelp::ID); + })); + Ok(call) + }); } + _ => (), } - last_mode = current_mode; }; @@ -917,10 +969,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,62 +994,43 @@ 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)) => { + // count handling + (key!(i @ '0'), Some(_)) | (key!(i @ '1'..='9'), _) => { 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) => { - 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, - } => { + InsertEvent::CompletionApply(compl) => { let (view, doc) = current!(cxt.editor); - if let Some(last_savepoint) = last_savepoint.as_deref() { - doc.restore(view, last_savepoint, true); - } + doc.restore(view); 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 shift_position = + |pos: usize| -> usize { pos + cursor - compl.trigger_offset }; let tx = Transaction::change( doc.text(), - changes.iter().cloned().map(|(start, end, t)| { + compl.changes.iter().cloned().map(|(start, end, t)| { (shift_position(start), shift_position(end), t) }), ); - doc.apply(&tx, view.id); + apply_transaction(&tx, doc, view); } InsertEvent::TriggerCompletion => { - last_savepoint = take(&mut last_request_savepoint); - } - InsertEvent::RequestCompletion => { - let (view, doc) = current!(cxt.editor); - last_request_savepoint = Some(doc.savepoint(view)); + let (_, doc) = current!(cxt.editor); + doc.savepoint(); } } } @@ -1016,116 +1047,76 @@ 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>, + 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); + // Immediately initialize a savepoint + doc_mut!(editor).savepoint(); + + editor.last_completion = None; 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); - } - })) - } - CompleteAction::Selected { savepoint } => { - let (view, doc) = current!(editor); - doc.restore(view, &savepoint, false); - } - } - } - on_next_key + + // Clear any savepoints + let doc = doc_mut!(editor); + doc.savepoint = None; + editor.clear_idle_timer(); // don't retrigger } 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) + }; + } + + if cx.editor.mode != Mode::Insert || !cx.editor.config().auto_completion { + return EventResult::Ignored(None); + } + + crate::commands::insert::idle_completion(cx); - EventResult::Ignored(None) + EventResult::Consumed(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) - } - let config = cxt.editor.config(); let MouseEvent { kind, @@ -1135,15 +1126,10 @@ impl EditorView { .. } = *event; - let pos_and_view = |editor: &Editor, row, column, ignore_virtual_text| { + let pos_and_view = |editor: &Editor, row, column| { 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)) + view.pos_at_screen_coords(&editor.documents[&view.doc], row, column) + .map(|pos| (pos, view.id)) }) }; @@ -1158,33 +1144,17 @@ impl EditorView { MouseEventKind::Down(MouseButton::Left) => { 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; + if let Some((pos, view_id)) = pos_and_view(editor, row, column) { 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)); } - if view_id != prev_view_id { - self.clear_completion(editor); - } - - editor.ensure_cursor_in_view(view_id); + editor.focus(view_id); return EventResult::Consumed(None); } @@ -1199,10 +1169,8 @@ impl EditorView { 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); + let line = coords.row + view.offset.row; + if line < doc.text().len_lines() { commands::dap_toggle_breakpoint_impl(cxt, path, line); return EventResult::Consumed(None); } @@ -1214,7 +1182,7 @@ impl EditorView { MouseEventKind::Drag(MouseButton::Left) => { 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), }; @@ -1223,8 +1191,7 @@ impl EditorView { let primary = selection.primary_mut(); *primary = primary.put_cursor(doc.text().slice(..), pos, true); doc.set_selection(view.id, selection); - let view_id = view.id; - cxt.editor.ensure_cursor_in_view(view_id); + EventResult::Consumed(None) } @@ -1237,16 +1204,15 @@ impl EditorView { _ => unreachable!(), }; - match pos_and_view(cxt.editor, row, column, false) { + match pos_and_view(cxt.editor, row, column) { Some((_, view_id)) => cxt.editor.tree.focus = view_id, None => return EventResult::Ignored(None), } 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); EventResult::Consumed(None) } @@ -1258,51 +1224,39 @@ 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); + let line = coords.row + view.offset.row; + if let Ok(pos) = doc.text().try_line_to_char(line) { + 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) } @@ -1319,7 +1273,7 @@ impl EditorView { return EventResult::Consumed(None); } - if let Some((pos, view_id)) = pos_and_view(editor, row, column, true) { + if let Some((pos, view_id)) = pos_and_view(editor, row, column) { let doc = doc_mut!(editor, &view!(editor, view_id).doc); doc.set_selection(view_id, Selection::point(pos)); cxt.editor.focus(view_id); @@ -1334,24 +1288,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 { @@ -1364,14 +1300,13 @@ impl Component for EditorView { editor: context.editor, count: None, register: None, - callback: Vec::new(), + callback: None, on_next_key_callback: None, jobs: context.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; @@ -1402,57 +1337,54 @@ impl Component for EditorView { cx.editor.status_msg = None; let mode = cx.editor.mode(); + let (view, _) = current!(cx.editor); + let focus = view.id; - 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 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: cx.editor, + jobs: cx.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.clear_completion(cx.editor); } } } // if completion didn't take the event, we pass it onto commands if !consumed { + if let Some(compl) = cx.editor.last_completion.take() { + self.last_insert.1.push(InsertEvent::CompletionApply(compl)); + } + self.insert_mode(&mut cx, key); // record last_insert key self.last_insert.1.push(InsertEvent::Key(key)); + + // lastly we recalculate completion + if let Some(completion) = &mut self.completion { + completion.update(&mut cx); + if completion.is_empty() { + self.clear_completion(cx.editor); + } + } } } mode => self.command_mode(mode, &mut cx, key), @@ -1461,12 +1393,12 @@ 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 - let callbacks = take(&mut cx.callback); + let callback = cx.callback.take(); // if the command consumed the last view, skip the render. // on the next loop cycle the Application will then terminate. @@ -1474,49 +1406,34 @@ impl Component for EditorView { return EventResult::Ignored(None); } - let config = cx.editor.config(); - let mode = cx.editor.mode(); - let (view, doc) = current!(cx.editor); + // if the focused view still exists and wasn't closed + if cx.editor.tree.contains(focus) { + let config = cx.editor.config(); + let mode = cx.editor.mode(); + let view = view_mut!(cx.editor, focus); + let doc = doc_mut!(cx.editor, &view.doc); - view.ensure_cursor_in_view(doc, config.scrolloff); + 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); + // 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 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::FocusGained => EventResult::Ignored(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)); } } - self.terminal_focused = false; EventResult::Consumed(None) } } @@ -1626,15 +1543,8 @@ 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) - } - } + // All block cursors are drawn manually + (pos, CursorKind::Block) => (pos, CursorKind::Hidden), cursor => cursor, } } |