aboutsummaryrefslogtreecommitdiff
path: root/crates/ra_assists/src/handlers/raw_string.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ra_assists/src/handlers/raw_string.rs')
-rw-r--r--crates/ra_assists/src/handlers/raw_string.rs176
1 files changed, 90 insertions, 86 deletions
diff --git a/crates/ra_assists/src/handlers/raw_string.rs b/crates/ra_assists/src/handlers/raw_string.rs
index d22d0aa55..4e8a0c2db 100644
--- a/crates/ra_assists/src/handlers/raw_string.rs
+++ b/crates/ra_assists/src/handlers/raw_string.rs
@@ -1,11 +1,14 @@
1use std::borrow::Cow;
2
1use ra_syntax::{ 3use ra_syntax::{
2 ast::{self, HasStringValue}, 4 ast::{self, HasQuotes, HasStringValue},
3 AstToken, 5 AstToken,
4 SyntaxKind::{RAW_STRING, STRING}, 6 SyntaxKind::{RAW_STRING, STRING},
5 TextSize, 7 TextRange, TextSize,
6}; 8};
9use test_utils::mark;
7 10
8use crate::{AssistContext, AssistId, Assists}; 11use crate::{AssistContext, AssistId, AssistKind, Assists};
9 12
10// Assist: make_raw_string 13// Assist: make_raw_string
11// 14//
@@ -26,14 +29,24 @@ pub(crate) fn make_raw_string(acc: &mut Assists, ctx: &AssistContext) -> Option<
26 let token = ctx.find_token_at_offset(STRING).and_then(ast::String::cast)?; 29 let token = ctx.find_token_at_offset(STRING).and_then(ast::String::cast)?;
27 let value = token.value()?; 30 let value = token.value()?;
28 let target = token.syntax().text_range(); 31 let target = token.syntax().text_range();
29 acc.add(AssistId("make_raw_string"), "Rewrite as raw string", target, |edit| { 32 acc.add(
30 let max_hash_streak = count_hashes(&value); 33 AssistId("make_raw_string", AssistKind::RefactorRewrite),
31 let mut hashes = String::with_capacity(max_hash_streak + 1); 34 "Rewrite as raw string",
32 for _ in 0..hashes.capacity() { 35 target,
33 hashes.push('#'); 36 |edit| {
34 } 37 let hashes = "#".repeat(required_hashes(&value).max(1));
35 edit.replace(token.syntax().text_range(), format!("r{}\"{}\"{}", hashes, value, hashes)); 38 if matches!(value, Cow::Borrowed(_)) {
36 }) 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 )
37} 50}
38 51
39// Assist: make_usual_string 52// Assist: make_usual_string
@@ -55,11 +68,24 @@ pub(crate) fn make_usual_string(acc: &mut Assists, ctx: &AssistContext) -> Optio
55 let token = ctx.find_token_at_offset(RAW_STRING).and_then(ast::RawString::cast)?; 68 let token = ctx.find_token_at_offset(RAW_STRING).and_then(ast::RawString::cast)?;
56 let value = token.value()?; 69 let value = token.value()?;
57 let target = token.syntax().text_range(); 70 let target = token.syntax().text_range();
58 acc.add(AssistId("make_usual_string"), "Rewrite as regular string", target, |edit| { 71 acc.add(
59 // parse inside string to escape `"` 72 AssistId("make_usual_string", AssistKind::RefactorRewrite),
60 let escaped = value.escape_default().to_string(); 73 "Rewrite as regular string",
61 edit.replace(token.syntax().text_range(), format!("\"{}\"", escaped)); 74 target,
62 }) 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 )
63} 89}
64 90
65// Assist: add_hash 91// Assist: add_hash
@@ -80,7 +106,7 @@ pub(crate) fn make_usual_string(acc: &mut Assists, ctx: &AssistContext) -> Optio
80pub(crate) fn add_hash(acc: &mut Assists, ctx: &AssistContext) -> Option<()> { 106pub(crate) fn add_hash(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
81 let token = ctx.find_token_at_offset(RAW_STRING)?; 107 let token = ctx.find_token_at_offset(RAW_STRING)?;
82 let target = token.text_range(); 108 let target = token.text_range();
83 acc.add(AssistId("add_hash"), "Add # to raw string", target, |edit| { 109 acc.add(AssistId("add_hash", AssistKind::Refactor), "Add #", target, |edit| {
84 edit.insert(token.text_range().start() + TextSize::of('r'), "#"); 110 edit.insert(token.text_range().start() + TextSize::of('r'), "#");
85 edit.insert(token.text_range().end(), "#"); 111 edit.insert(token.text_range().end(), "#");
86 }) 112 })
@@ -102,44 +128,58 @@ pub(crate) fn add_hash(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
102// } 128// }
103// ``` 129// ```
104pub(crate) fn remove_hash(acc: &mut Assists, ctx: &AssistContext) -> Option<()> { 130pub(crate) fn remove_hash(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
105 let token = ctx.find_token_at_offset(RAW_STRING)?; 131 let token = ctx.find_token_at_offset(RAW_STRING).and_then(ast::RawString::cast)?;
132
106 let text = token.text().as_str(); 133 let text = token.text().as_str();
107 if text.starts_with("r\"") { 134 if !text.starts_with("r#") && text.ends_with('#') {
108 // no hash to remove
109 return None; 135 return None;
110 } 136 }
111 let target = token.text_range(); 137
112 acc.add(AssistId("remove_hash"), "Remove hash from raw string", target, |edit| { 138 let existing_hashes = text.chars().skip(1).take_while(|&it| it == '#').count();
113 let result = &text[2..text.len() - 1]; 139
114 let result = if result.starts_with('\"') { 140 let text_range = token.syntax().text_range();
115 // FIXME: this logic is wrong, not only the last has has to handled specially 141 let internal_text = &text[token.text_range_between_quotes()? - text_range.start()];
116 // no more hash, escape 142
117 let internal_str = &result[1..result.len() - 1]; 143 if existing_hashes == required_hashes(internal_text) {
118 format!("\"{}\"", internal_str.escape_default().to_string()) 144 mark::hit!(cant_remove_required_hash);
119 } else { 145 return None;
120 result.to_owned() 146 }
121 }; 147
122 edit.replace(token.text_range(), format!("r{}", result)); 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()));
123 }) 151 })
124} 152}
125 153
126fn count_hashes(s: &str) -> usize { 154fn required_hashes(s: &str) -> usize {
127 let mut max_hash_streak = 0usize; 155 let mut res = 0usize;
128 for idx in s.match_indices("\"#").map(|(i, _)| i) { 156 for idx in s.match_indices('"').map(|(i, _)| i) {
129 let (_, sub) = s.split_at(idx + 1); 157 let (_, sub) = s.split_at(idx + 1);
130 let nb_hash = sub.chars().take_while(|c| *c == '#').count(); 158 let n_hashes = sub.chars().take_while(|c| *c == '#').count();
131 if nb_hash > max_hash_streak { 159 res = res.max(n_hashes + 1)
132 max_hash_streak = nb_hash;
133 }
134 } 160 }
135 max_hash_streak 161 res
162}
163
164#[test]
165fn 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"));
136} 173}
137 174
138#[cfg(test)] 175#[cfg(test)]
139mod test { 176mod test {
140 use super::*; 177 use test_utils::mark;
178
141 use crate::tests::{check_assist, check_assist_not_applicable, check_assist_target}; 179 use crate::tests::{check_assist, check_assist_not_applicable, check_assist_target};
142 180
181 use super::*;
182
143 #[test] 183 #[test]
144 fn make_raw_string_target() { 184 fn make_raw_string_target() {
145 check_assist_target( 185 check_assist_target(
@@ -341,33 +381,21 @@ string"###;
341 fn remove_hash_works() { 381 fn remove_hash_works() {
342 check_assist( 382 check_assist(
343 remove_hash, 383 remove_hash,
344 r##" 384 r##"fn f() { let s = <|>r#"random string"#; }"##,
345 fn f() { 385 r#"fn f() { let s = r"random string"; }"#,
346 let s = <|>r#"random string"#;
347 }
348 "##,
349 r#"
350 fn f() {
351 let s = r"random string";
352 }
353 "#,
354 ) 386 )
355 } 387 }
356 388
357 #[test] 389 #[test]
358 fn remove_hash_with_quote_works() { 390 fn cant_remove_required_hash() {
359 check_assist( 391 mark::check!(cant_remove_required_hash);
392 check_assist_not_applicable(
360 remove_hash, 393 remove_hash,
361 r##" 394 r##"
362 fn f() { 395 fn f() {
363 let s = <|>r#"random"str"ing"#; 396 let s = <|>r#"random"str"ing"#;
364 } 397 }
365 "##, 398 "##,
366 r#"
367 fn f() {
368 let s = r"random\"str\"ing";
369 }
370 "#,
371 ) 399 )
372 } 400 }
373 401
@@ -389,27 +417,13 @@ string"###;
389 } 417 }
390 418
391 #[test] 419 #[test]
392 fn remove_hash_not_works() { 420 fn remove_hash_doesnt_work() {
393 check_assist_not_applicable( 421 check_assist_not_applicable(remove_hash, r#"fn f() { let s = <|>"random string"; }"#);
394 remove_hash,
395 r#"
396 fn f() {
397 let s = <|>"random string";
398 }
399 "#,
400 );
401 } 422 }
402 423
403 #[test] 424 #[test]
404 fn remove_hash_no_hash_not_works() { 425 fn remove_hash_no_hash_doesnt_work() {
405 check_assist_not_applicable( 426 check_assist_not_applicable(remove_hash, r#"fn f() { let s = <|>r"random string"; }"#);
406 remove_hash,
407 r#"
408 fn f() {
409 let s = <|>r"random string";
410 }
411 "#,
412 );
413 } 427 }
414 428
415 #[test] 429 #[test]
@@ -487,14 +501,4 @@ string"###;
487 "#, 501 "#,
488 ); 502 );
489 } 503 }
490
491 #[test]
492 fn count_hashes_test() {
493 assert_eq!(0, count_hashes("abc"));
494 assert_eq!(0, count_hashes("###"));
495 assert_eq!(1, count_hashes("\"#abc"));
496 assert_eq!(0, count_hashes("#abc"));
497 assert_eq!(2, count_hashes("#ab\"##c"));
498 assert_eq!(4, count_hashes("#ab\"##\"####c"));
499 }
500} 504}