mirror of
https://github.com/rust-lang/rust-analyzer.git
synced 2025-09-19 08:40:24 +00:00
Auto merge of #12646 - lowr:fix/11897, r=lowr
fix: escape receiver texts in completion This PR fixes #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?).
This commit is contained in:
commit
f3e9b38e26
3 changed files with 84 additions and 15 deletions
|
@ -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,8 +502,7 @@ fn main() {
|
|||
|
||||
#[test]
|
||||
fn custom_postfix_completion() {
|
||||
check_edit_with_config(
|
||||
CompletionConfig {
|
||||
let config = CompletionConfig {
|
||||
snippets: vec![Snippet::new(
|
||||
&[],
|
||||
&["break".into()],
|
||||
|
@ -506,7 +513,10 @@ fn main() {
|
|||
)
|
||||
.unwrap()],
|
||||
..TEST_CONFIG
|
||||
},
|
||||
};
|
||||
|
||||
check_edit_with_config(
|
||||
config.clone(),
|
||||
"break",
|
||||
r#"
|
||||
//- minicore: try
|
||||
|
@ -516,6 +526,49 @@ fn main() { 42.$0 }
|
|||
use core::ops::ControlFlow;
|
||||
|
||||
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",
|
||||
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 => "\\\$",
|
||||
})
|
||||
}
|
||||
"#,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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"]]),
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
// "body": [
|
||||
// "thread::spawn(move || {",
|
||||
// "\t$0",
|
||||
// ")};",
|
||||
// "});",
|
||||
// ],
|
||||
// "description": "Insert a thread::spawn call",
|
||||
// "requires": "std::thread",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue