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