refactor(chunk): simplified leading trivia handling by removing duplicate preservation logic
- Removed leading trivia preservation logic that duplicated attributes during whole-chunk replacements. - Deleted LeadingTriviaFamily enum and related detection/matching functions (preserve_attached_leading_trivia, replacement_supplies_leading_trivia, detect_leading_trivia_family, matches_leading_trivia_family). - Added test case verifying that whole-chunk replace operations do not duplicate attributes.
This commit is contained in:
@@ -33,19 +33,6 @@ enum InsertPosition {
|
||||
LastChild,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum LeadingTriviaFamily {
|
||||
SlashLineComment,
|
||||
BlockComment,
|
||||
HashComment,
|
||||
DashDashComment,
|
||||
SemicolonComment,
|
||||
PercentComment,
|
||||
AtAttribute,
|
||||
RustAttribute,
|
||||
BracketAttribute,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct InsertSpacing {
|
||||
blank_line_before: bool,
|
||||
@@ -391,7 +378,6 @@ fn apply_replace(
|
||||
let mut replacement =
|
||||
normalize_inserted_content(content, &target_indent, Some(file_indent_step), file_indent_char);
|
||||
if target.region.is_none() {
|
||||
replacement = preserve_attached_leading_trivia(state, &anchor, &replacement);
|
||||
if !replacement.is_empty()
|
||||
&& !replacement.ends_with('\n')
|
||||
&& anchor.end_line < state.tree.line_count
|
||||
@@ -731,101 +717,6 @@ fn normalize_inserted_content(
|
||||
normalized
|
||||
}
|
||||
|
||||
fn preserve_attached_leading_trivia(
|
||||
state: &ChunkStateInner,
|
||||
anchor: &ChunkNode,
|
||||
replacement: &str,
|
||||
) -> String {
|
||||
if replacement.is_empty() {
|
||||
return replacement.to_owned();
|
||||
}
|
||||
|
||||
let trivia_start = anchor.start_byte as usize;
|
||||
let trivia_end = anchor.checksum_start_byte as usize;
|
||||
if trivia_end <= trivia_start {
|
||||
return replacement.to_owned();
|
||||
}
|
||||
|
||||
let line_start = state.source[..trivia_start]
|
||||
.rfind('\n')
|
||||
.map_or(0, |pos| pos + 1);
|
||||
let line_prefix = &state.source[line_start..trivia_start];
|
||||
let mut leading_trivia =
|
||||
if !line_prefix.is_empty() && line_prefix.chars().all(|ch| matches!(ch, ' ' | '\t')) {
|
||||
format!("{line_prefix}{}", &state.source[trivia_start..trivia_end])
|
||||
} else {
|
||||
state.source[trivia_start..trivia_end].to_owned()
|
||||
};
|
||||
if let Some(last_newline) = leading_trivia.rfind('\n')
|
||||
&& leading_trivia[last_newline + 1..]
|
||||
.chars()
|
||||
.all(|ch| matches!(ch, ' ' | '\t' | '\r'))
|
||||
{
|
||||
leading_trivia.truncate(last_newline + 1);
|
||||
}
|
||||
if leading_trivia.trim().is_empty()
|
||||
|| replacement.starts_with(&leading_trivia)
|
||||
|| replacement_supplies_leading_trivia(&leading_trivia, replacement)
|
||||
{
|
||||
return replacement.to_owned();
|
||||
}
|
||||
|
||||
let mut combined = String::with_capacity(leading_trivia.len() + replacement.len());
|
||||
combined.push_str(&leading_trivia);
|
||||
combined.push_str(replacement);
|
||||
combined
|
||||
}
|
||||
|
||||
fn replacement_supplies_leading_trivia(leading_trivia: &str, replacement: &str) -> bool {
|
||||
let Some(family) = detect_leading_trivia_family(leading_trivia) else {
|
||||
return false;
|
||||
};
|
||||
let Some(first_non_empty_line) = replacement.lines().find(|line| !line.trim().is_empty()) else {
|
||||
return false;
|
||||
};
|
||||
matches_leading_trivia_family(family, first_non_empty_line.trim_start())
|
||||
}
|
||||
|
||||
fn detect_leading_trivia_family(text: &str) -> Option<LeadingTriviaFamily> {
|
||||
let first_non_empty_line = text.lines().find(|line| !line.trim().is_empty())?;
|
||||
let trimmed = first_non_empty_line.trim_start();
|
||||
if trimmed.starts_with("//") {
|
||||
Some(LeadingTriviaFamily::SlashLineComment)
|
||||
} else if trimmed.starts_with("/*") || trimmed.starts_with('*') {
|
||||
Some(LeadingTriviaFamily::BlockComment)
|
||||
} else if trimmed.starts_with("#[") {
|
||||
Some(LeadingTriviaFamily::RustAttribute)
|
||||
} else if trimmed.starts_with('@') {
|
||||
Some(LeadingTriviaFamily::AtAttribute)
|
||||
} else if trimmed.starts_with('[') && trimmed.ends_with(']') {
|
||||
Some(LeadingTriviaFamily::BracketAttribute)
|
||||
} else if trimmed.starts_with("--") {
|
||||
Some(LeadingTriviaFamily::DashDashComment)
|
||||
} else if trimmed.starts_with(';') {
|
||||
Some(LeadingTriviaFamily::SemicolonComment)
|
||||
} else if trimmed.starts_with('%') {
|
||||
Some(LeadingTriviaFamily::PercentComment)
|
||||
} else if trimmed.starts_with('#') {
|
||||
Some(LeadingTriviaFamily::HashComment)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn matches_leading_trivia_family(family: LeadingTriviaFamily, line: &str) -> bool {
|
||||
match family {
|
||||
LeadingTriviaFamily::SlashLineComment => line.starts_with("//"),
|
||||
LeadingTriviaFamily::BlockComment => line.starts_with("/*") || line.starts_with('*'),
|
||||
LeadingTriviaFamily::HashComment => line.starts_with('#') && !line.starts_with("#["),
|
||||
LeadingTriviaFamily::DashDashComment => line.starts_with("--"),
|
||||
LeadingTriviaFamily::SemicolonComment => line.starts_with(';'),
|
||||
LeadingTriviaFamily::PercentComment => line.starts_with('%'),
|
||||
LeadingTriviaFamily::AtAttribute => line.starts_with('@'),
|
||||
LeadingTriviaFamily::RustAttribute => line.starts_with("#["),
|
||||
LeadingTriviaFamily::BracketAttribute => line.starts_with('[') && line.ends_with(']'),
|
||||
}
|
||||
}
|
||||
|
||||
fn line_offsets(text: &str) -> Vec<usize> {
|
||||
let mut offsets = vec![0usize];
|
||||
for (index, ch) in text.char_indices() {
|
||||
@@ -1709,6 +1600,36 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn whole_chunk_replace_does_not_duplicate_attributes() {
|
||||
let source = "#[napi]\nfn close() {\n old();\n}\n";
|
||||
let state = state_for(source, "rust");
|
||||
let chunk = state.inner().chunk("fn_close").expect("fn_close");
|
||||
// The chunk range should include the #[napi] attribute.
|
||||
assert_eq!(chunk.start_line, 1, "chunk should start at the attribute line");
|
||||
|
||||
let result = apply_single_edit(
|
||||
&state,
|
||||
"test.rs",
|
||||
EditOperation {
|
||||
op: ChunkEditOp::Replace,
|
||||
sel: Some("fn_close".to_owned()),
|
||||
crc: Some(chunk.checksum.clone()),
|
||||
region: None,
|
||||
content: Some("/// doc\n#[napi]\nfn close() {\n new();\n}".to_owned()),
|
||||
find: None,
|
||||
},
|
||||
);
|
||||
|
||||
let occurrences = result.diff_after.matches("#[napi]").count();
|
||||
assert_eq!(
|
||||
occurrences, 1,
|
||||
"expected exactly one #[napi] attribute, got {occurrences}. Full text:\n{}",
|
||||
result.diff_after
|
||||
);
|
||||
assert!(result.diff_after.contains("new()"), "replacement body should be present");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edit_auto_resolves_unique_chunk_paths() {
|
||||
let source = "class Worker {\n\trun(): void {\n\t\tconsole.log(this.name);\n\t}\n}\n";
|
||||
|
||||
Reference in New Issue
Block a user