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