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