diff options
Diffstat (limited to 'crates/ra_assists/src/handlers/raw_string.rs')
-rw-r--r-- | crates/ra_assists/src/handlers/raw_string.rs | 176 |
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 @@ | |||
1 | use std::borrow::Cow; | ||
2 | |||
1 | use ra_syntax::{ | 3 | use 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 | }; |
9 | use test_utils::mark; | ||
7 | 10 | ||
8 | use crate::{AssistContext, AssistId, Assists}; | 11 | use 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 | |||
80 | pub(crate) fn add_hash(acc: &mut Assists, ctx: &AssistContext) -> Option<()> { | 106 | pub(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 | // ``` |
104 | pub(crate) fn remove_hash(acc: &mut Assists, ctx: &AssistContext) -> Option<()> { | 130 | pub(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 | ||
126 | fn count_hashes(s: &str) -> usize { | 154 | fn 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] | ||
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")); | ||
136 | } | 173 | } |
137 | 174 | ||
138 | #[cfg(test)] | 175 | #[cfg(test)] |
139 | mod test { | 176 | mod 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 | } |