3 use syntax::{ast, ast::IsString, AstToken, TextRange, TextSize};
5 use crate::{AssistContext, AssistId, AssistKind, Assists};
7 // Assist: make_raw_string
9 // Adds `r#` to a plain string literal.
19 // r#"Hello, World!"#;
22 pub(crate) fn make_raw_string(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
23 let token = ctx.find_token_at_offset::<ast::String>()?;
27 let value = token.value()?;
28 let target = token.syntax().text_range();
30 AssistId("make_raw_string", AssistKind::RefactorRewrite),
31 "Rewrite as raw string",
34 let hashes = "#".repeat(required_hashes(&value).max(1));
35 if matches!(value, Cow::Borrowed(_)) {
36 // Avoid replacing the whole string to better position the cursor.
37 edit.insert(token.syntax().text_range().start(), format!("r{hashes}"));
38 edit.insert(token.syntax().text_range().end(), hashes);
40 edit.replace(token.syntax().text_range(), format!("r{hashes}\"{value}\"{hashes}"));
46 // Assist: make_usual_string
48 // Turns a raw string into a plain string.
52 // r#"Hello,$0 "World!""#;
58 // "Hello, \"World!\"";
61 pub(crate) fn make_usual_string(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
62 let token = ctx.find_token_at_offset::<ast::String>()?;
66 let value = token.value()?;
67 let target = token.syntax().text_range();
69 AssistId("make_usual_string", AssistKind::RefactorRewrite),
70 "Rewrite as regular string",
73 // parse inside string to escape `"`
74 let escaped = value.escape_default().to_string();
75 if let Some(offsets) = token.quote_offsets() {
76 if token.text()[offsets.contents - token.syntax().text_range().start()] == escaped {
77 edit.replace(offsets.quotes.0, "\"");
78 edit.replace(offsets.quotes.1, "\"");
83 edit.replace(token.syntax().text_range(), format!("\"{escaped}\""));
90 // Adds a hash to a raw string literal.
94 // r#"Hello,$0 World!"#;
100 // r##"Hello, World!"##;
103 pub(crate) fn add_hash(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
104 let token = ctx.find_token_at_offset::<ast::String>()?;
108 let text_range = token.syntax().text_range();
109 let target = text_range;
110 acc.add(AssistId("add_hash", AssistKind::Refactor), "Add #", target, |edit| {
111 edit.insert(text_range.start() + TextSize::of('r'), "#");
112 edit.insert(text_range.end(), "#");
116 // Assist: remove_hash
118 // Removes a hash from a raw string literal.
122 // r#"Hello,$0 World!"#;
131 pub(crate) fn remove_hash(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
132 let token = ctx.find_token_at_offset::<ast::String>()?;
137 let text = token.text();
138 if !text.starts_with("r#") && text.ends_with('#') {
142 let existing_hashes = text.chars().skip(1).take_while(|&it| it == '#').count();
144 let text_range = token.syntax().text_range();
145 let internal_text = &text[token.text_range_between_quotes()? - text_range.start()];
147 if existing_hashes == required_hashes(internal_text) {
148 cov_mark::hit!(cant_remove_required_hash);
152 acc.add(AssistId("remove_hash", AssistKind::RefactorRewrite), "Remove #", text_range, |edit| {
153 edit.delete(TextRange::at(text_range.start() + TextSize::of('r'), TextSize::of('#')));
154 edit.delete(TextRange::new(text_range.end() - TextSize::of('#'), text_range.end()));
158 fn required_hashes(s: &str) -> usize {
159 let mut res = 0usize;
160 for idx in s.match_indices('"').map(|(i, _)| i) {
161 let (_, sub) = s.split_at(idx + 1);
162 let n_hashes = sub.chars().take_while(|c| *c == '#').count();
163 res = res.max(n_hashes + 1)
170 use crate::tests::{check_assist, check_assist_not_applicable, check_assist_target};
175 fn test_required_hashes() {
176 assert_eq!(0, required_hashes("abc"));
177 assert_eq!(0, required_hashes("###"));
178 assert_eq!(1, required_hashes("\""));
179 assert_eq!(2, required_hashes("\"#abc"));
180 assert_eq!(0, required_hashes("#abc"));
181 assert_eq!(3, required_hashes("#ab\"##c"));
182 assert_eq!(5, required_hashes("#ab\"##\"####c"));
186 fn make_raw_string_target() {
191 let s = $0"random\nstring";
194 r#""random\nstring""#,
199 fn make_raw_string_works() {
204 let s = $0"random\nstring";
217 fn make_raw_string_works_inside_macros() {
222 format!($0"x = {}", 92)
227 format!(r#"x = {}"#, 92)
234 fn make_raw_string_hashes_inside_works() {
239 let s = $0"#random##\nstring";
252 fn make_raw_string_closing_hashes_inside_works() {
257 let s = $0"#random\"##\nstring";
262 let s = r###"#random"##
270 fn make_raw_string_nothing_to_unescape_works() {
275 let s = $0"random string";
280 let s = r#"random string"#;
287 fn make_raw_string_not_works_on_partial_string() {
288 check_assist_not_applicable(
299 fn make_usual_string_not_works_on_partial_string() {
300 check_assist_not_applicable(
311 fn add_hash_target() {
316 let s = $0r"random string";
319 r#"r"random string""#,
324 fn add_hash_works() {
329 let s = $0r"random string";
334 let s = r#"random string"#;
341 fn add_more_hash_works() {
346 let s = $0r#"random"string"#;
351 let s = r##"random"string"##;
358 fn add_hash_not_works() {
359 check_assist_not_applicable(
363 let s = $0"random string";
370 fn remove_hash_target() {
375 let s = $0r#"random string"#;
378 r##"r#"random string"#"##,
383 fn remove_hash_works() {
386 r##"fn f() { let s = $0r#"random string"#; }"##,
387 r#"fn f() { let s = r"random string"; }"#,
392 fn cant_remove_required_hash() {
393 cov_mark::check!(cant_remove_required_hash);
394 check_assist_not_applicable(
398 let s = $0r#"random"str"ing"#;
405 fn remove_more_hash_works() {
410 let s = $0r##"random string"##;
415 let s = r#"random string"#;
422 fn remove_hash_doesnt_work() {
423 check_assist_not_applicable(remove_hash, r#"fn f() { let s = $0"random string"; }"#);
427 fn remove_hash_no_hash_doesnt_work() {
428 check_assist_not_applicable(remove_hash, r#"fn f() { let s = $0r"random string"; }"#);
432 fn make_usual_string_target() {
437 let s = $0r#"random string"#;
440 r##"r#"random string"#"##,
445 fn make_usual_string_works() {
450 let s = $0r#"random string"#;
455 let s = "random string";
462 fn make_usual_string_with_quote_works() {
467 let s = $0r#"random"str"ing"#;
472 let s = "random\"str\"ing";
479 fn make_usual_string_more_hash_works() {
484 let s = $0r##"random string"##;
489 let s = "random string";
496 fn make_usual_string_not_works() {
497 check_assist_not_applicable(
501 let s = $0"random string";