Support ide-assists raw_string suffix

This commit is contained in:
A4-Tacks 2025-04-18 22:05:56 +08:00
parent 16745db3f7
commit 9ef1417d45
No known key found for this signature in database
GPG Key ID: 86AC1F526BA06668
2 changed files with 148 additions and 8 deletions

View File

@ -2,7 +2,10 @@ use std::borrow::Cow;
use syntax::{AstToken, TextRange, TextSize, ast, ast::IsString};
use crate::{AssistContext, AssistId, Assists, utils::required_hashes};
use crate::{
AssistContext, AssistId, Assists,
utils::{required_hashes, string_suffix},
};
// Assist: make_raw_string
//
@ -33,12 +36,15 @@ pub(crate) fn make_raw_string(acc: &mut Assists, ctx: &AssistContext<'_>) -> Opt
target,
|edit| {
let hashes = "#".repeat(required_hashes(&value).max(1));
let range = token.syntax().text_range();
let suffix = string_suffix(token.text()).unwrap_or_default();
let range = TextRange::new(range.start(), range.end() - TextSize::of(suffix));
if matches!(value, Cow::Borrowed(_)) {
// Avoid replacing the whole string to better position the cursor.
edit.insert(token.syntax().text_range().start(), format!("r{hashes}"));
edit.insert(token.syntax().text_range().end(), hashes);
edit.insert(range.start(), format!("r{hashes}"));
edit.insert(range.end(), hashes);
} else {
edit.replace(token.syntax().text_range(), format!("r{hashes}\"{value}\"{hashes}"));
edit.replace(range, format!("r{hashes}\"{value}\"{hashes}"));
}
},
)
@ -73,15 +79,19 @@ pub(crate) fn make_usual_string(acc: &mut Assists, ctx: &AssistContext<'_>) -> O
|edit| {
// parse inside string to escape `"`
let escaped = value.escape_default().to_string();
let suffix = string_suffix(token.text()).unwrap_or_default();
if let Some(offsets) = token.quote_offsets() {
if token.text()[offsets.contents - token.syntax().text_range().start()] == escaped {
let end_quote = offsets.quotes.1;
let end_quote =
TextRange::new(end_quote.start(), end_quote.end() - TextSize::of(suffix));
edit.replace(offsets.quotes.0, "\"");
edit.replace(offsets.quotes.1, "\"");
edit.replace(end_quote, "\"");
return;
}
}
edit.replace(token.syntax().text_range(), format!("\"{escaped}\""));
edit.replace(token.syntax().text_range(), format!("\"{escaped}\"{suffix}"));
},
)
}
@ -109,8 +119,9 @@ pub(crate) fn add_hash(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()>
let text_range = token.syntax().text_range();
let target = text_range;
acc.add(AssistId::refactor("add_hash"), "Add #", target, |edit| {
let suffix = string_suffix(token.text()).unwrap_or_default();
edit.insert(text_range.start() + TextSize::of('r'), "#");
edit.insert(text_range.end(), "#");
edit.insert(text_range.end() - TextSize::of(suffix), "#");
})
}
@ -151,8 +162,12 @@ pub(crate) fn remove_hash(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<
}
acc.add(AssistId::refactor_rewrite("remove_hash"), "Remove #", text_range, |edit| {
let suffix = string_suffix(text).unwrap_or_default();
edit.delete(TextRange::at(text_range.start() + TextSize::of('r'), TextSize::of('#')));
edit.delete(TextRange::new(text_range.end() - TextSize::of('#'), text_range.end()));
edit.delete(
TextRange::new(text_range.end() - TextSize::of('#'), text_range.end())
- TextSize::of(suffix),
);
})
}
@ -262,6 +277,23 @@ string"###;
)
}
#[test]
fn make_raw_string_has_suffix() {
check_assist(
make_raw_string,
r#"
fn f() {
let s = $0"random string"i32;
}
"#,
r##"
fn f() {
let s = r#"random string"#i32;
}
"##,
)
}
#[test]
fn make_raw_string_not_works_on_partial_string() {
check_assist_not_applicable(
@ -316,6 +348,23 @@ string"###;
)
}
#[test]
fn add_hash_has_suffix_works() {
check_assist(
add_hash,
r#"
fn f() {
let s = $0r"random string"i32;
}
"#,
r##"
fn f() {
let s = r#"random string"#i32;
}
"##,
)
}
#[test]
fn add_more_hash_works() {
check_assist(
@ -333,6 +382,23 @@ string"###;
)
}
#[test]
fn add_more_hash_has_suffix_works() {
check_assist(
add_hash,
r##"
fn f() {
let s = $0r#"random"string"#i32;
}
"##,
r###"
fn f() {
let s = r##"random"string"##i32;
}
"###,
)
}
#[test]
fn add_hash_not_works() {
check_assist_not_applicable(
@ -367,6 +433,15 @@ string"###;
)
}
#[test]
fn remove_hash_has_suffix_works() {
check_assist(
remove_hash,
r##"fn f() { let s = $0r#"random string"#i32; }"##,
r#"fn f() { let s = r"random string"i32; }"#,
)
}
#[test]
fn cant_remove_required_hash() {
cov_mark::check!(cant_remove_required_hash);
@ -397,6 +472,23 @@ string"###;
)
}
#[test]
fn remove_more_hash_has_suffix_works() {
check_assist(
remove_hash,
r###"
fn f() {
let s = $0r##"random string"##i32;
}
"###,
r##"
fn f() {
let s = r#"random string"#i32;
}
"##,
)
}
#[test]
fn remove_hash_does_not_work() {
check_assist_not_applicable(remove_hash, r#"fn f() { let s = $0"random string"; }"#);
@ -437,6 +529,23 @@ string"###;
)
}
#[test]
fn make_usual_string_has_suffix_works() {
check_assist(
make_usual_string,
r##"
fn f() {
let s = $0r#"random string"#i32;
}
"##,
r#"
fn f() {
let s = "random string"i32;
}
"#,
)
}
#[test]
fn make_usual_string_with_quote_works() {
check_assist(
@ -471,6 +580,23 @@ string"###;
)
}
#[test]
fn make_usual_string_more_hash_has_suffix_works() {
check_assist(
make_usual_string,
r###"
fn f() {
let s = $0r##"random string"##i32;
}
"###,
r##"
fn f() {
let s = "random string"i32;
}
"##,
)
}
#[test]
fn make_usual_string_not_works() {
check_assist_not_applicable(

View File

@ -1026,6 +1026,20 @@ fn test_required_hashes() {
assert_eq!(5, required_hashes("#ab\"##\"####c"));
}
/// Calculate the string literal suffix length
pub(crate) fn string_suffix(s: &str) -> Option<&str> {
s.rfind(['"', '\'', '#']).map(|i| &s[i + 1..])
}
#[test]
fn test_string_suffix() {
assert_eq!(Some(""), string_suffix(r#""abc""#));
assert_eq!(Some(""), string_suffix(r#""""#));
assert_eq!(Some("a"), string_suffix(r#"""a"#));
assert_eq!(Some("i32"), string_suffix(r#"""i32"#));
assert_eq!(Some("i32"), string_suffix(r#"r""i32"#));
assert_eq!(Some("i32"), string_suffix(r##"r#""#i32"##));
}
/// Replaces the record expression, handling field shorthands including inside macros.
pub(crate) fn replace_record_field_expr(
ctx: &AssistContext<'_>,