Improve import autocompletion

Now also works for functions, types, and packages
This commit is contained in:
Laurenz 2023-09-23 00:29:35 +02:00
parent 3dcbe859fb
commit 71a21b7ec1
3 changed files with 111 additions and 140 deletions

View File

@ -72,7 +72,6 @@ use std::mem;
use comemo::{Track, Tracked, TrackedMut, Validate};
use ecow::{EcoString, EcoVec};
use if_chain::if_chain;
use serde::{Deserialize, Serialize};
use unicode_segmentation::UnicodeSegmentation;
@ -256,7 +255,7 @@ pub struct Vm<'a> {
impl<'a> Vm<'a> {
/// Create a new virtual machine.
fn new(
pub fn new(
vt: Vt<'a>,
route: Tracked<'a, Route>,
file: Option<FileId>,
@ -1733,44 +1732,67 @@ impl Eval for ast::ForLoop<'_> {
}
}
/// Applies imports from `import` to the current scope.
fn apply_imports<V: IntoValue + Clone>(
imports: Option<ast::Imports>,
vm: &mut Vm,
source_value: V,
new_name: Option<&str>,
name: impl FnOnce(&V) -> EcoString,
scope: impl Fn(&V) -> &Scope,
) -> SourceResult<()> {
if let Some(new_name) = new_name {
// Renamed module => define it on the scope (possibly with further items).
if imports.is_none() {
// Avoid unneeded clone when there are no imported items.
vm.scopes.top.define(new_name, source_value);
return Ok(());
} else {
vm.scopes.top.define(new_name, source_value.clone());
impl Eval for ast::ModuleImport<'_> {
type Output = Value;
#[tracing::instrument(name = "ModuleImport::eval", skip_all)]
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
let source = self.source();
let source_span = source.span();
let mut source = source.eval(vm)?;
let new_name = self.new_name();
let imports = self.imports();
let name = match &source {
Value::Func(func) => {
func.scope()
.ok_or("cannot import from user-defined functions")
.at(source_span)?;
func.name().unwrap_or_default().into()
}
Value::Type(ty) => ty.short_name().into(),
other => {
let module = import(vm, other.clone(), source_span, true)?;
let name = module.name().clone();
source = Value::Module(module);
name
}
};
if let Some(new_name) = &new_name {
if let ast::Expr::Ident(ident) = self.source() {
if ident.as_str() == new_name.as_str() {
// Warn on `import x as x`
vm.vt.tracer.warn(warning!(
new_name.span(),
"unnecessary import rename to same name",
));
}
}
// Define renamed module on the scope.
vm.scopes.top.define(new_name.as_str(), source.clone());
}
let scope = source.scope().unwrap();
match imports {
None => {
// If the module were renamed and there were no imported items, we
// would have returned above. It is therefore safe to import the
// module with its original name here.
vm.scopes.top.define(name(&source_value), source_value);
// Only import here if there is no rename.
if new_name.is_none() {
vm.scopes.top.define(name, source);
}
}
Some(ast::Imports::Wildcard) => {
for (var, value) in scope(&source_value).iter() {
for (var, value) in scope.iter() {
vm.scopes.top.define(var.clone(), value.clone());
}
}
Some(ast::Imports::Items(items)) => {
let mut errors = vec![];
let scope = scope(&source_value);
for item in items.iter() {
let original_ident = item.original_name();
if let Some(value) = scope.get(&original_ident) {
// Warn on `import ...: x as x`
if let ast::ImportItem::Renamed(renamed_item) = &item {
if renamed_item.original_name().as_str()
== renamed_item.new_name().as_str()
@ -1781,6 +1803,7 @@ fn apply_imports<V: IntoValue + Clone>(
));
}
}
vm.define(item.bound_name(), value.clone());
} else {
errors.push(error!(original_ident.span(), "unresolved import"));
@ -1792,63 +1815,6 @@ fn apply_imports<V: IntoValue + Clone>(
}
}
Ok(())
}
impl Eval for ast::ModuleImport<'_> {
type Output = Value;
#[tracing::instrument(name = "ModuleImport::eval", skip_all)]
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
let span = self.source().span();
let source = self.source().eval(vm)?;
let new_name_ident = self.new_name();
let new_name = new_name_ident.map(ast::Ident::as_str);
if_chain! {
if let Some(new_name_ident) = new_name_ident;
if let ast::Expr::Ident(ident) = self.source();
if ident.as_str() == new_name_ident.as_str();
then {
// warn on `import x as x`
vm.vt.tracer.warn(warning!(
new_name_ident.span(),
"unnecessary import rename to same name",
));
}
}
if let Value::Func(func) = source {
let Some(scope) = func.scope() else {
bail!(span, "cannot import from user-defined functions");
};
apply_imports(
self.imports(),
vm,
func,
new_name,
|func| func.name().unwrap_or_default().into(),
|_| scope,
)?;
} else if let Value::Type(ty) = source {
apply_imports(
self.imports(),
vm,
ty,
new_name,
|ty| ty.short_name().into(),
|ty| ty.scope(),
)?;
} else {
let module = import(vm, source, span, true)?;
apply_imports(
self.imports(),
vm,
module,
new_name,
|module| module.name().clone(),
|module| module.scope(),
)?;
}
Ok(Value::None)
}
}
@ -1866,7 +1832,7 @@ impl Eval for ast::ModuleInclude<'_> {
}
/// Process an import of a module relative to the current location.
fn import(
pub(crate) fn import(
vm: &mut Vm,
source: Value,
span: Span,
@ -1875,13 +1841,10 @@ fn import(
let path = match source {
Value::Str(path) => path,
Value::Module(module) => return Ok(module),
v => {
if allow_scopes {
v if allow_scopes => {
bail!(span, "expected path, module, function, or type, found {}", v.ty())
} else {
bail!(span, "expected path or module, found {}", v.ty())
}
}
v => bail!(span, "expected path or module, found {}", v.ty()),
};
// Handle package and file imports.

View File

@ -2,9 +2,9 @@ use comemo::Track;
use ecow::{eco_vec, EcoString, EcoVec};
use crate::doc::Frame;
use crate::eval::{eval, Module, Route, Tracer, Value};
use crate::model::{Introspector, Label};
use crate::syntax::{ast, LinkedNode, Source, SyntaxKind};
use crate::eval::{Route, Scopes, Tracer, Value, Vm};
use crate::model::{DelayedErrors, Introspector, Label, Locator, Vt};
use crate::syntax::{ast, LinkedNode, Span, SyntaxKind};
use crate::World;
/// Try to determine a set of possible values for an expression.
@ -44,12 +44,30 @@ pub fn analyze_expr(world: &dyn World, node: &LinkedNode) -> EcoVec<Value> {
}
/// Try to load a module from the current source file.
pub fn analyze_import(world: &dyn World, source: &Source, path: &str) -> Option<Module> {
let route = Route::default();
pub fn analyze_import(world: &dyn World, source: &LinkedNode) -> Option<Value> {
let id = source.span().id()?;
let source = analyze_expr(world, source).into_iter().next()?;
if source.scope().is_some() {
return Some(source);
}
let mut locator = Locator::default();
let introspector = Introspector::default();
let mut delayed = DelayedErrors::new();
let mut tracer = Tracer::new();
let id = source.id().join(path);
let source = world.source(id).ok()?;
eval(world.track(), route.track(), tracer.track_mut(), &source).ok()
let vt = Vt {
world: world.track(),
introspector: introspector.track(),
locator: &mut locator,
delayed: delayed.track_mut(),
tracer: tracer.track_mut(),
};
let route = Route::default();
let mut vm = Vm::new(vt, route.track(), Some(id), Scopes::new(Some(world.library())));
crate::eval::import(&mut vm, source, Span::detached(), true)
.ok()
.map(Value::Module)
}
/// Find all labels and details for them.

View File

@ -457,10 +457,9 @@ fn complete_imports(ctx: &mut CompletionContext) -> bool {
if let Some(ast::Expr::Import(import)) = prev.get().cast();
if let Some(ast::Imports::Items(items)) = import.imports();
if let Some(source) = prev.children().find(|child| child.is::<ast::Expr>());
if let Some(value) = analyze_expr(ctx.world, &source).into_iter().next();
then {
ctx.from = ctx.cursor;
import_item_completions(ctx, items, &value);
import_item_completions(ctx, items, &source);
return true;
}
}
@ -475,10 +474,9 @@ fn complete_imports(ctx: &mut CompletionContext) -> bool {
if let Some(ast::Expr::Import(import)) = grand.get().cast();
if let Some(ast::Imports::Items(items)) = import.imports();
if let Some(source) = grand.children().find(|child| child.is::<ast::Expr>());
if let Some(value) = analyze_expr(ctx.world, &source).into_iter().next();
then {
ctx.from = ctx.leaf.offset();
import_item_completions(ctx, items, &value);
import_item_completions(ctx, items, &source);
return true;
}
}
@ -490,22 +488,16 @@ fn complete_imports(ctx: &mut CompletionContext) -> bool {
fn import_item_completions<'a>(
ctx: &mut CompletionContext<'a>,
existing: ast::ImportItems<'a>,
value: &Value,
source: &LinkedNode,
) {
let module = match value {
Value::Str(path) => match analyze_import(ctx.world, ctx.source, path) {
Some(module) => module,
None => return,
},
Value::Module(module) => module.clone(),
_ => return,
};
let Some(value) = analyze_import(ctx.world, source) else { return };
let Some(scope) = value.scope() else { return };
if existing.iter().next().is_none() {
ctx.snippet_completion("*", "*", "Import everything.");
}
for (name, value) in module.scope().iter() {
for (name, value) in scope.iter() {
if existing.iter().all(|item| item.original_name().as_str() != name) {
ctx.value_completion(Some(name.clone()), value, false, None);
}
@ -955,7 +947,6 @@ struct CompletionContext<'a> {
world: &'a (dyn World + 'static),
frames: &'a [Frame],
library: &'a Library,
source: &'a Source,
global: &'a Scope,
math: &'a Scope,
text: &'a str,
@ -985,7 +976,6 @@ impl<'a> CompletionContext<'a> {
world,
frames,
library,
source,
global: library.global.scope(),
math: library.math.scope(),
text,