diff --git a/src/file.rs b/src/file.rs index 1ee4cf09c..8aaa746be 100644 --- a/src/file.rs +++ b/src/file.rs @@ -290,6 +290,8 @@ impl PackageManifest { } /// The `package` key in the manifest. +/// +/// More fields are specified, but they are not relevant to the compiler. #[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] pub struct PackageInfo { /// The name of the package within its namespace. diff --git a/src/ide/complete.rs b/src/ide/complete.rs index f9f19bd69..16cba1bc6 100644 --- a/src/ide/complete.rs +++ b/src/ide/complete.rs @@ -7,7 +7,7 @@ use unscanny::Scanner; use super::analyze::analyze_labels; use super::{analyze_expr, analyze_import, plain_docs_sentence, summarize_font_family}; use crate::doc::Frame; -use crate::eval::{methods_on, CastInfo, Library, Scope, Value}; +use crate::eval::{format_str, methods_on, CastInfo, Library, Scope, Value}; use crate::syntax::{ ast, is_id_continue, is_id_start, is_ident, LinkedNode, Source, SyntaxKind, }; @@ -402,6 +402,22 @@ fn field_access_completions(ctx: &mut CompletionContext, value: &Value) { /// Complete imports. fn complete_imports(ctx: &mut CompletionContext) -> bool { + // In an import path for a package: + // "#import "@|", + if_chain! { + if matches!( + ctx.leaf.parent_kind(), + Some(SyntaxKind::ModuleImport | SyntaxKind::ModuleInclude) + ); + if let Some(ast::Expr::Str(str)) = ctx.leaf.cast(); + if str.get().starts_with('@'); + then { + ctx.from = ctx.leaf.offset(); + ctx.package_completions(); + return true; + } + } + // Behind an import list: // "#import "path.typ": |", // "#import "path.typ": a, b, |". @@ -413,7 +429,7 @@ fn complete_imports(ctx: &mut CompletionContext) -> bool { if let Some(value) = analyze_expr(ctx.world, &source).into_iter().next(); then { ctx.from = ctx.cursor; - import_completions(ctx, &items, &value); + import_item_completions(ctx, &items, &value); return true; } } @@ -431,7 +447,7 @@ fn complete_imports(ctx: &mut CompletionContext) -> bool { if let Some(value) = analyze_expr(ctx.world, &source).into_iter().next(); then { ctx.from = ctx.leaf.offset(); - import_completions(ctx, &items, &value); + import_item_completions(ctx, &items, &value); return true; } } @@ -440,7 +456,7 @@ fn complete_imports(ctx: &mut CompletionContext) -> bool { } /// Add completions for all exports of a module. -fn import_completions( +fn import_item_completions( ctx: &mut CompletionContext, existing: &[ast::Ident], value: &Value, @@ -839,14 +855,26 @@ fn code_completions(ctx: &mut CompletionContext, hashtag: bool) { ); ctx.snippet_completion( - "import", - "import \"${file.typ}\": ${items}", + "import (file)", + "import \"${file}.typ\": ${items}", "Imports variables from another file.", ); ctx.snippet_completion( - "include", - "include \"${file.typ}\"", + "import (package)", + "import \"@${}\": ${items}", + "Imports variables from another file.", + ); + + ctx.snippet_completion( + "include (file)", + "include \"${file}.typ\"", + "Includes content from another file.", + ); + + ctx.snippet_completion( + "include (package)", + "include \"@${}\"", "Includes content from another file.", ); @@ -960,6 +988,18 @@ impl<'a> CompletionContext<'a> { } } + /// Add completions for all available packages. + fn package_completions(&mut self) { + for (package, description) in self.world.packages() { + self.value_completion( + None, + &Value::Str(format_str!("{package}")), + false, + description.as_deref(), + ); + } + } + /// Add completions for raw block tags. fn raw_completions(&mut self) { for (name, mut tags) in (self.library.items.raw_languages)() { @@ -1014,7 +1054,10 @@ impl<'a> CompletionContext<'a> { let detail = docs.map(Into::into).or_else(|| match value { Value::Symbol(_) => None, Value::Func(func) => func.info().map(|info| plain_docs_sentence(info.docs)), - v => Some(v.repr().into()), + v => { + let repr = v.repr(); + (repr.as_str() != label).then(|| repr.into()) + } }); if parens && matches!(value, Value::Func(_)) { diff --git a/src/lib.rs b/src/lib.rs index 37c74c09f..8b3d1d3d5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -54,11 +54,12 @@ pub mod model; pub mod syntax; use comemo::{Prehashed, Track, TrackedMut}; +use ecow::EcoString; use crate::diag::{FileResult, SourceResult}; use crate::doc::Document; use crate::eval::{Datetime, Library, Route, Tracer}; -use crate::file::FileId; +use crate::file::{FileId, PackageSpec}; use crate::font::{Font, FontBook}; use crate::syntax::Source; use crate::util::Bytes; @@ -112,6 +113,11 @@ pub trait World { fn main(&self) -> Source; /// Try to access the specified source file. + /// + /// The returned `Source` file's [id](Source::id) does not have to match the + /// given `id`. Due to symlinks, two different file id's can point to the + /// same on-disk file. Implementors can deduplicate and return the same + /// `Source` if they want to, but do not have to. fn source(&self, id: FileId) -> FileResult; /// Try to access the specified file. @@ -124,5 +130,18 @@ pub trait World { /// /// If no offset is specified, the local date should be chosen. Otherwise, /// the UTC date should be chosen with the corresponding offset in hours. + /// + /// If this function returns `None`, Typst's `datetime` function will + /// return an error. fn today(&self, offset: Option) -> Option; + + /// A list of all available packages and optionally descriptions for them. + /// + /// This function is optional to implement. It enhances the user experience + /// by enabling autocompletion for packages. Details about packages from the + /// `@preview` namespace are available from + /// `https://packages.typst.org/preview/index.json`. + fn packages(&self) -> &[(PackageSpec, Option)] { + &[] + } } diff --git a/src/syntax/source.rs b/src/syntax/source.rs index 6eb6fd5d2..2f3e41446 100644 --- a/src/syntax/source.rs +++ b/src/syntax/source.rs @@ -34,9 +34,6 @@ struct Repr { impl Source { /// Create a new source file. - /// - /// The path must be canonical, so that the same source file has the same - /// id even if accessed through different paths. #[tracing::instrument(skip_all)] pub fn new(id: FileId, text: String) -> Self { let mut root = parse(&text);