From 83b28e99aed4a168238148a56a5fe2e0bc91d01f Mon Sep 17 00:00:00 2001 From: Laurenz Date: Sun, 29 Jan 2023 01:07:20 +0100 Subject: [PATCH] Import completions --- src/ide/analyze.rs | 33 +++++++++++++++++--- src/ide/complete.rs | 74 +++++++++++++++++++++++++++++++++++++++++++-- src/ide/tooltip.rs | 4 +-- src/syntax/ast.rs | 2 +- 4 files changed, 102 insertions(+), 11 deletions(-) diff --git a/src/ide/analyze.rs b/src/ide/analyze.rs index cfc77f380..357e98f8a 100644 --- a/src/ide/analyze.rs +++ b/src/ide/analyze.rs @@ -1,11 +1,14 @@ +use std::path::PathBuf; + use comemo::Track; -use crate::model::{eval, Route, Tracer, Value}; -use crate::syntax::{ast, LinkedNode, SyntaxKind}; +use crate::model::{eval, Module, Route, Tracer, Value}; +use crate::syntax::{ast, LinkedNode, Source, SyntaxKind}; +use crate::util::PathExt; use crate::World; /// Try to determine a set of possible values for an expression. -pub fn analyze(world: &(dyn World + 'static), node: &LinkedNode) -> Vec { +pub fn analyze_expr(world: &(dyn World + 'static), node: &LinkedNode) -> Vec { match node.cast::() { Some(ast::Expr::None(_)) => vec![Value::None], Some(ast::Expr::Auto(_)) => vec![Value::Auto], @@ -17,7 +20,7 @@ pub fn analyze(world: &(dyn World + 'static), node: &LinkedNode) -> Vec { Some(ast::Expr::FieldAccess(access)) => { let Some(child) = node.children().next() else { return vec![] }; - analyze(world, &child) + analyze_expr(world, &child) .into_iter() .filter_map(|target| target.field(&access.field()).ok()) .collect() @@ -26,7 +29,7 @@ pub fn analyze(world: &(dyn World + 'static), node: &LinkedNode) -> Vec { Some(_) => { if let Some(parent) = node.parent() { if parent.kind() == SyntaxKind::FieldAccess && node.index() > 0 { - return analyze(world, parent); + return analyze_expr(world, parent); } } @@ -41,3 +44,23 @@ pub fn analyze(world: &(dyn World + 'static), node: &LinkedNode) -> Vec { _ => vec![], } } + +/// Try to load a module from the current source file. +pub fn analyze_import( + world: &(dyn World + 'static), + source: &Source, + path: &str, +) -> Option { + let full: PathBuf = if let Some(path) = path.strip_prefix('/') { + world.root().join(path).normalize() + } else if let Some(dir) = source.path().parent() { + dir.join(path).normalize() + } else { + path.into() + }; + let route = Route::default(); + let mut tracer = Tracer::default(); + let id = world.resolve(&full).ok()?; + let source = world.source(id); + eval(world.track(), route.track(), tracer.track_mut(), source).ok() +} diff --git a/src/ide/complete.rs b/src/ide/complete.rs index 31e0bdddd..5c64d8016 100644 --- a/src/ide/complete.rs +++ b/src/ide/complete.rs @@ -2,7 +2,7 @@ use std::collections::{BTreeSet, HashSet}; use if_chain::if_chain; -use super::{analyze, plain_docs_sentence, summarize_font_family}; +use super::{analyze_expr, analyze_import, plain_docs_sentence, summarize_font_family}; use crate::model::{methods_on, CastInfo, Scope, Value}; use crate::syntax::{ast, LinkedNode, Source, SyntaxKind}; use crate::util::{format_eco, EcoString}; @@ -24,6 +24,7 @@ pub fn autocomplete( let mut ctx = CompletionContext::new(world, source, cursor, explicit)?; let _ = complete_field_accesses(&mut ctx) + || complete_imports(&mut ctx) || complete_rules(&mut ctx) || complete_params(&mut ctx) || complete_markup(&mut ctx) @@ -279,7 +280,7 @@ fn complete_field_accesses(ctx: &mut CompletionContext) -> bool { if ctx.leaf.range().end == ctx.cursor; if let Some(prev) = ctx.leaf.prev_sibling(); if prev.is::(); - if let Some(value) = analyze(ctx.world, &prev).into_iter().next(); + if let Some(value) = analyze_expr(ctx.world, &prev).into_iter().next(); then { ctx.from = ctx.cursor; field_access_completions(ctx, &value); @@ -294,7 +295,7 @@ fn complete_field_accesses(ctx: &mut CompletionContext) -> bool { if prev.kind() == SyntaxKind::Dot; if let Some(prev_prev) = prev.prev_sibling(); if prev_prev.is::(); - if let Some(value) = analyze(ctx.world, &prev_prev).into_iter().next(); + if let Some(value) = analyze_expr(ctx.world, &prev_prev).into_iter().next(); then { ctx.from = ctx.leaf.offset(); field_access_completions(ctx, &value); @@ -347,6 +348,71 @@ fn field_access_completions(ctx: &mut CompletionContext, value: &Value) { } } +/// Complete imports. +fn complete_imports(ctx: &mut CompletionContext) -> bool { + // Behind an import list: + // "#import "path.typ": |", + // "#import "path.typ": a, b, |". + if_chain! { + if let Some(prev) = ctx.leaf.prev_sibling(); + if let Some(ast::Expr::Import(import)) = prev.cast(); + if let Some(ast::Imports::Items(items)) = import.imports(); + if let Some(source) = prev.children().find(|child| child.is::()); + if let Some(value) = analyze_expr(ctx.world, &source).into_iter().next(); + then { + ctx.from = ctx.cursor; + import_completions(ctx, &items, &value); + return true; + } + } + + // Behind a half-started identifier in an import list: + // "#import "path.typ": thi|", + if_chain! { + if ctx.leaf.kind() == SyntaxKind::Ident; + if let Some(parent) = ctx.leaf.parent(); + if parent.kind() == SyntaxKind::ImportItems; + if let Some(grand) = parent.parent(); + if let Some(ast::Expr::Import(import)) = grand.cast(); + if let Some(ast::Imports::Items(items)) = import.imports(); + if let Some(source) = grand.children().find(|child| child.is::()); + if let Some(value) = analyze_expr(ctx.world, &source).into_iter().next(); + then { + ctx.from = ctx.leaf.offset(); + import_completions(ctx, &items, &value); + return true; + } + } + + false +} + +/// Add completions for all exports of a module. +fn import_completions( + ctx: &mut CompletionContext, + existing: &[ast::Ident], + value: &Value, +) { + 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, + }; + + if existing.is_empty() { + ctx.snippet_completion("*", "*", "Import everything."); + } + + for (name, value) in module.scope().iter() { + if existing.iter().all(|ident| ident.as_str() != name.as_str()) { + ctx.value_completion(Some(name.clone()), value, None); + } + } +} + /// Complete set and show rules. fn complete_rules(ctx: &mut CompletionContext) -> bool { // We don't want to complete directly behind the keyword. @@ -752,6 +818,7 @@ fn code_completions(ctx: &mut CompletionContext, hashtag: bool) { /// Context for autocompletion. struct CompletionContext<'a> { world: &'a (dyn World + 'static), + source: &'a Source, global: &'a Scope, math: &'a Scope, before: &'a str, @@ -775,6 +842,7 @@ impl<'a> CompletionContext<'a> { let leaf = LinkedNode::new(source.root()).leaf_at(cursor)?; Some(Self { world, + source, global: &world.library().global.scope(), math: &world.library().math.scope(), before: &text[..cursor], diff --git a/src/ide/tooltip.rs b/src/ide/tooltip.rs index 26fa3625c..566eb5a3b 100644 --- a/src/ide/tooltip.rs +++ b/src/ide/tooltip.rs @@ -1,7 +1,7 @@ use if_chain::if_chain; use unicode_segmentation::UnicodeSegmentation; -use super::{analyze, plain_docs_sentence, summarize_font_family}; +use super::{analyze_expr, plain_docs_sentence, summarize_font_family}; use crate::geom::{round_2, Length, Numeric}; use crate::model::{CastInfo, Tracer, Value}; use crate::syntax::ast; @@ -42,7 +42,7 @@ fn expr_tooltip(world: &(dyn World + 'static), leaf: &LinkedNode) -> Option Expr { - self.0.cast_last_match().unwrap_or_default() + self.0.cast_first_match().unwrap_or_default() } /// The items to be imported.