Skip to content

Commit 312d31d

Browse files
authored
Merge pull request rust-lang#20788 from A4-Tacks/any-raw-string
Fix not applicable c-str and byte-str for raw_string
2 parents 986bb35 + d1ecdca commit 312d31d

File tree

4 files changed

+235
-55
lines changed

4 files changed

+235
-55
lines changed

src/tools/rust-analyzer/crates/ide-assists/src/handlers/raw_string.rs

Lines changed: 133 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
use std::borrow::Cow;
2-
3-
use syntax::{AstToken, TextRange, TextSize, ast, ast::IsString};
1+
use ide_db::source_change::SourceChangeBuilder;
2+
use syntax::{
3+
AstToken,
4+
ast::{self, IsString, make::tokens::literal},
5+
};
46

57
use crate::{
68
AssistContext, AssistId, Assists,
7-
utils::{required_hashes, string_suffix},
9+
utils::{required_hashes, string_prefix, string_suffix},
810
};
911

1012
// Assist: make_raw_string
@@ -23,8 +25,7 @@ use crate::{
2325
// }
2426
// ```
2527
pub(crate) fn make_raw_string(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
26-
// FIXME: This should support byte and c strings as well.
27-
let token = ctx.find_token_at_offset::<ast::String>()?;
28+
let token = ctx.find_token_at_offset::<ast::AnyString>()?;
2829
if token.is_raw() {
2930
return None;
3031
}
@@ -36,16 +37,10 @@ pub(crate) fn make_raw_string(acc: &mut Assists, ctx: &AssistContext<'_>) -> Opt
3637
target,
3738
|edit| {
3839
let hashes = "#".repeat(required_hashes(&value).max(1));
39-
let range = token.syntax().text_range();
40+
let raw_prefix = token.raw_prefix();
4041
let suffix = string_suffix(token.text()).unwrap_or_default();
41-
let range = TextRange::new(range.start(), range.end() - TextSize::of(suffix));
42-
if matches!(value, Cow::Borrowed(_)) {
43-
// Avoid replacing the whole string to better position the cursor.
44-
edit.insert(range.start(), format!("r{hashes}"));
45-
edit.insert(range.end(), hashes);
46-
} else {
47-
edit.replace(range, format!("r{hashes}\"{value}\"{hashes}"));
48-
}
42+
let new_str = format!("{raw_prefix}{hashes}\"{value}\"{hashes}{suffix}");
43+
replace_literal(&token, &new_str, edit, ctx);
4944
},
5045
)
5146
}
@@ -66,7 +61,7 @@ pub(crate) fn make_raw_string(acc: &mut Assists, ctx: &AssistContext<'_>) -> Opt
6661
// }
6762
// ```
6863
pub(crate) fn make_usual_string(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
69-
let token = ctx.find_token_at_offset::<ast::String>()?;
64+
let token = ctx.find_token_at_offset::<ast::AnyString>()?;
7065
if !token.is_raw() {
7166
return None;
7267
}
@@ -80,18 +75,9 @@ pub(crate) fn make_usual_string(acc: &mut Assists, ctx: &AssistContext<'_>) -> O
8075
// parse inside string to escape `"`
8176
let escaped = value.escape_default().to_string();
8277
let suffix = string_suffix(token.text()).unwrap_or_default();
83-
if let Some(offsets) = token.quote_offsets()
84-
&& token.text()[offsets.contents - token.syntax().text_range().start()] == escaped
85-
{
86-
let end_quote = offsets.quotes.1;
87-
let end_quote =
88-
TextRange::new(end_quote.start(), end_quote.end() - TextSize::of(suffix));
89-
edit.replace(offsets.quotes.0, "\"");
90-
edit.replace(end_quote, "\"");
91-
return;
92-
}
93-
94-
edit.replace(token.syntax().text_range(), format!("\"{escaped}\"{suffix}"));
78+
let prefix = string_prefix(token.text()).map_or("", |s| s.trim_end_matches('r'));
79+
let new_str = format!("{prefix}\"{escaped}\"{suffix}");
80+
replace_literal(&token, &new_str, edit, ctx);
9581
},
9682
)
9783
}
@@ -112,16 +98,18 @@ pub(crate) fn make_usual_string(acc: &mut Assists, ctx: &AssistContext<'_>) -> O
11298
// }
11399
// ```
114100
pub(crate) fn add_hash(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
115-
let token = ctx.find_token_at_offset::<ast::String>()?;
101+
let token = ctx.find_token_at_offset::<ast::AnyString>()?;
116102
if !token.is_raw() {
117103
return None;
118104
}
119-
let text_range = token.syntax().text_range();
120-
let target = text_range;
105+
let target = token.syntax().text_range();
121106
acc.add(AssistId::refactor("add_hash"), "Add #", target, |edit| {
122-
let suffix = string_suffix(token.text()).unwrap_or_default();
123-
edit.insert(text_range.start() + TextSize::of('r'), "#");
124-
edit.insert(text_range.end() - TextSize::of(suffix), "#");
107+
let str = token.text();
108+
let suffix = string_suffix(str).unwrap_or_default();
109+
let raw_prefix = token.raw_prefix();
110+
let wrap_range = raw_prefix.len()..str.len() - suffix.len();
111+
let new_str = [raw_prefix, "#", &str[wrap_range], "#", suffix].concat();
112+
replace_literal(&token, &new_str, edit, ctx);
125113
})
126114
}
127115

@@ -141,17 +129,15 @@ pub(crate) fn add_hash(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()>
141129
// }
142130
// ```
143131
pub(crate) fn remove_hash(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
144-
let token = ctx.find_token_at_offset::<ast::String>()?;
132+
let token = ctx.find_token_at_offset::<ast::AnyString>()?;
145133
if !token.is_raw() {
146134
return None;
147135
}
148136

149137
let text = token.text();
150-
if !text.starts_with("r#") && text.ends_with('#') {
151-
return None;
152-
}
153138

154-
let existing_hashes = text.chars().skip(1).take_while(|&it| it == '#').count();
139+
let existing_hashes =
140+
text.chars().skip(token.raw_prefix().len()).take_while(|&it| it == '#').count();
155141

156142
let text_range = token.syntax().text_range();
157143
let internal_text = &text[token.text_range_between_quotes()? - text_range.start()];
@@ -163,14 +149,38 @@ pub(crate) fn remove_hash(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<
163149

164150
acc.add(AssistId::refactor_rewrite("remove_hash"), "Remove #", text_range, |edit| {
165151
let suffix = string_suffix(text).unwrap_or_default();
166-
edit.delete(TextRange::at(text_range.start() + TextSize::of('r'), TextSize::of('#')));
167-
edit.delete(
168-
TextRange::new(text_range.end() - TextSize::of('#'), text_range.end())
169-
- TextSize::of(suffix),
170-
);
152+
let prefix = token.raw_prefix();
153+
let wrap_range = prefix.len() + 1..text.len() - suffix.len() - 1;
154+
let new_str = [prefix, &text[wrap_range], suffix].concat();
155+
replace_literal(&token, &new_str, edit, ctx);
171156
})
172157
}
173158

159+
fn replace_literal(
160+
token: &impl AstToken,
161+
new: &str,
162+
builder: &mut SourceChangeBuilder,
163+
ctx: &AssistContext<'_>,
164+
) {
165+
let token = token.syntax();
166+
let node = token.parent().expect("no parent token");
167+
let mut edit = builder.make_editor(&node);
168+
let new_literal = literal(new);
169+
170+
edit.replace(token, mut_token(new_literal));
171+
172+
builder.add_file_edits(ctx.vfs_file_id(), edit);
173+
}
174+
175+
fn mut_token(token: syntax::SyntaxToken) -> syntax::SyntaxToken {
176+
let node = token.parent().expect("no parent token");
177+
node.clone_for_update()
178+
.children_with_tokens()
179+
.filter_map(|it| it.into_token())
180+
.find(|it| it.text_range() == token.text_range() && it.text() == token.text())
181+
.unwrap()
182+
}
183+
174184
#[cfg(test)]
175185
mod tests {
176186
use super::*;
@@ -224,6 +234,42 @@ string"#;
224234
)
225235
}
226236

237+
#[test]
238+
fn make_raw_byte_string_works() {
239+
check_assist(
240+
make_raw_string,
241+
r#"
242+
fn f() {
243+
let s = $0b"random\nstring";
244+
}
245+
"#,
246+
r##"
247+
fn f() {
248+
let s = br#"random
249+
string"#;
250+
}
251+
"##,
252+
)
253+
}
254+
255+
#[test]
256+
fn make_raw_c_string_works() {
257+
check_assist(
258+
make_raw_string,
259+
r#"
260+
fn f() {
261+
let s = $0c"random\nstring";
262+
}
263+
"#,
264+
r##"
265+
fn f() {
266+
let s = cr#"random
267+
string"#;
268+
}
269+
"##,
270+
)
271+
}
272+
227273
#[test]
228274
fn make_raw_string_hashes_inside_works() {
229275
check_assist(
@@ -348,6 +394,23 @@ string"###;
348394
)
349395
}
350396

397+
#[test]
398+
fn add_hash_works_for_c_str() {
399+
check_assist(
400+
add_hash,
401+
r#"
402+
fn f() {
403+
let s = $0cr"random string";
404+
}
405+
"#,
406+
r##"
407+
fn f() {
408+
let s = cr#"random string"#;
409+
}
410+
"##,
411+
)
412+
}
413+
351414
#[test]
352415
fn add_hash_has_suffix_works() {
353416
check_assist(
@@ -433,6 +496,15 @@ string"###;
433496
)
434497
}
435498

499+
#[test]
500+
fn remove_hash_works_for_c_str() {
501+
check_assist(
502+
remove_hash,
503+
r##"fn f() { let s = $0cr#"random string"#; }"##,
504+
r#"fn f() { let s = cr"random string"; }"#,
505+
)
506+
}
507+
436508
#[test]
437509
fn remove_hash_has_suffix_works() {
438510
check_assist(
@@ -529,6 +601,23 @@ string"###;
529601
)
530602
}
531603

604+
#[test]
605+
fn make_usual_string_for_c_str() {
606+
check_assist(
607+
make_usual_string,
608+
r##"
609+
fn f() {
610+
let s = $0cr#"random string"#;
611+
}
612+
"##,
613+
r#"
614+
fn f() {
615+
let s = c"random string";
616+
}
617+
"#,
618+
)
619+
}
620+
532621
#[test]
533622
fn make_usual_string_has_suffix_works() {
534623
check_assist(

src/tools/rust-analyzer/crates/ide-assists/src/utils.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1057,6 +1057,21 @@ fn test_string_suffix() {
10571057
assert_eq!(Some("i32"), string_suffix(r##"r#""#i32"##));
10581058
}
10591059

1060+
/// Calculate the string literal prefix length
1061+
pub(crate) fn string_prefix(s: &str) -> Option<&str> {
1062+
s.split_once(['"', '\'', '#']).map(|(prefix, _)| prefix)
1063+
}
1064+
#[test]
1065+
fn test_string_prefix() {
1066+
assert_eq!(Some(""), string_prefix(r#""abc""#));
1067+
assert_eq!(Some(""), string_prefix(r#""""#));
1068+
assert_eq!(Some(""), string_prefix(r#"""suffix"#));
1069+
assert_eq!(Some("c"), string_prefix(r#"c"""#));
1070+
assert_eq!(Some("r"), string_prefix(r#"r"""#));
1071+
assert_eq!(Some("cr"), string_prefix(r#"cr"""#));
1072+
assert_eq!(Some("r"), string_prefix(r##"r#""#"##));
1073+
}
1074+
10601075
/// Replaces the record expression, handling field shorthands including inside macros.
10611076
pub(crate) fn replace_record_field_expr(
10621077
ctx: &AssistContext<'_>,

src/tools/rust-analyzer/crates/syntax/src/ast.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ pub use self::{
2929
SlicePatComponents, StructKind, TypeBoundKind, TypeOrConstParam, VisibilityKind,
3030
},
3131
operators::{ArithOp, BinaryOp, CmpOp, LogicOp, Ordering, RangeOp, UnaryOp},
32-
token_ext::{CommentKind, CommentPlacement, CommentShape, IsString, QuoteOffsets, Radix},
32+
token_ext::{
33+
AnyString, CommentKind, CommentPlacement, CommentShape, IsString, QuoteOffsets, Radix,
34+
},
3335
traits::{
3436
AttrDocCommentIter, DocCommentIter, HasArgList, HasAttrs, HasDocComments, HasGenericArgs,
3537
HasGenericParams, HasLoopBody, HasModuleItem, HasName, HasTypeBounds, HasVisibility,

0 commit comments

Comments
 (0)