From 491e6eaf9e030d44fd0fa5f4a831199dc3257745 Mon Sep 17 00:00:00 2001
From: Ryo Yoshida <low.ryoshida@gmail.com>
Date: Mon, 27 Jun 2022 19:10:37 +0900
Subject: [PATCH 1/4] fix: escape ${receiver} when completing with custom
 snippets

---
 .../ide-completion/src/completions/postfix.rs | 76 +++++++++++++++----
 1 file changed, 62 insertions(+), 14 deletions(-)

diff --git a/crates/ide-completion/src/completions/postfix.rs b/crates/ide-completion/src/completions/postfix.rs
index 5af44aa4b68ac..762d7cddb97c9 100644
--- a/crates/ide-completion/src/completions/postfix.rs
+++ b/crates/ide-completion/src/completions/postfix.rs
@@ -193,13 +193,21 @@ pub(crate) fn complete_postfix(
 }
 
 fn get_receiver_text(receiver: &ast::Expr, receiver_is_ambiguous_float_literal: bool) -> String {
-    if receiver_is_ambiguous_float_literal {
+    let text = if receiver_is_ambiguous_float_literal {
         let text = receiver.syntax().text();
         let without_dot = ..text.len() - TextSize::of('.');
         text.slice(without_dot).to_string()
     } else {
         receiver.to_string()
-    }
+    };
+
+    // The receiver texts should be interpreted as-is, as they are expected to be
+    // normal Rust expressions. We escape '\' and '$' so they don't get treated as
+    // snippet-specific constructs.
+    //
+    // Note that we don't need to escape the other characters that can be escaped,
+    // because they wouldn't be treated as snippet-specific constructs without '$'.
+    text.replace('\\', "\\\\").replace('$', "\\$")
 }
 
 fn include_references(initial_element: &ast::Expr) -> ast::Expr {
@@ -494,19 +502,21 @@ fn main() {
 
     #[test]
     fn custom_postfix_completion() {
+        let config = CompletionConfig {
+            snippets: vec![Snippet::new(
+                &[],
+                &["break".into()],
+                &["ControlFlow::Break(${receiver})".into()],
+                "",
+                &["core::ops::ControlFlow".into()],
+                crate::SnippetScope::Expr,
+            )
+            .unwrap()],
+            ..TEST_CONFIG
+        };
+
         check_edit_with_config(
-            CompletionConfig {
-                snippets: vec![Snippet::new(
-                    &[],
-                    &["break".into()],
-                    &["ControlFlow::Break(${receiver})".into()],
-                    "",
-                    &["core::ops::ControlFlow".into()],
-                    crate::SnippetScope::Expr,
-                )
-                .unwrap()],
-                ..TEST_CONFIG
-            },
+            config.clone(),
             "break",
             r#"
 //- minicore: try
@@ -516,6 +526,44 @@ fn main() { 42.$0 }
 use core::ops::ControlFlow;
 
 fn main() { ControlFlow::Break(42) }
+"#,
+        );
+
+        check_edit_with_config(
+            config.clone(),
+            "break",
+            r#"
+//- minicore: try
+fn main() { '\\'.$0 }
+"#,
+            r#"
+use core::ops::ControlFlow;
+
+fn main() { ControlFlow::Break('\\\\') }
+"#,
+        );
+
+        check_edit_with_config(
+            config.clone(),
+            "break",
+            r#"
+//- minicore: try
+fn main() {
+    match true {
+        true => "${1:placeholder}",
+        false => "\$",
+    }.$0
+}
+"#,
+            r#"
+use core::ops::ControlFlow;
+
+fn main() {
+    ControlFlow::Break(match true {
+        true => "\${1:placeholder}",
+        false => "\\\$",
+    })
+}
 "#,
         );
     }

From 393a18b8ce4f8f4662174e51b3f3587a322f4ebe Mon Sep 17 00:00:00 2001
From: Ryo Yoshida <low.ryoshida@gmail.com>
Date: Mon, 27 Jun 2022 23:16:09 +0900
Subject: [PATCH 2/4] fix: escape receiver texts in format string completion

---
 .../src/completions/postfix/format_like.rs       | 16 ++++++++++++++++
 1 file changed, 16 insertions(+)

diff --git a/crates/ide-completion/src/completions/postfix/format_like.rs b/crates/ide-completion/src/completions/postfix/format_like.rs
index b5ef87b8812c3..16f902489b57d 100644
--- a/crates/ide-completion/src/completions/postfix/format_like.rs
+++ b/crates/ide-completion/src/completions/postfix/format_like.rs
@@ -115,6 +115,7 @@ impl FormatStrParser {
         // "{MyStruct { val_a: 0, val_b: 1 }}".
         let mut inexpr_open_count = 0;
 
+        // We need to escape '\' and '$'. See the comments on `get_receiver_text()` for detail.
         let mut chars = self.input.chars().peekable();
         while let Some(chr) = chars.next() {
             match (self.state, chr) {
@@ -127,6 +128,9 @@ impl FormatStrParser {
                     self.state = State::MaybeIncorrect;
                 }
                 (State::NotExpr, _) => {
+                    if matches!(chr, '\\' | '$') {
+                        self.output.push('\\');
+                    }
                     self.output.push(chr);
                 }
                 (State::MaybeIncorrect, '}') => {
@@ -150,6 +154,9 @@ impl FormatStrParser {
                     self.state = State::NotExpr;
                 }
                 (State::MaybeExpr, _) => {
+                    if matches!(chr, '\\' | '$') {
+                        current_expr.push('\\');
+                    }
                     current_expr.push(chr);
                     self.state = State::Expr;
                 }
@@ -187,6 +194,9 @@ impl FormatStrParser {
                     inexpr_open_count += 1;
                 }
                 (State::Expr, _) => {
+                    if matches!(chr, '\\' | '$') {
+                        current_expr.push('\\');
+                    }
                     current_expr.push(chr);
                 }
                 (State::FormatOpts, '}') => {
@@ -194,6 +204,9 @@ impl FormatStrParser {
                     self.state = State::NotExpr;
                 }
                 (State::FormatOpts, _) => {
+                    if matches!(chr, '\\' | '$') {
+                        self.output.push('\\');
+                    }
                     self.output.push(chr);
                 }
             }
@@ -241,8 +254,11 @@ mod tests {
     fn format_str_parser() {
         let test_vector = &[
             ("no expressions", expect![["no expressions"]]),
+            (r"no expressions with \$0$1", expect![r"no expressions with \\\$0\$1"]),
             ("{expr} is {2 + 2}", expect![["{} is {}; expr, 2 + 2"]]),
             ("{expr:?}", expect![["{:?}; expr"]]),
+            ("{expr:1$}", expect![[r"{:1\$}; expr"]]),
+            ("{$0}", expect![[r"{}; \$0"]]),
             ("{malformed", expect![["-"]]),
             ("malformed}", expect![["-"]]),
             ("{{correct", expect![["{{correct"]]),

From 80cc0ef1bc42a9ff00ca159e8c3710abdbfb6d08 Mon Sep 17 00:00:00 2001
From: Ryo Yoshida <low.ryoshida@gmail.com>
Date: Mon, 27 Jun 2022 19:15:59 +0900
Subject: [PATCH 3/4] Fix typo

---
 crates/ide-completion/src/snippet.rs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/crates/ide-completion/src/snippet.rs b/crates/ide-completion/src/snippet.rs
index a6bb3d0648bb0..b0a1897526b0d 100644
--- a/crates/ide-completion/src/snippet.rs
+++ b/crates/ide-completion/src/snippet.rs
@@ -17,7 +17,7 @@
 //       "body": [
 //         "thread::spawn(move || {",
 //         "\t$0",
-//         ")};",
+//         "});",
 //       ],
 //       "description": "Insert a thread::spawn call",
 //       "requires": "std::thread",

From cfc52adc65327190b5027de19a1a6d96dcf504ab Mon Sep 17 00:00:00 2001
From: Ryo Yoshida <low.ryoshida@gmail.com>
Date: Wed, 20 Jul 2022 19:10:52 +0900
Subject: [PATCH 4/4] Add comments

---
 crates/ide-completion/src/completions/postfix.rs | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/crates/ide-completion/src/completions/postfix.rs b/crates/ide-completion/src/completions/postfix.rs
index 762d7cddb97c9..b09f4634c6514 100644
--- a/crates/ide-completion/src/completions/postfix.rs
+++ b/crates/ide-completion/src/completions/postfix.rs
@@ -529,6 +529,11 @@ fn main() { ControlFlow::Break(42) }
 "#,
         );
 
+        // The receiver texts should be escaped, see comments in `get_receiver_text()`
+        // for detail.
+        //
+        // Note that the last argument is what *lsp clients would see* rather than
+        // what users would see. Unescaping happens thereafter.
         check_edit_with_config(
             config.clone(),
             "break",