Update marked text helpers to use more distinctive characters for markers

Max Brunsfeld created

Change summary

crates/editor/src/editor.rs                     | 404 ++++++++++--------
crates/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(-)

Detailed changes

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 <s|>
-                additional edit"},
+                additional edit
+            "},
             vec!["fourth_completion", "fifth_completion", "sixth_completion"],
         )
         .await;
@@ -10373,7 +10413,8 @@ mod tests {
                 one.second_completion
                 two si
                 three <si|>
-                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();
 

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::<MatchingBracketHighlight>(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::<MatchingBracketHighlight>(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::<MatchingBracketHighlight>(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::<MatchingBracketHighlight>(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("Te<st arg>ument") {
-                    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::<MatchingBracketHighlight>(indoc! {r#"
             pub fn test("Test argument") {
                 another_test(1, 2, 3);
-            }"#});
+            }
+        "#});
     }
 }

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::<lsp::request::HoverRequest, _, _>(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::<lsp::request::HoverRequest, _, _>(|_, _| 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::<lsp::request::HoverRequest, _, _>(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::<lsp::request::HoverRequest, _, _>(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),
@@ -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::<LinkGoToDefinitionState>(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::<GotoDefinition, _, _>(move |url, _, _| async move {
@@ -575,11 +563,8 @@ mod tests {
         requests.next().await;
         cx.foreground().run_until_parked();
         cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(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::<LinkGoToDefinitionState>(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::<LinkGoToDefinitionState>(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::<LinkGoToDefinitionState>(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::<LinkGoToDefinitionState>(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::<GotoDefinition, _, _>(move |url, _, _| async move {
@@ -743,20 +705,14 @@ mod tests {
         cx.foreground().run_until_parked();
 
         cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(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::<LinkGoToDefinitionState>(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::<LinkGoToDefinitionState>(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::<GotoDefinition, _, _>(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(); }
         "});
     }
 }

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()));
     }
 }

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<Editor>) {
-    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<Editor>,
 ) {
-    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<Range<usize>> {
+        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<language::Anchor> {
-        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<TextRangeMarker>, 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<Range<usize>> = 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<Tag: 'static>(&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<Range<usize>> = 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<Tag: ?Sized + 'static>(&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<Range<usize>> = snapshot
-            .display_snapshot
             .highlight_ranges::<Tag>()
             .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<Selection<usize>>) {
-        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::<Vec<_>>();
+        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<Range<usize>>,
-        expected_reverse_selections: Vec<Range<usize>>,
-        expected_forward_selections: Vec<Range<usize>>,
-        asserted_text: Option<String>,
+        expected_selections: Vec<Range<usize>>,
+        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::<usize>(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<String>> =
-            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<Range<usize>>,
-        reverse_selections: &Vec<Range<usize>>,
-        forward_selections: &Vec<Range<usize>>,
-    ) -> 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::<usize>(cx))
+            .into_iter()
+            .map(|s| s.range())
+            .collect::<Vec<_>>();
+        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<usize>) -> lsp::Range {

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"] }

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;

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<Range<usize>>)
     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<Range<usize>>)> {
+    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<usize>],
+    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"
+        );
+    }
+}