ide_assists/handlers/
raw_string.rs

1use ide_db::source_change::SourceChangeBuilder;
2use syntax::{
3    AstToken,
4    ast::{self, IsString, make::tokens::literal},
5};
6
7use crate::{
8    AssistContext, AssistId, Assists,
9    utils::{required_hashes, string_prefix, string_suffix},
10};
11
12// Assist: make_raw_string
13//
14// Adds `r#` to a plain string literal.
15//
16// ```
17// fn main() {
18//     "Hello,$0 World!";
19// }
20// ```
21// ->
22// ```
23// fn main() {
24//     r#"Hello, World!"#;
25// }
26// ```
27pub(crate) fn make_raw_string(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
28    let token = ctx.find_token_at_offset::<ast::AnyString>()?;
29    if token.is_raw() {
30        return None;
31    }
32    let value = token.value().ok()?;
33    let target = token.syntax().text_range();
34    acc.add(
35        AssistId::refactor_rewrite("make_raw_string"),
36        "Rewrite as raw string",
37        target,
38        |edit| {
39            let hashes = "#".repeat(required_hashes(&value).max(1));
40            let raw_prefix = token.raw_prefix();
41            let suffix = string_suffix(token.text()).unwrap_or_default();
42            let new_str = format!("{raw_prefix}{hashes}\"{value}\"{hashes}{suffix}");
43            replace_literal(&token, &new_str, edit, ctx);
44        },
45    )
46}
47
48// Assist: make_usual_string
49//
50// Turns a raw string into a plain string.
51//
52// ```
53// fn main() {
54//     r#"Hello,$0 "World!""#;
55// }
56// ```
57// ->
58// ```
59// fn main() {
60//     "Hello, \"World!\"";
61// }
62// ```
63pub(crate) fn make_usual_string(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
64    let token = ctx.find_token_at_offset::<ast::AnyString>()?;
65    if !token.is_raw() {
66        return None;
67    }
68    let value = token.value().ok()?;
69    let target = token.syntax().text_range();
70    acc.add(
71        AssistId::refactor_rewrite("make_usual_string"),
72        "Rewrite as regular string",
73        target,
74        |edit| {
75            // parse inside string to escape `"`
76            let escaped = value.escape_default().to_string();
77            let suffix = string_suffix(token.text()).unwrap_or_default();
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);
81        },
82    )
83}
84
85// Assist: add_hash
86//
87// Adds a hash to a raw string literal.
88//
89// ```
90// fn main() {
91//     r#"Hello,$0 World!"#;
92// }
93// ```
94// ->
95// ```
96// fn main() {
97//     r##"Hello, World!"##;
98// }
99// ```
100pub(crate) fn add_hash(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
101    let token = ctx.find_token_at_offset::<ast::AnyString>()?;
102    if !token.is_raw() {
103        return None;
104    }
105    let target = token.syntax().text_range();
106    acc.add(AssistId::refactor("add_hash"), "Add #", target, |edit| {
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);
113    })
114}
115
116// Assist: remove_hash
117//
118// Removes a hash from a raw string literal.
119//
120// ```
121// fn main() {
122//     r#"Hello,$0 World!"#;
123// }
124// ```
125// ->
126// ```
127// fn main() {
128//     r"Hello, World!";
129// }
130// ```
131pub(crate) fn remove_hash(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
132    let token = ctx.find_token_at_offset::<ast::AnyString>()?;
133    if !token.is_raw() {
134        return None;
135    }
136
137    let text = token.text();
138
139    let existing_hashes =
140        text.chars().skip(token.raw_prefix().len()).take_while(|&it| it == '#').count();
141
142    let text_range = token.syntax().text_range();
143    let internal_text = &text[token.text_range_between_quotes()? - text_range.start()];
144
145    if existing_hashes == required_hashes(internal_text) {
146        cov_mark::hit!(cant_remove_required_hash);
147        return None;
148    }
149
150    acc.add(AssistId::refactor_rewrite("remove_hash"), "Remove #", text_range, |edit| {
151        let suffix = string_suffix(text).unwrap_or_default();
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);
156    })
157}
158
159fn 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
175fn 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
184#[cfg(test)]
185mod tests {
186    use super::*;
187    use crate::tests::{check_assist, check_assist_not_applicable, check_assist_target};
188
189    #[test]
190    fn make_raw_string_target() {
191        check_assist_target(
192            make_raw_string,
193            r#"
194            fn f() {
195                let s = $0"random\nstring";
196            }
197            "#,
198            r#""random\nstring""#,
199        );
200    }
201
202    #[test]
203    fn make_raw_string_works() {
204        check_assist(
205            make_raw_string,
206            r#"
207fn f() {
208    let s = $0"random\nstring";
209}
210"#,
211            r##"
212fn f() {
213    let s = r#"random
214string"#;
215}
216"##,
217        )
218    }
219
220    #[test]
221    fn make_raw_string_works_inside_macros() {
222        check_assist(
223            make_raw_string,
224            r#"
225            fn f() {
226                format!($0"x = {}", 92)
227            }
228            "#,
229            r##"
230            fn f() {
231                format!(r#"x = {}"#, 92)
232            }
233            "##,
234        )
235    }
236
237    #[test]
238    fn make_raw_byte_string_works() {
239        check_assist(
240            make_raw_string,
241            r#"
242fn f() {
243    let s = $0b"random\nstring";
244}
245"#,
246            r##"
247fn f() {
248    let s = br#"random
249string"#;
250}
251"##,
252        )
253    }
254
255    #[test]
256    fn make_raw_c_string_works() {
257        check_assist(
258            make_raw_string,
259            r#"
260fn f() {
261    let s = $0c"random\nstring";
262}
263"#,
264            r##"
265fn f() {
266    let s = cr#"random
267string"#;
268}
269"##,
270        )
271    }
272
273    #[test]
274    fn make_raw_string_hashes_inside_works() {
275        check_assist(
276            make_raw_string,
277            r###"
278fn f() {
279    let s = $0"#random##\nstring";
280}
281"###,
282            r####"
283fn f() {
284    let s = r#"#random##
285string"#;
286}
287"####,
288        )
289    }
290
291    #[test]
292    fn make_raw_string_closing_hashes_inside_works() {
293        check_assist(
294            make_raw_string,
295            r###"
296fn f() {
297    let s = $0"#random\"##\nstring";
298}
299"###,
300            r####"
301fn f() {
302    let s = r###"#random"##
303string"###;
304}
305"####,
306        )
307    }
308
309    #[test]
310    fn make_raw_string_nothing_to_unescape_works() {
311        check_assist(
312            make_raw_string,
313            r#"
314            fn f() {
315                let s = $0"random string";
316            }
317            "#,
318            r##"
319            fn f() {
320                let s = r#"random string"#;
321            }
322            "##,
323        )
324    }
325
326    #[test]
327    fn make_raw_string_has_suffix() {
328        check_assist(
329            make_raw_string,
330            r#"
331            fn f() {
332                let s = $0"random string"i32;
333            }
334            "#,
335            r##"
336            fn f() {
337                let s = r#"random string"#i32;
338            }
339            "##,
340        )
341    }
342
343    #[test]
344    fn make_raw_string_not_works_on_partial_string() {
345        check_assist_not_applicable(
346            make_raw_string,
347            r#"
348            fn f() {
349                let s = "foo$0
350            }
351            "#,
352        )
353    }
354
355    #[test]
356    fn make_usual_string_not_works_on_partial_string() {
357        check_assist_not_applicable(
358            make_usual_string,
359            r#"
360            fn main() {
361                let s = r#"bar$0
362            }
363            "#,
364        )
365    }
366
367    #[test]
368    fn add_hash_target() {
369        check_assist_target(
370            add_hash,
371            r#"
372            fn f() {
373                let s = $0r"random string";
374            }
375            "#,
376            r#"r"random string""#,
377        );
378    }
379
380    #[test]
381    fn add_hash_works() {
382        check_assist(
383            add_hash,
384            r#"
385            fn f() {
386                let s = $0r"random string";
387            }
388            "#,
389            r##"
390            fn f() {
391                let s = r#"random string"#;
392            }
393            "##,
394        )
395    }
396
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
414    #[test]
415    fn add_hash_has_suffix_works() {
416        check_assist(
417            add_hash,
418            r#"
419            fn f() {
420                let s = $0r"random string"i32;
421            }
422            "#,
423            r##"
424            fn f() {
425                let s = r#"random string"#i32;
426            }
427            "##,
428        )
429    }
430
431    #[test]
432    fn add_more_hash_works() {
433        check_assist(
434            add_hash,
435            r##"
436            fn f() {
437                let s = $0r#"random"string"#;
438            }
439            "##,
440            r###"
441            fn f() {
442                let s = r##"random"string"##;
443            }
444            "###,
445        )
446    }
447
448    #[test]
449    fn add_more_hash_has_suffix_works() {
450        check_assist(
451            add_hash,
452            r##"
453            fn f() {
454                let s = $0r#"random"string"#i32;
455            }
456            "##,
457            r###"
458            fn f() {
459                let s = r##"random"string"##i32;
460            }
461            "###,
462        )
463    }
464
465    #[test]
466    fn add_hash_not_works() {
467        check_assist_not_applicable(
468            add_hash,
469            r#"
470            fn f() {
471                let s = $0"random string";
472            }
473            "#,
474        );
475    }
476
477    #[test]
478    fn remove_hash_target() {
479        check_assist_target(
480            remove_hash,
481            r##"
482            fn f() {
483                let s = $0r#"random string"#;
484            }
485            "##,
486            r##"r#"random string"#"##,
487        );
488    }
489
490    #[test]
491    fn remove_hash_works() {
492        check_assist(
493            remove_hash,
494            r##"fn f() { let s = $0r#"random string"#; }"##,
495            r#"fn f() { let s = r"random string"; }"#,
496        )
497    }
498
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
508    #[test]
509    fn remove_hash_has_suffix_works() {
510        check_assist(
511            remove_hash,
512            r##"fn f() { let s = $0r#"random string"#i32; }"##,
513            r#"fn f() { let s = r"random string"i32; }"#,
514        )
515    }
516
517    #[test]
518    fn cant_remove_required_hash() {
519        cov_mark::check!(cant_remove_required_hash);
520        check_assist_not_applicable(
521            remove_hash,
522            r##"
523            fn f() {
524                let s = $0r#"random"str"ing"#;
525            }
526            "##,
527        )
528    }
529
530    #[test]
531    fn remove_more_hash_works() {
532        check_assist(
533            remove_hash,
534            r###"
535            fn f() {
536                let s = $0r##"random string"##;
537            }
538            "###,
539            r##"
540            fn f() {
541                let s = r#"random string"#;
542            }
543            "##,
544        )
545    }
546
547    #[test]
548    fn remove_more_hash_has_suffix_works() {
549        check_assist(
550            remove_hash,
551            r###"
552            fn f() {
553                let s = $0r##"random string"##i32;
554            }
555            "###,
556            r##"
557            fn f() {
558                let s = r#"random string"#i32;
559            }
560            "##,
561        )
562    }
563
564    #[test]
565    fn remove_hash_does_not_work() {
566        check_assist_not_applicable(remove_hash, r#"fn f() { let s = $0"random string"; }"#);
567    }
568
569    #[test]
570    fn remove_hash_no_hash_does_not_work() {
571        check_assist_not_applicable(remove_hash, r#"fn f() { let s = $0r"random string"; }"#);
572    }
573
574    #[test]
575    fn make_usual_string_target() {
576        check_assist_target(
577            make_usual_string,
578            r##"
579            fn f() {
580                let s = $0r#"random string"#;
581            }
582            "##,
583            r##"r#"random string"#"##,
584        );
585    }
586
587    #[test]
588    fn make_usual_string_works() {
589        check_assist(
590            make_usual_string,
591            r##"
592            fn f() {
593                let s = $0r#"random string"#;
594            }
595            "##,
596            r#"
597            fn f() {
598                let s = "random string";
599            }
600            "#,
601        )
602    }
603
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
621    #[test]
622    fn make_usual_string_has_suffix_works() {
623        check_assist(
624            make_usual_string,
625            r##"
626            fn f() {
627                let s = $0r#"random string"#i32;
628            }
629            "##,
630            r#"
631            fn f() {
632                let s = "random string"i32;
633            }
634            "#,
635        )
636    }
637
638    #[test]
639    fn make_usual_string_with_quote_works() {
640        check_assist(
641            make_usual_string,
642            r##"
643            fn f() {
644                let s = $0r#"random"str"ing"#;
645            }
646            "##,
647            r#"
648            fn f() {
649                let s = "random\"str\"ing";
650            }
651            "#,
652        )
653    }
654
655    #[test]
656    fn make_usual_string_more_hash_works() {
657        check_assist(
658            make_usual_string,
659            r###"
660            fn f() {
661                let s = $0r##"random string"##;
662            }
663            "###,
664            r##"
665            fn f() {
666                let s = "random string";
667            }
668            "##,
669        )
670    }
671
672    #[test]
673    fn make_usual_string_more_hash_has_suffix_works() {
674        check_assist(
675            make_usual_string,
676            r###"
677            fn f() {
678                let s = $0r##"random string"##i32;
679            }
680            "###,
681            r##"
682            fn f() {
683                let s = "random string"i32;
684            }
685            "##,
686        )
687    }
688
689    #[test]
690    fn make_usual_string_not_works() {
691        check_assist_not_applicable(
692            make_usual_string,
693            r#"
694            fn f() {
695                let s = $0"random string";
696            }
697            "#,
698        );
699    }
700}