From 4dcf1fe66ba30a78edc054780d9b65c2f826530f Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Tue, 31 Jan 2023 18:03:19 +0100 Subject: rework positioning/rendering and enable softwrap/virtual text (#5420) * rework positioning/rendering, enables softwrap/virtual text This commit is a large rework of the core text positioning and rendering code in helix to remove the assumption that on-screen columns/lines correspond to text columns/lines. A generic `DocFormatter` is introduced that positions graphemes on and is used both for rendering and for movements/scrolling. Both virtual text support (inline, grapheme overlay and multi-line) and a capable softwrap implementation is included. fix picker highlight cleanup doc formatter, use word bondaries for wrapping make visual vertical movement a seperate commnad estimate line gutter width to improve performance cache cursor position cleanup and optimize doc formatter cleanup documentation fix typos Co-authored-by: Daniel Hines update documentation fix panic in last_visual_line funciton improve soft-wrap documentation add extend_visual_line_up/down commands fix non-visual vertical movement streamline virtual text highlighting, add softwrap indicator fix cursor position if softwrap is disabled improve documentation of text_annotations module avoid crashes if view anchor is out of bounds fix: consider horizontal offset when traslation char_idx -> vpos improve default configuration fix: mixed up horizontal and vertical offset reset view position after config reload apply suggestions from review disabled softwrap for very small screens to avoid endless spin fix wrap_indicator setting fix bar cursor disappearring on the EOF character add keybinding for linewise vertical movement fix: inconsistent gutter highlights improve virtual text API make scope idx lookup more ergonomic allow overlapping overlays correctly track char_pos for virtual text adjust configuration deprecate old position fucntions fix infinite loop in highlight lookup fix gutter style fix formatting document max-line-width interaction with softwrap change wrap-indicator example to use empty string fix: rare panic when view is in invalid state (bis) * Apply suggestions from code review Co-authored-by: Michael Davis * improve documentation for positoning functions * simplify tests * fix documentation of Grapheme::width * Apply suggestions from code review Co-authored-by: Michael Davis * add explicit drop invocation * Add explicit MoveFn type alias * add docuntation to Editor::cursor_cache * fix a few typos * explain use of allow(deprecated) * make gj and gk extend in select mode * remove unneded debug and TODO * mark tab_width_at #[inline] * add fast-path to move_vertically_visual in case softwrap is disabled * rename first_line to first_visual_line * simplify duplicate if/else --------- Co-authored-by: Michael Davis --- helix-core/src/doc_formatter/test.rs | 222 +++++++++++++++++++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 helix-core/src/doc_formatter/test.rs (limited to 'helix-core/src/doc_formatter/test.rs') diff --git a/helix-core/src/doc_formatter/test.rs b/helix-core/src/doc_formatter/test.rs new file mode 100644 index 00000000..e68b31fd --- /dev/null +++ b/helix-core/src/doc_formatter/test.rs @@ -0,0 +1,222 @@ +use std::rc::Rc; + +use crate::doc_formatter::{DocumentFormatter, TextFormat}; +use crate::text_annotations::{InlineAnnotation, Overlay, TextAnnotations}; + +impl TextFormat { + fn new_test(softwrap: bool) -> Self { + TextFormat { + soft_wrap: softwrap, + tab_width: 2, + max_wrap: 3, + max_indent_retain: 4, + wrap_indicator: ".".into(), + wrap_indicator_highlight: None, + // use a prime number to allow lining up too often with repeat + viewport_width: 17, + } + } +} + +impl<'t> DocumentFormatter<'t> { + fn collect_to_str(&mut self) -> String { + use std::fmt::Write; + let mut res = String::new(); + let viewport_width = self.text_fmt.viewport_width; + let mut line = 0; + + for (grapheme, pos) in self { + if pos.row != line { + line += 1; + assert_eq!(pos.row, line); + write!(res, "\n{}", ".".repeat(pos.col)).unwrap(); + assert!( + pos.col <= viewport_width as usize, + "softwrapped failed {}<={viewport_width}", + pos.col + ); + } + write!(res, "{}", grapheme.grapheme).unwrap(); + } + + res + } +} + +fn softwrap_text(text: &str) -> String { + DocumentFormatter::new_at_prev_checkpoint( + text.into(), + &TextFormat::new_test(true), + &TextAnnotations::default(), + 0, + ) + .0 + .collect_to_str() +} + +#[test] +fn basic_softwrap() { + assert_eq!( + softwrap_text(&"foo ".repeat(10)), + "foo foo foo foo \n.foo foo foo foo \n.foo foo " + ); + assert_eq!( + softwrap_text(&"fooo ".repeat(10)), + "fooo fooo fooo \n.fooo fooo fooo \n.fooo fooo fooo \n.fooo " + ); + + // check that we don't wrap unnecessarily + assert_eq!(softwrap_text("\t\txxxx1xxxx2xx\n"), " xxxx1xxxx2xx \n "); +} + +#[test] +fn softwrap_indentation() { + assert_eq!( + softwrap_text("\t\tfoo1 foo2 foo3 foo4 foo5 foo6\n"), + " foo1 foo2 \n.....foo3 foo4 \n.....foo5 foo6 \n " + ); + assert_eq!( + softwrap_text("\t\t\tfoo1 foo2 foo3 foo4 foo5 foo6\n"), + " foo1 foo2 \n.foo3 foo4 foo5 \n.foo6 \n " + ); +} + +#[test] +fn long_word_softwrap() { + assert_eq!( + softwrap_text("\t\txxxx1xxxx2xxxx3xxxx4xxxx5xxxx6xxxx7xxxx8xxxx9xxx\n"), + " xxxx1xxxx2xxx\n.....x3xxxx4xxxx5\n.....xxxx6xxxx7xx\n.....xx8xxxx9xxx \n " + ); + assert_eq!( + softwrap_text("xxxxxxxx1xxxx2xxx\n"), + "xxxxxxxx1xxxx2xxx\n. \n " + ); + assert_eq!( + softwrap_text("\t\txxxx1xxxx 2xxxx3xxxx4xxxx5xxxx6xxxx7xxxx8xxxx9xxx\n"), + " xxxx1xxxx \n.....2xxxx3xxxx4x\n.....xxx5xxxx6xxx\n.....x7xxxx8xxxx9\n.....xxx \n " + ); + assert_eq!( + softwrap_text("\t\txxxx1xxx 2xxxx3xxxx4xxxx5xxxx6xxxx7xxxx8xxxx9xxx\n"), + " xxxx1xxx 2xxx\n.....x3xxxx4xxxx5\n.....xxxx6xxxx7xx\n.....xx8xxxx9xxx \n " + ); +} + +fn overlay_text(text: &str, char_pos: usize, softwrap: bool, overlays: &[Overlay]) -> String { + DocumentFormatter::new_at_prev_checkpoint( + text.into(), + &TextFormat::new_test(softwrap), + TextAnnotations::default().add_overlay(overlays.into(), None), + char_pos, + ) + .0 + .collect_to_str() +} + +#[test] +fn overlay() { + assert_eq!( + overlay_text( + "foobar", + 0, + false, + &[ + Overlay { + char_idx: 0, + grapheme: "X".into(), + }, + Overlay { + char_idx: 2, + grapheme: "\t".into(), + }, + ] + ), + "Xo bar " + ); + assert_eq!( + overlay_text( + &"foo ".repeat(10), + 0, + true, + &[ + Overlay { + char_idx: 2, + grapheme: "\t".into(), + }, + Overlay { + char_idx: 5, + grapheme: "\t".into(), + }, + Overlay { + char_idx: 16, + grapheme: "X".into(), + }, + ] + ), + "fo f o foo \n.foo Xoo foo foo \n.foo foo foo " + ); +} + +fn annotate_text(text: &str, softwrap: bool, annotations: &[InlineAnnotation]) -> String { + DocumentFormatter::new_at_prev_checkpoint( + text.into(), + &TextFormat::new_test(softwrap), + TextAnnotations::default().add_inline_annotations(annotations.into(), None), + 0, + ) + .0 + .collect_to_str() +} + +#[test] +fn annotation() { + assert_eq!( + annotate_text( + "bar", + false, + &[InlineAnnotation { + char_idx: 0, + text: "foo".into(), + }] + ), + "foobar " + ); + assert_eq!( + annotate_text( + &"foo ".repeat(10), + true, + &[InlineAnnotation { + char_idx: 0, + text: "foo ".into(), + }] + ), + "foo foo foo foo \n.foo foo foo foo \n.foo foo foo " + ); +} +#[test] +fn annotation_and_overlay() { + assert_eq!( + DocumentFormatter::new_at_prev_checkpoint( + "bbar".into(), + &TextFormat::new_test(false), + TextAnnotations::default() + .add_inline_annotations( + Rc::new([InlineAnnotation { + char_idx: 0, + text: "fooo".into(), + }]), + None + ) + .add_overlay( + Rc::new([Overlay { + char_idx: 0, + grapheme: "\t".into(), + }]), + None + ), + 0, + ) + .0 + .collect_to_str(), + "fooo bar " + ); +} -- cgit v1.2.3-70-g09d2