From eabd9c02e50430797974497d2b7580f829fe5b8a Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 3 Aug 2022 14:20:05 -0700 Subject: [PATCH] Update marked text helpers to use more distinctive characters for markers --- crates/editor/src/editor.rs | 404 ++++++++++-------- .../editor/src/highlight_matching_bracket.rs | 91 ++-- crates/editor/src/hover_popover.rs | 44 +- crates/editor/src/link_go_to_definition.rs | 166 +++---- crates/editor/src/mouse_context_menu.rs | 24 +- crates/editor/src/test.rs | 275 +++--------- crates/util/Cargo.toml | 9 +- crates/util/src/lib.rs | 2 +- crates/util/src/test/marked_text.rs | 122 +++++- 9 files changed, 534 insertions(+), 603 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 5211466e9493753b8c72aa7e3b438cfdbb80c881..c185464a4203dd3fa3bd5c422da2ffee22b29711 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -8050,33 +8050,33 @@ mod tests { cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); cx.set_state(indoc! {" - const a: |A = ( - (| - [const_function}(|), - so{m]et[h}ing_|else,| - )| - |);| - "}); + const a: ˇA = ( + (ˇ + «const_functionˇ»(ˇ), + so«mˇ»et«hˇ»ing_ˇelse,ˇ + )ˇ + ˇ);ˇ + "}); cx.update_editor(|e, cx| e.newline_below(&NewlineBelow, cx)); cx.assert_editor_state(indoc! {" const a: A = ( - | + ˇ ( - | + ˇ const_function(), - | - | + ˇ + ˇ something_else, - | - | - | - | + ˇ + ˇ + ˇ + ˇ ) - | + ˇ ); - | - | - "}); + ˇ + ˇ + "}); } #[gpui::test] @@ -8115,25 +8115,25 @@ mod tests { }); }); cx.set_state(indoc! {" - |ab|c - |🏀|🏀|efg - d| + ˇabˇc + ˇ🏀ˇ🏀ˇefg + dˇ "}); cx.update_editor(|e, cx| e.tab(&Tab, cx)); cx.assert_editor_state(indoc! {" - |ab |c - |🏀 |🏀 |efg - d | + ˇab ˇc + ˇ🏀 ˇ🏀 ˇefg + d ˇ "}); cx.set_state(indoc! {" a - [🏀}🏀[🏀}🏀[🏀} + «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ» "}); cx.update_editor(|e, cx| e.tab(&Tab, cx)); cx.assert_editor_state(indoc! {" a - [🏀}🏀[🏀}🏀[🏀} + «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ» "}); } @@ -8154,26 +8154,26 @@ mod tests { // a soft tab. cursors that are to the left of the suggested indent // auto-indent their line. cx.set_state(indoc! {" - | + ˇ const a: B = ( c( d( - | + ˇ ) - | - | ) + ˇ + ˇ ) ); "}); cx.update_editor(|e, cx| e.tab(&Tab, cx)); cx.assert_editor_state(indoc! {" - | + ˇ const a: B = ( c( d( - | + ˇ ) - | - |) + ˇ + ˇ) ); "}); @@ -8181,16 +8181,16 @@ mod tests { cx.set_state(indoc! {" const a: B = ( c( - | | - | ) + ˇ ˇ + ˇ ) ); "}); cx.update_editor(|e, cx| e.tab(&Tab, cx)); cx.assert_editor_state(indoc! {" const a: B = ( c( - | - |) + ˇ + ˇ) ); "}); } @@ -8200,58 +8200,68 @@ mod tests { let mut cx = EditorTestContext::new(cx).await; cx.set_state(indoc! {" - [one} [two} + «oneˇ» «twoˇ» three - four"}); + four + "}); cx.update_editor(|e, cx| e.tab(&Tab, cx)); cx.assert_editor_state(indoc! {" - [one} [two} + «oneˇ» «twoˇ» three - four"}); + four + "}); cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); cx.assert_editor_state(indoc! {" - [one} [two} + «oneˇ» «twoˇ» three - four"}); + four + "}); // select across line ending cx.set_state(indoc! {" one two - t[hree - } four"}); + t«hree + ˇ» four + "}); cx.update_editor(|e, cx| e.tab(&Tab, cx)); cx.assert_editor_state(indoc! {" one two - t[hree - } four"}); + t«hree + ˇ» four + "}); cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); cx.assert_editor_state(indoc! {" one two - t[hree - } four"}); + t«hree + ˇ» four + "}); // Ensure that indenting/outdenting works when the cursor is at column 0. cx.set_state(indoc! {" one two - |three - four"}); + ˇthree + four + "}); cx.update_editor(|e, cx| e.tab(&Tab, cx)); cx.assert_editor_state(indoc! {" one two - |three - four"}); + ˇthree + four + "}); cx.set_state(indoc! {" one two - | three - four"}); + ˇ three + four + "}); cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); cx.assert_editor_state(indoc! {" one two - |three - four"}); + ˇthree + four + "}); } #[gpui::test] @@ -8265,75 +8275,90 @@ mod tests { // select two ranges on one line cx.set_state(indoc! {" - [one} [two} + «oneˇ» «twoˇ» three - four"}); + four + "}); cx.update_editor(|e, cx| e.tab(&Tab, cx)); cx.assert_editor_state(indoc! {" - \t[one} [two} + \t«oneˇ» «twoˇ» three - four"}); + four + "}); cx.update_editor(|e, cx| e.tab(&Tab, cx)); cx.assert_editor_state(indoc! {" - \t\t[one} [two} + \t\t«oneˇ» «twoˇ» three - four"}); + four + "}); cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); cx.assert_editor_state(indoc! {" - \t[one} [two} + \t«oneˇ» «twoˇ» three - four"}); + four + "}); cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); cx.assert_editor_state(indoc! {" - [one} [two} + «oneˇ» «twoˇ» three - four"}); + four + "}); // select across a line ending cx.set_state(indoc! {" one two - t[hree - }four"}); + t«hree + ˇ»four + "}); cx.update_editor(|e, cx| e.tab(&Tab, cx)); cx.assert_editor_state(indoc! {" one two - \tt[hree - }four"}); + \tt«hree + ˇ»four + "}); cx.update_editor(|e, cx| e.tab(&Tab, cx)); cx.assert_editor_state(indoc! {" one two - \t\tt[hree - }four"}); + \t\tt«hree + ˇ»four + "}); cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); cx.assert_editor_state(indoc! {" one two - \tt[hree - }four"}); + \tt«hree + ˇ»four + "}); cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); cx.assert_editor_state(indoc! {" one two - t[hree - }four"}); + t«hree + ˇ»four + "}); // Ensure that indenting/outdenting works when the cursor is at column 0. cx.set_state(indoc! {" one two - |three - four"}); + ˇthree + four + "}); + cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); cx.assert_editor_state(indoc! {" one two - |three - four"}); + ˇthree + four + "}); cx.update_editor(|e, cx| e.tab(&Tab, cx)); cx.assert_editor_state(indoc! {" one two - \t|three - four"}); + \tˇthree + four + "}); cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); cx.assert_editor_state(indoc! {" one two - |three - four"}); + ˇthree + four + "}); } #[gpui::test] @@ -8412,10 +8437,10 @@ mod tests { select_ranges( &mut editor, indoc! {" - [a] = 1 + «aˇ» = 1 b = 2 - [const c:] usize = 3; + «const c:ˇ» usize = 3; "}, cx, ); @@ -8424,10 +8449,10 @@ mod tests { assert_text_with_selections( &mut editor, indoc! {" - [a] = 1 + «aˇ» = 1 b = 2 - [const c:] usize = 3; + «const c:ˇ» usize = 3; "}, cx, ); @@ -8435,10 +8460,10 @@ mod tests { assert_text_with_selections( &mut editor, indoc! {" - [a] = 1 + «aˇ» = 1 b = 2 - [const c:] usize = 3; + «const c:ˇ» usize = 3; "}, cx, ); @@ -8450,43 +8475,48 @@ mod tests { #[gpui::test] async fn test_backspace(cx: &mut gpui::TestAppContext) { let mut cx = EditorTestContext::new(cx).await; + // Basic backspace cx.set_state(indoc! {" - on|e two three - fou[r} five six - seven {eight nine - ]ten"}); + onˇe two three + fou«rˇ» five six + seven «ˇeight nine + »ten + "}); cx.update_editor(|e, cx| e.backspace(&Backspace, cx)); cx.assert_editor_state(indoc! {" - o|e two three - fou| five six - seven |ten"}); + oˇe two three + fouˇ five six + seven ˇten + "}); // Test backspace inside and around indents cx.set_state(indoc! {" zero - |one - |two - | | | three - | | four"}); + ˇone + ˇtwo + ˇ ˇ ˇ three + ˇ ˇ four + "}); cx.update_editor(|e, cx| e.backspace(&Backspace, cx)); cx.assert_editor_state(indoc! {" zero - |one - |two - | three| four"}); + ˇone + ˇtwo + ˇ threeˇ four + "}); // Test backspace with line_mode set to true cx.update_editor(|e, _| e.selections.line_mode = true); cx.set_state(indoc! {" - The |quick |brown + The ˇquick ˇbrown fox jumps over the lazy dog - |The qu[ick b}rown"}); + ˇThe qu«ick bˇ»rown"}); cx.update_editor(|e, cx| e.backspace(&Backspace, cx)); cx.assert_editor_state(indoc! {" - |fox jumps over - the lazy dog|"}); + ˇfox jumps over + the lazy dogˇ"}); } #[gpui::test] @@ -8494,25 +8524,27 @@ mod tests { let mut cx = EditorTestContext::new(cx).await; cx.set_state(indoc! {" - on|e two three - fou[r} five six - seven {eight nine - ]ten"}); + onˇe two three + fou«rˇ» five six + seven «ˇeight nine + »ten + "}); cx.update_editor(|e, cx| e.delete(&Delete, cx)); cx.assert_editor_state(indoc! {" - on| two three - fou| five six - seven |ten"}); + onˇ two three + fouˇ five six + seven ˇten + "}); // Test backspace with line_mode set to true cx.update_editor(|e, _| e.selections.line_mode = true); cx.set_state(indoc! {" - The |quick |brown - fox {jum]ps over + The ˇquick ˇbrown + fox «ˇjum»ps over the lazy dog - |The qu[ick b}rown"}); + ˇThe qu«ick bˇ»rown"}); cx.update_editor(|e, cx| e.backspace(&Backspace, cx)); - cx.assert_editor_state("|the lazy dog|"); + cx.assert_editor_state("ˇthe lazy dogˇ"); } #[gpui::test] @@ -8824,19 +8856,19 @@ mod tests { async fn test_clipboard(cx: &mut gpui::TestAppContext) { let mut cx = EditorTestContext::new(cx).await; - cx.set_state("[one✅ }two [three }four [five }six "); + cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six "); cx.update_editor(|e, cx| e.cut(&Cut, cx)); - cx.assert_editor_state("|two |four |six "); + cx.assert_editor_state("ˇtwo ˇfour ˇsix "); // Paste with three cursors. Each cursor pastes one slice of the clipboard text. - cx.set_state("two |four |six |"); + cx.set_state("two ˇfour ˇsix ˇ"); cx.update_editor(|e, cx| e.paste(&Paste, cx)); - cx.assert_editor_state("two one✅ |four three |six five |"); + cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ"); // Paste again but with only two cursors. Since the number of cursors doesn't // match the number of slices in the clipboard, the entire clipboard text // is pasted at each cursor. - cx.set_state("|two one✅ four three six five |"); + cx.set_state("ˇtwo one✅ four three six five ˇ"); cx.update_editor(|e, cx| { e.handle_input("( ", cx); e.paste(&Paste, cx); @@ -8845,37 +8877,37 @@ mod tests { cx.assert_editor_state(indoc! {" ( one✅ three - five ) |two one✅ four three six five ( one✅ + five ) ˇtwo one✅ four three six five ( one✅ three - five ) |"}); + five ) ˇ"}); // Cut with three selections, one of which is full-line. cx.set_state(indoc! {" - 1[2}3 - 4|567 - [8}9"}); + 1«2ˇ»3 + 4ˇ567 + «8ˇ»9"}); cx.update_editor(|e, cx| e.cut(&Cut, cx)); cx.assert_editor_state(indoc! {" - 1|3 - |9"}); + 1ˇ3 + ˇ9"}); // Paste with three selections, noticing how the copied selection that was full-line // gets inserted before the second cursor. cx.set_state(indoc! {" - 1|3 - 9| - [o}ne"}); + 1ˇ3 + 9ˇ + «oˇ»ne"}); cx.update_editor(|e, cx| e.paste(&Paste, cx)); cx.assert_editor_state(indoc! {" - 12|3 + 12ˇ3 4567 - 9| - 8|ne"}); + 9ˇ + 8ˇne"}); // Copy with a single cursor only, which writes the whole line into the clipboard. cx.set_state(indoc! {" The quick brown - fox ju|mps over + fox juˇmps over the lazy dog"}); cx.update_editor(|e, cx| e.copy(&Copy, cx)); cx.cx.assert_clipboard_content(Some("fox jumps over\n")); @@ -8883,17 +8915,17 @@ mod tests { // Paste with three selections, noticing how the copied full-line selection is inserted // before the empty selections but replaces the selection that is non-empty. cx.set_state(indoc! {" - T|he quick brown - [fo}x jumps over - t|he lazy dog"}); + Tˇhe quick brown + «foˇ»x jumps over + tˇhe lazy dog"}); cx.update_editor(|e, cx| e.paste(&Paste, cx)); cx.assert_editor_state(indoc! {" fox jumps over - T|he quick brown + Tˇhe quick brown fox jumps over - |x jumps over + ˇx jumps over fox jumps over - t|he lazy dog"}); + tˇhe lazy dog"}); } #[gpui::test] @@ -8909,17 +8941,17 @@ mod tests { cx.set_state(indoc! {" const a: B = ( c(), - [d( + «d( e, f - )} + )ˇ» ); "}); cx.update_editor(|e, cx| e.cut(&Cut, cx)); cx.assert_editor_state(indoc! {" const a: B = ( c(), - | + ˇ ); "}); @@ -8931,13 +8963,13 @@ mod tests { d( e, f - )| + )ˇ ); "}); // Paste it at a line with a lower indent level. cx.set_state(indoc! {" - | + ˇ const a: B = ( c(), ); @@ -8947,7 +8979,7 @@ mod tests { d( e, f - )| + )ˇ const a: B = ( c(), ); @@ -8957,17 +8989,17 @@ mod tests { cx.set_state(indoc! {" const a: B = ( c(), - [ d( + « d( e, f ) - }); + ˇ»); "}); cx.update_editor(|e, cx| e.cut(&Cut, cx)); cx.assert_editor_state(indoc! {" const a: B = ( c(), - |); + ˇ); "}); // Paste it at the same position. @@ -8979,7 +9011,7 @@ mod tests { e, f ) - |); + ˇ); "}); // Paste it at a line with a higher indent level. @@ -8988,7 +9020,7 @@ mod tests { c(), d( e, - f| + fˇ ) ); "}); @@ -9002,7 +9034,7 @@ mod tests { e, f ) - | + ˇ ) ); "}); @@ -10293,16 +10325,18 @@ mod tests { .await; cx.set_state(indoc! {" - one| + oneˇ two - three"}); + three + "}); cx.simulate_keystroke("."); handle_completion_request( &mut cx, indoc! {" one.|<> two - three"}, + three + "}, vec!["first_completion", "second_completion"], ) .await; @@ -10315,9 +10349,10 @@ mod tests { .unwrap() }); cx.assert_editor_state(indoc! {" - one.second_completion| + one.second_completionˇ two - three"}); + three + "}); handle_resolve_completion_request( &mut cx, @@ -10325,23 +10360,26 @@ mod tests { indoc! {" one.second_completion two - three<>"}, + three<> + "}, "\nadditional edit", )), ) .await; apply_additional_edits.await.unwrap(); cx.assert_editor_state(indoc! {" - one.second_completion| + one.second_completionˇ two three - additional edit"}); + additional edit + "}); cx.set_state(indoc! {" one.second_completion - two| - three| - additional edit"}); + twoˇ + threeˇ + additional edit + "}); cx.simulate_keystroke(" "); assert!(cx.editor(|e, _| e.context_menu.is_none())); cx.simulate_keystroke("s"); @@ -10349,16 +10387,18 @@ mod tests { cx.assert_editor_state(indoc! {" one.second_completion - two s| - three s| - additional edit"}); + two sˇ + three sˇ + additional edit + "}); handle_completion_request( &mut cx, indoc! {" one.second_completion two s three - additional edit"}, + additional edit + "}, vec!["fourth_completion", "fifth_completion", "sixth_completion"], ) .await; @@ -10373,7 +10413,8 @@ mod tests { one.second_completion two si three - additional edit"}, + additional edit + "}, vec!["fourth_completion", "fifth_completion", "sixth_completion"], ) .await; @@ -10387,9 +10428,10 @@ mod tests { }); cx.assert_editor_state(indoc! {" one.second_completion - two sixth_completion| - three sixth_completion| - additional edit"}); + two sixth_completionˇ + three sixth_completionˇ + additional edit + "}); handle_resolve_completion_request(&mut cx, None).await; apply_additional_edits.await.unwrap(); @@ -10399,13 +10441,13 @@ mod tests { settings.show_completions_on_input = false; }) }); - cx.set_state("editor|"); + cx.set_state("editorˇ"); cx.simulate_keystroke("."); assert!(cx.editor(|e, _| e.context_menu.is_none())); cx.simulate_keystroke("c"); cx.simulate_keystroke("l"); cx.simulate_keystroke("o"); - cx.assert_editor_state("editor.clo|"); + cx.assert_editor_state("editor.cloˇ"); assert!(cx.editor(|e, _| e.context_menu.is_none())); cx.update_editor(|editor, cx| { editor.show_completions(&ShowCompletions, cx); @@ -10418,7 +10460,7 @@ mod tests { .confirm_completion(&ConfirmCompletion::default(), cx) .unwrap() }); - cx.assert_editor_state("editor.close|"); + cx.assert_editor_state("editor.closeˇ"); handle_resolve_completion_request(&mut cx, None).await; apply_additional_edits.await.unwrap(); diff --git a/crates/editor/src/highlight_matching_bracket.rs b/crates/editor/src/highlight_matching_bracket.rs index 30bf1091f28ad4ca588e6f63494e297abaebd1e2..789393d70bd09f27d57031cbefb51af422184ba2 100644 --- a/crates/editor/src/highlight_matching_bracket.rs +++ b/crates/editor/src/highlight_matching_bracket.rs @@ -32,14 +32,11 @@ pub fn refresh_matching_bracket_highlights(editor: &mut Editor, cx: &mut ViewCon #[cfg(test)] mod tests { + use super::*; + use crate::test::EditorLspTestContext; use indoc::indoc; - use language::{BracketPair, Language, LanguageConfig}; - use crate::test::EditorLspTestContext; - - use super::*; - #[gpui::test] async fn test_matching_bracket_highlights(cx: &mut gpui::TestAppContext) { let mut cx = EditorLspTestContext::new( @@ -76,67 +73,61 @@ mod tests { .await; // positioning cursor inside bracket highlights both - cx.set_state_by( - vec!['|'.into()], - indoc! {r#" - pub fn test("Test |argument") { - another_test(1, 2, 3); - }"#}, - ); + cx.set_state(indoc! {r#" + pub fn test("Test ˇargument") { + another_test(1, 2, 3); + } + "#}); cx.assert_editor_background_highlights::(indoc! {r#" - pub fn test[(]"Test argument"[)] { - another_test(1, 2, 3); - }"#}); + pub fn test«(»"Test argument"«)» { + another_test(1, 2, 3); + } + "#}); - cx.set_state_by( - vec!['|'.into()], - indoc! {r#" - pub fn test("Test argument") { - another_test(1, |2, 3); - }"#}, - ); + cx.set_state(indoc! {r#" + pub fn test("Test argument") { + another_test(1, ˇ2, 3); + } + "#}); cx.assert_editor_background_highlights::(indoc! {r#" pub fn test("Test argument") { - another_test[(]1, 2, 3[)]; - }"#}); + another_test«(»1, 2, 3«)»; + } + "#}); - cx.set_state_by( - vec!['|'.into()], - indoc! {r#" - pub fn test("Test argument") { - another|_test(1, 2, 3); - }"#}, - ); + cx.set_state(indoc! {r#" + pub fn test("Test argument") { + anotherˇ_test(1, 2, 3); + } + "#}); cx.assert_editor_background_highlights::(indoc! {r#" - pub fn test("Test argument") [{] + pub fn test("Test argument") «{» another_test(1, 2, 3); - [}]"#}); + «}» + "#}); // positioning outside of brackets removes highlight - cx.set_state_by( - vec!['|'.into()], - indoc! {r#" - pub f|n test("Test argument") { - another_test(1, 2, 3); - }"#}, - ); + cx.set_state(indoc! {r#" + pub fˇn test("Test argument") { + another_test(1, 2, 3); + } + "#}); cx.assert_editor_background_highlights::(indoc! {r#" pub fn test("Test argument") { another_test(1, 2, 3); - }"#}); + } + "#}); // non empty selection dismisses highlight - // positioning outside of brackets removes highlight - cx.set_state_by( - vec![('<', '>').into()], - indoc! {r#" - pub fn test("Teument") { - another_test(1, 2, 3); - }"#}, - ); + cx.set_state(indoc! {r#" + pub fn test("Te«st argˇ»ument") { + another_test(1, 2, 3); + } + "#}); cx.assert_editor_background_highlights::(indoc! {r#" pub fn test("Test argument") { another_test(1, 2, 3); - }"#}); + } + "#}); } } diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 99669cb15d0a8f760c0f3d78fa94390e01a39e53..37a2a66e0562192eea28f0477ad3bcbe660dc180 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -439,11 +439,11 @@ mod tests { // Basic hover delays and then pops without moving the mouse cx.set_state(indoc! {" - fn |test() - println!();"}); + fn ˇtest() { println!(); } + "}); let hover_point = cx.display_point(indoc! {" - fn test() - print|ln!();"}); + fn test() { printˇln!(); } + "}); cx.update_editor(|editor, cx| { hover_at( @@ -458,16 +458,16 @@ mod tests { // After delay, hover should be visible. let symbol_range = cx.lsp_range(indoc! {" - fn test() - [println!]();"}); + fn test() { «println!»(); } + "}); let mut requests = cx.handle_request::(move |_, _, _| async move { Ok(Some(lsp::Hover { contents: lsp::HoverContents::Markup(lsp::MarkupContent { kind: lsp::MarkupKind::Markdown, value: indoc! {" - # Some basic docs - Some test documentation"} + # Some basic docs + Some test documentation"} .to_string(), }), range: Some(symbol_range), @@ -496,8 +496,8 @@ mod tests { // Mouse moved with no hover response dismisses let hover_point = cx.display_point(indoc! {" - fn te|st() - println!();"}); + fn teˇst() { println!(); } + "}); let mut request = cx .lsp .handle_request::(|_, _| async move { Ok(None) }); @@ -531,12 +531,12 @@ mod tests { // Hover with keyboard has no delay cx.set_state(indoc! {" - f|n test() - println!();"}); + fˇn test() { println!(); } + "}); cx.update_editor(|editor, cx| hover(editor, &Hover, cx)); let symbol_range = cx.lsp_range(indoc! {" - [fn] test() - println!();"}); + «fn» test() { println!(); } + "}); cx.handle_request::(move |_, _, _| async move { Ok(Some(lsp::Hover { contents: lsp::HoverContents::Markup(lsp::MarkupContent { @@ -584,13 +584,13 @@ mod tests { // Hover with just diagnostic, pops DiagnosticPopover immediately and then // info popover once request completes cx.set_state(indoc! {" - fn te|st() - println!();"}); + fn teˇst() { println!(); } + "}); // Send diagnostic to client let range = cx.text_anchor_range(indoc! {" - fn [test]() - println!();"}); + fn «test»() { println!(); } + "}); cx.update_buffer(|buffer, cx| { let snapshot = buffer.text_snapshot(); let set = DiagnosticSet::from_sorted_entries( @@ -616,15 +616,15 @@ mod tests { // Info Popover shows after request responded to let range = cx.lsp_range(indoc! {" - fn [test]() - println!();"}); + fn «test»() { println!(); } + "}); cx.handle_request::(move |_, _, _| async move { Ok(Some(lsp::Hover { contents: lsp::HoverContents::Markup(lsp::MarkupContent { kind: lsp::MarkupKind::Markdown, value: indoc! {" - # Some other basic docs - Some other test documentation"} + # Some other basic docs + Some other test documentation"} .to_string(), }), range: Some(range), diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index b57179c07dca5cf3362b44353fd6d9997014ddc6..cc4592b6d4702818e7948faa4dfeb9f1816f4fb2 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -405,20 +405,20 @@ mod tests { cx.set_state(indoc! {" struct A; - let v|ariable = A; + let vˇariable = A; "}); // Basic hold cmd+shift, expect highlight in region if response contains type definition let hover_point = cx.display_point(indoc! {" struct A; - let v|ariable = A; + let vˇariable = A; "}); let symbol_range = cx.lsp_range(indoc! {" struct A; - let [variable] = A; + let «variable» = A; "}); let target_range = cx.lsp_range(indoc! {" - struct [A]; + struct «A»; let variable = A; "}); @@ -450,7 +450,7 @@ mod tests { cx.foreground().run_until_parked(); cx.assert_editor_text_highlights::(indoc! {" struct A; - let [variable] = A; + let «variable» = A; "}); // Unpress shift causes highlight to go away (normal goto-definition is not valid here) @@ -473,10 +473,10 @@ mod tests { // Cmd+shift click without existing definition requests and jumps let hover_point = cx.display_point(indoc! {" struct A; - let v|ariable = A; + let vˇariable = A; "}); let target_range = cx.lsp_range(indoc! {" - struct [A]; + struct «A»; let variable = A; "}); @@ -503,7 +503,7 @@ mod tests { cx.foreground().run_until_parked(); cx.assert_editor_state(indoc! {" - struct [A}; + struct «Aˇ»; let variable = A; "}); } @@ -520,34 +520,22 @@ mod tests { .await; cx.set_state(indoc! {" - fn |test() - do_work(); - - fn do_work() - test(); + fn ˇtest() { do_work(); } + fn do_work() { test(); } "}); // Basic hold cmd, expect highlight in region if response contains definition let hover_point = cx.display_point(indoc! {" - fn test() - do_w|ork(); - - fn do_work() - test(); + fn test() { do_wˇork(); } + fn do_work() { test(); } "}); let symbol_range = cx.lsp_range(indoc! {" - fn test() - [do_work](); - - fn do_work() - test(); + fn test() { «do_work»(); } + fn do_work() { test(); } "}); let target_range = cx.lsp_range(indoc! {" - fn test() - do_work(); - - fn [do_work]() - test(); + fn test() { do_work(); } + fn «do_work»() { test(); } "}); let mut requests = cx.handle_request::(move |url, _, _| async move { @@ -575,11 +563,8 @@ mod tests { requests.next().await; cx.foreground().run_until_parked(); cx.assert_editor_text_highlights::(indoc! {" - fn test() - [do_work](); - - fn do_work() - test(); + fn test() { «do_work»(); } + fn do_work() { test(); } "}); // Unpress cmd causes highlight to go away @@ -593,13 +578,11 @@ mod tests { cx, ); }); + // Assert no link highlights cx.assert_editor_text_highlights::(indoc! {" - fn test() - do_work(); - - fn do_work() - test(); + fn test() { do_work(); } + fn do_work() { test(); } "}); // Response without source range still highlights word @@ -630,20 +613,14 @@ mod tests { cx.foreground().run_until_parked(); cx.assert_editor_text_highlights::(indoc! {" - fn test() - [do_work](); - - fn do_work() - test(); + fn test() { «do_work»(); } + fn do_work() { test(); } "}); // Moving mouse to location with no response dismisses highlight let hover_point = cx.display_point(indoc! {" - f|n test() - do_work(); - - fn do_work() - test(); + fˇn test() { do_work(); } + fn do_work() { test(); } "}); let mut requests = cx .lsp @@ -667,20 +644,14 @@ mod tests { // Assert no link highlights cx.assert_editor_text_highlights::(indoc! {" - fn test() - do_work(); - - fn do_work() - test(); + fn test() { do_work(); } + fn do_work() { test(); } "}); // Move mouse without cmd and then pressing cmd triggers highlight let hover_point = cx.display_point(indoc! {" - fn test() - do_work(); - - fn do_work() - te|st(); + fn test() { do_work(); } + fn do_work() { teˇst(); } "}); cx.update_editor(|editor, cx| { update_go_to_definition_link( @@ -697,26 +668,17 @@ mod tests { // Assert no link highlights cx.assert_editor_text_highlights::(indoc! {" - fn test() - do_work(); - - fn do_work() - test(); + fn test() { do_work(); } + fn do_work() { test(); } "}); let symbol_range = cx.lsp_range(indoc! {" - fn test() - do_work(); - - fn do_work() - [test](); + fn test() { do_work(); } + fn do_work() { «test»(); } "}); let target_range = cx.lsp_range(indoc! {" - fn [test]() - do_work(); - - fn do_work() - test(); + fn «test»() { do_work(); } + fn do_work() { test(); } "}); let mut requests = cx.handle_request::(move |url, _, _| async move { @@ -743,20 +705,14 @@ mod tests { cx.foreground().run_until_parked(); cx.assert_editor_text_highlights::(indoc! {" - fn test() - do_work(); - - fn do_work() - [test](); + fn test() { do_work(); } + fn do_work() { «test»(); } "}); // Moving within symbol range doesn't re-request let hover_point = cx.display_point(indoc! {" - fn test() - do_work(); - - fn do_work() - tes|t(); + fn test() { do_work(); } + fn do_work() { tesˇt(); } "}); cx.update_editor(|editor, cx| { update_go_to_definition_link( @@ -771,11 +727,8 @@ mod tests { }); cx.foreground().run_until_parked(); cx.assert_editor_text_highlights::(indoc! {" - fn test() - do_work(); - - fn do_work() - [test](); + fn test() { do_work(); } + fn do_work() { «test»(); } "}); // Cmd click with existing definition doesn't re-request and dismisses highlight @@ -790,35 +743,24 @@ mod tests { Ok(Some(lsp::GotoDefinitionResponse::Link(vec![]))) }); cx.assert_editor_state(indoc! {" - fn [test}() - do_work(); - - fn do_work() - test(); + fn «testˇ»() { do_work(); } + fn do_work() { test(); } "}); + // Assert no link highlights after jump cx.assert_editor_text_highlights::(indoc! {" - fn test() - do_work(); - - fn do_work() - test(); + fn test() { do_work(); } + fn do_work() { test(); } "}); // Cmd click without existing definition requests and jumps let hover_point = cx.display_point(indoc! {" - fn test() - do_w|ork(); - - fn do_work() - test(); + fn test() { do_wˇork(); } + fn do_work() { test(); } "}); let target_range = cx.lsp_range(indoc! {" - fn test() - do_work(); - - fn [do_work]() - test(); + fn test() { do_work(); } + fn «do_work»() { test(); } "}); let mut requests = cx.handle_request::(move |url, _, _| async move { @@ -836,13 +778,9 @@ mod tests { }); requests.next().await; cx.foreground().run_until_parked(); - cx.assert_editor_state(indoc! {" - fn test() - do_work(); - - fn [do_work}() - test(); + fn test() { do_work(); } + fn «do_workˇ»() { test(); } "}); } } diff --git a/crates/editor/src/mouse_context_menu.rs b/crates/editor/src/mouse_context_menu.rs index 513a9ed99ce06e0ac25d2f70d5f4d829155f1426..3098e96e0714d6561882cb50608607414a15ea7b 100644 --- a/crates/editor/src/mouse_context_menu.rs +++ b/crates/editor/src/mouse_context_menu.rs @@ -67,11 +67,9 @@ pub fn deploy_context_menu( #[cfg(test)] mod tests { - use indoc::indoc; - - use crate::test::EditorLspTestContext; - use super::*; + use crate::test::EditorLspTestContext; + use indoc::indoc; #[gpui::test] async fn test_mouse_context_menu(cx: &mut gpui::TestAppContext) { @@ -85,11 +83,15 @@ mod tests { .await; cx.set_state(indoc! {" - fn te|st() - do_work();"}); + fn teˇst() { + do_work(); + } + "}); let point = cx.display_point(indoc! {" - fn test() - do_w|ork();"}); + fn test() { + do_wˇork(); + } + "}); cx.update_editor(|editor, cx| { deploy_context_menu( editor, @@ -102,8 +104,10 @@ mod tests { }); cx.assert_editor_state(indoc! {" - fn test() - do_w|ork();"}); + fn test() { + do_wˇork(); + } + "}); cx.editor(|editor, app| assert!(editor.mouse_context_menu.read(app).visible())); } } diff --git a/crates/editor/src/test.rs b/crates/editor/src/test.rs index 18c13a4ba6dbe7e060f8e00eb7f4b7e3a75f70db..38540af0585384e958e0b9e351a8f967736050a2 100644 --- a/crates/editor/src/test.rs +++ b/crates/editor/src/test.rs @@ -8,7 +8,6 @@ use anyhow::Result; use futures::{Future, StreamExt}; use indoc::indoc; -use collections::BTreeMap; use gpui::{ json, keymap::Keystroke, AppContext, ModelContext, ModelHandle, ViewContext, ViewHandle, }; @@ -20,7 +19,7 @@ use project::Project; use settings::Settings; use util::{ assert_set_eq, set_eq, - test::{marked_text, marked_text_ranges, marked_text_ranges_by, SetEqError, TextRangeMarker}, + test::{generate_marked_text, marked_text, parse_marked_text}, }; use workspace::{pane, AppState, Workspace, WorkspaceHandle}; @@ -65,7 +64,7 @@ pub fn marked_display_snapshot( } pub fn select_ranges(editor: &mut Editor, marked_text: &str, cx: &mut ViewContext) { - let (umarked_text, text_ranges) = marked_text_ranges(marked_text); + let (umarked_text, text_ranges) = parse_marked_text(marked_text, true).unwrap(); assert_eq!(editor.text(cx), umarked_text); editor.change_selections(None, cx, |s| s.select_ranges(text_ranges)); } @@ -75,8 +74,7 @@ pub fn assert_text_with_selections( marked_text: &str, cx: &mut ViewContext, ) { - let (unmarked_text, text_ranges) = marked_text_ranges(marked_text); - + let (unmarked_text, text_ranges) = parse_marked_text(marked_text, true).unwrap(); assert_eq!(editor.text(cx), unmarked_text); assert_eq!(editor.selections.ranges(cx), text_ranges); } @@ -190,94 +188,49 @@ impl<'a> EditorTestContext<'a> { } } - pub fn display_point(&mut self, cursor_location: &str) -> DisplayPoint { - let (_, locations) = marked_text(cursor_location); + pub fn ranges(&self, marked_text: &str) -> Vec> { + let (unmarked_text, ranges) = parse_marked_text(marked_text, false).unwrap(); + assert_eq!(self.buffer_text(), unmarked_text); + ranges + } + + pub fn display_point(&mut self, marked_text: &str) -> DisplayPoint { + let ranges = self.ranges(marked_text); let snapshot = self .editor .update(self.cx, |editor, cx| editor.snapshot(cx)); - locations[0].to_display_point(&snapshot.display_snapshot) + ranges[0].start.to_display_point(&snapshot) } - // Returns anchors for the current buffer using `[`..`]` + // Returns anchors for the current buffer using `«` and `»` pub fn text_anchor_range(&self, marked_text: &str) -> Range { - let range_marker: TextRangeMarker = ('[', ']').into(); - let (unmarked_text, mut ranges) = - marked_text_ranges_by(&marked_text, vec![range_marker.clone()]); - assert_eq!(self.buffer_text(), unmarked_text); - let offset_range = ranges.remove(&range_marker).unwrap()[0].clone(); + let ranges = self.ranges(marked_text); let snapshot = self.buffer_snapshot(); - - snapshot.anchor_before(offset_range.start)..snapshot.anchor_after(offset_range.end) - } - - // Sets the editor state via a marked string. - // `|` characters represent empty selections - // `[` to `}` represents a non empty selection with the head at `}` - // `{` to `]` represents a non empty selection with the head at `{` - pub fn set_state(&mut self, text: &str) { - self.set_state_by( - vec![ - '|'.into(), - ('[', '}').into(), - TextRangeMarker::ReverseRange('{', ']'), - ], - text, - ); + snapshot.anchor_before(ranges[0].start)..snapshot.anchor_after(ranges[0].end) } - pub fn set_state_by(&mut self, range_markers: Vec, text: &str) { + pub fn set_state(&mut self, marked_text: &str) { + let (unmarked_text, selection_ranges) = parse_marked_text(marked_text, true).unwrap(); self.editor.update(self.cx, |editor, cx| { - let (unmarked_text, selection_ranges) = marked_text_ranges_by(&text, range_markers); editor.set_text(unmarked_text, cx); - - let selection_ranges: Vec> = selection_ranges - .values() - .into_iter() - .flatten() - .cloned() - .collect(); editor.change_selections(Some(Autoscroll::Fit), cx, |s| { s.select_ranges(selection_ranges) }) }) } - // Asserts the editor state via a marked string. - // `|` characters represent empty selections - // `[` to `}` represents a non empty selection with the head at `}` - // `{` to `]` represents a non empty selection with the head at `{` - pub fn assert_editor_state(&mut self, text: &str) { - let (unmarked_text, mut selection_ranges) = marked_text_ranges_by( - &text, - vec!['|'.into(), ('[', '}').into(), ('{', ']').into()], - ); + pub fn assert_editor_state(&mut self, marked_text: &str) { + let (unmarked_text, expected_selections) = parse_marked_text(marked_text, true).unwrap(); let buffer_text = self.buffer_text(); assert_eq!( buffer_text, unmarked_text, "Unmarked text doesn't match buffer text" ); - - let expected_empty_selections = selection_ranges.remove(&'|'.into()).unwrap_or_default(); - let expected_reverse_selections = selection_ranges - .remove(&('{', ']').into()) - .unwrap_or_default(); - let expected_forward_selections = selection_ranges - .remove(&('[', '}').into()) - .unwrap_or_default(); - - self.assert_selections( - expected_empty_selections, - expected_reverse_selections, - expected_forward_selections, - Some(text.to_string()), - ) + self.assert_selections(expected_selections, marked_text.to_string()) } pub fn assert_editor_background_highlights(&mut self, marked_text: &str) { - let (unmarked, mut ranges) = marked_text_ranges_by(marked_text, vec![('[', ']').into()]); - assert_eq!(unmarked, self.buffer_text()); - - let asserted_ranges = ranges.remove(&('[', ']').into()).unwrap(); + let expected_ranges = self.ranges(marked_text); let actual_ranges: Vec> = self.update_editor(|editor, cx| { let snapshot = editor.snapshot(cx); editor @@ -289,175 +242,57 @@ impl<'a> EditorTestContext<'a> { .map(|range| range.to_offset(&snapshot.buffer_snapshot)) .collect() }); - - assert_set_eq!(asserted_ranges, actual_ranges); + assert_set_eq!(actual_ranges, expected_ranges); } pub fn assert_editor_text_highlights(&mut self, marked_text: &str) { - let (unmarked, mut ranges) = marked_text_ranges_by(marked_text, vec![('[', ']').into()]); - assert_eq!(unmarked, self.buffer_text()); - - let asserted_ranges = ranges.remove(&('[', ']').into()).unwrap(); + let expected_ranges = self.ranges(marked_text); let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx)); let actual_ranges: Vec> = snapshot - .display_snapshot .highlight_ranges::() .map(|ranges| ranges.as_ref().clone().1) .unwrap_or_default() .into_iter() .map(|range| range.to_offset(&snapshot.buffer_snapshot)) .collect(); - - assert_set_eq!(asserted_ranges, actual_ranges); + assert_set_eq!(actual_ranges, expected_ranges); } pub fn assert_editor_selections(&mut self, expected_selections: Vec>) { - let mut empty_selections = Vec::new(); - let mut reverse_selections = Vec::new(); - let mut forward_selections = Vec::new(); - - for selection in expected_selections { - let range = selection.range(); - if selection.is_empty() { - empty_selections.push(range); - } else if selection.reversed { - reverse_selections.push(range); - } else { - forward_selections.push(range) - } - } - - self.assert_selections( - empty_selections, - reverse_selections, - forward_selections, - None, - ) + let expected_selections = expected_selections + .into_iter() + .map(|s| s.range()) + .collect::>(); + let expected_marked_text = + generate_marked_text(&self.buffer_text(), &expected_selections, true); + self.assert_selections(expected_selections, expected_marked_text) } fn assert_selections( &mut self, - expected_empty_selections: Vec>, - expected_reverse_selections: Vec>, - expected_forward_selections: Vec>, - asserted_text: Option, + expected_selections: Vec>, + expected_marked_text: String, ) { - let (empty_selections, reverse_selections, forward_selections) = - self.editor.read_with(self.cx, |editor, cx| { - let mut empty_selections = Vec::new(); - let mut reverse_selections = Vec::new(); - let mut forward_selections = Vec::new(); - - for selection in editor.selections.all::(cx) { - let range = selection.range(); - if selection.is_empty() { - empty_selections.push(range); - } else if selection.reversed { - reverse_selections.push(range); - } else { - forward_selections.push(range) - } - } - - (empty_selections, reverse_selections, forward_selections) - }); - - let asserted_selections = asserted_text.unwrap_or_else(|| { - self.insert_markers( - &expected_empty_selections, - &expected_reverse_selections, - &expected_forward_selections, - ) - }); - let actual_selections = - self.insert_markers(&empty_selections, &reverse_selections, &forward_selections); - - let unmarked_text = self.buffer_text(); - let all_eq: Result<(), SetEqError> = - set_eq!(expected_empty_selections, empty_selections) - .map_err(|err| { - err.map(|missing| { - let mut error_text = unmarked_text.clone(); - error_text.insert(missing.start, '|'); - error_text - }) - }) - .and_then(|_| { - set_eq!(expected_reverse_selections, reverse_selections).map_err(|err| { - err.map(|missing| { - let mut error_text = unmarked_text.clone(); - error_text.insert(missing.start, '{'); - error_text.insert(missing.end, ']'); - error_text - }) - }) - }) - .and_then(|_| { - set_eq!(expected_forward_selections, forward_selections).map_err(|err| { - err.map(|missing| { - let mut error_text = unmarked_text.clone(); - error_text.insert(missing.start, '['); - error_text.insert(missing.end, '}'); - error_text - }) - }) - }); - - match all_eq { - Err(SetEqError::LeftMissing(location_text)) => { - panic!( - indoc! {" - Editor has extra selection - Extra Selection Location: - {} - Asserted selections: - {} - Actual selections: - {}"}, - location_text, asserted_selections, actual_selections, - ); - } - Err(SetEqError::RightMissing(location_text)) => { - panic!( - indoc! {" - Editor is missing empty selection - Missing Selection Location: - {} - Asserted selections: - {} - Actual selections: - {}"}, - location_text, asserted_selections, actual_selections, - ); - } - _ => {} - } - } - - fn insert_markers( - &mut self, - empty_selections: &Vec>, - reverse_selections: &Vec>, - forward_selections: &Vec>, - ) -> String { - let mut editor_text_with_selections = self.buffer_text(); - let mut selection_marks = BTreeMap::new(); - for range in empty_selections { - selection_marks.insert(&range.start, '|'); - } - for range in reverse_selections { - selection_marks.insert(&range.start, '{'); - selection_marks.insert(&range.end, ']'); - } - for range in forward_selections { - selection_marks.insert(&range.start, '['); - selection_marks.insert(&range.end, '}'); - } - for (offset, mark) in selection_marks.into_iter().rev() { - editor_text_with_selections.insert(*offset, mark); + let actual_selections = self + .editor + .read_with(self.cx, |editor, cx| editor.selections.all::(cx)) + .into_iter() + .map(|s| s.range()) + .collect::>(); + let actual_marked_text = + generate_marked_text(&self.buffer_text(), &actual_selections, true); + if expected_selections != actual_selections { + panic!( + indoc! {" + Editor has unexpected selections. + Expected selections: + {} + Actual selections: + {}", + }, + expected_marked_text, actual_marked_text, + ); } - - editor_text_with_selections } } @@ -575,10 +410,8 @@ impl<'a> EditorLspTestContext<'a> { // Constructs lsp range using a marked string with '[', ']' range delimiters pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range { - let (unmarked, mut ranges) = marked_text_ranges_by(marked_text, vec![('[', ']').into()]); - assert_eq!(unmarked, self.buffer_text()); - let offset_range = ranges.remove(&('[', ']').into()).unwrap()[0].clone(); - self.to_lsp_range(offset_range) + let ranges = self.ranges(marked_text); + self.to_lsp_range(ranges[0].clone()) } pub fn to_lsp_range(&mut self, range: Range) -> lsp::Range { diff --git a/crates/util/Cargo.toml b/crates/util/Cargo.toml index 87ec77d2dfc23ed2f9ef7088ec6363234ff8c24c..4ec214fef10d2c4e1a95212fce84b973de0d8336 100644 --- a/crates/util/Cargo.toml +++ b/crates/util/Cargo.toml @@ -15,6 +15,9 @@ futures = "0.3" log = { version = "0.4.16", features = ["kv_unstable_serde"] } rand = { version = "0.8", optional = true } tempdir = { version = "0.3.7", optional = true } -serde_json = { version = "1.0", features = [ - "preserve_order", -], optional = true } +serde_json = { version = "1.0", features = ["preserve_order"], optional = true } + +[dev-dependencies] +rand = { version = "0.8" } +tempdir = { version = "0.3.7" } +serde_json = { version = "1.0", features = ["preserve_order"] } diff --git a/crates/util/src/lib.rs b/crates/util/src/lib.rs index e5a98fd675579885fd7a472866543b1ef2faa767..0616b2a75ec8352de152cb8d95100c2aed484dc5 100644 --- a/crates/util/src/lib.rs +++ b/crates/util/src/lib.rs @@ -1,4 +1,4 @@ -#[cfg(feature = "test-support")] +#[cfg(any(test, feature = "test-support"))] pub mod test; use futures::Future; diff --git a/crates/util/src/test/marked_text.rs b/crates/util/src/test/marked_text.rs index 2a5969c26564e3603182e1b743b71494e31604f5..539134121c50316d15ba5ae17d043de393360950 100644 --- a/crates/util/src/test/marked_text.rs +++ b/crates/util/src/test/marked_text.rs @@ -1,4 +1,5 @@ -use std::{collections::HashMap, ops::Range}; +use anyhow::{anyhow, Result}; +use std::{cmp::Ordering, collections::HashMap, ops::Range}; pub fn marked_text_by( marked_text: &str, @@ -125,3 +126,122 @@ pub fn marked_text_ranges(full_marked_text: &str) -> (String, Vec>) combined_ranges.sort_by_key(|range| range.start); (unmarked, combined_ranges) } + +/// +pub fn parse_marked_text( + input_text: &str, + indicate_cursors: bool, +) -> Result<(String, Vec>)> { + let mut output_text = String::with_capacity(input_text.len()); + let mut ranges = Vec::new(); + let mut prev_input_ix = 0; + let mut current_range_start = None; + let mut current_range_cursor = None; + + for (input_ix, marker) in input_text.match_indices(&['«', '»', 'ˇ']) { + output_text.push_str(&input_text[prev_input_ix..input_ix]); + let output_len = output_text.len(); + let len = marker.len(); + prev_input_ix = input_ix + len; + + match marker { + "ˇ" => { + if current_range_start.is_some() { + if current_range_cursor.is_some() { + Err(anyhow!("duplicate point marker 'ˇ' at index {input_ix}"))?; + } else { + current_range_cursor = Some(output_len); + } + } else { + ranges.push(output_len..output_len); + } + } + "«" => { + if current_range_start.is_some() { + Err(anyhow!( + "unexpected range start marker '«' at index {input_ix}" + ))?; + } + current_range_start = Some(output_len); + } + "»" => { + let current_range_start = current_range_start.take().ok_or_else(|| { + anyhow!("unexpected range end marker '»' at index {input_ix}") + })?; + + let mut reversed = false; + if let Some(current_range_cursor) = current_range_cursor.take() { + if current_range_cursor == current_range_start { + reversed = true; + } else if current_range_cursor != output_len { + Err(anyhow!("unexpected 'ˇ' marker in the middle of a range"))?; + } + } else if indicate_cursors { + Err(anyhow!("missing 'ˇ' marker to indicate range direction"))?; + } + + ranges.push(if reversed { + output_len..current_range_start + } else { + current_range_start..output_len + }); + } + _ => unreachable!(), + } + } + + output_text.push_str(&input_text[prev_input_ix..]); + Ok((output_text, ranges)) +} + +pub fn generate_marked_text( + output_text: &str, + ranges: &[Range], + indicate_cursors: bool, +) -> String { + let mut marked_text = output_text.to_string(); + for range in ranges.iter().rev() { + if indicate_cursors { + match range.start.cmp(&range.end) { + Ordering::Less => { + marked_text.insert_str(range.end, "ˇ»"); + marked_text.insert_str(range.start, "«"); + } + Ordering::Equal => { + marked_text.insert_str(range.start, "ˇ"); + } + Ordering::Greater => { + marked_text.insert_str(range.start, "»"); + marked_text.insert_str(range.end, "«ˇ"); + } + } + } else { + marked_text.insert_str(range.end, "»"); + marked_text.insert_str(range.start, "«"); + } + } + marked_text +} + +#[cfg(test)] +mod tests { + use super::{generate_marked_text, parse_marked_text}; + + #[test] + fn test_marked_text() { + let (text, ranges) = + parse_marked_text("one «ˇtwo» «threeˇ» «ˇfour» fiveˇ six", true).unwrap(); + + assert_eq!(text, "one two three four five six"); + assert_eq!(ranges.len(), 4); + assert_eq!(ranges[0], 7..4); + assert_eq!(ranges[1], 8..13); + assert_eq!(ranges[2], 18..14); + assert_eq!(ranges[3], 23..23); + + assert_eq!( + generate_marked_text(&text, &ranges, true), + "one «ˇtwo» «threeˇ» «ˇfour» fiveˇ six" + ); + } +}