1use ide_db::source_change::SourceChangeBuilder;
2use syntax::{
3 AstNode, AstToken,
4 ast::{self, IsString},
5};
6
7use crate::{
8 AssistContext, AssistId, Assists,
9 utils::{required_hashes, string_prefix, string_suffix},
10};
11
12pub(crate) fn make_raw_string(acc: &mut Assists, ctx: &AssistContext<'_, '_>) -> Option<()> {
28 let token = ctx.find_token_at_offset::<ast::AnyString>()?;
29 if token.is_raw() {
30 return None;
31 }
32 let value = token.value().ok()?;
33 let target = token.syntax().text_range();
34 acc.add(
35 AssistId::refactor_rewrite("make_raw_string"),
36 "Rewrite as raw string",
37 target,
38 |edit| {
39 let hashes = "#".repeat(required_hashes(&value).max(1));
40 let raw_prefix = token.raw_prefix();
41 let suffix = string_suffix(token.text()).unwrap_or_default();
42 let new_str = format!("{raw_prefix}{hashes}\"{value}\"{hashes}{suffix}");
43 replace_literal(&token, &new_str, edit, ctx);
44 },
45 )
46}
47
48pub(crate) fn make_usual_string(acc: &mut Assists, ctx: &AssistContext<'_, '_>) -> Option<()> {
64 let token = ctx.find_token_at_offset::<ast::AnyString>()?;
65 if !token.is_raw() {
66 return None;
67 }
68 let value = token.value().ok()?;
69 let target = token.syntax().text_range();
70 acc.add(
71 AssistId::refactor_rewrite("make_usual_string"),
72 "Rewrite as regular string",
73 target,
74 |edit| {
75 let escaped = value.escape_default().to_string();
77 let suffix = string_suffix(token.text()).unwrap_or_default();
78 let prefix = string_prefix(token.text()).map_or("", |s| s.trim_end_matches('r'));
79 let new_str = format!("{prefix}\"{escaped}\"{suffix}");
80 replace_literal(&token, &new_str, edit, ctx);
81 },
82 )
83}
84
85pub(crate) fn add_hash(acc: &mut Assists, ctx: &AssistContext<'_, '_>) -> Option<()> {
101 let token = ctx.find_token_at_offset::<ast::AnyString>()?;
102 if !token.is_raw() {
103 return None;
104 }
105 let target = token.syntax().text_range();
106 acc.add(AssistId::refactor("add_hash"), "Add #", target, |edit| {
107 let str = token.text();
108 let suffix = string_suffix(str).unwrap_or_default();
109 let raw_prefix = token.raw_prefix();
110 let wrap_range = raw_prefix.len()..str.len() - suffix.len();
111 let new_str = [raw_prefix, "#", &str[wrap_range], "#", suffix].concat();
112 replace_literal(&token, &new_str, edit, ctx);
113 })
114}
115
116pub(crate) fn remove_hash(acc: &mut Assists, ctx: &AssistContext<'_, '_>) -> Option<()> {
132 let token = ctx.find_token_at_offset::<ast::AnyString>()?;
133 if !token.is_raw() {
134 return None;
135 }
136
137 let text = token.text();
138
139 let existing_hashes =
140 text.chars().skip(token.raw_prefix().len()).take_while(|&it| it == '#').count();
141
142 let text_range = token.syntax().text_range();
143 let internal_text = &text[token.text_range_between_quotes()? - text_range.start()];
144
145 if existing_hashes == required_hashes(internal_text) {
146 cov_mark::hit!(cant_remove_required_hash);
147 return None;
148 }
149
150 acc.add(AssistId::refactor_rewrite("remove_hash"), "Remove #", text_range, |edit| {
151 let suffix = string_suffix(text).unwrap_or_default();
152 let prefix = token.raw_prefix();
153 let wrap_range = prefix.len() + 1..text.len() - suffix.len() - 1;
154 let new_str = [prefix, &text[wrap_range], suffix].concat();
155 replace_literal(&token, &new_str, edit, ctx);
156 })
157}
158
159fn replace_literal(
160 token: &impl AstToken,
161 new: &str,
162 builder: &mut SourceChangeBuilder,
163 ctx: &AssistContext<'_, '_>,
164) {
165 let old_token = token.syntax();
166 let parent = old_token.parent().expect("no parent token");
167 let editor = builder.make_editor(&parent);
168 let make = editor.make();
169 let new_literal = make.expr_literal(new);
170 let new_token = new_literal
171 .syntax()
172 .first_child_or_token()
173 .and_then(|it| it.into_token())
174 .expect("literal has no token child");
175 editor.replace(old_token, new_token);
176 builder.add_file_edits(ctx.vfs_file_id(), editor);
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182 use crate::tests::{check_assist, check_assist_not_applicable, check_assist_target};
183
184 #[test]
185 fn make_raw_string_target() {
186 check_assist_target(
187 make_raw_string,
188 r#"
189 fn f() {
190 let s = $0"random\nstring";
191 }
192 "#,
193 r#""random\nstring""#,
194 );
195 }
196
197 #[test]
198 fn make_raw_string_works() {
199 check_assist(
200 make_raw_string,
201 r#"
202fn f() {
203 let s = $0"random\nstring";
204}
205"#,
206 r##"
207fn f() {
208 let s = r#"random
209string"#;
210}
211"##,
212 )
213 }
214
215 #[test]
216 fn make_raw_string_works_inside_macros() {
217 check_assist(
218 make_raw_string,
219 r#"
220 fn f() {
221 format!($0"x = {}", 92)
222 }
223 "#,
224 r##"
225 fn f() {
226 format!(r#"x = {}"#, 92)
227 }
228 "##,
229 )
230 }
231
232 #[test]
233 fn make_raw_byte_string_works() {
234 check_assist(
235 make_raw_string,
236 r#"
237fn f() {
238 let s = $0b"random\nstring";
239}
240"#,
241 r##"
242fn f() {
243 let s = br#"random
244string"#;
245}
246"##,
247 )
248 }
249
250 #[test]
251 fn make_raw_c_string_works() {
252 check_assist(
253 make_raw_string,
254 r#"
255fn f() {
256 let s = $0c"random\nstring";
257}
258"#,
259 r##"
260fn f() {
261 let s = cr#"random
262string"#;
263}
264"##,
265 )
266 }
267
268 #[test]
269 fn make_raw_string_hashes_inside_works() {
270 check_assist(
271 make_raw_string,
272 r###"
273fn f() {
274 let s = $0"#random##\nstring";
275}
276"###,
277 r####"
278fn f() {
279 let s = r#"#random##
280string"#;
281}
282"####,
283 )
284 }
285
286 #[test]
287 fn make_raw_string_closing_hashes_inside_works() {
288 check_assist(
289 make_raw_string,
290 r###"
291fn f() {
292 let s = $0"#random\"##\nstring";
293}
294"###,
295 r####"
296fn f() {
297 let s = r###"#random"##
298string"###;
299}
300"####,
301 )
302 }
303
304 #[test]
305 fn make_raw_string_nothing_to_unescape_works() {
306 check_assist(
307 make_raw_string,
308 r#"
309 fn f() {
310 let s = $0"random string";
311 }
312 "#,
313 r##"
314 fn f() {
315 let s = r#"random string"#;
316 }
317 "##,
318 )
319 }
320
321 #[test]
322 fn make_raw_string_has_suffix() {
323 check_assist(
324 make_raw_string,
325 r#"
326 fn f() {
327 let s = $0"random string"i32;
328 }
329 "#,
330 r##"
331 fn f() {
332 let s = r#"random string"#i32;
333 }
334 "##,
335 )
336 }
337
338 #[test]
339 fn make_raw_string_not_works_on_partial_string() {
340 check_assist_not_applicable(
341 make_raw_string,
342 r#"
343 fn f() {
344 let s = "foo$0
345 }
346 "#,
347 )
348 }
349
350 #[test]
351 fn make_usual_string_not_works_on_partial_string() {
352 check_assist_not_applicable(
353 make_usual_string,
354 r#"
355 fn main() {
356 let s = r#"bar$0
357 }
358 "#,
359 )
360 }
361
362 #[test]
363 fn add_hash_target() {
364 check_assist_target(
365 add_hash,
366 r#"
367 fn f() {
368 let s = $0r"random string";
369 }
370 "#,
371 r#"r"random string""#,
372 );
373 }
374
375 #[test]
376 fn add_hash_works() {
377 check_assist(
378 add_hash,
379 r#"
380 fn f() {
381 let s = $0r"random string";
382 }
383 "#,
384 r##"
385 fn f() {
386 let s = r#"random string"#;
387 }
388 "##,
389 )
390 }
391
392 #[test]
393 fn add_hash_works_for_c_str() {
394 check_assist(
395 add_hash,
396 r#"
397 fn f() {
398 let s = $0cr"random string";
399 }
400 "#,
401 r##"
402 fn f() {
403 let s = cr#"random string"#;
404 }
405 "##,
406 )
407 }
408
409 #[test]
410 fn add_hash_has_suffix_works() {
411 check_assist(
412 add_hash,
413 r#"
414 fn f() {
415 let s = $0r"random string"i32;
416 }
417 "#,
418 r##"
419 fn f() {
420 let s = r#"random string"#i32;
421 }
422 "##,
423 )
424 }
425
426 #[test]
427 fn add_more_hash_works() {
428 check_assist(
429 add_hash,
430 r##"
431 fn f() {
432 let s = $0r#"random"string"#;
433 }
434 "##,
435 r###"
436 fn f() {
437 let s = r##"random"string"##;
438 }
439 "###,
440 )
441 }
442
443 #[test]
444 fn add_more_hash_has_suffix_works() {
445 check_assist(
446 add_hash,
447 r##"
448 fn f() {
449 let s = $0r#"random"string"#i32;
450 }
451 "##,
452 r###"
453 fn f() {
454 let s = r##"random"string"##i32;
455 }
456 "###,
457 )
458 }
459
460 #[test]
461 fn add_hash_not_works() {
462 check_assist_not_applicable(
463 add_hash,
464 r#"
465 fn f() {
466 let s = $0"random string";
467 }
468 "#,
469 );
470 }
471
472 #[test]
473 fn remove_hash_target() {
474 check_assist_target(
475 remove_hash,
476 r##"
477 fn f() {
478 let s = $0r#"random string"#;
479 }
480 "##,
481 r##"r#"random string"#"##,
482 );
483 }
484
485 #[test]
486 fn remove_hash_works() {
487 check_assist(
488 remove_hash,
489 r##"fn f() { let s = $0r#"random string"#; }"##,
490 r#"fn f() { let s = r"random string"; }"#,
491 )
492 }
493
494 #[test]
495 fn remove_hash_works_for_c_str() {
496 check_assist(
497 remove_hash,
498 r##"fn f() { let s = $0cr#"random string"#; }"##,
499 r#"fn f() { let s = cr"random string"; }"#,
500 )
501 }
502
503 #[test]
504 fn remove_hash_has_suffix_works() {
505 check_assist(
506 remove_hash,
507 r##"fn f() { let s = $0r#"random string"#i32; }"##,
508 r#"fn f() { let s = r"random string"i32; }"#,
509 )
510 }
511
512 #[test]
513 fn cant_remove_required_hash() {
514 cov_mark::check!(cant_remove_required_hash);
515 check_assist_not_applicable(
516 remove_hash,
517 r##"
518 fn f() {
519 let s = $0r#"random"str"ing"#;
520 }
521 "##,
522 )
523 }
524
525 #[test]
526 fn remove_more_hash_works() {
527 check_assist(
528 remove_hash,
529 r###"
530 fn f() {
531 let s = $0r##"random string"##;
532 }
533 "###,
534 r##"
535 fn f() {
536 let s = r#"random string"#;
537 }
538 "##,
539 )
540 }
541
542 #[test]
543 fn remove_more_hash_has_suffix_works() {
544 check_assist(
545 remove_hash,
546 r###"
547 fn f() {
548 let s = $0r##"random string"##i32;
549 }
550 "###,
551 r##"
552 fn f() {
553 let s = r#"random string"#i32;
554 }
555 "##,
556 )
557 }
558
559 #[test]
560 fn remove_hash_does_not_work() {
561 check_assist_not_applicable(remove_hash, r#"fn f() { let s = $0"random string"; }"#);
562 }
563
564 #[test]
565 fn remove_hash_no_hash_does_not_work() {
566 check_assist_not_applicable(remove_hash, r#"fn f() { let s = $0r"random string"; }"#);
567 }
568
569 #[test]
570 fn make_usual_string_target() {
571 check_assist_target(
572 make_usual_string,
573 r##"
574 fn f() {
575 let s = $0r#"random string"#;
576 }
577 "##,
578 r##"r#"random string"#"##,
579 );
580 }
581
582 #[test]
583 fn make_usual_string_works() {
584 check_assist(
585 make_usual_string,
586 r##"
587 fn f() {
588 let s = $0r#"random string"#;
589 }
590 "##,
591 r#"
592 fn f() {
593 let s = "random string";
594 }
595 "#,
596 )
597 }
598
599 #[test]
600 fn make_usual_string_for_c_str() {
601 check_assist(
602 make_usual_string,
603 r##"
604 fn f() {
605 let s = $0cr#"random string"#;
606 }
607 "##,
608 r#"
609 fn f() {
610 let s = c"random string";
611 }
612 "#,
613 )
614 }
615
616 #[test]
617 fn make_usual_string_has_suffix_works() {
618 check_assist(
619 make_usual_string,
620 r##"
621 fn f() {
622 let s = $0r#"random string"#i32;
623 }
624 "##,
625 r#"
626 fn f() {
627 let s = "random string"i32;
628 }
629 "#,
630 )
631 }
632
633 #[test]
634 fn make_usual_string_with_quote_works() {
635 check_assist(
636 make_usual_string,
637 r##"
638 fn f() {
639 let s = $0r#"random"str"ing"#;
640 }
641 "##,
642 r#"
643 fn f() {
644 let s = "random\"str\"ing";
645 }
646 "#,
647 )
648 }
649
650 #[test]
651 fn make_usual_string_more_hash_works() {
652 check_assist(
653 make_usual_string,
654 r###"
655 fn f() {
656 let s = $0r##"random string"##;
657 }
658 "###,
659 r##"
660 fn f() {
661 let s = "random string";
662 }
663 "##,
664 )
665 }
666
667 #[test]
668 fn make_usual_string_more_hash_has_suffix_works() {
669 check_assist(
670 make_usual_string,
671 r###"
672 fn f() {
673 let s = $0r##"random string"##i32;
674 }
675 "###,
676 r##"
677 fn f() {
678 let s = "random string"i32;
679 }
680 "##,
681 )
682 }
683
684 #[test]
685 fn make_usual_string_not_works() {
686 check_assist_not_applicable(
687 make_usual_string,
688 r#"
689 fn f() {
690 let s = $0"random string";
691 }
692 "#,
693 );
694 }
695}