Skip to main content

ide_assists/handlers/
raw_string.rs

1use ide_db::source_change::SourceChangeBuilder;
2use syntax::{
3    AstNode, AstToken,
4    ast::{self, IsString},
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 old_token = token.syntax();
166    let parent = old_token.parent().expect("no parent token");
167    let editor = builder.make_editor(&parent);
168    let make = editor.make();
169    let new_literal = make.expr_literal(new);
170    let new_token = new_literal
171        .syntax()
172        .first_child_or_token()
173        .and_then(|it| it.into_token())
174        .expect("literal has no token child");
175    editor.replace(old_token, new_token);
176    builder.add_file_edits(ctx.vfs_file_id(), editor);
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182    use crate::tests::{check_assist, check_assist_not_applicable, check_assist_target};
183
184    #[test]
185    fn make_raw_string_target() {
186        check_assist_target(
187            make_raw_string,
188            r#"
189            fn f() {
190                let s = $0"random\nstring";
191            }
192            "#,
193            r#""random\nstring""#,
194        );
195    }
196
197    #[test]
198    fn make_raw_string_works() {
199        check_assist(
200            make_raw_string,
201            r#"
202fn f() {
203    let s = $0"random\nstring";
204}
205"#,
206            r##"
207fn f() {
208    let s = r#"random
209string"#;
210}
211"##,
212        )
213    }
214
215    #[test]
216    fn make_raw_string_works_inside_macros() {
217        check_assist(
218            make_raw_string,
219            r#"
220            fn f() {
221                format!($0"x = {}", 92)
222            }
223            "#,
224            r##"
225            fn f() {
226                format!(r#"x = {}"#, 92)
227            }
228            "##,
229        )
230    }
231
232    #[test]
233    fn make_raw_byte_string_works() {
234        check_assist(
235            make_raw_string,
236            r#"
237fn f() {
238    let s = $0b"random\nstring";
239}
240"#,
241            r##"
242fn f() {
243    let s = br#"random
244string"#;
245}
246"##,
247        )
248    }
249
250    #[test]
251    fn make_raw_c_string_works() {
252        check_assist(
253            make_raw_string,
254            r#"
255fn f() {
256    let s = $0c"random\nstring";
257}
258"#,
259            r##"
260fn f() {
261    let s = cr#"random
262string"#;
263}
264"##,
265        )
266    }
267
268    #[test]
269    fn make_raw_string_hashes_inside_works() {
270        check_assist(
271            make_raw_string,
272            r###"
273fn f() {
274    let s = $0"#random##\nstring";
275}
276"###,
277            r####"
278fn f() {
279    let s = r#"#random##
280string"#;
281}
282"####,
283        )
284    }
285
286    #[test]
287    fn make_raw_string_closing_hashes_inside_works() {
288        check_assist(
289            make_raw_string,
290            r###"
291fn f() {
292    let s = $0"#random\"##\nstring";
293}
294"###,
295            r####"
296fn f() {
297    let s = r###"#random"##
298string"###;
299}
300"####,
301        )
302    }
303
304    #[test]
305    fn make_raw_string_nothing_to_unescape_works() {
306        check_assist(
307            make_raw_string,
308            r#"
309            fn f() {
310                let s = $0"random string";
311            }
312            "#,
313            r##"
314            fn f() {
315                let s = r#"random string"#;
316            }
317            "##,
318        )
319    }
320
321    #[test]
322    fn make_raw_string_has_suffix() {
323        check_assist(
324            make_raw_string,
325            r#"
326            fn f() {
327                let s = $0"random string"i32;
328            }
329            "#,
330            r##"
331            fn f() {
332                let s = r#"random string"#i32;
333            }
334            "##,
335        )
336    }
337
338    #[test]
339    fn make_raw_string_not_works_on_partial_string() {
340        check_assist_not_applicable(
341            make_raw_string,
342            r#"
343            fn f() {
344                let s = "foo$0
345            }
346            "#,
347        )
348    }
349
350    #[test]
351    fn make_usual_string_not_works_on_partial_string() {
352        check_assist_not_applicable(
353            make_usual_string,
354            r#"
355            fn main() {
356                let s = r#"bar$0
357            }
358            "#,
359        )
360    }
361
362    #[test]
363    fn add_hash_target() {
364        check_assist_target(
365            add_hash,
366            r#"
367            fn f() {
368                let s = $0r"random string";
369            }
370            "#,
371            r#"r"random string""#,
372        );
373    }
374
375    #[test]
376    fn add_hash_works() {
377        check_assist(
378            add_hash,
379            r#"
380            fn f() {
381                let s = $0r"random string";
382            }
383            "#,
384            r##"
385            fn f() {
386                let s = r#"random string"#;
387            }
388            "##,
389        )
390    }
391
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
409    #[test]
410    fn add_hash_has_suffix_works() {
411        check_assist(
412            add_hash,
413            r#"
414            fn f() {
415                let s = $0r"random string"i32;
416            }
417            "#,
418            r##"
419            fn f() {
420                let s = r#"random string"#i32;
421            }
422            "##,
423        )
424    }
425
426    #[test]
427    fn add_more_hash_works() {
428        check_assist(
429            add_hash,
430            r##"
431            fn f() {
432                let s = $0r#"random"string"#;
433            }
434            "##,
435            r###"
436            fn f() {
437                let s = r##"random"string"##;
438            }
439            "###,
440        )
441    }
442
443    #[test]
444    fn add_more_hash_has_suffix_works() {
445        check_assist(
446            add_hash,
447            r##"
448            fn f() {
449                let s = $0r#"random"string"#i32;
450            }
451            "##,
452            r###"
453            fn f() {
454                let s = r##"random"string"##i32;
455            }
456            "###,
457        )
458    }
459
460    #[test]
461    fn add_hash_not_works() {
462        check_assist_not_applicable(
463            add_hash,
464            r#"
465            fn f() {
466                let s = $0"random string";
467            }
468            "#,
469        );
470    }
471
472    #[test]
473    fn remove_hash_target() {
474        check_assist_target(
475            remove_hash,
476            r##"
477            fn f() {
478                let s = $0r#"random string"#;
479            }
480            "##,
481            r##"r#"random string"#"##,
482        );
483    }
484
485    #[test]
486    fn remove_hash_works() {
487        check_assist(
488            remove_hash,
489            r##"fn f() { let s = $0r#"random string"#; }"##,
490            r#"fn f() { let s = r"random string"; }"#,
491        )
492    }
493
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
503    #[test]
504    fn remove_hash_has_suffix_works() {
505        check_assist(
506            remove_hash,
507            r##"fn f() { let s = $0r#"random string"#i32; }"##,
508            r#"fn f() { let s = r"random string"i32; }"#,
509        )
510    }
511
512    #[test]
513    fn cant_remove_required_hash() {
514        cov_mark::check!(cant_remove_required_hash);
515        check_assist_not_applicable(
516            remove_hash,
517            r##"
518            fn f() {
519                let s = $0r#"random"str"ing"#;
520            }
521            "##,
522        )
523    }
524
525    #[test]
526    fn remove_more_hash_works() {
527        check_assist(
528            remove_hash,
529            r###"
530            fn f() {
531                let s = $0r##"random string"##;
532            }
533            "###,
534            r##"
535            fn f() {
536                let s = r#"random string"#;
537            }
538            "##,
539        )
540    }
541
542    #[test]
543    fn remove_more_hash_has_suffix_works() {
544        check_assist(
545            remove_hash,
546            r###"
547            fn f() {
548                let s = $0r##"random string"##i32;
549            }
550            "###,
551            r##"
552            fn f() {
553                let s = r#"random string"#i32;
554            }
555            "##,
556        )
557    }
558
559    #[test]
560    fn remove_hash_does_not_work() {
561        check_assist_not_applicable(remove_hash, r#"fn f() { let s = $0"random string"; }"#);
562    }
563
564    #[test]
565    fn remove_hash_no_hash_does_not_work() {
566        check_assist_not_applicable(remove_hash, r#"fn f() { let s = $0r"random string"; }"#);
567    }
568
569    #[test]
570    fn make_usual_string_target() {
571        check_assist_target(
572            make_usual_string,
573            r##"
574            fn f() {
575                let s = $0r#"random string"#;
576            }
577            "##,
578            r##"r#"random string"#"##,
579        );
580    }
581
582    #[test]
583    fn make_usual_string_works() {
584        check_assist(
585            make_usual_string,
586            r##"
587            fn f() {
588                let s = $0r#"random string"#;
589            }
590            "##,
591            r#"
592            fn f() {
593                let s = "random string";
594            }
595            "#,
596        )
597    }
598
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
616    #[test]
617    fn make_usual_string_has_suffix_works() {
618        check_assist(
619            make_usual_string,
620            r##"
621            fn f() {
622                let s = $0r#"random string"#i32;
623            }
624            "##,
625            r#"
626            fn f() {
627                let s = "random string"i32;
628            }
629            "#,
630        )
631    }
632
633    #[test]
634    fn make_usual_string_with_quote_works() {
635        check_assist(
636            make_usual_string,
637            r##"
638            fn f() {
639                let s = $0r#"random"str"ing"#;
640            }
641            "##,
642            r#"
643            fn f() {
644                let s = "random\"str\"ing";
645            }
646            "#,
647        )
648    }
649
650    #[test]
651    fn make_usual_string_more_hash_works() {
652        check_assist(
653            make_usual_string,
654            r###"
655            fn f() {
656                let s = $0r##"random string"##;
657            }
658            "###,
659            r##"
660            fn f() {
661                let s = "random string";
662            }
663            "##,
664        )
665    }
666
667    #[test]
668    fn make_usual_string_more_hash_has_suffix_works() {
669        check_assist(
670            make_usual_string,
671            r###"
672            fn f() {
673                let s = $0r##"random string"##i32;
674            }
675            "###,
676            r##"
677            fn f() {
678                let s = "random string"i32;
679            }
680            "##,
681        )
682    }
683
684    #[test]
685    fn make_usual_string_not_works() {
686        check_assist_not_applicable(
687            make_usual_string,
688            r#"
689            fn f() {
690                let s = $0"random string";
691            }
692            "#,
693        );
694    }
695}