// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. use super::{Completion, text_edits::TextEditRange}; use crate::{ compilation::Compilation, protocol::{CompletionItemKind, TextEdit}, }; use qsc::{ NamespaceId, PRELUDE, display::{CodeDisplay, Lookup}, hir::{CallableDecl, ItemId, ItemKind, ty::Udt}, resolve::{Local, NameKind}, }; use rustc_hash::FxHashSet; use std::{mem::take, rc::Rc}; /// Provides the globals that are visible or importable at the cursor offset. pub(super) struct Globals<'a> { compilation: &'a Compilation, locals: Vec, } impl<'a> Globals<'a> { pub fn init(offset: u32, compilation: &'a Compilation) -> Self { let global_scope = &compilation.user_unit().ast.globals; let mut locals = compilation.user_unit().ast.locals.get_all_at_offset(offset); // Always include the prelude as an imported namespace as well locals.extend(PRELUDE.iter().filter_map(|ns| { let namespace_id = global_scope.find_namespace(ns.iter().copied(), None); namespace_id.map(|namespace_id| Local::NamespaceImport(namespace_id, None)) })); Self { compilation, locals, } } /// Returns all terms, and any namespaces that may lead to terms, that /// are available at the current offset. /// /// If an item's name is not in scope, includes the item with any /// text edits (auto-imports, etc.) to bring it into scope. pub fn expr_names(&self, edit_range: &TextEditRange) -> Vec> { // include UDTs as well since they can be constructors let mut completions = self.items( NameKind::Term, false, // in_scope_only Some(edit_range), ); completions.push(self.namespaces()); completions } /// Returns all types, and any namespaces that may lead to types, that /// are available at the current offset. /// /// If an item's name is not in scope, includes the item with any /// text edits (auto-imports, etc.) to bring it into scope. pub fn type_names(&self, edit_range: &TextEditRange) -> Vec> { let mut completions = self.items( NameKind::Ty, false, // in_scope_only Some(edit_range), ); completions.push(self.namespaces()); completions } /// Returns all importables, and any namespaces that may lead to importables, /// that are available at the current offset. /// /// If an item's name is not in scope, the item is *not* included, /// as it wouldn't quite make sense, for an `import` completion to /// bring in text edits that auto-import other items. pub fn importable_names(&self) -> Vec> { // include UDTs as well since they can be constructors let mut completions = self.items( NameKind::Importable, true, // in_scope_only None, ); completions.push(self.namespaces()); completions } /// Returns all namespaces that are available at the current offset. pub fn namespaces(&self) -> Vec { self.namespaces_in(&[]).into_iter().flatten().collect() } /// Returns all terms, and any namespaces that may lead to terms, that /// match the given qualifier prefix. pub fn expr_names_in(&self, qualifier: &[Rc]) -> Vec> { let mut groups = self.items_in(qualifier, NameKind::Term); groups.extend(self.namespaces_in(qualifier)); groups } /// Returns all types, and any namespaces that may lead to types, that /// match the given qualifier prefix. pub fn type_names_in(&self, qualifier: &[Rc]) -> Vec> { let mut groups = self.items_in(qualifier, NameKind::Ty); groups.extend(self.namespaces_in(qualifier)); groups } /// Returns all importables, and any namespaces that may lead to importables, that /// match the given qualifier prefix. pub fn importable_names_in(&self, qualifier: &[Rc]) -> Vec> { let mut groups = self.items_in(qualifier, NameKind::Importable); groups.extend(self.namespaces_in(qualifier)); groups } /// Returns all namespaces that match the given qualifier prefix. pub fn namespaces_in(&self, qualifier: &[Rc]) -> Vec> { let namespaces_matching_qualifier = self.namespaces_matching_qualifier(qualifier); let mut children = namespaces_matching_qualifier .flat_map(|namespace| self.global_scope().namespace_children(namespace)) .collect::>(); children.sort(); children.dedup(); vec![ children .into_iter() .map(|name| Completion::new(name.to_string(), CompletionItemKind::Module)) .collect(), ] } /// Returns all item names that are available at the current offset, /// taking into account any imports that are in scope. /// /// If the item name is not in scope, and `in_scope_only` is false, /// includes the item with text edits (auto-imports, etc.) to bring the item into scope. /// /// e.g. if the following imports are in scope: /// `import A.*;` /// `import B as C;` /// /// Then, if `in_scope_only` is false: /// - Items from namespace `A` will be included without any edits /// - Items from `B` will be included as `C.` /// - Items will any other namespace will include an auto-import edit, /// `import D.;` to bring the item into scope. /// /// If `in_scope_only` is true, then only items from `A` will be included. fn items( &self, name_kind: NameKind, in_scope_only: bool, edit_range: Option<&TextEditRange>, ) -> Vec> { let namespaces = self.namespaces_to_search(name_kind, in_scope_only); self.items_in_namespaces(namespaces, name_kind, edit_range) } /// Returns all items that match the given qualifier prefix. /// /// The qualifier is resolved taking into account any imports that are in scope. /// e.g. `A.` will return items in: /// - namespace `A` /// - namespace `B` if an `import B as A;` is in scope /// - namespace `C.A` if an `import C.*` is in scope fn items_in(&'a self, qualifier: &[Rc], name_kind: NameKind) -> Vec> { let namespaces = self .namespaces_matching_qualifier(qualifier) .map(|namespace_id| (namespace_id, Availability::Qualified)); self.items_in_namespaces(namespaces, name_kind, None) } /// Gathers all namespaces and their availability information, i.e. whether /// they are already open in the current scope or need a text edit to bring them into scope. fn namespaces_to_search( &self, name_kind: NameKind, in_scope_only: bool, ) -> impl Iterator { let open_namespaces = self.namespaces_matching_qualifier(&[]).collect::>(); let namespaces: FxHashSet = if in_scope_only { // Only include already open namespaces open_namespaces.iter().copied().collect() } else { // Include all known namespaces self.global_scope() .table(name_kind) .iter() .map(|(namespace_id, _)| namespace_id) .collect() }; namespaces.into_iter().filter_map(move |namespace| { if open_namespaces.contains(&namespace) { // Namespace already in open in the current scope Some((namespace, Availability::InScope)) } else if let Some(alias) = self.locals.iter().find_map(|local| { if let Local::NamespaceImport(imported, Some(alias)) = local && namespace == *imported { return Some(alias.clone()); } None }) { // Namespace is imported with an alias Some((namespace, Availability::InAliasedNamespace(alias))) } else if self .global_scope() .format_namespace_name(namespace) .starts_with("Std.OpenQASM") { // Don't suggest auto-imports for OpenQASM namespaces None } else { // If there are no existing exact or glob imports of the item, // no open aliases for the namespace it's in, // and we are not in the same namespace as the item, // we need to add an import for it. Some((namespace, Availability::NeedImport(namespace))) } }) } /// Returns all namespaces that match the given qualifier prefix. /// The qualifier can be empty, in which case only top-level and open namespaces /// are returned. /// /// The qualifier is resolved taking into account any imports that are in scope. /// e.g.: /// /// namespace A.B {} /// namespace C.D {} /// namespace E.A.G {} /// namespace H { import C as A; import E.*; } /// /// `A.` inside of the namespace `H` will return `B`, `D` and `G`. fn namespaces_matching_qualifier( &self, qualifier: &[Rc], ) -> impl Iterator { let global_scope = &self.compilation.user_unit().ast.globals; self.locals .iter() .filter_map(|local| match local { Local::NamespaceImport(namespace_id, None) => global_scope .find_namespace(qualifier.iter().map(AsRef::as_ref), Some(*namespace_id)), Local::NamespaceImport(namespace_id, Some(alias)) if Some(alias) == qualifier.first() => { global_scope.find_namespace( qualifier[1..].iter().map(AsRef::as_ref), Some(*namespace_id), ) } _ => None, }) .chain(global_scope.find_namespace(qualifier.iter().map(AsRef::as_ref), None)) } /// Collects all matching items in the given namespaces and returns /// them as completions. fn items_in_namespaces( &self, namespaces: impl IntoIterator, name_kind: NameKind, edit_range: Option<&TextEditRange>, ) -> Vec> { let mut candidates = Vec::new(); let global_scope = self.global_scope(); let names_table = global_scope.table(name_kind); for (namespace_id, namespace_availability) in namespaces { if let Some(names) = names_table.get(namespace_id) { for (name, res) in names { if let Some(item_id) = res.item_id() { let (item, _, _) = self .compilation .resolve_item_relative_to_user_package(&item_id); let decl = match &item.kind { ItemKind::Callable(callable_decl) => { ItemDecl::Callable(callable_decl.as_ref()) } ItemKind::Ty(_ident, udt) => ItemDecl::Udt(udt), ItemKind::Export(..) | ItemKind::Namespace(..) => continue, }; let availability = if namespace_availability != Availability::Qualified && self.locals.iter().any(|local| { if let Local::Item(import_item_id, ..) = local && *import_item_id == item_id { return true; } false }) { // Item is available in the local scope through a direct import. Availability::Local } else { namespace_availability.clone() }; let item = ItemInfo { item_id, namespace_id, name: name.clone(), decl, availability, }; candidates.push(item); } } } } // Remove duplicate paths to the same item. The same item may be available // by multiple names by way of imports/exports. // e.g. // `namespace A { function B() : Unit {}; export B; export B as C; }` // `namespace D { export A.B; }` // `namespace E { open A; import A.B as X }` // Makes the same item available as `A.B`, `A.C`, `D.B`, `B` and `X` // when in `namespace E`. // We want to keep distinct names to the same item (`B`, `C`) but // not distinct paths to the same item (`A.B`, `D.B`). // Sort by availability so that direct imports and in-scope names // are preferred when de-duping. // When tied, prefer the smaller namespace ID, assuming that that's the "original" namespace, // but really that's just a heuristic when we don't have much else to go on. candidates.sort_by_key(|i| { ( i.item_id, i.name.clone(), i.availability.clone(), i.namespace_id, ) }); candidates.dedup_by_key(|i| (i.item_id, i.name.clone())); // Drop any items that were direct local imports, since the locals completion module // would have already included them in the ultimate completion list candidates.retain(|item| !matches!(item.availability, Availability::Local)); self.to_completions(candidates, edit_range) } /// Turns the final list of items into completion items, filling in /// details and including any text edits if requested. /// /// Items are sorted by package, with the user package /// coming first, with the dependencies in reverse order, so /// that the "closer" dependencies are listed first in the completion list. fn to_completions( &self, mut items: Vec>, edit_range: Option<&TextEditRange>, ) -> Vec> { items.sort_by_key(|a| a.item_id.package); items.reverse(); let mut groups = Vec::new(); let mut group = Vec::new(); let mut last_package_id = items.first().map(|item| item.item_id.package); for item in items { let curr_package_id = Some(item.item_id.package); if curr_package_id != last_package_id { // push the group to the groups if !group.is_empty() { groups.push(take(&mut group)); } } let completion = self.to_completion(&item, edit_range); group.push(completion); last_package_id = curr_package_id; } if !group.is_empty() { groups.push(take(&mut group)); } groups } /// Creates a completion list entry for the given item, including /// any text edits that would bring the item into scope, if requested. fn to_completion(&self, item: &ItemInfo<'a>, text_edits: Option<&TextEditRange>) -> Completion { let display = CodeDisplay { compilation: self.compilation, }; let (kind, display) = match &item.decl { ItemDecl::Callable(callable_decl) => ( CompletionItemKind::Function, display.hir_callable_decl(callable_decl).to_string(), ), ItemDecl::Udt(udt) => ( CompletionItemKind::Interface, display.hir_udt(udt).to_string(), ), }; // Deprioritize names starting with "__" in the completion list let mut sort_priority = u32::from(item.name.starts_with("__")); match &item.availability { Availability::Local | Availability::InScope | Availability::Qualified => { Completion::with_text_edits( item.name.to_string(), kind, Some(display), None, sort_priority, ) } Availability::NeedImport(namespace) => { // Deprioritize auto-import items sort_priority += 1; let text_edits = text_edits.expect( "a text edit range should have been provided if `in_scope_only` is false", ); // if there is no place to insert an import, then we can't add an import. let edits = text_edits.insert_import_at.as_ref().map(|range| { vec![TextEdit { new_text: format!( "import {}.{};{}", self.global_scope().format_namespace_name(*namespace), item.name, &text_edits.indent ), range: *range, }] }); Completion::with_text_edits( item.name.to_string(), kind, Some(display), edits, sort_priority, ) } Availability::InAliasedNamespace(prefix) => Completion::with_text_edits( format!("{}.{}", prefix, item.name), kind, Some(display), None, sort_priority, ), } } fn global_scope(&self) -> &qsc::resolve::GlobalScope { &self.compilation.user_unit().ast.globals } } /// The global item that backs a completion item. struct ItemInfo<'a> { item_id: ItemId, namespace_id: NamespaceId, /// Can be the original name for the decl, /// or the alias if this item is found through an import. name: Rc, decl: ItemDecl<'a>, availability: Availability, } /// The declaration, used to format completion item details. #[derive(Debug)] enum ItemDecl<'a> { Callable(&'a CallableDecl), Udt(&'a Udt), } /// How the item name is available in the current scope. /// /// The order here is important, as it's used when de-duping. /// We prefer the first kind of import if there are multiple /// paths leading to the same item. #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] enum Availability { /// Available through a direct local import. Local, /// In scope because of open namespaces. /// Safe to include without any additional edits. InScope, /// In scope because of preceding qualifier. /// Safe to include without any additional edits. Qualified, /// In a namespace that is open with a local alias. /// The name should be prefixed with the namespace alias. /// /// e.g. `Foo` will appear as `Bar.Foo` if it's under an open /// namespace that is aliased as `Bar`. InAliasedNamespace(Rc), /// Not in scope at all. Needs an auto-import entry. NeedImport(NamespaceId), }