diff --git a/crates/tinymist-std/src/error.rs b/crates/tinymist-std/src/error.rs index 64a71aae..46e27b5a 100644 --- a/crates/tinymist-std/src/error.rs +++ b/crates/tinymist-std/src/error.rs @@ -330,7 +330,7 @@ pub mod prelude { use crate::Error; pub use super::{IgnoreLogging, WithContext, WithContextUntyped}; - pub use crate::Result; + pub use crate::{bail, Result}; pub fn map_string_err(loc: &'static str) -> impl Fn(T) -> Error { move |e| Error::new(loc, e.to_string().to_error_kind(), None) diff --git a/crates/tinymist/src/cmd.rs b/crates/tinymist/src/cmd.rs index afc144e6..40fff96e 100644 --- a/crates/tinymist/src/cmd.rs +++ b/crates/tinymist/src/cmd.rs @@ -22,7 +22,7 @@ use world::TaskInputs; use super::state::*; use super::*; -use crate::state::query::{run_query, LspClientExt}; +use crate::state::lsp_query::{run_query, LspClientExt}; use crate::tool::package::InitTask; /// See [`ProjectTask`]. diff --git a/crates/tinymist/src/lib.rs b/crates/tinymist/src/lib.rs index c2d0bc1d..d0fcff48 100644 --- a/crates/tinymist/src/lib.rs +++ b/crates/tinymist/src/lib.rs @@ -38,7 +38,7 @@ pub use world::{CompileFontArgs, CompileOnceArgs, CompilePackageArgs}; use lsp_server::ResponseError; use serde_json::from_value; -use state::query::QueryFuture; +use state::lsp_query::QueryFuture; use sync_lsp::*; use tinymist_std::error::Result; use utils::*; diff --git a/crates/tinymist/src/state.rs b/crates/tinymist/src/state.rs index d600ae2c..a77c42e9 100644 --- a/crates/tinymist/src/state.rs +++ b/crates/tinymist/src/state.rs @@ -2,7 +2,7 @@ mod server; pub use server::*; pub(crate) mod input; +pub(crate) mod lsp; +pub(crate) mod lsp_query; pub mod project; -pub(crate) mod protocol; -pub(crate) mod query; pub(crate) mod route; diff --git a/crates/tinymist/src/state/input.rs b/crates/tinymist/src/state/input.rs index de422ecc..61d62aa5 100644 --- a/crates/tinymist/src/state/input.rs +++ b/crates/tinymist/src/state/input.rs @@ -1,145 +1,17 @@ use std::path::PathBuf; -use lsp_types::request::WorkspaceConfiguration; use lsp_types::*; -use once_cell::sync::OnceCell; use reflexo_typst::Bytes; -use serde_json::{Map, Value as JsonValue}; -use sync_lsp::*; use tinymist_project::{Interrupt, ProjectResolutionKind}; use tinymist_query::{to_typst_range, PositionEncoding}; -use tinymist_std::error::{prelude::*, IgnoreLogging}; +use tinymist_std::error::prelude::*; use tinymist_std::ImmutPath; use typst::{diag::FileResult, syntax::Source}; use crate::route::ProjectResolution; -use crate::task::FormatterConfig; use crate::world::vfs::{notify::MemoryEvent, FileChangeSet}; use crate::world::TaskInputs; -use crate::{init::*, *}; - -/// LSP Document Synchronization -impl ServerState { - pub(crate) fn did_open(&mut self, params: DidOpenTextDocumentParams) -> LspResult<()> { - log::info!("did open {:?}", params.text_document.uri); - let path = as_path_(params.text_document.uri); - let text = params.text_document.text; - - self.create_source(path.clone(), text) - .map_err(|e| invalid_params(e.to_string()))?; - - // Focus after opening - self.implicit_focus_entry(|| Some(path.as_path().into()), 'o'); - Ok(()) - } - - pub(crate) fn did_close(&mut self, params: DidCloseTextDocumentParams) -> LspResult<()> { - let path = as_path_(params.text_document.uri); - - self.remove_source(path.clone()) - .map_err(|e| invalid_params(e.to_string()))?; - Ok(()) - } - - pub(crate) fn did_change(&mut self, params: DidChangeTextDocumentParams) -> LspResult<()> { - let path = as_path_(params.text_document.uri); - let changes = params.content_changes; - - self.edit_source(path.clone(), changes, self.const_config().position_encoding) - .map_err(|e| invalid_params(e.to_string()))?; - Ok(()) - } - - pub(crate) fn did_save(&mut self, _params: DidSaveTextDocumentParams) -> LspResult<()> { - Ok(()) - } -} - -/// LSP Configuration Synchronization -impl ServerState { - pub(crate) fn on_changed_configuration( - &mut self, - values: Map, - ) -> LspResult<()> { - let old_config = self.config.clone(); - match self.config.update_by_map(&values) { - Ok(()) => {} - Err(err) => { - self.config = old_config; - log::error!("error applying new settings: {err}"); - return Err(invalid_params(format!( - "error applying new settings: {err}" - ))); - } - } - - let new_export_config = self.config.export(); - if old_config.export() != new_export_config { - self.change_export_config(new_export_config); - } - - if old_config.compile.primary_opts() != self.config.compile.primary_opts() { - self.config.compile.fonts = OnceCell::new(); // todo: don't reload fonts if not changed - self.restart_primary() - .log_error("could not restart primary"); - } - - if old_config.semantic_tokens != self.config.semantic_tokens { - self.enable_sema_token_caps(self.config.semantic_tokens == SemanticTokensMode::Enable) - .log_error("could not change semantic tokens config"); - } - - let new_formatter_config = self.config.formatter(); - if !old_config.formatter().eq(&new_formatter_config) { - let enabled = !matches!(new_formatter_config.config, FormatterConfig::Disable); - self.enable_formatter_caps(enabled) - .log_error("could not change formatter config"); - - self.formatter.change_config(new_formatter_config); - } - - log::info!("new settings applied"); - Ok(()) - } - - pub(crate) fn did_change_configuration( - &mut self, - params: DidChangeConfigurationParams, - ) -> LspResult<()> { - // For some clients, we don't get the actual changed configuration and need to - // poll for it https://github.com/microsoft/language-server-protocol/issues/676 - if let JsonValue::Object(settings) = params.settings { - return self.on_changed_configuration(settings); - }; - - self.client.send_request::( - ConfigurationParams { - items: Config::get_items(), - }, - Self::workspace_configuration_callback, - ); - Ok(()) - } - - fn workspace_configuration_callback(this: &mut ServerState, resp: lsp_server::Response) { - if let Some(err) = resp.error { - log::error!("failed to request configuration: {err:?}"); - return; - } - - let Some(result) = resp.result else { - log::error!("no configuration returned"); - return; - }; - - let Some(resp) = serde_json::from_value::>(result) - .log_error("could not parse configuration") - else { - return; - }; - let _ = this.on_changed_configuration(Config::values_to_map(resp)); - } -} +use crate::*; /// In memory source file management. impl ServerState { diff --git a/crates/tinymist/src/state/protocol.rs b/crates/tinymist/src/state/lsp.rs similarity index 62% rename from crates/tinymist/src/state/protocol.rs rename to crates/tinymist/src/state/lsp.rs index 8ddbcc07..091dba5f 100644 --- a/crates/tinymist/src/state/protocol.rs +++ b/crates/tinymist/src/state/lsp.rs @@ -1,10 +1,204 @@ +use lsp_types::request::WorkspaceConfiguration; use lsp_types::*; +use once_cell::sync::OnceCell; use request::{RegisterCapability, UnregisterCapability}; +use serde_json::{Map, Value as JsonValue}; use sync_lsp::*; use tinymist_std::error::{prelude::*, IgnoreLogging}; +use crate::task::FormatterConfig; use crate::{init::*, *}; +/// Trait implemented by language server backends. +/// +/// This interface allows servers adhering to the [Language Server Protocol] to +/// be implemented in a safe and easily testable way without exposing the +/// low-level implementation details. +/// +/// [Language Server Protocol]: https://microsoft.github.io/language-server-protocol/ +impl ServerState { + /// The [`initialized`] notification is sent from the client to the server + /// after the client received the result of the initialize request but + /// before the client sends anything else. + /// + /// [`initialized`]: https://microsoft.github.io/language-server-protocol/specification#initialized + /// + /// The server can use the `initialized` notification, for example, to + /// dynamically register capabilities with the client. + pub(crate) fn initialized(&mut self, _params: InitializedParams) -> LspResult<()> { + if self.const_config().tokens_dynamic_registration + && self.config.semantic_tokens == SemanticTokensMode::Enable + { + self.enable_sema_token_caps(true) + .log_error("could not register semantic tokens for initialization"); + } + + if self.const_config().doc_fmt_dynamic_registration + && self.config.formatter_mode != FormatterMode::Disable + { + self.enable_formatter_caps(true) + .log_error("could not register formatter for initialization"); + } + + if self.const_config().cfg_change_registration { + log::trace!("setting up to request config change notifications"); + + const CONFIG_REGISTRATION_ID: &str = "config"; + const CONFIG_METHOD_ID: &str = "workspace/didChangeConfiguration"; + + self.register_capability(vec![Registration { + id: CONFIG_REGISTRATION_ID.to_owned(), + method: CONFIG_METHOD_ID.to_owned(), + register_options: None, + }]) + .log_error("could not register to watch config changes"); + } + + log::info!("server initialized"); + Ok(()) + } + + /// The [`shutdown`] request asks the server to gracefully shut down, but to + /// not exit. + /// + /// [`shutdown`]: https://microsoft.github.io/language-server-protocol/specification#shutdown + /// + /// This request is often later followed by an [`exit`] notification, which + /// will cause the server to exit immediately. + /// + /// [`exit`]: https://microsoft.github.io/language-server-protocol/specification#exit + /// + /// This method is guaranteed to only execute once. If the client sends this + /// request to the server again, the server will respond with JSON-RPC + /// error code `-32600` (invalid request). + pub(crate) fn shutdown(&mut self, _params: ()) -> SchedulableResponse<()> { + just_ok(()) + } +} + +/// LSP Document Synchronization +impl ServerState { + pub(crate) fn did_open(&mut self, params: DidOpenTextDocumentParams) -> LspResult<()> { + log::info!("did open {:?}", params.text_document.uri); + let path = as_path_(params.text_document.uri); + let text = params.text_document.text; + + self.create_source(path.clone(), text) + .map_err(|e| invalid_params(e.to_string()))?; + + // Focus after opening + self.implicit_focus_entry(|| Some(path.as_path().into()), 'o'); + Ok(()) + } + + pub(crate) fn did_close(&mut self, params: DidCloseTextDocumentParams) -> LspResult<()> { + let path = as_path_(params.text_document.uri); + + self.remove_source(path.clone()) + .map_err(|e| invalid_params(e.to_string()))?; + Ok(()) + } + + pub(crate) fn did_change(&mut self, params: DidChangeTextDocumentParams) -> LspResult<()> { + let path = as_path_(params.text_document.uri); + let changes = params.content_changes; + + self.edit_source(path.clone(), changes, self.const_config().position_encoding) + .map_err(|e| invalid_params(e.to_string()))?; + Ok(()) + } + + pub(crate) fn did_save(&mut self, _params: DidSaveTextDocumentParams) -> LspResult<()> { + Ok(()) + } +} + +/// LSP Configuration Synchronization +impl ServerState { + pub(crate) fn on_changed_configuration( + &mut self, + values: Map, + ) -> LspResult<()> { + let old_config = self.config.clone(); + match self.config.update_by_map(&values) { + Ok(()) => {} + Err(err) => { + self.config = old_config; + log::error!("error applying new settings: {err}"); + return Err(invalid_params(format!( + "error applying new settings: {err}" + ))); + } + } + + let new_export_config = self.config.export(); + if old_config.export() != new_export_config { + self.change_export_config(new_export_config); + } + + if old_config.compile.primary_opts() != self.config.compile.primary_opts() { + self.config.compile.fonts = OnceCell::new(); // todo: don't reload fonts if not changed + self.restart_primary() + .log_error("could not restart primary"); + } + + if old_config.semantic_tokens != self.config.semantic_tokens { + self.enable_sema_token_caps(self.config.semantic_tokens == SemanticTokensMode::Enable) + .log_error("could not change semantic tokens config"); + } + + let new_formatter_config = self.config.formatter(); + if !old_config.formatter().eq(&new_formatter_config) { + let enabled = !matches!(new_formatter_config.config, FormatterConfig::Disable); + self.enable_formatter_caps(enabled) + .log_error("could not change formatter config"); + + self.formatter.change_config(new_formatter_config); + } + + log::info!("new settings applied"); + Ok(()) + } + + pub(crate) fn did_change_configuration( + &mut self, + params: DidChangeConfigurationParams, + ) -> LspResult<()> { + // For some clients, we don't get the actual changed configuration and need to + // poll for it https://github.com/microsoft/language-server-protocol/issues/676 + if let JsonValue::Object(settings) = params.settings { + return self.on_changed_configuration(settings); + }; + + self.client.send_request::( + ConfigurationParams { + items: Config::get_items(), + }, + Self::workspace_configuration_callback, + ); + Ok(()) + } + + fn workspace_configuration_callback(this: &mut ServerState, resp: lsp_server::Response) { + if let Some(err) = resp.error { + log::error!("failed to request configuration: {err:?}"); + return; + } + + let Some(result) = resp.result else { + log::error!("no configuration returned"); + return; + }; + + let Some(resp) = serde_json::from_value::>(result) + .log_error("could not parse configuration") + else { + return; + }; + let _ = this.on_changed_configuration(Config::values_to_map(resp)); + } +} + impl ServerState { // todo: handle error pub(crate) fn register_capability(&self, registrations: Vec) -> Result<()> { @@ -122,70 +316,3 @@ impl ServerState { } } } - -/// Trait implemented by language server backends. -/// -/// This interface allows servers adhering to the [Language Server Protocol] to -/// be implemented in a safe and easily testable way without exposing the -/// low-level implementation details. -/// -/// [Language Server Protocol]: https://microsoft.github.io/language-server-protocol/ -impl ServerState { - /// The [`initialized`] notification is sent from the client to the server - /// after the client received the result of the initialize request but - /// before the client sends anything else. - /// - /// [`initialized`]: https://microsoft.github.io/language-server-protocol/specification#initialized - /// - /// The server can use the `initialized` notification, for example, to - /// dynamically register capabilities with the client. - pub(crate) fn initialized(&mut self, _params: InitializedParams) -> LspResult<()> { - if self.const_config().tokens_dynamic_registration - && self.config.semantic_tokens == SemanticTokensMode::Enable - { - self.enable_sema_token_caps(true) - .log_error("could not register semantic tokens for initialization"); - } - - if self.const_config().doc_fmt_dynamic_registration - && self.config.formatter_mode != FormatterMode::Disable - { - self.enable_formatter_caps(true) - .log_error("could not register formatter for initialization"); - } - - if self.const_config().cfg_change_registration { - log::trace!("setting up to request config change notifications"); - - const CONFIG_REGISTRATION_ID: &str = "config"; - const CONFIG_METHOD_ID: &str = "workspace/didChangeConfiguration"; - - self.register_capability(vec![Registration { - id: CONFIG_REGISTRATION_ID.to_owned(), - method: CONFIG_METHOD_ID.to_owned(), - register_options: None, - }]) - .log_error("could not register to watch config changes"); - } - - log::info!("server initialized"); - Ok(()) - } - - /// The [`shutdown`] request asks the server to gracefully shut down, but to - /// not exit. - /// - /// [`shutdown`]: https://microsoft.github.io/language-server-protocol/specification#shutdown - /// - /// This request is often later followed by an [`exit`] notification, which - /// will cause the server to exit immediately. - /// - /// [`exit`]: https://microsoft.github.io/language-server-protocol/specification#exit - /// - /// This method is guaranteed to only execute once. If the client sends this - /// request to the server again, the server will respond with JSON-RPC - /// error code `-32600` (invalid request). - pub(crate) fn shutdown(&mut self, _params: ()) -> SchedulableResponse<()> { - just_ok(()) - } -} diff --git a/crates/tinymist/src/state/query.rs b/crates/tinymist/src/state/lsp_query.rs similarity index 99% rename from crates/tinymist/src/state/query.rs rename to crates/tinymist/src/state/lsp_query.rs index d244ba7b..171c564b 100644 --- a/crates/tinymist/src/state/query.rs +++ b/crates/tinymist/src/state/lsp_query.rs @@ -50,7 +50,7 @@ macro_rules! run_query { } pub(crate) use run_query; -/// Standard Language Features +/// LSP Standard Language Features impl ServerState { pub(crate) fn goto_definition( &mut self, diff --git a/crates/tinymist/src/state/project.rs b/crates/tinymist/src/state/project.rs index d40ad0a7..0218590a 100644 --- a/crates/tinymist/src/state/project.rs +++ b/crates/tinymist/src/state/project.rs @@ -26,26 +26,212 @@ use std::sync::Arc; use parking_lot::Mutex; use reflexo::{hash::FxHashMap, path::unix_slash}; use reflexo_typst::CompileReport; -use sync_lsp::LspClient; +use sync_lsp::{LspClient, TypedLspClient}; +use tinymist_project::vfs::{FileChangeSet, MemoryEvent}; use tinymist_query::{ - analysis::{Analysis, AnalysisRevLock, LocalContextGuard}, - CompilerQueryRequest, CompilerQueryResponse, DiagnosticsMap, SemanticRequest, StatefulRequest, - VersionedDocument, -}; -use tinymist_std::{ - bail, - error::{prelude::*, IgnoreLogging}, + analysis::{Analysis, AnalysisRevLock, LocalContextGuard, PeriscopeProvider}, + CompilerQueryRequest, CompilerQueryResponse, DiagnosticsMap, LocalContext, SemanticRequest, + StatefulRequest, VersionedDocument, }; +use tinymist_render::PeriscopeRenderer; +use tinymist_std::{error::prelude::*, ImmutPath}; use tokio::sync::mpsc; +use typst::{diag::FileResult, foundations::Bytes, layout::Position as TypstPosition}; +use super::ServerState; use crate::actor::editor::{CompileStatus, CompileStatusEnum, EditorRequest, ProjVersion}; use crate::stats::{CompilerQueryStats, QueryStatGuard}; +use crate::{task::ExportUserConfig, Config}; type EditorSender = mpsc::UnboundedSender; /// LSP project compiler. pub type LspProjectCompiler = ProjectCompiler; +/// Getters and the main loop. +impl ServerState { + /// Changes the export configuration. + pub fn change_export_config(&mut self, config: ExportUserConfig) { + self.project.export.change_config(config); + } + + /// Snapshot the compiler thread for tasks + pub fn snapshot(&mut self) -> Result { + self.project.snapshot() + } + + /// Snapshot the compiler thread for language queries + pub fn query_snapshot(&mut self) -> Result { + self.project.query_snapshot(None) + } + + /// Snapshot the compiler thread for language queries + pub fn query_snapshot_with_stat( + &mut self, + q: &CompilerQueryRequest, + ) -> Result { + let name: &'static str = q.into(); + let path = q.associated_path(); + let stat = self.project.stats.query_stat(path, name); + let snap = self.project.query_snapshot(Some(q))?; + Ok((snap, stat)) + } + + /// Restart the primary server. + pub fn restart_primary(&mut self) -> Result { + // todo: hot replacement + #[cfg(feature = "preview")] + self.preview.stop_all(); + + let watchers = self.preview.watchers.clone(); + let editor_tx = self.editor_tx.clone(); + + let new_project = Self::project(&self.config, editor_tx, self.client.clone(), watchers); + + let mut old_project = std::mem::replace(&mut self.project, new_project); + + let snapshot = FileChangeSet::new_inserts( + self.memory_changes + .iter() + .map(|(path, content)| { + let content = Bytes::from(content.clone().text().as_bytes()); + (path.clone(), FileResult::Ok(content).into()) + }) + .collect(), + ); + + self.project + .interrupt(Interrupt::Memory(MemoryEvent::Update(snapshot))); + + rayon::spawn(move || { + old_project.stop(); + }); + + Ok(self.project.primary_id().clone()) + } + + /// Restart the server with the given group. + pub fn restart_dedicate( + &mut self, + dedicate: &str, + entry: Option, + ) -> Result { + let entry = self.config.compile.entry_resolver.resolve(entry); + self.project.restart_dedicate(dedicate, entry) + } + + // pub async fn settle(&mut self) { + // let _ = self.change_entry(None); + // log::info!("TypstActor({}): settle requested", self.handle.diag_group); + // match self.handle.settle().await { + // Ok(()) => log::info!("TypstActor({}): settled", + // self.handle.diag_group), Err(err) => error!( + // "TypstActor({}): failed to settle: {err:#}", + // self.handle.diag_group + // ), + // } + // } + + /// Create a fresh [`ProjectState`]. + pub fn project( + config: &Config, + editor_tx: tokio::sync::mpsc::UnboundedSender, + client: TypedLspClient, + preview: ProjectPreviewState, + ) -> ProjectState { + let const_config = &config.const_config; + + // Run Export actors before preparing cluster to avoid loss of events + let export = crate::task::ExportTask::new( + client.handle.clone(), + Some(editor_tx.clone()), + config.export(), + ); + + // Create the compile handler for client consuming results. + let periscope_args = config.compile.periscope_args.clone(); + let handle = Arc::new(CompileHandlerImpl { + #[cfg(feature = "preview")] + preview, + export: export.clone(), + editor_tx: editor_tx.clone(), + client: Box::new(client.clone().to_untyped()), + analysis: Arc::new(Analysis { + position_encoding: const_config.position_encoding, + allow_overlapping_token: const_config.tokens_overlapping_token_support, + allow_multiline_token: const_config.tokens_multiline_token_support, + remove_html: !config.support_html_in_markdown, + completion_feat: config.completion.clone(), + color_theme: match config.compile.color_theme.as_deref() { + Some("dark") => tinymist_query::ColorTheme::Dark, + _ => tinymist_query::ColorTheme::Light, + }, + periscope: periscope_args.map(|args| { + let r = TypstPeriscopeProvider(PeriscopeRenderer::new(args)); + Arc::new(r) as Arc + }), + tokens_caches: Arc::default(), + workers: Default::default(), + caches: Default::default(), + analysis_rev_cache: Arc::default(), + stats: Arc::default(), + }), + + notified_revision: Mutex::default(), + }); + + let default_path = config.compile.entry_resolver.resolve_default(); + let entry = config.compile.entry_resolver.resolve(default_path); + let inputs = config.compile.determine_inputs(); + let cert_path = config.compile.determine_certification_path(); + let package = config.compile.determine_package_opts(); + + log::info!("ServerState: creating ProjectState, entry: {entry:?}, inputs: {inputs:?}"); + + // todo: never fail? + let embedded_fonts = Arc::new(LspUniverseBuilder::only_embedded_fonts().unwrap()); + let package_registry = + LspUniverseBuilder::resolve_package(cert_path.clone(), Some(&package)); + let verse = LspUniverseBuilder::build(entry, inputs, embedded_fonts, package_registry); + + // todo: unify filesystem watcher + let (dep_tx, dep_rx) = tokio::sync::mpsc::unbounded_channel(); + let fs_client = client.clone().to_untyped(); + let async_handle = client.handle.clone(); + async_handle.spawn(watch_deps(dep_rx, move |event| { + fs_client.send_event(LspInterrupt::Fs(event)); + })); + + // Create the actor + let compile_handle = handle.clone(); + let compiler = ProjectCompiler::new( + verse, + dep_tx, + CompileServerOpts { + handler: compile_handle, + enable_watch: true, + ..Default::default() + }, + ); + + // Delayed Loads fonts + let font_client = client.clone(); + let font_resolver = config.compile.determine_fonts(); + client.handle.spawn_blocking(move || { + // Refresh fonts + font_client.send_event(LspInterrupt::Font(font_resolver.wait().clone())); + }); + + ProjectState { + compiler, + preview: Default::default(), + analysis: handle.analysis.clone(), + stats: CompilerQueryStats::default(), + export: handle.export.clone(), + } + } +} + #[derive(Default)] pub struct ProjectInsStateExt { pub is_compiling: bool, @@ -109,6 +295,20 @@ impl ProjectState { } } +struct TypstPeriscopeProvider(PeriscopeRenderer); + +impl PeriscopeProvider for TypstPeriscopeProvider { + /// Resolve periscope image at the given position. + fn periscope_at( + &self, + ctx: &mut LocalContext, + doc: VersionedDocument, + pos: TypstPosition, + ) -> Option { + self.0.render_marked(ctx, doc, pos) + } +} + #[derive(Default, Clone)] pub struct ProjectPreviewState { #[cfg(feature = "preview")] diff --git a/crates/tinymist/src/state/server.rs b/crates/tinymist/src/state/server.rs index e56bd3b0..40a9676b 100644 --- a/crates/tinymist/src/state/server.rs +++ b/crates/tinymist/src/state/server.rs @@ -4,35 +4,23 @@ use std::path::{Path, PathBuf}; use std::sync::Arc; use lsp_types::*; -use parking_lot::Mutex; use sync_lsp::*; -use task::ExportUserConfig; -use tinymist_project::{EntryResolver, Interrupt, LspCompileSnapshot, ProjectInsId}; -use tinymist_query::analysis::{Analysis, PeriscopeProvider}; -use tinymist_query::{ - CompilerQueryRequest, LocalContext, LspWorldExt, OnExportRequest, ServerInfoResponse, - VersionedDocument, -}; -use tinymist_render::PeriscopeRenderer; +use tinymist_project::{EntryResolver, LspCompileSnapshot, ProjectInsId}; +use tinymist_query::{LspWorldExt, OnExportRequest, ServerInfoResponse}; use tinymist_std::error::prelude::*; use tinymist_std::ImmutPath; use tokio::sync::mpsc; -use typst::diag::FileResult; -use typst::layout::Position as TypstPosition; use typst::syntax::Source; use crate::actor::editor::{EditorActor, EditorRequest}; use crate::project::{ - update_lock, watch_deps, CompileHandlerImpl, CompileServerOpts, LspInterrupt, LspQuerySnapshot, - ProjectCompiler, ProjectPreviewState, ProjectState, QuerySnapWithStat, + update_lock, LspInterrupt, ProjectPreviewState, ProjectState, PROJECT_ROUTE_USER_ACTION_PRIORITY, }; use crate::route::ProjectRouteState; -use crate::state::query::OnEnter; -use crate::stats::CompilerQueryStats; +use crate::state::lsp_query::OnEnter; use crate::task::{ExportTask, FormatTask, UserActionTask}; -use crate::vfs::{Bytes, FileChangeSet, MemoryEvent}; -use crate::world::{LspUniverseBuilder, TaskInputs}; +use crate::world::TaskInputs; use crate::{init::*, *}; pub(crate) use futures::Future; @@ -53,10 +41,21 @@ pub(crate) fn as_path_pos(inp: TextDocumentPositionParams) -> (PathBuf, Position pub struct ServerState { /// The lsp client pub client: TypedLspClient, + + // State /// The project route state. pub route: ProjectRouteState, /// The project state. pub project: ProjectState, + /// The preview state. + #[cfg(feature = "preview")] + pub preview: tool::preview::PreviewState, + /// The formatter tasks running in backend, which will be scheduled by async + /// runtime. + pub formatter: FormatTask, + /// The user action tasks running in backend, which will be scheduled by + /// async runtime. + pub user_action: UserActionTask, // State to synchronize with the client. /// Whether the server has registered semantic tokens capabilities. @@ -75,21 +74,10 @@ pub struct ServerState { // Configurations /// User configuration from the editor. pub config: Config, - - // Resources /// Source synchronized with client pub memory_changes: HashMap, Source>, - /// The preview state. - #[cfg(feature = "preview")] - pub preview: tool::preview::PreviewState, /// The diagnostics sender to send diagnostics to `crate::actor::cluster`. pub editor_tx: mpsc::UnboundedSender, - /// The formatter tasks running in backend, which will be scheduled by async - /// runtime. - pub formatter: FormatTask, - /// The user action tasks running in backend, which will be scheduled by - /// async runtime. - pub user_action: UserActionTask, } /// Getters and the main loop. @@ -126,6 +114,21 @@ impl ServerState { } } + /// Gets the const configuration. + pub fn const_config(&self) -> &ConstConfig { + &self.config.const_config + } + + /// Gets the compile configuration. + pub fn compile_config(&self) -> &CompileConfig { + &self.config.compile + } + + /// Gets the entry resolver. + pub fn entry_resolver(&self) -> &EntryResolver { + &self.compile_config().entry_resolver + } + /// The entry point for the language server. pub fn main(client: TypedLspClient, config: Config, start: bool) -> Self { log::info!("LanguageState: initialized with config {config:?}"); @@ -153,44 +156,7 @@ impl ServerState { service } - /// Get the const configuration. - pub fn const_config(&self) -> &ConstConfig { - &self.config.const_config - } - - /// Get the compile configuration. - pub fn compile_config(&self) -> &CompileConfig { - &self.config.compile - } - - /// Get the entry resolver. - pub fn entry_resolver(&self) -> &EntryResolver { - &self.compile_config().entry_resolver - } - - /// Snapshot the compiler thread for tasks - pub fn snapshot(&mut self) -> Result { - self.project.snapshot() - } - - /// Snapshot the compiler thread for language queries - pub fn query_snapshot(&mut self) -> Result { - self.project.query_snapshot(None) - } - - /// Snapshot the compiler thread for language queries - pub fn query_snapshot_with_stat( - &mut self, - q: &CompilerQueryRequest, - ) -> Result { - let name: &'static str = q.into(); - let path = q.associated_path(); - let stat = self.project.stats.query_stat(path, name); - let snap = self.project.query_snapshot(Some(q))?; - Ok((snap, stat)) - } - - /// Install handlers to the language server. + /// Installs handlers to the language server. pub fn install + AddCommands + 'static>( provider: LspBuilder, ) -> LspBuilder { @@ -288,6 +254,7 @@ impl ServerState { provider } + /// Handles the project interrupts. fn compile_interrupt>( mut state: ServiceState, params: LspInterrupt, @@ -306,7 +273,7 @@ impl ServerState { } impl ServerState { - /// Get the current server info. + /// Gets the current server info. pub fn collect_server_info(&mut self) -> QueryFuture { let dg = self.project.primary_id().to_string(); let api_stats = self.project.stats.report(); @@ -333,167 +300,7 @@ impl ServerState { }) } - /// Restart the primary server. - pub fn restart_primary(&mut self) -> Result { - // todo: hot replacement - #[cfg(feature = "preview")] - self.preview.stop_all(); - - let watchers = self.preview.watchers.clone(); - let editor_tx = self.editor_tx.clone(); - - let new_project = Self::project(&self.config, editor_tx, self.client.clone(), watchers); - - let mut old_project = std::mem::replace(&mut self.project, new_project); - - let snapshot = FileChangeSet::new_inserts( - self.memory_changes - .iter() - .map(|(path, content)| { - let content = Bytes::from(content.clone().text().as_bytes()); - (path.clone(), FileResult::Ok(content).into()) - }) - .collect(), - ); - - self.project - .interrupt(Interrupt::Memory(MemoryEvent::Update(snapshot))); - - rayon::spawn(move || { - old_project.stop(); - }); - - Ok(self.project.primary_id().clone()) - } - - /// Restart the server with the given group. - pub fn restart_dedicate( - &mut self, - dedicate: &str, - entry: Option, - ) -> Result { - let entry = self.config.compile.entry_resolver.resolve(entry); - self.project.restart_dedicate(dedicate, entry) - } - - // pub async fn settle(&mut self) { - // let _ = self.change_entry(None); - // log::info!("TypstActor({}): settle requested", self.handle.diag_group); - // match self.handle.settle().await { - // Ok(()) => log::info!("TypstActor({}): settled", - // self.handle.diag_group), Err(err) => error!( - // "TypstActor({}): failed to settle: {err:#}", - // self.handle.diag_group - // ), - // } - // } - - /// Create a fresh [`ProjectState`]. - pub fn project( - config: &Config, - editor_tx: tokio::sync::mpsc::UnboundedSender, - client: TypedLspClient, - preview: project::ProjectPreviewState, - ) -> ProjectState { - let const_config = &config.const_config; - - // Run Export actors before preparing cluster to avoid loss of events - let export = ExportTask::new( - client.handle.clone(), - Some(editor_tx.clone()), - config.export(), - ); - - // Create the compile handler for client consuming results. - let periscope_args = config.compile.periscope_args.clone(); - let handle = Arc::new(CompileHandlerImpl { - #[cfg(feature = "preview")] - preview, - export: export.clone(), - editor_tx: editor_tx.clone(), - client: Box::new(client.clone().to_untyped()), - analysis: Arc::new(Analysis { - position_encoding: const_config.position_encoding, - allow_overlapping_token: const_config.tokens_overlapping_token_support, - allow_multiline_token: const_config.tokens_multiline_token_support, - remove_html: !config.support_html_in_markdown, - completion_feat: config.completion.clone(), - color_theme: match config.compile.color_theme.as_deref() { - Some("dark") => tinymist_query::ColorTheme::Dark, - _ => tinymist_query::ColorTheme::Light, - }, - periscope: periscope_args.map(|args| { - let r = TypstPeriscopeProvider(PeriscopeRenderer::new(args)); - Arc::new(r) as Arc - }), - tokens_caches: Arc::default(), - workers: Default::default(), - caches: Default::default(), - analysis_rev_cache: Arc::default(), - stats: Arc::default(), - }), - - notified_revision: Mutex::default(), - }); - - let default_path = config.compile.entry_resolver.resolve_default(); - let entry = config.compile.entry_resolver.resolve(default_path); - let inputs = config.compile.determine_inputs(); - let cert_path = config.compile.determine_certification_path(); - let package = config.compile.determine_package_opts(); - - log::info!("ServerState: creating ProjectState, entry: {entry:?}, inputs: {inputs:?}"); - - // todo: never fail? - let embedded_fonts = Arc::new(LspUniverseBuilder::only_embedded_fonts().unwrap()); - let package_registry = - LspUniverseBuilder::resolve_package(cert_path.clone(), Some(&package)); - let verse = LspUniverseBuilder::build(entry, inputs, embedded_fonts, package_registry); - - // todo: unify filesystem watcher - let (dep_tx, dep_rx) = tokio::sync::mpsc::unbounded_channel(); - let fs_client = client.clone().to_untyped(); - let async_handle = client.handle.clone(); - async_handle.spawn(watch_deps(dep_rx, move |event| { - fs_client.send_event(LspInterrupt::Fs(event)); - })); - - // Create the actor - let compile_handle = handle.clone(); - let compiler = ProjectCompiler::new( - verse, - dep_tx, - CompileServerOpts { - handler: compile_handle, - enable_watch: true, - ..Default::default() - }, - ); - - // Delayed Loads fonts - let font_client = client.clone(); - let font_resolver = config.compile.determine_fonts(); - client.handle.spawn_blocking(move || { - // Refresh fonts - font_client.send_event(LspInterrupt::Font(font_resolver.wait().clone())); - }); - - ProjectState { - compiler, - preview: Default::default(), - analysis: handle.analysis.clone(), - stats: CompilerQueryStats::default(), - export: handle.export.clone(), - } - } -} - -impl ServerState { - pub(crate) fn change_export_config(&mut self, config: ExportUserConfig) { - self.project.export.change_config(config); - } - - /// Export the current document. + /// Exports the current document. pub fn on_export(&mut self, req: OnExportRequest) -> QueryFuture { let OnExportRequest { path, task, open } = req; let entry = self.entry_resolver().resolve(Some(path.as_path().into())); @@ -547,20 +354,6 @@ impl ServerState { } } -struct TypstPeriscopeProvider(PeriscopeRenderer); - -impl PeriscopeProvider for TypstPeriscopeProvider { - /// Resolve periscope image at the given position. - fn periscope_at( - &self, - ctx: &mut LocalContext, - doc: VersionedDocument, - pos: TypstPosition, - ) -> Option { - self.0.render_marked(ctx, doc, pos) - } -} - #[test] fn test_as_path() { use reflexo::path::PathClean;