diff --git a/Cargo.lock b/Cargo.lock index 8bbccc82ae..877c77a5aa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -671,7 +671,6 @@ dependencies = [ "syntax", "test-fixture", "test-utils", - "text-edit", "toolchain", "tracing", "triomphe", @@ -693,7 +692,6 @@ dependencies = [ "syntax", "test-fixture", "test-utils", - "text-edit", "tracing", ] @@ -712,7 +710,6 @@ dependencies = [ "syntax", "test-fixture", "test-utils", - "text-edit", "tracing", ] @@ -744,7 +741,6 @@ dependencies = [ "syntax", "test-fixture", "test-utils", - "text-edit", "tracing", "triomphe", ] @@ -766,7 +762,6 @@ dependencies = [ "syntax", "test-fixture", "test-utils", - "text-edit", "tracing", ] @@ -785,7 +780,6 @@ dependencies = [ "syntax", "test-fixture", "test-utils", - "text-edit", "triomphe", ] @@ -1979,7 +1973,6 @@ dependencies = [ "smol_str", "stdx", "test-utils", - "text-edit", "tracing", "triomphe", ] @@ -2027,14 +2020,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "text-edit" -version = "0.0.0" -dependencies = [ - "itertools", - "text-size", -] - [[package]] name = "text-size" version = "1.1.1" diff --git a/Cargo.toml b/Cargo.toml index 4e6861d351..f729aae4ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -79,7 +79,6 @@ span = { path = "./crates/span", version = "0.0.0" } stdx = { path = "./crates/stdx", version = "0.0.0" } syntax = { path = "./crates/syntax", version = "0.0.0" } syntax-bridge = { path = "./crates/syntax-bridge", version = "0.0.0" } -text-edit = { path = "./crates/text-edit", version = "0.0.0" } toolchain = { path = "./crates/toolchain", version = "0.0.0" } tt = { path = "./crates/tt", version = "0.0.0" } vfs-notify = { path = "./crates/vfs-notify", version = "0.0.0" } diff --git a/crates/ide-assists/Cargo.toml b/crates/ide-assists/Cargo.toml index 2a14fbe1e0..ba21586871 100644 --- a/crates/ide-assists/Cargo.toml +++ b/crates/ide-assists/Cargo.toml @@ -23,7 +23,6 @@ tracing.workspace = true # local deps stdx.workspace = true syntax.workspace = true -text-edit.workspace = true ide-db.workspace = true hir.workspace = true diff --git a/crates/ide-assists/src/handlers/bool_to_enum.rs b/crates/ide-assists/src/handlers/bool_to_enum.rs index c035c59ffc..605fd14052 100644 --- a/crates/ide-assists/src/handlers/bool_to_enum.rs +++ b/crates/ide-assists/src/handlers/bool_to_enum.rs @@ -1,5 +1,6 @@ use either::Either; use hir::ModuleDef; +use ide_db::text_edit::TextRange; use ide_db::{ assists::{AssistId, AssistKind}, defs::Definition, @@ -19,7 +20,6 @@ use syntax::{ }, AstNode, NodeOrToken, SyntaxKind, SyntaxNode, T, }; -use text_edit::TextRange; use crate::{ assist_context::{AssistContext, Assists}, diff --git a/crates/ide-assists/src/handlers/destructure_struct_binding.rs b/crates/ide-assists/src/handlers/destructure_struct_binding.rs index b229b750e8..22a1efdbea 100644 --- a/crates/ide-assists/src/handlers/destructure_struct_binding.rs +++ b/crates/ide-assists/src/handlers/destructure_struct_binding.rs @@ -1,4 +1,5 @@ use hir::{sym, HasVisibility}; +use ide_db::text_edit::TextRange; use ide_db::{ assists::{AssistId, AssistKind}, defs::Definition, @@ -8,7 +9,6 @@ use ide_db::{ }; use itertools::Itertools; use syntax::{ast, ted, AstNode, Edition, SmolStr, SyntaxNode, ToSmolStr}; -use text_edit::TextRange; use crate::{ assist_context::{AssistContext, Assists, SourceChangeBuilder}, diff --git a/crates/ide-assists/src/handlers/destructure_tuple_binding.rs b/crates/ide-assists/src/handlers/destructure_tuple_binding.rs index 9ecfb83ed5..3f0d5cf152 100644 --- a/crates/ide-assists/src/handlers/destructure_tuple_binding.rs +++ b/crates/ide-assists/src/handlers/destructure_tuple_binding.rs @@ -1,3 +1,4 @@ +use ide_db::text_edit::TextRange; use ide_db::{ assists::{AssistId, AssistKind}, defs::Definition, @@ -8,7 +9,6 @@ use syntax::{ ast::{self, make, AstNode, FieldExpr, HasName, IdentPat}, ted, }; -use text_edit::TextRange; use crate::{ assist_context::{AssistContext, Assists, SourceChangeBuilder}, diff --git a/crates/ide-assists/src/handlers/remove_unused_imports.rs b/crates/ide-assists/src/handlers/remove_unused_imports.rs index c6f99d6874..0570b44778 100644 --- a/crates/ide-assists/src/handlers/remove_unused_imports.rs +++ b/crates/ide-assists/src/handlers/remove_unused_imports.rs @@ -1,6 +1,7 @@ use std::collections::hash_map::Entry; use hir::{FileRange, HirFileIdExt, InFile, InRealFile, Module, ModuleSource}; +use ide_db::text_edit::TextRange; use ide_db::{ defs::Definition, search::{FileReference, ReferenceCategory, SearchScope}, @@ -10,7 +11,6 @@ use syntax::{ ast::{self, Rename}, AstNode, }; -use text_edit::TextRange; use crate::{AssistContext, AssistId, AssistKind, Assists}; diff --git a/crates/ide-assists/src/handlers/replace_named_generic_with_impl.rs b/crates/ide-assists/src/handlers/replace_named_generic_with_impl.rs index 8a6c2937d9..26fd887cc9 100644 --- a/crates/ide-assists/src/handlers/replace_named_generic_with_impl.rs +++ b/crates/ide-assists/src/handlers/replace_named_generic_with_impl.rs @@ -1,4 +1,5 @@ use hir::{FileRange, Semantics}; +use ide_db::text_edit::TextRange; use ide_db::{ defs::Definition, search::{SearchScope, UsageSearchResult}, @@ -11,7 +12,6 @@ use syntax::{ }, match_ast, ted, AstNode, }; -use text_edit::TextRange; use crate::{AssistContext, AssistId, AssistKind, Assists}; diff --git a/crates/ide-completion/Cargo.toml b/crates/ide-completion/Cargo.toml index 614465b4d0..1bef82af5a 100644 --- a/crates/ide-completion/Cargo.toml +++ b/crates/ide-completion/Cargo.toml @@ -25,7 +25,6 @@ base-db.workspace = true ide-db.workspace = true stdx.workspace = true syntax.workspace = true -text-edit.workspace = true # completions crate should depend only on the top-level `hir` package. if you need # something from some `hir-xxx` subpackage, reexport the API via `hir`. hir.workspace = true diff --git a/crates/ide-completion/src/completions/item_list/trait_impl.rs b/crates/ide-completion/src/completions/item_list/trait_impl.rs index 672e1796d1..c38a8ef29b 100644 --- a/crates/ide-completion/src/completions/item_list/trait_impl.rs +++ b/crates/ide-completion/src/completions/item_list/trait_impl.rs @@ -32,6 +32,7 @@ //! ``` use hir::{db::ExpandDatabase, HasAttrs, MacroFileId, Name}; +use ide_db::text_edit::TextEdit; use ide_db::{ documentation::HasDocs, path_transform::PathTransform, syntax_helpers::prettify_macro_expansion, traits::get_missing_assoc_items, SymbolKind, @@ -40,7 +41,6 @@ use syntax::{ ast::{self, edit_in_place::AttrsOwnerEdit, make, HasGenericArgs, HasTypeBounds}, format_smolstr, ted, AstNode, SmolStr, SyntaxElement, SyntaxKind, TextRange, ToSmolStr, T, }; -use text_edit::TextEdit; use crate::{ context::PathCompletionCtx, CompletionContext, CompletionItem, CompletionItemKind, diff --git a/crates/ide-completion/src/completions/postfix.rs b/crates/ide-completion/src/completions/postfix.rs index d3579fd8cc..495f82da86 100644 --- a/crates/ide-completion/src/completions/postfix.rs +++ b/crates/ide-completion/src/completions/postfix.rs @@ -3,6 +3,7 @@ mod format_like; use hir::ItemInNs; +use ide_db::text_edit::TextEdit; use ide_db::{ documentation::{Documentation, HasDocs}, imports::insert_use::ImportScope, @@ -15,7 +16,6 @@ use syntax::{ SyntaxKind::{BLOCK_EXPR, EXPR_STMT, FOR_EXPR, IF_EXPR, LOOP_EXPR, STMT_LIST, WHILE_EXPR}, TextRange, TextSize, }; -use text_edit::TextEdit; use crate::{ completions::postfix::format_like::add_format_like_completions, diff --git a/crates/ide-completion/src/context.rs b/crates/ide-completion/src/context.rs index 0e1302ff2e..efbee39a2d 100644 --- a/crates/ide-completion/src/context.rs +++ b/crates/ide-completion/src/context.rs @@ -20,7 +20,6 @@ use syntax::{ SyntaxKind::{self, *}, SyntaxToken, TextRange, TextSize, T, }; -use text_edit::Indel; use crate::{ context::analysis::{expand_and_analyze, AnalysisResult}, @@ -684,8 +683,7 @@ impl<'a> CompletionContext<'a> { // actual completion. let file_with_fake_ident = { let parse = db.parse(file_id); - let edit = Indel::insert(offset, COMPLETION_MARKER.to_owned()); - parse.reparse(&edit, file_id.edition()).tree() + parse.reparse(TextRange::empty(offset), COMPLETION_MARKER, file_id.edition()).tree() }; // always pick the token to the immediate left of the cursor, as that is what we are actually diff --git a/crates/ide-completion/src/item.rs b/crates/ide-completion/src/item.rs index 8c97ebd550..52f6bedaaa 100644 --- a/crates/ide-completion/src/item.rs +++ b/crates/ide-completion/src/item.rs @@ -3,6 +3,7 @@ use std::{fmt, mem}; use hir::Mutability; +use ide_db::text_edit::TextEdit; use ide_db::{ documentation::Documentation, imports::import_assets::LocatedImport, RootDatabase, SnippetCap, SymbolKind, @@ -11,7 +12,6 @@ use itertools::Itertools; use smallvec::SmallVec; use stdx::{impl_from, never}; use syntax::{format_smolstr, Edition, SmolStr, TextRange, TextSize}; -use text_edit::TextEdit; use crate::{ context::{CompletionContext, PathCompletionCtx}, @@ -426,7 +426,7 @@ impl CompletionItem { self.lookup.as_str() } - pub fn ref_match(&self) -> Option<(String, text_edit::Indel, CompletionRelevance)> { + pub fn ref_match(&self) -> Option<(String, ide_db::text_edit::Indel, CompletionRelevance)> { // Relevance of the ref match should be the same as the original // match, but with exact type match set because self.ref_match // is only set if there is an exact type match. @@ -436,7 +436,10 @@ impl CompletionItem { self.ref_match.map(|(mutability, offset)| { ( format!("&{}{}", mutability.as_keyword_for_ref(), self.label), - text_edit::Indel::insert(offset, format!("&{}", mutability.as_keyword_for_ref())), + ide_db::text_edit::Indel::insert( + offset, + format!("&{}", mutability.as_keyword_for_ref()), + ), relevance, ) }) diff --git a/crates/ide-completion/src/lib.rs b/crates/ide-completion/src/lib.rs index a78976d3fd..dfee01b187 100644 --- a/crates/ide-completion/src/lib.rs +++ b/crates/ide-completion/src/lib.rs @@ -10,16 +10,17 @@ mod snippet; #[cfg(test)] mod tests; +use ide_db::text_edit::TextEdit; use ide_db::{ helpers::mod_path_to_ast, imports::{ import_assets::NameToImport, insert_use::{self, ImportScope}, }, - items_locator, FilePosition, RootDatabase, + items_locator, + syntax_helpers::tree_diff::diff, + FilePosition, RootDatabase, }; -use syntax::algo; -use text_edit::TextEdit; use crate::{ completions::Completions, @@ -297,6 +298,6 @@ pub fn resolve_completion_edits( } }); - algo::diff(scope.as_syntax_node(), new_ast.as_syntax_node()).into_text_edit(&mut import_insert); + diff(scope.as_syntax_node(), new_ast.as_syntax_node()).into_text_edit(&mut import_insert); Some(vec![import_insert.finish()]) } diff --git a/crates/ide-completion/src/render.rs b/crates/ide-completion/src/render.rs index 4dd171142f..ec3c2fe355 100644 --- a/crates/ide-completion/src/render.rs +++ b/crates/ide-completion/src/render.rs @@ -11,6 +11,7 @@ pub(crate) mod union_literal; pub(crate) mod variant; use hir::{sym, AsAssocItem, HasAttrs, HirDisplay, ModuleDef, ScopeDef, Type}; +use ide_db::text_edit::TextEdit; use ide_db::{ documentation::{Documentation, HasDocs}, helpers::item_name, @@ -18,7 +19,6 @@ use ide_db::{ RootDatabase, SnippetCap, SymbolKind, }; use syntax::{ast, format_smolstr, AstNode, Edition, SmolStr, SyntaxKind, TextRange, ToSmolStr}; -use text_edit::TextEdit; use crate::{ context::{DotAccess, DotAccessKind, PathCompletionCtx, PathKind, PatternContext}, diff --git a/crates/ide-db/Cargo.toml b/crates/ide-db/Cargo.toml index c078188d6d..17f0e69bde 100644 --- a/crates/ide-db/Cargo.toml +++ b/crates/ide-db/Cargo.toml @@ -35,7 +35,6 @@ parser.workspace = true profile.workspace = true stdx.workspace = true syntax.workspace = true -text-edit.workspace = true span.workspace = true # ide should depend only on the top-level `hir` package. if you need # something from some `hir-xxx` subpackage, reexport the API via `hir`. diff --git a/crates/ide-db/src/documentation.rs b/crates/ide-db/src/documentation.rs index 5e443badf9..b52a325790 100644 --- a/crates/ide-db/src/documentation.rs +++ b/crates/ide-db/src/documentation.rs @@ -5,11 +5,11 @@ use hir::{ resolve_doc_path_on, sym, AttrId, AttrSourceMap, AttrsWithOwner, HasAttrs, InFile, }; use itertools::Itertools; +use span::{TextRange, TextSize}; use syntax::{ ast::{self, IsString}, AstToken, }; -use text_edit::{TextRange, TextSize}; /// Holds documentation #[derive(Debug, Clone, PartialEq, Eq, Hash)] diff --git a/crates/ide-db/src/lib.rs b/crates/ide-db/src/lib.rs index b764f852f0..81260c3e08 100644 --- a/crates/ide-db/src/lib.rs +++ b/crates/ide-db/src/lib.rs @@ -19,6 +19,7 @@ pub mod rust_doc; pub mod search; pub mod source_change; pub mod symbol_index; +pub mod text_edit; pub mod traits; pub mod ty_filter; pub mod use_trivial_constructor; @@ -36,6 +37,7 @@ pub mod generated { pub mod syntax_helpers { pub mod format_string; pub mod format_string_exprs; + pub mod tree_diff; pub use hir::prettify_macro_expansion; pub mod node_ext; pub mod suggest_name; diff --git a/crates/ide-db/src/rename.rs b/crates/ide-db/src/rename.rs index f1404ed9f2..1d1679c3ff 100644 --- a/crates/ide-db/src/rename.rs +++ b/crates/ide-db/src/rename.rs @@ -22,6 +22,7 @@ //! Our current behavior is ¯\_(ツ)_/¯. use std::fmt; +use crate::text_edit::{TextEdit, TextEditBuilder}; use base_db::AnchoredPathBuf; use either::Either; use hir::{FieldSource, FileRange, HirFileIdExt, InFile, ModuleSource, Semantics}; @@ -32,7 +33,6 @@ use syntax::{ utils::is_raw_identifier, AstNode, SyntaxKind, TextRange, T, }; -use text_edit::{TextEdit, TextEditBuilder}; use crate::{ defs::Definition, diff --git a/crates/ide-db/src/source_change.rs b/crates/ide-db/src/source_change.rs index 73073e92f7..27ff91dc19 100644 --- a/crates/ide-db/src/source_change.rs +++ b/crates/ide-db/src/source_change.rs @@ -5,7 +5,8 @@ use std::{collections::hash_map::Entry, iter, mem}; -use crate::{assists::Command, SnippetCap}; +use crate::text_edit::{TextEdit, TextEditBuilder}; +use crate::{assists::Command, syntax_helpers::tree_diff::diff, SnippetCap}; use base_db::AnchoredPathBuf; use itertools::Itertools; use nohash_hasher::IntMap; @@ -13,11 +14,9 @@ use rustc_hash::FxHashMap; use span::FileId; use stdx::never; use syntax::{ - algo, syntax_editor::{SyntaxAnnotation, SyntaxEditor}, AstNode, SyntaxElement, SyntaxNode, SyntaxNodePtr, SyntaxToken, TextRange, TextSize, }; -use text_edit::{TextEdit, TextEditBuilder}; #[derive(Default, Debug, Clone)] pub struct SourceChange { @@ -315,7 +314,7 @@ impl SourceChangeBuilder { } let mut edit = TextEdit::builder(); - algo::diff(edit_result.old_root(), edit_result.new_root()).into_text_edit(&mut edit); + diff(edit_result.old_root(), edit_result.new_root()).into_text_edit(&mut edit); let edit = edit.finish(); let snippet_edit = @@ -334,7 +333,7 @@ impl SourceChangeBuilder { }); if let Some(tm) = self.mutated_tree.take() { - algo::diff(&tm.immutable, &tm.mutable_clone).into_text_edit(&mut self.edit); + diff(&tm.immutable, &tm.mutable_clone).into_text_edit(&mut self.edit); } let edit = mem::take(&mut self.edit).finish(); @@ -373,7 +372,7 @@ impl SourceChangeBuilder { self.edit.replace(range, replace_with.into()) } pub fn replace_ast(&mut self, old: N, new: N) { - algo::diff(old.syntax(), new.syntax()).into_text_edit(&mut self.edit) + diff(old.syntax(), new.syntax()).into_text_edit(&mut self.edit) } pub fn create_file(&mut self, dst: AnchoredPathBuf, content: impl Into) { let file_system_edit = FileSystemEdit::CreateFile { dst, initial_contents: content.into() }; diff --git a/crates/ide-db/src/syntax_helpers/tree_diff.rs b/crates/ide-db/src/syntax_helpers/tree_diff.rs new file mode 100644 index 0000000000..02e24c4776 --- /dev/null +++ b/crates/ide-db/src/syntax_helpers/tree_diff.rs @@ -0,0 +1,559 @@ +//! Basic tree diffing functionality. +use rustc_hash::FxHashMap; +use syntax::{NodeOrToken, SyntaxElement, SyntaxNode}; + +use crate::{text_edit::TextEditBuilder, FxIndexMap}; + +#[derive(Debug, Hash, PartialEq, Eq)] +enum TreeDiffInsertPos { + After(SyntaxElement), + AsFirstChild(SyntaxElement), +} + +#[derive(Debug)] +pub struct TreeDiff { + replacements: FxHashMap, + deletions: Vec, + // the vec as well as the indexmap are both here to preserve order + insertions: FxIndexMap>, +} + +impl TreeDiff { + pub fn into_text_edit(&self, builder: &mut TextEditBuilder) { + let _p = tracing::info_span!("into_text_edit").entered(); + + for (anchor, to) in &self.insertions { + let offset = match anchor { + TreeDiffInsertPos::After(it) => it.text_range().end(), + TreeDiffInsertPos::AsFirstChild(it) => it.text_range().start(), + }; + to.iter().for_each(|to| builder.insert(offset, to.to_string())); + } + for (from, to) in &self.replacements { + builder.replace(from.text_range(), to.to_string()); + } + for text_range in self.deletions.iter().map(SyntaxElement::text_range) { + builder.delete(text_range); + } + } + + pub fn is_empty(&self) -> bool { + self.replacements.is_empty() && self.deletions.is_empty() && self.insertions.is_empty() + } +} + +/// Finds a (potentially minimal) diff, which, applied to `from`, will result in `to`. +/// +/// Specifically, returns a structure that consists of a replacements, insertions and deletions +/// such that applying this map on `from` will result in `to`. +/// +/// This function tries to find a fine-grained diff. +pub fn diff(from: &SyntaxNode, to: &SyntaxNode) -> TreeDiff { + let _p = tracing::info_span!("diff").entered(); + + let mut diff = TreeDiff { + replacements: FxHashMap::default(), + insertions: FxIndexMap::default(), + deletions: Vec::new(), + }; + let (from, to) = (from.clone().into(), to.clone().into()); + + if !syntax_element_eq(&from, &to) { + go(&mut diff, from, to); + } + return diff; + + fn syntax_element_eq(lhs: &SyntaxElement, rhs: &SyntaxElement) -> bool { + lhs.kind() == rhs.kind() + && lhs.text_range().len() == rhs.text_range().len() + && match (&lhs, &rhs) { + (NodeOrToken::Node(lhs), NodeOrToken::Node(rhs)) => { + lhs == rhs || lhs.text() == rhs.text() + } + (NodeOrToken::Token(lhs), NodeOrToken::Token(rhs)) => lhs.text() == rhs.text(), + _ => false, + } + } + + // FIXME: this is horribly inefficient. I bet there's a cool algorithm to diff trees properly. + fn go(diff: &mut TreeDiff, lhs: SyntaxElement, rhs: SyntaxElement) { + let (lhs, rhs) = match lhs.as_node().zip(rhs.as_node()) { + Some((lhs, rhs)) => (lhs, rhs), + _ => { + cov_mark::hit!(diff_node_token_replace); + diff.replacements.insert(lhs, rhs); + return; + } + }; + + let mut look_ahead_scratch = Vec::default(); + + let mut rhs_children = rhs.children_with_tokens(); + let mut lhs_children = lhs.children_with_tokens(); + let mut last_lhs = None; + loop { + let lhs_child = lhs_children.next(); + match (lhs_child.clone(), rhs_children.next()) { + (None, None) => break, + (None, Some(element)) => { + let insert_pos = match last_lhs.clone() { + Some(prev) => { + cov_mark::hit!(diff_insert); + TreeDiffInsertPos::After(prev) + } + // first iteration, insert into out parent as the first child + None => { + cov_mark::hit!(diff_insert_as_first_child); + TreeDiffInsertPos::AsFirstChild(lhs.clone().into()) + } + }; + diff.insertions.entry(insert_pos).or_default().push(element); + } + (Some(element), None) => { + cov_mark::hit!(diff_delete); + diff.deletions.push(element); + } + (Some(ref lhs_ele), Some(ref rhs_ele)) if syntax_element_eq(lhs_ele, rhs_ele) => {} + (Some(lhs_ele), Some(rhs_ele)) => { + // nodes differ, look for lhs_ele in rhs, if its found we can mark everything up + // until that element as insertions. This is important to keep the diff minimal + // in regards to insertions that have been actually done, this is important for + // use insertions as we do not want to replace the entire module node. + look_ahead_scratch.push(rhs_ele.clone()); + let mut rhs_children_clone = rhs_children.clone(); + let mut insert = false; + for rhs_child in &mut rhs_children_clone { + if syntax_element_eq(&lhs_ele, &rhs_child) { + cov_mark::hit!(diff_insertions); + insert = true; + break; + } + look_ahead_scratch.push(rhs_child); + } + let drain = look_ahead_scratch.drain(..); + if insert { + let insert_pos = if let Some(prev) = last_lhs.clone().filter(|_| insert) { + TreeDiffInsertPos::After(prev) + } else { + cov_mark::hit!(insert_first_child); + TreeDiffInsertPos::AsFirstChild(lhs.clone().into()) + }; + + diff.insertions.entry(insert_pos).or_default().extend(drain); + rhs_children = rhs_children_clone; + } else { + go(diff, lhs_ele, rhs_ele); + } + } + } + last_lhs = lhs_child.or(last_lhs); + } + } +} + +#[cfg(test)] +mod tests { + use expect_test::{expect, Expect}; + use itertools::Itertools; + use parser::{Edition, SyntaxKind}; + use syntax::{AstNode, SourceFile, SyntaxElement}; + + use crate::text_edit::TextEdit; + + #[test] + fn replace_node_token() { + cov_mark::check!(diff_node_token_replace); + check_diff( + r#"use node;"#, + r#"ident"#, + expect![[r#" + insertions: + + + + replacements: + + Line 0: Token(USE_KW@0..3 "use") -> ident + + deletions: + + Line 1: " " + Line 1: node + Line 1: ; + "#]], + ); + } + + #[test] + fn replace_parent() { + cov_mark::check!(diff_insert_as_first_child); + check_diff( + r#""#, + r#"use foo::bar;"#, + expect![[r#" + insertions: + + Line 0: AsFirstChild(Node(SOURCE_FILE@0..0)) + -> use foo::bar; + + replacements: + + + + deletions: + + + "#]], + ); + } + + #[test] + fn insert_last() { + cov_mark::check!(diff_insert); + check_diff( + r#" +use foo; +use bar;"#, + r#" +use foo; +use bar; +use baz;"#, + expect![[r#" + insertions: + + Line 2: After(Node(USE@10..18)) + -> "\n" + -> use baz; + + replacements: + + + + deletions: + + + "#]], + ); + } + + #[test] + fn insert_middle() { + check_diff( + r#" +use foo; +use baz;"#, + r#" +use foo; +use bar; +use baz;"#, + expect![[r#" + insertions: + + Line 2: After(Token(WHITESPACE@9..10 "\n")) + -> use bar; + -> "\n" + + replacements: + + + + deletions: + + + "#]], + ) + } + + #[test] + fn insert_first() { + check_diff( + r#" +use bar; +use baz;"#, + r#" +use foo; +use bar; +use baz;"#, + expect![[r#" + insertions: + + Line 0: After(Token(WHITESPACE@0..1 "\n")) + -> use foo; + -> "\n" + + replacements: + + + + deletions: + + + "#]], + ) + } + + #[test] + fn first_child_insertion() { + cov_mark::check!(insert_first_child); + check_diff( + r#"fn main() { + stdi + }"#, + r#"use foo::bar; + + fn main() { + stdi + }"#, + expect![[r#" + insertions: + + Line 0: AsFirstChild(Node(SOURCE_FILE@0..30)) + -> use foo::bar; + -> "\n\n " + + replacements: + + + + deletions: + + + "#]], + ); + } + + #[test] + fn delete_last() { + cov_mark::check!(diff_delete); + check_diff( + r#"use foo; + use bar;"#, + r#"use foo;"#, + expect![[r#" + insertions: + + + + replacements: + + + + deletions: + + Line 1: "\n " + Line 2: use bar; + "#]], + ); + } + + #[test] + fn delete_middle() { + cov_mark::check!(diff_insertions); + check_diff( + r#" +use expect_test::{expect, Expect}; +use text_edit::TextEdit; + +use crate::AstNode; +"#, + r#" +use expect_test::{expect, Expect}; + +use crate::AstNode; +"#, + expect![[r#" + insertions: + + Line 1: After(Node(USE@1..35)) + -> "\n\n" + -> use crate::AstNode; + + replacements: + + + + deletions: + + Line 2: use text_edit::TextEdit; + Line 3: "\n\n" + Line 4: use crate::AstNode; + Line 5: "\n" + "#]], + ) + } + + #[test] + fn delete_first() { + check_diff( + r#" +use text_edit::TextEdit; + +use crate::AstNode; +"#, + r#" +use crate::AstNode; +"#, + expect![[r#" + insertions: + + + + replacements: + + Line 2: Token(IDENT@5..14 "text_edit") -> crate + Line 2: Token(IDENT@16..24 "TextEdit") -> AstNode + Line 2: Token(WHITESPACE@25..27 "\n\n") -> "\n" + + deletions: + + Line 3: use crate::AstNode; + Line 4: "\n" + "#]], + ) + } + + #[test] + fn merge_use() { + check_diff( + r#" +use std::{ + fmt, + hash::BuildHasherDefault, + ops::{self, RangeInclusive}, +}; +"#, + r#" +use std::fmt; +use std::hash::BuildHasherDefault; +use std::ops::{self, RangeInclusive}; +"#, + expect![[r#" + insertions: + + Line 2: After(Node(PATH_SEGMENT@5..8)) + -> :: + -> fmt + Line 6: After(Token(WHITESPACE@86..87 "\n")) + -> use std::hash::BuildHasherDefault; + -> "\n" + -> use std::ops::{self, RangeInclusive}; + -> "\n" + + replacements: + + Line 2: Token(IDENT@5..8 "std") -> std + + deletions: + + Line 2: :: + Line 2: { + fmt, + hash::BuildHasherDefault, + ops::{self, RangeInclusive}, + } + "#]], + ) + } + + #[test] + fn early_return_assist() { + check_diff( + r#" +fn main() { + if let Ok(x) = Err(92) { + foo(x); + } +} + "#, + r#" +fn main() { + let x = match Err(92) { + Ok(it) => it, + _ => return, + }; + foo(x); +} + "#, + expect![[r#" + insertions: + + Line 3: After(Node(BLOCK_EXPR@40..63)) + -> " " + -> match Err(92) { + Ok(it) => it, + _ => return, + } + -> ; + Line 3: After(Node(IF_EXPR@17..63)) + -> "\n " + -> foo(x); + + replacements: + + Line 3: Token(IF_KW@17..19 "if") -> let + Line 3: Token(LET_KW@20..23 "let") -> x + Line 3: Node(BLOCK_EXPR@40..63) -> = + + deletions: + + Line 3: " " + Line 3: Ok(x) + Line 3: " " + Line 3: = + Line 3: " " + Line 3: Err(92) + "#]], + ) + } + + fn check_diff(from: &str, to: &str, expected_diff: Expect) { + let from_node = SourceFile::parse(from, Edition::CURRENT).tree().syntax().clone(); + let to_node = SourceFile::parse(to, Edition::CURRENT).tree().syntax().clone(); + let diff = super::diff(&from_node, &to_node); + + let line_number = + |syn: &SyntaxElement| from[..syn.text_range().start().into()].lines().count(); + + let fmt_syntax = |syn: &SyntaxElement| match syn.kind() { + SyntaxKind::WHITESPACE => format!("{:?}", syn.to_string()), + _ => format!("{syn}"), + }; + + let insertions = + diff.insertions.iter().format_with("\n", |(k, v), f| -> Result<(), std::fmt::Error> { + f(&format!( + "Line {}: {:?}\n-> {}", + line_number(match k { + super::TreeDiffInsertPos::After(syn) => syn, + super::TreeDiffInsertPos::AsFirstChild(syn) => syn, + }), + k, + v.iter().format_with("\n-> ", |v, f| f(&fmt_syntax(v))) + )) + }); + + let replacements = diff + .replacements + .iter() + .sorted_by_key(|(syntax, _)| syntax.text_range().start()) + .format_with("\n", |(k, v), f| { + f(&format!("Line {}: {k:?} -> {}", line_number(k), fmt_syntax(v))) + }); + + let deletions = diff + .deletions + .iter() + .format_with("\n", |v, f| f(&format!("Line {}: {}", line_number(v), fmt_syntax(v)))); + + let actual = format!( + "insertions:\n\n{insertions}\n\nreplacements:\n\n{replacements}\n\ndeletions:\n\n{deletions}\n" + ); + expected_diff.assert_eq(&actual); + + let mut from = from.to_owned(); + let mut text_edit = TextEdit::builder(); + diff.into_text_edit(&mut text_edit); + text_edit.finish().apply(&mut from); + assert_eq!(&*from, to, "diff did not turn `from` to `to`"); + } +} diff --git a/crates/text-edit/src/lib.rs b/crates/ide-db/src/text_edit.rs similarity index 99% rename from crates/text-edit/src/lib.rs rename to crates/ide-db/src/text_edit.rs index 3efe0850d8..0c675f0619 100644 --- a/crates/text-edit/src/lib.rs +++ b/crates/ide-db/src/text_edit.rs @@ -5,8 +5,8 @@ //! rust-analyzer. use itertools::Itertools; +pub use span::{TextRange, TextSize}; use std::cmp::max; -pub use text_size::{TextRange, TextSize}; /// `InsertDelete` -- a single "atomic" change to text /// diff --git a/crates/ide-diagnostics/Cargo.toml b/crates/ide-diagnostics/Cargo.toml index bf54f4ab32..281a08e542 100644 --- a/crates/ide-diagnostics/Cargo.toml +++ b/crates/ide-diagnostics/Cargo.toml @@ -22,7 +22,6 @@ tracing.workspace = true # local deps stdx.workspace = true syntax.workspace = true -text-edit.workspace = true cfg.workspace = true hir.workspace = true ide-db.workspace = true diff --git a/crates/ide-diagnostics/src/handlers/field_shorthand.rs b/crates/ide-diagnostics/src/handlers/field_shorthand.rs index c7071d1ce4..876c2ccd49 100644 --- a/crates/ide-diagnostics/src/handlers/field_shorthand.rs +++ b/crates/ide-diagnostics/src/handlers/field_shorthand.rs @@ -1,9 +1,9 @@ //! Suggests shortening `Foo { field: field }` to `Foo { field }` in both //! expressions and patterns. +use ide_db::text_edit::TextEdit; use ide_db::{source_change::SourceChange, EditionedFileId, FileRange}; use syntax::{ast, match_ast, AstNode, SyntaxNode}; -use text_edit::TextEdit; use crate::{fix, Diagnostic, DiagnosticCode}; diff --git a/crates/ide-diagnostics/src/handlers/json_is_not_rust.rs b/crates/ide-diagnostics/src/handlers/json_is_not_rust.rs index ccb33fed10..1011e73988 100644 --- a/crates/ide-diagnostics/src/handlers/json_is_not_rust.rs +++ b/crates/ide-diagnostics/src/handlers/json_is_not_rust.rs @@ -14,7 +14,7 @@ use syntax::{ ast::{self, make}, Edition, SyntaxKind, SyntaxNode, }; -use text_edit::TextEdit; +use ide_db::text_edit::TextEdit; use crate::{fix, Diagnostic, DiagnosticCode, DiagnosticsConfig, Severity}; diff --git a/crates/ide-diagnostics/src/handlers/missing_fields.rs b/crates/ide-diagnostics/src/handlers/missing_fields.rs index 3a622c6968..fd1044e51b 100644 --- a/crates/ide-diagnostics/src/handlers/missing_fields.rs +++ b/crates/ide-diagnostics/src/handlers/missing_fields.rs @@ -5,15 +5,14 @@ use hir::{ }; use ide_db::{ assists::Assist, famous_defs::FamousDefs, imports::import_assets::item_for_path_search, - source_change::SourceChange, use_trivial_constructor::use_trivial_constructor, FxHashMap, + source_change::SourceChange, syntax_helpers::tree_diff::diff, text_edit::TextEdit, + use_trivial_constructor::use_trivial_constructor, FxHashMap, }; use stdx::format_to; use syntax::{ - algo, ast::{self, make}, AstNode, Edition, SyntaxNode, SyntaxNodePtr, ToSmolStr, }; -use text_edit::TextEdit; use crate::{fix, Diagnostic, DiagnosticCode, DiagnosticsContext}; @@ -77,7 +76,7 @@ fn fixes(ctx: &DiagnosticsContext<'_>, d: &hir::MissingFields) -> Option = Result; diff --git a/crates/ide/src/move_item.rs b/crates/ide/src/move_item.rs index ea6cc9d6de..a232df2b82 100644 --- a/crates/ide/src/move_item.rs +++ b/crates/ide/src/move_item.rs @@ -1,10 +1,11 @@ use std::{iter::once, mem}; use hir::Semantics; +use ide_db::syntax_helpers::tree_diff::diff; +use ide_db::text_edit::{TextEdit, TextEditBuilder}; use ide_db::{helpers::pick_best_token, FileRange, RootDatabase}; use itertools::Itertools; -use syntax::{algo, ast, match_ast, AstNode, SyntaxElement, SyntaxKind, SyntaxNode, TextRange}; -use text_edit::{TextEdit, TextEditBuilder}; +use syntax::{ast, match_ast, AstNode, SyntaxElement, SyntaxKind, SyntaxNode, TextRange}; #[derive(Copy, Clone, Debug)] pub enum Direction { @@ -166,7 +167,7 @@ fn replace_nodes<'a>( let mut edit = TextEditBuilder::default(); - algo::diff(first, second).into_text_edit(&mut edit); + diff(first, second).into_text_edit(&mut edit); edit.replace(second.text_range(), first_with_cursor); edit.finish() diff --git a/crates/ide/src/rename.rs b/crates/ide/src/rename.rs index f17c1fa5c6..665fc954d2 100644 --- a/crates/ide/src/rename.rs +++ b/crates/ide/src/rename.rs @@ -15,7 +15,7 @@ use itertools::Itertools; use stdx::{always, never}; use syntax::{ast, AstNode, SyntaxKind, SyntaxNode, TextRange, TextSize}; -use text_edit::TextEdit; +use ide_db::text_edit::TextEdit; use crate::{FilePosition, RangeInfo, SourceChange}; @@ -449,9 +449,9 @@ fn text_edit_from_self_param(self_param: &ast::SelfParam, new_name: &str) -> Opt mod tests { use expect_test::{expect, Expect}; use ide_db::source_change::SourceChange; + use ide_db::text_edit::TextEdit; use stdx::trim_indent; use test_utils::assert_eq_text; - use text_edit::TextEdit; use crate::fixture; diff --git a/crates/ide/src/typing.rs b/crates/ide/src/typing.rs index a09e1e85ae..9bb5de9f2e 100644 --- a/crates/ide/src/typing.rs +++ b/crates/ide/src/typing.rs @@ -23,7 +23,7 @@ use syntax::{ AstNode, Parse, SourceFile, SyntaxKind, TextRange, TextSize, T, }; -use text_edit::{Indel, TextEdit}; +use ide_db::text_edit::TextEdit; use crate::SourceChange; @@ -126,7 +126,7 @@ fn on_opening_bracket_typed( return None; } // FIXME: Edition - let file = file.reparse(&Indel::delete(range), span::Edition::CURRENT_FIXME); + let file = file.reparse(range, "", span::Edition::CURRENT_FIXME); if let Some(edit) = bracket_expr(&file.tree(), offset, opening_bracket, closing_bracket) { return Some(edit); diff --git a/crates/ide/src/typing/on_enter.rs b/crates/ide/src/typing/on_enter.rs index 6e56bd6185..773e352220 100644 --- a/crates/ide/src/typing/on_enter.rs +++ b/crates/ide/src/typing/on_enter.rs @@ -12,7 +12,7 @@ use syntax::{ SyntaxNode, SyntaxToken, TextRange, TextSize, TokenAtOffset, }; -use text_edit::TextEdit; +use ide_db::text_edit::TextEdit; // Feature: On Enter // diff --git a/crates/syntax/Cargo.toml b/crates/syntax/Cargo.toml index fcb9b0ea35..51eaea5434 100644 --- a/crates/syntax/Cargo.toml +++ b/crates/syntax/Cargo.toml @@ -27,7 +27,6 @@ ra-ap-rustc_lexer.workspace = true parser.workspace = true stdx.workspace = true -text-edit.workspace = true [dev-dependencies] rayon.workspace = true diff --git a/crates/syntax/src/algo.rs b/crates/syntax/src/algo.rs index 8dc6d36a7e..2acb215831 100644 --- a/crates/syntax/src/algo.rs +++ b/crates/syntax/src/algo.rs @@ -1,11 +1,6 @@ //! Collection of assorted algorithms for syntax trees. -use std::hash::BuildHasherDefault; - -use indexmap::IndexMap; use itertools::Itertools; -use rustc_hash::FxHashMap; -use text_edit::TextEditBuilder; use crate::{ AstNode, Direction, NodeOrToken, SyntaxElement, SyntaxKind, SyntaxNode, SyntaxToken, TextRange, @@ -101,559 +96,3 @@ pub fn neighbor(me: &T, direction: Direction) -> Option { pub fn has_errors(node: &SyntaxNode) -> bool { node.children().any(|it| it.kind() == SyntaxKind::ERROR) } - -type FxIndexMap = IndexMap>; - -#[derive(Debug, Hash, PartialEq, Eq)] -enum TreeDiffInsertPos { - After(SyntaxElement), - AsFirstChild(SyntaxElement), -} - -#[derive(Debug)] -pub struct TreeDiff { - replacements: FxHashMap, - deletions: Vec, - // the vec as well as the indexmap are both here to preserve order - insertions: FxIndexMap>, -} - -impl TreeDiff { - pub fn into_text_edit(&self, builder: &mut TextEditBuilder) { - let _p = tracing::info_span!("into_text_edit").entered(); - - for (anchor, to) in &self.insertions { - let offset = match anchor { - TreeDiffInsertPos::After(it) => it.text_range().end(), - TreeDiffInsertPos::AsFirstChild(it) => it.text_range().start(), - }; - to.iter().for_each(|to| builder.insert(offset, to.to_string())); - } - for (from, to) in &self.replacements { - builder.replace(from.text_range(), to.to_string()); - } - for text_range in self.deletions.iter().map(SyntaxElement::text_range) { - builder.delete(text_range); - } - } - - pub fn is_empty(&self) -> bool { - self.replacements.is_empty() && self.deletions.is_empty() && self.insertions.is_empty() - } -} - -/// Finds a (potentially minimal) diff, which, applied to `from`, will result in `to`. -/// -/// Specifically, returns a structure that consists of a replacements, insertions and deletions -/// such that applying this map on `from` will result in `to`. -/// -/// This function tries to find a fine-grained diff. -pub fn diff(from: &SyntaxNode, to: &SyntaxNode) -> TreeDiff { - let _p = tracing::info_span!("diff").entered(); - - let mut diff = TreeDiff { - replacements: FxHashMap::default(), - insertions: FxIndexMap::default(), - deletions: Vec::new(), - }; - let (from, to) = (from.clone().into(), to.clone().into()); - - if !syntax_element_eq(&from, &to) { - go(&mut diff, from, to); - } - return diff; - - fn syntax_element_eq(lhs: &SyntaxElement, rhs: &SyntaxElement) -> bool { - lhs.kind() == rhs.kind() - && lhs.text_range().len() == rhs.text_range().len() - && match (&lhs, &rhs) { - (NodeOrToken::Node(lhs), NodeOrToken::Node(rhs)) => { - lhs == rhs || lhs.text() == rhs.text() - } - (NodeOrToken::Token(lhs), NodeOrToken::Token(rhs)) => lhs.text() == rhs.text(), - _ => false, - } - } - - // FIXME: this is horribly inefficient. I bet there's a cool algorithm to diff trees properly. - fn go(diff: &mut TreeDiff, lhs: SyntaxElement, rhs: SyntaxElement) { - let (lhs, rhs) = match lhs.as_node().zip(rhs.as_node()) { - Some((lhs, rhs)) => (lhs, rhs), - _ => { - cov_mark::hit!(diff_node_token_replace); - diff.replacements.insert(lhs, rhs); - return; - } - }; - - let mut look_ahead_scratch = Vec::default(); - - let mut rhs_children = rhs.children_with_tokens(); - let mut lhs_children = lhs.children_with_tokens(); - let mut last_lhs = None; - loop { - let lhs_child = lhs_children.next(); - match (lhs_child.clone(), rhs_children.next()) { - (None, None) => break, - (None, Some(element)) => { - let insert_pos = match last_lhs.clone() { - Some(prev) => { - cov_mark::hit!(diff_insert); - TreeDiffInsertPos::After(prev) - } - // first iteration, insert into out parent as the first child - None => { - cov_mark::hit!(diff_insert_as_first_child); - TreeDiffInsertPos::AsFirstChild(lhs.clone().into()) - } - }; - diff.insertions.entry(insert_pos).or_default().push(element); - } - (Some(element), None) => { - cov_mark::hit!(diff_delete); - diff.deletions.push(element); - } - (Some(ref lhs_ele), Some(ref rhs_ele)) if syntax_element_eq(lhs_ele, rhs_ele) => {} - (Some(lhs_ele), Some(rhs_ele)) => { - // nodes differ, look for lhs_ele in rhs, if its found we can mark everything up - // until that element as insertions. This is important to keep the diff minimal - // in regards to insertions that have been actually done, this is important for - // use insertions as we do not want to replace the entire module node. - look_ahead_scratch.push(rhs_ele.clone()); - let mut rhs_children_clone = rhs_children.clone(); - let mut insert = false; - for rhs_child in &mut rhs_children_clone { - if syntax_element_eq(&lhs_ele, &rhs_child) { - cov_mark::hit!(diff_insertions); - insert = true; - break; - } - look_ahead_scratch.push(rhs_child); - } - let drain = look_ahead_scratch.drain(..); - if insert { - let insert_pos = if let Some(prev) = last_lhs.clone().filter(|_| insert) { - TreeDiffInsertPos::After(prev) - } else { - cov_mark::hit!(insert_first_child); - TreeDiffInsertPos::AsFirstChild(lhs.clone().into()) - }; - - diff.insertions.entry(insert_pos).or_default().extend(drain); - rhs_children = rhs_children_clone; - } else { - go(diff, lhs_ele, rhs_ele); - } - } - } - last_lhs = lhs_child.or(last_lhs); - } - } -} - -#[cfg(test)] -mod tests { - use expect_test::{expect, Expect}; - use itertools::Itertools; - use parser::{Edition, SyntaxKind}; - use text_edit::TextEdit; - - use crate::{AstNode, SyntaxElement}; - - #[test] - fn replace_node_token() { - cov_mark::check!(diff_node_token_replace); - check_diff( - r#"use node;"#, - r#"ident"#, - expect![[r#" - insertions: - - - - replacements: - - Line 0: Token(USE_KW@0..3 "use") -> ident - - deletions: - - Line 1: " " - Line 1: node - Line 1: ; - "#]], - ); - } - - #[test] - fn replace_parent() { - cov_mark::check!(diff_insert_as_first_child); - check_diff( - r#""#, - r#"use foo::bar;"#, - expect![[r#" - insertions: - - Line 0: AsFirstChild(Node(SOURCE_FILE@0..0)) - -> use foo::bar; - - replacements: - - - - deletions: - - - "#]], - ); - } - - #[test] - fn insert_last() { - cov_mark::check!(diff_insert); - check_diff( - r#" -use foo; -use bar;"#, - r#" -use foo; -use bar; -use baz;"#, - expect![[r#" - insertions: - - Line 2: After(Node(USE@10..18)) - -> "\n" - -> use baz; - - replacements: - - - - deletions: - - - "#]], - ); - } - - #[test] - fn insert_middle() { - check_diff( - r#" -use foo; -use baz;"#, - r#" -use foo; -use bar; -use baz;"#, - expect![[r#" - insertions: - - Line 2: After(Token(WHITESPACE@9..10 "\n")) - -> use bar; - -> "\n" - - replacements: - - - - deletions: - - - "#]], - ) - } - - #[test] - fn insert_first() { - check_diff( - r#" -use bar; -use baz;"#, - r#" -use foo; -use bar; -use baz;"#, - expect![[r#" - insertions: - - Line 0: After(Token(WHITESPACE@0..1 "\n")) - -> use foo; - -> "\n" - - replacements: - - - - deletions: - - - "#]], - ) - } - - #[test] - fn first_child_insertion() { - cov_mark::check!(insert_first_child); - check_diff( - r#"fn main() { - stdi - }"#, - r#"use foo::bar; - - fn main() { - stdi - }"#, - expect![[r#" - insertions: - - Line 0: AsFirstChild(Node(SOURCE_FILE@0..30)) - -> use foo::bar; - -> "\n\n " - - replacements: - - - - deletions: - - - "#]], - ); - } - - #[test] - fn delete_last() { - cov_mark::check!(diff_delete); - check_diff( - r#"use foo; - use bar;"#, - r#"use foo;"#, - expect![[r#" - insertions: - - - - replacements: - - - - deletions: - - Line 1: "\n " - Line 2: use bar; - "#]], - ); - } - - #[test] - fn delete_middle() { - cov_mark::check!(diff_insertions); - check_diff( - r#" -use expect_test::{expect, Expect}; -use text_edit::TextEdit; - -use crate::AstNode; -"#, - r#" -use expect_test::{expect, Expect}; - -use crate::AstNode; -"#, - expect![[r#" - insertions: - - Line 1: After(Node(USE@1..35)) - -> "\n\n" - -> use crate::AstNode; - - replacements: - - - - deletions: - - Line 2: use text_edit::TextEdit; - Line 3: "\n\n" - Line 4: use crate::AstNode; - Line 5: "\n" - "#]], - ) - } - - #[test] - fn delete_first() { - check_diff( - r#" -use text_edit::TextEdit; - -use crate::AstNode; -"#, - r#" -use crate::AstNode; -"#, - expect![[r#" - insertions: - - - - replacements: - - Line 2: Token(IDENT@5..14 "text_edit") -> crate - Line 2: Token(IDENT@16..24 "TextEdit") -> AstNode - Line 2: Token(WHITESPACE@25..27 "\n\n") -> "\n" - - deletions: - - Line 3: use crate::AstNode; - Line 4: "\n" - "#]], - ) - } - - #[test] - fn merge_use() { - check_diff( - r#" -use std::{ - fmt, - hash::BuildHasherDefault, - ops::{self, RangeInclusive}, -}; -"#, - r#" -use std::fmt; -use std::hash::BuildHasherDefault; -use std::ops::{self, RangeInclusive}; -"#, - expect![[r#" - insertions: - - Line 2: After(Node(PATH_SEGMENT@5..8)) - -> :: - -> fmt - Line 6: After(Token(WHITESPACE@86..87 "\n")) - -> use std::hash::BuildHasherDefault; - -> "\n" - -> use std::ops::{self, RangeInclusive}; - -> "\n" - - replacements: - - Line 2: Token(IDENT@5..8 "std") -> std - - deletions: - - Line 2: :: - Line 2: { - fmt, - hash::BuildHasherDefault, - ops::{self, RangeInclusive}, - } - "#]], - ) - } - - #[test] - fn early_return_assist() { - check_diff( - r#" -fn main() { - if let Ok(x) = Err(92) { - foo(x); - } -} - "#, - r#" -fn main() { - let x = match Err(92) { - Ok(it) => it, - _ => return, - }; - foo(x); -} - "#, - expect![[r#" - insertions: - - Line 3: After(Node(BLOCK_EXPR@40..63)) - -> " " - -> match Err(92) { - Ok(it) => it, - _ => return, - } - -> ; - Line 3: After(Node(IF_EXPR@17..63)) - -> "\n " - -> foo(x); - - replacements: - - Line 3: Token(IF_KW@17..19 "if") -> let - Line 3: Token(LET_KW@20..23 "let") -> x - Line 3: Node(BLOCK_EXPR@40..63) -> = - - deletions: - - Line 3: " " - Line 3: Ok(x) - Line 3: " " - Line 3: = - Line 3: " " - Line 3: Err(92) - "#]], - ) - } - - fn check_diff(from: &str, to: &str, expected_diff: Expect) { - let from_node = crate::SourceFile::parse(from, Edition::CURRENT).tree().syntax().clone(); - let to_node = crate::SourceFile::parse(to, Edition::CURRENT).tree().syntax().clone(); - let diff = super::diff(&from_node, &to_node); - - let line_number = - |syn: &SyntaxElement| from[..syn.text_range().start().into()].lines().count(); - - let fmt_syntax = |syn: &SyntaxElement| match syn.kind() { - SyntaxKind::WHITESPACE => format!("{:?}", syn.to_string()), - _ => format!("{syn}"), - }; - - let insertions = - diff.insertions.iter().format_with("\n", |(k, v), f| -> Result<(), std::fmt::Error> { - f(&format!( - "Line {}: {:?}\n-> {}", - line_number(match k { - super::TreeDiffInsertPos::After(syn) => syn, - super::TreeDiffInsertPos::AsFirstChild(syn) => syn, - }), - k, - v.iter().format_with("\n-> ", |v, f| f(&fmt_syntax(v))) - )) - }); - - let replacements = diff - .replacements - .iter() - .sorted_by_key(|(syntax, _)| syntax.text_range().start()) - .format_with("\n", |(k, v), f| { - f(&format!("Line {}: {k:?} -> {}", line_number(k), fmt_syntax(v))) - }); - - let deletions = diff - .deletions - .iter() - .format_with("\n", |v, f| f(&format!("Line {}: {}", line_number(v), fmt_syntax(v)))); - - let actual = format!( - "insertions:\n\n{insertions}\n\nreplacements:\n\n{replacements}\n\ndeletions:\n\n{deletions}\n" - ); - expected_diff.assert_eq(&actual); - - let mut from = from.to_owned(); - let mut text_edit = TextEdit::builder(); - diff.into_text_edit(&mut text_edit); - text_edit.finish().apply(&mut from); - assert_eq!(&*from, to, "diff did not turn `from` to `to`"); - } -} diff --git a/crates/syntax/src/fuzz.rs b/crates/syntax/src/fuzz.rs index 682dcd7cc4..fd20e603ed 100644 --- a/crates/syntax/src/fuzz.rs +++ b/crates/syntax/src/fuzz.rs @@ -5,7 +5,6 @@ use std::str::{self, FromStr}; use parser::Edition; -use text_edit::Indel; use crate::{validation, AstNode, SourceFile, TextRange}; @@ -22,7 +21,8 @@ pub fn check_parser(text: &str) { #[derive(Debug, Clone)] pub struct CheckReparse { text: String, - edit: Indel, + delete: TextRange, + insert: String, edited_text: String, } @@ -43,14 +43,13 @@ impl CheckReparse { TextRange::at(delete_start.try_into().unwrap(), delete_len.try_into().unwrap()); let edited_text = format!("{}{}{}", &text[..delete_start], &insert, &text[delete_start + delete_len..]); - let edit = Indel { insert, delete }; - Some(CheckReparse { text, edit, edited_text }) + Some(CheckReparse { text, insert, delete, edited_text }) } #[allow(clippy::print_stderr)] pub fn run(&self) { let parse = SourceFile::parse(&self.text, Edition::CURRENT); - let new_parse = parse.reparse(&self.edit, Edition::CURRENT); + let new_parse = parse.reparse(self.delete, &self.insert, Edition::CURRENT); check_file_invariants(&new_parse.tree()); assert_eq!(&new_parse.tree().syntax().text().to_string(), &self.edited_text); let full_reparse = SourceFile::parse(&self.edited_text, Edition::CURRENT); diff --git a/crates/syntax/src/lib.rs b/crates/syntax/src/lib.rs index c1554c4b29..c9e9f468dc 100644 --- a/crates/syntax/src/lib.rs +++ b/crates/syntax/src/lib.rs @@ -44,10 +44,9 @@ pub mod syntax_editor; pub mod ted; pub mod utils; -use std::marker::PhantomData; +use std::{marker::PhantomData, ops::Range}; use stdx::format_to; -use text_edit::Indel; use triomphe::Arc; pub use crate::{ @@ -150,16 +149,22 @@ impl Parse { buf } - pub fn reparse(&self, indel: &Indel, edition: Edition) -> Parse { - self.incremental_reparse(indel, edition) - .unwrap_or_else(|| self.full_reparse(indel, edition)) + pub fn reparse(&self, delete: TextRange, insert: &str, edition: Edition) -> Parse { + self.incremental_reparse(delete, insert, edition) + .unwrap_or_else(|| self.full_reparse(delete, insert, edition)) } - fn incremental_reparse(&self, indel: &Indel, edition: Edition) -> Option> { + fn incremental_reparse( + &self, + delete: TextRange, + insert: &str, + edition: Edition, + ) -> Option> { // FIXME: validation errors are not handled here parsing::incremental_reparse( self.tree().syntax(), - indel, + delete, + insert, self.errors.as_deref().unwrap_or_default().iter().cloned(), edition, ) @@ -170,9 +175,9 @@ impl Parse { }) } - fn full_reparse(&self, indel: &Indel, edition: Edition) -> Parse { + fn full_reparse(&self, delete: TextRange, insert: &str, edition: Edition) -> Parse { let mut text = self.tree().syntax().text().to_string(); - indel.apply(&mut text); + text.replace_range(Range::::from(delete), insert); SourceFile::parse(&text, edition) } } diff --git a/crates/syntax/src/parsing/reparsing.rs b/crates/syntax/src/parsing/reparsing.rs index a5cc4e90df..f2eab18c27 100644 --- a/crates/syntax/src/parsing/reparsing.rs +++ b/crates/syntax/src/parsing/reparsing.rs @@ -6,8 +6,9 @@ //! - otherwise, we search for the nearest `{}` block which contains the edit //! and try to parse only this block. +use std::ops::Range; + use parser::{Edition, Reparser}; -use text_edit::Indel; use crate::{ parsing::build_tree, @@ -19,38 +20,48 @@ use crate::{ pub(crate) fn incremental_reparse( node: &SyntaxNode, - edit: &Indel, + delete: TextRange, + insert: &str, errors: impl IntoIterator, edition: Edition, ) -> Option<(GreenNode, Vec, TextRange)> { - if let Some((green, new_errors, old_range)) = reparse_token(node, edit, edition) { - return Some((green, merge_errors(errors, new_errors, old_range, edit), old_range)); + if let Some((green, new_errors, old_range)) = reparse_token(node, delete, insert, edition) { + return Some(( + green, + merge_errors(errors, new_errors, old_range, delete, insert), + old_range, + )); } - if let Some((green, new_errors, old_range)) = reparse_block(node, edit, edition) { - return Some((green, merge_errors(errors, new_errors, old_range, edit), old_range)); + if let Some((green, new_errors, old_range)) = reparse_block(node, delete, insert, edition) { + return Some(( + green, + merge_errors(errors, new_errors, old_range, delete, insert), + old_range, + )); } None } fn reparse_token( root: &SyntaxNode, - edit: &Indel, + delete: TextRange, + insert: &str, edition: Edition, ) -> Option<(GreenNode, Vec, TextRange)> { - let prev_token = root.covering_element(edit.delete).as_token()?.clone(); + let prev_token = root.covering_element(delete).as_token()?.clone(); let prev_token_kind = prev_token.kind(); match prev_token_kind { WHITESPACE | COMMENT | IDENT | STRING | BYTE_STRING | C_STRING => { if prev_token_kind == WHITESPACE || prev_token_kind == COMMENT { // removing a new line may extends previous token - let deleted_range = edit.delete - prev_token.text_range().start(); + let deleted_range = delete - prev_token.text_range().start(); if prev_token.text()[deleted_range].contains('\n') { return None; } } - let mut new_text = get_text_after_edit(prev_token.clone().into(), edit); + let mut new_text = get_text_after_edit(prev_token.clone().into(), delete, insert); let (new_token_kind, new_err) = parser::LexedStr::single_token(edition, &new_text)?; if new_token_kind != prev_token_kind @@ -85,11 +96,12 @@ fn reparse_token( fn reparse_block( root: &SyntaxNode, - edit: &Indel, + delete: TextRange, + insert: &str, edition: parser::Edition, ) -> Option<(GreenNode, Vec, TextRange)> { - let (node, reparser) = find_reparsable_node(root, edit.delete)?; - let text = get_text_after_edit(node.clone().into(), edit); + let (node, reparser) = find_reparsable_node(root, delete)?; + let text = get_text_after_edit(node.clone().into(), delete, insert); let lexed = parser::LexedStr::new(edition, text.as_str()); let parser_input = lexed.to_input(edition); @@ -104,14 +116,14 @@ fn reparse_block( Some((node.replace_with(green), new_parser_errors, node.text_range())) } -fn get_text_after_edit(element: SyntaxElement, edit: &Indel) -> String { - let edit = Indel::replace(edit.delete - element.text_range().start(), edit.insert.clone()); +fn get_text_after_edit(element: SyntaxElement, mut delete: TextRange, insert: &str) -> String { + delete -= element.text_range().start(); let mut text = match element { NodeOrToken::Token(token) => token.text().to_owned(), NodeOrToken::Node(node) => node.text().to_string(), }; - edit.apply(&mut text); + text.replace_range(Range::::from(delete), insert); text } @@ -153,7 +165,8 @@ fn merge_errors( old_errors: impl IntoIterator, new_errors: Vec, range_before_reparse: TextRange, - edit: &Indel, + delete: TextRange, + insert: &str, ) -> Vec { let mut res = Vec::new(); @@ -162,8 +175,8 @@ fn merge_errors( if old_err_range.end() <= range_before_reparse.start() { res.push(old_err); } else if old_err_range.start() >= range_before_reparse.end() { - let inserted_len = TextSize::of(&edit.insert); - res.push(old_err.with_range((old_err_range + inserted_len) - edit.delete.len())); + let inserted_len = TextSize::of(insert); + res.push(old_err.with_range((old_err_range + inserted_len) - delete.len())); // Note: extra parens are intentional to prevent uint underflow, HWAB (here was a bug) } } @@ -177,6 +190,8 @@ fn merge_errors( #[cfg(test)] mod tests { + use std::ops::Range; + use parser::Edition; use test_utils::{assert_eq_text, extract_range}; @@ -185,10 +200,9 @@ mod tests { fn do_check(before: &str, replace_with: &str, reparsed_len: u32) { let (range, before) = extract_range(before); - let edit = Indel::replace(range, replace_with.to_owned()); let after = { let mut after = before.clone(); - edit.apply(&mut after); + after.replace_range(Range::::from(range), replace_with); after }; @@ -197,7 +211,8 @@ mod tests { let before = SourceFile::parse(&before, Edition::CURRENT); let (green, new_errors, range) = incremental_reparse( before.tree().syntax(), - &edit, + range, + replace_with, before.errors.as_deref().unwrap_or_default().iter().cloned(), Edition::CURRENT, ) diff --git a/crates/text-edit/Cargo.toml b/crates/text-edit/Cargo.toml deleted file mode 100644 index dc6b3d31a0..0000000000 --- a/crates/text-edit/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "text-edit" -version = "0.0.0" -repository.workspace = true -description = "Representation of a `TextEdit` for rust-analyzer." - -authors.workspace = true -edition.workspace = true -license.workspace = true -rust-version.workspace = true - -[lib] -doctest = false - -[dependencies] -itertools.workspace = true -text-size.workspace = true - -[lints] -workspace = true \ No newline at end of file