diff --git a/Cargo.lock b/Cargo.lock index fa9d91ac..49684619 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2702,9 +2702,9 @@ dependencies = [ [[package]] name = "reflexo" -version = "0.4.2-rc9" +version = "0.5.0-rc2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3bf3d6c80be65ec408823031e560758c1de427601d61a82fdea0d0ecb7794f4" +checksum = "247ea8050cb5c88b41a68b3269f5a2eb7ebff55851a564d96b035643418346e6" dependencies = [ "base64 0.22.0", "bitvec", @@ -4033,9 +4033,9 @@ dependencies = [ [[package]] name = "typst-preview" -version = "0.11.1" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b8eebe95602e6723ce6e3c06e4c32000ab1bde2d0e775248b74ee2373aeff87" +checksum = "fc1323cc2de067d19919891a1ee3f43af5c96d91decce44044347137d3e06fce" dependencies = [ "anyhow", "await-tree", @@ -4095,9 +4095,9 @@ dependencies = [ [[package]] name = "typst-ts-compiler" -version = "0.4.2-rc9" +version = "0.5.0-rc2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "478dbcd334466a939420fa9e9429e0ecaa8ec8c5a943e74862b2b46b364e2938" +checksum = "c18cf7d96c0c558901b3f7e3f5200ecb7e3d7d3dcc5a1222e94bc875237ff352" dependencies = [ "append-only-vec", "base64 0.22.0", @@ -4134,9 +4134,9 @@ dependencies = [ [[package]] name = "typst-ts-core" -version = "0.4.2-rc9" +version = "0.5.0-rc2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d109fb49a9ffa1d9f7126b317313d29257ec0d34ae7a34c65d16ed1fea2b448" +checksum = "a69135c380eb60efa4aeabd986d27d82ecd1b4c843fd3393992b449409317847" dependencies = [ "base64 0.22.0", "base64-serde", @@ -4172,9 +4172,9 @@ dependencies = [ [[package]] name = "typst-ts-svg-exporter" -version = "0.4.2-rc9" +version = "0.5.0-rc2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f909564a69478f3e244ddc4380dc337f74edfa5d4ce7551ddfedc6f5d32f48b6" +checksum = "b6063f63c8e3ba3d4d7f4cb1a8fd96b096e8e713f24783278fea98dac0746966" dependencies = [ "base64 0.22.0", "comemo", diff --git a/Cargo.toml b/Cargo.toml index 7c460cd5..c9ca5b91 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,12 +44,12 @@ typst = "0.11.0" typst-ide = "0.11.0" typst-pdf = "0.11.0" typst-assets = "0.11.0" -reflexo = { version = "0.4.2-rc9", default-features = false, features = [ +reflexo = { version = "0.5.0-rc2", default-features = false, features = [ "flat-vector", ] } -typst-ts-core = { version = "0.4.2-rc9" } -typst-ts-compiler = { version = "0.4.2-rc9" } -typst-preview = { version = "0.11.0" } +typst-ts-core = { version = "0.5.0-rc2", default-features = false } +typst-ts-compiler = { version = "0.5.0-rc2" } +typst-preview = { version = "0.11.3" } lsp-server = "0.7.6" lsp-types = { version = "=0.95.0", features = ["proposed"] } @@ -108,7 +108,7 @@ undocumented_unsafe_blocks = "warn" typst = { git = "https://github.com/Myriad-Dreamin/typst.git", branch = "tinymist-v0.11.0" } typst-ide = { git = "https://github.com/Myriad-Dreamin/typst.git", branch = "tinymist-v0.11.0" } typst-pdf = { git = "https://github.com/Myriad-Dreamin/typst.git", branch = "tinymist-v0.11.0" } -typst-syntax = { git = "https://github.com/Myriad-Dreamin/typst.git", branch = "tinymist-v0.11.0" } +# typst-syntax = { git = "https://github.com/Myriad-Dreamin/typst.git", branch = "tinymist-v0.11.0" } # typst = { path = "../typst/crates/typst" } # typst-ide = { path = "../typst/crates/typst-ide" } diff --git a/crates/tinymist-query/Cargo.toml b/crates/tinymist-query/Cargo.toml index 437135f3..909c59df 100644 --- a/crates/tinymist-query/Cargo.toml +++ b/crates/tinymist-query/Cargo.toml @@ -34,12 +34,6 @@ typst.workspace = true typst-ide.workspace = true reflexo.workspace = true -typst-ts-compiler.workspace = true -typst-ts-core = { version = "0.4.2-rc8", default-features = false, features = [ - "flat-vector", - "vector-bbox", - "no-content-hint", -] } lsp-types.workspace = true if_chain = "1" @@ -49,8 +43,14 @@ once_cell.workspace = true insta.workspace = true serde.workspace = true serde_json.workspace = true +typst-ts-core = { workspace = true, default-features = false, features = [ + "flat-vector", + "vector-bbox", + "no-content-hint", +] } +typst-ts-compiler.workspace = true sha2 = { version = "0.10" } hex = { version = "0.4" } -# [lints] -# workspace = true +[lints] +workspace = true diff --git a/crates/tinymist-query/src/analysis.rs b/crates/tinymist-query/src/analysis.rs index 60884f7b..1408f2af 100644 --- a/crates/tinymist-query/src/analysis.rs +++ b/crates/tinymist-query/src/analysis.rs @@ -1,3 +1,5 @@ +//! Semantic static and dynamic analysis of the source code. + pub mod def_use; pub use def_use::*; pub mod track_values; diff --git a/crates/tinymist-query/src/analysis/def_use.rs b/crates/tinymist-query/src/analysis/def_use.rs index 2a5e5289..55c114ea 100644 --- a/crates/tinymist-query/src/analysis/def_use.rs +++ b/crates/tinymist-query/src/analysis/def_use.rs @@ -252,7 +252,7 @@ impl<'a, 'b, 'w> DefUseCollector<'a, 'b, 'w> { ModSrc::Expr(_) => {} ModSrc::Path(p) => { let src = find_source_by_import_path( - self.ctx.ctx.world, + self.ctx.ctx.world(), self.current_id, p.deref(), ); diff --git a/crates/tinymist-query/src/analysis/global.rs b/crates/tinymist-query/src/analysis/global.rs index 67949e41..31bc4195 100644 --- a/crates/tinymist-query/src/analysis/global.rs +++ b/crates/tinymist-query/src/analysis/global.rs @@ -8,15 +8,10 @@ use once_cell::sync::OnceCell; use reflexo::{cow_mut::CowMut, ImmutPath}; use typst::syntax::FileId as TypstFileId; use typst::{ - diag::{eco_format, FileError, FileResult}, - syntax::{Source, VirtualPath}, + diag::{eco_format, FileError, FileResult, PackageError}, + syntax::{package::PackageSpec, Source, VirtualPath}, World, }; -use typst_ts_compiler::package::Registry; -use typst_ts_compiler::TypstSystemWorld; -// use typst_ts_compiler::TypstSystemWorld; -// use typst_ts_compiler::{service::WorkspaceProvider, TypstSystemWorld}; -// use typst_ts_core::{cow_mut::CowMut, ImmutPath}; use super::{get_def_use_inner, DefUseInfo}; use crate::{ @@ -37,7 +32,7 @@ impl ModuleAnalysisCache { /// Get the source of a file. pub fn source(&self, ctx: &AnalysisContext, file_id: TypstFileId) -> FileResult { self.source - .get_or_init(|| ctx.world.source(file_id)) + .get_or_init(|| ctx.world().source(file_id)) .clone() } @@ -73,10 +68,22 @@ pub struct AnalysisCaches { module_deps: OnceCell>, } +/// The resources for analysis. +pub trait AnaylsisResources { + /// Get the world surface for Typst compiler. + fn world(&self) -> &dyn World; + + /// Resolve the real path for a package spec. + fn resolve(&self, spec: &PackageSpec) -> Result, PackageError>; + + /// Get all the files in the workspace. + fn iter_dependencies(&self, f: &mut dyn FnMut(&ImmutPath, std::time::SystemTime)); +} + /// The context for analyzers. pub struct AnalysisContext<'a> { /// The world surface for Typst compiler - pub world: &'a TypstSystemWorld, + pub resources: &'a dyn AnaylsisResources, /// The analysis data pub analysis: CowMut<'a, Analysis>, caches: AnalysisCaches, @@ -84,14 +91,19 @@ pub struct AnalysisContext<'a> { impl<'w> AnalysisContext<'w> { /// Create a new analysis context. - pub fn new(world: &'w TypstSystemWorld, a: Analysis) -> Self { + pub fn new(world: &'w dyn AnaylsisResources, a: Analysis) -> Self { Self { - world, + resources: world, analysis: CowMut::Owned(a), caches: AnalysisCaches::default(), } } + /// Get the world surface for Typst compiler. + pub fn world(&self) -> &dyn World { + self.resources.world() + } + #[cfg(test)] pub fn test_files(&mut self, f: impl FnOnce() -> Vec) -> &Vec { self.caches.root_files.get_or_init(f) @@ -125,7 +137,7 @@ impl<'w> AnalysisContext<'w> { // Determine the root path relative to which the file path // will be resolved. let root = match id.package() { - Some(spec) => self.world.registry.resolve(spec)?, + Some(spec) => self.resources.resolve(spec)?, None => self.analysis.root.clone(), }; @@ -182,24 +194,29 @@ impl<'w> AnalysisContext<'w> { } } + /// Get the position encoding during session. + pub(crate) fn position_encoding(&self) -> PositionEncoding { + self.analysis.position_encoding + } + + /// Convert a LSP position to a Typst position. pub fn to_typst_pos(&self, position: LspPosition, src: &Source) -> Option { lsp_to_typst::position(position, self.analysis.position_encoding, src) } - pub fn to_typst_range(&self, position: LspRange, src: &Source) -> Option { - lsp_to_typst::range(position, self.analysis.position_encoding, src) - } - + /// Convert a Typst offset to a LSP position. pub fn to_lsp_pos(&self, typst_offset: usize, src: &Source) -> LspPosition { typst_to_lsp::offset_to_position(typst_offset, self.analysis.position_encoding, src) } - pub fn to_lsp_range(&self, position: TypstRange, src: &Source) -> LspRange { - typst_to_lsp::range(position, src, self.analysis.position_encoding) + /// Convert a LSP range to a Typst range. + pub fn to_typst_range(&self, position: LspRange, src: &Source) -> Option { + lsp_to_typst::range(position, self.analysis.position_encoding, src) } - pub(crate) fn position_encoding(&self) -> PositionEncoding { - self.analysis.position_encoding + /// Convert a Typst range to a LSP range. + pub fn to_lsp_range(&self, position: TypstRange, src: &Source) -> LspRange { + typst_to_lsp::range(position, src, self.analysis.position_encoding) } } diff --git a/crates/tinymist-query/src/code_lens.rs b/crates/tinymist-query/src/code_lens.rs index 0e487e53..56d8b262 100644 --- a/crates/tinymist-query/src/code_lens.rs +++ b/crates/tinymist-query/src/code_lens.rs @@ -1,6 +1,6 @@ use lsp_types::Command; -use crate::{prelude::*, SyntaxRequest}; +use crate::{prelude::*, SemanticRequest}; /// The [`textDocument/codeLens`] request is sent from the client to the server /// to compute code lenses for a given text document. @@ -12,7 +12,7 @@ pub struct CodeLensRequest { pub path: PathBuf, } -impl SyntaxRequest for CodeLensRequest { +impl SemanticRequest for CodeLensRequest { type Response = Vec; fn request(self, ctx: &mut AnalysisContext) -> Option { diff --git a/crates/tinymist-query/src/completion.rs b/crates/tinymist-query/src/completion.rs index 367081c5..c8f3eb4a 100644 --- a/crates/tinymist-query/src/completion.rs +++ b/crates/tinymist-query/src/completion.rs @@ -1,9 +1,32 @@ use crate::{prelude::*, StatefulRequest}; +/// The [`textDocument/completion`] request is sent from the client to the +/// server to compute completion items at a given cursor position. +/// +/// If computing full completion items is expensive, servers can additionally +/// provide a handler for the completion item resolve request +/// (`completionItem/resolve`). This request is sent when a completion item is +/// selected in the user interface. +/// +/// [`textDocument/completion`]: https://microsoft.github.io/language-server-protocol/specification#textDocument_completion +/// +/// # Compatibility +/// +/// Since 3.16.0, the client can signal that it can resolve more properties +/// lazily. This is done using the `completion_item.resolve_support` client +/// capability which lists all properties that can be filled in during a +/// `completionItem/resolve` request. +/// +/// All other properties (usually `sort_text`, `filter_text`, `insert_text`, and +/// `text_edit`) must be provided in the `textDocument/completion` response and +/// must not be changed during resolve. #[derive(Debug, Clone)] pub struct CompletionRequest { + /// The path of the document to compute completions. pub path: PathBuf, + /// The position in the document at which to compute completions. pub position: LspPosition, + /// Whether the completion is triggered explicitly. pub explicit: bool, } @@ -35,7 +58,7 @@ impl StatefulRequest for CompletionRequest { let explicit = false; let (offset, completions) = - typst_ide::autocomplete(ctx.world, doc, &source, cursor, explicit)?; + typst_ide::autocomplete(ctx.world(), doc, &source, cursor, explicit)?; let lsp_start_position = ctx.to_lsp_pos(offset, &source); let replace_range = LspRange::new(lsp_start_position, self.position); diff --git a/crates/tinymist-query/src/diagnostics.rs b/crates/tinymist-query/src/diagnostics.rs index c5e685e5..7a97534a 100644 --- a/crates/tinymist-query/src/diagnostics.rs +++ b/crates/tinymist-query/src/diagnostics.rs @@ -5,7 +5,7 @@ pub type DiagnosticsMap = HashMap>; /// Converts a list of Typst diagnostics to LSP diagnostics. pub fn convert_diagnostics<'a>( - project: &TypstSystemWorld, + project: &AnalysisContext, errors: impl IntoIterator, position_encoding: PositionEncoding, ) -> DiagnosticsMap { @@ -23,7 +23,7 @@ pub fn convert_diagnostics<'a>( } fn convert_diagnostic( - project: &TypstSystemWorld, + project: &AnalysisContext, typst_diagnostic: &TypstDiagnostic, position_encoding: PositionEncoding, ) -> anyhow::Result<(Url, LspDiagnostic)> { @@ -31,10 +31,10 @@ fn convert_diagnostic( let lsp_range; if let Some((id, span)) = diagnostic_span_id(typst_diagnostic) { uri = Url::from_file_path(project.path_for_id(id)?).unwrap(); - let source = project.source(id)?; + let source = project.world().source(id)?; lsp_range = diagnostic_range(&source, span, position_encoding); } else { - uri = Url::from_file_path(project.root.clone()).unwrap(); + uri = Url::from_file_path(project.analysis.root.clone()).unwrap(); lsp_range = LspRange::default(); }; @@ -59,13 +59,13 @@ fn convert_diagnostic( } fn tracepoint_to_relatedinformation( - project: &TypstSystemWorld, + project: &AnalysisContext, tracepoint: &Spanned, position_encoding: PositionEncoding, ) -> anyhow::Result> { if let Some(id) = tracepoint.span.id() { let uri = Url::from_file_path(project.path_for_id(id)?).unwrap(); - let source = project.source(id)?; + let source = project.world().source(id)?; if let Some(typst_range) = source.range(tracepoint.span) { let lsp_range = typst_to_lsp::range(typst_range, &source, position_encoding); @@ -84,7 +84,7 @@ fn tracepoint_to_relatedinformation( } fn diagnostic_related_information( - project: &TypstSystemWorld, + project: &AnalysisContext, typst_diagnostic: &TypstDiagnostic, position_encoding: PositionEncoding, ) -> anyhow::Result> { diff --git a/crates/tinymist-query/src/document_symbol.rs b/crates/tinymist-query/src/document_symbol.rs index 20036913..924a93dd 100644 --- a/crates/tinymist-query/src/document_symbol.rs +++ b/crates/tinymist-query/src/document_symbol.rs @@ -1,22 +1,38 @@ use crate::{ prelude::*, syntax::{get_lexical_hierarchy, LexicalHierarchy, LexicalScopeKind}, + SyntaxRequest, }; +/// The [`textDocument/documentSymbol`] request is sent from the client to the +/// server to retrieve all symbols found in a given text document. +/// +/// [`textDocument/documentSymbol`]: https://microsoft.github.io/language-server-protocol/specification#textDocument_documentSymbol +/// +/// The returned result is either: +/// +/// * [`DocumentSymbolResponse::Flat`] which is a flat list of all symbols found +/// in a given text document. Then neither the symbol’s location range nor the +/// symbol’s container name should be used to infer a hierarchy. +/// * [`DocumentSymbolResponse::Nested`] which is a hierarchy of symbols found +/// in a given text document. #[derive(Debug, Clone)] pub struct DocumentSymbolRequest { + /// The path of the document to retrieve symbols from. pub path: PathBuf, } -impl DocumentSymbolRequest { - pub fn request( +impl SyntaxRequest for DocumentSymbolRequest { + type Response = DocumentSymbolResponse; + + fn request( self, - source: Source, + source: &Source, position_encoding: PositionEncoding, - ) -> Option { + ) -> Option { let symbols = get_lexical_hierarchy(source.clone(), LexicalScopeKind::Symbol)?; - let symbols = filter_document_symbols(&symbols, &source, position_encoding); + let symbols = filter_document_symbols(&symbols, source, position_encoding); Some(DocumentSymbolResponse::Nested(symbols)) } } @@ -62,7 +78,7 @@ mod tests { let source = ctx.source_by_path(&path).unwrap(); - let result = request.request(source, PositionEncoding::Utf16); + let result = request.request(&source, PositionEncoding::Utf16); assert_snapshot!(JsonRepr::new_redacted(result.unwrap(), &REDACT_LOC)); }); } diff --git a/crates/tinymist-query/src/folding_range.rs b/crates/tinymist-query/src/folding_range.rs index 0d2a8e57..218eef0d 100644 --- a/crates/tinymist-query/src/folding_range.rs +++ b/crates/tinymist-query/src/folding_range.rs @@ -1,32 +1,46 @@ use crate::{ prelude::*, syntax::{get_lexical_hierarchy, LexicalHierarchy, LexicalKind, LexicalScopeKind}, + SyntaxRequest, }; +/// The [`textDocument/foldingRange`] request is sent from the client to the +/// server to return all folding ranges found in a given text document. +/// +/// [`textDocument/foldingRange`]: https://microsoft.github.io/language-server-protocol/specification#textDocument_foldingRange +/// +/// # Compatibility +/// +/// This request was introduced in specification version 3.10.0. #[derive(Debug, Clone)] pub struct FoldingRangeRequest { + /// The path of the document to get folding ranges for. pub path: PathBuf, + /// If set, the client can only provide folding ranges that consist of whole + /// lines. pub line_folding_only: bool, } -impl FoldingRangeRequest { - pub fn request( +impl SyntaxRequest for FoldingRangeRequest { + type Response = Vec; + + fn request( self, - source: Source, + source: &Source, position_encoding: PositionEncoding, - ) -> Option> { + ) -> Option { let line_folding_only = self.line_folding_only; let symbols = get_lexical_hierarchy(source.clone(), LexicalScopeKind::Braced)?; let mut results = vec![]; let LspPosition { line, character } = - typst_to_lsp::offset_to_position(source.text().len(), position_encoding, &source); + typst_to_lsp::offset_to_position(source.text().len(), position_encoding, source); let loc = (line, Some(character)); calc_folding_range( &symbols, - &source, + source, position_encoding, line_folding_only, loc, @@ -126,7 +140,7 @@ mod tests { let source = world.source_by_path(&path).unwrap(); - let result = request.request(source, PositionEncoding::Utf16); + let result = request.request(&source, PositionEncoding::Utf16); assert_snapshot!(JsonRepr::new_pure(result.unwrap())); }); } diff --git a/crates/tinymist-query/src/goto_declaration.rs b/crates/tinymist-query/src/goto_declaration.rs index 957bf290..0b7d3d8a 100644 --- a/crates/tinymist-query/src/goto_declaration.rs +++ b/crates/tinymist-query/src/goto_declaration.rs @@ -6,16 +6,35 @@ use lsp_types::LocationLink; use crate::{ prelude::*, syntax::{get_deref_target, DerefTarget}, - SyntaxRequest, + SemanticRequest, }; +/// The [`textDocument/declaration`] request asks the server for the declaration +/// location of a symbol at a given text document position. +/// +/// [`textDocument/declaration`]: https://microsoft.github.io/language-server-protocol/specification#textDocument_declaration +/// +/// # Compatibility +/// +/// This request was introduced in specification version 3.14.0. +/// +/// The [`GotoDeclarationResponse::Link`](lsp_types::GotoDefinitionResponse::Link) return value +/// was introduced in specification version 3.14.0 and requires client-side +/// support in order to be used. It can be returned if the client set the +/// following field to `true` in the [`initialize`](Self::initialize) method: +/// +/// ```text +/// InitializeParams::capabilities::text_document::declaration::link_support +/// ``` #[derive(Debug, Clone)] pub struct GotoDeclarationRequest { + /// The path of the document to get the declaration location for. pub path: PathBuf, + /// The position of the symbol to get the declaration location for. pub position: LspPosition, } -impl SyntaxRequest for GotoDeclarationRequest { +impl SemanticRequest for GotoDeclarationRequest { type Response = GotoDeclarationResponse; fn request(self, ctx: &mut AnalysisContext) -> Option { diff --git a/crates/tinymist-query/src/goto_definition.rs b/crates/tinymist-query/src/goto_definition.rs index 96c05cb0..46788e4d 100644 --- a/crates/tinymist-query/src/goto_definition.rs +++ b/crates/tinymist-query/src/goto_definition.rs @@ -10,7 +10,7 @@ use crate::{ find_source_by_import, get_deref_target, DerefTarget, IdentRef, LexicalKind, LexicalModKind, LexicalVarKind, }, - SyntaxRequest, + SemanticRequest, }; /// The [`textDocument/definition`] request asks the server for the definition @@ -36,7 +36,7 @@ pub struct GotoDefinitionRequest { pub position: LspPosition, } -impl SyntaxRequest for GotoDefinitionRequest { +impl SemanticRequest for GotoDefinitionRequest { type Response = GotoDefinitionResponse; fn request(self, ctx: &mut AnalysisContext) -> Option { @@ -96,7 +96,7 @@ pub(crate) fn find_definition( let parent = path.parent()?; let def_fid = parent.span().id()?; let e = parent.cast::()?; - let source = find_source_by_import(ctx.world, def_fid, e)?; + let source = find_source_by_import(ctx.world(), def_fid, e)?; return Some(DefinitionLink { kind: LexicalKind::Mod(LexicalModKind::PathVar), name: String::new(), @@ -132,7 +132,7 @@ pub(crate) fn find_definition( let def_id = def_id.or_else(|| Some(def_use.get_def(source_id, &ident_ref)?.0)); let def_info = def_id.and_then(|def_id| def_use.get_def_by_id(def_id)); - let values = analyze_expr(ctx.world, &use_site); + let values = analyze_expr(ctx.world(), &use_site); for v in values { // mostly builtin functions if let Value::Func(f) = v.0 { @@ -193,7 +193,7 @@ pub(crate) fn find_definition( let root = LinkedNode::new(def_source.root()); let def_name = root.leaf_at(def.range.start + 1)?; log::info!("def_name for function: {def_name:?}", def_name = def_name); - let values = analyze_expr(ctx.world, &def_name); + let values = analyze_expr(ctx.world(), &def_name); let func = values.into_iter().find(|v| matches!(v.0, Value::Func(..))); log::info!("okay for function: {func:?}"); diff --git a/crates/tinymist-query/src/hover.rs b/crates/tinymist-query/src/hover.rs index c55c662f..96a93d13 100644 --- a/crates/tinymist-query/src/hover.rs +++ b/crates/tinymist-query/src/hover.rs @@ -8,9 +8,18 @@ use crate::{ DefinitionLink, LspHoverContents, StatefulRequest, }; +/// The [`textDocument/hover`] request asks the server for hover information at +/// a given text document position. +/// +/// [`textDocument/hover`]: https://microsoft.github.io/language-server-protocol/specification#textDocument_hover +/// +/// Such hover information typically includes type signature information and +/// inline documentation for the symbol at the given text document position. #[derive(Debug, Clone)] pub struct HoverRequest { + /// The path of the document to get hover information for. pub path: PathBuf, + /// The position of the symbol to get hover information for. pub position: LspPosition, } @@ -31,7 +40,10 @@ impl StatefulRequest for HoverRequest { let contents = def_tooltip(ctx, &source, cursor).or_else(|| { Some(typst_to_lsp::tooltip(&tooltip( - ctx.world, doc, &source, cursor, + ctx.world(), + doc, + &source, + cursor, )?)) })?; @@ -76,10 +88,10 @@ let {name}({params}); crate::syntax::LexicalKind::Var(LexicalVarKind::Variable) => { let deref_node = deref_target.node(); // todo: check sensible length, value highlighting - let values = expr_tooltip(ctx.world, deref_node) + let values = expr_tooltip(ctx.world(), deref_node) .map(|t| match t { - Tooltip::Text(s) => format!("// Values: {}", s), - Tooltip::Code(s) => format!("// Values: {}", s), + Tooltip::Text(s) => format!("// Values: {s}"), + Tooltip::Code(s) => format!("// Values: {s}"), }) .unwrap_or_default(); Some(LspHoverContents::Scalar(lsp_types::MarkedString::String( diff --git a/crates/tinymist-query/src/inlay_hint.rs b/crates/tinymist-query/src/inlay_hint.rs index 5daad0fd..6a084835 100644 --- a/crates/tinymist-query/src/inlay_hint.rs +++ b/crates/tinymist-query/src/inlay_hint.rs @@ -10,23 +10,30 @@ use typst::{ util::LazyHash, }; -use crate::{prelude::*, SyntaxRequest}; +use crate::{prelude::*, SemanticRequest}; +/// Configuration for inlay hints. pub struct InlayHintConfig { // positional arguments group + /// Show inlay hints for positional arguments. pub on_pos_args: bool, + /// Disable inlay hints for single positional arguments. pub off_single_pos_arg: bool, // variadic arguments group + /// Show inlay hints for variadic arguments. pub on_variadic_args: bool, + /// Disable inlay hints for all variadic arguments but the first variadic + /// argument. pub only_first_variadic_args: bool, - // todo // The typst sugar grammar + /// Show inlay hints for content block arguments. pub on_content_block_args: bool, } impl InlayHintConfig { + /// A smart configuration that enables most useful inlay hints. pub const fn smart() -> Self { Self { on_pos_args: true, @@ -40,20 +47,31 @@ impl InlayHintConfig { } } +/// The [`textDocument/inlayHint`] request is sent from the client to the server +/// to compute inlay hints for a given `(text document, range)` tuple that may +/// be rendered in the editor in place with other text. +/// +/// [`textDocument/inlayHint`]: https://microsoft.github.io/language-server-protocol/specification#textDocument_inlayHint +/// +/// # Compatibility +/// +/// This request was introduced in specification version 3.17.0 #[derive(Debug, Clone)] pub struct InlayHintRequest { + /// The path of the document to get inlay hints for. pub path: PathBuf, + /// The range of the document to get inlay hints for. pub range: LspRange, } -impl SyntaxRequest for InlayHintRequest { +impl SemanticRequest for InlayHintRequest { type Response = Vec; fn request(self, ctx: &mut AnalysisContext) -> Option { let source = ctx.source_by_path(&self.path).ok()?; let range = ctx.to_typst_range(self.range, &source)?; - let hints = inlay_hint(ctx.world, &source, range, ctx.position_encoding()).ok()?; + let hints = inlay_hint(ctx.world(), &source, range, ctx.position_encoding()).ok()?; debug!( "got inlay hints on {source:?} => {hints:?}", source = source.id(), @@ -516,7 +534,7 @@ impl ParamSpec { } #[derive(Debug, Clone)] -pub struct Signature { +pub(crate) struct Signature { pub pos: Vec>, pub named: HashMap, Arc>, has_fill_or_size_or_stroke: bool, diff --git a/crates/tinymist-query/src/lib.rs b/crates/tinymist-query/src/lib.rs index 3ee6bd96..85243d58 100644 --- a/crates/tinymist-query/src/lib.rs +++ b/crates/tinymist-query/src/lib.rs @@ -1,3 +1,12 @@ +//! # tinymist-query +//! +//! **Note: this crate is under development. it currently doesn't ensure stable +//! APIs, and heavily depending on some unstable crates.** +//! +//! This crate provides a set of APIs to query the information about the source +//! code. Currently it provides: +//! + language queries defined by the [Language Server Protocol](https://microsoft.github.io/language-server-protocol/). + mod adt; pub mod analysis; pub mod syntax; @@ -8,7 +17,7 @@ pub(crate) mod diagnostics; use std::sync::Arc; pub use analysis::AnalysisContext; -use typst::model::Document as TypstDocument; +use typst::{model::Document as TypstDocument, syntax::Source}; pub use diagnostics::*; pub(crate) mod code_lens; @@ -48,31 +57,57 @@ pub use references::*; pub mod lsp_typst_boundary; pub use lsp_typst_boundary::*; +pub(crate) mod lsp_features; +pub use lsp_features::*; mod prelude; +/// A compiled document with an self-incremented logical version. #[derive(Debug, Clone)] pub struct VersionedDocument { + /// The version of the document. pub version: usize, + /// The compiled document. pub document: Arc, } +/// A request handler with given syntax information. pub trait SyntaxRequest { + /// The response type of the request. type Response; - fn request(self, ctx: &mut AnalysisContext) -> Option; -} - -pub trait StatefulRequest { - type Response; - + /// Request the information from the given source. fn request( self, - ctx: &mut AnalysisContext, - v: Option, + source: &Source, + positing_encoding: PositionEncoding, ) -> Option; } +/// A request handler with given (semantic) analysis context. +pub trait SemanticRequest { + /// The response type of the request. + type Response; + + /// Request the information from the given context. + fn request(self, ctx: &mut AnalysisContext) -> Option; +} + +/// A request handler with given (semantic) analysis context and a versioned +/// document. +pub trait StatefulRequest { + /// The response type of the request. + type Response; + + /// Request the information from the given context. + fn request( + self, + ctx: &mut AnalysisContext, + doc: Option, + ) -> Option; +} + +#[allow(missing_docs)] mod polymorphic { use super::prelude::*; use super::*; diff --git a/crates/tinymist-query/src/lsp_features.rs b/crates/tinymist-query/src/lsp_features.rs new file mode 100644 index 00000000..eda50340 --- /dev/null +++ b/crates/tinymist-query/src/lsp_features.rs @@ -0,0 +1,49 @@ +// todo: remove this +#![allow(missing_docs)] + +use lsp_types::{ + Registration, SemanticTokensFullOptions, SemanticTokensLegend, SemanticTokensOptions, + Unregistration, +}; +use strum::IntoEnumIterator; + +use crate::{Modifier, TokenType}; + +fn get_legend() -> SemanticTokensLegend { + SemanticTokensLegend { + token_types: TokenType::iter() + .filter(|e| *e != TokenType::None) + .map(Into::into) + .collect(), + token_modifiers: Modifier::iter().map(Into::into).collect(), + } +} + +const SEMANTIC_TOKENS_REGISTRATION_ID: &str = "semantic_tokens"; +const SEMANTIC_TOKENS_METHOD_ID: &str = "textDocument/semanticTokens"; + +pub fn get_semantic_tokens_registration(options: SemanticTokensOptions) -> Registration { + Registration { + id: SEMANTIC_TOKENS_REGISTRATION_ID.to_owned(), + method: SEMANTIC_TOKENS_METHOD_ID.to_owned(), + register_options: Some( + serde_json::to_value(options) + .expect("semantic tokens options should be representable as JSON value"), + ), + } +} + +pub fn get_semantic_tokens_unregistration() -> Unregistration { + Unregistration { + id: SEMANTIC_TOKENS_REGISTRATION_ID.to_owned(), + method: SEMANTIC_TOKENS_METHOD_ID.to_owned(), + } +} + +pub fn get_semantic_tokens_options() -> SemanticTokensOptions { + SemanticTokensOptions { + legend: get_legend(), + full: Some(SemanticTokensFullOptions::Delta { delta: Some(true) }), + ..Default::default() + } +} diff --git a/crates/tinymist-query/src/lsp_typst_boundary.rs b/crates/tinymist-query/src/lsp_typst_boundary.rs index 55db7590..9614a772 100644 --- a/crates/tinymist-query/src/lsp_typst_boundary.rs +++ b/crates/tinymist-query/src/lsp_typst_boundary.rs @@ -1,5 +1,8 @@ //! Conversions between Typst and LSP types and representations +// todo: remove this +#![allow(missing_docs)] + use lsp_types; pub type LspPosition = lsp_types::Position; diff --git a/crates/tinymist-query/src/prelude.rs b/crates/tinymist-query/src/prelude.rs index 525f7002..109775a6 100644 --- a/crates/tinymist-query/src/prelude.rs +++ b/crates/tinymist-query/src/prelude.rs @@ -25,8 +25,6 @@ pub use typst::syntax::{ LinkedNode, Source, Spanned, SyntaxKind, }; pub use typst::World; -// use typst_ts_compiler::service::WorkspaceProvider; -pub use typst_ts_compiler::TypstSystemWorld; pub use crate::analysis::{analyze_expr, AnalysisContext}; pub use crate::lsp_typst_boundary::{ diff --git a/crates/tinymist-query/src/prepare_rename.rs b/crates/tinymist-query/src/prepare_rename.rs index 87c1cb7c..7776a7fa 100644 --- a/crates/tinymist-query/src/prepare_rename.rs +++ b/crates/tinymist-query/src/prepare_rename.rs @@ -1,4 +1,6 @@ -use crate::{find_definition, prelude::*, syntax::get_deref_target, DefinitionLink, SyntaxRequest}; +use crate::{ + find_definition, prelude::*, syntax::get_deref_target, DefinitionLink, SemanticRequest, +}; use log::debug; /// The [`textDocument/prepareRename`] request is sent from the client to the @@ -26,7 +28,7 @@ pub struct PrepareRenameRequest { // todo: rename alias // todo: rename import path? -impl SyntaxRequest for PrepareRenameRequest { +impl SemanticRequest for PrepareRenameRequest { type Response = PrepareRenameResponse; fn request(self, ctx: &mut AnalysisContext) -> Option { diff --git a/crates/tinymist-query/src/references.rs b/crates/tinymist-query/src/references.rs index ca5f1620..584648a2 100644 --- a/crates/tinymist-query/src/references.rs +++ b/crates/tinymist-query/src/references.rs @@ -3,7 +3,7 @@ use log::debug; use crate::{ prelude::*, syntax::{get_deref_target, DerefTarget, IdentRef}, - SyntaxRequest, + SemanticRequest, }; /// The [`textDocument/references`] request is sent from the client to the @@ -19,7 +19,7 @@ pub struct ReferencesRequest { pub position: LspPosition, } -impl SyntaxRequest for ReferencesRequest { +impl SemanticRequest for ReferencesRequest { type Response = Vec; fn request(self, ctx: &mut AnalysisContext) -> Option { diff --git a/crates/tinymist-query/src/rename.rs b/crates/tinymist-query/src/rename.rs index 07592583..31f4ece3 100644 --- a/crates/tinymist-query/src/rename.rs +++ b/crates/tinymist-query/src/rename.rs @@ -3,7 +3,7 @@ use lsp_types::TextEdit; use crate::{ find_definition, find_references, prelude::*, syntax::get_deref_target, - validate_renaming_definition, SyntaxRequest, + validate_renaming_definition, SemanticRequest, }; /// The [`textDocument/rename`] request is sent from the client to the server to @@ -21,7 +21,7 @@ pub struct RenameRequest { pub new_name: String, } -impl SyntaxRequest for RenameRequest { +impl SemanticRequest for RenameRequest { type Response = WorkspaceEdit; fn request(self, ctx: &mut AnalysisContext) -> Option { diff --git a/crates/tinymist-query/src/selection_range.rs b/crates/tinymist-query/src/selection_range.rs index 06aa2b47..3bb00d63 100644 --- a/crates/tinymist-query/src/selection_range.rs +++ b/crates/tinymist-query/src/selection_range.rs @@ -1,23 +1,41 @@ -use crate::prelude::*; +use crate::{prelude::*, SyntaxRequest}; +/// The [`textDocument/selectionRange`] request is sent from the client to the +/// server to return suggested selection ranges at an array of given positions. +/// A selection range is a range around the cursor position which the user might +/// be interested in selecting. +/// +/// [`textDocument/selectionRange`]: https://microsoft.github.io/language-server-protocol/specification#textDocument_selectionRange +/// +/// A selection range in the return array is for the position in the provided +/// parameters at the same index. Therefore `params.positions[i]` must be +/// contained in `result[i].range`. +/// +/// # Compatibility +/// +/// This request was introduced in specification version 3.15.0. #[derive(Debug, Clone)] pub struct SelectionRangeRequest { + /// The path of the document to get selection ranges for. pub path: PathBuf, + /// The positions to get selection ranges for. pub positions: Vec, } -impl SelectionRangeRequest { - pub fn request( +impl SyntaxRequest for SelectionRangeRequest { + type Response = Vec; + + fn request( self, - source: Source, + source: &Source, position_encoding: PositionEncoding, - ) -> Option> { + ) -> Option { let mut ranges = Vec::new(); for position in self.positions { - let typst_offset = lsp_to_typst::position(position, position_encoding, &source)?; + let typst_offset = lsp_to_typst::position(position, position_encoding, source)?; let tree = LinkedNode::new(source.root()); let leaf = tree.leaf_at(typst_offset + 1)?; - ranges.push(range_for_node(&source, position_encoding, &leaf)); + ranges.push(range_for_node(source, position_encoding, &leaf)); } Some(ranges) diff --git a/crates/tinymist-query/src/semantic_tokens/mod.rs b/crates/tinymist-query/src/semantic_tokens/mod.rs index 180e5554..ccfcac82 100644 --- a/crates/tinymist-query/src/semantic_tokens/mod.rs +++ b/crates/tinymist-query/src/semantic_tokens/mod.rs @@ -1,64 +1,22 @@ use std::ops::Range; -use lsp_types::{ - Registration, SemanticToken, SemanticTokensEdit, SemanticTokensFullOptions, - SemanticTokensLegend, SemanticTokensOptions, Unregistration, -}; +use lsp_types::{SemanticToken, SemanticTokensEdit}; use parking_lot::RwLock; -use strum::IntoEnumIterator; use typst::syntax::{ast, LinkedNode, Source, SyntaxKind}; use crate::{LspPosition, PositionEncoding}; use self::delta::token_delta; use self::modifier_set::ModifierSet; -use self::typst_tokens::{Modifier, TokenType}; use self::delta::CacheInner as TokenCacheInner; mod delta; mod modifier_set; mod typst_tokens; +pub use self::typst_tokens::{Modifier, TokenType}; -pub fn get_legend() -> SemanticTokensLegend { - SemanticTokensLegend { - token_types: TokenType::iter() - .filter(|e| *e != TokenType::None) - .map(Into::into) - .collect(), - token_modifiers: Modifier::iter().map(Into::into).collect(), - } -} - -const SEMANTIC_TOKENS_REGISTRATION_ID: &str = "semantic_tokens"; -const SEMANTIC_TOKENS_METHOD_ID: &str = "textDocument/semanticTokens"; - -pub fn get_semantic_tokens_registration(options: SemanticTokensOptions) -> Registration { - Registration { - id: SEMANTIC_TOKENS_REGISTRATION_ID.to_owned(), - method: SEMANTIC_TOKENS_METHOD_ID.to_owned(), - register_options: Some( - serde_json::to_value(options) - .expect("semantic tokens options should be representable as JSON value"), - ), - } -} - -pub fn get_semantic_tokens_unregistration() -> Unregistration { - Unregistration { - id: SEMANTIC_TOKENS_REGISTRATION_ID.to_owned(), - method: SEMANTIC_TOKENS_METHOD_ID.to_owned(), - } -} - -pub fn get_semantic_tokens_options() -> SemanticTokensOptions { - SemanticTokensOptions { - legend: get_legend(), - full: Some(SemanticTokensFullOptions::Delta { delta: Some(true) }), - ..Default::default() - } -} - +/// A semantic token context providing incremental semantic tokens rendering. #[derive(Default)] pub struct SemanticTokenContext { cache: RwLock, @@ -70,6 +28,7 @@ pub struct SemanticTokenContext { } impl SemanticTokenContext { + /// Create a new semantic token context. pub fn new( position_encoding: PositionEncoding, allow_overlapping_token: bool, @@ -83,6 +42,7 @@ impl SemanticTokenContext { } } + /// Get the semantic tokens for a source. pub fn get_semantic_tokens_full(&self, source: &Source) -> (Vec, String) { let root = LinkedNode::new(source.root()); @@ -98,6 +58,7 @@ impl SemanticTokenContext { (output, result_id) } + /// Get the semantic tokens delta for a source. pub fn try_semantic_tokens_delta_from_result_id( &self, source: &Source, @@ -292,7 +253,7 @@ impl Tokenizer { } #[derive(Clone, Default)] -pub struct Token { +struct Token { pub token_type: TokenType, pub modifiers: ModifierSet, pub range: Range, diff --git a/crates/tinymist-query/src/semantic_tokens/typst_tokens.rs b/crates/tinymist-query/src/semantic_tokens/typst_tokens.rs index 362255fa..b917d8b3 100644 --- a/crates/tinymist-query/src/semantic_tokens/typst_tokens.rs +++ b/crates/tinymist-query/src/semantic_tokens/typst_tokens.rs @@ -1,5 +1,8 @@ //! Types for tokens used for Typst syntax +// todo: remove this +#![allow(missing_docs)] + use lsp_types::{SemanticTokenModifier, SemanticTokenType}; use strum::EnumIter; diff --git a/crates/tinymist-query/src/semantic_tokens_delta.rs b/crates/tinymist-query/src/semantic_tokens_delta.rs index 63c997ee..23025275 100644 --- a/crates/tinymist-query/src/semantic_tokens_delta.rs +++ b/crates/tinymist-query/src/semantic_tokens_delta.rs @@ -1,12 +1,29 @@ use crate::{prelude::*, SemanticTokenContext}; +/// The [`textDocument/semanticTokens/full/delta`] request is sent from the +/// client to the server to resolve the semantic tokens of a given file, +/// **returning only the delta**. +/// +/// [`textDocument/semanticTokens/full/delta`]: https://microsoft.github.io/language-server-protocol/specification#textDocument_semanticTokens +/// +/// Similar to [`semantic_tokens_full`](Self::semantic_tokens_full), except it +/// returns a sequence of [`SemanticTokensEdit`] to transform a previous result +/// into a new result. +/// +/// # Compatibility +/// +/// This request was introduced in specification version 3.16.0. #[derive(Debug, Clone)] pub struct SemanticTokensDeltaRequest { + /// The path of the document to get semantic tokens for. pub path: PathBuf, + /// The previous result id to compute the delta from. pub previous_result_id: String, } impl SemanticTokensDeltaRequest { + /// Handles the request to compute the semantic tokens delta for a given + /// document. pub fn request( self, ctx: &SemanticTokenContext, diff --git a/crates/tinymist-query/src/semantic_tokens_full.rs b/crates/tinymist-query/src/semantic_tokens_full.rs index dc3a2ba8..5fc649e7 100644 --- a/crates/tinymist-query/src/semantic_tokens_full.rs +++ b/crates/tinymist-query/src/semantic_tokens_full.rs @@ -1,11 +1,29 @@ use crate::{prelude::*, SemanticTokenContext}; +/// The [`textDocument/semanticTokens/full`] request is sent from the client to +/// the server to resolve the semantic tokens of a given file. +/// +/// [`textDocument/semanticTokens/full`]: https://microsoft.github.io/language-server-protocol/specification#textDocument_semanticTokens +/// +/// Semantic tokens are used to add additional color information to a file that +/// depends on language specific symbol information. A semantic token request +/// usually produces a large result. The protocol therefore supports encoding +/// tokens with numbers. In addition, optional support for deltas is available, +/// i.e. [`semantic_tokens_full_delta`]. +/// +/// [`semantic_tokens_full_delta`]: Self::semantic_tokens_full_delta +/// +/// # Compatibility +/// +/// This request was introduced in specification version 3.16.0. #[derive(Debug, Clone)] pub struct SemanticTokensFullRequest { + /// The path of the document to get semantic tokens for. pub path: PathBuf, } impl SemanticTokensFullRequest { + /// Handles the request to compute the semantic tokens for a given document. pub fn request( self, ctx: &SemanticTokenContext, diff --git a/crates/tinymist-query/src/signature_help.rs b/crates/tinymist-query/src/signature_help.rs index 9c057248..f46ea14b 100644 --- a/crates/tinymist-query/src/signature_help.rs +++ b/crates/tinymist-query/src/signature_help.rs @@ -1,12 +1,18 @@ -use crate::{prelude::*, SyntaxRequest}; +use crate::{prelude::*, SemanticRequest}; +/// The [`textDocument/signatureHelp`] request is sent from the client to the +/// server to request signature information at a given cursor position. +/// +/// [`textDocument/signatureHelp`]: https://microsoft.github.io/language-server-protocol/specification#textDocument_signatureHelp #[derive(Debug, Clone)] pub struct SignatureHelpRequest { + /// The path of the document to get signature help for. pub path: PathBuf, + /// The position of the cursor to get signature help for. pub position: LspPosition, } -impl SyntaxRequest for SignatureHelpRequest { +impl SemanticRequest for SignatureHelpRequest { type Response = SignatureHelp; fn request(self, ctx: &mut AnalysisContext) -> Option { @@ -20,7 +26,7 @@ impl SyntaxRequest for SignatureHelpRequest { return None; } - let values = analyze_expr(ctx.world, &callee_node); + let values = analyze_expr(ctx.world(), &callee_node); let function = values.into_iter().find_map(|v| match v.0 { Value::Func(f) => Some(f), diff --git a/crates/tinymist-query/src/symbol.rs b/crates/tinymist-query/src/symbol.rs index 2913c39f..9d9c6499 100644 --- a/crates/tinymist-query/src/symbol.rs +++ b/crates/tinymist-query/src/symbol.rs @@ -1,17 +1,34 @@ -use typst_ts_compiler::NotifyApi; - use crate::{ prelude::*, syntax::{get_lexical_hierarchy, LexicalHierarchy, LexicalScopeKind}, - SyntaxRequest, + SemanticRequest, }; +/// The [`workspace/symbol`] request is sent from the client to the server to +/// list project-wide symbols matching the given query string. +/// +/// [`workspace/symbol`]: https://microsoft.github.io/language-server-protocol/specification#workspace_symbol +/// +/// # Compatibility +/// +/// Since 3.17.0, servers can also provider a handler for +/// [`workspaceSymbol/resolve`] requests. This allows servers to return +/// workspace symbols without a range for a `workspace/symbol` request. Clients +/// then need to resolve the range when necessary using the `workspaceSymbol/ +/// resolve` request. +/// +/// [`workspaceSymbol/resolve`]: Self::symbol_resolve +/// +/// Servers can only use this new model if clients advertise support for it via +/// the `workspace.symbol.resolve_support` capability. #[derive(Debug, Clone)] pub struct SymbolRequest { + /// The query string to filter symbols by. It is usually the exact content + /// of the user's input box in the UI. pub pattern: Option, } -impl SyntaxRequest for SymbolRequest { +impl SemanticRequest for SymbolRequest { type Response = Vec; fn request(self, ctx: &mut AnalysisContext) -> Option { @@ -19,7 +36,7 @@ impl SyntaxRequest for SymbolRequest { let mut symbols = vec![]; - ctx.world.iter_dependencies(&mut |path, _| { + ctx.resources.iter_dependencies(&mut |path, _| { let Ok(source) = ctx.source_by_path(path) else { return; }; diff --git a/crates/tinymist-query/src/syntax/lexical_hierarchy.rs b/crates/tinymist-query/src/syntax/lexical_hierarchy.rs index 7964c2e8..98e94beb 100644 --- a/crates/tinymist-query/src/syntax/lexical_hierarchy.rs +++ b/crates/tinymist-query/src/syntax/lexical_hierarchy.rs @@ -7,7 +7,6 @@ use anyhow::{anyhow, Context}; use ecow::{eco_vec, EcoVec}; use log::info; use lsp_types::SymbolKind; -use reflexo::error::prelude::*; use serde::{Deserialize, Serialize}; use typst::{ syntax::{ @@ -654,7 +653,7 @@ impl LexicalHierarchyWorker { let name = if e.starts_with('@') { let spec = e .parse::() - .context("parse package spec failed for name")?; + .map_err(|e| anyhow!("parse package spec failed: {:?}", e))?; spec.name.to_string() } else { let e = Path::new(e.as_ref()) diff --git a/crates/tinymist-query/src/syntax/mod.rs b/crates/tinymist-query/src/syntax/mod.rs index 63ef6367..ee1312f3 100644 --- a/crates/tinymist-query/src/syntax/mod.rs +++ b/crates/tinymist-query/src/syntax/mod.rs @@ -1,10 +1,17 @@ -pub mod import; +//! Analyzing the syntax of a source file. +//! +//! This module must hide all **AST details** from the rest of the codebase. + +// todo: remove this +#![allow(missing_docs)] + +pub(crate) mod import; pub use import::*; -pub mod lexical_hierarchy; -pub(crate) use lexical_hierarchy::*; -pub mod matcher; +pub(crate) mod lexical_hierarchy; +pub use lexical_hierarchy::*; +pub(crate) mod matcher; pub use matcher::*; -pub mod module; +pub(crate) mod module; pub use module::*; use core::fmt; @@ -70,7 +77,7 @@ impl<'de> Deserialize<'de> for IdentRef { .split("..") .map(|s| { s.parse().map_err(|e| { - serde::de::Error::custom(format!("failed to parse range: {}", e)) + serde::de::Error::custom(format!("failed to parse range: {e}")) }) }) .collect::, _>>()?; diff --git a/crates/tinymist-query/src/syntax/module.rs b/crates/tinymist-query/src/syntax/module.rs index 3b6af4f1..51f22293 100644 --- a/crates/tinymist-query/src/syntax/module.rs +++ b/crates/tinymist-query/src/syntax/module.rs @@ -7,11 +7,18 @@ use crate::prelude::AnalysisContext; use super::find_imports; +/// The dependency information of a module (file). pub struct ModuleDependency { + /// The dependencies of this module. pub dependencies: EcoVec, + /// The dependents of this module. pub dependents: EcoVec, } +/// Construct the module dependencies of the given context. +/// +/// It will scan all the files in the context, using [`AnalysisContext::files`], +/// and find the dependencies and dependents of each file. pub fn construct_module_dependencies( ctx: &mut AnalysisContext, ) -> HashMap { @@ -31,7 +38,7 @@ pub fn construct_module_dependencies( }; let file_id = source.id(); - let deps = find_imports(ctx.world, &source); + let deps = find_imports(ctx.world(), &source); dependencies .entry(file_id) .or_insert_with(|| ModuleDependency { @@ -55,6 +62,9 @@ pub fn construct_module_dependencies( dependencies } +/// Scan the files in the workspace and return the file ids. +/// +/// Note: this function will touch the physical file system. pub fn scan_workspace_files(root: &Path) -> Vec { let mut res = vec![]; for path in walkdir::WalkDir::new(root).follow_links(false).into_iter() { diff --git a/crates/tinymist-query/src/tests.rs b/crates/tinymist-query/src/tests.rs index 46989e5b..56294c4b 100644 --- a/crates/tinymist-query/src/tests.rs +++ b/crates/tinymist-query/src/tests.rs @@ -9,21 +9,44 @@ use serde::Serialize; use serde_json::{ser::PrettyFormatter, Serializer, Value}; use typst::syntax::{ ast::{self, AstNode}, - LinkedNode, Source, SyntaxKind, VirtualPath, + FileId as TypstFileId, LinkedNode, Source, SyntaxKind, VirtualPath, }; +use typst::{diag::PackageError, foundations::Bytes}; use typst_ts_compiler::{ - service::{CompileDriver, Compiler, WorkspaceProvider}, - ShadowApi, + service::{CompileDriver, Compiler, EntryManager}, + NotifyApi, ShadowApi, }; -use typst_ts_core::{config::CompileOpts, Bytes, TypstFileId}; +use typst_ts_core::{ + config::compiler::{EntryOpts, EntryState}, + package::Registry, +}; +use typst_ts_core::{config::CompileOpts, package::PackageSpec}; pub use insta::assert_snapshot; pub use typst_ts_compiler::TypstSystemWorld; use crate::{ - analysis::Analysis, prelude::AnalysisContext, typst_to_lsp, LspPosition, PositionEncoding, + analysis::{Analysis, AnaylsisResources}, + prelude::AnalysisContext, + typst_to_lsp, LspPosition, PositionEncoding, }; +struct WrapWorld<'a>(&'a mut TypstSystemWorld); + +impl<'a> AnaylsisResources for WrapWorld<'a> { + fn world(&self) -> &dyn typst::World { + self.0 + } + + fn resolve(&self, spec: &PackageSpec) -> Result, PackageError> { + self.0.registry.resolve(spec) + } + + fn iter_dependencies(&self, f: &mut dyn FnMut(&reflexo::ImmutPath, typst_ts_compiler::Time)) { + self.0.iter_dependencies(f) + } +} + pub fn snapshot_testing(name: &str, f: &impl Fn(&mut AnalysisContext, PathBuf)) { let mut settings = insta::Settings::new(); settings.set_prepend_module_to_snapshot(false); @@ -36,20 +59,19 @@ pub fn snapshot_testing(name: &str, f: &impl Fn(&mut AnalysisContext, PathBuf)) let contents = contents.replace("\r\n", "\n"); run_with_sources(&contents, |w: &mut TypstSystemWorld, p| { + let root = w.workspace_root().unwrap(); let paths = w .shadow_paths() .into_iter() .map(|p| { - TypstFileId::new( - None, - VirtualPath::new(p.strip_prefix(w.workspace_root()).unwrap()), - ) + TypstFileId::new(None, VirtualPath::new(p.strip_prefix(&root).unwrap())) }) .collect::>(); + let w = WrapWorld(w); let mut ctx = AnalysisContext::new( - w, + &w, Analysis { - root: w.workspace_root(), + root, position_encoding: PositionEncoding::Utf16, }, ); @@ -67,7 +89,7 @@ pub fn run_with_sources(source: &str, f: impl FnOnce(&mut TypstSystemWorld, P PathBuf::from("/") }; let mut world = TypstSystemWorld::new(CompileOpts { - root_dir: root.clone(), + entry: EntryOpts::new_rooted(root.as_path().into(), None), ..Default::default() }) .unwrap(); @@ -99,15 +121,21 @@ pub fn run_with_sources(source: &str, f: impl FnOnce(&mut TypstSystemWorld, P last_pw = Some(pw); } - world.set_main_id(TypstFileId::new(None, VirtualPath::new("/main.typ"))); + world.mutate_entry(EntryState::new_detached()).unwrap(); let mut driver = CompileDriver::new(world); let _ = driver.compile(&mut Default::default()); let pw = last_pw.unwrap(); - driver.world_mut().set_main_id(TypstFileId::new( - None, - VirtualPath::new(pw.strip_prefix(root).unwrap()), - )); + driver + .world_mut() + .mutate_entry(EntryState::new_rooted( + root.as_path().into(), + Some(TypstFileId::new( + None, + VirtualPath::new(pw.strip_prefix(root).unwrap()), + )), + )) + .unwrap(); f(driver.world_mut(), pw) } diff --git a/crates/tinymist/Cargo.toml b/crates/tinymist/Cargo.toml index 52a0eff3..549b6615 100644 --- a/crates/tinymist/Cargo.toml +++ b/crates/tinymist/Cargo.toml @@ -39,7 +39,7 @@ typst.workspace = true typst-pdf.workspace = true typst-assets = { workspace = true, features = ["fonts"] } -typst-ts-core = { version = "0.4.2-rc6", default-features = false, features = [ +typst-ts-core = { workspace = true, default-features = false, features = [ "flat-vector", "vector-bbox", "no-content-hint", diff --git a/crates/tinymist/src/actor.rs b/crates/tinymist/src/actor.rs index 9f4cb293..e315b657 100644 --- a/crates/tinymist/src/actor.rs +++ b/crates/tinymist/src/actor.rs @@ -5,28 +5,22 @@ pub mod compile; pub mod render; pub mod typst; -use std::{borrow::Cow, path::PathBuf}; - use ::typst::diag::FileResult; use tokio::sync::{broadcast, watch}; use typst_ts_compiler::vfs::notify::FileChangeSet; -use typst_ts_core::{config::CompileOpts, ImmutPath}; +use typst_ts_core::config::compiler::EntryState; use self::{ render::{PdfExportActor, PdfExportConfig}, - typst::{create_server, CompileActor, OptsState}, + typst::{create_server, CompileActor}, }; use crate::TypstLanguageServer; impl TypstLanguageServer { - pub fn server(&self, name: String, entry: Option) -> CompileActor { + pub fn server(&self, name: String, entry: EntryState) -> CompileActor { let (doc_tx, doc_rx) = watch::channel(None); let (render_tx, _) = broadcast::channel(10); - // todo: don't ignore entry from typst_extra_args - // entry: command.input, - let root_dir = self.config.determine_root(entry.as_ref()); - // Run the PDF export actor before preparing cluster to avoid loss of events tokio::spawn( PdfExportActor::new( @@ -34,36 +28,14 @@ impl TypstLanguageServer { render_tx.subscribe(), PdfExportConfig { substitute_pattern: self.config.output_path.clone(), - root: root_dir.clone(), - path: entry.clone().map(From::from), + entry: entry.clone(), mode: self.config.export_pdf, }, ) .run(), ); - let opts = { - let mut opts = self.compile_opts.clone(); - - if let Some(extras) = &self.config.typst_extra_args { - if let Some(inputs) = extras.inputs.as_ref() { - if opts.inputs.is_empty() { - opts.inputs = inputs.clone(); - } - } - if !extras.font_paths.is_empty() && opts.font_paths.is_empty() { - opts.font_paths = extras.font_paths.clone(); - } - } - - move |root_dir: PathBuf| CompileOpts { - root_dir, - // todo: additional inputs - with_embedded_fonts: typst_assets::fonts().map(Cow::Borrowed).collect(), - ..opts - } - }; - + // Take all dirty files in memory as the initial snapshot let snapshot = FileChangeSet::new_inserts( self.memory_changes .iter() @@ -74,11 +46,12 @@ impl TypstLanguageServer { .collect(), ); + // Create the server create_server( name, + &self.config, self.const_config(), - OptsState::new(root_dir.clone(), opts), - root_dir, + self.font.clone(), entry, snapshot, self.diag_tx.clone(), diff --git a/crates/tinymist/src/actor/compile.rs b/crates/tinymist/src/actor/compile.rs index ed261c23..b186c167 100644 --- a/crates/tinymist/src/actor/compile.rs +++ b/crates/tinymist/src/actor/compile.rs @@ -23,18 +23,31 @@ use typst_ts_compiler::{ ShadowApi, }; use typst_ts_core::{ + config::compiler::EntryState, debug_loc::{SourceLocation, SourceSpanOffset}, error::prelude::{map_string_err, ZResult}, - ImmutPath, TypstDocument, TypstFileId, + TypstDocument, TypstFileId, }; use typst_ts_compiler::service::{ - features::FeatureSet, CompileEnv, CompileReporter, Compiler, ConsoleDiagReporter, - WorkspaceProvider, WorldExporter, + features::FeatureSet, CompileEnv, CompileReporter, Compiler, ConsoleDiagReporter, EntryManager, }; use crate::{task::BorrowTask, utils}; +pub trait EntryStateExt { + fn is_inactive(&self) -> bool; +} + +impl EntryStateExt for EntryState { + fn is_inactive(&self) -> bool { + matches!( + self, + EntryState::Detached | EntryState::Workspace { main: None, .. } + ) + } +} + /// Interrupts for external sources enum ExternalInterrupt { /// Compile anyway. @@ -89,8 +102,6 @@ struct SuspendState { pub struct CompileActor { /// The underlying compiler. pub compiler: CompileReporter, - /// The root path of the workspace. - pub root: ImmutPath, /// Whether to enable file system watching. pub enable_watch: bool, @@ -117,16 +128,11 @@ pub struct CompileActor { suspend_state: SuspendState, } -impl CompileActor +impl CompileActor where C::World: for<'files> codespan_reporting::files::Files<'files, FileId = TypstFileId>, { - pub fn new_with_features( - compiler: C, - root: ImmutPath, - entry: Option, - feature_set: FeatureSet, - ) -> Self { + pub fn new_with_features(compiler: C, entry: EntryState, feature_set: FeatureSet) -> Self { let (steal_send, steal_recv) = mpsc::unbounded_channel(); let watch_feature_set = Arc::new( @@ -138,7 +144,6 @@ where Self { compiler: CompileReporter::new(compiler) .with_generic_reporter(ConsoleDiagReporter::default()), - root, logical_tick: 1, enable_watch: false, @@ -154,15 +159,15 @@ where steal_recv, suspend_state: SuspendState { - suspended: entry.is_none(), + suspended: entry.is_inactive(), dirty: false, }, } } /// Create a new compiler thread. - pub fn new(compiler: C, root: ImmutPath, entry: Option) -> Self { - Self::new_with_features(compiler, root, entry, FeatureSet::default()) + pub fn new(compiler: C, entry: EntryState) -> Self { + Self::new_with_features(compiler, entry, FeatureSet::default()) } pub fn success_doc(&self) -> Option { @@ -327,8 +332,8 @@ where Some(compile_thread) } - pub(crate) fn change_entry(&mut self, entry: Option>) { - let suspending = entry.is_none(); + pub(crate) fn change_entry(&mut self, entry: EntryState) { + let suspending = entry.is_inactive(); if suspending { self.suspend_state.suspended = true; } else { @@ -530,7 +535,7 @@ impl CompileActor { self, CompileClient { intr_tx: steal_send, - _ctx: typst_ts_core::PhantomParamData::default(), + _ctx: std::marker::PhantomData, }, ) } @@ -544,7 +549,7 @@ impl CompileActor { pub struct CompileClient { intr_tx: mpsc::UnboundedSender>, - _ctx: typst_ts_core::PhantomParamData, + _ctx: std::marker::PhantomData, } unsafe impl Send for CompileClient {} @@ -555,7 +560,7 @@ impl CompileClient { let (intr_tx, _) = mpsc::unbounded_channel(); Self { intr_tx, - _ctx: typst_ts_core::PhantomParamData::default(), + _ctx: std::marker::PhantomData, } } } @@ -627,7 +632,7 @@ pub struct DocToSrcJumpInfo { // todo: remove constraint to CompilerWorld impl>> CompileClient> where - Ctx::World: WorkspaceProvider, + Ctx::World: EntryManager, { /// fixme: character is 0-based, UTF-16 code unit. /// We treat it as UTF-8 now. @@ -643,7 +648,7 @@ where let world = this.compiler.world(); let relative_path = filepath - .strip_prefix(&this.compiler.world().workspace_root()) + .strip_prefix(&this.compiler.world().workspace_root()?) .ok()?; let source_id = TypstFileId::new(None, VirtualPath::new(relative_path)); @@ -666,7 +671,7 @@ where let filepath = Path::new(&loc.filepath); let relative_path = filepath - .strip_prefix(&this.compiler.world().workspace_root()) + .strip_prefix(&this.compiler.world().workspace_root()?) .ok()?; let source_id = TypstFileId::new(None, VirtualPath::new(relative_path)); diff --git a/crates/tinymist/src/actor/render.rs b/crates/tinymist/src/actor/render.rs index 13eb1cf0..9d89826c 100644 --- a/crates/tinymist/src/actor/render.rs +++ b/crates/tinymist/src/actor/render.rs @@ -13,7 +13,7 @@ use tokio::sync::{ oneshot, watch, }; use typst::foundations::Smart; -use typst_ts_core::{path::PathClean, ImmutPath, TypstDocument}; +use typst_ts_core::{config::compiler::EntryState, path::PathClean, ImmutPath, TypstDocument}; use crate::ExportPdfMode; @@ -29,15 +29,13 @@ pub enum RenderActorRequest { #[derive(Debug, Clone)] pub struct PdfPathVars { - pub root: Option, - pub path: Option, + pub entry: EntryState, } #[derive(Debug, Clone, Default)] pub struct PdfExportConfig { pub substitute_pattern: String, - pub root: Option, - pub path: Option, + pub entry: EntryState, pub mode: ExportPdfMode, } @@ -46,8 +44,7 @@ pub struct PdfExportActor { document: watch::Receiver>>, pub substitute_pattern: String, - pub root: Option, - pub path: Option, + pub entry: EntryState, pub mode: ExportPdfMode, } @@ -61,8 +58,7 @@ impl PdfExportActor { render_rx, document, substitute_pattern: config.substitute_pattern, - root: config.root, - path: config.path, + entry: config.entry, mode: config.mode, } } @@ -88,13 +84,11 @@ impl PdfExportActor { match req { RenderActorRequest::ChangeConfig(cfg) => { self.substitute_pattern = cfg.substitute_pattern; - self.root = cfg.root; - self.path = cfg.path; + self.entry = cfg.entry; self.mode = cfg.mode; } RenderActorRequest::ChangeExportPath(cfg) => { - self.root = cfg.root; - self.path = cfg.path; + self.entry = cfg.entry; } _ => { let sender = match &req { @@ -132,23 +126,36 @@ impl PdfExportActor { _ => unreachable!(), }; + // pub entry: EntryState, + let root = self.entry.root(); + let main = self.entry.main(); + info!( - "PdfRenderActor: check path {:?} with output directory {}", - self.path, self.substitute_pattern + "PdfRenderActor: check path {:?} and root {:?} with output directory {}", + main, root, self.substitute_pattern ); - if let Some((root, path)) = self.root.as_ref().zip(self.path.as_ref()) { - let should_do = matches!(req, RenderActorRequest::DoExport(..)); - let should_do = should_do || get_mode(self.mode) == eq_mode; - let should_do = should_do || validate_document(&req, self.mode, &document); - if should_do { - return match self.export_pdf(&document, root, path).await { - Ok(pdf) => Some(pdf), - Err(err) => { - error!("PdfRenderActor: failed to export PDF: {err}", err = err); - None - } - }; - } + + let root = root?; + let main = main?; + + // todo: package?? + if main.package().is_some() { + return None; + } + + let path = main.vpath().resolve(&root)?; + + let should_do = matches!(req, RenderActorRequest::DoExport(..)); + let should_do = should_do || get_mode(self.mode) == eq_mode; + let should_do = should_do || validate_document(&req, self.mode, &document); + if should_do { + return match self.export_pdf(&document, &root, &path).await { + Ok(pdf) => Some(pdf), + Err(err) => { + error!("PdfRenderActor: failed to export PDF: {err}", err = err); + None + } + }; } fn get_mode(mode: ExportPdfMode) -> ExportPdfMode { diff --git a/crates/tinymist/src/actor/typst.rs b/crates/tinymist/src/actor/typst.rs index 962a80d1..486eb96b 100644 --- a/crates/tinymist/src/actor/typst.rs +++ b/crates/tinymist/src/actor/typst.rs @@ -1,49 +1,46 @@ //! The typst actors running compilations. -use core::fmt; use std::{ path::{Path, PathBuf}, sync::Arc, }; use anyhow::anyhow; -use log::{debug, error, info, trace, warn}; -use once_cell::sync::OnceCell; +use log::{error, info, trace}; use parking_lot::Mutex; use tinymist_query::{ - analysis::{Analysis, AnalysisContext}, + analysis::{Analysis, AnalysisContext, AnaylsisResources}, CompilerQueryRequest, CompilerQueryResponse, DiagnosticsMap, FoldRequestFeature, - OnExportRequest, OnSaveExportRequest, PositionEncoding, StatefulRequest, SyntaxRequest, + OnExportRequest, OnSaveExportRequest, PositionEncoding, SemanticRequest, StatefulRequest, VersionedDocument, }; use tokio::sync::{broadcast, mpsc, oneshot, watch}; use typst::{ diag::{SourceDiagnostic, SourceResult}, - syntax::VirtualPath, util::Deferred, }; #[cfg(feature = "preview")] use typst_preview::{CompilationHandle, CompilationHandleImpl, CompileStatus}; use typst_ts_compiler::{ - service::{ - CompileDriver as CompileDriverInner, CompileExporter, CompileMiddleware, Compiler, - EnvWorld, WorkspaceProvider, WorldExporter, - }, + service::{CompileDriverImpl, CompileEnv, CompileMiddleware, Compiler, EntryManager, EnvWorld}, vfs::notify::{FileChangeSet, MemoryEvent}, - TypstSystemWorld, }; use typst_ts_core::{ - config::CompileOpts, error::prelude::*, typst::prelude::EcoVec, Error, ImmutPath, + config::compiler::EntryState, error::prelude::*, typst::prelude::EcoVec, Error, ImmutPath, TypstDocument, TypstWorld, }; use super::compile::CompileClient as TsCompileClient; use super::{compile::CompileActor as CompileActorInner, render::PdfExportConfig}; -use crate::ConstConfig; +use crate::{actor::compile::EntryStateExt, ConstConfig}; use crate::{ actor::render::{PdfPathVars, RenderActorRequest}, utils, }; +use crate::{ + world::{LspWorld, LspWorldBuilder, SharedFontResolver}, + Config, +}; #[cfg(not(feature = "preview"))] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] @@ -60,34 +57,20 @@ pub trait CompilationHandle: Send + 'static { fn notify_compile(&self, res: Result, CompileStatus>); } -type CompileService = CompileActorInner, H>>; -type CompileClient = TsCompileClient>; +type CompileDriverInner = CompileDriverImpl; +type CompileService = CompileActorInner; +type CompileClient = TsCompileClient; type DiagnosticsSender = mpsc::UnboundedSender<(String, Option)>; -pub enum OptsState { - Exact(CompileOpts), - Rootless(Box CompileOpts + Send + Sync>), -} -impl OptsState { - pub(crate) fn new( - root: Option>, - opts: impl FnOnce(PathBuf) -> CompileOpts + Send + Sync + 'static, - ) -> OptsState { - match root { - Some(root) => OptsState::Exact(opts(root.as_ref().to_owned())), - None => OptsState::Rootless(Box::new(opts)), - } - } -} - #[allow(clippy::too_many_arguments)] pub fn create_server( diag_group: String, + config: &Config, cfg: &ConstConfig, - opts: OptsState, - root: Option, - entry: Option, + // opts: OptsState, + font_resolver: Deferred, + entry: EntryState, snapshot: FileChangeSet, diag_tx: DiagnosticsSender, doc_sender: watch::Sender>>, @@ -95,14 +78,6 @@ pub fn create_server( ) -> CompileActor { let pos_encoding = cfg.position_encoding; - info!( - "TypstActor: creating server for {} with opts(determined={:?})", - diag_group, - matches!(opts, OptsState::Exact(_)) - ); - - let (root_tx, root_rx) = oneshot::channel(); - let inner = Deferred::new({ let current_runtime = tokio::runtime::Handle::current(); let handler = CompileHandler { @@ -115,63 +90,31 @@ pub fn create_server( let render_tx = render_tx.clone(); move || { - let opts = match opts { - OptsState::Exact(opts) => opts, - OptsState::Rootless(opts) => { - let root: ImmutPath = match utils::threaded_receive(root_rx) { - Ok(Some(root)) => root, - Ok(None) => { - error!("TypstActor: failed to receive root path: root is none"); - return CompileClient::faked(); - } - Err(err) => { - error!("TypstActor: failed to receive root path: {:#}", err); - return CompileClient::faked(); - } - }; + info!("TypstActor: creating server for {diag_group}"); - opts(root.as_ref().into()) - } - }; + let font_resolver = font_resolver.wait().clone(); - let root: ImmutPath = opts.root_dir.as_path().into(); - - info!( - "TypstActor: creating server for {} with arguments {:#?}", - diag_group, - ShowOpts(&opts) - ); - - // todo: entry is PathBuf, which is inefficient - let compiler_driver = CompileDriver::new(opts, entry.clone(), handler); - let handler: CompileHandler = compiler_driver.handler.clone(); - - let ontyped_render_tx = render_tx.clone(); - let driver = CompileExporter::new(compiler_driver).with_exporter(Box::new( - move |_w: &dyn TypstWorld, doc| { - let _ = doc_sender.send(Some(doc)); - // todo: is it right that ignore zero broadcast receiver? - let _ = ontyped_render_tx.send(RenderActorRequest::OnTyped); - - Ok(()) - }, - )); - let driver = Reporter { + let world = + LspWorldBuilder::build(entry.clone(), font_resolver).expect("incorrect options"); + let driver = CompileDriverInner::new(world); + let driver = CompileDriver { + inner: driver, + handler, + doc_sender, + render_tx: render_tx.clone(), diag_group: diag_group.clone(), position_encoding: pos_encoding, diag_tx, - inner: driver, - cb: handler.clone(), }; - let driver = - CompileActorInner::new(driver, root.clone(), entry.clone()).with_watch(true); - let (server, client) = driver.split(); + let actor = CompileActorInner::new(driver, entry).with_watch(true); + let (server, client) = actor.split(); // We do send memory changes instead of initializing compiler with them. // This is because there are state recorded inside of the compiler actor, and we // must update them. client.add_memory_changes(MemoryEvent::Update(snapshot)); + current_runtime.spawn(server.spawn()); client @@ -180,8 +123,7 @@ pub fn create_server( CompileActor::new( diag_group, - root_tx, - root, + config.clone(), entry, pos_encoding, inner, @@ -189,20 +131,6 @@ pub fn create_server( ) } -struct ShowOpts<'a>(&'a CompileOpts); - -impl<'a> fmt::Debug for ShowOpts<'a> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("CompileOpts") - .field("root_dir", &self.0.root_dir) - .field("entry", &self.0.entry) - .field("inputs", &self.0.inputs) - .field("font_paths", &self.0.font_paths) - .field("no_system_fonts", &self.0.no_system_fonts) - .finish() - } -} - macro_rules! query_state { ($self:ident, $method:ident, $req:expr) => {{ let res = $self.steal_state(move |w, doc| $req.request(w, doc)); @@ -247,7 +175,15 @@ impl CompilationHandle for CompileHandler { pub struct CompileDriver { inner: CompileDriverInner, + #[allow(unused)] handler: CompileHandler, + + doc_sender: watch::Sender>>, + render_tx: broadcast::Sender, + + diag_group: String, + position_encoding: PositionEncoding, + diag_tx: DiagnosticsSender, } impl CompileMiddleware for CompileDriver { @@ -260,90 +196,26 @@ impl CompileMiddleware for CompileDriver { fn inner_mut(&mut self) -> &mut Self::Compiler { &mut self.inner } -} -impl CompileDriver { - pub fn new(opts: CompileOpts, entry: Option, handler: CompileHandler) -> Self { - let world = TypstSystemWorld::new(opts).expect("incorrect options"); - let driver = CompileDriverInner::new(world); - - let mut this = Self { - inner: driver, - handler, - }; - - if let Some(entry) = entry { - this.set_entry_file(entry.as_ref().to_owned()); - } - - this - } - - // todo: determine root - fn set_entry_file(&mut self, entry: PathBuf) { - // let candidates = self - // .current - // .iter() - // .filter_map(|(root, package)| Some((root, - // package.uri_to_vpath(uri).ok()?))) .inspect(|(package_root, - // path)| trace!(%package_root, ?path, %uri, "considering - // candidate for full id")); - - // // Our candidates are projects containing a URI, so we expect to get - // a set of // subdirectories. The "best" is the "most - // specific", that is, the project that is a // subdirectory of - // the rest. This should have the longest length. - // let (best_package_root, best_path) = - // candidates.max_by_key(|(_, path)| - // path.as_rootless_path().components().count())?; - - // let package_id = PackageId::new_current(best_package_root.clone()); - // let full_file_id = FullFileId::new(package_id, best_path); - - self.inner.set_entry_file(entry); - } -} - -pub struct Reporter { - diag_group: String, - position_encoding: PositionEncoding, - diag_tx: DiagnosticsSender, - inner: C, - #[allow(unused)] - cb: H, -} - -impl, H> CompileMiddleware for Reporter -where - H: CompilationHandle, -{ - type Compiler = C; - - fn inner(&self) -> &Self::Compiler { - &self.inner - } - - fn inner_mut(&mut self) -> &mut Self::Compiler { - &mut self.inner - } - - fn wrap_compile( - &mut self, - env: &mut typst_ts_compiler::service::CompileEnv, - ) -> SourceResult> { + fn wrap_compile(&mut self, env: &mut CompileEnv) -> SourceResult> { #[cfg(feature = "preview")] - self.cb.status(CompileStatus::Compiling); + self.handler.status(CompileStatus::Compiling); match self.inner_mut().compile(env) { Ok(doc) => { #[cfg(feature = "preview")] - self.cb.notify_compile(Ok(doc.clone())); + self.handler.notify_compile(Ok(doc.clone())); + + let _ = self.doc_sender.send(Some(doc.clone())); + // todo: is it right that ignore zero broadcast receiver? + let _ = self.render_tx.send(RenderActorRequest::OnTyped); self.notify_diagnostics(EcoVec::new()); Ok(doc) } Err(err) => { #[cfg(feature = "preview")] - self.cb.notify_compile(Err(CompileStatus::CompileError)); + self.handler + .notify_compile(Err(CompileStatus::CompileError)); self.notify_diagnostics(err); Err(EcoVec::new()) @@ -352,13 +224,7 @@ where } } -impl WorldExporter for Reporter { - fn export(&mut self, output: Arc) -> SourceResult<()> { - self.inner.export(output) - } -} - -impl, H> Reporter { +impl CompileDriver { fn push_diagnostics(&mut self, diagnostics: Option) { let err = self.diag_tx.send((self.diag_group.clone(), diagnostics)); if let Err(err) = err { @@ -370,8 +236,17 @@ impl, H> Reporter { trace!("notify diagnostics: {:#?}", diagnostics); // todo encoding + let w = self.inner.world_mut(); + // todo: root + let root = w.entry.root().clone().unwrap(); let diagnostics = tinymist_query::convert_diagnostics( - self.inner.world(), + &AnalysisContext::new( + &WrapWorld(w), + Analysis { + root, + position_encoding: self.position_encoding, + }, + ), diagnostics.as_ref(), self.position_encoding, ); @@ -379,8 +254,8 @@ impl, H> Reporter { // todo: better way to remove diagnostics // todo: check all errors in this file - let main = self.inner.world().main; - let valid = main.is_some_and(|e| e.vpath() != &VirtualPath::new("detached.typ")); + let detached = self.inner.world().entry.is_inactive(); + let valid = !detached; self.push_diagnostics(valid.then_some(diagnostics)); } @@ -389,10 +264,11 @@ impl, H> Reporter { pub struct CompileActor { diag_group: String, position_encoding: PositionEncoding, - root_tx: Mutex>>>, - root: OnceCell>, - entry: Arc>>, - inner: Deferred>, + config: Config, + // root_tx: Mutex>>>, + // root: OnceCell>, + entry: Arc>, + inner: Deferred, render_tx: broadcast::Sender, } @@ -400,20 +276,20 @@ impl CompileActor { #[allow(clippy::too_many_arguments)] fn new( diag_group: String, - root_tx: oneshot::Sender>, - root: Option, - entry: Option, + config: Config, + entry: EntryState, position_encoding: PositionEncoding, - inner: Deferred>, + inner: Deferred, render_tx: broadcast::Sender, ) -> Self { Self { diag_group, - root_tx: Mutex::new(root.is_none().then_some(root_tx)), - root: match root { - Some(root) => OnceCell::from(Some(root)), - None => OnceCell::new(), - }, + config, + // root_tx: Mutex::new(root.is_none().then_some(root_tx)), + // root: match root { + // Some(root) => OnceCell::from(Some(root)), + // None => OnceCell::new(), + // }, position_encoding, entry: Arc::new(Mutex::new(entry)), inner, @@ -421,14 +297,14 @@ impl CompileActor { } } - fn inner(&self) -> &CompileClient { + fn inner(&self) -> &CompileClient { self.inner.wait() } /// Steal the compiler thread and run the given function. pub fn steal( &self, - f: impl FnOnce(&mut CompileService) -> Ret + Send + 'static, + f: impl FnOnce(&mut CompileService) -> Ret + Send + 'static, ) -> ZResult { self.inner().steal(f) } @@ -436,15 +312,13 @@ impl CompileActor { /// Steal the compiler thread and run the given function. pub async fn steal_async( &self, - f: impl FnOnce(&mut CompileService, tokio::runtime::Handle) -> Ret - + Send - + 'static, + f: impl FnOnce(&mut CompileService, tokio::runtime::Handle) -> Ret + Send + 'static, ) -> ZResult { self.inner().steal_async(f).await } pub fn settle(&self) { - let _ = self.change_entry(None, |_| None); + let _ = self.change_entry(None); info!("TypstActor({}): settle requested", self.diag_group); let res = self.inner().settle(); match res { @@ -458,86 +332,66 @@ impl CompileActor { } } - pub fn change_entry( - &self, - path: Option, - resolve_root: impl FnOnce(Option) -> Option, - ) -> Result<(), Error> { + pub fn change_entry(&self, path: Option) -> Result<(), Error> { if path.as_deref().is_some_and(|p| !p.is_absolute()) { return Err(error_once!("entry file must be absolute", path: path.unwrap().display())); } - { - let path = path.clone(); - self.root.get_or_init(|| { - info!("TypstActor({}): delayed root resolution", self.diag_group); - let mut root_tx = self.root_tx.lock(); - let root_tx = root_tx.take().unwrap(); - let root = resolve_root(path); - let _ = root_tx.send(root.clone()); - info!("TypstActor({}): resolved root: {root:?}", self.diag_group); - root - }) - }; + let next_entry = self.config.determine_entry(path); // todo: more robust rollback logic let entry = self.entry.clone(); let should_change = { - let mut entry = entry.lock(); - let should_change = entry.as_deref() != path.as_deref(); - let prev = entry.clone(); - *entry = path.clone(); - - should_change.then_some(prev) + let prev_entry = entry.lock(); + let should_change = next_entry != *prev_entry; + should_change.then(|| prev_entry.clone()) }; if let Some(prev) = should_change { - let next = path.clone(); + let next = next_entry.clone(); - debug!( - "the entry file of TypstActor({}) is changed to {next:?}", + info!( + "the entry file of TypstActor({}) is changing to {next:?}", self.diag_group, ); self.render_tx .send(RenderActorRequest::ChangeExportPath(PdfPathVars { - root: self.root.get().cloned().flatten(), - path: next.clone(), + entry: next.clone(), })) .unwrap(); // todo let res = self.steal(move |compiler| { - let root = compiler.compiler.world().workspace_root(); - if path.as_ref().is_some_and(|p| !p.starts_with(&root)) { - warn!("entry file is not in workspace root {path:?}"); - return; - } + compiler.change_entry(next.clone()); - if let Some(path) = &path { - let driver = &mut compiler.compiler.compiler.inner.compiler; - driver.set_entry_file(path.as_ref().to_owned()); - } + let next_is_inactive = next.is_inactive(); + let res = compiler.compiler.world_mut().mutate_entry(next); - compiler.change_entry(path.clone()); - - if path.is_none() { + if next_is_inactive { info!("TypstActor: removing diag"); compiler.compiler.compiler.push_diagnostics(None); } + + res.map(|_| ()) + .map_err(|err| error_once!("failed to change entry", err: format!("{err:?}"))) }); + let res = match res { + Ok(res) => res, + Err(res) => Err(res), + }; + if res.is_err() { self.render_tx .send(RenderActorRequest::ChangeExportPath(PdfPathVars { - root: self.root.get().cloned().flatten(), - path: prev.clone(), + entry: prev.clone(), })) .unwrap(); let mut entry = entry.lock(); // todo: the rollback is actually not atomic - if *entry == next { + if *entry == next_entry { *entry = prev; } @@ -552,59 +406,45 @@ impl CompileActor { Ok(()) } - pub fn add_memory_changes( - &self, - event: MemoryEvent, - resolve_root: impl FnOnce(Option) -> Option, - ) { - self.root.get_or_init(|| { - info!( - "TypstActor({}): delayed root resolution on memory change events", - self.diag_group - ); - - // determine path by event - let entry = { - let entry = self.entry.lock().clone(); - - entry.or_else(|| match &event { - MemoryEvent::Sync(changeset) | MemoryEvent::Update(changeset) => changeset - .inserts - .first() - .map(|e| e.0.clone()) - .or_else(|| changeset.removes.first().cloned()), - }) - }; - - let mut root_tx = self.root_tx.lock(); - let root_tx = root_tx.take().unwrap(); - let root = resolve_root(entry); - let _ = root_tx.send(root.clone()); - - info!("TypstActor({}): resolved root: {root:?}", self.diag_group); - root - }); - + pub fn add_memory_changes(&self, event: MemoryEvent) { self.inner.wait().add_memory_changes(event); } pub(crate) fn change_export_pdf(&self, config: PdfExportConfig) { - let entry = self.entry.lock(); - let path = entry - .as_ref() - .map(|e| e.clone().with_extension("pdf").into()); + let entry = self.entry.lock().clone(); let _ = self .render_tx .send(RenderActorRequest::ChangeConfig(PdfExportConfig { substitute_pattern: config.substitute_pattern, - root: self.root.get().cloned().flatten(), - path, + // root: self.root.get().cloned().flatten(), + entry, mode: config.mode, })) .unwrap(); } } +struct WrapWorld<'a>(&'a mut LspWorld); + +impl<'a> AnaylsisResources for WrapWorld<'a> { + fn world(&self) -> &dyn typst::World { + self.0 + } + + fn resolve( + &self, + spec: &typst_ts_core::package::PackageSpec, + ) -> Result, typst::diag::PackageError> { + use typst_ts_compiler::package::Registry; + self.0.registry.resolve(spec) + } + + fn iter_dependencies(&self, f: &mut dyn FnMut(&ImmutPath, typst_ts_compiler::Time)) { + use typst_ts_compiler::NotifyApi; + self.0.iter_dependencies(f) + } +} + impl CompileActor { pub fn query(&self, query: CompilerQueryRequest) -> anyhow::Result { use CompilerQueryRequest::*; @@ -669,14 +509,17 @@ impl CompileActor { let enc = self.position_encoding; self.steal(move |compiler| { - // todo: record analysis let doc = compiler.success_doc(); let w = compiler.compiler.world_mut(); - let Some(main) = w.main else { + let Some(main) = w.main_id() else { log::error!("TypstActor: main file is not set"); return Err(anyhow!("main file is not set")); }; + let Some(root) = w.entry.root() else { + log::error!("TypstActor: root is not set"); + return Err(anyhow!("root is not set")); + }; w.source(main).map_err(|err| { log::info!("TypstActor: failed to prepare main file: {:?}", err); anyhow!("failed to get source: {err}") @@ -686,11 +529,12 @@ impl CompileActor { anyhow!("failed to prepare env") })?; + let w = WrapWorld(w); Ok(f( &mut AnalysisContext::new( - w, + &w, Analysis { - root: w.root.clone(), + root, position_encoding: enc, }, ), @@ -704,15 +548,39 @@ impl CompileActor { f: impl FnOnce(&mut AnalysisContext) -> T + Send + Sync + 'static, ) -> anyhow::Result { let enc = self.position_encoding; + // let opts = match opts { + // OptsState::Exact(opts) => opts, + // OptsState::Rootless(opts) => { + // let root: ImmutPath = match utils::threaded_receive(root_rx) { + // Ok(Some(root)) => root, + // Ok(None) => { + // error!("TypstActor: failed to receive root path: root is + // none"); return CompileClient::faked(); + // } + // Err(err) => { + // error!("TypstActor: failed to receive root path: {:#}", err); + // return CompileClient::faked(); + // } + // }; + + // opts(root.as_ref().into()) + // } + // }; + // mut opts: CompileOnceOpts, + // let inputs = std::mem::take(&mut opts.inputs); + // w.set_inputs(Arc::new(Prehashed::new(inputs))); self.steal(move |compiler| { - // todo: record analysis let w = compiler.compiler.world_mut(); - let Some(main) = w.main else { + let Some(main) = w.main_id() else { log::error!("TypstActor: main file is not set"); return Err(anyhow!("main file is not set")); }; + let Some(root) = w.entry.root() else { + log::error!("TypstActor: root is not set"); + return Err(anyhow!("root is not set")); + }; w.source(main).map_err(|err| { log::info!("TypstActor: failed to prepare main file: {:?}", err); anyhow!("failed to get source: {err}") @@ -722,10 +590,11 @@ impl CompileActor { anyhow!("failed to prepare env") })?; + let w = WrapWorld(w); Ok(f(&mut AnalysisContext::new( - w, + &w, Analysis { - root: w.root.clone(), + root, position_encoding: enc, }, ))) diff --git a/crates/tinymist/src/init.rs b/crates/tinymist/src/init.rs index 2dc25d43..8769a246 100644 --- a/crates/tinymist/src/init.rs +++ b/crates/tinymist/src/init.rs @@ -1,3 +1,4 @@ +use std::sync::Arc; use std::{collections::HashMap, path::PathBuf}; use anyhow::bail; @@ -11,10 +12,18 @@ use serde_json::{Map, Value as JsonValue}; use tinymist_query::{get_semantic_tokens_options, PositionEncoding}; use tokio::sync::mpsc; use typst::foundations::IntoValue; -use typst_ts_core::{config::CompileOpts, ImmutPath, TypstDict}; +use typst::syntax::VirtualPath; +use typst::util::Deferred; +use typst_ts_core::config::compiler::EntryState; +use typst_ts_core::error::prelude::*; +use typst_ts_core::{ImmutPath, TypstDict, TypstFileId as FileId}; use crate::actor::cluster::CompileClusterActor; -use crate::{invalid_params, LspHost, LspResult, TypstLanguageServer, TypstLanguageServerArgs}; +use crate::world::{CompileOpts, SharedFontResolver}; +use crate::{ + invalid_params, CompileFontOpts, LspHost, LspResult, TypstLanguageServer, + TypstLanguageServerArgs, +}; // todo: svelte-language-server responds to a Goto Definition request with // LocationLink[] even if the client does not report the @@ -283,6 +292,18 @@ impl Config { return Some(path.as_path().into()); } + if let Some(extras) = &self.typst_extra_args { + // todo: inputs + // if let Some(inputs) = extras.inputs.as_ref() { + // if opts.inputs.is_empty() { + // opts.inputs = inputs.clone(); + // } + // } + if let Some(root) = &extras.root_dir { + return Some(root.as_path().into()); + } + } + if let Some(path) = &self .typst_extra_args .as_ref() @@ -314,6 +335,34 @@ impl Config { None } + pub fn determine_entry(&self, entry: Option) -> EntryState { + // todo: don't ignore entry from typst_extra_args + // entry: command.input, + let root_dir = self.determine_root(entry.as_ref()); + + let entry = match (entry, root_dir) { + (Some(entry), Some(root)) => match entry.strip_prefix(&root) { + Ok(stripped) => Some(EntryState::new_rooted( + root, + Some(FileId::new(None, VirtualPath::new(stripped))), + )), + Err(err) => { + log::info!("Entry is not in root directory: err {err:?}: entry: {entry:?}, root: {root:?}"); + EntryState::new_rootless(entry) + } + }, + (Some(entry), None) => EntryState::new_rootless(entry), + (None, Some(root)) => Some(EntryState::new_workspace(root)), + (None, None) => None, + }; + + entry.unwrap_or_else(|| match self.determine_root(None) { + Some(root) => EntryState::new_workspace(root), + // todo + None => EntryState::new_detached(), + }) + } + fn validate(&self) -> anyhow::Result<()> { if let Some(root) = &self.root_path { if !root.is_absolute() { @@ -436,7 +485,7 @@ impl Init { /// # Errors /// Errors if the configuration could not be updated. pub fn initialize( - self, + mut self, params: InitializeParams, ) -> (TypstLanguageServer, LspResult) { // self.tracing_init(); @@ -447,44 +496,59 @@ impl Init { "initialized with const_config {const_config:?}", const_config = cc ); + let mut config = Config { + roots: match params.workspace_folders.as_ref() { + Some(roots) => roots + .iter() + .map(|root| &root.uri) + .map(Url::to_file_path) + .collect::, _>>() + .unwrap(), + #[allow(deprecated)] // `params.root_path` is marked as deprecated + None => params + .root_uri + .as_ref() + .map(|uri| uri.to_file_path().unwrap()) + .or_else(|| params.root_path.clone().map(PathBuf::from)) + .into_iter() + .collect(), + }, + ..Config::default() + }; + let res = match ¶ms.initialization_options { + Some(init) => config + .update(init) + .map_err(|e| e.to_string()) + .map_err(invalid_params), + None => Ok(()), + }; - let mut config = Config::default(); + // prepare fonts + // todo: on font resolving failure, downgrade to a fake font book + let font = { + let opts = std::mem::take(&mut self.compile_opts.font); + if opts.font_paths.is_empty() { + if let Some(font_paths) = config.typst_extra_args.as_ref().map(|x| &x.font_paths) { + self.compile_opts.font.font_paths = font_paths.clone(); + } + } + + Deferred::new(|| create_font_book(opts).expect("failed to create font book")) + }; // Bootstrap server let (diag_tx, diag_rx) = mpsc::unbounded_channel(); let mut service = TypstLanguageServer::new(TypstLanguageServerArgs { client: self.host.clone(), - compile_opts: self.compile_opts, + compile_opts: self.compile_opts.once, const_config: cc.clone(), diag_tx, + font, }); - config.roots = match params.workspace_folders.as_ref() { - Some(roots) => roots - .iter() - .map(|root| &root.uri) - .map(Url::to_file_path) - .collect::, _>>() - .unwrap(), - #[allow(deprecated)] // `params.root_path` is marked as deprecated - None => params - .root_uri - .as_ref() - .map(|uri| uri.to_file_path().unwrap()) - .or_else(|| params.root_path.clone().map(PathBuf::from)) - .into_iter() - .collect(), - }; - if let Some(init) = ¶ms.initialization_options { - if let Err(err) = config - .update(init) - .as_ref() - .map_err(ToString::to_string) - .map_err(invalid_params) - { - return (service, Err(err)); - } + if let Err(err) = res { + return (service, Err(err)); } info!("initialized with config {config:?}", config = config); @@ -498,7 +562,7 @@ impl Init { published_primary: false, }; - let primary = service.server("primary".to_owned(), None); + let primary = service.server("primary".to_owned(), service.config.determine_entry(None)); if service.primary.is_some() { panic!("primary already initialized"); } @@ -588,6 +652,14 @@ impl Init { } } +fn create_font_book(opts: CompileFontOpts) -> ZResult { + let res = crate::world::LspWorldBuilder::resolve_fonts(opts)?; + Ok(SharedFontResolver { + inner: Arc::new(res), + // inner: res, + }) +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/tinymist/src/lib.rs b/crates/tinymist/src/lib.rs index bdfa5341..d84b7cbf 100644 --- a/crates/tinymist/src/lib.rs +++ b/crates/tinymist/src/lib.rs @@ -34,6 +34,7 @@ mod task; mod tools; pub mod transport; mod utils; +mod world; use core::fmt; use std::path::Path; @@ -66,10 +67,13 @@ use tinymist_query::{ use tokio::sync::mpsc; use typst::diag::StrResult; use typst::syntax::package::{PackageSpec, VersionlessPackageSpec}; +use typst::util::Deferred; use typst_ts_compiler::service::Compiler; -use typst_ts_core::{config::CompileOpts, error::prelude::*, ImmutPath}; +use typst_ts_core::{error::prelude::*, ImmutPath}; pub type MaySyncResult<'a> = Result>; +use world::SharedFontResolver; +pub use world::{CompileFontOpts, CompileOnceOpts, CompileOpts}; use crate::actor::render::PdfExportConfig; use crate::init::*; @@ -285,9 +289,10 @@ fn as_path_pos(inp: TextDocumentPositionParams) -> (PathBuf, Position) { pub struct TypstLanguageServerArgs { pub client: LspHost, - pub compile_opts: CompileOpts, + pub compile_opts: CompileOnceOpts, pub const_config: ConstConfig, pub diag_tx: mpsc::UnboundedSender<(String, Option)>, + pub font: Deferred, } /// The object providing the language server functionality. @@ -307,7 +312,7 @@ pub struct TypstLanguageServer { /// For example, the position encoding. pub const_config: ConstConfig, /// The default opts for the compiler. - pub compile_opts: CompileOpts, + pub compile_opts: CompileOnceOpts, // Command maps /// Extra commands provided with `textDocument/executeCommand`. @@ -322,6 +327,7 @@ pub struct TypstLanguageServer { primary: Option, pinning: bool, main: Option, + font: Deferred, tokens_ctx: SemanticTokenContext, } @@ -350,6 +356,7 @@ impl TypstLanguageServer { primary: None, pinning: false, main: None, + font: args.font, tokens_ctx, } } diff --git a/crates/tinymist/src/main.rs b/crates/tinymist/src/main.rs index 290ed43b..9dd6162e 100644 --- a/crates/tinymist/src/main.rs +++ b/crates/tinymist/src/main.rs @@ -12,8 +12,7 @@ use log::{info, trace, warn}; use lsp_types::{InitializeParams, InitializedParams}; use parking_lot::RwLock; use serde::de::DeserializeOwned; -use tinymist::{init::Init, transport::io_transport, LspHost}; -use typst_ts_core::config::CompileOpts; +use tinymist::{init::Init, transport::io_transport, CompileFontOpts, CompileOpts, LspHost}; use crate::args::CliArguments; @@ -126,8 +125,11 @@ async fn main() -> anyhow::Result<()> { let (mut service, initialize_result) = Init { host: host.clone(), compile_opts: CompileOpts { - font_paths: args.font_paths, - no_system_fonts: args.no_system_fonts, + font: CompileFontOpts { + font_paths: args.font_paths, + no_system_fonts: args.no_system_fonts, + ..Default::default() + }, ..Default::default() }, } diff --git a/crates/tinymist/src/state.rs b/crates/tinymist/src/state.rs index 18ddb2a5..c419d55e 100644 --- a/crates/tinymist/src/state.rs +++ b/crates/tinymist/src/state.rs @@ -5,7 +5,9 @@ use std::path::PathBuf; use ::typst::{diag::FileResult, syntax::Source}; use anyhow::anyhow; use lsp_types::TextDocumentContentChangeEvent; -use tinymist_query::{lsp_to_typst, CompilerQueryRequest, CompilerQueryResponse, PositionEncoding}; +use tinymist_query::{ + lsp_to_typst, CompilerQueryRequest, CompilerQueryResponse, PositionEncoding, SyntaxRequest, +}; use typst_ts_compiler::{ vfs::notify::{FileChangeSet, MemoryEvent}, Time, @@ -22,16 +24,18 @@ pub struct MemoryFileMeta { impl TypstLanguageServer { /// Updates the main entry - // todo: the changed entry may be out of root directory pub fn update_main_entry(&mut self, new_entry: Option) -> Result<(), Error> { self.pinning = new_entry.is_some(); match (new_entry, self.main.is_some()) { (Some(new_entry), true) => { let main = self.main.as_mut().unwrap(); - main.change_entry(Some(new_entry), |e| self.config.determine_root(e.as_ref()))?; + main.change_entry(Some(new_entry))?; } (Some(new_entry), false) => { - let main_node = self.server("main".to_owned(), Some(new_entry)); + let main_node = self.server( + "main".to_owned(), + self.config.determine_entry(Some(new_entry)), + ); self.main = Some(main_node); } @@ -47,11 +51,7 @@ impl TypstLanguageServer { /// Updates the primary (focusing) entry pub fn update_primary_entry(&self, new_entry: Option) -> Result<(), Error> { - self.primary().change_entry(new_entry.clone(), |e| { - self.config.determine_root(e.as_ref()) - })?; - - Ok(()) + self.primary().change_entry(new_entry.clone()) } } @@ -62,9 +62,7 @@ impl TypstLanguageServer { let clients_to_notify = (primary.into_iter()).chain(main); for client in clients_to_notify { - client.add_memory_changes(MemoryEvent::Update(files.clone()), |e| { - self.config.determine_root(e.as_ref()) - }); + client.add_memory_changes(MemoryEvent::Update(files.clone())); } Ok(()) @@ -169,7 +167,7 @@ macro_rules! query_source { let source = snapshot.content.clone(); let enc = $self.const_config.position_encoding; - let res = $req.request(source, enc); + let res = $req.request(&source, enc); Ok(CompilerQueryResponse::$method(res)) }}; } @@ -197,20 +195,16 @@ impl TypstLanguageServer { SelectionRange(req) => query_source!(self, SelectionRange, req), DocumentSymbol(req) => query_source!(self, DocumentSymbol, req), _ => { - let query_target = match self.main.as_ref() { - Some(main) if self.pinning => main, + match self.main.as_ref() { + Some(main) if self.pinning => main.query(query), Some(..) | None => { // todo: race condition, we need atomic primary query if let Some(path) = query.associated_path() { - self.primary().change_entry(Some(path.into()), |e| { - self.config.determine_root(e.as_ref()) - })?; + self.primary().change_entry(Some(path.into()))?; } - self.primary() + self.primary().query(query) } - }; - - query_target.query(query) + } } } } diff --git a/crates/tinymist/src/tools/package/init.rs b/crates/tinymist/src/tools/package/init.rs index fecbb4c6..b9fc2e6d 100644 --- a/crates/tinymist/src/tools/package/init.rs +++ b/crates/tinymist/src/tools/package/init.rs @@ -5,9 +5,10 @@ use typst::diag::{bail, eco_format, FileError, FileResult, StrResult}; use typst::syntax::package::{PackageManifest, PackageSpec, TemplateInfo}; use typst::syntax::VirtualPath; use typst::World; -use typst_ts_compiler::TypstSystemWorld; use typst_ts_core::{Bytes, ImmutPath, TypstFileId}; +use crate::world::LspWorld; + #[derive(Debug, Clone)] pub enum TemplateSource { Package(PackageSpec), @@ -19,7 +20,7 @@ pub struct InitTask { } /// Execute an initialization command. -pub fn get_entry(world: &TypstSystemWorld, tmpl: TemplateSource) -> StrResult { +pub fn get_entry(world: &LspWorld, tmpl: TemplateSource) -> StrResult { let TemplateSource::Package(spec) = tmpl; let toml_id = TypstFileId::new(Some(spec.clone()), VirtualPath::new("typst.toml")); @@ -41,7 +42,7 @@ pub fn get_entry(world: &TypstSystemWorld, tmpl: TemplateSource) -> StrResult StrResult { +pub fn init(world: &LspWorld, task: InitTask) -> StrResult { let TemplateSource::Package(spec) = task.tmpl; let project_dir = task .dir @@ -71,7 +72,7 @@ pub fn init(world: &TypstSystemWorld, task: InitTask) -> StrResult { } /// Parses the manifest of the package located at `package_path`. -fn parse_manifest(world: &TypstSystemWorld, toml_id: TypstFileId) -> StrResult { +fn parse_manifest(world: &LspWorld, toml_id: TypstFileId) -> StrResult { let toml_data = world .file(toml_id) .map_err(|err| eco_format!("failed to read package manifest ({})", err))?; @@ -86,7 +87,7 @@ fn parse_manifest(world: &TypstSystemWorld, toml_id: TypstFileId) -> StrResult

StrResult { if spec.namespace == "preview" { diff --git a/crates/tinymist/src/world.rs b/crates/tinymist/src/world.rs new file mode 100644 index 00000000..075368d4 --- /dev/null +++ b/crates/tinymist/src/world.rs @@ -0,0 +1,113 @@ +use std::{borrow::Cow, path::PathBuf, sync::Arc}; + +use comemo::Prehashed; +use serde::{Deserialize, Serialize}; +use typst_ts_core::{ + config::{compiler::EntryState, CompileFontOpts as FontOptsInner}, + error::prelude::*, + font::FontResolverImpl, + FontResolver, TypstDict, +}; + +use typst_ts_compiler::{ + font::system::SystemFontSearcher, + package::http::HttpRegistry, + vfs::{system::SystemAccessModel, Vfs}, + world::CompilerWorld, +}; + +#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)] +pub struct CompileOpts { + #[serde(flatten)] + pub once: CompileOnceOpts, + + #[serde(flatten)] + pub font: CompileFontOpts, +} + +#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)] +pub struct CompileOnceOpts { + /// The root directory for compilation routine. + #[serde(rename = "rootDir")] + pub root_dir: PathBuf, + + /// Path to entry + pub entry: PathBuf, + + /// Additional input arguments to compile the entry file. + pub inputs: TypstDict, +} + +#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)] +pub struct CompileFontOpts { + /// Path to font profile for cache + #[serde(rename = "fontProfileCachePath")] + pub font_profile_cache_path: PathBuf, + + /// will remove later + #[serde(rename = "fontPaths")] + pub font_paths: Vec, + + /// Exclude system font paths + #[serde(rename = "noSystemFonts")] + pub no_system_fonts: bool, +} + +#[derive(Debug, Clone)] +pub struct SharedFontResolver { + pub inner: Arc, +} + +impl FontResolver for SharedFontResolver { + fn font(&self, idx: usize) -> Option { + self.inner.font(idx) + } + fn font_book(&self) -> &Prehashed { + self.inner.font_book() + } +} + +/// type trait of [`LspWorld`]. +#[derive(Debug, Clone, Copy)] +pub struct SystemCompilerFeat; + +impl typst_ts_compiler::world::CompilerFeat for SystemCompilerFeat { + /// Uses [`SharedFontResolver`] directly. + type FontResolver = SharedFontResolver; + /// It accesses a physical file system. + type AccessModel = SystemAccessModel; + /// It performs native HTTP requests for fetching package data. + type Registry = HttpRegistry; +} + +/// The compiler world in system environment. +pub type LspWorld = CompilerWorld; + +pub struct LspWorldBuilder; +// Self::resolve_fonts(opts)?, + +impl LspWorldBuilder { + /// Create [`LspWorld`] with the given options. + /// See SystemCompilerFeat for instantiation details. + /// See [`CompileOpts`] for available options. + pub fn build(entry: EntryState, font_resolver: SharedFontResolver) -> ZResult { + Ok(CompilerWorld::new_raw( + entry, + Vfs::new(SystemAccessModel {}), + HttpRegistry::default(), + font_resolver, + )) + } + + /// Resolve fonts from given options. + pub(crate) fn resolve_fonts(opts: CompileFontOpts) -> ZResult { + let mut searcher = SystemFontSearcher::new(); + searcher.resolve_opts(FontOptsInner { + font_profile_cache_path: opts.font_profile_cache_path, + font_paths: opts.font_paths, + no_system_fonts: opts.no_system_fonts, + with_embedded_fonts: typst_assets::fonts().map(Cow::Borrowed).collect(), + })?; + Ok(searcher.into()) + } +} diff --git a/editors/vscode/src/extension.ts b/editors/vscode/src/extension.ts index 50569c33..d359598a 100644 --- a/editors/vscode/src/extension.ts +++ b/editors/vscode/src/extension.ts @@ -254,7 +254,7 @@ async function commandPinMain(isPin: boolean): Promise { if (!isPin) { await client?.sendRequest("workspace/executeCommand", { command: "tinymist.pinMain", - arguments: ["detached"], + arguments: [null], }); return; }