Skip to content

Commit

Permalink
feat: LSP auto-import completion (#5741)
Browse files Browse the repository at this point in the history
# Description

## Problem

Part of #1577

## Summary

I said I wasn't going to send any more LSP features, but I had this one
in the back of my head since I started working on autocompletion. I had
the idea of how to do it in my head, but I wanted to first work on the
easier stuff (regular autocompletion). It turns out auto-import wasn't
that hard to implement after all!


![lsp-autoimport](https://github.com/user-attachments/assets/9d2268fb-5caf-4b42-9bb3-b01f6ca40a9b)


![lsp-autoimport-nested](https://github.com/user-attachments/assets/ac4cd18a-d87d-4311-82d0-61f6d5a8dd19)

## Additional Context

In this PR every new imported item is added as a full path. Ideally,
like in Rust Analyzer, we'd group common use prefixes, add to existing
ones, etc. We can do that! But maybe it could be part of a follow-up PR,
or maybe we could do it much later. Or maybe `nargo fmt` could handle
this, so the auto-import doesn't do it, but once you save the file it's
done. That's why I didn't spend time on that in this PR.

There's another thing I noticed while working on this PR: Rust Analyzer
will offer matches that match **any** part of the name, not just the
beginning. So, in Noir, if you type `merkle` it should probably offer
`compute_merkle_root` as an auto-import completion, which is nice
because maybe you are looking for stuff related to "merkle" but don't
know the full name... but I didn't want to introduce that change in this
PR (it also works for every other autocompletion). But, as always, this
could be done later on.

## Documentation

Check one:
- [x] No documentation needed.
- [ ] Documentation included in this PR.
- [ ] **[For Experimental Features]** Documentation to be submitted in a
separate PR.

# PR Checklist\*

- [x] I have tested the changes locally.
- [x] I have formatted the changes with [Prettier](https://prettier.io/)
and/or `cargo fmt` on default settings.
  • Loading branch information
asterite authored Aug 16, 2024
1 parent c2a1a87 commit cdbb940
Show file tree
Hide file tree
Showing 11 changed files with 648 additions and 55 deletions.
16 changes: 15 additions & 1 deletion compiler/noirc_frontend/src/ast/expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use crate::ast::{
use crate::hir::def_collector::errors::DefCollectorErrorKind;
use crate::macros_api::StructId;
use crate::node_interner::{ExprId, QuotedTypeId};
use crate::token::{Attributes, Token, Tokens};
use crate::token::{Attributes, FunctionAttribute, Token, Tokens};
use crate::{Kind, Type};
use acvm::{acir::AcirField, FieldElement};
use iter_extended::vecmap;
Expand Down Expand Up @@ -478,6 +478,20 @@ pub struct FunctionDefinition {
pub return_visibility: Visibility,
}

impl FunctionDefinition {
pub fn is_private(&self) -> bool {
self.visibility == ItemVisibility::Private
}

pub fn is_test(&self) -> bool {
if let Some(attribute) = &self.attributes.function {
matches!(attribute, FunctionAttribute::Test(..))
} else {
false
}
}
}

#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Param {
pub visibility: Visibility,
Expand Down
8 changes: 8 additions & 0 deletions compiler/noirc_frontend/src/elaborator/comptime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,14 @@ impl<'context> Elaborator<'context> {
let id = self.interner.push_empty_fn();
let module = self.module_id();
self.interner.push_function(id, &function.def, module, location);

if self.interner.is_in_lsp_mode()
&& !function.def.is_test()
&& !function.def.is_private()
{
self.interner.register_function(id, &function.def);
}

let functions = vec![(self.local_module, id, function)];
generated_items.functions.push(UnresolvedFunctions {
file_id: self.file,
Expand Down
11 changes: 9 additions & 2 deletions compiler/noirc_frontend/src/elaborator/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1297,6 +1297,12 @@ impl<'context> Elaborator<'context> {
self.current_item = Some(DependencyId::Global(global_id));
let let_stmt = global.stmt_def;

let name = if self.interner.is_in_lsp_mode() {
Some(let_stmt.pattern.name_ident().to_string())
} else {
None
};

if !self.in_contract()
&& let_stmt.attributes.iter().any(|attr| matches!(attr, SecondaryAttribute::Abi(_)))
{
Expand All @@ -1319,8 +1325,9 @@ impl<'context> Elaborator<'context> {
self.elaborate_comptime_global(global_id);
}

self.interner
.add_definition_location(ReferenceId::Global(global_id), Some(self.module_id()));
if let Some(name) = name {
self.interner.register_global(global_id, name, self.module_id());
}

self.local_module = old_module;
self.file = old_file;
Expand Down
48 changes: 30 additions & 18 deletions compiler/noirc_frontend/src/hir/def_collector/dc_mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use crate::ast::{
TypeImpl,
};
use crate::macros_api::NodeInterner;
use crate::node_interner::{ModuleAttributes, ReferenceId};
use crate::node_interner::ModuleAttributes;
use crate::{
graph::CrateId,
hir::def_collector::dc_crate::{UnresolvedStruct, UnresolvedTrait},
Expand Down Expand Up @@ -232,6 +232,13 @@ impl<'a> ModCollector<'a> {
let location = Location::new(function.span(), self.file_id);
context.def_interner.push_function(func_id, &function.def, module, location);

if context.def_interner.is_in_lsp_mode()
&& !function.def.is_test()
&& !function.def.is_private()
{
context.def_interner.register_function(func_id, &function.def);
}

// Now link this func_id to a crate level map with the noir function and the module id
// Encountering a NoirFunction, we retrieve it's module_data to get the namespace
// Once we have lowered it to a HirFunction, we retrieve it's Id from the DefInterner
Expand Down Expand Up @@ -307,8 +314,8 @@ impl<'a> ModCollector<'a> {
};

// Add the struct to scope so its path can be looked up later
let result =
self.def_collector.def_map.modules[self.module_id.0].declare_struct(name, id);
let result = self.def_collector.def_map.modules[self.module_id.0]
.declare_struct(name.clone(), id);

if let Err((first_def, second_def)) = result {
let error = DefCollectorErrorKind::Duplicate {
Expand All @@ -322,10 +329,10 @@ impl<'a> ModCollector<'a> {
// And store the TypeId -> StructType mapping somewhere it is reachable
self.def_collector.items.types.insert(id, unresolved);

context.def_interner.add_definition_location(
ReferenceId::Struct(id),
Some(ModuleId { krate, local_id: self.module_id }),
);
if context.def_interner.is_in_lsp_mode() {
let parent_module_id = ModuleId { krate, local_id: self.module_id };
context.def_interner.register_struct(id, name.to_string(), parent_module_id);
}
}
definition_errors
}
Expand Down Expand Up @@ -383,7 +390,7 @@ impl<'a> ModCollector<'a> {

// Add the type alias to scope so its path can be looked up later
let result = self.def_collector.def_map.modules[self.module_id.0]
.declare_type_alias(name, type_alias_id);
.declare_type_alias(name.clone(), type_alias_id);

if let Err((first_def, second_def)) = result {
let err = DefCollectorErrorKind::Duplicate {
Expand All @@ -396,10 +403,11 @@ impl<'a> ModCollector<'a> {

self.def_collector.items.type_aliases.insert(type_alias_id, unresolved);

context.def_interner.add_definition_location(
ReferenceId::Alias(type_alias_id),
Some(ModuleId { krate, local_id: self.module_id }),
);
if context.def_interner.is_in_lsp_mode() {
let parent_module_id = ModuleId { krate, local_id: self.module_id };
let name = name.to_string();
context.def_interner.register_type_alias(type_alias_id, name, parent_module_id);
}
}
errors
}
Expand Down Expand Up @@ -432,8 +440,8 @@ impl<'a> ModCollector<'a> {
};

// Add the trait to scope so its path can be looked up later
let result =
self.def_collector.def_map.modules[self.module_id.0].declare_trait(name, trait_id);
let result = self.def_collector.def_map.modules[self.module_id.0]
.declare_trait(name.clone(), trait_id);

if let Err((first_def, second_def)) = result {
let error = DefCollectorErrorKind::Duplicate {
Expand Down Expand Up @@ -567,10 +575,10 @@ impl<'a> ModCollector<'a> {
};
context.def_interner.push_empty_trait(trait_id, &unresolved, resolved_generics);

context.def_interner.add_definition_location(
ReferenceId::Trait(trait_id),
Some(ModuleId { krate, local_id: self.module_id }),
);
if context.def_interner.is_in_lsp_mode() {
let parent_module_id = ModuleId { krate, local_id: self.module_id };
context.def_interner.register_trait(trait_id, name.to_string(), parent_module_id);
}

self.def_collector.items.traits.insert(trait_id, unresolved);
}
Expand Down Expand Up @@ -766,6 +774,10 @@ impl<'a> ModCollector<'a> {
parent: self.module_id,
},
);

if context.def_interner.is_in_lsp_mode() {
context.def_interner.register_module(mod_id, mod_name.0.contents.clone());
}
}

Ok(mod_id)
Expand Down
2 changes: 1 addition & 1 deletion compiler/noirc_frontend/src/hir/def_map/module_def.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use crate::node_interner::{FuncId, GlobalId, StructId, TraitId, TypeAliasId};
use super::ModuleId;

/// A generic ID that references either a module, function, type, interface or global
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum ModuleDefId {
ModuleId(ModuleId),
FunctionId(FuncId),
Expand Down
76 changes: 74 additions & 2 deletions compiler/noirc_frontend/src/locations.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
use fm::FileId;
use noirc_errors::Location;
use rangemap::RangeMap;
use rustc_hash::FxHashMap;
use rustc_hash::FxHashMap as HashMap;

use crate::{
ast::{FunctionDefinition, ItemVisibility},
hir::def_map::{ModuleDefId, ModuleId},
macros_api::{NodeInterner, StructId},
node_interner::{DefinitionId, FuncId, GlobalId, ReferenceId, TraitId, TypeAliasId},
Expand All @@ -12,7 +13,7 @@ use petgraph::prelude::NodeIndex as PetGraphIndex;

#[derive(Debug, Default)]
pub(crate) struct LocationIndices {
map_file_to_range: FxHashMap<FileId, RangeMap<u32, PetGraphIndex>>,
map_file_to_range: HashMap<FileId, RangeMap<u32, PetGraphIndex>>,
}

impl LocationIndices {
Expand Down Expand Up @@ -275,4 +276,75 @@ impl NodeInterner {
.neighbors_directed(reference_index, petgraph::Direction::Outgoing)
.next()
}

pub(crate) fn register_module(&mut self, id: ModuleId, name: String) {
self.register_name_for_auto_import(name, ModuleDefId::ModuleId(id), ItemVisibility::Public);
}

pub(crate) fn register_global(
&mut self,
id: GlobalId,
name: String,
parent_module_id: ModuleId,
) {
self.add_definition_location(ReferenceId::Global(id), Some(parent_module_id));

let visibility = ItemVisibility::Public;
self.register_name_for_auto_import(name, ModuleDefId::GlobalId(id), visibility);
}

pub(crate) fn register_struct(
&mut self,
id: StructId,
name: String,
parent_module_id: ModuleId,
) {
self.add_definition_location(ReferenceId::Struct(id), Some(parent_module_id));

let visibility = ItemVisibility::Public;
self.register_name_for_auto_import(name, ModuleDefId::TypeId(id), visibility);
}

pub(crate) fn register_trait(&mut self, id: TraitId, name: String, parent_module_id: ModuleId) {
self.add_definition_location(ReferenceId::Trait(id), Some(parent_module_id));

self.register_name_for_auto_import(name, ModuleDefId::TraitId(id), ItemVisibility::Public);
}

pub(crate) fn register_type_alias(
&mut self,
id: TypeAliasId,
name: String,
parent_module_id: ModuleId,
) {
self.add_definition_location(ReferenceId::Alias(id), Some(parent_module_id));

let visibility = ItemVisibility::Public;
self.register_name_for_auto_import(name, ModuleDefId::TypeAliasId(id), visibility);
}

pub(crate) fn register_function(&mut self, id: FuncId, func_def: &FunctionDefinition) {
self.register_name_for_auto_import(
func_def.name.0.contents.clone(),
ModuleDefId::FunctionId(id),
func_def.visibility,
);
}

fn register_name_for_auto_import(
&mut self,
name: String,
module_def_id: ModuleDefId,
visibility: ItemVisibility,
) {
if !self.lsp_mode {
return;
}

self.auto_import_names.entry(name).or_default().push((module_def_id, visibility));
}

pub fn get_auto_import_names(&self) -> &HashMap<String, Vec<(ModuleDefId, ItemVisibility)>> {
&self.auto_import_names
}
}
8 changes: 8 additions & 0 deletions compiler/noirc_frontend/src/node_interner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use crate::hir::comptime;
use crate::hir::def_collector::dc_crate::CompilationError;
use crate::hir::def_collector::dc_crate::{UnresolvedStruct, UnresolvedTrait, UnresolvedTypeAlias};
use crate::hir::def_map::{LocalModuleId, ModuleId};
use crate::macros_api::ModuleDefId;
use crate::macros_api::UnaryOp;
use crate::QuotedType;

Expand Down Expand Up @@ -231,6 +232,11 @@ pub struct NodeInterner {
// (ReferenceId::Reference and ReferenceId::Local aren't included here)
pub(crate) reference_modules: HashMap<ReferenceId, ModuleId>,

// All names (and their definitions) that can be offered for auto_import.
// These include top-level functions, global variables and types, but excludes
// impl and trait-impl methods.
pub(crate) auto_import_names: HashMap<String, Vec<(ModuleDefId, ItemVisibility)>>,

/// Each value currently in scope in the comptime interpreter.
/// Each element of the Vec represents a scope with every scope together making
/// up all currently visible definitions. The first scope is always the global scope.
Expand Down Expand Up @@ -602,6 +608,7 @@ impl Default for NodeInterner {
reference_graph: petgraph::graph::DiGraph::new(),
reference_graph_indices: HashMap::default(),
reference_modules: HashMap::default(),
auto_import_names: HashMap::default(),
comptime_scopes: vec![HashMap::default()],
}
}
Expand Down Expand Up @@ -910,6 +917,7 @@ impl NodeInterner {
// This needs to be done after pushing the definition since it will reference the
// location that was stored
self.add_definition_location(ReferenceId::Function(id), Some(module));

definition_id
}

Expand Down
Loading

0 comments on commit cdbb940

Please sign in to comment.