aboutsummaryrefslogtreecommitdiff
path: root/crates/ide_assists/src/handlers/raw_string.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ide_assists/src/handlers/raw_string.rs')
-rw-r--r--crates/ide_assists/src/handlers/raw_string.rs512
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 @@
1use std::borrow::Cow;
2
3use syntax::{ast, AstToken, TextRange, TextSize};
4use test_utils::mark;
5
6use 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// ```
23pub(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// ```
65pub(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// ```
107pub(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// ```
135pub(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
162fn 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]
173fn 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)]
184mod 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#"
209fn f() {
210 let s = $0"random\nstring";
211}
212"#,
213 r##"
214fn f() {
215 let s = r#"random
216string"#;
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###"
244fn f() {
245 let s = $0"#random##\nstring";
246}
247"###,
248 r####"
249fn f() {
250 let s = r#"#random##
251string"#;
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###"
262fn f() {
263 let s = $0"#random\"##\nstring";
264}
265"###,
266 r####"
267fn f() {
268 let s = r###"#random"##
269string"###;
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}