aboutsummaryrefslogtreecommitdiff
path: root/crates/assists/src/handlers/raw_string.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/assists/src/handlers/raw_string.rs')
-rw-r--r--crates/assists/src/handlers/raw_string.rs504
1 files changed, 504 insertions, 0 deletions
diff --git a/crates/assists/src/handlers/raw_string.rs b/crates/assists/src/handlers/raw_string.rs
new file mode 100644
index 000000000..9ddd116e0
--- /dev/null
+++ b/crates/assists/src/handlers/raw_string.rs
@@ -0,0 +1,504 @@
1use std::borrow::Cow;
2
3use syntax::{
4 ast::{self, HasQuotes, HasStringValue},
5 AstToken,
6 SyntaxKind::{RAW_STRING, STRING},
7 TextRange, TextSize,
8};
9use test_utils::mark;
10
11use crate::{AssistContext, AssistId, AssistKind, Assists};
12
13// Assist: make_raw_string
14//
15// Adds `r#` to a plain string literal.
16//
17// ```
18// fn main() {
19// "Hello,<|> World!";
20// }
21// ```
22// ->
23// ```
24// fn main() {
25// r#"Hello, World!"#;
26// }
27// ```
28pub(crate) fn make_raw_string(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
29 let token = ctx.find_token_at_offset(STRING).and_then(ast::String::cast)?;
30 let value = token.value()?;
31 let target = token.syntax().text_range();
32 acc.add(
33 AssistId("make_raw_string", AssistKind::RefactorRewrite),
34 "Rewrite as raw string",
35 target,
36 |edit| {
37 let hashes = "#".repeat(required_hashes(&value).max(1));
38 if matches!(value, Cow::Borrowed(_)) {
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 )
50}
51
52// Assist: make_usual_string
53//
54// Turns a raw string into a plain string.
55//
56// ```
57// fn main() {
58// r#"Hello,<|> "World!""#;
59// }
60// ```
61// ->
62// ```
63// fn main() {
64// "Hello, \"World!\"";
65// }
66// ```
67pub(crate) fn make_usual_string(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
68 let token = ctx.find_token_at_offset(RAW_STRING).and_then(ast::RawString::cast)?;
69 let value = token.value()?;
70 let target = token.syntax().text_range();
71 acc.add(
72 AssistId("make_usual_string", AssistKind::RefactorRewrite),
73 "Rewrite as regular string",
74 target,
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 )
89}
90
91// Assist: add_hash
92//
93// Adds a hash to a raw string literal.
94//
95// ```
96// fn main() {
97// r#"Hello,<|> World!"#;
98// }
99// ```
100// ->
101// ```
102// fn main() {
103// r##"Hello, World!"##;
104// }
105// ```
106pub(crate) fn add_hash(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
107 let token = ctx.find_token_at_offset(RAW_STRING)?;
108 let target = token.text_range();
109 acc.add(AssistId("add_hash", AssistKind::Refactor), "Add #", target, |edit| {
110 edit.insert(token.text_range().start() + TextSize::of('r'), "#");
111 edit.insert(token.text_range().end(), "#");
112 })
113}
114
115// Assist: remove_hash
116//
117// Removes a hash from a raw string literal.
118//
119// ```
120// fn main() {
121// r#"Hello,<|> World!"#;
122// }
123// ```
124// ->
125// ```
126// fn main() {
127// r"Hello, World!";
128// }
129// ```
130pub(crate) fn remove_hash(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
131 let token = ctx.find_token_at_offset(RAW_STRING).and_then(ast::RawString::cast)?;
132
133 let text = token.text().as_str();
134 if !text.starts_with("r#") && text.ends_with('#') {
135 return None;
136 }
137
138 let existing_hashes = text.chars().skip(1).take_while(|&it| it == '#').count();
139
140 let text_range = token.syntax().text_range();
141 let internal_text = &text[token.text_range_between_quotes()? - text_range.start()];
142
143 if existing_hashes == required_hashes(internal_text) {
144 mark::hit!(cant_remove_required_hash);
145 return None;
146 }
147
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()));
151 })
152}
153
154fn required_hashes(s: &str) -> usize {
155 let mut res = 0usize;
156 for idx in s.match_indices('"').map(|(i, _)| i) {
157 let (_, sub) = s.split_at(idx + 1);
158 let n_hashes = sub.chars().take_while(|c| *c == '#').count();
159 res = res.max(n_hashes + 1)
160 }
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"));
173}
174
175#[cfg(test)]
176mod tests {
177 use test_utils::mark;
178
179 use crate::tests::{check_assist, check_assist_not_applicable, check_assist_target};
180
181 use super::*;
182
183 #[test]
184 fn make_raw_string_target() {
185 check_assist_target(
186 make_raw_string,
187 r#"
188 fn f() {
189 let s = <|>"random\nstring";
190 }
191 "#,
192 r#""random\nstring""#,
193 );
194 }
195
196 #[test]
197 fn make_raw_string_works() {
198 check_assist(
199 make_raw_string,
200 r#"
201fn f() {
202 let s = <|>"random\nstring";
203}
204"#,
205 r##"
206fn f() {
207 let s = r#"random
208string"#;
209}
210"##,
211 )
212 }
213
214 #[test]
215 fn make_raw_string_works_inside_macros() {
216 check_assist(
217 make_raw_string,
218 r#"
219 fn f() {
220 format!(<|>"x = {}", 92)
221 }
222 "#,
223 r##"
224 fn f() {
225 format!(r#"x = {}"#, 92)
226 }
227 "##,
228 )
229 }
230
231 #[test]
232 fn make_raw_string_hashes_inside_works() {
233 check_assist(
234 make_raw_string,
235 r###"
236fn f() {
237 let s = <|>"#random##\nstring";
238}
239"###,
240 r####"
241fn f() {
242 let s = r#"#random##
243string"#;
244}
245"####,
246 )
247 }
248
249 #[test]
250 fn make_raw_string_closing_hashes_inside_works() {
251 check_assist(
252 make_raw_string,
253 r###"
254fn f() {
255 let s = <|>"#random\"##\nstring";
256}
257"###,
258 r####"
259fn f() {
260 let s = r###"#random"##
261string"###;
262}
263"####,
264 )
265 }
266
267 #[test]
268 fn make_raw_string_nothing_to_unescape_works() {
269 check_assist(
270 make_raw_string,
271 r#"
272 fn f() {
273 let s = <|>"random string";
274 }
275 "#,
276 r##"
277 fn f() {
278 let s = r#"random string"#;
279 }
280 "##,
281 )
282 }
283
284 #[test]
285 fn make_raw_string_not_works_on_partial_string() {
286 check_assist_not_applicable(
287 make_raw_string,
288 r#"
289 fn f() {
290 let s = "foo<|>
291 }
292 "#,
293 )
294 }
295
296 #[test]
297 fn make_usual_string_not_works_on_partial_string() {
298 check_assist_not_applicable(
299 make_usual_string,
300 r#"
301 fn main() {
302 let s = r#"bar<|>
303 }
304 "#,
305 )
306 }
307
308 #[test]
309 fn add_hash_target() {
310 check_assist_target(
311 add_hash,
312 r#"
313 fn f() {
314 let s = <|>r"random string";
315 }
316 "#,
317 r#"r"random string""#,
318 );
319 }
320
321 #[test]
322 fn add_hash_works() {
323 check_assist(
324 add_hash,
325 r#"
326 fn f() {
327 let s = <|>r"random string";
328 }
329 "#,
330 r##"
331 fn f() {
332 let s = r#"random string"#;
333 }
334 "##,
335 )
336 }
337
338 #[test]
339 fn add_more_hash_works() {
340 check_assist(
341 add_hash,
342 r##"
343 fn f() {
344 let s = <|>r#"random"string"#;
345 }
346 "##,
347 r###"
348 fn f() {
349 let s = r##"random"string"##;
350 }
351 "###,
352 )
353 }
354
355 #[test]
356 fn add_hash_not_works() {
357 check_assist_not_applicable(
358 add_hash,
359 r#"
360 fn f() {
361 let s = <|>"random string";
362 }
363 "#,
364 );
365 }
366
367 #[test]
368 fn remove_hash_target() {
369 check_assist_target(
370 remove_hash,
371 r##"
372 fn f() {
373 let s = <|>r#"random string"#;
374 }
375 "##,
376 r##"r#"random string"#"##,
377 );
378 }
379
380 #[test]
381 fn remove_hash_works() {
382 check_assist(
383 remove_hash,
384 r##"fn f() { let s = <|>r#"random string"#; }"##,
385 r#"fn f() { let s = r"random string"; }"#,
386 )
387 }
388
389 #[test]
390 fn cant_remove_required_hash() {
391 mark::check!(cant_remove_required_hash);
392 check_assist_not_applicable(
393 remove_hash,
394 r##"
395 fn f() {
396 let s = <|>r#"random"str"ing"#;
397 }
398 "##,
399 )
400 }
401
402 #[test]
403 fn remove_more_hash_works() {
404 check_assist(
405 remove_hash,
406 r###"
407 fn f() {
408 let s = <|>r##"random string"##;
409 }
410 "###,
411 r##"
412 fn f() {
413 let s = r#"random string"#;
414 }
415 "##,
416 )
417 }
418
419 #[test]
420 fn remove_hash_doesnt_work() {
421 check_assist_not_applicable(remove_hash, r#"fn f() { let s = <|>"random string"; }"#);
422 }
423
424 #[test]
425 fn remove_hash_no_hash_doesnt_work() {
426 check_assist_not_applicable(remove_hash, r#"fn f() { let s = <|>r"random string"; }"#);
427 }
428
429 #[test]
430 fn make_usual_string_target() {
431 check_assist_target(
432 make_usual_string,
433 r##"
434 fn f() {
435 let s = <|>r#"random string"#;
436 }
437 "##,
438 r##"r#"random string"#"##,
439 );
440 }
441
442 #[test]
443 fn make_usual_string_works() {
444 check_assist(
445 make_usual_string,
446 r##"
447 fn f() {
448 let s = <|>r#"random string"#;
449 }
450 "##,
451 r#"
452 fn f() {
453 let s = "random string";
454 }
455 "#,
456 )
457 }
458
459 #[test]
460 fn make_usual_string_with_quote_works() {
461 check_assist(
462 make_usual_string,
463 r##"
464 fn f() {
465 let s = <|>r#"random"str"ing"#;
466 }
467 "##,
468 r#"
469 fn f() {
470 let s = "random\"str\"ing";
471 }
472 "#,
473 )
474 }
475
476 #[test]
477 fn make_usual_string_more_hash_works() {
478 check_assist(
479 make_usual_string,
480 r###"
481 fn f() {
482 let s = <|>r##"random string"##;
483 }
484 "###,
485 r##"
486 fn f() {
487 let s = "random string";
488 }
489 "##,
490 )
491 }
492
493 #[test]
494 fn make_usual_string_not_works() {
495 check_assist_not_applicable(
496 make_usual_string,
497 r#"
498 fn f() {
499 let s = <|>"random string";
500 }
501 "#,
502 );
503 }
504}