diff options
Diffstat (limited to 'src/tools/rust-analyzer/crates/ide-db')
13 files changed, 265 insertions, 173 deletions
diff --git a/src/tools/rust-analyzer/crates/ide-db/Cargo.toml b/src/tools/rust-analyzer/crates/ide-db/Cargo.toml index 4e75dc4db..faec74206 100644 --- a/src/tools/rust-analyzer/crates/ide-db/Cargo.toml +++ b/src/tools/rust-analyzer/crates/ide-db/Cargo.toml @@ -21,7 +21,7 @@ once_cell = "1.17.0" either = "1.7.0" itertools = "0.10.5" arrayvec = "0.7.2" -indexmap = "1.9.1" +indexmap = "2.0.0" memchr = "2.5.0" triomphe.workspace = true nohash-hasher.workspace = true diff --git a/src/tools/rust-analyzer/crates/ide-db/src/apply_change.rs b/src/tools/rust-analyzer/crates/ide-db/src/apply_change.rs index 0dd544d0a..a0b05c87a 100644 --- a/src/tools/rust-analyzer/crates/ide-db/src/apply_change.rs +++ b/src/tools/rust-analyzer/crates/ide-db/src/apply_change.rs @@ -99,8 +99,8 @@ impl RootDatabase { hir::db::AstIdMapQuery hir::db::ParseMacroExpansionQuery hir::db::InternMacroCallQuery - hir::db::MacroArgTextQuery - hir::db::MacroDefQuery + hir::db::MacroArgNodeQuery + hir::db::DeclMacroExpanderQuery hir::db::MacroExpandQuery hir::db::ExpandProcMacroQuery hir::db::HygieneFrameQuery diff --git a/src/tools/rust-analyzer/crates/ide-db/src/defs.rs b/src/tools/rust-analyzer/crates/ide-db/src/defs.rs index 760834bfa..5e4562d9c 100644 --- a/src/tools/rust-analyzer/crates/ide-db/src/defs.rs +++ b/src/tools/rust-analyzer/crates/ide-db/src/defs.rs @@ -7,10 +7,10 @@ use arrayvec::ArrayVec; use hir::{ - Adt, AsAssocItem, AssocItem, BuiltinAttr, BuiltinType, Const, Crate, DeriveHelper, Field, - Function, GenericParam, HasVisibility, Impl, Label, Local, Macro, Module, ModuleDef, Name, - PathResolution, Semantics, Static, ToolModule, Trait, TraitAlias, TypeAlias, Variant, - Visibility, + Adt, AsAssocItem, AssocItem, BuiltinAttr, BuiltinType, Const, Crate, DeriveHelper, + ExternCrateDecl, Field, Function, GenericParam, HasVisibility, Impl, Label, Local, Macro, + Module, ModuleDef, Name, PathResolution, Semantics, Static, ToolModule, Trait, TraitAlias, + TypeAlias, Variant, Visibility, }; use stdx::impl_from; use syntax::{ @@ -42,6 +42,7 @@ pub enum Definition { DeriveHelper(DeriveHelper), BuiltinAttr(BuiltinAttr), ToolModule(ToolModule), + ExternCrateDecl(ExternCrateDecl), } impl Definition { @@ -73,6 +74,7 @@ impl Definition { Definition::Local(it) => it.module(db), Definition::GenericParam(it) => it.module(db), Definition::Label(it) => it.module(db), + Definition::ExternCrateDecl(it) => it.module(db), Definition::DeriveHelper(it) => it.derive().module(db), Definition::BuiltinAttr(_) | Definition::BuiltinType(_) | Definition::ToolModule(_) => { return None @@ -93,6 +95,7 @@ impl Definition { Definition::TraitAlias(it) => it.visibility(db), Definition::TypeAlias(it) => it.visibility(db), Definition::Variant(it) => it.visibility(db), + Definition::ExternCrateDecl(it) => it.visibility(db), Definition::BuiltinType(_) => Visibility::Public, Definition::Macro(_) => return None, Definition::BuiltinAttr(_) @@ -127,6 +130,7 @@ impl Definition { Definition::BuiltinAttr(_) => return None, // FIXME Definition::ToolModule(_) => return None, // FIXME Definition::DeriveHelper(it) => it.name(db), + Definition::ExternCrateDecl(it) => return it.alias_or_name(db), }; Some(name) } @@ -196,6 +200,10 @@ impl IdentClass { res.push(Definition::Local(local_ref)); res.push(Definition::Field(field_ref)); } + IdentClass::NameRefClass(NameRefClass::ExternCrateShorthand { decl, krate }) => { + res.push(Definition::ExternCrateDecl(decl)); + res.push(Definition::Module(krate.root_module())); + } IdentClass::Operator( OperatorClass::Await(func) | OperatorClass::Prefix(func) @@ -222,6 +230,10 @@ impl IdentClass { res.push(Definition::Local(local_ref)); res.push(Definition::Field(field_ref)); } + IdentClass::NameRefClass(NameRefClass::ExternCrateShorthand { decl, krate }) => { + res.push(Definition::ExternCrateDecl(decl)); + res.push(Definition::Module(krate.root_module())); + } IdentClass::Operator(_) => (), } res @@ -310,6 +322,7 @@ impl NameClass { ast::Item::Enum(it) => Definition::Adt(hir::Adt::Enum(sema.to_def(&it)?)), ast::Item::Struct(it) => Definition::Adt(hir::Adt::Struct(sema.to_def(&it)?)), ast::Item::Union(it) => Definition::Adt(hir::Adt::Union(sema.to_def(&it)?)), + ast::Item::ExternCrate(it) => Definition::ExternCrateDecl(sema.to_def(&it)?), _ => return None, }; Some(definition) @@ -346,10 +359,8 @@ impl NameClass { let path = use_tree.path()?; sema.resolve_path(&path).map(Definition::from) } else { - let extern_crate = rename.syntax().parent().and_then(ast::ExternCrate::cast)?; - let krate = sema.resolve_extern_crate(&extern_crate)?; - let root_module = krate.root_module(sema.db); - Some(Definition::Module(root_module)) + sema.to_def(&rename.syntax().parent().and_then(ast::ExternCrate::cast)?) + .map(Definition::ExternCrateDecl) } } } @@ -427,7 +438,19 @@ impl OperatorClass { #[derive(Debug)] pub enum NameRefClass { Definition(Definition), - FieldShorthand { local_ref: Local, field_ref: Field }, + FieldShorthand { + local_ref: Local, + field_ref: Field, + }, + /// The specific situation where we have an extern crate decl without a rename + /// Here we have both a declaration and a reference. + /// ```rs + /// extern crate foo; + /// ``` + ExternCrateShorthand { + decl: ExternCrateDecl, + krate: Crate, + }, } impl NameRefClass { @@ -513,10 +536,14 @@ impl NameRefClass { } None }, - ast::ExternCrate(extern_crate) => { - let krate = sema.resolve_extern_crate(&extern_crate)?; - let root_module = krate.root_module(sema.db); - Some(NameRefClass::Definition(Definition::Module(root_module))) + ast::ExternCrate(extern_crate_ast) => { + let extern_crate = sema.to_def(&extern_crate_ast)?; + let krate = extern_crate.resolved_crate(sema.db)?; + Some(if extern_crate_ast.rename().is_some() { + NameRefClass::Definition(Definition::Module(krate.root_module())) + } else { + NameRefClass::ExternCrateShorthand { krate, decl: extern_crate } + }) }, _ => None } diff --git a/src/tools/rust-analyzer/crates/ide-db/src/famous_defs.rs b/src/tools/rust-analyzer/crates/ide-db/src/famous_defs.rs index c8341fed1..b63dde2c2 100644 --- a/src/tools/rust-analyzer/crates/ide-db/src/famous_defs.rs +++ b/src/tools/rust-analyzer/crates/ide-db/src/famous_defs.rs @@ -167,7 +167,7 @@ impl FamousDefs<'_, '_> { lang_crate => lang_crate, }; let std_crate = self.find_lang_crate(lang_crate)?; - let mut module = std_crate.root_module(db); + let mut module = std_crate.root_module(); for segment in path { module = module.children(db).find_map(|child| { let name = child.name(db)?; diff --git a/src/tools/rust-analyzer/crates/ide-db/src/generated/lints.rs b/src/tools/rust-analyzer/crates/ide-db/src/generated/lints.rs index e488300b4..49b37024a 100644 --- a/src/tools/rust-analyzer/crates/ide-db/src/generated/lints.rs +++ b/src/tools/rust-analyzer/crates/ide-db/src/generated/lints.rs @@ -931,22 +931,6 @@ $ cat $(find -name '*.s') "##, }, Lint { - label: "abi_thiscall", - description: r##"# `abi_thiscall` - -The tracking issue for this feature is: [#42202] - -[#42202]: https://github.com/rust-lang/rust/issues/42202 - ------------------------- - -The MSVC ABI on x86 Windows uses the `thiscall` calling convention for C++ -instance methods by default; it is identical to the usual (C) calling -convention on x86 Windows except that the first parameter of the method, -the `this` pointer, is passed in the ECX register. -"##, - }, - Lint { label: "allocator_api", description: r##"# `allocator_api` diff --git a/src/tools/rust-analyzer/crates/ide-db/src/helpers.rs b/src/tools/rust-analyzer/crates/ide-db/src/helpers.rs index eba9d8afc..1eb8f0002 100644 --- a/src/tools/rust-analyzer/crates/ide-db/src/helpers.rs +++ b/src/tools/rust-analyzer/crates/ide-db/src/helpers.rs @@ -9,7 +9,10 @@ use syntax::{ AstToken, SyntaxKind, SyntaxToken, TokenAtOffset, }; -use crate::{defs::Definition, generated, RootDatabase}; +use crate::{ + defs::{Definition, IdentClass}, + generated, RootDatabase, +}; pub fn item_name(db: &RootDatabase, item: ItemInNs) -> Option<Name> { match item { @@ -109,3 +112,16 @@ pub fn is_editable_crate(krate: Crate, db: &RootDatabase) -> bool { let source_root_id = db.file_source_root(root_file); !db.source_root(source_root_id).is_library } + +pub fn get_definition( + sema: &Semantics<'_, RootDatabase>, + token: SyntaxToken, +) -> Option<Definition> { + for token in sema.descend_into_macros(token) { + let def = IdentClass::classify_token(sema, &token).map(IdentClass::definitions_no_ops); + if let Some(&[x]) = def.as_deref() { + return Some(x); + } + } + None +} diff --git a/src/tools/rust-analyzer/crates/ide-db/src/imports/import_assets.rs b/src/tools/rust-analyzer/crates/ide-db/src/imports/import_assets.rs index 901d592c6..e52dc3567 100644 --- a/src/tools/rust-analyzer/crates/ide-db/src/imports/import_assets.rs +++ b/src/tools/rust-analyzer/crates/ide-db/src/imports/import_assets.rs @@ -13,7 +13,7 @@ use syntax::{ use crate::{ helpers::item_name, - items_locator::{self, AssocItemSearch, DEFAULT_QUERY_SEARCH_LIMIT}, + items_locator::{self, AssocSearchMode, DEFAULT_QUERY_SEARCH_LIMIT}, RootDatabase, }; @@ -317,7 +317,7 @@ fn path_applicable_imports( // * improve the associated completion item matching and/or scoring to ensure no noisy completions appear // // see also an ignored test under FIXME comment in the qualify_path.rs module - AssocItemSearch::Exclude, + AssocSearchMode::Exclude, Some(DEFAULT_QUERY_SEARCH_LIMIT.inner()), ) .filter_map(|item| { @@ -334,7 +334,7 @@ fn path_applicable_imports( sema, current_crate, path_candidate.name.clone(), - AssocItemSearch::Include, + AssocSearchMode::Include, Some(DEFAULT_QUERY_SEARCH_LIMIT.inner()), ) .filter_map(|item| { @@ -483,7 +483,7 @@ fn trait_applicable_items( sema, current_crate, trait_candidate.assoc_item_name.clone(), - AssocItemSearch::AssocItemsOnly, + AssocSearchMode::AssocItemsOnly, Some(DEFAULT_QUERY_SEARCH_LIMIT.inner()), ) .filter_map(|input| item_as_assoc(db, input)) diff --git a/src/tools/rust-analyzer/crates/ide-db/src/items_locator.rs b/src/tools/rust-analyzer/crates/ide-db/src/items_locator.rs index 46f1353e2..3f7a3ec2d 100644 --- a/src/tools/rust-analyzer/crates/ide-db/src/items_locator.rs +++ b/src/tools/rust-analyzer/crates/ide-db/src/items_locator.rs @@ -3,10 +3,7 @@ //! The main reason for this module to exist is the fact that project's items and dependencies' items //! are located in different caches, with different APIs. use either::Either; -use hir::{ - import_map::{self, ImportKind}, - AsAssocItem, Crate, ItemInNs, Semantics, -}; +use hir::{import_map, AsAssocItem, Crate, ItemInNs, Semantics}; use limit::Limit; use crate::{imports::import_assets::NameToImport, symbol_index, RootDatabase}; @@ -14,23 +11,14 @@ use crate::{imports::import_assets::NameToImport, symbol_index, RootDatabase}; /// A value to use, when uncertain which limit to pick. pub static DEFAULT_QUERY_SEARCH_LIMIT: Limit = Limit::new(40); -/// Three possible ways to search for the name in associated and/or other items. -#[derive(Debug, Clone, Copy)] -pub enum AssocItemSearch { - /// Search for the name in both associated and other items. - Include, - /// Search for the name in other items only. - Exclude, - /// Search for the name in the associated items only. - AssocItemsOnly, -} +pub use import_map::AssocSearchMode; /// Searches for importable items with the given name in the crate and its dependencies. pub fn items_with_name<'a>( sema: &'a Semantics<'_, RootDatabase>, krate: Crate, name: NameToImport, - assoc_item_search: AssocItemSearch, + assoc_item_search: AssocSearchMode, limit: Option<usize>, ) -> impl Iterator<Item = ItemInNs> + 'a { let _p = profile::span("items_with_name").detail(|| { @@ -48,9 +36,7 @@ pub fn items_with_name<'a>( let mut local_query = symbol_index::Query::new(exact_name.clone()); local_query.exact(); - let external_query = import_map::Query::new(exact_name) - .name_only() - .search_mode(import_map::SearchMode::Equals); + let external_query = import_map::Query::new(exact_name); ( local_query, @@ -61,17 +47,8 @@ pub fn items_with_name<'a>( let mut local_query = symbol_index::Query::new(fuzzy_search_string.clone()); let mut external_query = import_map::Query::new(fuzzy_search_string.clone()) - .search_mode(import_map::SearchMode::Fuzzy) - .name_only(); - match assoc_item_search { - AssocItemSearch::Include => {} - AssocItemSearch::Exclude => { - external_query = external_query.exclude_import_kind(ImportKind::AssociatedItem); - } - AssocItemSearch::AssocItemsOnly => { - external_query = external_query.assoc_items_only(); - } - } + .fuzzy() + .assoc_search_mode(assoc_item_search); if fuzzy_search_string.to_lowercase() != fuzzy_search_string { local_query.case_sensitive(); @@ -93,13 +70,15 @@ pub fn items_with_name<'a>( fn find_items<'a>( sema: &'a Semantics<'_, RootDatabase>, krate: Crate, - assoc_item_search: AssocItemSearch, + assoc_item_search: AssocSearchMode, local_query: symbol_index::Query, external_query: import_map::Query, ) -> impl Iterator<Item = ItemInNs> + 'a { let _p = profile::span("find_items"); let db = sema.db; + // NOTE: `external_query` includes `assoc_item_search`, so we don't need to + // filter on our own. let external_importables = krate.query_external_importables(db, external_query).map(|external_importable| { match external_importable { @@ -112,18 +91,15 @@ fn find_items<'a>( let local_results = local_query .search(&symbol_index::crate_symbols(db, krate)) .into_iter() - .filter_map(|local_candidate| match local_candidate.def { - hir::ModuleDef::Macro(macro_def) => Some(ItemInNs::Macros(macro_def)), - def => Some(ItemInNs::from(def)), + .filter(move |candidate| match assoc_item_search { + AssocSearchMode::Include => true, + AssocSearchMode::Exclude => candidate.def.as_assoc_item(db).is_none(), + AssocSearchMode::AssocItemsOnly => candidate.def.as_assoc_item(db).is_some(), + }) + .map(|local_candidate| match local_candidate.def { + hir::ModuleDef::Macro(macro_def) => ItemInNs::Macros(macro_def), + def => ItemInNs::from(def), }); - external_importables.chain(local_results).filter(move |&item| match assoc_item_search { - AssocItemSearch::Include => true, - AssocItemSearch::Exclude => !is_assoc_item(item, sema.db), - AssocItemSearch::AssocItemsOnly => is_assoc_item(item, sema.db), - }) -} - -fn is_assoc_item(item: ItemInNs, db: &RootDatabase) -> bool { - item.as_module_def().and_then(|module_def| module_def.as_assoc_item(db)).is_some() + external_importables.chain(local_results) } diff --git a/src/tools/rust-analyzer/crates/ide-db/src/lib.rs b/src/tools/rust-analyzer/crates/ide-db/src/lib.rs index ff1a20f03..f27ed485d 100644 --- a/src/tools/rust-analyzer/crates/ide-db/src/lib.rs +++ b/src/tools/rust-analyzer/crates/ide-db/src/lib.rs @@ -200,8 +200,8 @@ impl RootDatabase { hir_db::AstIdMapQuery // hir_db::ParseMacroExpansionQuery // hir_db::InternMacroCallQuery - hir_db::MacroArgTextQuery - hir_db::MacroDefQuery + hir_db::MacroArgNodeQuery + hir_db::DeclMacroExpanderQuery // hir_db::MacroExpandQuery hir_db::ExpandProcMacroQuery hir_db::HygieneFrameQuery diff --git a/src/tools/rust-analyzer/crates/ide-db/src/path_transform.rs b/src/tools/rust-analyzer/crates/ide-db/src/path_transform.rs index 73e6a920e..1d0cb426a 100644 --- a/src/tools/rust-analyzer/crates/ide-db/src/path_transform.rs +++ b/src/tools/rust-analyzer/crates/ide-db/src/path_transform.rs @@ -195,7 +195,7 @@ fn postorder(item: &SyntaxNode) -> impl Iterator<Item = SyntaxNode> { }) } -impl<'a> Ctx<'a> { +impl Ctx<'_> { fn apply(&self, item: &SyntaxNode) { // `transform_path` may update a node's parent and that would break the // tree traversal. Thus all paths in the tree are collected into a vec diff --git a/src/tools/rust-analyzer/crates/ide-db/src/rename.rs b/src/tools/rust-analyzer/crates/ide-db/src/rename.rs index 52a23b4b8..aa0bb7cce 100644 --- a/src/tools/rust-analyzer/crates/ide-db/src/rename.rs +++ b/src/tools/rust-analyzer/crates/ide-db/src/rename.rs @@ -82,8 +82,9 @@ impl Definition { } /// Textual range of the identifier which will change when renaming this - /// `Definition`. Note that some definitions, like builtin types, can't be - /// renamed. + /// `Definition`. Note that builtin types can't be + /// renamed and extern crate names will report its range, though a rename will introduce + /// an alias instead. pub fn range_for_rename(self, sema: &Semantics<'_, RootDatabase>) -> Option<FileRange> { let res = match self { Definition::Macro(mac) => { @@ -146,6 +147,16 @@ impl Definition { let lifetime = src.value.lifetime()?; src.with_value(lifetime.syntax()).original_file_range_opt(sema.db) } + Definition::ExternCrateDecl(it) => { + let src = it.source(sema.db)?; + if let Some(rename) = src.value.rename() { + let name = rename.name()?; + src.with_value(name.syntax()).original_file_range_opt(sema.db) + } else { + let name = src.value.name_ref()?; + src.with_value(name.syntax()).original_file_range_opt(sema.db) + } + } Definition::BuiltinType(_) => return None, Definition::SelfType(_) => return None, Definition::BuiltinAttr(_) => return None, @@ -526,6 +537,9 @@ fn source_edit_from_def( TextRange::new(range.start() + syntax::TextSize::from(1), range.end()), new_name.strip_prefix('\'').unwrap_or(new_name).to_owned(), ), + Definition::ExternCrateDecl(decl) if decl.alias(sema.db).is_none() => { + (TextRange::empty(range.end()), format!(" as {new_name}")) + } _ => (range, new_name.to_owned()), }; edit.replace(range, new_name); diff --git a/src/tools/rust-analyzer/crates/ide-db/src/search.rs b/src/tools/rust-analyzer/crates/ide-db/src/search.rs index e8ff107bd..d5abd0991 100644 --- a/src/tools/rust-analyzer/crates/ide-db/src/search.rs +++ b/src/tools/rust-analyzer/crates/ide-db/src/search.rs @@ -127,7 +127,7 @@ impl SearchScope { } /// Build a search scope spanning the given module and all its submodules. - fn module_and_children(db: &RootDatabase, module: hir::Module) -> SearchScope { + pub fn module_and_children(db: &RootDatabase, module: hir::Module) -> SearchScope { let mut entries = IntMap::default(); let (file_id, range) = { @@ -149,10 +149,8 @@ impl SearchScope { let mut to_visit: Vec<_> = module.children(db).collect(); while let Some(module) = to_visit.pop() { - if let InFile { file_id, value: ModuleSource::SourceFile(_) } = - module.definition_source(db) - { - entries.insert(file_id.original_file(db), None); + if let Some(file_id) = module.as_source_file_id(db) { + entries.insert(file_id, None); } to_visit.extend(module.children(db)); } @@ -331,7 +329,7 @@ impl Definition { pub struct FindUsages<'a> { def: Definition, sema: &'a Semantics<'a, RootDatabase>, - scope: Option<SearchScope>, + scope: Option<&'a SearchScope>, /// The container of our definition should it be an assoc item assoc_item_container: Option<hir::AssocItemContainer>, /// whether to search for the `Self` type of the definition @@ -342,19 +340,19 @@ pub struct FindUsages<'a> { impl<'a> FindUsages<'a> { /// Enable searching for `Self` when the definition is a type or `self` for modules. - pub fn include_self_refs(mut self) -> FindUsages<'a> { + pub fn include_self_refs(mut self) -> Self { self.include_self_kw_refs = def_to_ty(self.sema, &self.def); self.search_self_mod = true; self } /// Limit the search to a given [`SearchScope`]. - pub fn in_scope(self, scope: SearchScope) -> FindUsages<'a> { + pub fn in_scope(self, scope: &'a SearchScope) -> Self { self.set_scope(Some(scope)) } /// Limit the search to a given [`SearchScope`]. - pub fn set_scope(mut self, scope: Option<SearchScope>) -> FindUsages<'a> { + pub fn set_scope(mut self, scope: Option<&'a SearchScope>) -> Self { assert!(self.scope.is_none()); self.scope = scope; self @@ -378,7 +376,7 @@ impl<'a> FindUsages<'a> { res } - fn search(&self, sink: &mut dyn FnMut(FileId, FileReference) -> bool) { + pub fn search(&self, sink: &mut dyn FnMut(FileId, FileReference) -> bool) { let _p = profile::span("FindUsages:search"); let sema = self.sema; diff --git a/src/tools/rust-analyzer/crates/ide-db/src/source_change.rs b/src/tools/rust-analyzer/crates/ide-db/src/source_change.rs index 061fb0f05..39763479c 100644 --- a/src/tools/rust-analyzer/crates/ide-db/src/source_change.rs +++ b/src/tools/rust-analyzer/crates/ide-db/src/source_change.rs @@ -7,14 +7,17 @@ use std::{collections::hash_map::Entry, iter, mem}; use crate::SnippetCap; use base_db::{AnchoredPathBuf, FileId}; +use itertools::Itertools; use nohash_hasher::IntMap; use stdx::never; -use syntax::{algo, ast, ted, AstNode, SyntaxNode, SyntaxNodePtr, TextRange, TextSize}; +use syntax::{ + algo, AstNode, SyntaxElement, SyntaxNode, SyntaxNodePtr, SyntaxToken, TextRange, TextSize, +}; use text_edit::{TextEdit, TextEditBuilder}; #[derive(Default, Debug, Clone)] pub struct SourceChange { - pub source_file_edits: IntMap<FileId, TextEdit>, + pub source_file_edits: IntMap<FileId, (TextEdit, Option<SnippetEdit>)>, pub file_system_edits: Vec<FileSystemEdit>, pub is_snippet: bool, } @@ -23,7 +26,7 @@ impl SourceChange { /// Creates a new SourceChange with the given label /// from the edits. pub fn from_edits( - source_file_edits: IntMap<FileId, TextEdit>, + source_file_edits: IntMap<FileId, (TextEdit, Option<SnippetEdit>)>, file_system_edits: Vec<FileSystemEdit>, ) -> Self { SourceChange { source_file_edits, file_system_edits, is_snippet: false } @@ -31,7 +34,7 @@ impl SourceChange { pub fn from_text_edit(file_id: FileId, edit: TextEdit) -> Self { SourceChange { - source_file_edits: iter::once((file_id, edit)).collect(), + source_file_edits: iter::once((file_id, (edit, None))).collect(), ..Default::default() } } @@ -39,12 +42,31 @@ impl SourceChange { /// Inserts a [`TextEdit`] for the given [`FileId`]. This properly handles merging existing /// edits for a file if some already exist. pub fn insert_source_edit(&mut self, file_id: FileId, edit: TextEdit) { + self.insert_source_and_snippet_edit(file_id, edit, None) + } + + /// Inserts a [`TextEdit`] and potentially a [`SnippetEdit`] for the given [`FileId`]. + /// This properly handles merging existing edits for a file if some already exist. + pub fn insert_source_and_snippet_edit( + &mut self, + file_id: FileId, + edit: TextEdit, + snippet_edit: Option<SnippetEdit>, + ) { match self.source_file_edits.entry(file_id) { Entry::Occupied(mut entry) => { - never!(entry.get_mut().union(edit).is_err(), "overlapping edits for same file"); + let value = entry.get_mut(); + never!(value.0.union(edit).is_err(), "overlapping edits for same file"); + never!( + value.1.is_some() && snippet_edit.is_some(), + "overlapping snippet edits for same file" + ); + if value.1.is_none() { + value.1 = snippet_edit; + } } Entry::Vacant(entry) => { - entry.insert(edit); + entry.insert((edit, snippet_edit)); } } } @@ -53,7 +75,10 @@ impl SourceChange { self.file_system_edits.push(edit); } - pub fn get_source_edit(&self, file_id: FileId) -> Option<&TextEdit> { + pub fn get_source_and_snippet_edit( + &self, + file_id: FileId, + ) -> Option<&(TextEdit, Option<SnippetEdit>)> { self.source_file_edits.get(&file_id) } @@ -67,7 +92,18 @@ impl SourceChange { impl Extend<(FileId, TextEdit)> for SourceChange { fn extend<T: IntoIterator<Item = (FileId, TextEdit)>>(&mut self, iter: T) { - iter.into_iter().for_each(|(file_id, edit)| self.insert_source_edit(file_id, edit)); + self.extend(iter.into_iter().map(|(file_id, edit)| (file_id, (edit, None)))) + } +} + +impl Extend<(FileId, (TextEdit, Option<SnippetEdit>))> for SourceChange { + fn extend<T: IntoIterator<Item = (FileId, (TextEdit, Option<SnippetEdit>))>>( + &mut self, + iter: T, + ) { + iter.into_iter().for_each(|(file_id, (edit, snippet_edit))| { + self.insert_source_and_snippet_edit(file_id, edit, snippet_edit) + }); } } @@ -79,6 +115,8 @@ impl Extend<FileSystemEdit> for SourceChange { impl From<IntMap<FileId, TextEdit>> for SourceChange { fn from(source_file_edits: IntMap<FileId, TextEdit>) -> SourceChange { + let source_file_edits = + source_file_edits.into_iter().map(|(file_id, edit)| (file_id, (edit, None))).collect(); SourceChange { source_file_edits, file_system_edits: Vec::new(), is_snippet: false } } } @@ -91,6 +129,65 @@ impl FromIterator<(FileId, TextEdit)> for SourceChange { } } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SnippetEdit(Vec<(u32, TextRange)>); + +impl SnippetEdit { + pub fn new(snippets: Vec<Snippet>) -> Self { + let mut snippet_ranges = snippets + .into_iter() + .zip(1..) + .with_position() + .map(|pos| { + let (snippet, index) = match pos { + itertools::Position::First(it) | itertools::Position::Middle(it) => it, + // last/only snippet gets index 0 + itertools::Position::Last((snippet, _)) + | itertools::Position::Only((snippet, _)) => (snippet, 0), + }; + + let range = match snippet { + Snippet::Tabstop(pos) => TextRange::empty(pos), + Snippet::Placeholder(range) => range, + }; + (index, range) + }) + .collect_vec(); + + snippet_ranges.sort_by_key(|(_, range)| range.start()); + + // Ensure that none of the ranges overlap + let disjoint_ranges = snippet_ranges + .iter() + .zip(snippet_ranges.iter().skip(1)) + .all(|((_, left), (_, right))| left.end() <= right.start() || left == right); + stdx::always!(disjoint_ranges); + + SnippetEdit(snippet_ranges) + } + + /// Inserts all of the snippets into the given text. + pub fn apply(&self, text: &mut String) { + // Start from the back so that we don't have to adjust ranges + for (index, range) in self.0.iter().rev() { + if range.is_empty() { + // is a tabstop + text.insert_str(range.start().into(), &format!("${index}")); + } else { + // is a placeholder + text.insert(range.end().into(), '}'); + text.insert_str(range.start().into(), &format!("${{{index}:")); + } + } + } + + /// Gets the underlying snippet index + text range + /// Tabstops are represented by an empty range, and placeholders use the range that they were given + pub fn into_edit_ranges(self) -> Vec<(u32, TextRange)> { + self.0 + } +} + pub struct SourceChangeBuilder { pub edit: TextEditBuilder, pub file_id: FileId, @@ -149,24 +246,19 @@ impl SourceChangeBuilder { } fn commit(&mut self) { - // Render snippets first so that they get bundled into the tree diff - if let Some(mut snippets) = self.snippet_builder.take() { - // Last snippet always has stop index 0 - let last_stop = snippets.places.pop().unwrap(); - last_stop.place(0); - - for (index, stop) in snippets.places.into_iter().enumerate() { - stop.place(index + 1) - } - } + let snippet_edit = self.snippet_builder.take().map(|builder| { + SnippetEdit::new( + builder.places.into_iter().map(PlaceSnippet::finalize_position).collect_vec(), + ) + }); if let Some(tm) = self.mutated_tree.take() { - algo::diff(&tm.immutable, &tm.mutable_clone).into_text_edit(&mut self.edit) + algo::diff(&tm.immutable, &tm.mutable_clone).into_text_edit(&mut self.edit); } let edit = mem::take(&mut self.edit).finish(); - if !edit.is_empty() { - self.source_change.insert_source_edit(self.file_id, edit); + if !edit.is_empty() || snippet_edit.is_some() { + self.source_change.insert_source_and_snippet_edit(self.file_id, edit, snippet_edit); } } @@ -237,19 +329,31 @@ impl SourceChangeBuilder { /// Adds a tabstop snippet to place the cursor before `node` pub fn add_tabstop_before(&mut self, _cap: SnippetCap, node: impl AstNode) { assert!(node.syntax().parent().is_some()); - self.add_snippet(PlaceSnippet::Before(node.syntax().clone())); + self.add_snippet(PlaceSnippet::Before(node.syntax().clone().into())); } /// Adds a tabstop snippet to place the cursor after `node` pub fn add_tabstop_after(&mut self, _cap: SnippetCap, node: impl AstNode) { assert!(node.syntax().parent().is_some()); - self.add_snippet(PlaceSnippet::After(node.syntax().clone())); + self.add_snippet(PlaceSnippet::After(node.syntax().clone().into())); + } + + /// Adds a tabstop snippet to place the cursor before `token` + pub fn add_tabstop_before_token(&mut self, _cap: SnippetCap, token: SyntaxToken) { + assert!(token.parent().is_some()); + self.add_snippet(PlaceSnippet::Before(token.clone().into())); + } + + /// Adds a tabstop snippet to place the cursor after `token` + pub fn add_tabstop_after_token(&mut self, _cap: SnippetCap, token: SyntaxToken) { + assert!(token.parent().is_some()); + self.add_snippet(PlaceSnippet::After(token.clone().into())); } /// Adds a snippet to move the cursor selected over `node` pub fn add_placeholder_snippet(&mut self, _cap: SnippetCap, node: impl AstNode) { assert!(node.syntax().parent().is_some()); - self.add_snippet(PlaceSnippet::Over(node.syntax().clone())) + self.add_snippet(PlaceSnippet::Over(node.syntax().clone().into())) } fn add_snippet(&mut self, snippet: PlaceSnippet) { @@ -260,6 +364,16 @@ impl SourceChangeBuilder { pub fn finish(mut self) -> SourceChange { self.commit(); + + // Only one file can have snippet edits + stdx::never!(self + .source_change + .source_file_edits + .iter() + .filter(|(_, (_, snippet_edit))| snippet_edit.is_some()) + .at_most_one() + .is_err()); + mem::take(&mut self.source_change) } } @@ -281,65 +395,28 @@ impl From<FileSystemEdit> for SourceChange { } } +pub enum Snippet { + /// A tabstop snippet (e.g. `$0`). + Tabstop(TextSize), + /// A placeholder snippet (e.g. `${0:placeholder}`). + Placeholder(TextRange), +} + enum PlaceSnippet { - /// Place a tabstop before a node - Before(SyntaxNode), - /// Place a tabstop before a node - After(SyntaxNode), - /// Place a placeholder snippet in place of the node - Over(SyntaxNode), + /// Place a tabstop before an element + Before(SyntaxElement), + /// Place a tabstop before an element + After(SyntaxElement), + /// Place a placeholder snippet in place of the element + Over(SyntaxElement), } impl PlaceSnippet { - /// Places the snippet before or over a node with the given tab stop index - fn place(self, order: usize) { - // ensure the target node is still attached - match &self { - PlaceSnippet::Before(node) | PlaceSnippet::After(node) | PlaceSnippet::Over(node) => { - // node should still be in the tree, but if it isn't - // then it's okay to just ignore this place - if stdx::never!(node.parent().is_none()) { - return; - } - } - } - + fn finalize_position(self) -> Snippet { match self { - PlaceSnippet::Before(node) => { - ted::insert_raw(ted::Position::before(&node), Self::make_tab_stop(order)); - } - PlaceSnippet::After(node) => { - ted::insert_raw(ted::Position::after(&node), Self::make_tab_stop(order)); - } - PlaceSnippet::Over(node) => { - let position = ted::Position::before(&node); - node.detach(); - - let snippet = ast::SourceFile::parse(&format!("${{{order}:_}}")) - .syntax_node() - .clone_for_update(); - - let placeholder = - snippet.descendants().find_map(ast::UnderscoreExpr::cast).unwrap(); - ted::replace(placeholder.syntax(), node); - - ted::insert_raw(position, snippet); - } + PlaceSnippet::Before(it) => Snippet::Tabstop(it.text_range().start()), + PlaceSnippet::After(it) => Snippet::Tabstop(it.text_range().end()), + PlaceSnippet::Over(it) => Snippet::Placeholder(it.text_range()), } } - - fn make_tab_stop(order: usize) -> SyntaxNode { - let stop = ast::SourceFile::parse(&format!("stop!(${order})")) - .syntax_node() - .descendants() - .find_map(ast::TokenTree::cast) - .unwrap() - .syntax() - .clone_for_update(); - - stop.first_token().unwrap().detach(); - stop.last_token().unwrap().detach(); - - stop - } } |