use reflexo_typst::TypstDocument;
use serde::{Deserialize, Serialize};
pub use tinymist_project::*;
use std::sync::atomic::AtomicUsize;
use std::{num::NonZeroUsize, sync::Arc};
use parking_lot::Mutex;
use reflexo::hash::FxHashMap;
use sync_ls::{LspClient, TypedLspClient};
use tinymist_project::vfs::{FileChangeSet, MemoryEvent};
use tinymist_query::analysis::{Analysis, LspQuerySnapshot, PeriscopeProvider};
use tinymist_query::{
CheckRequest, CompilerQueryRequest, DiagnosticsMap, LocalContext, SemanticRequest, GLOBAL_STATS,
};
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::{EditorRequest, ProjVersion};
use crate::stats::{CompilerQueryStats, QueryStatGuard};
#[cfg(feature = "export")]
use crate::task::ExportUserConfig;
use crate::vfs::notify::NotifyMessage;
use crate::Config;
#[cfg(feature = "preview")]
use crate::ServerEvent;
type EditorSender = mpsc::UnboundedSender<EditorRequest>;
pub type LspProjectCompiler = ProjectCompiler<LspCompilerFeat, ProjectInsStateExt>;
impl ServerState {
#[cfg(feature = "export")]
pub fn change_export_config(&mut self, config: ExportUserConfig) {
self.project.export.change_config(config);
}
pub fn snapshot(&mut self) -> Result<LspComputeGraph> {
self.project.snapshot()
}
pub fn query_snapshot(&mut self) -> Result<LspQuerySnapshot> {
self.project.query_snapshot(None)
}
pub fn query_snapshot_with_stat(
&mut self,
q: &CompilerQueryRequest,
) -> Result<QuerySnapWithStat> {
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))
}
pub fn reload_projects(&mut self) -> Result<()> {
#[cfg(feature = "preview")]
self.preview.stop_all();
let editor_tx = self.editor_tx.clone();
let new_project = Self::project(
&self.config,
editor_tx,
self.client.clone(),
self.dep_tx.clone(),
#[cfg(feature = "preview")]
self.preview.watchers.clone(),
#[cfg(all(not(feature = "system"), feature = "web"))]
if let sync_ls::TransportHost::Js { sender, .. } =
self.client.clone().to_untyped().sender
{
sender.resolve_fn
} else {
panic!("Expected Js TransportHost")
},
);
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_string(content.clone().text().to_owned());
(path.clone(), FileResult::Ok(content).into())
})
.collect(),
);
self.project
.interrupt(Interrupt::Memory(MemoryEvent::Update(snapshot)));
spawn_cpu(move || {
old_project.stop();
});
Ok(())
}
pub fn restart_dedicate(
&mut self,
dedicate: &str,
entry: Option<ImmutPath>,
) -> Result<ProjectInsId> {
let entry = self.config.entry_resolver.resolve(entry);
self.project.restart_dedicate(dedicate, entry)
}
pub fn project(
config: &Config,
editor_tx: mpsc::UnboundedSender<EditorRequest>,
client: TypedLspClient<ServerState>,
dep_tx: mpsc::UnboundedSender<NotifyMessage>,
#[cfg(feature = "preview")] preview: ProjectPreviewState,
#[cfg(all(not(feature = "system"), feature = "web"))] resolve_fn: js_sys::Function,
) -> ProjectState {
let const_config = &config.const_config;
#[cfg(feature = "export")]
let export = crate::task::ExportTask::new(
client.handle.clone(),
Some(editor_tx.clone()),
config.export(),
);
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: config.support_client_codelens,
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<dyn PeriscopeProvider + Send + Sync>
}),
local_packages: Arc::default(),
tokens_caches: Arc::default(),
workers: Default::default(),
caches: Default::default(),
analysis_rev_cache: Arc::default(),
stats: Arc::default(),
}),
status_revision: Mutex::default(),
notified_revision: Mutex::default(),
});
let export_target = config.export_target;
let default_path = config.entry_resolver.resolve_default();
let entry = config.entry_resolver.resolve(default_path);
let inputs = config.inputs();
let cert_path = config.certification_path();
let package = config.package_opts();
let features = config.typst_features().unwrap_or_default();
let access_model = config.access_model(&client);
log::info!("ServerState: creating ProjectState, entry: {entry:?}, inputs: {inputs:?}");
let fonts = config.fonts();
#[cfg(all(not(feature = "system"), feature = "web"))]
let packages =
LspUniverseBuilder::resolve_package(cert_path.clone(), Some(&package), resolve_fn);
#[cfg(any(feature = "system", not(feature = "web")))]
let packages = LspUniverseBuilder::resolve_package(cert_path.clone(), Some(&package));
let creation_timestamp = config.creation_timestamp();
let verse = LspUniverseBuilder::build(
entry,
export_target,
features,
inputs,
packages,
fonts,
creation_timestamp,
access_model,
);
let compile_handle = handle.clone();
let compiler = ProjectCompiler::new(
verse,
dep_tx,
CompileServerOpts {
handler: compile_handle,
export_target: config.export_target,
syntax_only: config.syntax_only,
ignore_first_sync: true,
},
);
ProjectState {
compiler,
#[cfg(feature = "preview")]
preview: handle.preview.clone(),
analysis: handle.analysis.clone(),
stats: CompilerQueryStats::default(),
#[cfg(feature = "export")]
export: handle.export.clone(),
}
}
}
#[derive(Default)]
pub struct ProjectInsStateExt {
pub notified_revision: usize,
pub pending_reasons: CompileSignal,
pub emitted_reasons: CompileSignal,
pub compiling_since: Option<tinymist_std::time::Time>,
pub last_compilation: Option<LspCompiledArtifact>,
}
impl ProjectInsStateExt {
pub fn compiled(
&mut self,
revision: &NonZeroUsize,
handler: &dyn CompileHandler<LspCompilerFeat, ProjectInsStateExt>,
compilation: &LspCompiledArtifact,
) {
self.compiling_since = None;
let rev = compilation.world().revision().get();
if self.notified_revision >= rev {
return;
}
self.notified_revision = rev;
self.last_compilation = Some(compilation.clone());
self.emit_pending_reasons(revision, handler);
}
pub fn emit_pending_reasons(
&mut self,
revision: &NonZeroUsize,
handler: &dyn CompileHandler<LspCompilerFeat, ProjectInsStateExt>,
) -> bool {
let Some(last_compilation) = self.last_compilation.as_ref() else {
return false;
};
let last_rev = last_compilation.world().revision();
if last_rev != *revision {
log::info!("skipping emit pending reasons for {revision:?} != {last_rev:?}");
return false;
}
let pending_reasons = self.pending_reasons.exclude(self.emitted_reasons);
if !pending_reasons.any() {
return false;
}
self.emitted_reasons.merge(self.pending_reasons);
let last_compilation = last_compilation.clone().with_signal(pending_reasons);
handler.notify_compile(&last_compilation);
self.pending_reasons = CompileSignal::default();
true
}
}
pub struct ProjectState {
pub compiler: LspProjectCompiler,
pub analysis: Arc<Analysis>,
pub stats: CompilerQueryStats,
#[cfg(feature = "preview")]
pub preview: ProjectPreviewState,
#[cfg(feature = "export")]
pub export: crate::task::ExportTask,
}
impl ProjectState {
pub fn primary_id(&self) -> &ProjectInsId {
&self.compiler.primary.id
}
pub fn snapshot(&mut self) -> Result<LspComputeGraph> {
Ok(self.compiler.snapshot())
}
pub fn query_snapshot(&mut self, q: Option<&CompilerQueryRequest>) -> Result<LspQuerySnapshot> {
let snap = self.snapshot()?;
Ok(self.analysis.clone().query_snapshot(snap, q))
}
pub fn do_interrupt(compiler: &mut LspProjectCompiler, intr: Interrupt<LspCompilerFeat>) {
if let Interrupt::Compiled(compiled) = &intr {
let proj = compiler.projects().find(|p| &p.id == compiled.id());
if let Some(proj) = proj {
proj.ext
.compiled(&proj.verse.revision, proj.handler.as_ref(), compiled);
} else {
log::info!(
"Project({:?}): process compiled not found",
compiled.snap.id
);
}
}
compiler.process(intr);
}
pub fn interrupt(&mut self, intr: Interrupt<LspCompilerFeat>) {
Self::do_interrupt(&mut self.compiler, intr);
}
pub(crate) fn stop(&mut self) {
}
pub(crate) fn restart_dedicate(
&mut self,
group: &str,
entry: EntryState,
) -> Result<ProjectInsId> {
self.compiler.restart_dedicate(group, entry)
}
}
struct TypstPeriscopeProvider(PeriscopeRenderer);
impl PeriscopeProvider for TypstPeriscopeProvider {
fn periscope_at(
&self,
ctx: &mut LocalContext,
doc: &TypstDocument,
pos: TypstPosition,
) -> Option<String> {
self.0.render_marked(ctx, doc, pos)
}
}
#[derive(Default, Clone)]
pub struct ProjectPreviewState {
#[cfg(feature = "preview")]
pub(crate) inner: Arc<Mutex<FxHashMap<ProjectInsId, Arc<tinymist_preview::CompileWatcher>>>>,
}
#[cfg(feature = "preview")]
impl ProjectPreviewState {
#[must_use]
pub fn register(
&self,
id: &ProjectInsId,
handle: &Arc<tinymist_preview::CompileWatcher>,
) -> bool {
let mut p = self.inner.lock();
if p.contains_key(id) {
return false;
}
p.insert(id.clone(), handle.clone());
true
}
#[must_use]
pub fn unregister(&self, task_id: &ProjectInsId) -> bool {
self.inner.lock().remove(task_id).is_some()
}
#[must_use]
pub fn get(&self, task_id: &ProjectInsId) -> Option<Arc<tinymist_preview::CompileWatcher>> {
self.inner.lock().get(task_id).cloned()
}
}
pub struct CompileHandlerImpl {
pub(crate) analysis: Arc<Analysis>,
#[cfg(feature = "preview")]
pub(crate) preview: ProjectPreviewState,
pub is_standalone: bool,
#[cfg(feature = "export")]
pub(crate) export: crate::task::ExportTask,
pub(crate) editor_tx: EditorSender,
pub(crate) client: Arc<dyn ProjectClient>,
pub(crate) status_revision: Mutex<FxHashMap<ProjectInsId, usize>>,
pub(crate) notified_revision: Mutex<FxHashMap<ProjectInsId, (usize, CompileSignal)>>,
}
pub trait ProjectClient: Send + Sync + 'static {
fn interrupt(&self, event: LspInterrupt);
#[cfg(feature = "preview")]
fn server_event(&self, event: ServerEvent);
#[cfg(feature = "export")]
fn dev_event(&self, event: DevEvent);
}
impl ProjectClient for LspClient {
fn interrupt(&self, event: LspInterrupt) {
self.send_event(event);
}
#[cfg(feature = "preview")]
fn server_event(&self, event: ServerEvent) {
self.send_event(event);
}
#[cfg(feature = "export")]
fn dev_event(&self, event: DevEvent) {
self.send_notification::<DevEvent>(&event);
}
}
impl ProjectClient for mpsc::UnboundedSender<LspInterrupt> {
fn interrupt(&self, event: LspInterrupt) {
self.send(event).log_error("failed to send interrupt");
}
#[cfg(feature = "preview")]
fn server_event(&self, _event: ServerEvent) {
log::warn!("ProjectClient: server_event is not implemented for mpsc::UnboundedSender<LspInterrupt>");
}
#[cfg(feature = "export")]
fn dev_event(&self, _event: DevEvent) {
log::warn!(
"ProjectClient: dev_event is not implemented for mpsc::UnboundedSender<LspInterrupt>"
);
}
}
impl CompileHandlerImpl {
fn push_diagnostics(&self, dv: ProjVersion, diagnostics: Option<DiagnosticsMap>) {
self.editor_tx
.send(EditorRequest::Diag(dv, diagnostics))
.log_error("failed to send diagnostics");
}
fn notify_diagnostics(&self, art: &LspCompiledArtifact) {
let dv = ProjVersion {
id: art.id().clone(),
revision: art.world().revision().get(),
};
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());
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");
});
}
}
}
impl CompileHandler<LspCompilerFeat, ProjectInsStateExt> for CompileHandlerImpl {
fn on_any_compile_reason(&self, c: &mut LspProjectCompiler) {
let instances_mut = std::iter::once(&mut c.primary).chain(c.dedicates.iter_mut());
for s in instances_mut {
let reason = s.reason;
if !reason.any() {
continue;
}
let id = &s.id;
if let Some(compiling_since) = &s.ext.compiling_since {
static CHECK_COMPILE_TIMEOUT: AtomicUsize = AtomicUsize::new(0);
let check_stalled = (CHECK_COMPILE_TIMEOUT
.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
& 0xFF)
== 0;
if check_stalled {
let since = (tinymist_std::time::now().duration_since(*compiling_since))
.unwrap_or_default();
if since.as_secs() > 60 {
log::warn!(
"Project({id:?}): compiling for more than 60 seconds, since: {since:?}, \
pending reasons: {:?}",
s.ext.pending_reasons
);
}
}
continue;
}
const VFS_SUB: CompileSignal = CompileSignal {
by_mem_events: true,
by_fs_events: true,
by_entry_update: false,
};
let is_vfs_sub = reason.any() && !reason.exclude(VFS_SUB).any();
if is_vfs_sub
&& 'vfs_is_clean: {
let Some(compilation) = &s.ext.last_compilation else {
break 'vfs_is_clean false;
};
if compilation.world().entry_state() != s.verse.entry_state() {
log::info!("Project: updated regardless of vfs for {id:?} due to entry state change, world: {:?} v.s. verse: {:?}",
compilation.world().entry_state(),
s.verse.entry_state(),
);
break 'vfs_is_clean false;
}
let last_rev = compilation.world().vfs().revision();
let deps = compilation.depended_files().clone();
s.verse.vfs().is_clean_compile(last_rev.get(), &deps)
}
{
s.ext.pending_reasons.merge(reason);
s.reason = CompileSignal::default();
let pending_reasons = s.ext.pending_reasons.exclude(s.ext.emitted_reasons);
let emitted = s
.ext
.emit_pending_reasons(&s.verse.revision, s.handler.as_ref());
if !emitted {
log::info!("Project: skip compilation for {id:?} due to harmless vfs changes");
} else {
log::info!(
"Project: emit compilation again for {id:?}, reason: {pending_reasons:?}"
);
}
continue;
}
s.ext.pending_reasons = CompileSignal::default();
s.ext.emitted_reasons = reason;
let Some(compile_fn) = s.may_compile(&c.handler) else {
continue;
};
let id = s.snapshot().world().main_id();
s.ext.compiling_since = Some(tinymist_std::time::now());
spawn_cpu(move || {
let _guard = GLOBAL_STATS.stat(id, "main_compile");
compile_fn();
});
}
}
fn status(&self, revision: usize, rep: CompileReport) {
{
let mut n_revs = self.status_revision.lock();
let n_rev = n_revs.entry(rep.id.clone()).or_default();
if *n_rev > revision {
log::info!("Project: outdated status for revision {revision} <= {n_rev}");
return;
}
*n_rev = revision;
}
if matches!(rep.status, tinymist_project::CompileStatusEnum::Suspend) {
let dv = ProjVersion {
id: rep.id.clone(),
revision,
};
self.push_diagnostics(dv, 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,
});
}
self.editor_tx.send(EditorRequest::Status(rep)).unwrap();
}
fn notify_removed(&self, id: &ProjectInsId) {
let n_revs = &mut self.notified_revision.lock();
let last_rev = n_revs.remove(id).unwrap_or_default();
let dv = ProjVersion {
id: id.clone(),
revision: last_rev.0,
};
self.push_diagnostics(dv, None);
}
fn notify_compile(&self, art: &LspCompiledArtifact) {
self.client.interrupt(LspInterrupt::Compiled(art.clone()));
{
let mut n_revs = self.notified_revision.lock();
let (n_rev, n_signal) = n_revs.entry(art.id().clone()).or_default();
let rev = art.world().revision().get();
if *n_rev < rev {
*n_rev = rev;
*n_signal = art.snap.signal;
} else if *n_rev == rev && n_signal.exclude(art.snap.signal).any() {
n_signal.merge(art.snap.signal);
} else {
log::info!(
"Project: already notified for revision {} <= {n_rev}, signal: {n_signal:?} contains {:?}",
art.world().revision(),
art.snap.signal
);
return;
}
}
#[cfg(feature = "system")]
if self.is_standalone {
crate::project::system::print_diagnostics(
art.world(),
art.diagnostics(),
reflexo_typst::DiagnosticFormat::Human,
)
.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);
}
}
pub type QuerySnapWithStat = (LspQuerySnapshot, QueryStatGuard);
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DevExportEvent {
pub id: String,
pub when: TaskWhen,
pub need_export: bool,
pub signal: CompileSignal,
pub path: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "type")]
pub enum DevEvent {
Export(DevExportEvent),
}
impl lsp_types::notification::Notification for DevEvent {
const METHOD: &'static str = "tinymist/devEvent";
type Params = Self;
}