diff options
Diffstat (limited to 'crates/ide_assists/src/handlers/raw_string.rs')
-rw-r--r-- | crates/ide_assists/src/handlers/raw_string.rs | 512 |
1 files changed, 512 insertions, 0 deletions
diff --git a/crates/ide_assists/src/handlers/raw_string.rs b/crates/ide_assists/src/handlers/raw_string.rs new file mode 100644 index 000000000..d95267607 --- /dev/null +++ b/crates/ide_assists/src/handlers/raw_string.rs | |||
@@ -0,0 +1,512 @@ | |||
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 | } | ||