// 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<Local>,
}
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<Vec<Completion>> {
// 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<Vec<Completion>> {
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<Vec<Completion>> {
// 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<Completion> {
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<str>]) -> Vec<Vec<Completion>> {
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<str>]) -> Vec<Vec<Completion>> {
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<str>]) -> Vec<Vec<Completion>> {
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<str>]) -> Vec<Vec<Completion>> {
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::<Vec<_>>();
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.<item_name>`
/// - Items will any other namespace will include an auto-import edit,
/// `import D.<item_name>;` 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<Vec<Completion>> {
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<str>], name_kind: NameKind) -> Vec<Vec<Completion>> {
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<Item = (NamespaceId, Availability)> {
let open_namespaces = self.namespaces_matching_qualifier(&[]).collect::<Vec<_>>();
let namespaces: FxHashSet<NamespaceId> = 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<str>],
) -> impl Iterator<Item = NamespaceId> {
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<Item = (NamespaceId, Availability)>,
name_kind: NameKind,
edit_range: Option<&TextEditRange>,
) -> Vec<Vec<Completion>> {
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<ItemInfo<'_>>,
edit_range: Option<&TextEditRange>,
) -> Vec<Vec<Completion>> {
items.sort_by(|a, b| a.item_id.package.cmp(&b.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<str>,
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<str>),
/// Not in scope at all. Needs an auto-import entry.
NeedImport(NamespaceId),
}microsoft/qdk
Publicmirrored from https://github.com/microsoft/qdkAvailable
source/language_service/src/completion/global_items.rs
519lines · modepreview