ide_assists/handlers/
replace_string_with_char.rs

1use syntax::{
2    AstToken,
3    SyntaxKind::{CHAR, STRING},
4    TextRange, TextSize, ast,
5    ast::IsString,
6};
7
8use crate::{AssistContext, AssistId, Assists, utils::string_suffix};
9
10// Assist: replace_string_with_char
11//
12// Replace string literal with char literal.
13//
14// ```
15// fn main() {
16//     find("{$0");
17// }
18// ```
19// ->
20// ```
21// fn main() {
22//     find('{');
23// }
24// ```
25pub(crate) fn replace_string_with_char(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
26    let token = ctx.find_token_syntax_at_offset(STRING).and_then(ast::String::cast)?;
27    let value = token.value().ok()?;
28    let target = token.syntax().text_range();
29
30    if value.chars().take(2).count() != 1 {
31        return None;
32    }
33    let quote_offsets = token.quote_offsets()?;
34
35    acc.add(
36        AssistId::refactor_rewrite("replace_string_with_char"),
37        "Replace string with char",
38        target,
39        |edit| {
40            let (left, right) = quote_offsets.quotes;
41            let suffix = TextSize::of(string_suffix(token.text()).unwrap_or_default());
42            let right = TextRange::new(right.start(), right.end() - suffix);
43            edit.replace(left, '\'');
44            edit.replace(right, '\'');
45            if token.text_without_quotes() == "'" {
46                edit.insert(left.end(), '\\');
47            }
48        },
49    )
50}
51
52// Assist: replace_char_with_string
53//
54// Replace a char literal with a string literal.
55//
56// ```
57// fn main() {
58//     find('{$0');
59// }
60// ```
61// ->
62// ```
63// fn main() {
64//     find("{");
65// }
66// ```
67pub(crate) fn replace_char_with_string(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
68    let token = ctx.find_token_syntax_at_offset(CHAR)?;
69    let target = token.text_range();
70
71    acc.add(
72        AssistId::refactor_rewrite("replace_char_with_string"),
73        "Replace char with string",
74        target,
75        |edit| {
76            let suffix = string_suffix(token.text()).unwrap_or_default();
77            if token.text().starts_with("'\"'") {
78                edit.replace(token.text_range(), format!(r#""\""{suffix}"#));
79            } else {
80                let len = TextSize::of('\'');
81                let suffix = TextSize::of(suffix);
82                edit.replace(TextRange::at(target.start(), len), '"');
83                edit.replace(TextRange::at(target.end() - suffix - len, len), '"');
84            }
85        },
86    )
87}
88
89#[cfg(test)]
90mod tests {
91    use crate::tests::{check_assist, check_assist_not_applicable};
92
93    use super::*;
94
95    #[test]
96    fn replace_string_with_char_assist() {
97        check_assist(
98            replace_string_with_char,
99            r#"
100fn f() {
101    let s = "$0c";
102}
103"#,
104            r##"
105fn f() {
106    let s = 'c';
107}
108"##,
109        )
110    }
111
112    #[test]
113    fn replace_string_with_char_has_suffix() {
114        check_assist(
115            replace_string_with_char,
116            r#"
117fn f() {
118    let s = "$0c"i32;
119}
120"#,
121            r##"
122fn f() {
123    let s = 'c'i32;
124}
125"##,
126        )
127    }
128
129    #[test]
130    fn replace_string_with_char_assist_with_multi_byte_char() {
131        check_assist(
132            replace_string_with_char,
133            r#"
134fn f() {
135    let s = "$0😀";
136}
137"#,
138            r##"
139fn f() {
140    let s = '😀';
141}
142"##,
143        )
144    }
145
146    #[test]
147    fn replace_string_with_char_multiple_chars() {
148        check_assist_not_applicable(
149            replace_string_with_char,
150            r#"
151fn f() {
152    let s = "$0test";
153}
154"#,
155        )
156    }
157
158    #[test]
159    fn replace_string_with_char_works_inside_macros() {
160        check_assist(
161            replace_string_with_char,
162            r#"
163fn f() {
164    format!($0"x", 92)
165}
166"#,
167            r##"
168fn f() {
169    format!('x', 92)
170}
171"##,
172        )
173    }
174
175    #[test]
176    fn replace_string_with_char_newline() {
177        check_assist(
178            replace_string_with_char,
179            r#"
180fn f() {
181    find($0"\n");
182}
183"#,
184            r##"
185fn f() {
186    find('\n');
187}
188"##,
189        )
190    }
191
192    #[test]
193    fn replace_string_with_char_unicode_escape() {
194        check_assist(
195            replace_string_with_char,
196            r#"
197fn f() {
198    find($0"\u{7FFF}");
199}
200"#,
201            r##"
202fn f() {
203    find('\u{7FFF}');
204}
205"##,
206        )
207    }
208
209    #[test]
210    fn replace_raw_string_with_char() {
211        check_assist(
212            replace_string_with_char,
213            r##"
214fn f() {
215    $0r#"X"#
216}
217"##,
218            r##"
219fn f() {
220    'X'
221}
222"##,
223        )
224    }
225
226    #[test]
227    fn replace_char_with_string_assist() {
228        check_assist(
229            replace_char_with_string,
230            r"
231fn f() {
232    let s = '$0c';
233}
234",
235            r#"
236fn f() {
237    let s = "c";
238}
239"#,
240        )
241    }
242
243    #[test]
244    fn replace_char_with_string_assist_with_multi_byte_char() {
245        check_assist(
246            replace_char_with_string,
247            r"
248fn f() {
249    let s = '$0😀';
250}
251",
252            r#"
253fn f() {
254    let s = "😀";
255}
256"#,
257        )
258    }
259
260    #[test]
261    fn replace_char_with_string_newline() {
262        check_assist(
263            replace_char_with_string,
264            r"
265fn f() {
266    find($0'\n');
267}
268",
269            r#"
270fn f() {
271    find("\n");
272}
273"#,
274        )
275    }
276
277    #[test]
278    fn replace_char_with_string_unicode_escape() {
279        check_assist(
280            replace_char_with_string,
281            r"
282fn f() {
283    find($0'\u{7FFF}');
284}
285",
286            r#"
287fn f() {
288    find("\u{7FFF}");
289}
290"#,
291        )
292    }
293
294    #[test]
295    fn replace_char_with_string_quote() {
296        check_assist(
297            replace_char_with_string,
298            r#"
299fn f() {
300    find($0'"');
301}
302"#,
303            r#"
304fn f() {
305    find("\"");
306}
307"#,
308        )
309    }
310
311    #[test]
312    fn replace_char_with_string_quote_has_suffix() {
313        check_assist(
314            replace_char_with_string,
315            r#"
316fn f() {
317    find($0'"'i32);
318}
319"#,
320            r#"
321fn f() {
322    find("\""i32);
323}
324"#,
325        )
326    }
327
328    #[test]
329    fn replace_char_with_string_escaped_quote_has_suffix() {
330        check_assist(
331            replace_char_with_string,
332            r#"
333fn f() {
334    find($0'\"'i32);
335}
336"#,
337            r#"
338fn f() {
339    find("\""i32);
340}
341"#,
342        )
343    }
344
345    #[test]
346    fn replace_string_with_char_quote() {
347        check_assist(
348            replace_string_with_char,
349            r#"
350fn f() {
351    find($0"'");
352}
353"#,
354            r#"
355fn f() {
356    find('\'');
357}
358"#,
359        )
360    }
361
362    #[test]
363    fn replace_string_with_escaped_char_quote() {
364        check_assist(
365            replace_string_with_char,
366            r#"
367fn f() {
368    find($0"\'");
369}
370"#,
371            r#"
372fn f() {
373    find('\'');
374}
375"#,
376        )
377    }
378
379    #[test]
380    fn replace_string_with_char_quote_has_suffix() {
381        check_assist(
382            replace_string_with_char,
383            r#"
384fn f() {
385    find($0"'"i32);
386}
387"#,
388            r#"
389fn f() {
390    find('\''i32);
391}
392"#,
393        )
394    }
395
396    #[test]
397    fn replace_string_with_escaped_char_quote_has_suffix() {
398        check_assist(
399            replace_string_with_char,
400            r#"
401fn f() {
402    find($0"\'"i32);
403}
404"#,
405            r#"
406fn f() {
407    find('\''i32);
408}
409"#,
410        )
411    }
412
413    #[test]
414    fn replace_raw_string_with_char_quote() {
415        check_assist(
416            replace_string_with_char,
417            r#"
418fn f() {
419    find($0r"'");
420}
421"#,
422            r#"
423fn f() {
424    find('\'');
425}
426"#,
427        )
428    }
429
430    #[test]
431    fn replace_string_with_code_escaped_char_quote() {
432        check_assist(
433            replace_string_with_char,
434            r#"
435fn f() {
436    find($0"\x27");
437}
438"#,
439            r#"
440fn f() {
441    find('\x27');
442}
443"#,
444        )
445    }
446}