diff options
Diffstat (limited to 'helix-term/src/ui/editor.rs')
-rw-r--r-- | helix-term/src/ui/editor.rs | 512 |
1 files changed, 192 insertions, 320 deletions
diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index a0518964..f297b44e 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -4,7 +4,10 @@ use crate::{ job::{self, Callback}, key, keymap::{KeymapResult, Keymaps}, - ui::{Completion, ProgressSpinners}, + ui::{ + document::{render_document, LinePos, TextRenderer, TranslatedPosition}, + Completion, ProgressSpinners, + }, }; use helix_core::{ @@ -13,8 +16,9 @@ use helix_core::{ }, movement::Direction, syntax::{self, HighlightEvent}, + text_annotations::TextAnnotations, unicode::width::UnicodeWidthStr, - visual_coords_at_pos, LineEnding, Position, Range, Selection, Transaction, + visual_offset_from_block, Position, Range, Selection, Transaction, }; use helix_view::{ document::{Mode, SCRATCH_BUFFER_NAME}, @@ -24,12 +28,12 @@ use helix_view::{ keyboard::{KeyCode, KeyModifiers}, Document, Editor, Theme, View, }; -use std::{borrow::Cow, cmp::min, num::NonZeroUsize, path::PathBuf}; +use std::{num::NonZeroUsize, path::PathBuf, rc::Rc}; use tui::buffer::Buffer as Surface; -use super::lsp::SignatureHelp; use super::statusline; +use super::{document::LineDecoration, lsp::SignatureHelp}; pub struct EditorView { pub keymaps: Keymaps, @@ -83,6 +87,10 @@ impl EditorView { let theme = &editor.theme; let config = editor.config(); + let text_annotations = view.text_annotations(doc, Some(theme)); + let mut line_decorations: Vec<Box<dyn LineDecoration>> = Vec::new(); + let mut translated_positions: Vec<TranslatedPosition> = Vec::new(); + // 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) { @@ -103,28 +111,40 @@ impl EditorView { == 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"), - ); - } + let style = theme.get("ui.highlight"); + let line_decoration = move |renderer: &mut TextRenderer, pos: LinePos| { + if pos.doc_line != line { + return; + } + renderer + .surface + .set_style(Rect::new(area.x, pos.visual_line, area.width, 1), style); + }; + + line_decorations.push(Box::new(line_decoration)); } } if is_focused && config.cursorline { - Self::highlight_cursorline(doc, view, surface, theme); + line_decorations.push(Self::cursorline_decorator(doc, view, theme)) } + if is_focused && config.cursorcolumn { - Self::highlight_cursorcolumn(doc, view, surface, theme); + Self::highlight_cursorcolumn(doc, view, surface, theme, inner, &text_annotations); + } + + let mut highlights = + Self::doc_syntax_highlights(doc, view.offset.anchor, inner.height, theme); + let overlay_highlights = Self::overlay_syntax_highlights( + doc, + view.offset.anchor, + inner.height, + &text_annotations, + ); + if !overlay_highlights.is_empty() { + highlights = Box::new(syntax::merge(highlights, overlay_highlights)); } - 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. @@ -133,8 +153,9 @@ impl EditorView { } highlights = Box::new(syntax::merge(highlights, diagnostic)); } + let highlights: Box<dyn Iterator<Item = HighlightEvent>> = if is_focused { - Box::new(syntax::merge( + let highlights = syntax::merge( highlights, Self::doc_selection_highlights( editor.mode(), @@ -143,19 +164,52 @@ impl EditorView { theme, &config.cursor_shape, ), - )) + ); + let focused_view_elements = Self::highlight_focused_view_elements(view, doc, theme); + if focused_view_elements.is_empty() { + Box::new(highlights) + } else { + Box::new(syntax::merge(highlights, focused_view_elements)) + } } else { Box::new(highlights) }; - 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); + Self::render_gutter( + editor, + doc, + view, + view.area, + theme, + is_focused, + &mut line_decorations, + ); if is_focused { - Self::render_focused_view_elements(view, doc, inner, theme, surface); + 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))); } + render_document( + surface, + inner, + doc, + view.offset, + &text_annotations, + highlights, + theme, + &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() { let x = area.right(); @@ -203,31 +257,53 @@ impl EditorView { .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.col 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)) } + pub fn overlay_syntax_highlights( + doc: &Document, + anchor: usize, + height: u16, + 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 range = { + // 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 + }; + + text_annotations.collect_overlay_highlights(range) + } + /// 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, - offset: Position, + anchor: usize, height: u16, _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 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 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 @@ -272,11 +348,11 @@ impl EditorView { use helix_core::diagnostic::Severity; let get_scope_of = |scope| { theme - .find_scope_index(scope) + .find_scope_index_exact(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")) + .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`", ) @@ -339,29 +415,29 @@ impl EditorView { let cursor_is_block = cursorkind == CursorKind::Block; let selection_scope = theme - .find_scope_index("ui.selection") + .find_scope_index_exact("ui.selection") .expect("could not find `ui.selection` scope in the theme!"); let primary_selection_scope = theme - .find_scope_index("ui.selection.primary") + .find_scope_index_exact("ui.selection.primary") .unwrap_or(selection_scope); let base_cursor_scope = theme - .find_scope_index("ui.cursor") + .find_scope_index_exact("ui.cursor") .unwrap_or(selection_scope); let base_primary_cursor_scope = theme .find_scope_index("ui.cursor.primary") .unwrap_or(base_cursor_scope); let cursor_scope = match mode { - Mode::Insert => theme.find_scope_index("ui.cursor.insert"), - Mode::Select => theme.find_scope_index("ui.cursor.select"), - Mode::Normal => theme.find_scope_index("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_scope_index("ui.cursor.primary.insert"), - Mode::Select => theme.find_scope_index("ui.cursor.primary.select"), - Mode::Normal => theme.find_scope_index("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); @@ -424,248 +500,26 @@ impl EditorView { 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 render_focused_view_elements( + pub fn highlight_focused_view_elements( view: &View, doc: &Document, - viewport: Rect, theme: &Theme, - surface: &mut Surface, - ) { + ) -> Vec<(usize, std::ops::Range<usize>)> { // Highlight matching braces if let Some(syntax) = doc.syntax() { let text = doc.text().slice(..); use helix_core::match_brackets; let pos = doc.selection(view.id).primary().cursor(text); - let pos = match_brackets::find_matching_bracket(syntax, doc.text(), pos) - .and_then(|pos| view.screen_coords_at_pos(doc, text, pos)); - - if let Some(pos) = pos { + if let Some(pos) = match_brackets::find_matching_bracket(syntax, doc.text(), pos) { // ensure col is on screen - if (pos.col as u16) < viewport.width + view.offset.col as u16 - && pos.col >= view.offset.col - { - let style = theme.try_get("ui.cursor.match").unwrap_or_else(|| { - Style::default() - .add_modifier(Modifier::REVERSED) - .add_modifier(Modifier::DIM) - }); - - surface[(viewport.x + pos.col as u16, viewport.y + pos.row as u16)] - .set_style(style); + if let Some(highlight) = theme.find_scope_index_exact("ui.cursor.match") { + return vec![(highlight, pos..pos + 1)]; } } } + Vec::new() } /// Render bufferline at the top @@ -721,22 +575,17 @@ impl EditorView { } } - pub fn render_gutter( - editor: &Editor, - doc: &Document, + pub fn render_gutter<'d>( + editor: &'d Editor, + doc: &'d Document, view: &View, viewport: Rect, - surface: &mut Surface, theme: &Theme, is_focused: bool, + line_decorations: &mut Vec<Box<(dyn LineDecoration + 'd)>>, ) { let text = doc.text().slice(..); - let last_line = view.last_line(doc); - - // it's used inside an iterator so the collect isn't needless: - // https://github.com/rust-lang/rust-clippy/issues/6164 - #[allow(clippy::needless_collect)] - let cursors: Vec<_> = doc + let cursors: Rc<[_]> = doc .selection(view.id) .iter() .map(|range| range.cursor_line(text)) @@ -746,29 +595,36 @@ impl EditorView { let gutter_style = theme.get("ui.gutter"); let gutter_selected_style = theme.get("ui.gutter.selected"); - - // avoid lots of small allocations by reusing a text buffer for each line - let mut text = String::with_capacity(8); + let gutter_style_virtual = theme.get("ui.gutter.virtual"); + let gutter_selected_style_virtual = theme.get("ui.gutter.selected.virtual"); for gutter_type in view.gutters() { let mut gutter = gutter_type.style(editor, doc, view, theme, is_focused); let width = gutter_type.width(view, doc); - 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); + // avoid lots of small allocations by reusing a text buffer for each line + let mut text = String::with_capacity(width); + let cursors = cursors.clone(); + let gutter_decoration = move |renderer: &mut TextRenderer, pos: LinePos| { + // TODO handle softwrap in gutters + let selected = cursors.contains(&pos.doc_line); let x = viewport.x + offset; - let y = viewport.y + i as u16; + let y = viewport.y + pos.visual_line; - let gutter_style = if selected { - gutter_selected_style - } else { - gutter_style + 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, }; - if let Some(style) = gutter(line, selected, &mut text) { - surface.set_stringn(x, y, &text, width, gutter_style.patch(style)); + if let Some(style) = + gutter(pos.doc_line, selected, pos.first_visual_line, &mut text) + { + renderer + .surface + .set_stringn(x, y, &text, width, gutter_style.patch(style)); } else { - surface.set_style( + renderer.surface.set_style( Rect { x, y, @@ -779,7 +635,8 @@ impl EditorView { ); } text.clear(); - } + }; + line_decorations.push(Box::new(gutter_decoration)); offset += width as u16; } @@ -840,10 +697,13 @@ impl EditorView { } /// Apply the highlighting on the lines where a cursor is active - pub fn highlight_cursorline(doc: &Document, view: &View, surface: &mut Surface, theme: &Theme) { + pub fn cursorline_decorator( + doc: &Document, + view: &View, + theme: &Theme, + ) -> Box<dyn LineDecoration> { let text = doc.text().slice(..); - let last_line = view.last_line(doc); - + // TODO only highlight the visual line that contains the cursor instead of the full visual line let primary_line = doc.selection(view.id).primary().cursor_line(text); // The secondary_lines do contain the primary_line, it doesn't matter @@ -860,20 +720,23 @@ impl EditorView { let primary_style = theme.get("ui.cursorline.primary"); let secondary_style = theme.get("ui.cursorline.secondary"); + let viewport = view.area; - for line in view.offset.row..(last_line + 1) { + let line_decoration = move |renderer: &mut TextRenderer, pos: LinePos| { let area = Rect::new( - view.area.x, - view.area.y + (line - view.offset.row) as u16, - view.area.width, + viewport.x, + viewport.y + pos.visual_line as u16, + viewport.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); + if primary_line == pos.doc_line { + renderer.surface.set_style(area, primary_style); + } else if secondary_lines.binary_search(&pos.doc_line).is_ok() { + renderer.surface.set_style(area, secondary_style); } - } + }; + + Box::new(line_decoration) } /// Apply the highlighting on the columns where a cursor is active @@ -882,6 +745,8 @@ impl EditorView { view: &View, surface: &mut Surface, theme: &Theme, + viewport: Rect, + text_annotations: &TextAnnotations, ) { let text = doc.text().slice(..); @@ -897,19 +762,23 @@ 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 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 >= offset && inner_area.width > (col - 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 - offset) as u16, + inner_area.x + (col - view.offset.horizontal_offset) as u16, view.area.y, 1, view.area.height, @@ -1149,7 +1018,7 @@ impl EditorView { 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) + view.pos_at_screen_coords(&editor.documents[&view.doc], row, column, true) .map(|pos| (pos, view.id)) }) }; @@ -1191,8 +1060,10 @@ impl EditorView { None => return EventResult::Ignored(None), }; - let line = coords.row + view.offset.row; - if line < doc.text().len_lines() { + if let Some(char_idx) = + view.pos_at_visual_coords(doc, coords.row as u16, coords.col as u16, true) + { + let line = doc.text().char_to_line(char_idx); commands::dap_toggle_breakpoint_impl(cxt, path, line); return EventResult::Consumed(None); } @@ -1204,7 +1075,7 @@ impl EditorView { MouseEventKind::Drag(MouseButton::Left) => { let (view, doc) = current!(cxt.editor); - let pos = match view.pos_at_screen_coords(doc, row, column) { + let pos = match view.pos_at_screen_coords(doc, row, column, true) { Some(pos) => pos, None => return EventResult::Ignored(None), }; @@ -1268,8 +1139,9 @@ impl EditorView { cxt.editor.focus(view_id); let (view, doc) = current!(cxt.editor); - let line = coords.row + view.offset.row; - if let Ok(pos) = doc.text().try_line_to_char(line) { + 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); |