mirror of
https://github.com/rust-lang/rust-analyzer.git
synced 2025-10-15 12:59:19 +00:00
Merge pull request #20788 from A4-Tacks/any-raw-string
Fix not applicable c-str and byte-str for raw_string
This commit is contained in:
commit
edaeac1df7
4 changed files with 235 additions and 55 deletions
|
@ -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()
|
||||
&& 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::*;
|
||||
|
@ -224,6 +234,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(
|
||||
|
@ -348,6 +394,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(
|
||||
|
@ -433,6 +496,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(
|
||||
|
@ -529,6 +601,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(
|
||||
|
|
|
@ -1057,6 +1057,21 @@ fn test_string_suffix() {
|
|||
assert_eq!(Some("i32"), string_suffix(r##"r#""#i32"##));
|
||||
}
|
||||
|
||||
/// Calculate the string literal prefix length
|
||||
pub(crate) fn string_prefix(s: &str) -> Option<&str> {
|
||||
s.split_once(['"', '\'', '#']).map(|(prefix, _)| prefix)
|
||||
}
|
||||
#[test]
|
||||
fn test_string_prefix() {
|
||||
assert_eq!(Some(""), string_prefix(r#""abc""#));
|
||||
assert_eq!(Some(""), string_prefix(r#""""#));
|
||||
assert_eq!(Some(""), string_prefix(r#"""suffix"#));
|
||||
assert_eq!(Some("c"), string_prefix(r#"c"""#));
|
||||
assert_eq!(Some("r"), string_prefix(r#"r"""#));
|
||||
assert_eq!(Some("cr"), string_prefix(r#"cr"""#));
|
||||
assert_eq!(Some("r"), string_prefix(r##"r#""#"##));
|
||||
}
|
||||
|
||||
/// Replaces the record expression, handling field shorthands including inside macros.
|
||||
pub(crate) fn replace_record_field_expr(
|
||||
ctx: &AssistContext<'_>,
|
||||
|
|
|
@ -29,7 +29,9 @@ pub use self::{
|
|||
SlicePatComponents, StructKind, TypeBoundKind, TypeOrConstParam, VisibilityKind,
|
||||
},
|
||||
operators::{ArithOp, BinaryOp, CmpOp, LogicOp, Ordering, RangeOp, UnaryOp},
|
||||
token_ext::{CommentKind, CommentPlacement, CommentShape, IsString, QuoteOffsets, Radix},
|
||||
token_ext::{
|
||||
AnyString, CommentKind, CommentPlacement, CommentShape, IsString, QuoteOffsets, Radix,
|
||||
},
|
||||
traits::{
|
||||
AttrDocCommentIter, DocCommentIter, HasArgList, HasAttrs, HasDocComments, HasGenericArgs,
|
||||
HasGenericParams, HasLoopBody, HasModuleItem, HasName, HasTypeBounds, HasVisibility,
|
||||
|
|
|
@ -151,10 +151,10 @@ impl QuoteOffsets {
|
|||
}
|
||||
|
||||
pub trait IsString: AstToken {
|
||||
const RAW_PREFIX: &'static str;
|
||||
fn unescape(s: &str, callback: impl FnMut(Range<usize>, Result<char, EscapeError>));
|
||||
fn raw_prefix(&self) -> &'static str;
|
||||
fn unescape(&self, s: &str, callback: impl FnMut(Range<usize>, Result<char, EscapeError>));
|
||||
fn is_raw(&self) -> bool {
|
||||
self.text().starts_with(Self::RAW_PREFIX)
|
||||
self.text().starts_with(self.raw_prefix())
|
||||
}
|
||||
fn quote_offsets(&self) -> Option<QuoteOffsets> {
|
||||
let text = self.text();
|
||||
|
@ -187,7 +187,7 @@ pub trait IsString: AstToken {
|
|||
let text = &self.text()[text_range_no_quotes - start];
|
||||
let offset = text_range_no_quotes.start() - start;
|
||||
|
||||
Self::unescape(text, &mut |range: Range<usize>, unescaped_char| {
|
||||
self.unescape(text, &mut |range: Range<usize>, unescaped_char| {
|
||||
if let Some((s, e)) = range.start.try_into().ok().zip(range.end.try_into().ok()) {
|
||||
cb(TextRange::new(s, e) + offset, unescaped_char);
|
||||
}
|
||||
|
@ -204,8 +204,10 @@ pub trait IsString: AstToken {
|
|||
}
|
||||
|
||||
impl IsString for ast::String {
|
||||
const RAW_PREFIX: &'static str = "r";
|
||||
fn unescape(s: &str, cb: impl FnMut(Range<usize>, Result<char, EscapeError>)) {
|
||||
fn raw_prefix(&self) -> &'static str {
|
||||
"r"
|
||||
}
|
||||
fn unescape(&self, s: &str, cb: impl FnMut(Range<usize>, Result<char, EscapeError>)) {
|
||||
unescape_str(s, cb)
|
||||
}
|
||||
}
|
||||
|
@ -246,8 +248,10 @@ impl ast::String {
|
|||
}
|
||||
|
||||
impl IsString for ast::ByteString {
|
||||
const RAW_PREFIX: &'static str = "br";
|
||||
fn unescape(s: &str, mut callback: impl FnMut(Range<usize>, Result<char, EscapeError>)) {
|
||||
fn raw_prefix(&self) -> &'static str {
|
||||
"br"
|
||||
}
|
||||
fn unescape(&self, s: &str, mut callback: impl FnMut(Range<usize>, Result<char, EscapeError>)) {
|
||||
unescape_byte_str(s, |range, res| callback(range, res.map(char::from)))
|
||||
}
|
||||
}
|
||||
|
@ -288,10 +292,12 @@ impl ast::ByteString {
|
|||
}
|
||||
|
||||
impl IsString for ast::CString {
|
||||
const RAW_PREFIX: &'static str = "cr";
|
||||
fn raw_prefix(&self) -> &'static str {
|
||||
"cr"
|
||||
}
|
||||
// NOTE: This method should only be used for highlighting ranges. The unescaped
|
||||
// char/byte is not used. For simplicity, we return an arbitrary placeholder char.
|
||||
fn unescape(s: &str, mut callback: impl FnMut(Range<usize>, Result<char, EscapeError>)) {
|
||||
fn unescape(&self, s: &str, mut callback: impl FnMut(Range<usize>, Result<char, EscapeError>)) {
|
||||
unescape_c_str(s, |range, _res| callback(range, Ok('_')))
|
||||
}
|
||||
}
|
||||
|
@ -465,6 +471,74 @@ impl ast::Byte {
|
|||
}
|
||||
}
|
||||
|
||||
pub enum AnyString {
|
||||
ByteString(ast::ByteString),
|
||||
CString(ast::CString),
|
||||
String(ast::String),
|
||||
}
|
||||
|
||||
impl AnyString {
|
||||
pub fn value(&self) -> Result<Cow<'_, str>, EscapeError> {
|
||||
fn from_utf8(s: Cow<'_, [u8]>) -> Result<Cow<'_, str>, EscapeError> {
|
||||
match s {
|
||||
Cow::Borrowed(s) => str::from_utf8(s)
|
||||
.map_err(|_| EscapeError::NonAsciiCharInByte)
|
||||
.map(Cow::Borrowed),
|
||||
Cow::Owned(s) => String::from_utf8(s)
|
||||
.map_err(|_| EscapeError::NonAsciiCharInByte)
|
||||
.map(Cow::Owned),
|
||||
}
|
||||
}
|
||||
|
||||
match self {
|
||||
AnyString::String(s) => s.value(),
|
||||
AnyString::ByteString(s) => s.value().and_then(from_utf8),
|
||||
AnyString::CString(s) => s.value().and_then(from_utf8),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ast::AstToken for AnyString {
|
||||
fn can_cast(kind: crate::SyntaxKind) -> bool {
|
||||
ast::String::can_cast(kind)
|
||||
|| ast::ByteString::can_cast(kind)
|
||||
|| ast::CString::can_cast(kind)
|
||||
}
|
||||
|
||||
fn cast(syntax: crate::SyntaxToken) -> Option<Self> {
|
||||
ast::String::cast(syntax.clone())
|
||||
.map(Self::String)
|
||||
.or_else(|| ast::ByteString::cast(syntax.clone()).map(Self::ByteString))
|
||||
.or_else(|| ast::CString::cast(syntax).map(Self::CString))
|
||||
}
|
||||
|
||||
fn syntax(&self) -> &crate::SyntaxToken {
|
||||
match self {
|
||||
Self::ByteString(it) => it.syntax(),
|
||||
Self::CString(it) => it.syntax(),
|
||||
Self::String(it) => it.syntax(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IsString for AnyString {
|
||||
fn raw_prefix(&self) -> &'static str {
|
||||
match self {
|
||||
AnyString::ByteString(s) => s.raw_prefix(),
|
||||
AnyString::CString(s) => s.raw_prefix(),
|
||||
AnyString::String(s) => s.raw_prefix(),
|
||||
}
|
||||
}
|
||||
|
||||
fn unescape(&self, s: &str, callback: impl FnMut(Range<usize>, Result<char, EscapeError>)) {
|
||||
match self {
|
||||
AnyString::ByteString(it) => it.unescape(s, callback),
|
||||
AnyString::CString(it) => it.unescape(s, callback),
|
||||
AnyString::String(it) => it.unescape(s, callback),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rustc_apfloat::ieee::Quad as f128;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue