@@ -4,7 +4,10 @@ use crate::{
44 job::{self, Callback},
55 key,
66 keymap::{KeymapResult, Keymaps},
7− ui::{Completion, ProgressSpinners},
7+ ui::{
8+ document::{render_document, LinePos, TextRenderer, TranslatedPosition},
9+ Completion, ProgressSpinners,
10+ },
811};
912
1013use helix_core::{
@@ -13,8 +16,9 @@ use helix_core::{
1316 },
1417 movement::Direction,
1518 syntax::{self, HighlightEvent},
19+ text_annotations::TextAnnotations,
1620 unicode::width::UnicodeWidthStr,
17− visual_coords_at_pos, LineEnding, Position, Range, Selection, Transaction,
21+ visual_offset_from_block, Position, Range, Selection, Transaction,
1822};
1923use helix_view::{
2024 document::{Mode, SCRATCH_BUFFER_NAME},
@@ -24,12 +28,12 @@ use helix_view::{
2428 keyboard::{KeyCode, KeyModifiers},
2529 Document, Editor, Theme, View,
2630};
27−use std::{borrow::Cow, cmp::min, num::NonZeroUsize, path::PathBuf};
31+use std::{num::NonZeroUsize, path::PathBuf, rc::Rc};
2832
2933use tui::buffer::Buffer as Surface;
3034
31−use super::lsp::SignatureHelp;
3235use super::statusline;
36+use super::{document::LineDecoration, lsp::SignatureHelp};
3337
3438pub struct EditorView {
3539 pub keymaps: Keymaps,
@@ -83,6 +87,10 @@ impl EditorView {
8387 let theme = &editor.theme;
8488 let config = editor.config();
8589
90+ let text_annotations = view.text_annotations(doc, Some(theme));
91+ let mut line_decorations: Vec<Box<dyn LineDecoration>> = Vec::new();
92+ let mut translated_positions: Vec<TranslatedPosition> = Vec::new();
93+
8694 // DAP: Highlight current stack frame position
8795 let stack_frame = editor.debugger.as_ref().and_then(|debugger| {
8896 if let (Some(frame), Some(thread_id)) = (debugger.active_frame, debugger.thread_id) {
@@ -103,28 +111,40 @@ impl EditorView {
103111 == doc.path()
104112 {
105113 let line = frame.line - 1; // convert to 0-indexing
106− if line >= view.offset.row && line < view.offset.row + area.height as usize {
107− surface.set_style(
108− Rect::new(
109− area.x,
110− area.y + (line - view.offset.row) as u16,
111− area.width,
112− 1,
113− ),
114− theme.get("ui.highlight"),
115− );
116− }
114+ let style = theme.get("ui.highlight");
115+ let line_decoration = move |renderer: &mut TextRenderer, pos: LinePos| {
116+ if pos.doc_line != line {
117+ return;
118+ }
119+ renderer
120+ .surface
121+ .set_style(Rect::new(area.x, pos.visual_line, area.width, 1), style);
122+ };
123+
124+ line_decorations.push(Box::new(line_decoration));
117125 }
118126 }
119127
120128 if is_focused && config.cursorline {
121− Self::highlight_cursorline(doc, view, surface, theme);
129+ line_decorations.push(Self::cursorline_decorator(doc, view, theme))
122130 }
131+
123132 if is_focused && config.cursorcolumn {
124− Self::highlight_cursorcolumn(doc, view, surface, theme);
133+ Self::highlight_cursorcolumn(doc, view, surface, theme, inner, &text_annotations);
134+ }
135+
136+ let mut highlights =
137+ Self::doc_syntax_highlights(doc, view.offset.anchor, inner.height, theme);
138+ let overlay_highlights = Self::overlay_syntax_highlights(
139+ doc,
140+ view.offset.anchor,
141+ inner.height,
142+ &text_annotations,
143+ );
144+ if !overlay_highlights.is_empty() {
145+ highlights = Box::new(syntax::merge(highlights, overlay_highlights));
125146 }
126147
127− let mut highlights = Self::doc_syntax_highlights(doc, view.offset, inner.height, theme);
128148 for diagnostic in Self::doc_diagnostics_highlights(doc, theme) {
129149 // Most of the `diagnostic` Vecs are empty most of the time. Skipping
130150 // a merge for any empty Vec saves a significant amount of work.
@@ -133,8 +153,9 @@ impl EditorView {
133153 }
134154 highlights = Box::new(syntax::merge(highlights, diagnostic));
135155 }
156+
136157 let highlights: Box<dyn Iterator<Item = HighlightEvent>> = if is_focused {
137− Box::new(syntax::merge(
158+ let highlights = syntax::merge(
138159 highlights,
139160 Self::doc_selection_highlights(
140161 editor.mode(),
@@ -143,19 +164,52 @@ impl EditorView {
143164 theme,
144165 &config.cursor_shape,
145166 ),
146− ))
167+ );
168+ let focused_view_elements = Self::highlight_focused_view_elements(view, doc, theme);
169+ if focused_view_elements.is_empty() {
170+ Box::new(highlights)
171+ } else {
172+ Box::new(syntax::merge(highlights, focused_view_elements))
173+ }
147174 } else {
148175 Box::new(highlights)
149176 };
150177
151− Self::render_text_highlights(doc, view.offset, inner, surface, theme, highlights, &config);
152− Self::render_gutter(editor, doc, view, view.area, surface, theme, is_focused);
153− Self::render_rulers(editor, doc, view, inner, surface, theme);
178+ Self::render_gutter(
179+ editor,
180+ doc,
181+ view,
182+ view.area,
183+ theme,
184+ is_focused,
185+ &mut line_decorations,
186+ );
154187
155188 if is_focused {
156− Self::render_focused_view_elements(view, doc, inner, theme, surface);
189+ let cursor = doc
190+ .selection(view.id)
191+ .primary()
192+ .cursor(doc.text().slice(..));
193+ // set the cursor_cache to out of view in case the position is not found
194+ editor.cursor_cache.set(Some(None));
195+ let update_cursor_cache =
196+ |_: &mut TextRenderer, pos| editor.cursor_cache.set(Some(Some(pos)));
197+ translated_positions.push((cursor, Box::new(update_cursor_cache)));
157198 }
158199
200+ render_document(
201+ surface,
202+ inner,
203+ doc,
204+ view.offset,
205+ &text_annotations,
206+ highlights,
207+ theme,
208+ &mut line_decorations,
209+ &mut *translated_positions,
210+ );
211+ Self::render_rulers(editor, doc, view, inner, surface, theme);
212+
159213 // if we're not at the edge of the screen, draw a right border
160214 if viewport.right() != view.area.right() {
161215 let x = area.right();
@@ -203,31 +257,53 @@ impl EditorView {
203257 .iter()
204258 // View might be horizontally scrolled, convert from absolute distance
205259 // from the 1st column to relative distance from left of viewport
206− .filter_map(|ruler| ruler.checked_sub(1 + view.offset.col as u16))
260+ .filter_map(|ruler| ruler.checked_sub(1 + view.offset.horizontal_offset as u16))
207261 .filter(|ruler| ruler < &viewport.width)
208262 .map(|ruler| viewport.clip_left(ruler).with_width(1))
209263 .for_each(|area| surface.set_style(area, ruler_theme))
210264 }
211265
266+ pub fn overlay_syntax_highlights(
267+ doc: &Document,
268+ anchor: usize,
269+ height: u16,
270+ text_annotations: &TextAnnotations,
271+ ) -> Vec<(usize, std::ops::Range<usize>)> {
272+ let text = doc.text().slice(..);
273+ let row = text.char_to_line(anchor.min(text.len_chars()));
274+
275+ let range = {
276+ // Calculate viewport byte ranges:
277+ // Saturating subs to make it inclusive zero indexing.
278+ let last_line = text.len_lines().saturating_sub(1);
279+ let last_visible_line = (row + height as usize).saturating_sub(1).min(last_line);
280+ let start = text.line_to_byte(row.min(last_line));
281+ let end = text.line_to_byte(last_visible_line + 1);
282+
283+ start..end
284+ };
285+
286+ text_annotations.collect_overlay_highlights(range)
287+ }
288+
212289 /// Get syntax highlights for a document in a view represented by the first line
213290 /// and column (`offset`) and the last line. This is done instead of using a view
214291 /// directly to enable rendering syntax highlighted docs anywhere (eg. picker preview)
215292 pub fn doc_syntax_highlights<'doc>(
216293 doc: &'doc Document,
217− offset: Position,
294+ anchor: usize,
218295 height: u16,
219296 _theme: &Theme,
220297 ) -> Box<dyn Iterator<Item = HighlightEvent> + 'doc> {
221298 let text = doc.text().slice(..);
299+ let row = text.char_to_line(anchor.min(text.len_chars()));
222300
223301 let range = {
224302 // Calculate viewport byte ranges:
225303 // Saturating subs to make it inclusive zero indexing.
226− let last_line = doc.text().len_lines().saturating_sub(1);
227− let last_visible_line = (offset.row + height as usize)
228− .saturating_sub(1)
229− .min(last_line);
230− let start = text.line_to_byte(offset.row.min(last_line));
304+ let last_line = text.len_lines().saturating_sub(1);
305+ let last_visible_line = (row + height as usize).saturating_sub(1).min(last_line);
306+ let start = text.line_to_byte(row.min(last_line));
231307 let end = text.line_to_byte(last_visible_line + 1);
232308
233309 start..end
@@ -272,11 +348,11 @@ impl EditorView {
272348 use helix_core::diagnostic::Severity;
273349 let get_scope_of = |scope| {
274350 theme
275− .find_scope_index(scope)
351+ .find_scope_index_exact(scope)
276352 // get one of the themes below as fallback values
277− .or_else(|| theme.find_scope_index("diagnostic"))
278− .or_else(|| theme.find_scope_index("ui.cursor"))
279− .or_else(|| theme.find_scope_index("ui.selection"))
353+ .or_else(|| theme.find_scope_index_exact("diagnostic"))
354+ .or_else(|| theme.find_scope_index_exact("ui.cursor"))
355+ .or_else(|| theme.find_scope_index_exact("ui.selection"))
280356 .expect(
281357 "at least one of the following scopes must be defined in the theme: `diagnostic`, `ui.cursor`, or `ui.selection`",
282358 )
@@ -339,29 +415,29 @@ impl EditorView {
339415 let cursor_is_block = cursorkind == CursorKind::Block;
340416
341417 let selection_scope = theme
342− .find_scope_index("ui.selection")
418+ .find_scope_index_exact("ui.selection")
343419 .expect("could not find `ui.selection` scope in the theme!");
344420 let primary_selection_scope = theme
345− .find_scope_index("ui.selection.primary")
421+ .find_scope_index_exact("ui.selection.primary")
346422 .unwrap_or(selection_scope);
347423 let base_cursor_scope = theme
348− .find_scope_index("ui.cursor")
424+ .find_scope_index_exact("ui.cursor")
349425 .unwrap_or(selection_scope);
350426 let base_primary_cursor_scope = theme
351427 .find_scope_index("ui.cursor.primary")
352428 .unwrap_or(base_cursor_scope);
353429
354430 let cursor_scope = match mode {
355− Mode::Insert => theme.find_scope_index("ui.cursor.insert"),
356− Mode::Select => theme.find_scope_index("ui.cursor.select"),
357− Mode::Normal => theme.find_scope_index("ui.cursor.normal"),
431+ Mode::Insert => theme.find_scope_index_exact("ui.cursor.insert"),
432+ Mode::Select => theme.find_scope_index_exact("ui.cursor.select"),
433+ Mode::Normal => theme.find_scope_index_exact("ui.cursor.normal"),
358434 }
359435 .unwrap_or(base_cursor_scope);
360436
361437 let primary_cursor_scope = match mode {
362− Mode::Insert => theme.find_scope_index("ui.cursor.primary.insert"),
363− Mode::Select => theme.find_scope_index("ui.cursor.primary.select"),
364− Mode::Normal => theme.find_scope_index("ui.cursor.primary.normal"),
438+ Mode::Insert => theme.find_scope_index_exact("ui.cursor.primary.insert"),
439+ Mode::Select => theme.find_scope_index_exact("ui.cursor.primary.select"),
440+ Mode::Normal => theme.find_scope_index_exact("ui.cursor.primary.normal"),
365441 }
366442 .unwrap_or(base_primary_cursor_scope);
367443
@@ -424,248 +500,26 @@ impl EditorView {
424500 spans
425501 }
426502
427− pub fn render_text_highlights<H: Iterator<Item = HighlightEvent>>(
428− doc: &Document,
429− offset: Position,
430− viewport: Rect,
431− surface: &mut Surface,
432− theme: &Theme,
433− highlights: H,
434− config: &helix_view::editor::Config,
435− ) {
436− let whitespace = &config.whitespace;
437− use helix_view::editor::WhitespaceRenderValue;
438−
439− // It's slightly more efficient to produce a full RopeSlice from the Rope, then slice that a bunch
440− // of times than it is to always call Rope::slice/get_slice (it will internally always hit RSEnum::Light).
441− let text = doc.text().slice(..);
442−
443− let characters = &whitespace.characters;
444−
445− let mut spans = Vec::new();
446− let mut visual_x = 0usize;
447− let mut line = 0u16;
448− let tab_width = doc.tab_width();
449− let tab = if whitespace.render.tab() == WhitespaceRenderValue::All {
450− std::iter::once(characters.tab)
451− .chain(std::iter::repeat(characters.tabpad).take(tab_width - 1))
452− .collect()
453− } else {
454− " ".repeat(tab_width)
455− };
456− let space = characters.space.to_string();
457− let nbsp = characters.nbsp.to_string();
458− let newline = if whitespace.render.newline() == WhitespaceRenderValue::All {
459− characters.newline.to_string()
460− } else {
461− " ".to_string()
462− };
463− let indent_guide_char = config.indent_guides.character.to_string();
464−
465− let text_style = theme.get("ui.text");
466− let whitespace_style = theme.get("ui.virtual.whitespace");
467−
468− let mut is_in_indent_area = true;
469− let mut last_line_indent_level = 0;
470−
471− // use whitespace style as fallback for indent-guide
472− let indent_guide_style = text_style.patch(
473− theme
474− .try_get("ui.virtual.indent-guide")
475− .unwrap_or_else(|| theme.get("ui.virtual.whitespace")),
476− );
477−
478− let draw_indent_guides = |indent_level, line, surface: &mut Surface| {
479− if !config.indent_guides.render {
480− return;
481− }
482−
483− let starting_indent =
484− (offset.col / tab_width) + config.indent_guides.skip_levels as usize;
485−
486− // Don't draw indent guides outside of view
487− let end_indent = min(
488− indent_level,
489− // Add tab_width - 1 to round up, since the first visible
490− // indent might be a bit after offset.col
491− offset.col + viewport.width as usize + (tab_width - 1),
492− ) / tab_width;
493−
494− for i in starting_indent..end_indent {
495− let x = (viewport.x as usize + (i * tab_width) - offset.col) as u16;
496− let y = viewport.y + line;
497− debug_assert!(surface.in_bounds(x, y));
498− surface.set_string(x, y, &indent_guide_char, indent_guide_style);
499− }
500− };
501−
502− 'outer: for event in highlights {
503− match event {
504− HighlightEvent::HighlightStart(span) => {
505− spans.push(span);
506− }
507− HighlightEvent::HighlightEnd => {
508− spans.pop();
509− }
510− HighlightEvent::Source { start, end } => {
511− let is_trailing_cursor = text.len_chars() < end;
512−
513− // `unwrap_or_else` part is for off-the-end indices of
514− // the rope, to allow cursor highlighting at the end
515− // of the rope.
516− let text = text.get_slice(start..end).unwrap_or_else(|| " ".into());
517− let style = spans
518− .iter()
519− .fold(text_style, |acc, span| acc.patch(theme.highlight(span.0)));
520−
521− let space = if whitespace.render.space() == WhitespaceRenderValue::All
522− && !is_trailing_cursor
523− {
524− &space
525− } else {
526− " "
527− };
528−
529− let nbsp = if whitespace.render.nbsp() == WhitespaceRenderValue::All
530− && text.len_chars() < end
531− {
532−  
533− } else {
534− " "
535− };
536−
537− use helix_core::graphemes::{grapheme_width, RopeGraphemes};
538−
539− for grapheme in RopeGraphemes::new(text) {
540− let out_of_bounds = offset.col > visual_x
541− || visual_x >= viewport.width as usize + offset.col;
542−
543− if LineEnding::from_rope_slice(&grapheme).is_some() {
544− if !out_of_bounds {
545− // we still want to render an empty cell with the style
546− surface.set_string(
547− (viewport.x as usize + visual_x - offset.col) as u16,
548− viewport.y + line,
549− &newline,
550− style.patch(whitespace_style),
551− );
552− }
553−
554− draw_indent_guides(last_line_indent_level, line, surface);
555−
556− visual_x = 0;
557− line += 1;
558− is_in_indent_area = true;
559−
560− // TODO: with proper iter this shouldn't be necessary
561− if line >= viewport.height {
562− break 'outer;
563− }
564− } else {
565− let grapheme = Cow::from(grapheme);
566− let is_whitespace;
567−
568− let (display_grapheme, width) = if grapheme == "\t" {
569− is_whitespace = true;
570− // make sure we display tab as appropriate amount of spaces
571− let visual_tab_width = tab_width - (visual_x % tab_width);
572− let grapheme_tab_width =
573− helix_core::str_utils::char_to_byte_idx(&tab, visual_tab_width);
574−
575− (&tab[..grapheme_tab_width], visual_tab_width)
576− } else if grapheme == " " {
577− is_whitespace = true;
578− (space, 1)
579− } else if grapheme == "\u{00A0}" {
580− is_whitespace = true;
581− (nbsp, 1)
582− } else {
583− is_whitespace = false;
584− // Cow will prevent allocations if span contained in a single slice
585− // which should really be the majority case
586− let width = grapheme_width(&grapheme);
587− (grapheme.as_ref(), width)
588− };
589−
590− let cut_off_start = offset.col.saturating_sub(visual_x);
591−
592− if !out_of_bounds {
593− // if we're offscreen just keep going until we hit a new line
594− surface.set_string(
595− (viewport.x as usize + visual_x - offset.col) as u16,
596− viewport.y + line,
597− display_grapheme,
598− if is_whitespace {
599− style.patch(whitespace_style)
600− } else {
601− style
602− },
603− );
604− } else if cut_off_start != 0 && cut_off_start < width {
605− // partially on screen
606− let rect = Rect::new(
607− viewport.x,
608− viewport.y + line,
609− (width - cut_off_start) as u16,
610− 1,
611− );
612− surface.set_style(
613− rect,
614− if is_whitespace {
615− style.patch(whitespace_style)
616− } else {
617− style
618− },
619− );
620− }
621−
622− if is_in_indent_area && !(grapheme == " " || grapheme == "\t") {
623− draw_indent_guides(visual_x, line, surface);
624− is_in_indent_area = false;
625− last_line_indent_level = visual_x;
626− }
627−
628− visual_x = visual_x.saturating_add(width);
629− }
630− }
631− }
632− }
633− }
634− }
635−
636503 /// Render brace match, etc (meant for the focused view only)
637− pub fn render_focused_view_elements(
504+ pub fn highlight_focused_view_elements(
638505 view: &View,
639506 doc: &Document,
640− viewport: Rect,
641507 theme: &Theme,
642− surface: &mut Surface,
643− ) {
508+ ) -> Vec<(usize, std::ops::Range<usize>)> {
644509 // Highlight matching braces
645510 if let Some(syntax) = doc.syntax() {
646511 let text = doc.text().slice(..);
647512 use helix_core::match_brackets;
648513 let pos = doc.selection(view.id).primary().cursor(text);
649514
650− let pos = match_brackets::find_matching_bracket(syntax, doc.text(), pos)
651− .and_then(|pos| view.screen_coords_at_pos(doc, text, pos));
652−
653− if let Some(pos) = pos {
515+ if let Some(pos) = match_brackets::find_matching_bracket(syntax, doc.text(), pos) {
654516 // ensure col is on screen
655− if (pos.col as u16) < viewport.width + view.offset.col as u16
656− && pos.col >= view.offset.col
657− {
658− let style = theme.try_get("ui.cursor.match").unwrap_or_else(|| {
659− Style::default()
660− .add_modifier(Modifier::REVERSED)
661− .add_modifier(Modifier::DIM)
662− });
663−
664− surface[(viewport.x + pos.col as u16, viewport.y + pos.row as u16)]
665− .set_style(style);
517+ if let Some(highlight) = theme.find_scope_index_exact("ui.cursor.match") {
518+ return vec![(highlight, pos..pos + 1)];
666519 }
667520 }
668521 }
522+ Vec::new()
669523 }
670524
671525 /// Render bufferline at the top
@@ -721,22 +575,17 @@ impl EditorView {
721575 }
722576 }
723577
724− pub fn render_gutter(
725− editor: &Editor,
726− doc: &Document,
578+ pub fn render_gutter<'d>(
579+ editor: &'d Editor,
580+ doc: &'d Document,
727581 view: &View,
728582 viewport: Rect,
729− surface: &mut Surface,
730583 theme: &Theme,
731584 is_focused: bool,
585+ line_decorations: &mut Vec<Box<(dyn LineDecoration + 'd)>>,
732586 ) {
733587 let text = doc.text().slice(..);
734− let last_line = view.last_line(doc);
735−
736− // it's used inside an iterator so the collect isn't needless:
737− // https://github.com/rust-lang/rust-clippy/issues/6164
738− #[allow(clippy::needless_collect)]
739− let cursors: Vec<_> = doc
588+ let cursors: Rc<[_]> = doc
740589 .selection(view.id)
741590 .iter()
742591 .map(|range| range.cursor_line(text))
@@ -746,29 +595,36 @@ impl EditorView {
746595
747596 let gutter_style = theme.get("ui.gutter");
748597 let gutter_selected_style = theme.get("ui.gutter.selected");
749−
750− // avoid lots of small allocations by reusing a text buffer for each line
751− let mut text = String::with_capacity(8);
598+ let gutter_style_virtual = theme.get("ui.gutter.virtual");
599+ let gutter_selected_style_virtual = theme.get("ui.gutter.selected.virtual");
752600
753601 for gutter_type in view.gutters() {
754602 let mut gutter = gutter_type.style(editor, doc, view, theme, is_focused);
755603 let width = gutter_type.width(view, doc);
756− text.reserve(width); // ensure there's enough space for the gutter
757− for (i, line) in (view.offset.row..(last_line + 1)).enumerate() {
758− let selected = cursors.contains(&line);
604+ // avoid lots of small allocations by reusing a text buffer for each line
605+ let mut text = String::with_capacity(width);
606+ let cursors = cursors.clone();
607+ let gutter_decoration = move |renderer: &mut TextRenderer, pos: LinePos| {
608+ // TODO handle softwrap in gutters
609+ let selected = cursors.contains(&pos.doc_line);
759610 let x = viewport.x + offset;
760− let y = viewport.y + i as u16;
611+ let y = viewport.y + pos.visual_line;
761612
762− let gutter_style = if selected {
763− gutter_selected_style
764− } else {
765− gutter_style
613+ let gutter_style = match (selected, pos.first_visual_line) {
614+ (false, true) => gutter_style,
615+ (true, true) => gutter_selected_style,
616+ (false, false) => gutter_style_virtual,
617+ (true, false) => gutter_selected_style_virtual,
766618 };
767619
768− if let Some(style) = gutter(line, selected, &mut text) {
769− surface.set_stringn(x, y, &text, width, gutter_style.patch(style));
620+ if let Some(style) =
621+ gutter(pos.doc_line, selected, pos.first_visual_line, &mut text)
622+ {
623+ renderer
624+ .surface
625+ .set_stringn(x, y, &text, width, gutter_style.patch(style));
770626 } else {
771− surface.set_style(
627+ renderer.surface.set_style(
772628 Rect {
773629 x,
774630 y,
@@ -779,7 +635,8 @@ impl EditorView {
779635 );
780636 }
781637 text.clear();
782− }
638+ };
639+ line_decorations.push(Box::new(gutter_decoration));
783640
784641 offset += width as u16;
785642 }
@@ -840,10 +697,13 @@ impl EditorView {
840697 }
841698
842699 /// Apply the highlighting on the lines where a cursor is active
843− pub fn highlight_cursorline(doc: &Document, view: &View, surface: &mut Surface, theme: &Theme) {
700+ pub fn cursorline_decorator(
701+ doc: &Document,
702+ view: &View,
703+ theme: &Theme,
704+ ) -> Box<dyn LineDecoration> {
844705 let text = doc.text().slice(..);
845− let last_line = view.last_line(doc);
846−
706+ // TODO only highlight the visual line that contains the cursor instead of the full visual line
847707 let primary_line = doc.selection(view.id).primary().cursor_line(text);
848708
849709 // The secondary_lines do contain the primary_line, it doesn't matter
@@ -860,20 +720,23 @@ impl EditorView {
860720
861721 let primary_style = theme.get("ui.cursorline.primary");
862722 let secondary_style = theme.get("ui.cursorline.secondary");
723+ let viewport = view.area;
863724
864− for line in view.offset.row..(last_line + 1) {
725+ let line_decoration = move |renderer: &mut TextRenderer, pos: LinePos| {
865726 let area = Rect::new(
866− view.area.x,
867− view.area.y + (line - view.offset.row) as u16,
868− view.area.width,
727+ viewport.x,
728+ viewport.y + pos.visual_line as u16,
729+ viewport.width,
869730 1,
870731 );
871− if primary_line == line {
872− surface.set_style(area, primary_style);
873− } else if secondary_lines.binary_search(&line).is_ok() {
874− surface.set_style(area, secondary_style);
732+ if primary_line == pos.doc_line {
733+ renderer.surface.set_style(area, primary_style);
734+ } else if secondary_lines.binary_search(&pos.doc_line).is_ok() {
735+ renderer.surface.set_style(area, secondary_style);
875736 }
876− }
737+ };
738+
739+ Box::new(line_decoration)
877740 }
878741
879742 /// Apply the highlighting on the columns where a cursor is active
@@ -882,6 +745,8 @@ impl EditorView {
882745 view: &View,
883746 surface: &mut Surface,
884747 theme: &Theme,
748+ viewport: Rect,
749+ text_annotations: &TextAnnotations,
885750 ) {
886751 let text = doc.text().slice(..);
887752
@@ -897,19 +762,23 @@ impl EditorView {
897762 .unwrap_or_else(|| theme.get("ui.cursorline.secondary"));
898763
899764 let inner_area = view.inner_area(doc);
900− let offset = view.offset.col;
901765
902766 let selection = doc.selection(view.id);
903767 let primary = selection.primary();
768+ let text_format = doc.text_format(viewport.width, None);
904769 for range in selection.iter() {
905770 let is_primary = primary == *range;
771+ let cursor = range.cursor(text);
772+
773+ let Position { col, .. } =
774+ visual_offset_from_block(text, cursor, cursor, &text_format, text_annotations).0;
906775
907− let Position { row: _, col } =
908− visual_coords_at_pos(text, range.cursor(text), doc.tab_width());
909776 // if the cursor is horizontally in the view
910− if col >= offset && inner_area.width > (col - offset) as u16 {
777+ if col >= view.offset.horizontal_offset
778+ && inner_area.width > (col - view.offset.horizontal_offset) as u16
779+ {
911780 let area = Rect::new(
912− inner_area.x + (col - offset) as u16,
781+ inner_area.x + (col - view.offset.horizontal_offset) as u16,
913782 view.area.y,
914783 1,
915784 view.area.height,
@@ -1149,7 +1018,7 @@ impl EditorView {
11491018
11501019 let pos_and_view = |editor: &Editor, row, column| {
11511020 editor.tree.views().find_map(|(view, _focus)| {
1152− view.pos_at_screen_coords(&editor.documents[&view.doc], row, column)
1021+ view.pos_at_screen_coords(&editor.documents[&view.doc], row, column, true)
11531022 .map(|pos| (pos, view.id))
11541023 })
11551024 };
@@ -1191,8 +1060,10 @@ impl EditorView {
11911060 None => return EventResult::Ignored(None),
11921061 };
11931062
1194− let line = coords.row + view.offset.row;
1195− if line < doc.text().len_lines() {
1063+ if let Some(char_idx) =
1064+ view.pos_at_visual_coords(doc, coords.row as u16, coords.col as u16, true)
1065+ {
1066+ let line = doc.text().char_to_line(char_idx);
11961067 commands::dap_toggle_breakpoint_impl(cxt, path, line);
11971068 return EventResult::Consumed(None);
11981069 }
@@ -1204,7 +1075,7 @@ impl EditorView {
12041075 MouseEventKind::Drag(MouseButton::Left) => {
12051076 let (view, doc) = current!(cxt.editor);
12061077
1207− let pos = match view.pos_at_screen_coords(doc, row, column) {
1078+ let pos = match view.pos_at_screen_coords(doc, row, column, true) {
12081079 Some(pos) => pos,
12091080 None => return EventResult::Ignored(None),
12101081 };
@@ -1268,8 +1139,9 @@ impl EditorView {
12681139 cxt.editor.focus(view_id);
12691140
12701141 let (view, doc) = current!(cxt.editor);
1271− let line = coords.row + view.offset.row;
1272− if let Ok(pos) = doc.text().try_line_to_char(line) {
1142+ if let Some(pos) =
1143+ view.pos_at_visual_coords(doc, coords.row as u16, coords.col as u16, true)
1144+ {
12731145 doc.set_selection(view_id, Selection::point(pos));
12741146 if modifiers == KeyModifiers::ALT {
12751147 commands::MappableCommand::dap_edit_log.execute(cxt);