1use ide_db::source_change::SourceChangeBuilder;
2use syntax::{
3 AstToken,
4 ast::{self, IsString, make::tokens::literal},
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 token = token.syntax();
166 let node = token.parent().expect("no parent token");
167 let mut edit = builder.make_editor(&node);
168 let new_literal = literal(new);
169
170 edit.replace(token, mut_token(new_literal));
171
172 builder.add_file_edits(ctx.vfs_file_id(), edit);
173}
174
175fn mut_token(token: syntax::SyntaxToken) -> syntax::SyntaxToken {
176 let node = token.parent().expect("no parent token");
177 node.clone_for_update()
178 .children_with_tokens()
179 .filter_map(|it| it.into_token())
180 .find(|it| it.text_range() == token.text_range() && it.text() == token.text())
181 .unwrap()
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187 use crate::tests::{check_assist, check_assist_not_applicable, check_assist_target};
188
189 #[test]
190 fn make_raw_string_target() {
191 check_assist_target(
192 make_raw_string,
193 r#"
194 fn f() {
195 let s = $0"random\nstring";
196 }
197 "#,
198 r#""random\nstring""#,
199 );
200 }
201
202 #[test]
203 fn make_raw_string_works() {
204 check_assist(
205 make_raw_string,
206 r#"
207fn f() {
208 let s = $0"random\nstring";
209}
210"#,
211 r##"
212fn f() {
213 let s = r#"random
214string"#;
215}
216"##,
217 )
218 }
219
220 #[test]
221 fn make_raw_string_works_inside_macros() {
222 check_assist(
223 make_raw_string,
224 r#"
225 fn f() {
226 format!($0"x = {}", 92)
227 }
228 "#,
229 r##"
230 fn f() {
231 format!(r#"x = {}"#, 92)
232 }
233 "##,
234 )
235 }
236
237 #[test]
238 fn make_raw_byte_string_works() {
239 check_assist(
240 make_raw_string,
241 r#"
242fn f() {
243 let s = $0b"random\nstring";
244}
245"#,
246 r##"
247fn f() {
248 let s = br#"random
249string"#;
250}
251"##,
252 )
253 }
254
255 #[test]
256 fn make_raw_c_string_works() {
257 check_assist(
258 make_raw_string,
259 r#"
260fn f() {
261 let s = $0c"random\nstring";
262}
263"#,
264 r##"
265fn f() {
266 let s = cr#"random
267string"#;
268}
269"##,
270 )
271 }
272
273 #[test]
274 fn make_raw_string_hashes_inside_works() {
275 check_assist(
276 make_raw_string,
277 r###"
278fn f() {
279 let s = $0"#random##\nstring";
280}
281"###,
282 r####"
283fn f() {
284 let s = r#"#random##
285string"#;
286}
287"####,
288 )
289 }
290
291 #[test]
292 fn make_raw_string_closing_hashes_inside_works() {
293 check_assist(
294 make_raw_string,
295 r###"
296fn f() {
297 let s = $0"#random\"##\nstring";
298}
299"###,
300 r####"
301fn f() {
302 let s = r###"#random"##
303string"###;
304}
305"####,
306 )
307 }
308
309 #[test]
310 fn make_raw_string_nothing_to_unescape_works() {
311 check_assist(
312 make_raw_string,
313 r#"
314 fn f() {
315 let s = $0"random string";
316 }
317 "#,
318 r##"
319 fn f() {
320 let s = r#"random string"#;
321 }
322 "##,
323 )
324 }
325
326 #[test]
327 fn make_raw_string_has_suffix() {
328 check_assist(
329 make_raw_string,
330 r#"
331 fn f() {
332 let s = $0"random string"i32;
333 }
334 "#,
335 r##"
336 fn f() {
337 let s = r#"random string"#i32;
338 }
339 "##,
340 )
341 }
342
343 #[test]
344 fn make_raw_string_not_works_on_partial_string() {
345 check_assist_not_applicable(
346 make_raw_string,
347 r#"
348 fn f() {
349 let s = "foo$0
350 }
351 "#,
352 )
353 }
354
355 #[test]
356 fn make_usual_string_not_works_on_partial_string() {
357 check_assist_not_applicable(
358 make_usual_string,
359 r#"
360 fn main() {
361 let s = r#"bar$0
362 }
363 "#,
364 )
365 }
366
367 #[test]
368 fn add_hash_target() {
369 check_assist_target(
370 add_hash,
371 r#"
372 fn f() {
373 let s = $0r"random string";
374 }
375 "#,
376 r#"r"random string""#,
377 );
378 }
379
380 #[test]
381 fn add_hash_works() {
382 check_assist(
383 add_hash,
384 r#"
385 fn f() {
386 let s = $0r"random string";
387 }
388 "#,
389 r##"
390 fn f() {
391 let s = r#"random string"#;
392 }
393 "##,
394 )
395 }
396
397 #[test]
398 fn add_hash_works_for_c_str() {
399 check_assist(
400 add_hash,
401 r#"
402 fn f() {
403 let s = $0cr"random string";
404 }
405 "#,
406 r##"
407 fn f() {
408 let s = cr#"random string"#;
409 }
410 "##,
411 )
412 }
413
414 #[test]
415 fn add_hash_has_suffix_works() {
416 check_assist(
417 add_hash,
418 r#"
419 fn f() {
420 let s = $0r"random string"i32;
421 }
422 "#,
423 r##"
424 fn f() {
425 let s = r#"random string"#i32;
426 }
427 "##,
428 )
429 }
430
431 #[test]
432 fn add_more_hash_works() {
433 check_assist(
434 add_hash,
435 r##"
436 fn f() {
437 let s = $0r#"random"string"#;
438 }
439 "##,
440 r###"
441 fn f() {
442 let s = r##"random"string"##;
443 }
444 "###,
445 )
446 }
447
448 #[test]
449 fn add_more_hash_has_suffix_works() {
450 check_assist(
451 add_hash,
452 r##"
453 fn f() {
454 let s = $0r#"random"string"#i32;
455 }
456 "##,
457 r###"
458 fn f() {
459 let s = r##"random"string"##i32;
460 }
461 "###,
462 )
463 }
464
465 #[test]
466 fn add_hash_not_works() {
467 check_assist_not_applicable(
468 add_hash,
469 r#"
470 fn f() {
471 let s = $0"random string";
472 }
473 "#,
474 );
475 }
476
477 #[test]
478 fn remove_hash_target() {
479 check_assist_target(
480 remove_hash,
481 r##"
482 fn f() {
483 let s = $0r#"random string"#;
484 }
485 "##,
486 r##"r#"random string"#"##,
487 );
488 }
489
490 #[test]
491 fn remove_hash_works() {
492 check_assist(
493 remove_hash,
494 r##"fn f() { let s = $0r#"random string"#; }"##,
495 r#"fn f() { let s = r"random string"; }"#,
496 )
497 }
498
499 #[test]
500 fn remove_hash_works_for_c_str() {
501 check_assist(
502 remove_hash,
503 r##"fn f() { let s = $0cr#"random string"#; }"##,
504 r#"fn f() { let s = cr"random string"; }"#,
505 )
506 }
507
508 #[test]
509 fn remove_hash_has_suffix_works() {
510 check_assist(
511 remove_hash,
512 r##"fn f() { let s = $0r#"random string"#i32; }"##,
513 r#"fn f() { let s = r"random string"i32; }"#,
514 )
515 }
516
517 #[test]
518 fn cant_remove_required_hash() {
519 cov_mark::check!(cant_remove_required_hash);
520 check_assist_not_applicable(
521 remove_hash,
522 r##"
523 fn f() {
524 let s = $0r#"random"str"ing"#;
525 }
526 "##,
527 )
528 }
529
530 #[test]
531 fn remove_more_hash_works() {
532 check_assist(
533 remove_hash,
534 r###"
535 fn f() {
536 let s = $0r##"random string"##;
537 }
538 "###,
539 r##"
540 fn f() {
541 let s = r#"random string"#;
542 }
543 "##,
544 )
545 }
546
547 #[test]
548 fn remove_more_hash_has_suffix_works() {
549 check_assist(
550 remove_hash,
551 r###"
552 fn f() {
553 let s = $0r##"random string"##i32;
554 }
555 "###,
556 r##"
557 fn f() {
558 let s = r#"random string"#i32;
559 }
560 "##,
561 )
562 }
563
564 #[test]
565 fn remove_hash_does_not_work() {
566 check_assist_not_applicable(remove_hash, r#"fn f() { let s = $0"random string"; }"#);
567 }
568
569 #[test]
570 fn remove_hash_no_hash_does_not_work() {
571 check_assist_not_applicable(remove_hash, r#"fn f() { let s = $0r"random string"; }"#);
572 }
573
574 #[test]
575 fn make_usual_string_target() {
576 check_assist_target(
577 make_usual_string,
578 r##"
579 fn f() {
580 let s = $0r#"random string"#;
581 }
582 "##,
583 r##"r#"random string"#"##,
584 );
585 }
586
587 #[test]
588 fn make_usual_string_works() {
589 check_assist(
590 make_usual_string,
591 r##"
592 fn f() {
593 let s = $0r#"random string"#;
594 }
595 "##,
596 r#"
597 fn f() {
598 let s = "random string";
599 }
600 "#,
601 )
602 }
603
604 #[test]
605 fn make_usual_string_for_c_str() {
606 check_assist(
607 make_usual_string,
608 r##"
609 fn f() {
610 let s = $0cr#"random string"#;
611 }
612 "##,
613 r#"
614 fn f() {
615 let s = c"random string";
616 }
617 "#,
618 )
619 }
620
621 #[test]
622 fn make_usual_string_has_suffix_works() {
623 check_assist(
624 make_usual_string,
625 r##"
626 fn f() {
627 let s = $0r#"random string"#i32;
628 }
629 "##,
630 r#"
631 fn f() {
632 let s = "random string"i32;
633 }
634 "#,
635 )
636 }
637
638 #[test]
639 fn make_usual_string_with_quote_works() {
640 check_assist(
641 make_usual_string,
642 r##"
643 fn f() {
644 let s = $0r#"random"str"ing"#;
645 }
646 "##,
647 r#"
648 fn f() {
649 let s = "random\"str\"ing";
650 }
651 "#,
652 )
653 }
654
655 #[test]
656 fn make_usual_string_more_hash_works() {
657 check_assist(
658 make_usual_string,
659 r###"
660 fn f() {
661 let s = $0r##"random string"##;
662 }
663 "###,
664 r##"
665 fn f() {
666 let s = "random string";
667 }
668 "##,
669 )
670 }
671
672 #[test]
673 fn make_usual_string_more_hash_has_suffix_works() {
674 check_assist(
675 make_usual_string,
676 r###"
677 fn f() {
678 let s = $0r##"random string"##i32;
679 }
680 "###,
681 r##"
682 fn f() {
683 let s = "random string"i32;
684 }
685 "##,
686 )
687 }
688
689 #[test]
690 fn make_usual_string_not_works() {
691 check_assist_not_applicable(
692 make_usual_string,
693 r#"
694 fn f() {
695 let s = $0"random string";
696 }
697 "#,
698 );
699 }
700}