Skip to content

Commit e4cceca

Browse files
committed
Fix not applicable c-str and byte-str for raw_string
Example --- Assist: `make_raw_string` ```rust fn f() { let s = $0b"random\nstring"; } ``` -> ```rust fn f() { let s = br#"random string"#; } ``` --- Assist: `make_raw_string` ```rust fn f() { let s = $0c"random\nstring"; } ``` -> ```rust fn f() { let s = cr#"random string"#; } ``` --- Assist: `add_hash` ```rust fn f() { let s = $0cr"random string"; } ``` -> ```rust fn f() { let s = cr#"random string"#; } ``` --- Assist: `remove_hash` ```rust fn f() { let s = $0cr#"random string"#; } ``` -> ```rust fn f() { let s = cr"random string"; } ``` --- Assist: `make_usual_string` ```rust fn f() { let s = $0cr#"random string"#; } ``` -> ```rust fn f() { let s = c"random string"; } ```
1 parent 90d2e1c commit e4cceca

File tree

4 files changed

+202
-27
lines changed

4 files changed

+202
-27
lines changed

crates/ide-assists/src/handlers/raw_string.rs

Lines changed: 100 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use syntax::{AstToken, TextRange, TextSize, ast, ast::IsString};
44

55
use crate::{
66
AssistContext, AssistId, Assists,
7-
utils::{required_hashes, string_suffix},
7+
utils::{required_hashes, string_prefix, string_suffix},
88
};
99

1010
// Assist: make_raw_string
@@ -23,8 +23,7 @@ use crate::{
2323
// }
2424
// ```
2525
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>()?;
26+
let token = ctx.find_token_at_offset::<ast::AnyString>()?;
2827
if token.is_raw() {
2928
return None;
3029
}
@@ -37,14 +36,15 @@ pub(crate) fn make_raw_string(acc: &mut Assists, ctx: &AssistContext<'_>) -> Opt
3736
|edit| {
3837
let hashes = "#".repeat(required_hashes(&value).max(1));
3938
let range = token.syntax().text_range();
39+
let raw_prefix = token.raw_prefix();
4040
let suffix = string_suffix(token.text()).unwrap_or_default();
4141
let range = TextRange::new(range.start(), range.end() - TextSize::of(suffix));
4242
if matches!(value, Cow::Borrowed(_)) {
4343
// Avoid replacing the whole string to better position the cursor.
44-
edit.insert(range.start(), format!("r{hashes}"));
44+
edit.insert(range.start(), format!("{raw_prefix}{hashes}"));
4545
edit.insert(range.end(), hashes);
4646
} else {
47-
edit.replace(range, format!("r{hashes}\"{value}\"{hashes}"));
47+
edit.replace(range, format!("{raw_prefix}{hashes}\"{value}\"{hashes}"));
4848
}
4949
},
5050
)
@@ -66,7 +66,7 @@ pub(crate) fn make_raw_string(acc: &mut Assists, ctx: &AssistContext<'_>) -> Opt
6666
// }
6767
// ```
6868
pub(crate) fn make_usual_string(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
69-
let token = ctx.find_token_at_offset::<ast::String>()?;
69+
let token = ctx.find_token_at_offset::<ast::AnyString>()?;
7070
if !token.is_raw() {
7171
return None;
7272
}
@@ -80,18 +80,22 @@ pub(crate) fn make_usual_string(acc: &mut Assists, ctx: &AssistContext<'_>) -> O
8080
// parse inside string to escape `"`
8181
let escaped = value.escape_default().to_string();
8282
let suffix = string_suffix(token.text()).unwrap_or_default();
83+
let prefix = string_prefix(token.text()).map_or("", |s| s.trim_end_matches('r'));
8384
if let Some(offsets) = token.quote_offsets()
8485
&& token.text()[offsets.contents - token.syntax().text_range().start()] == escaped
8586
{
87+
let start_quote = offsets.quotes.0;
88+
let start_quote =
89+
TextRange::new(start_quote.start() + TextSize::of(prefix), start_quote.end());
8690
let end_quote = offsets.quotes.1;
8791
let end_quote =
8892
TextRange::new(end_quote.start(), end_quote.end() - TextSize::of(suffix));
89-
edit.replace(offsets.quotes.0, "\"");
9093
edit.replace(end_quote, "\"");
94+
edit.replace(start_quote, "\"");
9195
return;
9296
}
9397

94-
edit.replace(token.syntax().text_range(), format!("\"{escaped}\"{suffix}"));
98+
edit.replace(token.syntax().text_range(), format!("{prefix}\"{escaped}\"{suffix}"));
9599
},
96100
)
97101
}
@@ -112,15 +116,15 @@ pub(crate) fn make_usual_string(acc: &mut Assists, ctx: &AssistContext<'_>) -> O
112116
// }
113117
// ```
114118
pub(crate) fn add_hash(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
115-
let token = ctx.find_token_at_offset::<ast::String>()?;
119+
let token = ctx.find_token_at_offset::<ast::AnyString>()?;
116120
if !token.is_raw() {
117121
return None;
118122
}
119123
let text_range = token.syntax().text_range();
120124
let target = text_range;
121125
acc.add(AssistId::refactor("add_hash"), "Add #", target, |edit| {
122126
let suffix = string_suffix(token.text()).unwrap_or_default();
123-
edit.insert(text_range.start() + TextSize::of('r'), "#");
127+
edit.insert(text_range.start() + TextSize::of(token.raw_prefix()), "#");
124128
edit.insert(text_range.end() - TextSize::of(suffix), "#");
125129
})
126130
}
@@ -141,17 +145,15 @@ pub(crate) fn add_hash(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()>
141145
// }
142146
// ```
143147
pub(crate) fn remove_hash(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
144-
let token = ctx.find_token_at_offset::<ast::String>()?;
148+
let token = ctx.find_token_at_offset::<ast::AnyString>()?;
145149
if !token.is_raw() {
146150
return None;
147151
}
148152

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

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

156158
let text_range = token.syntax().text_range();
157159
let internal_text = &text[token.text_range_between_quotes()? - text_range.start()];
@@ -163,7 +165,10 @@ pub(crate) fn remove_hash(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<
163165

164166
acc.add(AssistId::refactor_rewrite("remove_hash"), "Remove #", text_range, |edit| {
165167
let suffix = string_suffix(text).unwrap_or_default();
166-
edit.delete(TextRange::at(text_range.start() + TextSize::of('r'), TextSize::of('#')));
168+
edit.delete(TextRange::at(
169+
text_range.start() + TextSize::of(token.raw_prefix()),
170+
TextSize::of('#'),
171+
));
167172
edit.delete(
168173
TextRange::new(text_range.end() - TextSize::of('#'), text_range.end())
169174
- TextSize::of(suffix),
@@ -224,6 +229,42 @@ string"#;
224229
)
225230
}
226231

232+
#[test]
233+
fn make_raw_byte_string_works() {
234+
check_assist(
235+
make_raw_string,
236+
r#"
237+
fn f() {
238+
let s = $0b"random\nstring";
239+
}
240+
"#,
241+
r##"
242+
fn f() {
243+
let s = br#"random
244+
string"#;
245+
}
246+
"##,
247+
)
248+
}
249+
250+
#[test]
251+
fn make_raw_c_string_works() {
252+
check_assist(
253+
make_raw_string,
254+
r#"
255+
fn f() {
256+
let s = $0c"random\nstring";
257+
}
258+
"#,
259+
r##"
260+
fn f() {
261+
let s = cr#"random
262+
string"#;
263+
}
264+
"##,
265+
)
266+
}
267+
227268
#[test]
228269
fn make_raw_string_hashes_inside_works() {
229270
check_assist(
@@ -348,6 +389,23 @@ string"###;
348389
)
349390
}
350391

392+
#[test]
393+
fn add_hash_works_for_c_str() {
394+
check_assist(
395+
add_hash,
396+
r#"
397+
fn f() {
398+
let s = $0cr"random string";
399+
}
400+
"#,
401+
r##"
402+
fn f() {
403+
let s = cr#"random string"#;
404+
}
405+
"##,
406+
)
407+
}
408+
351409
#[test]
352410
fn add_hash_has_suffix_works() {
353411
check_assist(
@@ -433,6 +491,15 @@ string"###;
433491
)
434492
}
435493

494+
#[test]
495+
fn remove_hash_works_for_c_str() {
496+
check_assist(
497+
remove_hash,
498+
r##"fn f() { let s = $0cr#"random string"#; }"##,
499+
r#"fn f() { let s = cr"random string"; }"#,
500+
)
501+
}
502+
436503
#[test]
437504
fn remove_hash_has_suffix_works() {
438505
check_assist(
@@ -529,6 +596,23 @@ string"###;
529596
)
530597
}
531598

599+
#[test]
600+
fn make_usual_string_for_c_str() {
601+
check_assist(
602+
make_usual_string,
603+
r##"
604+
fn f() {
605+
let s = $0cr#"random string"#;
606+
}
607+
"##,
608+
r#"
609+
fn f() {
610+
let s = c"random string";
611+
}
612+
"#,
613+
)
614+
}
615+
532616
#[test]
533617
fn make_usual_string_has_suffix_works() {
534618
check_assist(

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<'_>,

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)