Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'crates/ide-assists/src/handlers/raw_string.rs')
| -rw-r--r-- | crates/ide-assists/src/handlers/raw_string.rs | 177 |
1 files changed, 133 insertions, 44 deletions
diff --git a/crates/ide-assists/src/handlers/raw_string.rs b/crates/ide-assists/src/handlers/raw_string.rs index 94b49c5df0..d6d99b8b6d 100644 --- a/crates/ide-assists/src/handlers/raw_string.rs +++ b/crates/ide-assists/src/handlers/raw_string.rs @@ -1,10 +1,12 @@ -use std::borrow::Cow; - -use syntax::{AstToken, TextRange, TextSize, ast, ast::IsString}; +use ide_db::source_change::SourceChangeBuilder; +use syntax::{ + AstToken, + ast::{self, IsString, make::tokens::literal}, +}; use crate::{ AssistContext, AssistId, Assists, - utils::{required_hashes, string_suffix}, + utils::{required_hashes, string_prefix, string_suffix}, }; // Assist: make_raw_string @@ -23,8 +25,7 @@ use crate::{ // } // ``` pub(crate) fn make_raw_string(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> { - // FIXME: This should support byte and c strings as well. - let token = ctx.find_token_at_offset::<ast::String>()?; + let token = ctx.find_token_at_offset::<ast::AnyString>()?; if token.is_raw() { return None; } @@ -36,16 +37,10 @@ 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 raw_prefix = token.raw_prefix(); 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(range.start(), format!("r{hashes}")); - edit.insert(range.end(), hashes); - } else { - edit.replace(range, format!("r{hashes}\"{value}\"{hashes}")); - } + let new_str = format!("{raw_prefix}{hashes}\"{value}\"{hashes}{suffix}"); + replace_literal(&token, &new_str, edit, ctx); }, ) } @@ -66,7 +61,7 @@ pub(crate) fn make_raw_string(acc: &mut Assists, ctx: &AssistContext<'_>) -> Opt // } // ``` pub(crate) fn make_usual_string(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> { - let token = ctx.find_token_at_offset::<ast::String>()?; + let token = ctx.find_token_at_offset::<ast::AnyString>()?; if !token.is_raw() { return None; } @@ -80,18 +75,9 @@ pub(crate) fn make_usual_string(acc: &mut Assists, ctx: &AssistContext<'_>) -> O // 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(end_quote, "\""); - return; - } - } - - edit.replace(token.syntax().text_range(), format!("\"{escaped}\"{suffix}")); + let prefix = string_prefix(token.text()).map_or("", |s| s.trim_end_matches('r')); + let new_str = format!("{prefix}\"{escaped}\"{suffix}"); + replace_literal(&token, &new_str, edit, ctx); }, ) } @@ -112,16 +98,18 @@ pub(crate) fn make_usual_string(acc: &mut Assists, ctx: &AssistContext<'_>) -> O // } // ``` pub(crate) fn add_hash(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> { - let token = ctx.find_token_at_offset::<ast::String>()?; + let token = ctx.find_token_at_offset::<ast::AnyString>()?; if !token.is_raw() { return None; } - let text_range = token.syntax().text_range(); - let target = text_range; + let target = token.syntax().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() - TextSize::of(suffix), "#"); + let str = token.text(); + let suffix = string_suffix(str).unwrap_or_default(); + let raw_prefix = token.raw_prefix(); + let wrap_range = raw_prefix.len()..str.len() - suffix.len(); + let new_str = [raw_prefix, "#", &str[wrap_range], "#", suffix].concat(); + replace_literal(&token, &new_str, edit, ctx); }) } @@ -141,17 +129,15 @@ pub(crate) fn add_hash(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> // } // ``` pub(crate) fn remove_hash(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> { - let token = ctx.find_token_at_offset::<ast::String>()?; + let token = ctx.find_token_at_offset::<ast::AnyString>()?; if !token.is_raw() { return None; } let text = token.text(); - if !text.starts_with("r#") && text.ends_with('#') { - return None; - } - let existing_hashes = text.chars().skip(1).take_while(|&it| it == '#').count(); + let existing_hashes = + text.chars().skip(token.raw_prefix().len()).take_while(|&it| it == '#').count(); let text_range = token.syntax().text_range(); let internal_text = &text[token.text_range_between_quotes()? - text_range.start()]; @@ -163,14 +149,38 @@ 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()) - - TextSize::of(suffix), - ); + let prefix = token.raw_prefix(); + let wrap_range = prefix.len() + 1..text.len() - suffix.len() - 1; + let new_str = [prefix, &text[wrap_range], suffix].concat(); + replace_literal(&token, &new_str, edit, ctx); }) } +fn replace_literal( + token: &impl AstToken, + new: &str, + builder: &mut SourceChangeBuilder, + ctx: &AssistContext<'_>, +) { + let token = token.syntax(); + let node = token.parent().expect("no parent token"); + let mut edit = builder.make_editor(&node); + let new_literal = literal(new); + + edit.replace(token, mut_token(new_literal)); + + builder.add_file_edits(ctx.vfs_file_id(), edit); +} + +fn mut_token(token: syntax::SyntaxToken) -> syntax::SyntaxToken { + let node = token.parent().expect("no parent token"); + node.clone_for_update() + .children_with_tokens() + .filter_map(|it| it.into_token()) + .find(|it| it.text_range() == token.text_range() && it.text() == token.text()) + .unwrap() +} + #[cfg(test)] mod tests { use super::*; @@ -225,6 +235,42 @@ string"#; } #[test] + fn make_raw_byte_string_works() { + check_assist( + make_raw_string, + r#" +fn f() { + let s = $0b"random\nstring"; +} +"#, + r##" +fn f() { + let s = br#"random +string"#; +} +"##, + ) + } + + #[test] + fn make_raw_c_string_works() { + check_assist( + make_raw_string, + r#" +fn f() { + let s = $0c"random\nstring"; +} +"#, + r##" +fn f() { + let s = cr#"random +string"#; +} +"##, + ) + } + + #[test] fn make_raw_string_hashes_inside_works() { check_assist( make_raw_string, @@ -349,6 +395,23 @@ string"###; } #[test] + fn add_hash_works_for_c_str() { + check_assist( + add_hash, + r#" + fn f() { + let s = $0cr"random string"; + } + "#, + r##" + fn f() { + let s = cr#"random string"#; + } + "##, + ) + } + + #[test] fn add_hash_has_suffix_works() { check_assist( add_hash, @@ -434,6 +497,15 @@ string"###; } #[test] + fn remove_hash_works_for_c_str() { + check_assist( + remove_hash, + r##"fn f() { let s = $0cr#"random string"#; }"##, + r#"fn f() { let s = cr"random string"; }"#, + ) + } + + #[test] fn remove_hash_has_suffix_works() { check_assist( remove_hash, @@ -530,6 +602,23 @@ string"###; } #[test] + fn make_usual_string_for_c_str() { + check_assist( + make_usual_string, + r##" + fn f() { + let s = $0cr#"random string"#; + } + "##, + r#" + fn f() { + let s = c"random string"; + } + "#, + ) + } + + #[test] fn make_usual_string_has_suffix_works() { check_assist( make_usual_string, |