]> git.lizzy.rs Git - rust.git/blob - src/tools/rust-analyzer/crates/ide-assists/src/handlers/raw_string.rs
Auto merge of #103913 - Neutron3529:patch-1, r=thomcc
[rust.git] / src / tools / rust-analyzer / crates / ide-assists / src / handlers / raw_string.rs
1 use std::borrow::Cow;
2
3 use syntax::{ast, ast::IsString, AstToken, TextRange, TextSize};
4
5 use crate::{AssistContext, AssistId, AssistKind, Assists};
6
7 // Assist: make_raw_string
8 //
9 // Adds `r#` to a plain string literal.
10 //
11 // ```
12 // fn main() {
13 //     "Hello,$0 World!";
14 // }
15 // ```
16 // ->
17 // ```
18 // fn main() {
19 //     r#"Hello, World!"#;
20 // }
21 // ```
22 pub(crate) fn make_raw_string(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
23     let token = ctx.find_token_at_offset::<ast::String>()?;
24     if token.is_raw() {
25         return None;
26     }
27     let value = token.value()?;
28     let target = token.syntax().text_range();
29     acc.add(
30         AssistId("make_raw_string", AssistKind::RefactorRewrite),
31         "Rewrite as raw string",
32         target,
33         |edit| {
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);
39             } else {
40                 edit.replace(token.syntax().text_range(), format!("r{hashes}\"{value}\"{hashes}"));
41             }
42         },
43     )
44 }
45
46 // Assist: make_usual_string
47 //
48 // Turns a raw string into a plain string.
49 //
50 // ```
51 // fn main() {
52 //     r#"Hello,$0 "World!""#;
53 // }
54 // ```
55 // ->
56 // ```
57 // fn main() {
58 //     "Hello, \"World!\"";
59 // }
60 // ```
61 pub(crate) fn make_usual_string(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
62     let token = ctx.find_token_at_offset::<ast::String>()?;
63     if !token.is_raw() {
64         return None;
65     }
66     let value = token.value()?;
67     let target = token.syntax().text_range();
68     acc.add(
69         AssistId("make_usual_string", AssistKind::RefactorRewrite),
70         "Rewrite as regular string",
71         target,
72         |edit| {
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, "\"");
79                     return;
80                 }
81             }
82
83             edit.replace(token.syntax().text_range(), format!("\"{escaped}\""));
84         },
85     )
86 }
87
88 // Assist: add_hash
89 //
90 // Adds a hash to a raw string literal.
91 //
92 // ```
93 // fn main() {
94 //     r#"Hello,$0 World!"#;
95 // }
96 // ```
97 // ->
98 // ```
99 // fn main() {
100 //     r##"Hello, World!"##;
101 // }
102 // ```
103 pub(crate) fn add_hash(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
104     let token = ctx.find_token_at_offset::<ast::String>()?;
105     if !token.is_raw() {
106         return None;
107     }
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(), "#");
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 // ```
131 pub(crate) fn remove_hash(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
132     let token = ctx.find_token_at_offset::<ast::String>()?;
133     if !token.is_raw() {
134         return None;
135     }
136
137     let text = token.text();
138     if !text.starts_with("r#") && text.ends_with('#') {
139         return None;
140     }
141
142     let existing_hashes = text.chars().skip(1).take_while(|&it| it == '#').count();
143
144     let text_range = token.syntax().text_range();
145     let internal_text = &text[token.text_range_between_quotes()? - text_range.start()];
146
147     if existing_hashes == required_hashes(internal_text) {
148         cov_mark::hit!(cant_remove_required_hash);
149         return None;
150     }
151
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()));
155     })
156 }
157
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)
164     }
165     res
166 }
167
168 #[cfg(test)]
169 mod tests {
170     use crate::tests::{check_assist, check_assist_not_applicable, check_assist_target};
171
172     use super::*;
173
174     #[test]
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"));
183     }
184
185     #[test]
186     fn make_raw_string_target() {
187         check_assist_target(
188             make_raw_string,
189             r#"
190             fn f() {
191                 let s = $0"random\nstring";
192             }
193             "#,
194             r#""random\nstring""#,
195         );
196     }
197
198     #[test]
199     fn make_raw_string_works() {
200         check_assist(
201             make_raw_string,
202             r#"
203 fn f() {
204     let s = $0"random\nstring";
205 }
206 "#,
207             r##"
208 fn f() {
209     let s = r#"random
210 string"#;
211 }
212 "##,
213         )
214     }
215
216     #[test]
217     fn make_raw_string_works_inside_macros() {
218         check_assist(
219             make_raw_string,
220             r#"
221             fn f() {
222                 format!($0"x = {}", 92)
223             }
224             "#,
225             r##"
226             fn f() {
227                 format!(r#"x = {}"#, 92)
228             }
229             "##,
230         )
231     }
232
233     #[test]
234     fn make_raw_string_hashes_inside_works() {
235         check_assist(
236             make_raw_string,
237             r###"
238 fn f() {
239     let s = $0"#random##\nstring";
240 }
241 "###,
242             r####"
243 fn f() {
244     let s = r#"#random##
245 string"#;
246 }
247 "####,
248         )
249     }
250
251     #[test]
252     fn make_raw_string_closing_hashes_inside_works() {
253         check_assist(
254             make_raw_string,
255             r###"
256 fn f() {
257     let s = $0"#random\"##\nstring";
258 }
259 "###,
260             r####"
261 fn f() {
262     let s = r###"#random"##
263 string"###;
264 }
265 "####,
266         )
267     }
268
269     #[test]
270     fn make_raw_string_nothing_to_unescape_works() {
271         check_assist(
272             make_raw_string,
273             r#"
274             fn f() {
275                 let s = $0"random string";
276             }
277             "#,
278             r##"
279             fn f() {
280                 let s = r#"random string"#;
281             }
282             "##,
283         )
284     }
285
286     #[test]
287     fn make_raw_string_not_works_on_partial_string() {
288         check_assist_not_applicable(
289             make_raw_string,
290             r#"
291             fn f() {
292                 let s = "foo$0
293             }
294             "#,
295         )
296     }
297
298     #[test]
299     fn make_usual_string_not_works_on_partial_string() {
300         check_assist_not_applicable(
301             make_usual_string,
302             r#"
303             fn main() {
304                 let s = r#"bar$0
305             }
306             "#,
307         )
308     }
309
310     #[test]
311     fn add_hash_target() {
312         check_assist_target(
313             add_hash,
314             r#"
315             fn f() {
316                 let s = $0r"random string";
317             }
318             "#,
319             r#"r"random string""#,
320         );
321     }
322
323     #[test]
324     fn add_hash_works() {
325         check_assist(
326             add_hash,
327             r#"
328             fn f() {
329                 let s = $0r"random string";
330             }
331             "#,
332             r##"
333             fn f() {
334                 let s = r#"random string"#;
335             }
336             "##,
337         )
338     }
339
340     #[test]
341     fn add_more_hash_works() {
342         check_assist(
343             add_hash,
344             r##"
345             fn f() {
346                 let s = $0r#"random"string"#;
347             }
348             "##,
349             r###"
350             fn f() {
351                 let s = r##"random"string"##;
352             }
353             "###,
354         )
355     }
356
357     #[test]
358     fn add_hash_not_works() {
359         check_assist_not_applicable(
360             add_hash,
361             r#"
362             fn f() {
363                 let s = $0"random string";
364             }
365             "#,
366         );
367     }
368
369     #[test]
370     fn remove_hash_target() {
371         check_assist_target(
372             remove_hash,
373             r##"
374             fn f() {
375                 let s = $0r#"random string"#;
376             }
377             "##,
378             r##"r#"random string"#"##,
379         );
380     }
381
382     #[test]
383     fn remove_hash_works() {
384         check_assist(
385             remove_hash,
386             r##"fn f() { let s = $0r#"random string"#; }"##,
387             r#"fn f() { let s = r"random string"; }"#,
388         )
389     }
390
391     #[test]
392     fn cant_remove_required_hash() {
393         cov_mark::check!(cant_remove_required_hash);
394         check_assist_not_applicable(
395             remove_hash,
396             r##"
397             fn f() {
398                 let s = $0r#"random"str"ing"#;
399             }
400             "##,
401         )
402     }
403
404     #[test]
405     fn remove_more_hash_works() {
406         check_assist(
407             remove_hash,
408             r###"
409             fn f() {
410                 let s = $0r##"random string"##;
411             }
412             "###,
413             r##"
414             fn f() {
415                 let s = r#"random string"#;
416             }
417             "##,
418         )
419     }
420
421     #[test]
422     fn remove_hash_doesnt_work() {
423         check_assist_not_applicable(remove_hash, r#"fn f() { let s = $0"random string"; }"#);
424     }
425
426     #[test]
427     fn remove_hash_no_hash_doesnt_work() {
428         check_assist_not_applicable(remove_hash, r#"fn f() { let s = $0r"random string"; }"#);
429     }
430
431     #[test]
432     fn make_usual_string_target() {
433         check_assist_target(
434             make_usual_string,
435             r##"
436             fn f() {
437                 let s = $0r#"random string"#;
438             }
439             "##,
440             r##"r#"random string"#"##,
441         );
442     }
443
444     #[test]
445     fn make_usual_string_works() {
446         check_assist(
447             make_usual_string,
448             r##"
449             fn f() {
450                 let s = $0r#"random string"#;
451             }
452             "##,
453             r#"
454             fn f() {
455                 let s = "random string";
456             }
457             "#,
458         )
459     }
460
461     #[test]
462     fn make_usual_string_with_quote_works() {
463         check_assist(
464             make_usual_string,
465             r##"
466             fn f() {
467                 let s = $0r#"random"str"ing"#;
468             }
469             "##,
470             r#"
471             fn f() {
472                 let s = "random\"str\"ing";
473             }
474             "#,
475         )
476     }
477
478     #[test]
479     fn make_usual_string_more_hash_works() {
480         check_assist(
481             make_usual_string,
482             r###"
483             fn f() {
484                 let s = $0r##"random string"##;
485             }
486             "###,
487             r##"
488             fn f() {
489                 let s = "random string";
490             }
491             "##,
492         )
493     }
494
495     #[test]
496     fn make_usual_string_not_works() {
497         check_assist_not_applicable(
498             make_usual_string,
499             r#"
500             fn f() {
501                 let s = $0"random string";
502             }
503             "#,
504         );
505     }
506 }