Skip to content

Commit f3e9b38

Browse files
committed
Auto merge of rust-lang#12646 - lowr:fix/11897, r=lowr
fix: escape receiver texts in completion This PR fixes rust-lang#11897 by escaping '\\' and '$' in the text of the receiver position expression. See [here](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#snippet_syntax) for the specification of the snippet syntax (especially [this section](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#grammar) discusses escaping). Although not all occurrences of '\\' and '$' have to be replaced, I chose to replace all as that's simpler and easier to understand. There *are* more clever ways to implement it, but I thought they were premature optimization for the time being (maybe I should put FIXME notes?).
2 parents 8454413 + cfc52ad commit f3e9b38

File tree

3 files changed

+84
-15
lines changed

3 files changed

+84
-15
lines changed

crates/ide-completion/src/completions/postfix.rs

Lines changed: 67 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -193,13 +193,21 @@ pub(crate) fn complete_postfix(
193193
}
194194

195195
fn get_receiver_text(receiver: &ast::Expr, receiver_is_ambiguous_float_literal: bool) -> String {
196-
if receiver_is_ambiguous_float_literal {
196+
let text = if receiver_is_ambiguous_float_literal {
197197
let text = receiver.syntax().text();
198198
let without_dot = ..text.len() - TextSize::of('.');
199199
text.slice(without_dot).to_string()
200200
} else {
201201
receiver.to_string()
202-
}
202+
};
203+
204+
// The receiver texts should be interpreted as-is, as they are expected to be
205+
// normal Rust expressions. We escape '\' and '$' so they don't get treated as
206+
// snippet-specific constructs.
207+
//
208+
// Note that we don't need to escape the other characters that can be escaped,
209+
// because they wouldn't be treated as snippet-specific constructs without '$'.
210+
text.replace('\\', "\\\\").replace('$', "\\$")
203211
}
204212

205213
fn include_references(initial_element: &ast::Expr) -> ast::Expr {
@@ -494,19 +502,21 @@ fn main() {
494502

495503
#[test]
496504
fn custom_postfix_completion() {
505+
let config = CompletionConfig {
506+
snippets: vec![Snippet::new(
507+
&[],
508+
&["break".into()],
509+
&["ControlFlow::Break(${receiver})".into()],
510+
"",
511+
&["core::ops::ControlFlow".into()],
512+
crate::SnippetScope::Expr,
513+
)
514+
.unwrap()],
515+
..TEST_CONFIG
516+
};
517+
497518
check_edit_with_config(
498-
CompletionConfig {
499-
snippets: vec![Snippet::new(
500-
&[],
501-
&["break".into()],
502-
&["ControlFlow::Break(${receiver})".into()],
503-
"",
504-
&["core::ops::ControlFlow".into()],
505-
crate::SnippetScope::Expr,
506-
)
507-
.unwrap()],
508-
..TEST_CONFIG
509-
},
519+
config.clone(),
510520
"break",
511521
r#"
512522
//- minicore: try
@@ -516,6 +526,49 @@ fn main() { 42.$0 }
516526
use core::ops::ControlFlow;
517527
518528
fn main() { ControlFlow::Break(42) }
529+
"#,
530+
);
531+
532+
// The receiver texts should be escaped, see comments in `get_receiver_text()`
533+
// for detail.
534+
//
535+
// Note that the last argument is what *lsp clients would see* rather than
536+
// what users would see. Unescaping happens thereafter.
537+
check_edit_with_config(
538+
config.clone(),
539+
"break",
540+
r#"
541+
//- minicore: try
542+
fn main() { '\\'.$0 }
543+
"#,
544+
r#"
545+
use core::ops::ControlFlow;
546+
547+
fn main() { ControlFlow::Break('\\\\') }
548+
"#,
549+
);
550+
551+
check_edit_with_config(
552+
config.clone(),
553+
"break",
554+
r#"
555+
//- minicore: try
556+
fn main() {
557+
match true {
558+
true => "${1:placeholder}",
559+
false => "\$",
560+
}.$0
561+
}
562+
"#,
563+
r#"
564+
use core::ops::ControlFlow;
565+
566+
fn main() {
567+
ControlFlow::Break(match true {
568+
true => "\${1:placeholder}",
569+
false => "\\\$",
570+
})
571+
}
519572
"#,
520573
);
521574
}

crates/ide-completion/src/completions/postfix/format_like.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ impl FormatStrParser {
115115
// "{MyStruct { val_a: 0, val_b: 1 }}".
116116
let mut inexpr_open_count = 0;
117117

118+
// We need to escape '\' and '$'. See the comments on `get_receiver_text()` for detail.
118119
let mut chars = self.input.chars().peekable();
119120
while let Some(chr) = chars.next() {
120121
match (self.state, chr) {
@@ -127,6 +128,9 @@ impl FormatStrParser {
127128
self.state = State::MaybeIncorrect;
128129
}
129130
(State::NotExpr, _) => {
131+
if matches!(chr, '\\' | '$') {
132+
self.output.push('\\');
133+
}
130134
self.output.push(chr);
131135
}
132136
(State::MaybeIncorrect, '}') => {
@@ -150,6 +154,9 @@ impl FormatStrParser {
150154
self.state = State::NotExpr;
151155
}
152156
(State::MaybeExpr, _) => {
157+
if matches!(chr, '\\' | '$') {
158+
current_expr.push('\\');
159+
}
153160
current_expr.push(chr);
154161
self.state = State::Expr;
155162
}
@@ -187,13 +194,19 @@ impl FormatStrParser {
187194
inexpr_open_count += 1;
188195
}
189196
(State::Expr, _) => {
197+
if matches!(chr, '\\' | '$') {
198+
current_expr.push('\\');
199+
}
190200
current_expr.push(chr);
191201
}
192202
(State::FormatOpts, '}') => {
193203
self.output.push(chr);
194204
self.state = State::NotExpr;
195205
}
196206
(State::FormatOpts, _) => {
207+
if matches!(chr, '\\' | '$') {
208+
self.output.push('\\');
209+
}
197210
self.output.push(chr);
198211
}
199212
}
@@ -241,8 +254,11 @@ mod tests {
241254
fn format_str_parser() {
242255
let test_vector = &[
243256
("no expressions", expect![["no expressions"]]),
257+
(r"no expressions with \$0$1", expect![r"no expressions with \\\$0\$1"]),
244258
("{expr} is {2 + 2}", expect![["{} is {}; expr, 2 + 2"]]),
245259
("{expr:?}", expect![["{:?}; expr"]]),
260+
("{expr:1$}", expect![[r"{:1\$}; expr"]]),
261+
("{$0}", expect![[r"{}; \$0"]]),
246262
("{malformed", expect![["-"]]),
247263
("malformed}", expect![["-"]]),
248264
("{{correct", expect![["{{correct"]]),

crates/ide-completion/src/snippet.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
// "body": [
1818
// "thread::spawn(move || {",
1919
// "\t$0",
20-
// ")};",
20+
// "});",
2121
// ],
2222
// "description": "Insert a thread::spawn call",
2323
// "requires": "std::thread",

0 commit comments

Comments
 (0)