Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rework text rendering and integrate softwrap rendering #5008

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
478 changes: 478 additions & 0 deletions helix-core/src/doc_cursor.rs

Large diffs are not rendered by default.

144 changes: 144 additions & 0 deletions helix-core/src/doc_cursor/test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
use crate::doc_cursor::{CursorConfig, DocumentCursor};

const WRAP_INDENT: u16 = 1;
impl CursorConfig {
fn new_test(softwrap: bool) -> CursorConfig {
CursorConfig {
soft_wrap: softwrap,
tab_width: 2,
max_wrap: 3,
max_indent_retain: 4,
wrap_indent: WRAP_INDENT,
// use a prime number to allow linging up too often with repear
viewport_width: 17,
}
}
}

impl<'t> DocumentCursor<'t, (), ()> {
fn new_test(text: &'t str, char_pos: usize, softwrap: bool) -> Self {
Self::new_at_prev_line(text.into(), CursorConfig::new_test(softwrap), char_pos, ())
}

fn collect_to_str(&mut self, res: &mut String) {
use std::fmt::Write;
let wrap_indent = self.config.wrap_indent;
let viewport_width = self.config.viewport_width;
let mut line_width = 0;

while let Some(mut word) = self.advance() {
let mut word_width_check = 0;
let word_width = word.visual_width;
for grapheme in word.consume_graphemes(self) {
word_width_check += grapheme.width() as usize;
write!(res, "{}", grapheme.grapheme).unwrap();
}
assert_eq!(word_width, word_width_check);
line_width += word.visual_width;

if let Some(line_break) = word.terminating_linebreak {
assert!(
line_width <= viewport_width as usize,
"softwrapped failed {line_width}<={viewport_width}"
);
res.push('\n');
if line_break.is_softwrap {
for i in 0..line_break.indent {
if i < wrap_indent {
res.push('.');
} else {
res.push(' ')
}
}
} else {
assert_eq!(line_break.indent, 0);
}
line_width = line_break.indent as usize;
}
}

for grapheme in self.finish().consume_graphemes(self) {
write!(res, "{}", grapheme.grapheme).unwrap();
}
assert!(
line_width <= viewport_width as usize,
"softwrapped failed {line_width}<={viewport_width}"
);
}
}

fn softwrap_text(text: &str, char_pos: usize) -> String {
let mut cursor = DocumentCursor::new_test(text, char_pos, true);
let mut res = String::new();
for i in 0..cursor.visual_pos().col {
if i < WRAP_INDENT as usize {
res.push('.');
} else {
res.push(' ')
}
}
cursor.collect_to_str(&mut res);
res
}

#[test]
fn basic_softwrap() {
assert_eq!(
softwrap_text(&"foo ".repeat(10), 0),
"foo foo foo foo \n.foo foo foo foo \n.foo foo "
);
assert_eq!(
softwrap_text(&"fooo ".repeat(10), 0),
"fooo fooo fooo \n.fooo fooo fooo \n.fooo fooo fooo \n.fooo "
);

// check that we don't wrap unecessarly
assert_eq!(
softwrap_text("\t\txxxx1xxxx2xx\n", 0),
" xxxx1xxxx2xx \n"
);
}

#[test]
fn softwrap_indentation() {
assert_eq!(
softwrap_text("\t\tfoo1 foo2 foo3 foo4 foo5 foo6\n", 0),
" foo1 foo2 \n. foo3 foo4 \n. foo5 foo6 \n"
);
assert_eq!(
softwrap_text("\t\t\tfoo1 foo2 foo3 foo4 foo5 foo6\n", 0),
" foo1 foo2 \n.foo3 foo4 foo5 \n.foo6 \n"
);
}

#[test]
fn long_word_softwrap() {
assert_eq!(
softwrap_text("\t\txxxx1xxxx2xxxx3xxxx4xxxx5xxxx6xxxx7xxxx8xxxx9xxx\n", 0),
" xxxx1xxxx2xxx\n. x3xxxx4xxxx5\n. xxxx6xxxx7xx\n. xx8xxxx9xxx \n"
);
assert_eq!(
softwrap_text("xxxxxxxx1xxxx2xxx\n", 0),
"xxxxxxxx1xxxx2xxx\n. \n"
);
assert_eq!(
softwrap_text("\t\txxxx1xxxx 2xxxx3xxxx4xxxx5xxxx6xxxx7xxxx8xxxx9xxx\n", 0),
" xxxx1xxxx \n. 2xxxx3xxxx4x\n. xxx5xxxx6xxx\n. x7xxxx8xxxx9\n. xxx \n"
);
assert_eq!(
softwrap_text("\t\txxxx1xxx 2xxxx3xxxx4xxxx5xxxx6xxxx7xxxx8xxxx9xxx\n", 0),
" xxxx1xxx 2xxx\n. x3xxxx4xxxx5\n. xxxx6xxxx7xx\n. xx8xxxx9xxx \n"
);
}

#[test]
fn softwrap_checkpoint() {
assert_eq!(
softwrap_text(&"foo ".repeat(10), 4),
"foo foo foo foo \n.foo foo foo foo \n.foo foo "
);
let text = "foo ".repeat(10);
assert_eq!(softwrap_text(&text, 18), ".foo foo foo foo \n.foo foo ");
println!("{}", &text[32..]);
assert_eq!(softwrap_text(&"foo ".repeat(10), 32), ".foo foo ");
}
Empty file added helix-core/src/doc_formatter.rs
Empty file.
83 changes: 82 additions & 1 deletion helix-core/src/graphemes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,76 @@ use ropey::{iter::Chunks, str_utils::byte_to_char_idx, RopeSlice};
use unicode_segmentation::{GraphemeCursor, GraphemeIncomplete};
use unicode_width::UnicodeWidthStr;

use std::fmt;
use std::borrow::Cow;
use std::fmt::{self, Display};

#[derive(Debug, Clone)]
/// A preprossed Grapheme that is ready for rendering
pub enum Grapheme<'a> {
Space,
Newline,
Nbsp,
Tab { width: u16 },
Other { raw: Cow<'a, str>, width: u16 },
}

impl<'a> Grapheme<'a> {
pub fn new(raw: Cow<'a, str>, visual_x: usize, tab_width: u16) -> Grapheme<'a> {
match &*raw {
"\t" => {
let width = tab_width - (visual_x % tab_width as usize) as u16;
Grapheme::Tab { width }
}
" " => Grapheme::Space,
"\u{00A0}" => Grapheme::Nbsp,
_ => Grapheme::Other {
width: grapheme_width(&*raw) as u16,
raw,
},
}
}

pub fn change_position(&mut self, visual_x: usize, tab_width: u16) {
if let Grapheme::Tab { width } = self {
*width = tab_width - (visual_x % tab_width as usize) as u16
}
}

/// Returns the approximate visual width of this grapheme,
/// This serves as a lower bound for the width for use during soft wrapping.
/// The actual displayed witdth might be position dependent and larger (primarly tabs)
pub fn width(&self) -> u16 {
match *self {
Grapheme::Other { width, .. } | Grapheme::Tab { width } => width,
_ => 1,
}
}

pub fn is_whitespace(&self) -> bool {
!matches!(&self, Grapheme::Other { .. })
}

pub fn is_breaking_space(&self) -> bool {
!matches!(&self, Grapheme::Other { .. } | Grapheme::Nbsp)
}
}

impl Display for Grapheme<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Grapheme::Space | Grapheme::Newline | Grapheme::Nbsp => write!(f, " "),
Grapheme::Tab { width } => {
for _ in 0..width {
write!(f, " ")?;
}
Ok(())
}
Grapheme::Other { ref raw, .. } => {
write!(f, "{raw}")
}
}
}
}

#[must_use]
pub fn grapheme_width(g: &str) -> usize {
Expand Down Expand Up @@ -300,6 +369,18 @@ impl<'a> RopeGraphemes<'a> {
cursor: GraphemeCursor::new(0, slice.len_bytes(), true),
}
}

/// Advances to `byte_pos` if it is at a grapheme boundrary
/// otherwise advances to the next grapheme boundrary after byte
pub fn advance_to(&mut self, byte_pos: usize) {
while byte_pos > self.byte_pos() {
self.next();
}
}

pub fn byte_pos(&self) -> usize {
self.cursor.cur_cursor()
}
}

impl<'a> Iterator for RopeGraphemes<'a> {
Expand Down
5 changes: 4 additions & 1 deletion helix-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ pub mod comment;
pub mod config;
pub mod diagnostic;
pub mod diff;
pub mod doc_cursor;
pub mod doc_formatter;
pub mod graphemes;
pub mod history;
pub mod increment;
Expand Down Expand Up @@ -95,7 +97,8 @@ pub use {regex, tree_sitter};

pub use graphemes::RopeGraphemes;
pub use position::{
coords_at_pos, pos_at_coords, pos_at_visual_coords, visual_coords_at_pos, Position,
coords_at_pos, pos_at_coords, pos_at_visual_coords, pos_at_visual_coords_2,
visual_coords_at_pos, visual_coords_at_pos_2, Position,
};
pub use selection::{Range, Selection};
pub use smallvec::{smallvec, SmallVec};
Expand Down
85 changes: 85 additions & 0 deletions helix-core/src/position.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::borrow::Cow;

use crate::{
chars::char_is_line_ending,
doc_cursor::{CursorConfig, DocumentCursor},
graphemes::{ensure_grapheme_boundary_prev, grapheme_width, RopeGraphemes},
line_ending::line_end_char_index,
RopeSlice,
Expand Down Expand Up @@ -93,6 +94,46 @@ pub fn visual_coords_at_pos(text: RopeSlice, pos: usize, tab_width: usize) -> Po
Position::new(line, col)
}

/// Convert a character index to (line, column) coordinates visually.
///
/// Takes \t, double-width characters (CJK) into account as well as text
/// not in the document in the future.
/// See [`coords_at_pos`] for an "objective" one.
pub fn visual_coords_at_pos_2(
text: RopeSlice,
anchor: usize,
pos: usize,
config: CursorConfig,
) -> Position {
// TODO consider inline annotations
let mut cursor: DocumentCursor<(), _> =
DocumentCursor::new_at_prev_line(text, config, anchor, ());
let mut visual_pos = cursor.visual_pos();
let mut doc_char_idx = cursor.doc_char_idx();
let mut word = loop {
if let Some(mut word) = cursor.advance() {
if cursor.doc_char_idx() >= pos {
break word;
} else {
word.consume_graphemes(&mut cursor);
visual_pos = cursor.visual_pos();
doc_char_idx = cursor.doc_char_idx();
}
} else {
break cursor.finish();
}
};

for grapheme in word.consume_graphemes(&mut cursor) {
if doc_char_idx + grapheme.doc_chars as usize > pos {
break;
}
doc_char_idx += grapheme.doc_chars as usize;
visual_pos.col += grapheme.width() as usize;
}
visual_pos
}

/// Convert (line, column) coordinates to a character index.
///
/// If the `line` coordinate is beyond the end of the file, the EOF
Expand Down Expand Up @@ -169,6 +210,50 @@ pub fn pos_at_visual_coords(text: RopeSlice, coords: Position, tab_width: usize)
line_start + col_char_offset
}

/// Convert a character index to (line, column) coordinates visually.
///
/// Takes \t, double-width characters (CJK) into account as well as text
/// not in the document in the future.
/// See [`coords_at_pos`] for an "objective" one.
pub fn pos_at_visual_coords_2(
text: RopeSlice,
anchor: usize,
cords: Position,
config: CursorConfig,
) -> usize {
// TODO consider inline annotations
let mut cursor: DocumentCursor<(), _> =
DocumentCursor::new_at_prev_line(text, config, anchor, ());
let mut visual_pos = cursor.visual_pos();
let mut doc_char_idx = cursor.doc_char_idx();
let mut word = loop {
if let Some(mut word) = cursor.advance() {
if visual_pos.row == cords.row {
if visual_pos.col + word.visual_width > cords.col {
break word;
} else if word.terminating_linebreak.is_some() {
word.consume_graphemes(&mut cursor);
return cursor.doc_char_idx();
}
}
word.consume_graphemes(&mut cursor);
visual_pos = cursor.visual_pos();
doc_char_idx = cursor.doc_char_idx();
} else {
break cursor.finish();
}
};

for grapheme in word.consume_graphemes(&mut cursor) {
if visual_pos.col + grapheme.width() as usize > cords.col {
break;
}
doc_char_idx += grapheme.doc_chars as usize;
visual_pos.col += grapheme.width() as usize;
}
doc_char_idx
}

#[cfg(test)]
mod test {
use super::*;
Expand Down
Loading