feat: encode and use workspace information into PackageSpec (#1187)

* feat: remove an unused API

* feat: encode workspace information into `PackageSpec`

* feat: remove unused real_path

* feat: remove unused mtime

* feat: add ResolveAccessModel

* feat: implement id overlay semantics

* feat: remove mtime checking in overlay model

* feat: remove mtime checking in notify model

* feat: format ids

* fix: cases

* feat: resolve root by world

* dev: add untitled root

* fix: warnings

* fix: a wrong usage

* fix: snapshots

* fix: tests
This commit is contained in:
Myriad-Dreamin 2025-01-19 11:51:00 +08:00 committed by GitHub
parent a25d208124
commit 56714675b7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
49 changed files with 835 additions and 774 deletions

View file

@ -24,7 +24,8 @@ pub mod signature;
pub use signature::*;
pub mod semantic_tokens;
pub use semantic_tokens::*;
use typst::syntax::{Source, VirtualPath};
use tinymist_world::vfs::WorkspaceResolver;
use typst::syntax::Source;
use typst::World;
mod post_tyck;
mod tyck;
@ -40,12 +41,11 @@ pub use global::*;
use ecow::eco_format;
use lsp_types::Url;
use tinymist_world::EntryReader;
use typst::diag::{FileError, FileResult};
use typst::foundations::{Func, Value};
use typst::syntax::FileId;
use crate::path_to_url;
use crate::path_res_to_url;
pub(crate) trait ToFunc {
fn to_func(&self) -> Option<Func>;
@ -76,16 +76,13 @@ pub trait LspWorldExt {
impl LspWorldExt for tinymist_project::LspWorld {
fn file_id_by_path(&self, path: &Path) -> FileResult<FileId> {
// todo: source in packages
let root = self.workspace_root().ok_or_else(|| {
let reason = eco_format!("workspace root not found");
FileError::Other(Some(reason))
})?;
let relative_path = path.strip_prefix(&root).map_err(|_| {
let reason = eco_format!("access denied, path: {path:?}, root: {root:?}");
FileError::Other(Some(reason))
})?;
Ok(FileId::new(None, VirtualPath::new(relative_path)))
match self.id_for_path(path) {
Some(id) => Ok(id),
None => WorkspaceResolver::file_with_parent_root(path).ok_or_else(|| {
let reason = eco_format!("invalid path: {path:?}");
FileError::Other(Some(reason))
}),
}
}
fn source_by_path(&self, path: &Path) -> FileResult<Source> {
@ -94,10 +91,10 @@ impl LspWorldExt for tinymist_project::LspWorld {
}
fn uri_for_id(&self, fid: FileId) -> Result<Url, FileError> {
self.path_for_id(fid).and_then(|path| {
path_to_url(&path)
.map_err(|err| FileError::Other(Some(eco_format!("convert to url: {err:?}"))))
})
let res = path_res_to_url(self.path_for_id(fid)?);
log::info!("uri_for_id: {fid:?} -> {res:?}");
res.map_err(|err| FileError::Other(Some(eco_format!("convert to url: {err:?}"))))
}
}
@ -133,6 +130,7 @@ mod matcher_tests {
mod expr_tests {
use tinymist_std::path::unix_slash;
use tinymist_world::vfs::WorkspaceResolver;
use typst::syntax::Source;
use crate::syntax::{Expr, RefExpr};
@ -150,8 +148,10 @@ mod expr_tests {
let fid = if let Some(fid) = decl.file_id() {
let vpath = fid.vpath().as_rooted_path();
match fid.package() {
Some(package) => format!(" in {package:?}{}", unix_slash(vpath)),
None => format!(" in {}", unix_slash(vpath)),
Some(package) if WorkspaceResolver::is_package_file(fid) => {
format!(" in {package:?}{}", unix_slash(vpath))
}
Some(_) | None => format!(" in {}", unix_slash(vpath)),
}
} else {
"".to_string()

View file

@ -1,10 +1,12 @@
//! Completion of paths (string literal).
use tinymist_world::vfs::WorkspaceResolver;
use super::*;
impl CompletionPair<'_, '_, '_> {
pub fn complete_path(&mut self, preference: &PathPreference) -> Option<Vec<CompletionItem>> {
let id = self.cursor.source.id();
if id.package().is_some() {
if WorkspaceResolver::is_package_file(id) {
return None;
}

View file

@ -11,6 +11,7 @@ use rustc_hash::FxHashMap;
use tinymist_project::LspWorld;
use tinymist_std::debug_loc::DataSource;
use tinymist_std::hash::{hash128, FxDashMap};
use tinymist_world::vfs::{PathResolution, WorkspaceResolver};
use tinymist_world::{EntryReader, WorldDeps, DETACHED_ENTRY};
use typst::diag::{eco_format, At, FileError, FileResult, SourceResult, StrResult};
use typst::engine::{Route, Sink, Traced};
@ -315,9 +316,9 @@ impl LocalContext {
self.caches
.completion_files
.get_or_init(|| {
if let Some(root) = self.world.workspace_root() {
if let Some(root) = self.world.entry_state().workspace_root() {
scan_workspace_files(&root, PathPreference::Special.ext_matcher(), |path| {
TypstFileId::new(None, VirtualPath::new(path))
WorkspaceResolver::workspace_file(Some(&root), VirtualPath::new(path))
})
} else {
vec![]
@ -357,7 +358,7 @@ impl LocalContext {
}
/// Get all depended files in the workspace, inclusively.
pub fn depended_source_files(&self) -> Vec<TypstFileId> {
pub fn depended_source_files(&self) -> EcoVec<TypstFileId> {
let mut ids = self.depended_files();
let preference = PathPreference::Source {
allow_package: false,
@ -368,22 +369,10 @@ impl LocalContext {
/// Get all depended file ids of a compilation, inclusively.
/// Note: must be called after compilation.
pub fn depended_files(&self) -> Vec<TypstFileId> {
let mut ids = vec![];
for dep in self.depended_paths() {
if let Ok(ref_fid) = self.file_id_by_path(&dep) {
ids.push(ref_fid);
}
}
ids
}
/// Get depended paths of a compilation.
/// Note: must be called after compilation.
pub(crate) fn depended_paths(&self) -> EcoVec<tinymist_std::ImmutPath> {
pub fn depended_files(&self) -> EcoVec<TypstFileId> {
let mut deps = EcoVec::new();
self.world.iter_dependencies(&mut |path| {
deps.push(path);
self.world.iter_dependencies(&mut |file_id| {
deps.push(file_id);
});
deps
@ -585,7 +574,7 @@ impl SharedContext {
}
/// Resolve the real path for a file id.
pub fn path_for_id(&self, id: TypstFileId) -> Result<PathBuf, FileError> {
pub fn path_for_id(&self, id: TypstFileId) -> Result<PathResolution, FileError> {
self.world.path_for_id(id)
}

View file

@ -40,9 +40,8 @@ impl LinkTarget {
LinkTarget::Url(url) => Some(url.as_ref().clone()),
LinkTarget::Path(id, path) => {
// Avoid creating new ids here.
let base = id.vpath().join(path.as_str());
let root = ctx.path_for_id(id.join("/")).ok()?;
crate::path_to_url(&base.resolve(&root)?).ok()
crate::path_res_to_url(root.join(path).ok()?).ok()
}
}
}

View file

@ -2,7 +2,7 @@ pub use core::fmt;
pub use std::collections::{BTreeMap, HashMap};
pub use std::hash::{Hash, Hasher};
pub use std::ops::Range;
pub use std::path::{Path, PathBuf};
pub use std::path::Path;
pub use std::sync::{Arc, LazyLock};
pub use comemo::Track;

View file

@ -169,7 +169,7 @@ mod tests {
let mut results = vec![];
for s in rng.clone() {
let request = CompletionRequest {
path: ctx.path_for_id(id).unwrap(),
path: ctx.path_for_id(id).unwrap().as_path().to_owned(),
position: ctx.to_lsp_pos(s, &source),
explicit: false,
trigger_character,

View file

@ -1,5 +1,4 @@
use tinymist_project::LspWorld;
use tinymist_world::EntryReader;
use typst::syntax::Span;
use crate::{prelude::*, LspWorldExt};
@ -54,19 +53,10 @@ fn convert_diagnostic(
ctx: &LocalDiagContext,
typst_diagnostic: &TypstDiagnostic,
) -> anyhow::Result<(Url, Diagnostic)> {
let uri;
let lsp_range;
if let Some((id, span)) = diagnostic_span_id(typst_diagnostic) {
uri = ctx.uri_for_id(id)?;
let source = ctx.source(id)?;
lsp_range = diagnostic_range(&source, span, ctx.position_encoding);
} else {
let root = ctx
.workspace_root()
.ok_or_else(|| anyhow::anyhow!("no workspace root"))?;
uri = path_to_url(&root)?;
lsp_range = LspRange::default();
};
let (id, span) = diagnostic_span_id(ctx, typst_diagnostic);
let uri = ctx.uri_for_id(id)?;
let source = ctx.source(id)?;
let lsp_range = diagnostic_range(&source, span, ctx.position_encoding);
let lsp_severity = diagnostic_severity(typst_diagnostic.severity);
@ -131,10 +121,14 @@ fn diagnostic_related_information(
Ok(tracepoints)
}
fn diagnostic_span_id(typst_diagnostic: &TypstDiagnostic) -> Option<(TypstFileId, Span)> {
fn diagnostic_span_id(
ctx: &LocalDiagContext,
typst_diagnostic: &TypstDiagnostic,
) -> (TypstFileId, Span) {
iter::once(typst_diagnostic.span)
.chain(typst_diagnostic.trace.iter().map(|trace| trace.span))
.find_map(|span| Some((span.id()?, span)))
.unwrap_or_else(|| (ctx.main(), Span::detached()))
}
fn diagnostic_range(

View file

@ -1,37 +1,29 @@
use std::path::Path;
use std::sync::{Arc, LazyLock};
use ecow::{eco_format, EcoString};
use parking_lot::Mutex;
use tinymist_world::{EntryState, ShadowApi, TaskInputs};
use tinymist_world::{EntryReader, ShadowApi, TaskInputs};
use typlite::scopes::Scopes;
use typlite::value::Value;
use typlite::TypliteFeat;
use typst::diag::StrResult;
use typst::foundations::Bytes;
use typst::{
diag::StrResult,
syntax::{FileId, VirtualPath},
};
use typst::World;
use crate::analysis::SharedContext;
// Unfortunately, we have only 65536 possible file ids and we cannot revoke
// them. So we share a global file id for all docs conversion.
static DOCS_CONVERT_ID: LazyLock<Mutex<FileId>> =
LazyLock::new(|| Mutex::new(FileId::new(None, VirtualPath::new("__tinymist_docs__.typ"))));
pub(crate) fn convert_docs(ctx: &SharedContext, content: &str) -> StrResult<EcoString> {
static DOCS_LIB: LazyLock<Arc<Scopes<Value>>> =
LazyLock::new(|| Arc::new(typlite::library::docstring_lib()));
let conv_id = DOCS_CONVERT_ID.lock();
let entry = EntryState::new_rootless(conv_id.vpath().as_rooted_path().into()).unwrap();
let entry = entry.select_in_workspace(*conv_id);
let entry = ctx.world.entry_state();
let entry = entry.select_in_workspace(Path::new("__tinymist_docs__.typ"));
let mut w = ctx.world.task(TaskInputs {
entry: Some(entry),
inputs: None,
});
w.map_shadow_by_id(*conv_id, Bytes::from(content.as_bytes().to_owned()))?;
w.map_shadow_by_id(w.main(), Bytes::from(content.as_bytes().to_owned()))?;
// todo: bad performance
w.source_db.take_state();

View file

@ -215,7 +215,7 @@ impl DocumentMetricsWorker<'_> {
let column = source.byte_to_column(byte_index)?;
let filepath = self.ctx.path_for_id(file_id).ok()?;
let filepath_str = filepath.to_string_lossy().to_string();
let filepath_str = filepath.as_path().display().to_string();
Some((filepath_str, line as u32 + 1, column as u32 + 1))
}

View file

@ -1,7 +1,7 @@
use anyhow::bail;
use tinymist_std::ImmutPath;
use tinymist_world::EntryState;
use typst::syntax::{FileId, VirtualPath};
use typst::syntax::VirtualPath;
/// Entry resolver
#[derive(Debug, Default, Clone)]
@ -60,14 +60,14 @@ impl EntryResolver {
(Some(entry), Some(root)) => match entry.strip_prefix(&root) {
Ok(stripped) => Some(EntryState::new_rooted(
root,
Some(FileId::new(None, VirtualPath::new(stripped))),
Some(VirtualPath::new(stripped)),
)),
Err(err) => {
log::info!("Entry is not in root directory: err {err:?}: entry: {entry:?}, root: {root:?}");
EntryState::new_rootless(entry)
EntryState::new_rooted_by_parent(entry)
}
},
(Some(entry), None) => EntryState::new_rootless(entry),
(Some(entry), None) => EntryState::new_rooted_by_parent(entry),
(None, Some(root)) => Some(EntryState::new_workspace(root)),
(None, None) => None,
};
@ -106,6 +106,8 @@ impl EntryResolver {
#[cfg(test)]
#[cfg(any(windows, unix, target_os = "macos"))]
mod entry_tests {
use tinymist_world::vfs::WorkspaceResolver;
use super::*;
use std::path::Path;
@ -127,7 +129,10 @@ mod entry_tests {
assert_eq!(entry.root(), Some(ImmutPath::from(root_path)));
assert_eq!(
entry.main(),
Some(FileId::new(None, VirtualPath::new("main.typ")))
Some(WorkspaceResolver::workspace_file(
entry.root().as_ref(),
VirtualPath::new("main.typ")
))
);
}
@ -152,7 +157,10 @@ mod entry_tests {
assert_eq!(entry.root(), Some(ImmutPath::from(root_path)));
assert_eq!(
entry.main(),
Some(FileId::new(None, VirtualPath::new("main.typ")))
Some(WorkspaceResolver::workspace_file(
entry.root().as_ref(),
VirtualPath::new("main.typ")
))
);
}
@ -166,7 +174,10 @@ mod entry_tests {
assert_eq!(entry.root(), Some(ImmutPath::from(root2_path)));
assert_eq!(
entry.main(),
Some(FileId::new(None, VirtualPath::new("main.typ")))
Some(WorkspaceResolver::workspace_file(
entry.root().as_ref(),
VirtualPath::new("main.typ")
))
);
}
}

View file

@ -3,6 +3,7 @@
use std::cmp::Ordering;
use tinymist_std::path::PathClean;
use tinymist_world::vfs::PathResolution;
use typst::syntax::Source;
use crate::prelude::*;
@ -42,6 +43,11 @@ impl From<PositionEncoding> for lsp_types::PositionEncodingKind {
const UNTITLED_ROOT: &str = "/untitled";
static EMPTY_URL: LazyLock<Url> = LazyLock::new(|| Url::parse("file://").unwrap());
/// Convert a path to a URL.
pub fn untitled_url(path: &Path) -> anyhow::Result<Url> {
Ok(Url::parse(&format!("untitled:{}", path.display()))?)
}
/// Convert a path to a URL.
pub fn path_to_url(path: &Path) -> anyhow::Result<Url> {
if let Ok(untitled) = path.strip_prefix(UNTITLED_ROOT) {
@ -50,7 +56,7 @@ pub fn path_to_url(path: &Path) -> anyhow::Result<Url> {
return Ok(EMPTY_URL.clone());
}
return Ok(Url::parse(&format!("untitled:{}", untitled.display()))?);
return untitled_url(untitled);
}
Url::from_file_path(path).or_else(|never| {
@ -60,6 +66,14 @@ pub fn path_to_url(path: &Path) -> anyhow::Result<Url> {
})
}
/// Convert a path resolution to a URL.
pub fn path_res_to_url(path: PathResolution) -> anyhow::Result<Url> {
match path {
PathResolution::Rootless(path) => untitled_url(path.as_rooted_path()),
PathResolution::Resolved(path) => path_to_url(&path),
}
}
/// Convert a URL to a path.
pub fn url_to_path(uri: Url) -> PathBuf {
if uri.scheme() == "file" {

View file

@ -1,3 +1,5 @@
use tinymist_world::vfs::WorkspaceResolver;
use crate::{
analysis::Definition,
prelude::*,
@ -65,7 +67,7 @@ pub(crate) fn prepare_renaming(
let name = def.name().clone();
let (def_fid, _def_range) = def.location(ctx.shared()).clone()?;
if def_fid.package().is_some() {
if WorkspaceResolver::is_package_file(def_fid) {
crate::log_debug_ct!(
"prepare_rename: {name} is in a package {pkg:?}",
pkg = def_fid.package(),

View file

@ -80,15 +80,7 @@ struct ReferencesWorker<'a> {
impl ReferencesWorker<'_> {
fn label_root(mut self) -> Option<Vec<LspLocation>> {
let mut ids = vec![];
for dep in self.ctx.ctx.depended_paths() {
if let Ok(ref_fid) = self.ctx.ctx.file_id_by_path(&dep) {
ids.push(ref_fid);
}
}
for ref_fid in ids {
for ref_fid in self.ctx.ctx.depended_files() {
self.file(ref_fid)?;
}

View file

@ -59,7 +59,8 @@ impl StatefulRequest for RenameRequest {
};
let def_fid = def.location(ctx.shared())?.0;
let old_path = ctx.path_for_id(def_fid).ok()?;
// todo: rename in untitled files
let old_path = ctx.path_for_id(def_fid).ok()?.to_err().ok()?;
let rename_loc = Path::new(ref_path_str.as_str());
let diff = pathdiff::diff_paths(Path::new(&new_path_str), rename_loc)?;

View file

@ -36,11 +36,11 @@ impl SemanticRequest for SymbolRequest {
let mut symbols = vec![];
for path in ctx.depended_paths() {
let Ok(source) = ctx.source_by_path(&path) else {
for id in ctx.depended_files() {
let Ok(source) = ctx.source_by_id(id) else {
continue;
};
let uri = path_to_url(&path).unwrap();
let uri = ctx.uri_for_id(id).unwrap();
let res = get_lexical_hierarchy(&source, LexicalScopeKind::Symbol).map(|symbols| {
filter_document_symbols(
&symbols,

View file

@ -12,18 +12,18 @@ use once_cell::sync::Lazy;
use serde_json::{ser::PrettyFormatter, Serializer, Value};
use tinymist_project::CompileFontArgs;
use tinymist_world::package::PackageSpec;
use tinymist_world::vfs::WorkspaceResolver;
use tinymist_world::EntryState;
use tinymist_world::TaskInputs;
use tinymist_world::{EntryManager, EntryReader, ShadowApi};
use typst::foundations::Bytes;
use typst::syntax::ast::{self, AstNode};
use typst::syntax::{FileId as TypstFileId, LinkedNode, Source, SyntaxKind, VirtualPath};
use typst::syntax::{LinkedNode, Source, SyntaxKind, VirtualPath};
pub use insta::assert_snapshot;
pub use serde::Serialize;
pub use serde_json::json;
pub use tinymist_project::{LspUniverse, LspUniverseBuilder};
use typst::World;
use typst_shim::syntax::LinkedNodeExt;
use crate::syntax::find_module_level_docs;
@ -57,11 +57,16 @@ pub fn run_with_ctx<T>(
path: PathBuf,
f: &impl Fn(&mut LocalContext, PathBuf) -> T,
) -> T {
let root = verse.workspace_root().unwrap();
let root = verse.entry_state().workspace_root().unwrap();
let paths = verse
.shadow_paths()
.into_iter()
.map(|path| TypstFileId::new(None, VirtualPath::new(path.strip_prefix(&root).unwrap())))
.map(|path| {
WorkspaceResolver::workspace_file(
Some(&root),
VirtualPath::new(path.strip_prefix(&root).unwrap()),
)
})
.collect::<Vec<_>>();
let world = verse.snapshot();
@ -115,20 +120,19 @@ pub fn compile_doc_for_test(
ctx: &mut LocalContext,
properties: &HashMap<&str, &str>,
) -> Option<VersionedDocument> {
let main_id = properties.get("compile").and_then(|v| match v.trim() {
"true" => Some(ctx.world.main()),
"false" => None,
let entry = match properties.get("compile")?.trim() {
"true" => ctx.world.entry_state(),
"false" => return None,
path if path.ends_with(".typ") => {
let vp = VirtualPath::new(path);
Some(TypstFileId::new(None, vp))
ctx.world.entry_state().select_in_workspace(Path::new(path))
}
_ => panic!("invalid value for 'compile' property: {v}"),
})?;
v => panic!("invalid value for 'compile' property: {v}"),
};
let mut world = Cow::Borrowed(&ctx.world);
if main_id != ctx.world.main() {
if entry != ctx.world.entry_state() {
world = Cow::Owned(world.task(TaskInputs {
entry: Some(world.entry_state().select_in_workspace(main_id)),
entry: Some(entry),
..Default::default()
}));
}
@ -192,10 +196,7 @@ pub fn run_with_sources<T>(source: &str, f: impl FnOnce(&mut LspUniverse, PathBu
verse
.mutate_entry(EntryState::new_rooted(
root.as_path().into(),
Some(TypstFileId::new(
None,
VirtualPath::new(pw.strip_prefix(root).unwrap()),
)),
Some(VirtualPath::new(pw.strip_prefix(root).unwrap())),
))
.unwrap();
f(&mut verse, pw)

View file

@ -25,10 +25,9 @@ impl SemanticRequest for WorkspaceLabelRequest {
let Ok(source) = ctx.source_by_id(fid) else {
continue;
};
let Ok(path) = ctx.path_for_id(fid) else {
let Ok(uri) = ctx.uri_for_id(fid) else {
continue;
};
let uri = path_to_url(&path).unwrap();
let res = get_lexical_hierarchy(&source, LexicalScopeKind::Symbol).map(|hierarchy| {
filter_document_labels(&hierarchy, &source, &uri, ctx.position_encoding())
});