diff --git a/crates/tinymist-query/src/check.rs b/crates/tinymist-query/src/check.rs index 12c7b26f5..bc07edb57 100644 --- a/crates/tinymist-query/src/check.rs +++ b/crates/tinymist-query/src/check.rs @@ -10,14 +10,24 @@ pub struct CheckRequest { pub snap: LspCompiledArtifact, } +/// The diagnostics emitted by a full check run. +#[derive(Debug, Clone, Default)] +pub struct DiagnosticsResult { + /// Diagnostics reported by the compiler. + pub compiler: DiagnosticsMap, + /// Diagnostics reported by lint passes. + pub lint: DiagnosticsMap, +} + impl SemanticRequest for CheckRequest { - type Response = DiagnosticsMap; + type Response = DiagnosticsResult; fn request(self, ctx: &mut LocalContext) -> Option { - let worker = DiagWorker::new(ctx); - let compiler_diags = self.snap.diagnostics(); + let compiler_diags: Vec<_> = self.snap.diagnostics().cloned().collect(); + let known_issues = KnownIssues::from_compiler_diagnostics(compiler_diags.iter()); + let lint = DiagWorker::new(ctx).check(&known_issues).results; + let compiler = DiagWorker::new(ctx).convert_all(compiler_diags.iter()); - let known_issues = KnownIssues::from_compiler_diagnostics(compiler_diags.clone()); - Some(worker.check(&known_issues).convert_all(compiler_diags)) + Some(DiagnosticsResult { compiler, lint }) } } diff --git a/crates/tinymist/src/actor/editor.rs b/crates/tinymist/src/actor/editor.rs index 845416332..aca7acb1b 100644 --- a/crates/tinymist/src/actor/editor.rs +++ b/crates/tinymist/src/actor/editor.rs @@ -25,13 +25,34 @@ pub struct EditorActorConfig { pub enum EditorRequest { Config(EditorActorConfig), /// Publishes diagnostics to the editor. - Diag(ProjVersion, Option), + Diag(ProjVersion, DiagKind, Option), /// Updates compile status to the editor. Status(CompileReport), /// Updastes words count status to the editor. WordCount(ProjectInsId, WordsCount), } +/// The kind of diagnostics published to the editor. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum DiagKind { + /// Diagnostics reported by the Typst compiler. + Compiler, + /// Diagnostics reported by Tinymist's lint engine. + Lint, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +struct DiagKey { + pub project: ProjectInsId, + pub kind: DiagKind, +} + +impl DiagKey { + fn new(project: ProjectInsId, kind: DiagKind) -> Self { + Self { project, kind } + } +} + /// The actor maintaining output to the editor, including diagnostics and /// compile status. pub struct EditorActor { @@ -44,11 +65,11 @@ pub struct EditorActor { /// Accumulated diagnostics per file. /// The outer `HashMap` is indexed by the file's URL. - /// The inner `HashMap` is indexed by the project ID, allowing multiple - /// projects publishing diagnostics to the same file independently. - diagnostics: HashMap>>, - /// The map from project ID to the affected files. - affect_map: HashMap>, + /// The inner `HashMap` is indexed by the `(project, kind)` pair, allowing + /// multiple sources publishing diagnostics to the same file independently. + diagnostics: HashMap>>, + /// The map from `(project, kind)` to the affected files. + affect_map: HashMap>, /// The local state. status: StatusAll, @@ -100,13 +121,13 @@ impl EditorActor { log::info!("received config request: {config:?}"); self.config = config; } - EditorRequest::Diag(version, diagnostics) => { + EditorRequest::Diag(version, kind, diagnostics) => { log::debug!( "received diagnostics from {version:?}: diag({:?})", diagnostics.as_ref().map(|files| files.len()) ); - self.publish(version.id, diagnostics); + self.publish(version, kind, diagnostics); } EditorRequest::Status(compile_status) => { log::trace!("received status request: {compile_status:?}"); @@ -137,12 +158,18 @@ impl EditorActor { } /// Publishes diagnostics of a project to the editor. - pub fn publish(&mut self, id: ProjectInsId, next_diag: Option) { + pub fn publish( + &mut self, + version: ProjVersion, + kind: DiagKind, + next_diag: Option, + ) { + let key = DiagKey::new(version.id.clone(), kind); let affected = match next_diag.as_ref() { Some(next_diag) => self .affect_map - .insert(id.clone(), next_diag.keys().cloned().collect()), - None => self.affect_map.remove(&id), + .insert(key.clone(), next_diag.keys().cloned().collect()), + None => self.affect_map.remove(&key), }; // Gets sources which had some diagnostic published last time, but not this @@ -155,24 +182,24 @@ impl EditorActor { // Gets sources that affected by this group in last round but not this time for uri in affected.into_iter().flatten() { if !next_diag.as_ref().is_some_and(|e| e.contains_key(&uri)) { - self.publish_file(&id, uri, None) + self.publish_file(&key, uri, None) } } // Gets touched updates for (uri, next) in next_diag.into_iter().flatten() { - self.publish_file(&id, uri, Some(next)) + self.publish_file(&key, uri, Some(next)) } } /// Publishes diagnostics of a file to the editor. - fn publish_file(&mut self, id: &ProjectInsId, uri: Url, next: Option>) { + fn publish_file(&mut self, key: &DiagKey, uri: Url, next: Option>) { let mut diagnostics = EcoVec::new(); // Gets the diagnostics from other groups let path_diags = self.diagnostics.entry(uri.clone()).or_default(); - for (existing_id, diags) in path_diags.iter() { - if existing_id != id { + for (existing_key, diags) in path_diags.iter() { + if existing_key != key { diagnostics.push(diags.clone()); } } @@ -184,8 +211,8 @@ impl EditorActor { // Updates the diagnostics for this group match next { - Some(next) => path_diags.insert(id.clone(), next), - None => path_diags.remove(id), + Some(next) => path_diags.insert(key.clone(), next), + None => path_diags.remove(key), }; // Publishes the diagnostics diff --git a/crates/tinymist/src/project.rs b/crates/tinymist/src/project.rs index 2eded4db2..db89c1700 100644 --- a/crates/tinymist/src/project.rs +++ b/crates/tinymist/src/project.rs @@ -38,7 +38,7 @@ use tokio::sync::mpsc; use typst::{diag::FileResult, foundations::Bytes, layout::Position as TypstPosition}; use super::ServerState; -use crate::actor::editor::{EditorRequest, ProjVersion}; +use crate::actor::editor::{DiagKind, EditorRequest, ProjVersion}; use crate::stats::{CompilerQueryStats, QueryStatGuard}; #[cfg(feature = "export")] use crate::task::ExportUserConfig; @@ -159,45 +159,54 @@ impl ServerState { config.export(), ); + #[cfg(feature = "preview")] + let preview_state = preview.clone(); + // Create the compile handler for client consuming results. let periscope_args = config.periscope_args.clone(); - let handle = Arc::new(CompileHandlerImpl { - #[cfg(feature = "preview")] - preview, - is_standalone: false, - #[cfg(feature = "export")] - export: export.clone(), - editor_tx: editor_tx.clone(), - client: Arc::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, - support_client_codelens: true, - extended_code_action: config.extended_code_action, - completion_feat: config.completion.clone(), - color_theme: match config.color_theme.as_deref() { - Some("dark") => tinymist_query::ColorTheme::Dark, - _ => tinymist_query::ColorTheme::Light, - }, - lint: config.lint.when().clone(), - periscope: periscope_args.map(|args| { - let r = TypstPeriscopeProvider(PeriscopeRenderer::new(args)); - Arc::new(r) as Arc - }), - local_packages: Arc::default(), - tokens_caches: Arc::default(), - workers: Default::default(), - caches: Default::default(), - analysis_rev_cache: Arc::default(), - stats: Arc::default(), + let 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, + support_client_codelens: true, + extended_code_action: config.extended_code_action, + completion_feat: config.completion.clone(), + color_theme: match config.color_theme.as_deref() { + Some("dark") => tinymist_query::ColorTheme::Dark, + _ => tinymist_query::ColorTheme::Light, + }, + lint: config.lint.when().clone(), + periscope: periscope_args.map(|args| { + let r = TypstPeriscopeProvider(PeriscopeRenderer::new(args)); + Arc::new(r) as Arc }), - - status_revision: Mutex::default(), - notified_revision: Mutex::default(), + local_packages: Arc::default(), + tokens_caches: Arc::default(), + workers: Default::default(), + caches: Default::default(), + analysis_rev_cache: Arc::default(), + stats: Arc::default(), }); + #[allow(unused_mut)] + let mut hooks: Vec> = vec![ + Box::new(DiagHook::new(analysis.clone(), editor_tx.clone())), + Box::new(LintHook::new(analysis.clone(), editor_tx.clone())), + ]; + #[cfg(feature = "preview")] + hooks.push(Box::new(PreviewHook::new(preview))); + #[cfg(feature = "export")] + hooks.push(Box::new(ExportHook::new(export.clone()))); + + let handle = CompileHandlerImpl::new( + analysis.clone(), + editor_tx.clone(), + Arc::new(client.clone().to_untyped()), + false, + hooks, + ); + let export_target = config.export_target; let default_path = config.entry_resolver.resolve_default(); let entry = config.entry_resolver.resolve(default_path); @@ -245,11 +254,11 @@ impl ServerState { ProjectState { compiler, #[cfg(feature = "preview")] - preview: handle.preview.clone(), + preview: preview_state, analysis: handle.analysis.clone(), stats: CompilerQueryStats::default(), #[cfg(feature = "export")] - export: handle.export.clone(), + export, } } } @@ -448,19 +457,234 @@ impl ProjectPreviewState { } } +fn push_editor_diagnostics( + editor_tx: &EditorSender, + dv: ProjVersion, + kind: DiagKind, + diagnostics: Option, +) { + editor_tx + .send(EditorRequest::Diag(dv, kind, diagnostics)) + .log_error("failed to send diagnostics"); +} + +/// A hook that handles diagnostics. +pub struct DiagHook { + analysis: Arc, + editor_tx: EditorSender, +} + +impl DiagHook { + /// Creates a new diagnostics hook. + pub fn new(analysis: Arc, editor_tx: EditorSender) -> Self { + Self { + analysis, + editor_tx, + } + } + + fn notify(&self, dv: ProjVersion, art: &LspCompiledArtifact) { + let enc = self.analysis.position_encoding; + let diagnostics = + tinymist_query::convert_diagnostics(art.graph.clone(), art.diagnostics(), enc); + + log::trace!( + "notify compiler diagnostics({:?}): {:#?}", + dv.id, + diagnostics + ); + + push_editor_diagnostics( + &self.editor_tx, + dv.clone(), + DiagKind::Compiler, + Some(diagnostics), + ); + } +} + +/// A hook that handles compilation events. +pub trait CompileHook { + /// Notifies the hook of a compilation result. + fn notify(&self, dv: ProjVersion, art: &LspCompiledArtifact, client: &Arc); + /// Notifies the hook of a compilation status. + fn status(&self, _revision: usize, _rep: &CompileReport) {} +} + +impl CompileHook for DiagHook { + fn notify(&self, dv: ProjVersion, art: &LspCompiledArtifact, _client: &Arc) { + if art.world().entry_state().is_inactive() { + push_editor_diagnostics(&self.editor_tx, dv.clone(), DiagKind::Compiler, None); + return; + } + + self.notify(dv, art); + } +} + +/// A hook that handles linting. +pub struct LintHook { + analysis: Arc, + editor_tx: EditorSender, +} + +impl LintHook { + /// Creates a new lint hook. + pub fn new(analysis: Arc, editor_tx: EditorSender) -> Self { + Self { + analysis, + editor_tx, + } + } + + fn notify(&self, dv: ProjVersion, art: &LspCompiledArtifact) { + let should_lint = art + .snap + .signal + .should_run_task_dyn(&self.analysis.lint, art.doc.as_ref()) + .unwrap_or_default(); + log::debug!( + "Project: should_lint: {should_lint:?}, signal: {:?}", + art.snap.signal + ); + + if !should_lint { + return; + } + + let snap = art.clone(); + let editor_tx = self.editor_tx.clone(); + let analysis = self.analysis.clone(); + spawn_cpu(move || { + let mut ctx = analysis.enter(snap.graph.clone()); + + // todo: check all errors in this file + let Some(diagnostics) = CheckRequest { snap }.request(&mut ctx) else { + return; + }; + + log::trace!( + "notify lint diagnostics({:?}): {:#?}", + dv.id, + diagnostics.lint + ); + + editor_tx + .send(EditorRequest::Diag( + dv, + DiagKind::Lint, + Some(diagnostics.lint), + )) + .log_error("failed to send lint diagnostics"); + }); + } +} + +impl CompileHook for LintHook { + fn notify(&self, dv: ProjVersion, art: &LspCompiledArtifact, _client: &Arc) { + if art.world().entry_state().is_inactive() { + push_editor_diagnostics(&self.editor_tx, dv.clone(), DiagKind::Lint, None); + return; + } + + self.notify(dv, art); + } +} + +#[cfg(feature = "preview")] +#[derive(Clone)] +/// A hook that handles preview. +pub struct PreviewHook { + state: ProjectPreviewState, +} + +#[cfg(feature = "preview")] +impl PreviewHook { + /// Creates a new preview hook. + pub fn new(state: ProjectPreviewState) -> Self { + Self { state } + } + + fn notify(&self, art: &LspCompiledArtifact) { + if let Some(inner) = self.state.get(art.id()) { + let art = art.clone(); + inner.notify_compile(Arc::new(crate::tool::preview::PreviewCompileView { art })); + } else { + log::debug!("Project: no preview for {:?}", art.id()); + } + } + + fn status(&self, _revision: usize, rep: &CompileReport) { + if let Some(inner) = self.state.get(&rep.id) { + use tinymist_preview::CompileStatus; + use tinymist_project::CompileStatusEnum::*; + + inner.status(match &rep.status { + Compiling => CompileStatus::Compiling, + Suspend | CompileSuccess { .. } => CompileStatus::CompileSuccess, + ExportError { .. } | CompileError { .. } => CompileStatus::CompileError, + }); + } + } + + #[allow(dead_code)] + fn state(&self) -> ProjectPreviewState { + self.state.clone() + } +} + +#[cfg(feature = "preview")] +impl CompileHook for PreviewHook { + fn notify( + &self, + _dv: ProjVersion, + art: &LspCompiledArtifact, + _client: &Arc, + ) { + self.notify(art); + } + + fn status(&self, revision: usize, rep: &CompileReport) { + self.status(revision, rep); + } +} + +#[cfg(feature = "export")] +#[derive(Clone)] +/// A hook that handles export. +pub struct ExportHook { + task: crate::task::ExportTask, +} + +#[cfg(feature = "export")] +impl ExportHook { + /// Creates a new export hook. + pub fn new(task: crate::task::ExportTask) -> Self { + Self { task } + } + + #[allow(dead_code)] + fn task(&self) -> crate::task::ExportTask { + self.task.clone() + } +} + +#[cfg(feature = "export")] +impl CompileHook for ExportHook { + fn notify(&self, _dv: ProjVersion, art: &LspCompiledArtifact, client: &Arc) { + self.task.signal(art, client); + } +} + /// The implementation of the compile handler. pub struct CompileHandlerImpl { /// The analysis data. pub(crate) analysis: Arc, + hooks: Vec>, - #[cfg(feature = "preview")] - pub(crate) preview: ProjectPreviewState, /// Whether the compile server is running in standalone CLI (not as a /// language server). pub is_standalone: bool, - /// The export task. - #[cfg(feature = "export")] - pub(crate) export: crate::task::ExportTask, /// The editor sender, used to send editor requests to the editor. pub(crate) editor_tx: EditorSender, /// The client used to send events back to the server itself or the clients. @@ -519,11 +743,22 @@ impl ProjectClient for mpsc::UnboundedSender { } impl CompileHandlerImpl { - /// Pushes diagnostics to the editor. - fn push_diagnostics(&self, dv: ProjVersion, diagnostics: Option) { - self.editor_tx - .send(EditorRequest::Diag(dv, diagnostics)) - .log_error("failed to send diagnostics"); + pub(crate) fn new( + analysis: Arc, + editor_tx: EditorSender, + client: Arc, + is_standalone: bool, + hooks: Vec>, + ) -> Arc { + Arc::new(Self { + analysis, + is_standalone, + editor_tx, + client, + status_revision: Mutex::default(), + notified_revision: Mutex::default(), + hooks, + }) } /// Notifies the diagnostics. @@ -532,51 +767,9 @@ impl CompileHandlerImpl { id: art.id().clone(), revision: art.world().revision().get(), }; - // todo: better way to remove diagnostics - let valid = !art.world().entry_state().is_inactive(); - if !valid { - self.push_diagnostics(dv, None); - return; - } - let should_lint = art - .snap - .signal - .should_run_task_dyn(&self.analysis.lint, art.doc.as_ref()) - .unwrap_or_default(); - log::debug!( - "Project: should_lint: {should_lint:?}, signal: {:?}", - art.snap.signal - ); - - if !should_lint { - let enc = self.analysis.position_encoding; - let diagnostics = - tinymist_query::convert_diagnostics(art.graph.clone(), art.diagnostics(), enc); - - log::trace!("notify diagnostics({dv:?}): {diagnostics:#?}"); - - self.editor_tx - .send(EditorRequest::Diag(dv, Some(diagnostics))) - .log_error("failed to send diagnostics"); - } else { - let snap = art.clone(); - let editor_tx = self.editor_tx.clone(); - let analysis = self.analysis.clone(); - spawn_cpu(move || { - let mut ctx = analysis.enter(snap.graph.clone()); - - // todo: check all errors in this file - let Some(diagnostics) = CheckRequest { snap }.request(&mut ctx) else { - return; - }; - - log::trace!("notify diagnostics({dv:?}): {diagnostics:#?}"); - - editor_tx - .send(EditorRequest::Diag(dv, Some(diagnostics))) - .log_error("failed to send diagnostics"); - }); + for hook in &self.hooks { + hook.notify(dv.clone(), art, &self.client); } } } @@ -691,19 +884,13 @@ impl CompileHandler for CompileHandlerImpl id: rep.id.clone(), revision, }; - self.push_diagnostics(dv, None); + push_editor_diagnostics(&self.editor_tx, dv.clone(), DiagKind::Compiler, None); + push_editor_diagnostics(&self.editor_tx, dv, DiagKind::Lint, None); } #[cfg(feature = "preview")] - if let Some(inner) = self.preview.get(&rep.id) { - use tinymist_preview::CompileStatus; - use tinymist_project::CompileStatusEnum::*; - - inner.status(match &rep.status { - Compiling => CompileStatus::Compiling, - Suspend | CompileSuccess { .. } => CompileStatus::CompileSuccess, - ExportError { .. } | CompileError { .. } => CompileStatus::CompileError, - }); + for hook in &self.hooks { + hook.status(revision, &rep); } self.editor_tx.send(EditorRequest::Status(rep)).unwrap(); @@ -720,7 +907,8 @@ impl CompileHandler for CompileHandlerImpl // todo: race condition with notify_compile? // remove diagnostics - self.push_diagnostics(dv, None); + push_editor_diagnostics(&self.editor_tx, dv.clone(), DiagKind::Compiler, None); + push_editor_diagnostics(&self.editor_tx, dv, DiagKind::Lint, None); } fn notify_compile(&self, art: &LspCompiledArtifact) { @@ -770,17 +958,6 @@ impl CompileHandler for CompileHandlerImpl .log_error("failed to print diagnostics"); } - #[cfg(feature = "export")] - self.export.signal(art, &self.client); - - #[cfg(feature = "preview")] - if let Some(inner) = self.preview.get(art.id()) { - let art = art.clone(); - inner.notify_compile(Arc::new(crate::tool::preview::PreviewCompileView { art })); - } else { - log::debug!("Project: no preview for {:?}", art.id()); - } - self.notify_diagnostics(art); } } diff --git a/crates/tinymist/src/task/export.rs b/crates/tinymist/src/task/export.rs index b99b61c57..02f9b42b9 100644 --- a/crates/tinymist/src/task/export.rs +++ b/crates/tinymist/src/task/export.rs @@ -16,7 +16,7 @@ use tinymist_std::path::PathClean; use tinymist_std::typst::TypstDocument; use tinymist_task::{ output_template, DocumentQuery, ExportMarkdownTask, ExportPngTask, ExportSvgTask, ExportTarget, - ImageOutput, PdfExport, PngExport, SvgExport, TextExport, + ExportTask as ProjectExportTask, ImageOutput, PdfExport, PngExport, SvgExport, TextExport, }; use tokio::sync::mpsc; use typlite::{Format, Typlite}; @@ -27,16 +27,16 @@ use parking_lot::Mutex; use rayon::Scope; use super::SyncTaskFactory; +use crate::actor::editor::EditorRequest; use crate::lsp::query::QueryFuture; use crate::project::{ update_lock, ApplyProjectTask, CompiledArtifact, DevEvent, DevExportEvent, EntryReader, - ExportHtmlTask, ExportPdfTask, ExportTask as ProjectExportTask, ExportTeXTask, ExportTextTask, - LspCompiledArtifact, LspComputeGraph, ProjectClient, ProjectTask, TaskWhen, - PROJECT_ROUTE_USER_ACTION_PRIORITY, + ExportHtmlTask, ExportPdfTask, ExportTeXTask, ExportTextTask, LspCompiledArtifact, + LspComputeGraph, ProjectClient, ProjectTask, TaskWhen, PROJECT_ROUTE_USER_ACTION_PRIORITY, }; +use crate::tool::word_count; use crate::world::TaskInputs; use crate::ServerState; -use crate::{actor::editor::EditorRequest, tool::word_count}; impl ServerState { /// Exports the current document. @@ -159,15 +159,11 @@ impl ExportTask { self.factory.mutate(|data| *data = config); } - pub(crate) fn signal( - &self, - snap: &LspCompiledArtifact, - client: &std::sync::Arc, - ) { + pub(crate) fn signal(&self, art: &LspCompiledArtifact, client: &Arc) { let config = self.factory.task(); - self.signal_export(snap, &config, client); - self.signal_count_word(snap, &config); + self.signal_export(art, &config, client); + self.signal_count_word(art, &config); } fn signal_export( diff --git a/crates/tinymist/src/tool/project.rs b/crates/tinymist/src/tool/project.rs index fab2b69ab..9871056ba 100644 --- a/crates/tinymist/src/tool/project.rs +++ b/crates/tinymist/src/tool/project.rs @@ -2,7 +2,6 @@ use std::sync::Arc; -use parking_lot::Mutex; use tinymist_query::analysis::Analysis; use tokio::sync::mpsc; @@ -76,20 +75,33 @@ where log::warn!("Project: system watcher is not enabled, file changes will not be watched"); } - // Create the actor - let compile_handle = Arc::new(CompileHandlerImpl { - #[cfg(feature = "preview")] - preview: opts.preview, - is_standalone: true, - #[cfg(feature = "export")] - export: crate::task::ExportTask::new(handle, Some(editor_tx.clone()), opts.config.export()), - editor_tx, - client: Arc::new(intr_tx.clone()), + let analysis = opts.analysis.clone(); - analysis: opts.analysis, - status_revision: Mutex::default(), - notified_revision: Mutex::default(), - }); + #[cfg(feature = "preview")] + let preview = opts.preview; + + #[cfg(feature = "export")] + let export_task = + crate::task::ExportTask::new(handle, Some(editor_tx.clone()), opts.config.export()); + + #[allow(unused_mut)] + let mut hooks: Vec> = vec![ + Box::new(DiagHook::new(analysis.clone(), editor_tx.clone())), + Box::new(LintHook::new(analysis.clone(), editor_tx.clone())), + ]; + #[cfg(feature = "preview")] + hooks.push(Box::new(PreviewHook::new(preview))); + #[cfg(feature = "export")] + hooks.push(Box::new(ExportHook::new(export_task))); + + // Create the actor + let compile_handle = CompileHandlerImpl::new( + analysis, + editor_tx.clone(), + Arc::new(intr_tx.clone()), + true, + hooks, + ); let mut compiler = ProjectCompiler::new( verse,