mirror of
https://github.com/typst/typst
synced 2025-05-14 04:56:26 +08:00
Autocomplete file paths
This commit is contained in:
parent
a5a4b0b72f
commit
c0dfe4aab7
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -2780,6 +2780,7 @@ dependencies = [
|
|||||||
"ecow",
|
"ecow",
|
||||||
"if_chain",
|
"if_chain",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
"pathdiff",
|
||||||
"serde",
|
"serde",
|
||||||
"typst",
|
"typst",
|
||||||
"typst-assets",
|
"typst-assets",
|
||||||
|
@ -18,6 +18,7 @@ typst-eval = { workspace = true }
|
|||||||
comemo = { workspace = true }
|
comemo = { workspace = true }
|
||||||
ecow = { workspace = true }
|
ecow = { workspace = true }
|
||||||
if_chain = { workspace = true }
|
if_chain = { workspace = true }
|
||||||
|
pathdiff = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
unscanny = { workspace = true }
|
unscanny = { workspace = true }
|
||||||
|
|
||||||
|
@ -1,17 +1,19 @@
|
|||||||
use std::cmp::Reverse;
|
use std::cmp::Reverse;
|
||||||
use std::collections::{BTreeSet, HashSet};
|
use std::collections::{BTreeSet, HashSet};
|
||||||
|
use std::ffi::OsStr;
|
||||||
|
|
||||||
use ecow::{eco_format, EcoString};
|
use ecow::{eco_format, EcoString};
|
||||||
use if_chain::if_chain;
|
use if_chain::if_chain;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use typst::foundations::{
|
use typst::foundations::{
|
||||||
fields_on, format_str, repr, AutoValue, CastInfo, Func, Label, NoneValue, Repr,
|
fields_on, format_str, repr, AutoValue, CastInfo, Func, Label, NoneValue, ParamInfo,
|
||||||
Scope, StyleChain, Styles, Type, Value,
|
Repr, Scope, StyleChain, Styles, Type, Value,
|
||||||
};
|
};
|
||||||
use typst::model::Document;
|
use typst::model::Document;
|
||||||
use typst::syntax::ast::AstNode;
|
use typst::syntax::ast::AstNode;
|
||||||
use typst::syntax::{
|
use typst::syntax::{
|
||||||
ast, is_id_continue, is_id_start, is_ident, LinkedNode, Side, Source, SyntaxKind,
|
ast, is_id_continue, is_id_start, is_ident, FileId, LinkedNode, Side, Source,
|
||||||
|
SyntaxKind,
|
||||||
};
|
};
|
||||||
use typst::text::RawElem;
|
use typst::text::RawElem;
|
||||||
use typst::visualize::Color;
|
use typst::visualize::Color;
|
||||||
@ -480,8 +482,8 @@ fn complete_open_labels(ctx: &mut CompletionContext) -> bool {
|
|||||||
|
|
||||||
/// Complete imports.
|
/// Complete imports.
|
||||||
fn complete_imports(ctx: &mut CompletionContext) -> bool {
|
fn complete_imports(ctx: &mut CompletionContext) -> bool {
|
||||||
// In an import path for a package:
|
// In an import path for a file or package:
|
||||||
// "#import "@|",
|
// "#import "|",
|
||||||
if_chain! {
|
if_chain! {
|
||||||
if matches!(
|
if matches!(
|
||||||
ctx.leaf.parent_kind(),
|
ctx.leaf.parent_kind(),
|
||||||
@ -489,11 +491,14 @@ fn complete_imports(ctx: &mut CompletionContext) -> bool {
|
|||||||
);
|
);
|
||||||
if let Some(ast::Expr::Str(str)) = ctx.leaf.cast();
|
if let Some(ast::Expr::Str(str)) = ctx.leaf.cast();
|
||||||
let value = str.get();
|
let value = str.get();
|
||||||
if value.starts_with('@');
|
|
||||||
then {
|
then {
|
||||||
let all_versions = value.contains(':');
|
|
||||||
ctx.from = ctx.leaf.offset();
|
ctx.from = ctx.leaf.offset();
|
||||||
ctx.package_completions(all_versions);
|
if value.starts_with('@') {
|
||||||
|
let all_versions = value.contains(':');
|
||||||
|
ctx.package_completions(all_versions);
|
||||||
|
} else {
|
||||||
|
ctx.file_completions_with_extensions(&["typ"]);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -757,7 +762,7 @@ fn param_completions<'a>(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.cast_completions(¶m.input);
|
param_value_completions(ctx, func, param);
|
||||||
}
|
}
|
||||||
|
|
||||||
if param.named {
|
if param.named {
|
||||||
@ -791,16 +796,39 @@ fn named_param_value_completions<'a>(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.cast_completions(¶m.input);
|
param_value_completions(ctx, func, param);
|
||||||
if name == "font" {
|
|
||||||
ctx.font_completions();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ctx.before.ends_with(':') {
|
if ctx.before.ends_with(':') {
|
||||||
ctx.enrich(" ", "");
|
ctx.enrich(" ", "");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Add completions for the values of a parameter.
|
||||||
|
fn param_value_completions<'a>(
|
||||||
|
ctx: &mut CompletionContext<'a>,
|
||||||
|
func: &Func,
|
||||||
|
param: &'a ParamInfo,
|
||||||
|
) {
|
||||||
|
ctx.cast_completions(¶m.input);
|
||||||
|
|
||||||
|
if param.name == "font" {
|
||||||
|
ctx.font_completions();
|
||||||
|
} else if param.name == "path" {
|
||||||
|
ctx.file_completions_with_extensions(match func.name() {
|
||||||
|
Some("image") => &["png", "jpg", "jpeg", "gif", "svg", "svgz"],
|
||||||
|
Some("csv") => &["csv"],
|
||||||
|
Some("plugin") => &["wasm"],
|
||||||
|
Some("cbor") => &["cbor"],
|
||||||
|
Some("json") => &["json"],
|
||||||
|
Some("toml") => &["toml"],
|
||||||
|
Some("xml") => &["xml"],
|
||||||
|
Some("yaml") => &["yml", "yaml"],
|
||||||
|
Some("bibliography") => &["bib", "yml", "yaml"],
|
||||||
|
_ => &[],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Resolve a callee expression to a global function.
|
/// Resolve a callee expression to a global function.
|
||||||
fn resolve_global_callee<'a>(
|
fn resolve_global_callee<'a>(
|
||||||
ctx: &CompletionContext<'a>,
|
ctx: &CompletionContext<'a>,
|
||||||
@ -977,25 +1005,19 @@ fn code_completions(ctx: &mut CompletionContext, hash: bool) {
|
|||||||
|
|
||||||
ctx.snippet_completion(
|
ctx.snippet_completion(
|
||||||
"import (file)",
|
"import (file)",
|
||||||
"import \"${file}.typ\": ${items}",
|
"import \"${}\": ${}",
|
||||||
"Imports variables from another file.",
|
"Imports variables from another file.",
|
||||||
);
|
);
|
||||||
|
|
||||||
ctx.snippet_completion(
|
ctx.snippet_completion(
|
||||||
"import (package)",
|
"import (package)",
|
||||||
"import \"@${}\": ${items}",
|
"import \"@${}\": ${}",
|
||||||
"Imports variables from another file.",
|
"Imports variables from a package.",
|
||||||
);
|
);
|
||||||
|
|
||||||
ctx.snippet_completion(
|
ctx.snippet_completion(
|
||||||
"include (file)",
|
"include (file)",
|
||||||
"include \"${file}.typ\"",
|
"include \"${}\"",
|
||||||
"Includes content from another file.",
|
|
||||||
);
|
|
||||||
|
|
||||||
ctx.snippet_completion(
|
|
||||||
"include (package)",
|
|
||||||
"include \"@${}\"",
|
|
||||||
"Includes content from another file.",
|
"Includes content from another file.",
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -1040,7 +1062,6 @@ struct CompletionContext<'a> {
|
|||||||
impl<'a> CompletionContext<'a> {
|
impl<'a> CompletionContext<'a> {
|
||||||
/// Create a new autocompletion context.
|
/// Create a new autocompletion context.
|
||||||
fn new(
|
fn new(
|
||||||
world: &'a (dyn World + 'a),
|
|
||||||
world: &'a (dyn IdeWorld + 'a),
|
world: &'a (dyn IdeWorld + 'a),
|
||||||
document: Option<&'a Document>,
|
document: Option<&'a Document>,
|
||||||
source: &'a Source,
|
source: &'a Source,
|
||||||
@ -1130,6 +1151,50 @@ impl<'a> CompletionContext<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Add completions for all available files.
|
||||||
|
fn file_completions(&mut self, mut filter: impl FnMut(FileId) -> bool) {
|
||||||
|
let Some(base_id) = self.leaf.span().id() else { return };
|
||||||
|
let Some(base_path) = base_id.vpath().as_rooted_path().parent() else { return };
|
||||||
|
|
||||||
|
let mut paths: Vec<EcoString> = self
|
||||||
|
.world
|
||||||
|
.files()
|
||||||
|
.iter()
|
||||||
|
.filter(|&&file_id| file_id != base_id && filter(file_id))
|
||||||
|
.filter_map(|file_id| {
|
||||||
|
let file_path = file_id.vpath().as_rooted_path();
|
||||||
|
pathdiff::diff_paths(file_path, base_path)
|
||||||
|
})
|
||||||
|
.map(|path| path.to_string_lossy().replace('\\', "/").into())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
paths.sort();
|
||||||
|
|
||||||
|
for path in paths {
|
||||||
|
self.value_completion(None, &Value::Str(path.into()), false, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add completions for all files with any of the given extensions.
|
||||||
|
///
|
||||||
|
/// If the array is empty, all extensions are allowed.
|
||||||
|
fn file_completions_with_extensions(&mut self, extensions: &[&str]) {
|
||||||
|
if extensions.is_empty() {
|
||||||
|
self.file_completions(|_| true);
|
||||||
|
}
|
||||||
|
self.file_completions(|id| {
|
||||||
|
let ext = id
|
||||||
|
.vpath()
|
||||||
|
.as_rooted_path()
|
||||||
|
.extension()
|
||||||
|
.and_then(OsStr::to_str)
|
||||||
|
.map(EcoString::from)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_lowercase();
|
||||||
|
extensions.contains(&ext.as_str())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// Add completions for raw block tags.
|
/// Add completions for raw block tags.
|
||||||
fn raw_completions(&mut self) {
|
fn raw_completions(&mut self) {
|
||||||
for (name, mut tags) in RawElem::languages() {
|
for (name, mut tags) in RawElem::languages() {
|
||||||
@ -1409,11 +1474,19 @@ mod tests {
|
|||||||
use std::collections::BTreeSet;
|
use std::collections::BTreeSet;
|
||||||
|
|
||||||
use typst::model::Document;
|
use typst::model::Document;
|
||||||
use typst::syntax::Source;
|
use typst::syntax::{FileId, Source, VirtualPath};
|
||||||
|
use typst::World;
|
||||||
|
|
||||||
use super::{autocomplete, Completion};
|
use super::{autocomplete, Completion};
|
||||||
use crate::tests::{SourceExt, TestWorld};
|
use crate::tests::{SourceExt, TestWorld};
|
||||||
|
|
||||||
|
/// Quote a string.
|
||||||
|
macro_rules! q {
|
||||||
|
($s:literal) => {
|
||||||
|
concat!("\"", $s, "\"")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
type Response = Option<(usize, Vec<Completion>)>;
|
type Response = Option<(usize, Vec<Completion>)>;
|
||||||
|
|
||||||
trait ResponseExt {
|
trait ResponseExt {
|
||||||
@ -1488,6 +1561,14 @@ mod tests {
|
|||||||
test_full(world, &world.main, doc.as_ref(), cursor)
|
test_full(world, &world.main, doc.as_ref(), cursor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
|
fn test_with_path(world: &TestWorld, path: &str, cursor: isize) -> Response {
|
||||||
|
let doc = typst::compile(&world).output.ok();
|
||||||
|
let id = FileId::new(None, VirtualPath::new(path));
|
||||||
|
let source = world.source(id).unwrap();
|
||||||
|
test_full(world, &source, doc.as_ref(), cursor)
|
||||||
|
}
|
||||||
|
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
fn test_full(
|
fn test_full(
|
||||||
world: &TestWorld,
|
world: &TestWorld,
|
||||||
@ -1495,7 +1576,7 @@ mod tests {
|
|||||||
doc: Option<&Document>,
|
doc: Option<&Document>,
|
||||||
cursor: isize,
|
cursor: isize,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
autocomplete(&world, doc, source, source.cursor(cursor), true)
|
autocomplete(world, doc, source, source.cursor(cursor), true)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -1530,8 +1611,8 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_autocomplete_cite_function() {
|
fn test_autocomplete_cite_function() {
|
||||||
// First compile a working file to get a document.
|
// First compile a working file to get a document.
|
||||||
let mut world = TestWorld::new("#bibliography(\"works.bib\") <bib>")
|
let mut world =
|
||||||
.with_asset_by_name("works.bib");
|
TestWorld::new("#bibliography(\"works.bib\") <bib>").with_asset("works.bib");
|
||||||
let doc = typst::compile(&world).output.ok();
|
let doc = typst::compile(&world).output.ok();
|
||||||
|
|
||||||
// Then, add the invalid `#cite` call. Had the document been invalid
|
// Then, add the invalid `#cite` call. Had the document been invalid
|
||||||
@ -1573,4 +1654,36 @@ mod tests {
|
|||||||
.must_include(["integer"])
|
.must_include(["integer"])
|
||||||
.must_exclude(["string"]);
|
.must_exclude(["string"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_autocomplete_packages() {
|
||||||
|
test("#import \"@\"", -1).must_include([q!("@preview/example:0.1.0")]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_autocomplete_file_path() {
|
||||||
|
let world = TestWorld::new("#include \"\"")
|
||||||
|
.with_source("utils.typ", "")
|
||||||
|
.with_source("content/a.typ", "#image()")
|
||||||
|
.with_source("content/b.typ", "#csv(\"\")")
|
||||||
|
.with_source("content/c.typ", "#include \"\"")
|
||||||
|
.with_asset_at("assets/tiger.jpg", "tiger.jpg")
|
||||||
|
.with_asset_at("assets/rhino.png", "rhino.png")
|
||||||
|
.with_asset_at("data/example.csv", "example.csv");
|
||||||
|
|
||||||
|
test_with_path(&world, "main.typ", -1)
|
||||||
|
.must_include([q!("content/a.typ"), q!("content/b.typ"), q!("utils.typ")])
|
||||||
|
.must_exclude([q!("assets/tiger.jpg")]);
|
||||||
|
|
||||||
|
test_with_path(&world, "content/c.typ", -1)
|
||||||
|
.must_include([q!("../main.typ"), q!("a.typ"), q!("b.typ")])
|
||||||
|
.must_exclude([q!("c.typ")]);
|
||||||
|
|
||||||
|
test_with_path(&world, "content/a.typ", -1)
|
||||||
|
.must_include([q!("../assets/tiger.jpg"), q!("../assets/rhino.png")])
|
||||||
|
.must_exclude([q!("../data/example.csv"), q!("b.typ")]);
|
||||||
|
|
||||||
|
test_with_path(&world, "content/b.typ", -2)
|
||||||
|
.must_include([q!("../data/example.csv")]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@ use std::fmt::Write;
|
|||||||
|
|
||||||
use ecow::{eco_format, EcoString};
|
use ecow::{eco_format, EcoString};
|
||||||
use typst::syntax::package::PackageSpec;
|
use typst::syntax::package::PackageSpec;
|
||||||
|
use typst::syntax::FileId;
|
||||||
use typst::text::{FontInfo, FontStyle};
|
use typst::text::{FontInfo, FontStyle};
|
||||||
use typst::World;
|
use typst::World;
|
||||||
|
|
||||||
@ -40,6 +41,14 @@ pub trait IdeWorld: World {
|
|||||||
fn packages(&self) -> &[(PackageSpec, Option<EcoString>)] {
|
fn packages(&self) -> &[(PackageSpec, Option<EcoString>)] {
|
||||||
&[]
|
&[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a list of all known files.
|
||||||
|
///
|
||||||
|
/// This function is **optional** to implement. It enhances the user
|
||||||
|
/// experience by enabling autocompletion for file paths.
|
||||||
|
fn files(&self) -> Vec<FileId> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extract the first sentence of plain text of a piece of documentation.
|
/// Extract the first sentence of plain text of a piece of documentation.
|
||||||
@ -122,9 +131,11 @@ fn summarize_font_family<'a>(variants: impl Iterator<Item = &'a FontInfo>) -> Ec
|
|||||||
mod tests {
|
mod tests {
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use ecow::EcoString;
|
||||||
use typst::diag::{FileError, FileResult};
|
use typst::diag::{FileError, FileResult};
|
||||||
use typst::foundations::{Bytes, Datetime, Smart};
|
use typst::foundations::{Bytes, Datetime, Smart};
|
||||||
use typst::layout::{Abs, Margin, PageElem};
|
use typst::layout::{Abs, Margin, PageElem};
|
||||||
|
use typst::syntax::package::{PackageSpec, PackageVersion};
|
||||||
use typst::syntax::{FileId, Source, VirtualPath};
|
use typst::syntax::{FileId, Source, VirtualPath};
|
||||||
use typst::text::{Font, FontBook, TextElem, TextSize};
|
use typst::text::{Font, FontBook, TextElem, TextSize};
|
||||||
use typst::utils::{singleton, LazyHash};
|
use typst::utils::{singleton, LazyHash};
|
||||||
@ -155,16 +166,6 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add an additional asset file to the test world.
|
|
||||||
#[track_caller]
|
|
||||||
pub fn with_asset_by_name(mut self, filename: &str) -> Self {
|
|
||||||
let id = FileId::new(None, VirtualPath::new(filename));
|
|
||||||
let data = typst_dev_assets::get_by_name(filename).unwrap();
|
|
||||||
let bytes = Bytes::from_static(data);
|
|
||||||
self.assets.insert(id, bytes);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add an additional source file to the test world.
|
/// Add an additional source file to the test world.
|
||||||
pub fn with_source(mut self, path: &str, text: &str) -> Self {
|
pub fn with_source(mut self, path: &str, text: &str) -> Self {
|
||||||
let id = FileId::new(None, VirtualPath::new(path));
|
let id = FileId::new(None, VirtualPath::new(path));
|
||||||
@ -173,6 +174,22 @@ mod tests {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Add an additional asset file to the test world.
|
||||||
|
#[track_caller]
|
||||||
|
pub fn with_asset(self, filename: &str) -> Self {
|
||||||
|
self.with_asset_at(filename, filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add an additional asset file to the test world.
|
||||||
|
#[track_caller]
|
||||||
|
pub fn with_asset_at(mut self, path: &str, filename: &str) -> Self {
|
||||||
|
let id = FileId::new(None, VirtualPath::new(path));
|
||||||
|
let data = typst_dev_assets::get_by_name(filename).unwrap();
|
||||||
|
let bytes = Bytes::from_static(data);
|
||||||
|
self.assets.insert(id, bytes);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// The ID of the main file in a `TestWorld`.
|
/// The ID of the main file in a `TestWorld`.
|
||||||
pub fn main_id() -> FileId {
|
pub fn main_id() -> FileId {
|
||||||
*singleton!(FileId, FileId::new(None, VirtualPath::new("main.typ")))
|
*singleton!(FileId, FileId::new(None, VirtualPath::new("main.typ")))
|
||||||
@ -222,6 +239,25 @@ mod tests {
|
|||||||
fn upcast(&self) -> &dyn World {
|
fn upcast(&self) -> &dyn World {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn files(&self) -> Vec<FileId> {
|
||||||
|
std::iter::once(self.main.id())
|
||||||
|
.chain(self.sources.keys().copied())
|
||||||
|
.chain(self.assets.keys().copied())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn packages(&self) -> &[(PackageSpec, Option<EcoString>)] {
|
||||||
|
const LIST: &[(PackageSpec, Option<EcoString>)] = &[(
|
||||||
|
PackageSpec {
|
||||||
|
namespace: EcoString::inline("preview"),
|
||||||
|
name: EcoString::inline("example"),
|
||||||
|
version: PackageVersion { major: 0, minor: 1, patch: 0 },
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
)];
|
||||||
|
LIST
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extra methods for [`Source`].
|
/// Extra methods for [`Source`].
|
||||||
|
Loading…
x
Reference in New Issue
Block a user