Merge remote-tracking branch 'nikitarevenco/render-helix'
This commit is contained in:
commit
038fe9d331
6 changed files with 198 additions and 106 deletions
|
@ -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));
|
||||
|
|
|
@ -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)],
|
||||
|
|
|
@ -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()
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue