Merge remote-tracking branch 'nikitarevenco/render-helix'

This commit is contained in:
Kalle Carlbark 2025-02-25 22:17:24 +01:00
commit 038fe9d331
6 changed files with 198 additions and 106 deletions

View file

@ -2050,7 +2050,7 @@ mod test {
];
for (before, expected) in tests {
let (s, selection) = crate::test::print(before);
let (s, selection) = crate::test::parse_selection_string(before).unwrap();
let text = Rope::from(s.as_str());
let selection =
selection.transform(|r| move_prev_paragraph(text.slice(..), r, 1, Movement::Move));
@ -2073,7 +2073,7 @@ mod test {
];
for (before, expected) in tests {
let (s, selection) = crate::test::print(before);
let (s, selection) = crate::test::parse_selection_string(before).unwrap();
let text = Rope::from(s.as_str());
let selection =
selection.transform(|r| move_prev_paragraph(text.slice(..), r, 2, Movement::Move));
@ -2096,7 +2096,7 @@ mod test {
];
for (before, expected) in tests {
let (s, selection) = crate::test::print(before);
let (s, selection) = crate::test::parse_selection_string(before).unwrap();
let text = Rope::from(s.as_str());
let selection = selection
.transform(|r| move_prev_paragraph(text.slice(..), r, 1, Movement::Extend));
@ -2138,7 +2138,7 @@ mod test {
];
for (before, expected) in tests {
let (s, selection) = crate::test::print(before);
let (s, selection) = crate::test::parse_selection_string(before).unwrap();
let text = Rope::from(s.as_str());
let selection =
selection.transform(|r| move_next_paragraph(text.slice(..), r, 1, Movement::Move));
@ -2161,7 +2161,7 @@ mod test {
];
for (before, expected) in tests {
let (s, selection) = crate::test::print(before);
let (s, selection) = crate::test::parse_selection_string(before).unwrap();
let text = Rope::from(s.as_str());
let selection =
selection.transform(|r| move_next_paragraph(text.slice(..), r, 2, Movement::Move));
@ -2184,7 +2184,7 @@ mod test {
];
for (before, expected) in tests {
let (s, selection) = crate::test::print(before);
let (s, selection) = crate::test::parse_selection_string(before).unwrap();
let text = Rope::from(s.as_str());
let selection = selection
.transform(|r| move_next_paragraph(text.slice(..), r, 1, Movement::Extend));

View file

@ -1202,7 +1202,7 @@ mod test {
#[test]
fn selection_line_ranges() {
let (text, selection) = crate::test::print(
let (text, selection) = crate::test::parse_selection_string(
r#" L0
#[|these]# line #(|ranges)# are #(|merged)# L1
L2
@ -1218,7 +1218,8 @@ mod test {
adjacent #(|ranges)# L12
are merged #(|the same way)# L13
"#,
);
)
.unwrap();
let rope = Rope::from_str(&text);
assert_eq!(
vec![(1, 1), (3, 3), (5, 6), (8, 10), (12, 13)],

View file

@ -5,7 +5,14 @@ use smallvec::SmallVec;
use std::cmp::Reverse;
use unicode_segmentation::UnicodeSegmentation;
/// Convert annotated test string to test string and selection.
#[derive(Debug)]
pub enum ParseSelectionError {
MoreThanOnePrimary(String),
MissingClosingPair(String),
MissingPrimary(String),
}
/// Convert string annotated with selections to string and selection.
///
/// `#[|` for primary selection with head before anchor followed by `]#`.
/// `#(|` for secondary selection with head before anchor followed by `)#`.
@ -19,21 +26,15 @@ use unicode_segmentation::UnicodeSegmentation;
/// # Examples
///
/// ```
/// use helix_core::{Range, Selection, test::print};
/// use helix_core::{Range, Selection, test::parse_selection_string};
/// use smallvec::smallvec;
///
/// assert_eq!(
/// print("#[a|]#b#(|c)#"),
/// parse_selection_string("#[a|]#b#(|c)#").unwrap(),
/// ("abc".to_owned(), Selection::new(smallvec![Range::new(0, 1), Range::new(3, 2)], 0))
/// );
/// ```
///
/// # Panics
///
/// Panics when missing primary or appeared more than once.
/// Panics when missing head or anchor.
/// Panics when head come after head or anchor come after anchor.
pub fn print(s: &str) -> (String, Selection) {
pub fn parse_selection_string(s: &str) -> Result<(String, Selection), ParseSelectionError> {
let mut primary_idx = None;
let mut ranges = SmallVec::new();
let mut iter = UnicodeSegmentation::graphemes(s, true).peekable();
@ -59,7 +60,10 @@ pub fn print(s: &str) -> (String, Selection) {
};
if is_primary && primary_idx.is_some() {
panic!("primary `#[` already appeared {:?} {:?}", left, s);
return Err(ParseSelectionError::MoreThanOnePrimary(format!(
"Can only have 1 primary selection: {:?} {:?}",
left, s
)));
}
let head_at_beg = iter.next_if_eq(&"|").is_some();
@ -116,19 +120,30 @@ pub fn print(s: &str) -> (String, Selection) {
}
if head_at_beg {
panic!("missing end `{}#` {:?} {:?}", close_pair, left, s);
return Err(ParseSelectionError::MissingClosingPair(format!(
"Missing end `{}#`: {:?} {:?}",
close_pair, left, s
)));
} else {
panic!("missing end `|{}#` {:?} {:?}", close_pair, left, s);
return Err(ParseSelectionError::MissingClosingPair(format!(
"Missing end `|{}#`: {:?} {:?}",
close_pair, left, s
)));
}
}
let primary = match primary_idx {
Some(i) => i,
None => panic!("missing primary `#[|]#` {:?}", s),
None => {
return Err(ParseSelectionError::MissingPrimary(format!(
"Missing primary `#[|]#:` {:?}",
s
)));
}
};
let selection = Selection::new(ranges, primary);
(left, selection)
Ok((left, selection))
}
/// Convert test string and selection to annotated test string.
@ -187,27 +202,27 @@ mod test {
fn print_single() {
assert_eq!(
(String::from("hello"), Selection::single(1, 0)),
print("#[|h]#ello")
parse_selection_string("#[|h]#ello").unwrap()
);
assert_eq!(
(String::from("hello"), Selection::single(0, 1)),
print("#[h|]#ello")
parse_selection_string("#[h|]#ello").unwrap()
);
assert_eq!(
(String::from("hello"), Selection::single(4, 0)),
print("#[|hell]#o")
parse_selection_string("#[|hell]#o").unwrap()
);
assert_eq!(
(String::from("hello"), Selection::single(0, 4)),
print("#[hell|]#o")
parse_selection_string("#[hell|]#o").unwrap()
);
assert_eq!(
(String::from("hello"), Selection::single(5, 0)),
print("#[|hello]#")
parse_selection_string("#[|hello]#").unwrap()
);
assert_eq!(
(String::from("hello"), Selection::single(0, 5)),
print("#[hello|]#")
parse_selection_string("#[hello|]#").unwrap()
);
}
@ -221,7 +236,7 @@ mod test {
0
)
),
print("#[|h]#ell#(|o)#")
parse_selection_string("#[|h]#ell#(|o)#").unwrap()
);
assert_eq!(
(
@ -231,7 +246,7 @@ mod test {
0
)
),
print("#[h|]#ell#(o|)#")
parse_selection_string("#[h|]#ell#(o|)#").unwrap()
);
assert_eq!(
(
@ -241,7 +256,7 @@ mod test {
0
)
),
print("#[|he]#l#(|lo)#")
parse_selection_string("#[|he]#l#(|lo)#").unwrap()
);
assert_eq!(
(
@ -255,7 +270,7 @@ mod test {
0
)
),
print("hello#[|\r\n]#hello#(|\r\n)#hello#(|\r\n)#")
parse_selection_string("hello#[|\r\n]#hello#(|\r\n)#hello#(|\r\n)#").unwrap()
);
}
@ -263,23 +278,23 @@ mod test {
fn print_multi_byte_code_point() {
assert_eq!(
(String::from("„“"), Selection::single(1, 0)),
print("#[|„]#“")
parse_selection_string("#[|„]#“").unwrap()
);
assert_eq!(
(String::from("„“"), Selection::single(2, 1)),
print("„#[|“]#")
parse_selection_string("„#[|“]#").unwrap()
);
assert_eq!(
(String::from("„“"), Selection::single(0, 1)),
print("#[„|]#“")
parse_selection_string("#[„|]#“").unwrap()
);
assert_eq!(
(String::from("„“"), Selection::single(1, 2)),
print("„#[“|]#")
parse_selection_string("„#[“|]#").unwrap()
);
assert_eq!(
(String::from("they said „hello“"), Selection::single(11, 10)),
print("they said #[|„]#hello“")
parse_selection_string("they said #[|„]#hello“").unwrap()
);
}
@ -290,7 +305,7 @@ mod test {
String::from("hello 👨‍👩‍👧‍👦 goodbye"),
Selection::single(13, 6)
),
print("hello #[|👨‍👩‍👧‍👦]# goodbye")
parse_selection_string("hello #[|👨‍👩‍👧‍👦]# goodbye").unwrap()
);
}

View file

@ -435,7 +435,7 @@ mod test {
];
for (before, expected) in tests {
let (s, selection) = crate::test::print(before);
let (s, selection) = crate::test::parse_selection_string(before).unwrap();
let text = Rope::from(s.as_str());
let selection = selection
.transform(|r| textobject_paragraph(text.slice(..), r, TextObject::Inside, 1));
@ -458,7 +458,7 @@ mod test {
];
for (before, expected) in tests {
let (s, selection) = crate::test::print(before);
let (s, selection) = crate::test::parse_selection_string(before).unwrap();
let text = Rope::from(s.as_str());
let selection = selection
.transform(|r| textobject_paragraph(text.slice(..), r, TextObject::Inside, 2));
@ -489,7 +489,7 @@ mod test {
];
for (before, expected) in tests {
let (s, selection) = crate::test::print(before);
let (s, selection) = crate::test::parse_selection_string(before).unwrap();
let text = Rope::from(s.as_str());
let selection = selection
.transform(|r| textobject_paragraph(text.slice(..), r, TextObject::Around, 1));

View file

@ -5,12 +5,13 @@ use tui::{
text::{Span, Spans, Text},
};
use std::sync::Arc;
use std::{collections::HashSet, sync::Arc};
use pulldown_cmark::{CodeBlockKind, Event, HeadingLevel, Options, Parser, Tag, TagEnd};
use helix_core::{
syntax::{self, HighlightEvent, InjectionLanguageMarker, Syntax},
test::parse_selection_string,
RopeSlice,
};
use helix_view::{
@ -39,74 +40,147 @@ pub fn highlighted_code_block<'a>(
let mut lines = Vec::new();
let get_theme = |key: &str| -> Style { theme.map(|t| t.get(key)).unwrap_or_default() };
let text_style = get_theme(Markdown::TEXT_STYLE);
let code_style = get_theme(Markdown::BLOCK_STYLE);
let theme = match theme {
Some(t) => t,
None => return styled_multiline_text(text, code_style),
};
let ropeslice = RopeSlice::from(text);
let syntax = config_loader
.load()
.language_configuration_for_injection_string(&InjectionLanguageMarker::Name(
language.into(),
))
.and_then(|config| config.highlight_config(theme.scopes()))
.and_then(|config| Syntax::new(ropeslice, config, Arc::clone(&config_loader)));
let syntax = match syntax {
Some(s) => s,
None => return styled_multiline_text(text, code_style),
};
let highlight_iter = syntax
.highlight_iter(ropeslice, None, None)
.map(|e| e.unwrap());
let highlight_iter: Box<dyn Iterator<Item = HighlightEvent>> =
if let Some(spans) = additional_highlight_spans {
Box::new(helix_core::syntax::merge(highlight_iter, spans))
} else {
Box::new(highlight_iter)
// Apply custom rendering rules to multicursor code blocks.
// These render selections as if in the real editor.
if language == "multicursor" {
let (text, selections) = match parse_selection_string(text) {
Ok(value) => value,
Err(err) => {
return styled_multiline_text(
&format!("Could not parse selection: {err:#?}"),
get_theme("error"),
)
}
};
let mut highlights = Vec::new();
for event in highlight_iter {
match event {
HighlightEvent::HighlightStart(span) => {
highlights.push(span);
let style_cursor = get_theme("ui.cursor");
let style_cursor_primary = get_theme("ui.cursor.primary");
let style_selection = get_theme("ui.selection");
let style_selection_primary = get_theme("ui.selection.primary");
let style_text = get_theme("ui.text");
let mut selection_positions = HashSet::new();
let mut cursors_positions = HashSet::new();
let primary = selections.primary();
for range in selections.iter() {
selection_positions.extend(range.from()..range.to());
cursors_positions.insert(if range.head > range.anchor {
range.head.saturating_sub(1)
} else {
range.head
});
}
let mut chars = text.chars().enumerate().peekable();
while let Some((idx, ch)) = chars.next() {
// handle \r\n line break.
if ch == '\r' && chars.peek().is_some_and(|(_, ch)| *ch == '\n') {
// We're on a line break. We already have the
// code to handle newlines in place, so we can just
// handle the newline on the next iteration
continue;
}
HighlightEvent::HighlightEnd => {
highlights.pop();
}
HighlightEvent::Source { start, end } => {
let style = highlights
.iter()
.fold(text_style, |acc, span| acc.patch(theme.highlight(span.0)));
let mut slice = &text[start..end];
// TODO: do we need to handle all unicode line endings
// here, or is just '\n' okay?
while let Some(end) = slice.find('\n') {
// emit span up to newline
let text = &slice[..end];
let text = text.replace('\t', " "); // replace tabs
let span = Span::styled(text, style);
spans.push(span);
let is_cursor = cursors_positions.contains(&idx);
let is_selection = selection_positions.contains(&idx);
let is_primary = idx <= primary.to() && idx >= primary.from();
// truncate slice to after newline
slice = &slice[end + 1..];
// make a new line
let spans = std::mem::take(&mut spans);
lines.push(Spans::from(spans));
let style = if is_cursor {
if is_primary {
style_cursor_primary
} else {
style_cursor
}
} else if is_selection {
if is_primary {
style_selection_primary
} else {
style_selection
}
} else {
style_text
};
// if there's anything left, emit it too
if !slice.is_empty() {
let span = Span::styled(slice.replace('\t', " "), style);
spans.push(span);
if ch == '\n' {
lines.push(Spans::from(spans));
spans = vec![];
} else {
spans.push(Span::styled(ch.to_string(), style));
}
}
} else {
let text_style = get_theme(Markdown::TEXT_STYLE);
let code_style = get_theme(Markdown::BLOCK_STYLE);
let theme = match theme {
Some(t) => t,
None => return styled_multiline_text(text, code_style),
};
let ropeslice = RopeSlice::from(text);
let syntax = config_loader
.load()
.language_configuration_for_injection_string(&InjectionLanguageMarker::Name(
language.into(),
))
.and_then(|config| config.highlight_config(theme.scopes()))
.and_then(|config| Syntax::new(ropeslice, config, Arc::clone(&config_loader)));
let syntax = match syntax {
Some(s) => s,
None => return styled_multiline_text(text, code_style),
};
let highlight_iter = syntax
.highlight_iter(ropeslice, None, None)
.map(|e| e.unwrap());
let highlight_iter: Box<dyn Iterator<Item = HighlightEvent>> =
if let Some(spans) = additional_highlight_spans {
Box::new(helix_core::syntax::merge(highlight_iter, spans))
} else {
Box::new(highlight_iter)
};
let mut highlights = Vec::new();
for event in highlight_iter {
match event {
HighlightEvent::HighlightStart(span) => {
highlights.push(span);
}
HighlightEvent::HighlightEnd => {
highlights.pop();
}
HighlightEvent::Source { start, end } => {
let style = highlights
.iter()
.fold(text_style, |acc, span| acc.patch(theme.highlight(span.0)));
let mut slice = &text[start..end];
// TODO: do we need to handle all unicode line endings
// here, or is just '\n' okay?
while let Some(end) = slice.find('\n') {
// emit span up to newline
let text = &slice[..end];
let text = text.replace('\t', " "); // replace tabs
let span = Span::styled(text, style);
spans.push(span);
// truncate slice to after newline
slice = &slice[end + 1..];
// make a new line
let spans = std::mem::take(&mut spans);
lines.push(Spans::from(spans));
}
// if there's anything left, emit it too
if !slice.is_empty() {
let span = Span::styled(slice.replace('\t', " "), style);
spans.push(span);
}
}
}
}

View file

@ -82,8 +82,10 @@ where
V: Into<String>,
{
fn from((input, keys, output, line_feed_handling): (S, R, V, LineFeedHandling)) -> Self {
let (in_text, in_selection) = test::print(&line_feed_handling.apply(&input.into()));
let (out_text, out_selection) = test::print(&line_feed_handling.apply(&output.into()));
let (in_text, in_selection) =
test::parse_selection_string(&line_feed_handling.apply(&input.into())).unwrap();
let (out_text, out_selection) =
test::parse_selection_string(&line_feed_handling.apply(&output.into())).unwrap();
TestCase {
in_text,
@ -362,7 +364,7 @@ impl AppBuilder {
}
pub fn with_input_text<S: Into<String>>(mut self, input_text: S) -> Self {
self.input = Some(test::print(&input_text.into()));
self.input = Some(test::parse_selection_string(&input_text.into()).unwrap());
self
}