diff --git a/helix-core/src/movement.rs b/helix-core/src/movement.rs index e446d8cc4..3732c84eb 100644 --- a/helix-core/src/movement.rs +++ b/helix-core/src/movement.rs @@ -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)); diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs index 1db2d619e..2e08cfbe8 100644 --- a/helix-core/src/selection.rs +++ b/helix-core/src/selection.rs @@ -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)], diff --git a/helix-core/src/test.rs b/helix-core/src/test.rs index 71c71cce2..6c7199f29 100644 --- a/helix-core/src/test.rs +++ b/helix-core/src/test.rs @@ -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() ); } diff --git a/helix-core/src/textobject.rs b/helix-core/src/textobject.rs index 7576b3a78..c65d0be11 100644 --- a/helix-core/src/textobject.rs +++ b/helix-core/src/textobject.rs @@ -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)); diff --git a/helix-term/src/ui/markdown.rs b/helix-term/src/ui/markdown.rs index fe581b5ad..118dafdee 100644 --- a/helix-term/src/ui/markdown.rs +++ b/helix-term/src/ui/markdown.rs @@ -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> = - 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> = + 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); + } } } } diff --git a/helix-term/tests/test/helpers.rs b/helix-term/tests/test/helpers.rs index ef910852c..ee438ac96 100644 --- a/helix-term/tests/test/helpers.rs +++ b/helix-term/tests/test/helpers.rs @@ -82,8 +82,10 @@ where V: Into, { 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>(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 }