mirror of
https://github.com/Myriad-Dreamin/tinymist.git
synced 2025-08-04 02:08:17 +00:00
feat: run project compilations on main thread (#1197)
* dev: handle compile interrupt * dev: remove cache task use * feat: create project crate again dev: changes feat: delete unused code fix: errors fix: errors * feat: extra compilation is not needed * dev: implement all todos * fix: make event queue unbounded * fix: make preview work again * feat: event-driven recompilation * feat: evict vfs cache * feat: update evict strategy * feat: remove lock update
This commit is contained in:
parent
02a14c9cc9
commit
e4bf2e9e46
30 changed files with 1895 additions and 1778 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -4105,6 +4105,7 @@ dependencies = [
|
|||
"parking_lot",
|
||||
"pathdiff",
|
||||
"rayon",
|
||||
"reflexo-typst",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
@ -4254,7 +4255,6 @@ dependencies = [
|
|||
"tar",
|
||||
"tinymist-std",
|
||||
"tinymist-vfs",
|
||||
"toml",
|
||||
"typst",
|
||||
"typst-assets",
|
||||
"wasm-bindgen",
|
||||
|
|
|
@ -5,7 +5,7 @@ use std::{
|
|||
thread,
|
||||
};
|
||||
|
||||
use crossbeam_channel::{bounded, Receiver, Sender};
|
||||
use crossbeam_channel::{bounded, unbounded, Receiver, Sender};
|
||||
|
||||
use crate::{Connection, ConnectionRx, ConnectionTx, Message};
|
||||
|
||||
|
@ -67,7 +67,7 @@ pub fn with_stdio_transport(
|
|||
};
|
||||
let o = || std::io::stdout().lock();
|
||||
|
||||
let (event_sender, event_receiver) = bounded::<crate::Event>(10);
|
||||
let (event_sender, event_receiver) = unbounded::<crate::Event>();
|
||||
|
||||
// Create the transport. Includes the stdio (stdin and stdout) versions but this
|
||||
// could also be implemented to use sockets or HTTP.
|
||||
|
|
|
@ -23,6 +23,7 @@ parking_lot.workspace = true
|
|||
pathdiff.workspace = true
|
||||
tokio.workspace = true
|
||||
rayon.workspace = true
|
||||
reflexo-typst.workspace = true
|
||||
semver.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
|
793
crates/tinymist-project/src/compiler.rs
Normal file
793
crates/tinymist-project/src/compiler.rs
Normal file
|
@ -0,0 +1,793 @@
|
|||
//! Project Model for tinymist
|
||||
//!
|
||||
//! The [`ProjectCompiler`] implementation borrowed from typst.ts.
|
||||
//!
|
||||
//! Please check `tinymist::actor::typ_client` for architecture details.
|
||||
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use core::fmt;
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
path::Path,
|
||||
sync::{Arc, OnceLock},
|
||||
};
|
||||
|
||||
use ecow::{EcoString, EcoVec};
|
||||
use reflexo_typst::{
|
||||
features::{CompileFeature, FeatureSet, WITH_COMPILING_STATUS_FEATURE},
|
||||
CompileEnv, CompileReport, Compiler, TypstDocument,
|
||||
};
|
||||
use tinymist_std::error::prelude::ZResult;
|
||||
use tokio::sync::mpsc;
|
||||
use typst::diag::{SourceDiagnostic, SourceResult};
|
||||
|
||||
use crate::LspCompilerFeat;
|
||||
use tinymist_world::{
|
||||
vfs::{
|
||||
notify::{FilesystemEvent, MemoryEvent, NotifyMessage, UpstreamUpdateEvent},
|
||||
FsProvider, RevisingVfs,
|
||||
},
|
||||
CompilerFeat, CompilerUniverse, CompilerWorld, EntryReader, EntryState, TaskInputs, WorldDeps,
|
||||
};
|
||||
|
||||
/// LSP interrupt.
|
||||
pub type LspInterrupt = Interrupt<LspCompilerFeat>;
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
|
||||
pub struct ProjectInsId(EcoString);
|
||||
|
||||
/// A signal that possibly triggers an export.
|
||||
///
|
||||
/// Whether to export depends on the current state of the document and the user
|
||||
/// settings.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct ExportSignal {
|
||||
/// Whether the revision is annotated by memory events.
|
||||
pub by_mem_events: bool,
|
||||
/// Whether the revision is annotated by file system events.
|
||||
pub by_fs_events: bool,
|
||||
/// Whether the revision is annotated by entry update.
|
||||
pub by_entry_update: bool,
|
||||
}
|
||||
|
||||
pub struct CompileSnapshot<F: CompilerFeat> {
|
||||
/// The project id.
|
||||
pub id: ProjectInsId,
|
||||
/// The export signal for the document.
|
||||
pub signal: ExportSignal,
|
||||
/// Using env
|
||||
pub env: CompileEnv,
|
||||
/// Using world
|
||||
pub world: CompilerWorld<F>,
|
||||
/// The last successfully compiled document.
|
||||
pub success_doc: Option<Arc<TypstDocument>>,
|
||||
}
|
||||
|
||||
impl<F: CompilerFeat + 'static> CompileSnapshot<F> {
|
||||
pub fn task(mut self, inputs: TaskInputs) -> Self {
|
||||
'check_changed: {
|
||||
if let Some(entry) = &inputs.entry {
|
||||
if *entry != self.world.entry_state() {
|
||||
break 'check_changed;
|
||||
}
|
||||
}
|
||||
if let Some(inputs) = &inputs.inputs {
|
||||
if inputs.clone() != self.world.inputs() {
|
||||
break 'check_changed;
|
||||
}
|
||||
}
|
||||
|
||||
return self;
|
||||
};
|
||||
|
||||
self.world = self.world.task(inputs);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn compile(self) -> CompiledArtifact<F> {
|
||||
let mut snap = self;
|
||||
snap.world.set_is_compiling(true);
|
||||
let warned = std::marker::PhantomData.compile(&snap.world, &mut snap.env);
|
||||
snap.world.set_is_compiling(false);
|
||||
let (doc, warnings) = match warned {
|
||||
Ok(doc) => (Ok(doc.output), doc.warnings),
|
||||
Err(err) => (Err(err), EcoVec::default()),
|
||||
};
|
||||
CompiledArtifact {
|
||||
snap,
|
||||
doc,
|
||||
warnings,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: CompilerFeat> Clone for CompileSnapshot<F> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
id: self.id.clone(),
|
||||
signal: self.signal,
|
||||
env: self.env.clone(),
|
||||
world: self.world.clone(),
|
||||
success_doc: self.success_doc.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CompiledArtifact<F: CompilerFeat> {
|
||||
/// The used snapshot.
|
||||
pub snap: CompileSnapshot<F>,
|
||||
/// The diagnostics of the document.
|
||||
pub warnings: EcoVec<SourceDiagnostic>,
|
||||
/// The compiled document.
|
||||
pub doc: SourceResult<Arc<TypstDocument>>,
|
||||
}
|
||||
|
||||
impl<F: CompilerFeat> std::ops::Deref for CompiledArtifact<F> {
|
||||
type Target = CompileSnapshot<F>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.snap
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: CompilerFeat> Clone for CompiledArtifact<F> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
snap: self.snap.clone(),
|
||||
doc: self.doc.clone(),
|
||||
warnings: self.warnings.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: CompilerFeat> CompiledArtifact<F> {
|
||||
pub fn success_doc(&self) -> Option<Arc<TypstDocument>> {
|
||||
self.doc
|
||||
.as_ref()
|
||||
.ok()
|
||||
.cloned()
|
||||
.or_else(|| self.snap.success_doc.clone())
|
||||
}
|
||||
}
|
||||
|
||||
pub trait CompileHandler<F: CompilerFeat, Ext>: Send + Sync + 'static {
|
||||
fn on_any_compile_reason(&self, state: &mut ProjectCompiler<F, Ext>);
|
||||
// todo: notify project specific compile
|
||||
fn notify_compile(&self, res: &CompiledArtifact<F>, rep: CompileReport);
|
||||
fn status(&self, revision: usize, id: &ProjectInsId, rep: CompileReport);
|
||||
}
|
||||
|
||||
/// No need so no compilation.
|
||||
impl<F: CompilerFeat + Send + Sync + 'static, Ext: 'static> CompileHandler<F, Ext>
|
||||
for std::marker::PhantomData<fn(F, Ext)>
|
||||
{
|
||||
fn on_any_compile_reason(&self, _state: &mut ProjectCompiler<F, Ext>) {
|
||||
log::info!("ProjectHandle: no need to compile");
|
||||
}
|
||||
fn notify_compile(&self, _res: &CompiledArtifact<F>, _rep: CompileReport) {}
|
||||
fn status(&self, _revision: usize, _id: &ProjectInsId, _rep: CompileReport) {}
|
||||
}
|
||||
|
||||
pub enum Interrupt<F: CompilerFeat> {
|
||||
/// Compile anyway.
|
||||
Compile(ProjectInsId),
|
||||
/// Settle a dedicated project.
|
||||
Settle(ProjectInsId),
|
||||
/// Compiled from computing thread.
|
||||
Compiled(CompiledArtifact<F>),
|
||||
/// Change the watching entry.
|
||||
ChangeTask(ProjectInsId, TaskInputs),
|
||||
/// Font changes.
|
||||
Font(Arc<F::FontResolver>),
|
||||
/// Memory file changes.
|
||||
Memory(MemoryEvent),
|
||||
/// File system event.
|
||||
Fs(FilesystemEvent),
|
||||
}
|
||||
|
||||
impl fmt::Debug for Interrupt<LspCompilerFeat> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Interrupt::Compile(id) => write!(f, "Compile({id:?})"),
|
||||
Interrupt::Settle(id) => write!(f, "Settle({id:?})"),
|
||||
Interrupt::Compiled(artifact) => write!(f, "Compiled({:?})", artifact.id),
|
||||
Interrupt::ChangeTask(id, change) => {
|
||||
write!(f, "ChangeTask({id:?}, entry={:?})", change.entry.is_some())
|
||||
}
|
||||
Interrupt::Font(..) => write!(f, "Font(..)"),
|
||||
Interrupt::Memory(..) => write!(f, "Memory(..)"),
|
||||
Interrupt::Fs(..) => write!(f, "Fs(..)"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub struct CompileReasons {
|
||||
/// The snapshot is taken by the memory editing events.
|
||||
pub by_memory_events: bool,
|
||||
/// The snapshot is taken by the file system events.
|
||||
pub by_fs_events: bool,
|
||||
/// The snapshot is taken by the entry change.
|
||||
pub by_entry_update: bool,
|
||||
}
|
||||
|
||||
impl CompileReasons {
|
||||
/// Merge two reasons.
|
||||
pub fn see(&mut self, reason: CompileReasons) {
|
||||
self.by_memory_events |= reason.by_memory_events;
|
||||
self.by_fs_events |= reason.by_fs_events;
|
||||
self.by_entry_update |= reason.by_entry_update;
|
||||
}
|
||||
|
||||
/// Whether there is any reason to compile.
|
||||
pub fn any(&self) -> bool {
|
||||
self.by_memory_events || self.by_fs_events || self.by_entry_update
|
||||
}
|
||||
}
|
||||
|
||||
fn no_reason() -> CompileReasons {
|
||||
CompileReasons::default()
|
||||
}
|
||||
|
||||
fn reason_by_mem() -> CompileReasons {
|
||||
CompileReasons {
|
||||
by_memory_events: true,
|
||||
..CompileReasons::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn reason_by_fs() -> CompileReasons {
|
||||
CompileReasons {
|
||||
by_fs_events: true,
|
||||
..CompileReasons::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn reason_by_entry_change() -> CompileReasons {
|
||||
CompileReasons {
|
||||
by_entry_update: true,
|
||||
..CompileReasons::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// A tagged memory event with logical tick.
|
||||
struct TaggedMemoryEvent {
|
||||
/// The logical tick when the event is received.
|
||||
logical_tick: usize,
|
||||
/// The memory event happened.
|
||||
event: MemoryEvent,
|
||||
}
|
||||
|
||||
pub struct CompileServerOpts<F: CompilerFeat, Ext> {
|
||||
pub handler: Arc<dyn CompileHandler<F, Ext>>,
|
||||
pub feature_set: FeatureSet,
|
||||
pub enable_watch: bool,
|
||||
}
|
||||
|
||||
impl<F: CompilerFeat + Send + Sync + 'static, Ext: 'static> Default for CompileServerOpts<F, Ext> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
handler: Arc::new(std::marker::PhantomData),
|
||||
feature_set: Default::default(),
|
||||
enable_watch: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The synchronous compiler that runs on one project or multiple projects.
|
||||
pub struct ProjectCompiler<F: CompilerFeat, Ext> {
|
||||
/// The compilation handle.
|
||||
pub handler: Arc<dyn CompileHandler<F, Ext>>,
|
||||
/// Channel for sending interrupts to the compiler actor.
|
||||
dep_tx: mpsc::UnboundedSender<NotifyMessage>,
|
||||
/// Whether to enable file system watching.
|
||||
pub enable_watch: bool,
|
||||
|
||||
/// The current logical tick.
|
||||
logical_tick: usize,
|
||||
/// Last logical tick when invalidation is caused by shadow update.
|
||||
dirty_shadow_logical_tick: usize,
|
||||
/// Estimated latest set of shadow files.
|
||||
estimated_shadow_files: HashSet<Arc<Path>>,
|
||||
|
||||
/// The primary state.
|
||||
pub primary: ProjectState<F, Ext>,
|
||||
/// The states for dedicate tasks
|
||||
pub dedicates: Vec<ProjectState<F, Ext>>,
|
||||
}
|
||||
|
||||
impl<F: CompilerFeat + Send + Sync + 'static, Ext: Default + 'static> ProjectCompiler<F, Ext> {
|
||||
/// Create a compiler with options
|
||||
pub fn new(
|
||||
verse: CompilerUniverse<F>,
|
||||
dep_tx: mpsc::UnboundedSender<NotifyMessage>,
|
||||
CompileServerOpts {
|
||||
handler,
|
||||
feature_set,
|
||||
enable_watch,
|
||||
}: CompileServerOpts<F, Ext>,
|
||||
) -> Self {
|
||||
let primary = Self::create_project(
|
||||
ProjectInsId("primary".into()),
|
||||
verse,
|
||||
handler.clone(),
|
||||
dep_tx.clone(),
|
||||
feature_set.clone(),
|
||||
);
|
||||
Self {
|
||||
handler,
|
||||
dep_tx,
|
||||
enable_watch,
|
||||
|
||||
logical_tick: 1,
|
||||
dirty_shadow_logical_tick: 0,
|
||||
|
||||
estimated_shadow_files: Default::default(),
|
||||
|
||||
primary,
|
||||
dedicates: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn create_project(
|
||||
id: ProjectInsId,
|
||||
verse: CompilerUniverse<F>,
|
||||
handler: Arc<dyn CompileHandler<F, Ext>>,
|
||||
dep_tx: mpsc::UnboundedSender<NotifyMessage>,
|
||||
feature_set: FeatureSet,
|
||||
) -> ProjectState<F, Ext> {
|
||||
ProjectState {
|
||||
id,
|
||||
ext: Default::default(),
|
||||
verse,
|
||||
reason: no_reason(),
|
||||
snapshot: None,
|
||||
handler,
|
||||
dep_tx,
|
||||
compilation: OnceLock::default(),
|
||||
latest_doc: None,
|
||||
latest_success_doc: None,
|
||||
once_feature_set: Arc::new(feature_set.clone()),
|
||||
watch_feature_set: Arc::new(
|
||||
feature_set
|
||||
.clone()
|
||||
.configure(&WITH_COMPILING_STATUS_FEATURE, true),
|
||||
),
|
||||
committed_revision: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process(&mut self, intr: Interrupt<F>) {
|
||||
// todo: evcit cache
|
||||
self.process_inner(intr);
|
||||
// Customized Project Compilation Handler
|
||||
self.handler.clone().on_any_compile_reason(self);
|
||||
}
|
||||
|
||||
pub fn snapshot(&mut self) -> CompileSnapshot<F> {
|
||||
self.primary.snapshot()
|
||||
}
|
||||
|
||||
/// Compile the document once.
|
||||
pub fn compile_once(&mut self) -> CompiledArtifact<F> {
|
||||
let snap = self.primary.make_snapshot(true);
|
||||
ProjectState::run_compile(self.handler.clone(), snap)()
|
||||
}
|
||||
|
||||
/// Apply delayed memory changes to underlying compiler.
|
||||
fn apply_delayed_memory_changes(
|
||||
verse: &mut RevisingVfs<'_, F::AccessModel>,
|
||||
dirty_shadow_logical_tick: &mut usize,
|
||||
event: &Option<UpstreamUpdateEvent>,
|
||||
) -> Option<()> {
|
||||
// Handle delayed upstream update event before applying file system changes
|
||||
if let Some(event) = event {
|
||||
let TaggedMemoryEvent {
|
||||
logical_tick,
|
||||
event,
|
||||
} = event.opaque.as_ref().downcast_ref()?;
|
||||
|
||||
// Recovery from dirty shadow state.
|
||||
if logical_tick == dirty_shadow_logical_tick {
|
||||
*dirty_shadow_logical_tick = 0;
|
||||
}
|
||||
|
||||
Self::apply_memory_changes(verse, event.clone());
|
||||
}
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
/// Apply memory changes to underlying compiler.
|
||||
fn apply_memory_changes(vfs: &mut RevisingVfs<'_, F::AccessModel>, event: MemoryEvent) {
|
||||
if matches!(event, MemoryEvent::Sync(..)) {
|
||||
vfs.reset_shadow();
|
||||
}
|
||||
match event {
|
||||
MemoryEvent::Update(event) | MemoryEvent::Sync(event) => {
|
||||
for path in event.removes {
|
||||
let _ = vfs.unmap_shadow(&path);
|
||||
}
|
||||
for (path, snap) in event.inserts {
|
||||
let _ = vfs.map_shadow(&path, snap);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn find_project<'a>(
|
||||
primary: &'a mut ProjectState<F, Ext>,
|
||||
dedicates: &'a mut [ProjectState<F, Ext>],
|
||||
id: &ProjectInsId,
|
||||
) -> &'a mut ProjectState<F, Ext> {
|
||||
if id == &primary.id {
|
||||
return primary;
|
||||
}
|
||||
|
||||
dedicates.iter_mut().find(|e| e.id == *id).unwrap()
|
||||
}
|
||||
|
||||
pub fn projects(&mut self) -> impl Iterator<Item = &mut ProjectState<F, Ext>> {
|
||||
std::iter::once(&mut self.primary).chain(self.dedicates.iter_mut())
|
||||
}
|
||||
|
||||
fn process_inner(&mut self, intr: Interrupt<F>) {
|
||||
match intr {
|
||||
Interrupt::Compile(id) => {
|
||||
let proj = Self::find_project(&mut self.primary, &mut self.dedicates, &id);
|
||||
// Increment the revision anyway.
|
||||
proj.verse.increment_revision(|verse| {
|
||||
verse.flush();
|
||||
});
|
||||
|
||||
proj.reason.see(reason_by_entry_change());
|
||||
}
|
||||
Interrupt::Compiled(artifact) => {
|
||||
let proj = Self::find_project(&mut self.primary, &mut self.dedicates, &artifact.id);
|
||||
|
||||
proj.process_compile(artifact);
|
||||
}
|
||||
Interrupt::Settle(id) => {
|
||||
self.remove_dedicates(&id);
|
||||
}
|
||||
Interrupt::ChangeTask(id, change) => {
|
||||
let proj = Self::find_project(&mut self.primary, &mut self.dedicates, &id);
|
||||
proj.verse.increment_revision(|verse| {
|
||||
if let Some(inputs) = change.inputs.clone() {
|
||||
verse.set_inputs(inputs);
|
||||
}
|
||||
|
||||
if let Some(entry) = change.entry.clone() {
|
||||
let res = verse.mutate_entry(entry);
|
||||
if let Err(err) = res {
|
||||
log::error!("ProjectCompiler: change entry error: {err:?}");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// After incrementing the revision
|
||||
if let Some(entry) = change.entry {
|
||||
// todo: dedicate suspended
|
||||
if entry.is_inactive() {
|
||||
log::info!("ProjectCompiler: removing diag");
|
||||
self.handler.status(
|
||||
proj.verse.revision.get(),
|
||||
&proj.id,
|
||||
CompileReport::Suspend,
|
||||
);
|
||||
}
|
||||
|
||||
// Reset the watch state and document state.
|
||||
proj.latest_doc = None;
|
||||
proj.latest_success_doc = None;
|
||||
}
|
||||
|
||||
proj.reason.see(reason_by_entry_change());
|
||||
}
|
||||
|
||||
Interrupt::Font(fonts) => {
|
||||
self.projects().for_each(|proj| {
|
||||
let font_changed = proj.verse.increment_revision(|verse| {
|
||||
verse.set_fonts(fonts.clone());
|
||||
verse.font_changed()
|
||||
});
|
||||
if font_changed {
|
||||
// todo: reason_by_font_change
|
||||
proj.reason.see(reason_by_entry_change());
|
||||
}
|
||||
});
|
||||
}
|
||||
Interrupt::Memory(event) => {
|
||||
log::debug!("ProjectCompiler: memory event incoming");
|
||||
|
||||
// Emulate memory changes.
|
||||
let mut files = HashSet::new();
|
||||
if matches!(event, MemoryEvent::Sync(..)) {
|
||||
std::mem::swap(&mut files, &mut self.estimated_shadow_files);
|
||||
}
|
||||
|
||||
let (MemoryEvent::Sync(e) | MemoryEvent::Update(e)) = &event;
|
||||
for path in &e.removes {
|
||||
self.estimated_shadow_files.remove(path);
|
||||
files.insert(Arc::clone(path));
|
||||
}
|
||||
for (path, _) in &e.inserts {
|
||||
self.estimated_shadow_files.insert(Arc::clone(path));
|
||||
files.remove(path);
|
||||
}
|
||||
|
||||
// If there is no invalidation happening, apply memory changes directly.
|
||||
if files.is_empty() && self.dirty_shadow_logical_tick == 0 {
|
||||
let changes = std::iter::repeat_n(event, 1 + self.dedicates.len());
|
||||
for (proj, event) in std::iter::once(&mut self.primary).zip(changes) {
|
||||
let vfs_changed = proj.verse.increment_revision(|verse| {
|
||||
Self::apply_memory_changes(&mut verse.vfs(), event.clone());
|
||||
verse.vfs_changed()
|
||||
});
|
||||
if vfs_changed {
|
||||
proj.reason.see(reason_by_mem());
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, send upstream update event.
|
||||
// Also, record the logical tick when shadow is dirty.
|
||||
self.dirty_shadow_logical_tick = self.logical_tick;
|
||||
let event = NotifyMessage::UpstreamUpdate(UpstreamUpdateEvent {
|
||||
invalidates: files.into_iter().collect(),
|
||||
opaque: Box::new(TaggedMemoryEvent {
|
||||
logical_tick: self.logical_tick,
|
||||
event,
|
||||
}),
|
||||
});
|
||||
let err = self.dep_tx.send(event);
|
||||
log_send_error("dep_tx", err);
|
||||
}
|
||||
Interrupt::Fs(event) => {
|
||||
log::debug!("ProjectCompiler: fs event incoming {event:?}");
|
||||
|
||||
// Apply file system changes.
|
||||
let dirty_tick = &mut self.dirty_shadow_logical_tick;
|
||||
let (changes, event) = event.split();
|
||||
let changes = std::iter::repeat_n(changes, 1 + self.dedicates.len());
|
||||
let proj = std::iter::once(&mut self.primary).chain(self.dedicates.iter_mut());
|
||||
|
||||
for (proj, changes) in proj.zip(changes) {
|
||||
let vfs_changed = proj.verse.increment_revision(|verse| {
|
||||
{
|
||||
let mut vfs = verse.vfs();
|
||||
|
||||
// Handle delayed upstream update event before applying file system
|
||||
// changes
|
||||
if Self::apply_delayed_memory_changes(&mut vfs, dirty_tick, &event)
|
||||
.is_none()
|
||||
{
|
||||
log::warn!("ProjectCompiler: unknown upstream update event");
|
||||
|
||||
// Actual a delayed memory event.
|
||||
proj.reason.see(reason_by_mem());
|
||||
}
|
||||
vfs.notify_fs_changes(changes);
|
||||
}
|
||||
verse.vfs_changed()
|
||||
});
|
||||
|
||||
if vfs_changed {
|
||||
proj.reason.see(reason_by_fs());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn restart_dedicate(&mut self, group: &str, entry: EntryState) -> ZResult<ProjectInsId> {
|
||||
let id = ProjectInsId(group.into());
|
||||
|
||||
let verse = CompilerUniverse::<F>::new_raw(
|
||||
entry,
|
||||
Some(self.primary.verse.inputs().clone()),
|
||||
self.primary.verse.vfs().fork(),
|
||||
self.primary.verse.registry.clone(),
|
||||
self.primary.verse.font_resolver.clone(),
|
||||
);
|
||||
|
||||
let proj = Self::create_project(
|
||||
id.clone(),
|
||||
verse,
|
||||
self.handler.clone(),
|
||||
self.dep_tx.clone(),
|
||||
self.primary.once_feature_set.as_ref().to_owned(),
|
||||
);
|
||||
|
||||
self.remove_dedicates(&id);
|
||||
self.dedicates.push(proj);
|
||||
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
fn remove_dedicates(&mut self, id: &ProjectInsId) {
|
||||
let proj = self.dedicates.iter().position(|e| e.id == *id);
|
||||
if let Some(idx) = proj {
|
||||
let _proj = self.dedicates.remove(idx);
|
||||
// todo: kill compilations
|
||||
} else {
|
||||
log::warn!("ProjectCompiler: settle project not found {id:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ProjectState<F: CompilerFeat, Ext> {
|
||||
pub id: ProjectInsId,
|
||||
/// The extension
|
||||
pub ext: Ext,
|
||||
/// The underlying universe.
|
||||
pub verse: CompilerUniverse<F>,
|
||||
/// The reason to compile.
|
||||
pub reason: CompileReasons,
|
||||
/// The latest snapshot.
|
||||
snapshot: Option<CompileSnapshot<F>>,
|
||||
/// The latest compilation.
|
||||
pub compilation: OnceLock<CompiledArtifact<F>>,
|
||||
/// The compilation handle.
|
||||
pub handler: Arc<dyn CompileHandler<F, Ext>>,
|
||||
/// Channel for sending interrupts to the compiler actor.
|
||||
dep_tx: mpsc::UnboundedSender<NotifyMessage>,
|
||||
|
||||
/// The latest compiled document.
|
||||
pub(crate) latest_doc: Option<Arc<TypstDocument>>,
|
||||
/// The latest successly compiled document.
|
||||
latest_success_doc: Option<Arc<TypstDocument>>,
|
||||
/// feature set for compile_once mode.
|
||||
once_feature_set: Arc<FeatureSet>,
|
||||
/// Shared feature set for watch mode.
|
||||
watch_feature_set: Arc<FeatureSet>,
|
||||
|
||||
committed_revision: usize,
|
||||
}
|
||||
|
||||
impl<F: CompilerFeat, Ext: 'static> ProjectState<F, Ext> {
|
||||
pub fn make_env(&self, feature_set: Arc<FeatureSet>) -> CompileEnv {
|
||||
CompileEnv::default().configure_shared(feature_set)
|
||||
}
|
||||
|
||||
pub fn snapshot(&mut self) -> CompileSnapshot<F> {
|
||||
match self.snapshot.as_ref() {
|
||||
Some(snap) if snap.world.revision() == self.verse.revision => snap.clone(),
|
||||
_ => {
|
||||
let snap = self.make_snapshot(false);
|
||||
self.snapshot = Some(snap.clone());
|
||||
snap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn make_snapshot(&self, is_once: bool) -> CompileSnapshot<F> {
|
||||
let world = self.verse.snapshot();
|
||||
let env = self.make_env(if is_once {
|
||||
self.once_feature_set.clone()
|
||||
} else {
|
||||
self.watch_feature_set.clone()
|
||||
});
|
||||
CompileSnapshot {
|
||||
id: self.id.clone(),
|
||||
world,
|
||||
env: env.clone(),
|
||||
signal: ExportSignal {
|
||||
by_entry_update: self.reason.by_entry_update,
|
||||
by_mem_events: self.reason.by_memory_events,
|
||||
by_fs_events: self.reason.by_fs_events,
|
||||
},
|
||||
success_doc: self.latest_success_doc.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn process_compile(&mut self, artifact: CompiledArtifact<F>) {
|
||||
let world = &artifact.snap.world;
|
||||
let compiled_revision = world.revision().get();
|
||||
if self.committed_revision >= compiled_revision {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update state.
|
||||
let doc = artifact.doc.ok();
|
||||
self.committed_revision = compiled_revision;
|
||||
self.latest_doc.clone_from(&doc);
|
||||
if doc.is_some() {
|
||||
self.latest_success_doc.clone_from(&self.latest_doc);
|
||||
}
|
||||
|
||||
// Notify the new file dependencies.
|
||||
let mut deps = vec![];
|
||||
world.iter_dependencies(&mut |dep| {
|
||||
if let Ok(x) = world.file_path(dep).and_then(|e| e.to_err()) {
|
||||
deps.push(x.into())
|
||||
}
|
||||
});
|
||||
let event = NotifyMessage::SyncDependency(deps);
|
||||
let err = self.dep_tx.send(event);
|
||||
log_send_error("dep_tx", err);
|
||||
|
||||
let mut world = artifact.snap.world;
|
||||
|
||||
let is_primary = self.id == ProjectInsId("primary".into());
|
||||
|
||||
// Trigger an evict task.
|
||||
rayon::spawn(move || {
|
||||
let evict_start = std::time::Instant::now();
|
||||
if is_primary {
|
||||
comemo::evict(10);
|
||||
|
||||
// Since all the projects share the same cache, we need to evict the cache
|
||||
// on the primary instance for all the projects.
|
||||
world.evict_source_cache(30);
|
||||
}
|
||||
world.evict_vfs(60);
|
||||
let elapsed = evict_start.elapsed();
|
||||
log::info!("ProjectCompiler: evict cache in {elapsed:?}");
|
||||
});
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn may_compile(
|
||||
&mut self,
|
||||
handler: &Arc<dyn CompileHandler<F, Ext>>,
|
||||
) -> Option<impl FnOnce() -> CompiledArtifact<F>> {
|
||||
if !self.reason.any() || self.verse.entry_state().is_inactive() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let snap = self.snapshot();
|
||||
self.reason = Default::default();
|
||||
|
||||
Some(Self::run_compile(handler.clone(), snap))
|
||||
}
|
||||
|
||||
/// Compile the document once.
|
||||
fn run_compile(
|
||||
h: Arc<dyn CompileHandler<F, Ext>>,
|
||||
snap: CompileSnapshot<F>,
|
||||
) -> impl FnOnce() -> CompiledArtifact<F> {
|
||||
let start = tinymist_std::time::now();
|
||||
|
||||
// todo unwrap main id
|
||||
let id = snap.world.main_id().unwrap();
|
||||
let revision = snap.world.revision().get();
|
||||
|
||||
h.status(
|
||||
revision,
|
||||
&snap.id,
|
||||
CompileReport::Stage(id, "compiling", start),
|
||||
);
|
||||
|
||||
move || {
|
||||
let compiled = snap.compile();
|
||||
|
||||
let elapsed = start.elapsed().unwrap_or_default();
|
||||
let rep = match &compiled.doc {
|
||||
Ok(..) => CompileReport::CompileSuccess(id, compiled.warnings.clone(), elapsed),
|
||||
Err(err) => CompileReport::CompileError(id, err.clone(), elapsed),
|
||||
};
|
||||
|
||||
// todo: we need to check revision for really concurrent compilation
|
||||
log_compile_report(&compiled.env, &rep);
|
||||
h.notify_compile(&compiled, rep);
|
||||
|
||||
compiled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn log_compile_report(env: &CompileEnv, rep: &CompileReport) {
|
||||
if WITH_COMPILING_STATUS_FEATURE.retrieve(&env.features) {
|
||||
log::info!("{}", rep.message());
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn log_send_error<T>(chan: &'static str, res: Result<(), mpsc::error::SendError<T>>) -> bool {
|
||||
res.map_err(|err| log::warn!("ProjectCompiler: send to {chan} error: {err}"))
|
||||
.is_ok()
|
||||
}
|
|
@ -2,14 +2,16 @@
|
|||
|
||||
#![allow(missing_docs)]
|
||||
|
||||
mod lock;
|
||||
pub use lock::*;
|
||||
mod model;
|
||||
pub use model::*;
|
||||
mod args;
|
||||
pub use args::*;
|
||||
mod watch;
|
||||
pub use watch::*;
|
||||
pub mod world;
|
||||
pub use world::*;
|
||||
mod compiler;
|
||||
pub mod font;
|
||||
mod lock;
|
||||
mod model;
|
||||
mod watch;
|
||||
pub mod world;
|
||||
pub use args::*;
|
||||
pub use compiler::*;
|
||||
pub use lock::*;
|
||||
pub use model::*;
|
||||
pub use watch::*;
|
||||
pub use world::*;
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
use std::{path::Path, sync::Arc};
|
||||
|
||||
use ecow::EcoVec;
|
||||
use tinymist_std::path::unix_slash;
|
||||
use tinymist_world::EntryReader;
|
||||
use typst::{diag::EcoString, syntax::FileId};
|
||||
|
||||
use super::model::{Id, ProjectInput, ProjectMaterial, ProjectRoute, ProjectTask, ResourcePath};
|
||||
use crate::model::{Id, ProjectInput, ProjectMaterial, ProjectRoute, ProjectTask, ResourcePath};
|
||||
use crate::LspWorld;
|
||||
|
||||
/// Make a new project lock updater.
|
||||
|
@ -81,7 +82,7 @@ impl ProjectLockUpdater {
|
|||
self.updates.push(LockUpdate::Task(task));
|
||||
}
|
||||
|
||||
pub fn update_materials(&mut self, doc_id: Id, ids: Vec<FileId>) {
|
||||
pub fn update_materials(&mut self, doc_id: Id, ids: EcoVec<FileId>) {
|
||||
let mut files = ids
|
||||
.into_iter()
|
||||
.map(ResourcePath::from_file_id)
|
||||
|
|
|
@ -25,6 +25,7 @@ pub use signature::*;
|
|||
pub mod semantic_tokens;
|
||||
pub use semantic_tokens::*;
|
||||
use tinymist_world::vfs::WorkspaceResolver;
|
||||
use tinymist_world::WorldDeps;
|
||||
use typst::syntax::Source;
|
||||
use typst::World;
|
||||
mod post_tyck;
|
||||
|
@ -39,7 +40,7 @@ mod prelude;
|
|||
mod global;
|
||||
pub use global::*;
|
||||
|
||||
use ecow::eco_format;
|
||||
use ecow::{eco_format, EcoVec};
|
||||
use lsp_types::Url;
|
||||
use typst::diag::{FileError, FileResult};
|
||||
use typst::foundations::{Func, Value};
|
||||
|
@ -71,6 +72,10 @@ pub trait LspWorldExt {
|
|||
|
||||
/// Resolve the uri for a file id.
|
||||
fn uri_for_id(&self, fid: FileId) -> FileResult<Url>;
|
||||
|
||||
/// Get all depended file ids of a compilation, inclusively.
|
||||
/// Note: must be called after compilation.
|
||||
fn depended_files(&self) -> EcoVec<FileId>;
|
||||
}
|
||||
|
||||
impl LspWorldExt for tinymist_project::LspWorld {
|
||||
|
@ -96,6 +101,14 @@ impl LspWorldExt for tinymist_project::LspWorld {
|
|||
log::info!("uri_for_id: {fid:?} -> {res:?}");
|
||||
res.map_err(|err| FileError::Other(Some(eco_format!("convert to url: {err:?}"))))
|
||||
}
|
||||
|
||||
fn depended_files(&self) -> EcoVec<FileId> {
|
||||
let mut deps = EcoVec::new();
|
||||
self.iter_dependencies(&mut |file_id| {
|
||||
deps.push(file_id);
|
||||
});
|
||||
deps
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
@ -12,7 +12,7 @@ use tinymist_project::LspWorld;
|
|||
use tinymist_std::debug_loc::DataSource;
|
||||
use tinymist_std::hash::{hash128, FxDashMap};
|
||||
use tinymist_world::vfs::{PathResolution, WorkspaceResolver};
|
||||
use tinymist_world::{EntryReader, WorldDeps, DETACHED_ENTRY};
|
||||
use tinymist_world::{EntryReader, DETACHED_ENTRY};
|
||||
use typst::diag::{eco_format, At, FileError, FileResult, SourceResult, StrResult};
|
||||
use typst::engine::{Route, Sink, Traced};
|
||||
use typst::eval::Eval;
|
||||
|
@ -370,12 +370,7 @@ impl LocalContext {
|
|||
/// Get all depended file ids of a compilation, inclusively.
|
||||
/// Note: must be called after compilation.
|
||||
pub fn depended_files(&self) -> EcoVec<TypstFileId> {
|
||||
let mut deps = EcoVec::new();
|
||||
self.world.iter_dependencies(&mut |file_id| {
|
||||
deps.push(file_id);
|
||||
});
|
||||
|
||||
deps
|
||||
self.world.depended_files()
|
||||
}
|
||||
|
||||
/// Get the world surface for Typst compiler.
|
||||
|
|
|
@ -205,6 +205,16 @@ impl<M: PathAccessModel + Clone + Sized> Vfs<M> {
|
|||
access_model: self.access_model.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fork(&self) -> Self {
|
||||
Self {
|
||||
source_cache: self.source_cache.clone(),
|
||||
managed: Arc::new(Mutex::new(EntryMap::default())),
|
||||
paths: Arc::new(Mutex::new(PathMap::default())),
|
||||
revision: NonZeroUsize::new(1).expect("initial revision is 1"),
|
||||
access_model: self.access_model.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<M: PathAccessModel + Sized> Vfs<M> {
|
||||
|
@ -245,8 +255,8 @@ impl<M: PathAccessModel + Sized> Vfs<M> {
|
|||
/// Reset all state.
|
||||
pub fn reset_all(&mut self) {
|
||||
self.reset_access_model();
|
||||
self.reset_cache();
|
||||
self.take_state();
|
||||
self.reset_mapping();
|
||||
self.take_source_cache();
|
||||
}
|
||||
|
||||
/// Reset access model.
|
||||
|
@ -255,7 +265,7 @@ impl<M: PathAccessModel + Sized> Vfs<M> {
|
|||
}
|
||||
|
||||
/// Reset all possible caches.
|
||||
pub fn reset_cache(&mut self) {
|
||||
pub fn reset_mapping(&mut self) {
|
||||
self.revise().reset_cache();
|
||||
}
|
||||
|
||||
|
@ -271,10 +281,14 @@ impl<M: PathAccessModel + Sized> Vfs<M> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn take_state(&mut self) -> SourceCache {
|
||||
pub fn take_source_cache(&mut self) -> SourceCache {
|
||||
std::mem::take(&mut self.source_cache)
|
||||
}
|
||||
|
||||
pub fn clone_source_cache(&self) -> SourceCache {
|
||||
self.source_cache.clone()
|
||||
}
|
||||
|
||||
/// Resolve the real path for a file id.
|
||||
pub fn file_path(&self, id: TypstFileId) -> Result<PathResolution, FileError> {
|
||||
self.access_model.inner.resolver.path_for_id(id)
|
||||
|
|
|
@ -38,7 +38,6 @@ tar.workspace = true
|
|||
tinymist-std.workspace = true
|
||||
tinymist-vfs.workspace = true
|
||||
typst.workspace = true
|
||||
toml.workspace = true
|
||||
typst-assets.workspace = true
|
||||
wasm-bindgen = { workspace = true, optional = true }
|
||||
web-sys = { workspace = true, optional = true, features = ["console"] }
|
||||
|
|
|
@ -147,6 +147,10 @@ impl<F: CompilerFeat> CompilerUniverse<F> {
|
|||
self.mutate_entry_(state).map(|_| ())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn vfs(&self) -> &Vfs<F::AccessModel> {
|
||||
&self.vfs
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: CompilerFeat> CompilerUniverse<F> {
|
||||
|
@ -432,7 +436,11 @@ impl<F: CompilerFeat> CompilerWorld<F> {
|
|||
}
|
||||
|
||||
pub fn take_cache(&mut self) -> SourceCache {
|
||||
self.vfs.take_state()
|
||||
self.vfs.take_source_cache()
|
||||
}
|
||||
|
||||
pub fn clone_cache(&mut self) -> SourceCache {
|
||||
self.vfs.clone_source_cache()
|
||||
}
|
||||
|
||||
pub fn take_db(&mut self) -> SourceDb {
|
||||
|
@ -486,6 +494,16 @@ impl<F: CompilerFeat> CompilerWorld<F> {
|
|||
pub fn revision(&self) -> NonZeroUsize {
|
||||
self.revision
|
||||
}
|
||||
|
||||
pub fn evict_vfs(&mut self, threshold: usize) {
|
||||
self.vfs.evict(threshold);
|
||||
}
|
||||
|
||||
pub fn evict_source_cache(&mut self, threshold: usize) {
|
||||
self.vfs
|
||||
.clone_source_cache()
|
||||
.evict(self.vfs.revision(), threshold);
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: CompilerFeat> ShadowApi for CompilerWorld<F> {
|
||||
|
|
|
@ -3,184 +3,3 @@
|
|||
pub mod editor;
|
||||
#[cfg(feature = "preview")]
|
||||
pub mod preview;
|
||||
pub mod typ_client;
|
||||
pub mod typ_server;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::world::vfs::{notify::MemoryEvent, FileChangeSet};
|
||||
use crate::world::EntryState;
|
||||
use reflexo::ImmutPath;
|
||||
use tinymist_query::analysis::{Analysis, PeriscopeProvider};
|
||||
use tinymist_query::{ExportKind, LocalContext, VersionedDocument};
|
||||
use tinymist_render::PeriscopeRenderer;
|
||||
use tokio::sync::mpsc;
|
||||
use typst::layout::Position;
|
||||
|
||||
use crate::{
|
||||
project::{ImmutDict, LspUniverseBuilder},
|
||||
task::{ExportConfig, ExportTask, ExportUserConfig},
|
||||
LanguageState,
|
||||
};
|
||||
use typ_client::{CompileClientActor, CompileHandler};
|
||||
use typ_server::{CompileServerActor, CompileServerOpts};
|
||||
|
||||
impl LanguageState {
|
||||
/// Restart the primary server.
|
||||
pub fn restart_primary(&mut self) {
|
||||
let entry = self.entry_resolver().resolve_default();
|
||||
self.restart_server("primary", entry);
|
||||
}
|
||||
|
||||
/// Restart the server with the given group.
|
||||
pub fn restart_dedicate(&mut self, dedicate: &str, entry: Option<ImmutPath>) {
|
||||
self.restart_server(dedicate, entry);
|
||||
}
|
||||
|
||||
/// Restart the server with the given group.
|
||||
fn restart_server(&mut self, group: &str, entry: Option<ImmutPath>) {
|
||||
let server = self.server(
|
||||
group.to_owned(),
|
||||
self.entry_resolver().resolve(entry),
|
||||
self.compile_config().determine_inputs(),
|
||||
self.vfs_snapshot(),
|
||||
);
|
||||
|
||||
let prev = if group == "primary" {
|
||||
self.primary.replace(server)
|
||||
} else {
|
||||
let cell = self
|
||||
.dedicates
|
||||
.iter_mut()
|
||||
.find(|dedicate| dedicate.handle.diag_group == group);
|
||||
if let Some(dedicate) = cell {
|
||||
Some(std::mem::replace(dedicate, server))
|
||||
} else {
|
||||
self.dedicates.push(server);
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(mut prev) = prev {
|
||||
self.client.handle.spawn(async move { prev.settle().await });
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new server for the given group.
|
||||
pub fn server(
|
||||
&self,
|
||||
editor_group: String,
|
||||
entry: EntryState,
|
||||
inputs: ImmutDict,
|
||||
snapshot: FileChangeSet,
|
||||
) -> CompileClientActor {
|
||||
let (intr_tx, intr_rx) = mpsc::unbounded_channel();
|
||||
|
||||
// Run Export actors before preparing cluster to avoid loss of events
|
||||
let export = ExportTask::new(ExportConfig {
|
||||
group: editor_group.clone(),
|
||||
editor_tx: Some(self.editor_tx.clone()),
|
||||
config: ExportUserConfig {
|
||||
output: self.compile_config().output_path.clone(),
|
||||
mode: self.compile_config().export_pdf,
|
||||
},
|
||||
kind: ExportKind::Pdf {
|
||||
creation_timestamp: self.config.compile.determine_creation_timestamp(),
|
||||
},
|
||||
count_words: self.config.compile.notify_status,
|
||||
});
|
||||
|
||||
log::info!(
|
||||
"TypstActor: creating server for {editor_group}, entry: {entry:?}, inputs: {inputs:?}"
|
||||
);
|
||||
|
||||
// Create the compile handler for client consuming results.
|
||||
let const_config = self.const_config();
|
||||
let periscope_args = self.compile_config().periscope_args.clone();
|
||||
let handle = Arc::new(CompileHandler {
|
||||
#[cfg(feature = "preview")]
|
||||
inner: std::sync::Arc::new(parking_lot::RwLock::new(None)),
|
||||
diag_group: editor_group.clone(),
|
||||
intr_tx: intr_tx.clone(),
|
||||
export: export.clone(),
|
||||
editor_tx: self.editor_tx.clone(),
|
||||
stats: Default::default(),
|
||||
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: !self.config.support_html_in_markdown,
|
||||
completion_feat: self.config.completion.clone(),
|
||||
color_theme: match self.compile_config().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<dyn PeriscopeProvider + Send + Sync>
|
||||
}),
|
||||
tokens_caches: Arc::default(),
|
||||
workers: Default::default(),
|
||||
caches: Default::default(),
|
||||
analysis_rev_cache: Arc::default(),
|
||||
stats: Arc::default(),
|
||||
}),
|
||||
|
||||
notified_revision: parking_lot::Mutex::new(0),
|
||||
});
|
||||
|
||||
let font_resolver = self.compile_config().determine_fonts();
|
||||
let entry_ = entry.clone();
|
||||
let compile_handle = handle.clone();
|
||||
let cache = self.cache.clone();
|
||||
let cert_path = self.compile_config().determine_certification_path();
|
||||
let package = self.compile_config().determine_package_opts();
|
||||
|
||||
self.client.handle.spawn_blocking(move || {
|
||||
// Create the world
|
||||
let font_resolver = font_resolver.wait().clone();
|
||||
let package_registry =
|
||||
LspUniverseBuilder::resolve_package(cert_path.clone(), Some(&package));
|
||||
let verse =
|
||||
LspUniverseBuilder::build(entry_.clone(), inputs, font_resolver, package_registry)
|
||||
.expect("incorrect options");
|
||||
|
||||
// Create the actor
|
||||
let server = CompileServerActor::new_with(
|
||||
verse,
|
||||
intr_tx,
|
||||
intr_rx,
|
||||
CompileServerOpts {
|
||||
compile_handle,
|
||||
cache,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.with_watch(true);
|
||||
tokio::spawn(server.run());
|
||||
});
|
||||
|
||||
// Create the client
|
||||
let config = self.compile_config().clone();
|
||||
let client = CompileClientActor::new(handle, config, entry);
|
||||
// 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));
|
||||
client
|
||||
}
|
||||
}
|
||||
|
||||
struct TypstPeriscopeProvider(PeriscopeRenderer);
|
||||
|
||||
impl PeriscopeProvider for TypstPeriscopeProvider {
|
||||
/// Resolve periscope image at the given position.
|
||||
fn periscope_at(
|
||||
&self,
|
||||
ctx: &mut LocalContext,
|
||||
doc: VersionedDocument,
|
||||
pos: Position,
|
||||
) -> Option<String> {
|
||||
self.0.render_marked(ctx, doc, pos)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,8 +7,10 @@ use sync_lsp::{internal_error, LspClient, LspResult};
|
|||
use tokio::sync::{mpsc, oneshot};
|
||||
use typst_preview::{ControlPlaneMessage, Previewer};
|
||||
|
||||
use super::typ_client::CompileHandler;
|
||||
use crate::tool::preview::HttpServer;
|
||||
use crate::{
|
||||
project::LspPreviewState,
|
||||
tool::preview::{HttpServer, PreviewProjectHandler},
|
||||
};
|
||||
|
||||
pub struct PreviewTab {
|
||||
/// Task ID
|
||||
|
@ -20,7 +22,7 @@ pub struct PreviewTab {
|
|||
/// Control plane message sender
|
||||
pub ctl_tx: mpsc::UnboundedSender<ControlPlaneMessage>,
|
||||
/// Compile handler
|
||||
pub compile_handler: Arc<CompileHandler>,
|
||||
pub compile_handler: Arc<PreviewProjectHandler>,
|
||||
/// Whether this tab is primary
|
||||
pub is_primary: bool,
|
||||
}
|
||||
|
@ -35,6 +37,8 @@ pub struct PreviewActor {
|
|||
pub client: LspClient,
|
||||
pub tabs: HashMap<String, PreviewTab>,
|
||||
pub preview_rx: mpsc::UnboundedReceiver<PreviewRequest>,
|
||||
/// the watchers for the preview
|
||||
pub(crate) watchers: LspPreviewState,
|
||||
}
|
||||
|
||||
impl PreviewActor {
|
||||
|
@ -52,7 +56,7 @@ impl PreviewActor {
|
|||
};
|
||||
|
||||
// Unregister preview early
|
||||
let unregistered = tab.compile_handler.unregister_preview(&task_id);
|
||||
let unregistered = self.watchers.unregister(&tab.compile_handler.project_id);
|
||||
if !unregistered {
|
||||
log::warn!("PreviewTask({task_id}): failed to unregister preview");
|
||||
}
|
||||
|
|
|
@ -1,539 +0,0 @@
|
|||
//! The actor that runs compilations.
|
||||
//!
|
||||
//! ```ascii
|
||||
//! ┌────────────────────────────────┐
|
||||
//! │ main::compile_actor (client) │
|
||||
//! └─────┬────────────────────▲─────┘
|
||||
//! │ │
|
||||
//! ┌─────▼────────────────────┴─────┐ ┌────────────┐
|
||||
//! │compiler::compile_actor (server)│◄───────►│notify_actor│
|
||||
//! └─────┬────────────────────▲─────┘ └────────────┘
|
||||
//! │ │
|
||||
//! ┌─────▼────────────────────┴─────┐ handler ┌────────────┐
|
||||
//! │ compiler::compile_handler ├────────►│ rest actors│
|
||||
//! └────────────────────────────────┘ └────────────┘
|
||||
//! ```
|
||||
//!
|
||||
//! We use typst by creating a
|
||||
//! [`CompileServerActor`][`crate::actor::typ_server::CompileServerActor`] and
|
||||
//! running compiler with callbacking [`CompileHandler`] incrementally. An
|
||||
//! additional [`CompileClientActor`] is also created to control the
|
||||
//! [`CompileServerActor`][`crate::actor::typ_server::CompileServerActor`].
|
||||
//!
|
||||
//! The [`CompileHandler`] will push information to other actors.
|
||||
|
||||
use std::{collections::HashMap, ops::Deref, sync::Arc};
|
||||
|
||||
use anyhow::bail;
|
||||
use log::{error, info, trace};
|
||||
use reflexo::path::unix_slash;
|
||||
use reflexo_typst::{error::prelude::*, typst::prelude::*, CompileReport, Error, ImmutPath};
|
||||
|
||||
use sync_lsp::just_future;
|
||||
use tinymist_query::{
|
||||
analysis::{Analysis, AnalysisRevLock, LocalContextGuard},
|
||||
CompilerQueryRequest, CompilerQueryResponse, DiagnosticsMap, EntryResolver, OnExportRequest,
|
||||
SemanticRequest, ServerInfoResponse, StatefulRequest, VersionedDocument,
|
||||
};
|
||||
use tokio::sync::{mpsc, oneshot};
|
||||
use typst::{diag::SourceDiagnostic, World};
|
||||
|
||||
use super::{
|
||||
editor::{CompileStatus, DocVersion, EditorRequest, TinymistCompileStatusEnum},
|
||||
typ_server::{
|
||||
CompilationHandle, CompileSnapshot, CompiledArtifact, Interrupt, SucceededArtifact,
|
||||
},
|
||||
};
|
||||
use crate::world::{vfs::notify::MemoryEvent, EntryReader, EntryState, TaskInputs};
|
||||
use crate::{
|
||||
stats::{CompilerQueryStats, QueryStatGuard},
|
||||
task::{ExportTask, ExportUserConfig},
|
||||
world::{LspCompilerFeat, LspWorld},
|
||||
CompileConfig, QueryFuture,
|
||||
};
|
||||
|
||||
type EditorSender = mpsc::UnboundedSender<EditorRequest>;
|
||||
|
||||
pub struct CompileHandler {
|
||||
pub(crate) diag_group: String,
|
||||
pub(crate) analysis: Arc<Analysis>,
|
||||
pub(crate) stats: CompilerQueryStats,
|
||||
|
||||
#[cfg(feature = "preview")]
|
||||
pub(crate) inner: Arc<parking_lot::RwLock<Option<Arc<typst_preview::CompileWatcher>>>>,
|
||||
|
||||
pub(crate) intr_tx: mpsc::UnboundedSender<Interrupt<LspCompilerFeat>>,
|
||||
pub(crate) export: ExportTask,
|
||||
pub(crate) editor_tx: EditorSender,
|
||||
|
||||
pub(crate) notified_revision: parking_lot::Mutex<usize>,
|
||||
}
|
||||
|
||||
impl CompileHandler {
|
||||
/// Snapshot the compiler thread for tasks
|
||||
pub fn snapshot(&self) -> ZResult<WorldSnapFut> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
self.intr_tx
|
||||
.send(Interrupt::SnapshotRead(tx))
|
||||
.map_err(map_string_err("failed to send snapshot request"))?;
|
||||
|
||||
Ok(WorldSnapFut { rx })
|
||||
}
|
||||
|
||||
/// Snapshot the compiler thread for language queries
|
||||
pub fn query_snapshot(&self, q: Option<&CompilerQueryRequest>) -> ZResult<QuerySnapFut> {
|
||||
let fut = self.snapshot()?;
|
||||
let analysis = self.analysis.clone();
|
||||
let rev_lock = analysis.lock_revision(q);
|
||||
|
||||
Ok(QuerySnapFut {
|
||||
fut,
|
||||
analysis,
|
||||
rev_lock,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get latest artifact the compiler thread for tasks
|
||||
pub fn artifact(&self) -> ZResult<ArtifactSnap> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
self.intr_tx
|
||||
.send(Interrupt::CurrentRead(tx))
|
||||
.map_err(map_string_err("failed to send snapshot request"))?;
|
||||
|
||||
Ok(ArtifactSnap { rx })
|
||||
}
|
||||
|
||||
pub fn flush_compile(&self) {
|
||||
// todo: better way to flush compile
|
||||
let _ = self.intr_tx.send(Interrupt::Compile);
|
||||
}
|
||||
|
||||
pub fn add_memory_changes(&self, event: MemoryEvent) {
|
||||
let _ = self.intr_tx.send(Interrupt::Memory(event));
|
||||
}
|
||||
|
||||
pub fn change_task(&self, task_inputs: TaskInputs) {
|
||||
let _ = self.intr_tx.send(Interrupt::ChangeTask(task_inputs));
|
||||
}
|
||||
|
||||
pub async fn settle(&self) -> anyhow::Result<()> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
let _ = self.intr_tx.send(Interrupt::Settle(tx));
|
||||
rx.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn push_diagnostics(&self, revision: usize, diagnostics: Option<DiagnosticsMap>) {
|
||||
let dv = DocVersion {
|
||||
group: self.diag_group.clone(),
|
||||
revision,
|
||||
};
|
||||
let res = self.editor_tx.send(EditorRequest::Diag(dv, diagnostics));
|
||||
if let Err(err) = res {
|
||||
error!("failed to send diagnostics: {err:#}");
|
||||
}
|
||||
}
|
||||
|
||||
fn notify_diagnostics(
|
||||
&self,
|
||||
world: &LspWorld,
|
||||
errors: EcoVec<SourceDiagnostic>,
|
||||
warnings: EcoVec<SourceDiagnostic>,
|
||||
) {
|
||||
let revision = world.revision().get();
|
||||
trace!("notify diagnostics({revision}): {errors:#?} {warnings:#?}");
|
||||
|
||||
let diagnostics = tinymist_query::convert_diagnostics(
|
||||
world,
|
||||
errors.iter().chain(warnings.iter()),
|
||||
self.analysis.position_encoding,
|
||||
);
|
||||
|
||||
let entry = world.entry_state();
|
||||
// todo: better way to remove diagnostics
|
||||
// todo: check all errors in this file
|
||||
let detached = entry.is_inactive();
|
||||
let valid = !detached;
|
||||
self.push_diagnostics(revision, valid.then_some(diagnostics));
|
||||
}
|
||||
|
||||
// todo: multiple preview support
|
||||
#[cfg(feature = "preview")]
|
||||
#[must_use]
|
||||
pub fn register_preview(&self, handle: &Arc<typst_preview::CompileWatcher>) -> bool {
|
||||
let mut p = self.inner.write();
|
||||
if p.as_ref().is_some() {
|
||||
return false;
|
||||
}
|
||||
*p = Some(handle.clone());
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(feature = "preview")]
|
||||
#[must_use]
|
||||
pub fn unregister_preview(&self, task_id: &str) -> bool {
|
||||
let mut p = self.inner.write();
|
||||
if p.as_ref().is_some_and(|p| p.task_id() == task_id) {
|
||||
*p = None;
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl CompilationHandle<LspCompilerFeat> for CompileHandler {
|
||||
fn status(&self, revision: usize, rep: CompileReport) {
|
||||
// todo: seems to duplicate with CompileStatus
|
||||
let status = match rep {
|
||||
CompileReport::Suspend => {
|
||||
self.push_diagnostics(revision, None);
|
||||
TinymistCompileStatusEnum::CompileSuccess
|
||||
}
|
||||
CompileReport::Stage(_, _, _) => TinymistCompileStatusEnum::Compiling,
|
||||
CompileReport::CompileSuccess(_, _, _) => TinymistCompileStatusEnum::CompileSuccess,
|
||||
CompileReport::CompileError(_, _, _) | CompileReport::ExportError(_, _, _) => {
|
||||
TinymistCompileStatusEnum::CompileError
|
||||
}
|
||||
};
|
||||
|
||||
let this = &self;
|
||||
this.editor_tx
|
||||
.send(EditorRequest::Status(CompileStatus {
|
||||
group: this.diag_group.clone(),
|
||||
path: rep
|
||||
.compiling_id()
|
||||
.map(|s| unix_slash(s.vpath().as_rooted_path()))
|
||||
.unwrap_or_default(),
|
||||
status,
|
||||
}))
|
||||
.unwrap();
|
||||
|
||||
#[cfg(feature = "preview")]
|
||||
if let Some(inner) = this.inner.read().as_ref() {
|
||||
use typst_preview::CompileStatus;
|
||||
|
||||
let status = match rep {
|
||||
CompileReport::Suspend => CompileStatus::CompileSuccess,
|
||||
CompileReport::Stage(_, _, _) => CompileStatus::Compiling,
|
||||
CompileReport::CompileSuccess(_, _, _) => CompileStatus::CompileSuccess,
|
||||
CompileReport::CompileError(_, _, _) | CompileReport::ExportError(_, _, _) => {
|
||||
CompileStatus::CompileError
|
||||
}
|
||||
};
|
||||
|
||||
inner.status(status);
|
||||
}
|
||||
}
|
||||
|
||||
fn notify_compile(&self, snap: &CompiledArtifact<LspCompilerFeat>, rep: CompileReport) {
|
||||
// todo: we need to manage the revision for fn status() as well
|
||||
{
|
||||
let mut n_rev = self.notified_revision.lock();
|
||||
if *n_rev >= snap.world.revision().get() {
|
||||
log::info!(
|
||||
"TypstActor: already notified for revision {} <= {n_rev}",
|
||||
snap.world.revision(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
*n_rev = snap.world.revision().get();
|
||||
}
|
||||
|
||||
self.notify_diagnostics(
|
||||
&snap.world,
|
||||
snap.doc.clone().err().unwrap_or_default(),
|
||||
snap.warnings.clone(),
|
||||
);
|
||||
|
||||
self.export.signal(snap, snap.signal);
|
||||
|
||||
self.editor_tx
|
||||
.send(EditorRequest::Status(CompileStatus {
|
||||
group: self.diag_group.clone(),
|
||||
path: rep
|
||||
.compiling_id()
|
||||
.map(|s| unix_slash(s.vpath().as_rooted_path()))
|
||||
.unwrap_or_default(),
|
||||
status: if snap.doc.is_ok() {
|
||||
TinymistCompileStatusEnum::CompileSuccess
|
||||
} else {
|
||||
TinymistCompileStatusEnum::CompileError
|
||||
},
|
||||
}))
|
||||
.unwrap();
|
||||
|
||||
#[cfg(feature = "preview")]
|
||||
if let Some(inner) = self.inner.read().as_ref() {
|
||||
let snap = snap.clone();
|
||||
inner.notify_compile(Arc::new(crate::tool::preview::PreviewCompileView { snap }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CompileClientActor {
|
||||
pub handle: Arc<CompileHandler>,
|
||||
|
||||
pub config: CompileConfig,
|
||||
entry: EntryState,
|
||||
}
|
||||
|
||||
impl CompileClientActor {
|
||||
pub(crate) fn new(
|
||||
handle: Arc<CompileHandler>,
|
||||
config: CompileConfig,
|
||||
entry: EntryState,
|
||||
) -> Self {
|
||||
Self {
|
||||
handle,
|
||||
config,
|
||||
entry,
|
||||
}
|
||||
}
|
||||
|
||||
/// Snapshot the compiler thread for tasks
|
||||
pub fn snapshot(&self) -> ZResult<WorldSnapFut> {
|
||||
self.handle.clone().snapshot()
|
||||
}
|
||||
|
||||
/// Get the entry resolver.
|
||||
pub fn entry_resolver(&self) -> &EntryResolver {
|
||||
&self.config.entry_resolver
|
||||
}
|
||||
|
||||
/// Snapshot the compiler thread for language queries
|
||||
pub fn query_snapshot(&self) -> ZResult<QuerySnapFut> {
|
||||
self.handle.clone().query_snapshot(None)
|
||||
}
|
||||
|
||||
/// Snapshot the compiler thread for language queries
|
||||
pub fn query_snapshot_with_stat(&self, q: &CompilerQueryRequest) -> ZResult<QuerySnapWithStat> {
|
||||
let name: &'static str = q.into();
|
||||
let path = q.associated_path();
|
||||
let stat = self.handle.stats.query_stat(path, name);
|
||||
let fut = self.handle.clone().query_snapshot(Some(q))?;
|
||||
Ok(QuerySnapWithStat { fut, stat })
|
||||
}
|
||||
|
||||
pub fn add_memory_changes(&self, event: MemoryEvent) {
|
||||
self.handle.add_memory_changes(event);
|
||||
}
|
||||
|
||||
pub fn change_task(&self, task_inputs: TaskInputs) {
|
||||
self.handle.change_task(task_inputs);
|
||||
}
|
||||
|
||||
pub fn sync_config(&mut self, config: CompileConfig) {
|
||||
self.config = config;
|
||||
}
|
||||
|
||||
pub(crate) fn change_export_config(&mut self, config: ExportUserConfig) {
|
||||
self.handle.export.change_config(config);
|
||||
}
|
||||
|
||||
pub fn on_export(&self, req: OnExportRequest) -> QueryFuture {
|
||||
let OnExportRequest { path, kind, open } = req;
|
||||
let snap = self.snapshot()?;
|
||||
|
||||
let entry = self.entry_resolver().resolve(Some(path.as_path().into()));
|
||||
let export = self.handle.export.factory.oneshot(snap, Some(entry), kind);
|
||||
just_future(async move {
|
||||
let res = export.await?;
|
||||
|
||||
// See https://github.com/Myriad-Dreamin/tinymist/issues/837
|
||||
// Also see https://github.com/Byron/open-rs/issues/105
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let do_open = ::open::that_detached;
|
||||
#[cfg(target_os = "windows")]
|
||||
fn do_open(path: impl AsRef<std::ffi::OsStr>) -> std::io::Result<()> {
|
||||
::open::with_detached(path, "explorer")
|
||||
}
|
||||
|
||||
if let Some(Some(path)) = open.then_some(res.as_ref()) {
|
||||
log::info!("open with system default apps: {path:?}");
|
||||
if let Err(e) = do_open(path) {
|
||||
log::error!("failed to open with system default apps: {e}");
|
||||
};
|
||||
}
|
||||
|
||||
log::info!("CompileActor: on export end: {path:?} as {res:?}");
|
||||
Ok(tinymist_query::CompilerQueryResponse::OnExport(res))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl CompileClientActor {
|
||||
pub async fn settle(&mut self) {
|
||||
let _ = self.change_entry(None);
|
||||
info!("TypstActor({}): settle requested", self.handle.diag_group);
|
||||
match self.handle.settle().await {
|
||||
Ok(()) => info!("TypstActor({}): settled", self.handle.diag_group),
|
||||
Err(err) => error!(
|
||||
"TypstActor({}): failed to settle: {err:#}",
|
||||
self.handle.diag_group
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn change_entry(&mut self, path: Option<ImmutPath>) -> Result<bool, Error> {
|
||||
if path
|
||||
.as_deref()
|
||||
.is_some_and(|p| !p.is_absolute() && !p.starts_with("/untitled"))
|
||||
{
|
||||
return Err(error_once!("entry file must be absolute", path: path.unwrap().display()));
|
||||
}
|
||||
|
||||
let next_entry = self.entry_resolver().resolve(path);
|
||||
if next_entry == self.entry {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let diag_group = &self.handle.diag_group;
|
||||
info!("the entry file of TypstActor({diag_group}) is changing to {next_entry:?}");
|
||||
|
||||
self.change_task(TaskInputs {
|
||||
entry: Some(next_entry.clone()),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
self.entry = next_entry;
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub fn clear_cache(&mut self) {
|
||||
self.handle.analysis.clear_cache();
|
||||
}
|
||||
|
||||
pub fn collect_server_info(&self) -> QueryFuture {
|
||||
let dg = self.handle.diag_group.clone();
|
||||
let api_stats = self.handle.stats.report();
|
||||
let query_stats = self.handle.analysis.report_query_stats();
|
||||
let alloc_stats = self.handle.analysis.report_alloc_stats();
|
||||
|
||||
let snap = self.snapshot()?;
|
||||
just_future(async move {
|
||||
let snap = snap.receive().await?;
|
||||
let w = &snap.world;
|
||||
|
||||
let info = ServerInfoResponse {
|
||||
root: w.entry_state().root().map(|e| e.as_ref().to_owned()),
|
||||
font_paths: w.font_resolver.font_paths().to_owned(),
|
||||
inputs: w.inputs().as_ref().deref().clone(),
|
||||
stats: HashMap::from_iter([
|
||||
("api".to_owned(), api_stats),
|
||||
("query".to_owned(), query_stats),
|
||||
("alloc".to_owned(), alloc_stats),
|
||||
]),
|
||||
};
|
||||
|
||||
let info = Some(HashMap::from_iter([(dg, info)]));
|
||||
Ok(tinymist_query::CompilerQueryResponse::ServerInfo(info))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct QuerySnapWithStat {
|
||||
pub fut: QuerySnapFut,
|
||||
pub(crate) stat: QueryStatGuard,
|
||||
}
|
||||
|
||||
pub struct WorldSnapFut {
|
||||
rx: oneshot::Receiver<CompileSnapshot<LspCompilerFeat>>,
|
||||
}
|
||||
|
||||
impl WorldSnapFut {
|
||||
/// wait for the snapshot to be ready
|
||||
pub async fn receive(self) -> ZResult<CompileSnapshot<LspCompilerFeat>> {
|
||||
self.rx
|
||||
.await
|
||||
.map_err(map_string_err("failed to get snapshot"))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct QuerySnapFut {
|
||||
fut: WorldSnapFut,
|
||||
analysis: Arc<Analysis>,
|
||||
rev_lock: AnalysisRevLock,
|
||||
}
|
||||
|
||||
impl QuerySnapFut {
|
||||
/// wait for the snapshot to be ready
|
||||
pub async fn receive(self) -> ZResult<QuerySnap> {
|
||||
let snap = self.fut.receive().await?;
|
||||
Ok(QuerySnap {
|
||||
snap,
|
||||
analysis: self.analysis,
|
||||
rev_lock: self.rev_lock,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct QuerySnap {
|
||||
pub snap: CompileSnapshot<LspCompilerFeat>,
|
||||
analysis: Arc<Analysis>,
|
||||
rev_lock: AnalysisRevLock,
|
||||
}
|
||||
|
||||
impl std::ops::Deref for QuerySnap {
|
||||
type Target = CompileSnapshot<LspCompilerFeat>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.snap
|
||||
}
|
||||
}
|
||||
|
||||
impl QuerySnap {
|
||||
pub fn task(mut self, inputs: TaskInputs) -> Self {
|
||||
self.snap = self.snap.task(inputs);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn run_stateful<T: StatefulRequest>(
|
||||
self,
|
||||
query: T,
|
||||
wrapper: fn(Option<T::Response>) -> CompilerQueryResponse,
|
||||
) -> anyhow::Result<CompilerQueryResponse> {
|
||||
let doc = self.snap.success_doc.as_ref().map(|doc| VersionedDocument {
|
||||
version: self.world.revision().get(),
|
||||
document: doc.clone(),
|
||||
});
|
||||
self.run_analysis(|ctx| query.request(ctx, doc))
|
||||
.map(wrapper)
|
||||
}
|
||||
|
||||
pub fn run_semantic<T: SemanticRequest>(
|
||||
self,
|
||||
query: T,
|
||||
wrapper: fn(Option<T::Response>) -> CompilerQueryResponse,
|
||||
) -> anyhow::Result<CompilerQueryResponse> {
|
||||
self.run_analysis(|ctx| query.request(ctx)).map(wrapper)
|
||||
}
|
||||
|
||||
pub fn run_analysis<T>(self, f: impl FnOnce(&mut LocalContextGuard) -> T) -> anyhow::Result<T> {
|
||||
let world = self.snap.world;
|
||||
let Some(main) = world.main_id() else {
|
||||
error!("TypstActor: main file is not set");
|
||||
bail!("main file is not set");
|
||||
};
|
||||
world.source(main).map_err(|err| {
|
||||
info!("TypstActor: failed to prepare main file: {err:?}");
|
||||
anyhow::anyhow!("failed to get source: {err}")
|
||||
})?;
|
||||
|
||||
let mut analysis = self.analysis.snapshot_(world, self.rev_lock);
|
||||
Ok(f(&mut analysis))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ArtifactSnap {
|
||||
rx: oneshot::Receiver<SucceededArtifact<LspCompilerFeat>>,
|
||||
}
|
||||
|
||||
impl ArtifactSnap {
|
||||
/// Get latest artifact the compiler thread for tasks
|
||||
pub async fn receive(self) -> ZResult<SucceededArtifact<LspCompilerFeat>> {
|
||||
self.rx
|
||||
.await
|
||||
.map_err(map_string_err("failed to get snapshot"))
|
||||
}
|
||||
}
|
|
@ -1,747 +0,0 @@
|
|||
//! The [`CompileServerActor`] implementation borrowed from typst.ts.
|
||||
//!
|
||||
//! Please check `tinymist::actor::typ_client` for architecture details.
|
||||
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
path::Path,
|
||||
sync::{Arc, OnceLock},
|
||||
};
|
||||
|
||||
use reflexo_typst::{
|
||||
features::WITH_COMPILING_STATUS_FEATURE, typst::prelude::EcoVec, CompileEnv, CompileReport,
|
||||
Compiler, ConsoleDiagReporter, FeatureSet, GenericExporter, TypstDocument,
|
||||
};
|
||||
use tinymist_project::watch_deps;
|
||||
use tinymist_project::{vfs::FsProvider, RevisingUniverse};
|
||||
use tokio::sync::{mpsc, oneshot};
|
||||
use typst::diag::{SourceDiagnostic, SourceResult};
|
||||
|
||||
use crate::task::CacheTask;
|
||||
use crate::world::base::{
|
||||
// features::{FeatureSet, WITH_COMPILING_STATUS_FEATURE},
|
||||
// typst::prelude::EcoVec,
|
||||
vfs::notify::{FilesystemEvent, MemoryEvent, NotifyMessage, UpstreamUpdateEvent},
|
||||
CompilerFeat,
|
||||
CompilerUniverse,
|
||||
CompilerWorld,
|
||||
EntryReader,
|
||||
TaskInputs,
|
||||
WorldDeps,
|
||||
};
|
||||
|
||||
/// A signal that possibly triggers an export.
|
||||
///
|
||||
/// Whether to export depends on the current state of the document and the user
|
||||
/// settings.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct ExportSignal {
|
||||
/// Whether the revision is annotated by memory events.
|
||||
pub by_mem_events: bool,
|
||||
/// Whether the revision is annotated by file system events.
|
||||
pub by_fs_events: bool,
|
||||
/// Whether the revision is annotated by entry update.
|
||||
pub by_entry_update: bool,
|
||||
}
|
||||
|
||||
pub struct CompileSnapshot<F: CompilerFeat> {
|
||||
/// The export signal for the document.
|
||||
pub signal: ExportSignal,
|
||||
/// Using env
|
||||
pub env: CompileEnv,
|
||||
/// Using world
|
||||
pub world: CompilerWorld<F>,
|
||||
/// The last successfully compiled document.
|
||||
pub success_doc: Option<Arc<TypstDocument>>,
|
||||
}
|
||||
|
||||
impl<F: CompilerFeat + 'static> CompileSnapshot<F> {
|
||||
pub fn task(mut self, inputs: TaskInputs) -> Self {
|
||||
'check_changed: {
|
||||
if let Some(entry) = &inputs.entry {
|
||||
if *entry != self.world.entry_state() {
|
||||
break 'check_changed;
|
||||
}
|
||||
}
|
||||
if let Some(inputs) = &inputs.inputs {
|
||||
if inputs.clone() != self.world.inputs() {
|
||||
break 'check_changed;
|
||||
}
|
||||
}
|
||||
|
||||
return self;
|
||||
};
|
||||
|
||||
self.world = self.world.task(inputs);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn compile(self) -> CompiledArtifact<F> {
|
||||
let mut snap = self;
|
||||
let warned = std::marker::PhantomData.compile(&snap.world, &mut snap.env);
|
||||
let (doc, warnings) = match warned {
|
||||
Ok(doc) => (Ok(doc.output), doc.warnings),
|
||||
Err(err) => (Err(err), EcoVec::default()),
|
||||
};
|
||||
CompiledArtifact {
|
||||
snap,
|
||||
doc,
|
||||
warnings,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: CompilerFeat> Clone for CompileSnapshot<F> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
signal: self.signal,
|
||||
env: self.env.clone(),
|
||||
world: self.world.clone(),
|
||||
success_doc: self.success_doc.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CompiledArtifact<F: CompilerFeat> {
|
||||
/// The used snapshot.
|
||||
pub snap: CompileSnapshot<F>,
|
||||
/// The diagnostics of the document.
|
||||
pub warnings: EcoVec<SourceDiagnostic>,
|
||||
/// The compiled document.
|
||||
pub doc: SourceResult<Arc<TypstDocument>>,
|
||||
}
|
||||
|
||||
impl<F: CompilerFeat> std::ops::Deref for CompiledArtifact<F> {
|
||||
type Target = CompileSnapshot<F>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.snap
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: CompilerFeat> Clone for CompiledArtifact<F> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
snap: self.snap.clone(),
|
||||
doc: self.doc.clone(),
|
||||
warnings: self.warnings.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: CompilerFeat> CompiledArtifact<F> {
|
||||
pub fn success_doc(&self) -> Option<Arc<TypstDocument>> {
|
||||
self.doc
|
||||
.as_ref()
|
||||
.ok()
|
||||
.cloned()
|
||||
.or_else(|| self.snap.success_doc.clone())
|
||||
}
|
||||
}
|
||||
|
||||
pub trait CompilationHandle<F: CompilerFeat>: Send + Sync + 'static {
|
||||
fn status(&self, revision: usize, rep: CompileReport);
|
||||
fn notify_compile(&self, res: &CompiledArtifact<F>, rep: CompileReport);
|
||||
}
|
||||
|
||||
impl<F: CompilerFeat + Send + Sync + 'static> CompilationHandle<F>
|
||||
for std::marker::PhantomData<fn(F)>
|
||||
{
|
||||
fn status(&self, _revision: usize, _: CompileReport) {}
|
||||
fn notify_compile(&self, _: &CompiledArtifact<F>, _: CompileReport) {}
|
||||
}
|
||||
|
||||
pub enum SucceededArtifact<F: CompilerFeat> {
|
||||
Compiled(CompiledArtifact<F>),
|
||||
Suspend(CompileSnapshot<F>),
|
||||
}
|
||||
|
||||
impl<F: CompilerFeat> SucceededArtifact<F> {
|
||||
pub fn success_doc(&self) -> Option<Arc<TypstDocument>> {
|
||||
match self {
|
||||
SucceededArtifact::Compiled(artifact) => artifact.success_doc(),
|
||||
SucceededArtifact::Suspend(snapshot) => snapshot.success_doc.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn world(&self) -> &CompilerWorld<F> {
|
||||
match self {
|
||||
SucceededArtifact::Compiled(artifact) => &artifact.world,
|
||||
SucceededArtifact::Suspend(snapshot) => &snapshot.world,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Interrupt<F: CompilerFeat> {
|
||||
/// Compile anyway.
|
||||
Compile,
|
||||
/// Compiled from computing thread.
|
||||
Compiled(CompiledArtifact<F>),
|
||||
/// Change the watching entry.
|
||||
ChangeTask(TaskInputs),
|
||||
/// Request compiler to respond a snapshot without needing to wait latest
|
||||
/// compilation.
|
||||
SnapshotRead(oneshot::Sender<CompileSnapshot<F>>),
|
||||
/// Request compiler to respond a snapshot with at least a compilation
|
||||
/// happens on or after current revision.
|
||||
CurrentRead(oneshot::Sender<SucceededArtifact<F>>),
|
||||
/// Memory file changes.
|
||||
Memory(MemoryEvent),
|
||||
/// File system event.
|
||||
Fs(FilesystemEvent),
|
||||
/// Request compiler to stop.
|
||||
Settle(oneshot::Sender<()>),
|
||||
}
|
||||
|
||||
/// Responses from the compiler actor.
|
||||
enum CompilerResponse {
|
||||
/// Response to the file watcher
|
||||
Notify(NotifyMessage),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
struct CompileReasons {
|
||||
/// The snapshot is taken by the memory editing events.
|
||||
by_memory_events: bool,
|
||||
/// The snapshot is taken by the file system events.
|
||||
by_fs_events: bool,
|
||||
/// The snapshot is taken by the entry change.
|
||||
by_entry_update: bool,
|
||||
}
|
||||
|
||||
impl CompileReasons {
|
||||
fn see(&mut self, reason: CompileReasons) {
|
||||
self.by_memory_events |= reason.by_memory_events;
|
||||
self.by_fs_events |= reason.by_fs_events;
|
||||
self.by_entry_update |= reason.by_entry_update;
|
||||
}
|
||||
|
||||
fn any(&self) -> bool {
|
||||
self.by_memory_events || self.by_fs_events || self.by_entry_update
|
||||
}
|
||||
}
|
||||
|
||||
fn no_reason() -> CompileReasons {
|
||||
CompileReasons::default()
|
||||
}
|
||||
|
||||
fn reason_by_mem() -> CompileReasons {
|
||||
CompileReasons {
|
||||
by_memory_events: true,
|
||||
..CompileReasons::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn reason_by_fs() -> CompileReasons {
|
||||
CompileReasons {
|
||||
by_fs_events: true,
|
||||
..CompileReasons::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn reason_by_entry_change() -> CompileReasons {
|
||||
CompileReasons {
|
||||
by_entry_update: true,
|
||||
..CompileReasons::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// A tagged memory event with logical tick.
|
||||
struct TaggedMemoryEvent {
|
||||
/// The logical tick when the event is received.
|
||||
logical_tick: usize,
|
||||
/// The memory event happened.
|
||||
event: MemoryEvent,
|
||||
}
|
||||
|
||||
pub struct CompileServerOpts<F: CompilerFeat> {
|
||||
pub compile_handle: Arc<dyn CompilationHandle<F>>,
|
||||
pub feature_set: FeatureSet,
|
||||
pub cache: CacheTask,
|
||||
}
|
||||
|
||||
impl<F: CompilerFeat + Send + Sync + 'static> Default for CompileServerOpts<F> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
compile_handle: Arc::new(std::marker::PhantomData),
|
||||
feature_set: Default::default(),
|
||||
cache: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The compiler actor.
|
||||
pub struct CompileServerActor<F: CompilerFeat> {
|
||||
/// The underlying universe.
|
||||
pub verse: CompilerUniverse<F>,
|
||||
/// The compilation handle.
|
||||
pub compile_handle: Arc<dyn CompilationHandle<F>>,
|
||||
/// Whether to enable file system watching.
|
||||
pub enable_watch: bool,
|
||||
|
||||
/// The current logical tick.
|
||||
logical_tick: usize,
|
||||
/// Last logical tick when invalidation is caused by shadow update.
|
||||
dirty_shadow_logical_tick: usize,
|
||||
|
||||
/// Estimated latest set of shadow files.
|
||||
estimated_shadow_files: HashSet<Arc<Path>>,
|
||||
/// The latest compiled document.
|
||||
pub(crate) latest_doc: Option<Arc<TypstDocument>>,
|
||||
/// The latest successly compiled document.
|
||||
latest_success_doc: Option<Arc<TypstDocument>>,
|
||||
/// feature set for compile_once mode.
|
||||
once_feature_set: Arc<FeatureSet>,
|
||||
/// Shared feature set for watch mode.
|
||||
watch_feature_set: Arc<FeatureSet>,
|
||||
|
||||
/// Channel for sending interrupts to the compiler actor.
|
||||
intr_tx: mpsc::UnboundedSender<Interrupt<F>>,
|
||||
/// Channel for receiving interrupts from the compiler actor.
|
||||
intr_rx: mpsc::UnboundedReceiver<Interrupt<F>>,
|
||||
/// Shared cache evict task.
|
||||
cache: CacheTask,
|
||||
|
||||
watch_snap: OnceLock<CompileSnapshot<F>>,
|
||||
suspended: bool,
|
||||
compiling: bool,
|
||||
suspended_reason: CompileReasons,
|
||||
committed_revision: usize,
|
||||
}
|
||||
|
||||
impl<F: CompilerFeat + Send + Sync + 'static> CompileServerActor<F> {
|
||||
/// Create a new compiler actor with options
|
||||
pub fn new_with(
|
||||
verse: CompilerUniverse<F>,
|
||||
intr_tx: mpsc::UnboundedSender<Interrupt<F>>,
|
||||
intr_rx: mpsc::UnboundedReceiver<Interrupt<F>>,
|
||||
CompileServerOpts {
|
||||
compile_handle,
|
||||
feature_set,
|
||||
cache: cache_evict,
|
||||
}: CompileServerOpts<F>,
|
||||
) -> Self {
|
||||
let entry = verse.entry_state();
|
||||
|
||||
Self {
|
||||
verse,
|
||||
|
||||
logical_tick: 1,
|
||||
compile_handle,
|
||||
enable_watch: false,
|
||||
dirty_shadow_logical_tick: 0,
|
||||
|
||||
estimated_shadow_files: Default::default(),
|
||||
latest_doc: None,
|
||||
latest_success_doc: None,
|
||||
once_feature_set: Arc::new(feature_set.clone()),
|
||||
watch_feature_set: Arc::new(
|
||||
feature_set.configure(&WITH_COMPILING_STATUS_FEATURE, true),
|
||||
),
|
||||
|
||||
intr_tx,
|
||||
intr_rx,
|
||||
cache: cache_evict,
|
||||
|
||||
watch_snap: OnceLock::new(),
|
||||
suspended: entry.is_inactive(),
|
||||
compiling: false,
|
||||
suspended_reason: no_reason(),
|
||||
committed_revision: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_watch(mut self, watch: bool) -> Self {
|
||||
self.enable_watch = watch;
|
||||
self
|
||||
}
|
||||
|
||||
fn make_env(&self, feature_set: Arc<FeatureSet>) -> CompileEnv {
|
||||
CompileEnv::default().configure_shared(feature_set)
|
||||
}
|
||||
|
||||
/// Launches the compiler actor.
|
||||
pub async fn run(mut self) -> bool {
|
||||
if !self.enable_watch {
|
||||
let artifact = self.compile_once().await;
|
||||
return artifact.doc.is_ok();
|
||||
}
|
||||
|
||||
let (dep_tx, dep_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
let mut curr_reads = vec![];
|
||||
|
||||
log::debug!("CompileServerActor: initialized");
|
||||
|
||||
// Trigger the first compilation (if active)
|
||||
self.run_compile(reason_by_entry_change(), &mut curr_reads, false)
|
||||
.await;
|
||||
|
||||
// Spawn file system watcher.
|
||||
let fs_tx = self.intr_tx.clone();
|
||||
tokio::spawn(watch_deps(dep_rx, move |event| {
|
||||
log_send_error("fs_event", fs_tx.send(Interrupt::Fs(event)));
|
||||
}));
|
||||
|
||||
'event_loop: while let Some(mut event) = self.intr_rx.recv().await {
|
||||
let mut comp_reason = no_reason();
|
||||
|
||||
'accumulate: loop {
|
||||
// Warp the logical clock by one.
|
||||
self.logical_tick += 1;
|
||||
|
||||
// If settle, stop the actor.
|
||||
if let Interrupt::Settle(e) = event {
|
||||
log::info!("CompileServerActor: requested stop");
|
||||
e.send(()).ok();
|
||||
break 'event_loop;
|
||||
}
|
||||
|
||||
if let Interrupt::CurrentRead(event) = event {
|
||||
curr_reads.push(event);
|
||||
} else {
|
||||
comp_reason.see(self.process(event, |res: CompilerResponse| match res {
|
||||
CompilerResponse::Notify(msg) => {
|
||||
log_send_error("compile_deps", dep_tx.send(msg));
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// Try to accumulate more events.
|
||||
match self.intr_rx.try_recv() {
|
||||
Ok(new_event) => event = new_event,
|
||||
_ => break 'accumulate,
|
||||
}
|
||||
}
|
||||
|
||||
// Either we have a reason to compile or we have events that want to have any
|
||||
// compilation.
|
||||
if comp_reason.any() || !curr_reads.is_empty() {
|
||||
self.run_compile(comp_reason, &mut curr_reads, false).await;
|
||||
}
|
||||
}
|
||||
|
||||
log_send_error("settle_notify", dep_tx.send(NotifyMessage::Settle));
|
||||
log::info!("CompileServerActor: exited");
|
||||
true
|
||||
}
|
||||
|
||||
fn snapshot(&self, is_once: bool, reason: CompileReasons) -> CompileSnapshot<F> {
|
||||
let world = self.verse.snapshot();
|
||||
let env = self.make_env(if is_once {
|
||||
self.once_feature_set.clone()
|
||||
} else {
|
||||
self.watch_feature_set.clone()
|
||||
});
|
||||
CompileSnapshot {
|
||||
world,
|
||||
env: env.clone(),
|
||||
signal: ExportSignal {
|
||||
by_entry_update: reason.by_entry_update,
|
||||
by_mem_events: reason.by_memory_events,
|
||||
by_fs_events: reason.by_fs_events,
|
||||
},
|
||||
success_doc: self.latest_success_doc.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Compile the document once.
|
||||
pub async fn compile_once(&mut self) -> CompiledArtifact<F> {
|
||||
self.run_compile(reason_by_entry_change(), &mut vec![], true)
|
||||
.await
|
||||
.expect("is_once is set")
|
||||
}
|
||||
|
||||
/// Compile the document once.
|
||||
async fn run_compile(
|
||||
&mut self,
|
||||
reason: CompileReasons,
|
||||
curr_reads: &mut Vec<oneshot::Sender<SucceededArtifact<F>>>,
|
||||
is_once: bool,
|
||||
) -> Option<CompiledArtifact<F>> {
|
||||
self.suspended_reason.see(reason);
|
||||
let reason = std::mem::take(&mut self.suspended_reason);
|
||||
let start = reflexo::time::now();
|
||||
|
||||
let compiling = self.snapshot(is_once, reason);
|
||||
self.watch_snap = OnceLock::new();
|
||||
self.watch_snap.get_or_init(|| compiling.clone());
|
||||
|
||||
if self.suspended {
|
||||
self.suspended_reason.see(reason);
|
||||
|
||||
for reader in curr_reads.drain(..) {
|
||||
let _ = reader.send(SucceededArtifact::Suspend(compiling.clone()));
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
if self.compiling {
|
||||
self.suspended_reason.see(reason);
|
||||
return None;
|
||||
}
|
||||
|
||||
self.compiling = true;
|
||||
|
||||
let h = self.compile_handle.clone();
|
||||
let curr_reads = std::mem::take(curr_reads);
|
||||
|
||||
// todo unwrap main id
|
||||
let id = compiling.world.main_id().unwrap();
|
||||
let revision = compiling.world.revision().get();
|
||||
|
||||
h.status(revision, CompileReport::Stage(id, "compiling", start));
|
||||
|
||||
let compile = move || async move {
|
||||
let compiled = compiling.compile().await;
|
||||
|
||||
for reader in curr_reads {
|
||||
let _ = reader.send(SucceededArtifact::Compiled(compiled.clone()));
|
||||
}
|
||||
|
||||
let elapsed = start.elapsed().unwrap_or_default();
|
||||
let rep = match &compiled.doc {
|
||||
Ok(..) => CompileReport::CompileSuccess(id, compiled.warnings.clone(), elapsed),
|
||||
Err(err) => CompileReport::CompileError(id, err.clone(), elapsed),
|
||||
};
|
||||
|
||||
let _ = ConsoleDiagReporter::default().export(
|
||||
&compiled.world,
|
||||
Arc::new((compiled.env.features.clone(), rep.clone())),
|
||||
);
|
||||
|
||||
// todo: we need to check revision for really concurrent compilation
|
||||
h.notify_compile(&compiled, rep);
|
||||
|
||||
compiled
|
||||
};
|
||||
|
||||
if is_once {
|
||||
Some(compile().await)
|
||||
} else {
|
||||
let intr_tx = self.intr_tx.clone();
|
||||
tokio::task::spawn(async move {
|
||||
let err = intr_tx.send(Interrupt::Compiled(compile().await));
|
||||
log_send_error("compiled", err);
|
||||
});
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn process_compile(&mut self, artifact: CompiledArtifact<F>, send: impl Fn(CompilerResponse)) {
|
||||
self.compiling = false;
|
||||
|
||||
let mut world = artifact.snap.world;
|
||||
let compiled_revision = world.revision().get();
|
||||
if self.committed_revision >= compiled_revision {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update state.
|
||||
let doc = artifact.doc.ok();
|
||||
self.committed_revision = compiled_revision;
|
||||
self.latest_doc.clone_from(&doc);
|
||||
if doc.is_some() {
|
||||
self.latest_success_doc.clone_from(&self.latest_doc);
|
||||
}
|
||||
|
||||
// Notify the new file dependencies.
|
||||
let mut deps = vec![];
|
||||
world.iter_dependencies(&mut |dep| {
|
||||
if let Ok(x) = world.file_path(dep).and_then(|e| e.to_err()) {
|
||||
deps.push(x.into())
|
||||
}
|
||||
});
|
||||
send(CompilerResponse::Notify(NotifyMessage::SyncDependency(
|
||||
deps,
|
||||
)));
|
||||
|
||||
// Trigger an evict task.
|
||||
self.cache.evict(world.revision(), world.take_cache());
|
||||
}
|
||||
|
||||
/// Process some interrupt. Return whether it needs compilation.
|
||||
fn process(&mut self, event: Interrupt<F>, send: impl Fn(CompilerResponse)) -> CompileReasons {
|
||||
use CompilerResponse::*;
|
||||
|
||||
match event {
|
||||
Interrupt::Compile => {
|
||||
// Increment the revision anyway.
|
||||
self.verse.increment_revision(|_| {});
|
||||
|
||||
reason_by_entry_change()
|
||||
}
|
||||
Interrupt::SnapshotRead(task) => {
|
||||
log::debug!("CompileServerActor: take snapshot");
|
||||
if self
|
||||
.watch_snap
|
||||
.get()
|
||||
.is_some_and(|e| e.world.revision() < self.verse.revision)
|
||||
{
|
||||
self.watch_snap = OnceLock::new();
|
||||
}
|
||||
|
||||
let _ = task.send(
|
||||
self.watch_snap
|
||||
.get_or_init(|| self.snapshot(false, no_reason()))
|
||||
.clone(),
|
||||
);
|
||||
no_reason()
|
||||
}
|
||||
Interrupt::CurrentRead(..) => {
|
||||
unreachable!()
|
||||
}
|
||||
Interrupt::ChangeTask(change) => {
|
||||
self.verse.increment_revision(|verse| {
|
||||
if let Some(inputs) = change.inputs {
|
||||
verse.set_inputs(inputs);
|
||||
}
|
||||
|
||||
if let Some(entry) = change.entry.clone() {
|
||||
let res = verse.mutate_entry(entry);
|
||||
if let Err(err) = res {
|
||||
log::error!("CompileServerActor: change entry error: {err:?}");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// After incrementing the revision
|
||||
if let Some(entry) = change.entry {
|
||||
self.suspended = entry.is_inactive();
|
||||
if self.suspended {
|
||||
log::info!("CompileServerActor: removing diag");
|
||||
self.compile_handle
|
||||
.status(self.verse.revision.get(), CompileReport::Suspend);
|
||||
}
|
||||
|
||||
// Reset the watch state and document state.
|
||||
self.latest_doc = None;
|
||||
self.latest_success_doc = None;
|
||||
self.suspended_reason = no_reason();
|
||||
}
|
||||
|
||||
reason_by_entry_change()
|
||||
}
|
||||
Interrupt::Compiled(artifact) => {
|
||||
self.process_compile(artifact, send);
|
||||
self.process_lagged_compile()
|
||||
}
|
||||
Interrupt::Memory(event) => {
|
||||
log::debug!("CompileServerActor: memory event incoming");
|
||||
|
||||
// Emulate memory changes.
|
||||
let mut files = HashSet::new();
|
||||
if matches!(event, MemoryEvent::Sync(..)) {
|
||||
std::mem::swap(&mut files, &mut self.estimated_shadow_files);
|
||||
}
|
||||
|
||||
let (MemoryEvent::Sync(e) | MemoryEvent::Update(e)) = &event;
|
||||
for path in &e.removes {
|
||||
self.estimated_shadow_files.remove(path);
|
||||
files.insert(Arc::clone(path));
|
||||
}
|
||||
for (path, _) in &e.inserts {
|
||||
self.estimated_shadow_files.insert(Arc::clone(path));
|
||||
files.remove(path);
|
||||
}
|
||||
|
||||
// If there is no invalidation happening, apply memory changes directly.
|
||||
if files.is_empty() && self.dirty_shadow_logical_tick == 0 {
|
||||
self.verse
|
||||
.increment_revision(|verse| Self::apply_memory_changes(verse, event));
|
||||
return reason_by_mem();
|
||||
}
|
||||
|
||||
// Otherwise, send upstream update event.
|
||||
// Also, record the logical tick when shadow is dirty.
|
||||
self.dirty_shadow_logical_tick = self.logical_tick;
|
||||
send(Notify(NotifyMessage::UpstreamUpdate(UpstreamUpdateEvent {
|
||||
invalidates: files.into_iter().collect(),
|
||||
opaque: Box::new(TaggedMemoryEvent {
|
||||
logical_tick: self.logical_tick,
|
||||
event,
|
||||
}),
|
||||
})));
|
||||
|
||||
no_reason()
|
||||
}
|
||||
Interrupt::Fs(mut event) => {
|
||||
log::debug!("CompileServerActor: fs event incoming {event:?}");
|
||||
|
||||
let mut reason = reason_by_fs();
|
||||
|
||||
// Apply file system changes.
|
||||
let dirty_tick = &mut self.dirty_shadow_logical_tick;
|
||||
self.verse.increment_revision(|verse| {
|
||||
// Handle delayed upstream update event before applying file system changes
|
||||
if Self::apply_delayed_memory_changes(verse, dirty_tick, &mut event).is_none() {
|
||||
log::warn!("CompileServerActor: unknown upstream update event");
|
||||
|
||||
// Actual a delayed memory event.
|
||||
reason = reason_by_mem();
|
||||
}
|
||||
verse.vfs().notify_fs_event(event)
|
||||
});
|
||||
|
||||
reason
|
||||
}
|
||||
Interrupt::Settle(_) => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Process reason after each compilation.
|
||||
fn process_lagged_compile(&mut self) -> CompileReasons {
|
||||
// The reason which is kept but not used.
|
||||
std::mem::take(&mut self.suspended_reason)
|
||||
}
|
||||
|
||||
/// Apply delayed memory changes to underlying compiler.
|
||||
fn apply_delayed_memory_changes(
|
||||
verse: &mut RevisingUniverse<F>,
|
||||
dirty_shadow_logical_tick: &mut usize,
|
||||
event: &mut FilesystemEvent,
|
||||
) -> Option<()> {
|
||||
// Handle delayed upstream update event before applying file system changes
|
||||
if let FilesystemEvent::UpstreamUpdate { upstream_event, .. } = event {
|
||||
let event = upstream_event.take()?.opaque;
|
||||
let TaggedMemoryEvent {
|
||||
logical_tick,
|
||||
event,
|
||||
} = *event.downcast().ok()?;
|
||||
|
||||
// Recovery from dirty shadow state.
|
||||
if logical_tick == *dirty_shadow_logical_tick {
|
||||
*dirty_shadow_logical_tick = 0;
|
||||
}
|
||||
|
||||
Self::apply_memory_changes(verse, event);
|
||||
}
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
/// Apply memory changes to underlying compiler.
|
||||
fn apply_memory_changes(verse: &mut RevisingUniverse<F>, event: MemoryEvent) {
|
||||
let mut vfs = verse.vfs();
|
||||
if matches!(event, MemoryEvent::Sync(..)) {
|
||||
vfs.reset_shadow();
|
||||
}
|
||||
match event {
|
||||
MemoryEvent::Update(event) | MemoryEvent::Sync(event) => {
|
||||
for path in event.removes {
|
||||
let _ = vfs.unmap_shadow(&path);
|
||||
}
|
||||
for (path, snap) in event.inserts {
|
||||
let _ = vfs.map_shadow(&path, snap);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn log_send_error<T>(chan: &'static str, res: Result<(), mpsc::error::SendError<T>>) -> bool {
|
||||
res.map_err(|err| log::warn!("CompileServerActor: send to {chan} error: {err}"))
|
||||
.is_ok()
|
||||
}
|
|
@ -5,13 +5,13 @@ use std::path::PathBuf;
|
|||
|
||||
use lsp_server::RequestId;
|
||||
use lsp_types::*;
|
||||
use reflexo_typst::error::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value as JsonValue;
|
||||
use task::TraceParams;
|
||||
use tinymist_assets::TYPST_PREVIEW_HTML;
|
||||
use tinymist_query::package::PackageInfo;
|
||||
use tinymist_query::{ExportKind, LocalContextGuard, PageSelection};
|
||||
use tinymist_std::error::prelude::*;
|
||||
use typst::diag::{eco_format, EcoString, StrResult};
|
||||
use typst::syntax::package::{PackageSpec, VersionlessPackageSpec};
|
||||
use world::TaskInputs;
|
||||
|
@ -211,9 +211,7 @@ impl LanguageState {
|
|||
/// Clear all cached resources.
|
||||
pub fn clear_cache(&mut self, _arguments: Vec<JsonValue>) -> AnySchedulableResponse {
|
||||
comemo::evict(0);
|
||||
for dead in self.servers_mut() {
|
||||
dead.clear_cache();
|
||||
}
|
||||
self.project.analysis.clear_cache();
|
||||
just_ok(JsonValue::Null)
|
||||
}
|
||||
|
||||
|
@ -285,31 +283,36 @@ impl LanguageState {
|
|||
}
|
||||
|
||||
let previewer = typst_preview::PreviewBuilder::new(cli_args.preview.clone());
|
||||
let watcher = previewer.compile_watcher();
|
||||
|
||||
let primary = self.primary().handle.clone();
|
||||
if !cli_args.not_as_primary && primary.register_preview(previewer.compile_watcher()) {
|
||||
let primary = &mut self.project.state.primary;
|
||||
if !cli_args.not_as_primary && self.preview.watchers.register(&primary.id, watcher) {
|
||||
let id = primary.id.clone();
|
||||
// todo: recover pin status reliably
|
||||
self.pin_entry(Some(entry))
|
||||
.map_err(|e| internal_error(format!("could not pin file: {e}")))?;
|
||||
|
||||
self.preview.start(cli_args, previewer, primary, true)
|
||||
self.preview.start(cli_args, previewer, id, true)
|
||||
} else {
|
||||
self.restart_dedicate(&task_id, Some(entry));
|
||||
let Some(dedicate) = self.dedicate(&task_id) else {
|
||||
let id = self
|
||||
.restart_dedicate(&task_id, Some(entry))
|
||||
.map_err(internal_error)?;
|
||||
|
||||
// Gets the task-dedicated compile server.
|
||||
let Some(dedicate) = self.project.state.dedicates.iter_mut().find(|d| d.id == id)
|
||||
else {
|
||||
return Err(invalid_params(
|
||||
"just restarted compiler instance for the task is not found",
|
||||
));
|
||||
};
|
||||
|
||||
let handle = dedicate.handle.clone();
|
||||
|
||||
if !handle.register_preview(previewer.compile_watcher()) {
|
||||
if !self.project.preview.register(&dedicate.id, watcher) {
|
||||
return Err(invalid_params(
|
||||
"cannot register preview to the compiler instance",
|
||||
));
|
||||
}
|
||||
|
||||
self.preview.start(cli_args, previewer, handle, false)
|
||||
self.preview.start(cli_args, previewer, id, false)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -345,7 +348,7 @@ impl LanguageState {
|
|||
let from_source = get_arg!(args[0] as String);
|
||||
let to_path = get_arg!(args[1] as Option<PathBuf>).map(From::from);
|
||||
|
||||
let snap = self.primary().snapshot().map_err(z_internal_error)?;
|
||||
let snap = self.snapshot().map_err(z_internal_error)?;
|
||||
|
||||
just_future(async move {
|
||||
let snap = snap.receive().await.map_err(z_internal_error)?;
|
||||
|
@ -390,7 +393,7 @@ impl LanguageState {
|
|||
|
||||
let from_source = get_arg!(args[0] as String);
|
||||
|
||||
let snap = self.primary().snapshot().map_err(z_internal_error)?;
|
||||
let snap = self.snapshot().map_err(z_internal_error)?;
|
||||
|
||||
just_future(async move {
|
||||
let snap = snap.receive().await.map_err(z_internal_error)?;
|
||||
|
@ -458,7 +461,7 @@ impl LanguageState {
|
|||
|
||||
let entry = self.entry_resolver().resolve(Some(path));
|
||||
|
||||
let snap = self.primary().snapshot().map_err(z_internal_error)?;
|
||||
let snap = self.snapshot().map_err(z_internal_error)?;
|
||||
let user_action = self.user_action;
|
||||
|
||||
just_future(async move {
|
||||
|
@ -526,13 +529,13 @@ impl LanguageState {
|
|||
impl LanguageState {
|
||||
/// Get the all valid fonts
|
||||
pub fn resource_fonts(&mut self, _arguments: Vec<JsonValue>) -> AnySchedulableResponse {
|
||||
let snapshot = self.primary().snapshot().map_err(z_internal_error)?;
|
||||
let snapshot = self.snapshot().map_err(z_internal_error)?;
|
||||
just_future(Self::get_font_resources(snapshot))
|
||||
}
|
||||
|
||||
/// Get the all valid symbols
|
||||
pub fn resource_symbols(&mut self, _arguments: Vec<JsonValue>) -> AnySchedulableResponse {
|
||||
let snapshot = self.primary().snapshot().map_err(z_internal_error)?;
|
||||
let snapshot = self.snapshot().map_err(z_internal_error)?;
|
||||
just_future(Self::get_symbol_resources(snapshot))
|
||||
}
|
||||
|
||||
|
@ -549,7 +552,7 @@ impl LanguageState {
|
|||
|
||||
/// Get directory of packages
|
||||
pub fn resource_package_dirs(&mut self, _arguments: Vec<JsonValue>) -> AnySchedulableResponse {
|
||||
let snap = self.primary().snapshot().map_err(z_internal_error)?;
|
||||
let snap = self.snapshot().map_err(z_internal_error)?;
|
||||
just_future(async move {
|
||||
let snap = snap.receive().await.map_err(z_internal_error)?;
|
||||
let paths = snap.world.registry.paths();
|
||||
|
@ -563,7 +566,7 @@ impl LanguageState {
|
|||
&mut self,
|
||||
_arguments: Vec<JsonValue>,
|
||||
) -> AnySchedulableResponse {
|
||||
let snap = self.primary().snapshot().map_err(z_internal_error)?;
|
||||
let snap = self.snapshot().map_err(z_internal_error)?;
|
||||
just_future(async move {
|
||||
let snap = snap.receive().await.map_err(z_internal_error)?;
|
||||
let paths = snap.world.registry.local_path();
|
||||
|
@ -579,7 +582,7 @@ impl LanguageState {
|
|||
) -> AnySchedulableResponse {
|
||||
let ns = get_arg!(arguments[1] as EcoString);
|
||||
|
||||
let snap = self.primary().snapshot().map_err(z_internal_error)?;
|
||||
let snap = self.snapshot().map_err(z_internal_error)?;
|
||||
just_future(async move {
|
||||
let snap = snap.receive().await.map_err(z_internal_error)?;
|
||||
let packages =
|
||||
|
@ -597,7 +600,7 @@ impl LanguageState {
|
|||
&mut self,
|
||||
mut arguments: Vec<JsonValue>,
|
||||
) -> AnySchedulableResponse {
|
||||
let fut = self.primary().query_snapshot().map_err(internal_error)?;
|
||||
let fut = self.query_snapshot().map_err(internal_error)?;
|
||||
let info = get_arg!(arguments[1] as PackageInfo);
|
||||
|
||||
just_future(async move {
|
||||
|
@ -657,7 +660,7 @@ impl LanguageState {
|
|||
info: PackageInfo,
|
||||
f: impl FnOnce(&mut LocalContextGuard) -> LspResult<T> + Send + Sync,
|
||||
) -> LspResult<impl Future<Output = LspResult<T>>> {
|
||||
let fut = self.primary().query_snapshot().map_err(internal_error)?;
|
||||
let fut = self.query_snapshot().map_err(internal_error)?;
|
||||
|
||||
Ok(async move {
|
||||
let snap = fut.receive().await.map_err(z_internal_error)?;
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
mod actor;
|
||||
mod cmd;
|
||||
mod init;
|
||||
pub mod project;
|
||||
mod resource;
|
||||
mod server;
|
||||
mod stats;
|
||||
|
@ -33,15 +34,15 @@ mod utils;
|
|||
pub use init::*;
|
||||
pub use server::*;
|
||||
pub use sync_lsp::LspClient;
|
||||
pub use tinymist_project as project;
|
||||
pub use tinymist_project::world;
|
||||
pub use tinymist_query as query;
|
||||
pub use world::*;
|
||||
pub use world::{CompileFontArgs, CompileOnceArgs, CompilePackageArgs};
|
||||
|
||||
use lsp_server::{RequestId, ResponseError};
|
||||
use serde_json::from_value;
|
||||
use sync_lsp::*;
|
||||
use utils::*;
|
||||
use world::*;
|
||||
|
||||
use tinymist_query::CompilerQueryResponse;
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ use tinymist::{
|
|||
tool::project::{project_main, task_main},
|
||||
CompileConfig, Config, LanguageState, RegularInit, SuperInit, UserActionTask,
|
||||
};
|
||||
use tinymist::{world::TaskInputs, WorldProvider};
|
||||
use tinymist::{world::TaskInputs, world::WorldProvider};
|
||||
use tinymist_core::LONG_VERSION;
|
||||
use tinymist_query::{package::PackageInfo, EntryResolver};
|
||||
use typst::foundations::IntoValue;
|
||||
|
@ -253,7 +253,7 @@ pub fn trace_lsp_main(args: TraceLspArgs) -> anyhow::Result<()> {
|
|||
|
||||
let entry = state.entry_resolver().resolve(Some(input.as_path().into()));
|
||||
|
||||
let snap = state.primary().snapshot().unwrap();
|
||||
let snap = state.snapshot().unwrap();
|
||||
|
||||
RUNTIMES.tokio_runtime.block_on(async {
|
||||
let snap = snap.receive().await.unwrap();
|
||||
|
@ -274,7 +274,7 @@ pub fn trace_lsp_main(args: TraceLspArgs) -> anyhow::Result<()> {
|
|||
|
||||
/// The main entry point for language server queries.
|
||||
pub fn query_main(cmds: QueryCommands) -> anyhow::Result<()> {
|
||||
use tinymist::package::PackageRegistry;
|
||||
use tinymist_project::package::PackageRegistry;
|
||||
|
||||
with_stdio_transport(MirrorArgs::default(), |conn| {
|
||||
let client_root = LspClientRoot::new(RUNTIMES.tokio_runtime.handle().clone(), conn.sender);
|
||||
|
@ -302,7 +302,7 @@ pub fn query_main(cmds: QueryCommands) -> anyhow::Result<()> {
|
|||
|
||||
let state = service.state_mut().unwrap();
|
||||
|
||||
let snap = state.primary().snapshot().unwrap();
|
||||
let snap = state.snapshot().unwrap();
|
||||
let res = RUNTIMES.tokio_runtime.block_on(async move {
|
||||
let w = snap.receive().await.map_err(internal_error)?;
|
||||
match cmds {
|
||||
|
|
421
crates/tinymist/src/project.rs
Normal file
421
crates/tinymist/src/project.rs
Normal file
|
@ -0,0 +1,421 @@
|
|||
//! The project.
|
||||
//!
|
||||
//! ```ascii
|
||||
//! ┌────────────────────────────────┐ ┌────────────┐
|
||||
//! │ main::compile_actor │◄───────►│notify_actor│
|
||||
//! └─────┬────────────────────▲─────┘ └────────────┘
|
||||
//! │ │
|
||||
//! ┌─────▼────────────────────┴─────┐ handler ┌────────────┐
|
||||
//! │ compiler::compile_handler ├────────►│ rest actors│
|
||||
//! └────────────────────────────────┘ └────────────┘
|
||||
//! ```
|
||||
//!
|
||||
//! We use typst by creating a [`ProjectCompiler`] and
|
||||
//! running compiler with callbacking [`LspProjectHandler`] incrementally. An
|
||||
//! additional [`LocalCompileHandler`] is also created to control the
|
||||
//! [`ProjectCompiler`].
|
||||
//!
|
||||
//! The [`LspProjectHandler`] will push information to other actors.
|
||||
|
||||
#![allow(missing_docs)]
|
||||
|
||||
pub use tinymist_project::*;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::bail;
|
||||
use log::{error, info, trace};
|
||||
use parking_lot::Mutex;
|
||||
use reflexo::{hash::FxHashMap, path::unix_slash};
|
||||
use reflexo_typst::{typst::prelude::EcoVec, CompileReport};
|
||||
use sync_lsp::LspClient;
|
||||
use tinymist_query::{
|
||||
analysis::{Analysis, AnalysisRevLock, LocalContextGuard},
|
||||
CompilerQueryRequest, CompilerQueryResponse, DiagnosticsMap, SemanticRequest, StatefulRequest,
|
||||
VersionedDocument,
|
||||
};
|
||||
use tinymist_std::error::prelude::*;
|
||||
use tokio::sync::{mpsc, oneshot};
|
||||
use typst::{diag::SourceDiagnostic, World};
|
||||
|
||||
use crate::actor::editor::{CompileStatus, DocVersion, EditorRequest, TinymistCompileStatusEnum};
|
||||
use crate::stats::{CompilerQueryStats, QueryStatGuard};
|
||||
use crate::world::vfs::MemoryEvent;
|
||||
|
||||
type EditorSender = mpsc::UnboundedSender<EditorRequest>;
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct LspPreviewState {
|
||||
#[cfg(feature = "preview")]
|
||||
pub(crate) inner: Arc<Mutex<FxHashMap<ProjectInsId, Arc<typst_preview::CompileWatcher>>>>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "preview")]
|
||||
impl LspPreviewState {
|
||||
// todo: multiple preview support
|
||||
#[must_use]
|
||||
pub fn register(&self, id: &ProjectInsId, handle: &Arc<typst_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 {
|
||||
let mut p = self.inner.lock();
|
||||
if p.remove(task_id).is_some() {
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get(&self, task_id: &ProjectInsId) -> Option<Arc<typst_preview::CompileWatcher>> {
|
||||
self.inner.lock().get(task_id).cloned()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ProjectStateExt {
|
||||
pub is_compiling: bool,
|
||||
}
|
||||
|
||||
/// LSP project compiler.
|
||||
pub type LspProjectCompiler = ProjectCompiler<LspCompilerFeat, ProjectStateExt>;
|
||||
|
||||
pub struct Project {
|
||||
pub diag_group: String,
|
||||
pub state: LspProjectCompiler,
|
||||
pub preview: LspPreviewState,
|
||||
pub analysis: Arc<Analysis>,
|
||||
pub stats: CompilerQueryStats,
|
||||
pub export: crate::task::ExportTask,
|
||||
}
|
||||
|
||||
impl Project {
|
||||
/// Snapshot the compiler thread for tasks
|
||||
pub fn snapshot(&mut self) -> ZResult<WorldSnapFut> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
let snap = self.state.snapshot();
|
||||
let _ = tx.send(snap);
|
||||
|
||||
Ok(WorldSnapFut { rx })
|
||||
}
|
||||
|
||||
/// Snapshot the compiler thread for language queries
|
||||
pub fn query_snapshot(&mut self, q: Option<&CompilerQueryRequest>) -> ZResult<QuerySnapFut> {
|
||||
let fut = self.snapshot()?;
|
||||
let analysis = self.analysis.clone();
|
||||
let rev_lock = analysis.lock_revision(q);
|
||||
|
||||
Ok(QuerySnapFut {
|
||||
fut,
|
||||
analysis,
|
||||
rev_lock,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn add_memory_changes(&mut self, event: MemoryEvent) {
|
||||
self.state.process(Interrupt::Memory(event));
|
||||
}
|
||||
|
||||
pub fn interrupt(&mut self, intr: Interrupt<LspCompilerFeat>) {
|
||||
if let Interrupt::Compiled(compiled) = &intr {
|
||||
let proj = self.state.projects().find(|p| p.id == compiled.id);
|
||||
if let Some(proj) = proj {
|
||||
proj.ext.is_compiling = false;
|
||||
}
|
||||
}
|
||||
|
||||
self.state.process(intr);
|
||||
}
|
||||
|
||||
pub fn change_task(&mut self, task: TaskInputs) {
|
||||
self.state
|
||||
.process(Interrupt::ChangeTask(self.state.primary.id.clone(), task));
|
||||
}
|
||||
|
||||
pub(crate) fn stop(&mut self) {
|
||||
// todo: stop all compilations
|
||||
}
|
||||
|
||||
pub(crate) fn restart_dedicate(
|
||||
&mut self,
|
||||
group: &str,
|
||||
entry: EntryState,
|
||||
) -> ZResult<ProjectInsId> {
|
||||
self.state.restart_dedicate(group, entry)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CompileHandlerImpl {
|
||||
pub(crate) diag_group: String,
|
||||
pub(crate) analysis: Arc<Analysis>,
|
||||
|
||||
pub(crate) preview: LspPreviewState,
|
||||
|
||||
pub(crate) export: crate::task::ExportTask,
|
||||
pub(crate) editor_tx: EditorSender,
|
||||
pub(crate) client: Box<dyn ProjectClient>,
|
||||
|
||||
pub(crate) notified_revision: parking_lot::Mutex<usize>,
|
||||
}
|
||||
|
||||
pub trait ProjectClient: Send + Sync + 'static {
|
||||
fn send_event(&self, event: LspInterrupt);
|
||||
}
|
||||
|
||||
impl ProjectClient for LspClient {
|
||||
fn send_event(&self, event: LspInterrupt) {
|
||||
self.send_event(event);
|
||||
}
|
||||
}
|
||||
|
||||
impl ProjectClient for tokio::sync::mpsc::UnboundedSender<LspInterrupt> {
|
||||
fn send_event(&self, event: LspInterrupt) {
|
||||
if let Err(err) = self.send(event) {
|
||||
error!("failed to send interrupt: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CompileHandlerImpl {
|
||||
fn push_diagnostics(&self, revision: usize, diagnostics: Option<DiagnosticsMap>) {
|
||||
let dv = DocVersion {
|
||||
group: self.diag_group.clone(),
|
||||
revision,
|
||||
};
|
||||
let res = self.editor_tx.send(EditorRequest::Diag(dv, diagnostics));
|
||||
if let Err(err) = res {
|
||||
error!("failed to send diagnostics: {err:#}");
|
||||
}
|
||||
}
|
||||
|
||||
fn notify_diagnostics(
|
||||
&self,
|
||||
world: &LspWorld,
|
||||
errors: EcoVec<SourceDiagnostic>,
|
||||
warnings: EcoVec<SourceDiagnostic>,
|
||||
) {
|
||||
let revision = world.revision().get();
|
||||
trace!("notify diagnostics({revision}): {errors:#?} {warnings:#?}");
|
||||
|
||||
let diagnostics = tinymist_query::convert_diagnostics(
|
||||
world,
|
||||
errors.iter().chain(warnings.iter()),
|
||||
self.analysis.position_encoding,
|
||||
);
|
||||
|
||||
let entry = world.entry_state();
|
||||
// todo: better way to remove diagnostics
|
||||
// todo: check all errors in this file
|
||||
let detached = entry.is_inactive();
|
||||
let valid = !detached;
|
||||
self.push_diagnostics(revision, valid.then_some(diagnostics));
|
||||
}
|
||||
}
|
||||
|
||||
impl CompileHandler<LspCompilerFeat, ProjectStateExt> 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 {
|
||||
if s.ext.is_compiling {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(compile_fn) = s.may_compile(&c.handler) else {
|
||||
continue;
|
||||
};
|
||||
s.ext.is_compiling = true;
|
||||
rayon::spawn(move || {
|
||||
compile_fn();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn status(&self, revision: usize, id: &ProjectInsId, rep: CompileReport) {
|
||||
// todo: seems to duplicate with CompileStatus
|
||||
let status = match rep {
|
||||
CompileReport::Suspend => {
|
||||
self.push_diagnostics(revision, None);
|
||||
TinymistCompileStatusEnum::CompileSuccess
|
||||
}
|
||||
CompileReport::Stage(_, _, _) => TinymistCompileStatusEnum::Compiling,
|
||||
CompileReport::CompileSuccess(_, _, _) => TinymistCompileStatusEnum::CompileSuccess,
|
||||
CompileReport::CompileError(_, _, _) | CompileReport::ExportError(_, _, _) => {
|
||||
TinymistCompileStatusEnum::CompileError
|
||||
}
|
||||
};
|
||||
|
||||
let this = &self;
|
||||
this.editor_tx
|
||||
.send(EditorRequest::Status(CompileStatus {
|
||||
group: this.diag_group.clone(),
|
||||
path: rep
|
||||
.compiling_id()
|
||||
.map(|s| unix_slash(s.vpath().as_rooted_path()))
|
||||
.unwrap_or_default(),
|
||||
status,
|
||||
}))
|
||||
.unwrap();
|
||||
|
||||
#[cfg(feature = "preview")]
|
||||
if let Some(inner) = this.preview.get(id) {
|
||||
use typst_preview::CompileStatus;
|
||||
|
||||
let status = match rep {
|
||||
CompileReport::Suspend => CompileStatus::CompileSuccess,
|
||||
CompileReport::Stage(_, _, _) => CompileStatus::Compiling,
|
||||
CompileReport::CompileSuccess(_, _, _) => CompileStatus::CompileSuccess,
|
||||
CompileReport::CompileError(_, _, _) | CompileReport::ExportError(_, _, _) => {
|
||||
CompileStatus::CompileError
|
||||
}
|
||||
};
|
||||
|
||||
inner.status(status);
|
||||
}
|
||||
}
|
||||
|
||||
fn notify_compile(&self, snap: &CompiledArtifact<LspCompilerFeat>, rep: CompileReport) {
|
||||
// todo: we need to manage the revision for fn status() as well
|
||||
{
|
||||
let mut n_rev = self.notified_revision.lock();
|
||||
if *n_rev >= snap.world.revision().get() {
|
||||
log::info!(
|
||||
"TypstActor: already notified for revision {} <= {n_rev}",
|
||||
snap.world.revision(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
*n_rev = snap.world.revision().get();
|
||||
}
|
||||
|
||||
self.notify_diagnostics(
|
||||
&snap.world,
|
||||
snap.doc.clone().err().unwrap_or_default(),
|
||||
snap.warnings.clone(),
|
||||
);
|
||||
|
||||
self.client.send_event(LspInterrupt::Compiled(snap.clone()));
|
||||
self.export.signal(snap, snap.signal);
|
||||
|
||||
self.editor_tx
|
||||
.send(EditorRequest::Status(CompileStatus {
|
||||
group: self.diag_group.clone(),
|
||||
path: rep
|
||||
.compiling_id()
|
||||
.map(|s| unix_slash(s.vpath().as_rooted_path()))
|
||||
.unwrap_or_default(),
|
||||
status: if snap.doc.is_ok() {
|
||||
TinymistCompileStatusEnum::CompileSuccess
|
||||
} else {
|
||||
TinymistCompileStatusEnum::CompileError
|
||||
},
|
||||
}))
|
||||
.unwrap();
|
||||
|
||||
#[cfg(feature = "preview")]
|
||||
if let Some(inner) = self.preview.get(&snap.id) {
|
||||
let snap = snap.clone();
|
||||
inner.notify_compile(Arc::new(crate::tool::preview::PreviewCompileView { snap }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct QuerySnapWithStat {
|
||||
pub fut: QuerySnapFut,
|
||||
pub(crate) stat: QueryStatGuard,
|
||||
}
|
||||
|
||||
pub struct WorldSnapFut {
|
||||
rx: oneshot::Receiver<CompileSnapshot<LspCompilerFeat>>,
|
||||
}
|
||||
|
||||
impl WorldSnapFut {
|
||||
/// wait for the snapshot to be ready
|
||||
pub async fn receive(self) -> ZResult<CompileSnapshot<LspCompilerFeat>> {
|
||||
self.rx
|
||||
.await
|
||||
.map_err(map_string_err("failed to get snapshot"))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct QuerySnapFut {
|
||||
fut: WorldSnapFut,
|
||||
analysis: Arc<Analysis>,
|
||||
rev_lock: AnalysisRevLock,
|
||||
}
|
||||
|
||||
impl QuerySnapFut {
|
||||
/// wait for the snapshot to be ready
|
||||
pub async fn receive(self) -> ZResult<QuerySnap> {
|
||||
let snap = self.fut.receive().await?;
|
||||
Ok(QuerySnap {
|
||||
snap,
|
||||
analysis: self.analysis,
|
||||
rev_lock: self.rev_lock,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct QuerySnap {
|
||||
pub snap: CompileSnapshot<LspCompilerFeat>,
|
||||
analysis: Arc<Analysis>,
|
||||
rev_lock: AnalysisRevLock,
|
||||
}
|
||||
|
||||
impl std::ops::Deref for QuerySnap {
|
||||
type Target = CompileSnapshot<LspCompilerFeat>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.snap
|
||||
}
|
||||
}
|
||||
|
||||
impl QuerySnap {
|
||||
pub fn task(mut self, inputs: TaskInputs) -> Self {
|
||||
self.snap = self.snap.task(inputs);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn run_stateful<T: StatefulRequest>(
|
||||
self,
|
||||
query: T,
|
||||
wrapper: fn(Option<T::Response>) -> CompilerQueryResponse,
|
||||
) -> anyhow::Result<CompilerQueryResponse> {
|
||||
let doc = self.snap.success_doc.as_ref().map(|doc| VersionedDocument {
|
||||
version: self.world.revision().get(),
|
||||
document: doc.clone(),
|
||||
});
|
||||
self.run_analysis(|ctx| query.request(ctx, doc))
|
||||
.map(wrapper)
|
||||
}
|
||||
|
||||
pub fn run_semantic<T: SemanticRequest>(
|
||||
self,
|
||||
query: T,
|
||||
wrapper: fn(Option<T::Response>) -> CompilerQueryResponse,
|
||||
) -> anyhow::Result<CompilerQueryResponse> {
|
||||
self.run_analysis(|ctx| query.request(ctx)).map(wrapper)
|
||||
}
|
||||
|
||||
pub fn run_analysis<T>(self, f: impl FnOnce(&mut LocalContextGuard) -> T) -> anyhow::Result<T> {
|
||||
let world = self.snap.world;
|
||||
let Some(main) = world.main_id() else {
|
||||
error!("TypstActor: main file is not set");
|
||||
bail!("main file is not set");
|
||||
};
|
||||
world.source(main).map_err(|err| {
|
||||
info!("TypstActor: failed to prepare main file: {err:?}");
|
||||
anyhow::anyhow!("failed to get source: {err}")
|
||||
})?;
|
||||
|
||||
let mut analysis = self.analysis.snapshot_(world, self.rev_lock);
|
||||
Ok(f(&mut analysis))
|
||||
}
|
||||
}
|
|
@ -5,7 +5,7 @@ use tinymist_std::debug_loc::DataSource;
|
|||
use typst::text::{FontStretch, FontStyle, FontWeight};
|
||||
|
||||
use super::prelude::*;
|
||||
use crate::actor::typ_client::WorldSnapFut;
|
||||
use crate::project::WorldSnapFut;
|
||||
use crate::world::font::FontResolver;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
|
|
@ -6,13 +6,13 @@ mod prelude {
|
|||
pub use std::collections::HashMap;
|
||||
|
||||
pub use once_cell::sync::Lazy;
|
||||
pub use reflexo_typst::error::prelude::*;
|
||||
pub use reflexo_typst::Compiler;
|
||||
pub use reflexo_vec2svg::ir::{GlyphItem, GlyphRef};
|
||||
pub use reflexo_vec2svg::{DefaultExportFeature, SvgTask, SvgText};
|
||||
pub use serde::{Deserialize, Serialize};
|
||||
pub use serde_json::Value as JsonValue;
|
||||
pub use sync_lsp::*;
|
||||
pub use tinymist_std::error::prelude::*;
|
||||
pub use typst::foundations::{Scope, Value};
|
||||
pub use typst::symbols::Symbol;
|
||||
|
||||
|
|
|
@ -3,11 +3,10 @@ use std::{collections::BTreeMap, path::Path, sync::Arc};
|
|||
// use reflexo_typst::font::GlyphId;
|
||||
use reflexo_typst::{vector::font::GlyphId, TypstDocument, TypstFont};
|
||||
use sync_lsp::LspResult;
|
||||
use typst::syntax::VirtualPath;
|
||||
use typst::World;
|
||||
use typst::{syntax::VirtualPath, World};
|
||||
|
||||
use crate::world::{base::ShadowApi, EntryState, TaskInputs};
|
||||
use crate::{actor::typ_client::WorldSnapFut, z_internal_error};
|
||||
use crate::{project::WorldSnapFut, z_internal_error};
|
||||
|
||||
use super::prelude::*;
|
||||
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
//! tinymist's language server
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::ops::Deref;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::world::vfs::{notify::MemoryEvent, FileChangeSet};
|
||||
use actor::editor::EditorActor;
|
||||
use anyhow::anyhow;
|
||||
use anyhow::Context;
|
||||
|
@ -13,24 +13,41 @@ use lsp_server::RequestId;
|
|||
use lsp_types::request::{GotoDeclarationParams, WorkspaceConfiguration};
|
||||
use lsp_types::*;
|
||||
use once_cell::sync::OnceCell;
|
||||
use reflexo_typst::{error::prelude::*, Bytes, Error, ImmutPath};
|
||||
use prelude::*;
|
||||
use project::world::EntryState;
|
||||
use project::{watch_deps, LspPreviewState};
|
||||
use project::{CompileHandlerImpl, Project, QuerySnapFut, QuerySnapWithStat, WorldSnapFut};
|
||||
use reflexo_typst::Bytes;
|
||||
use request::{RegisterCapability, UnregisterCapability};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Map, Value as JsonValue};
|
||||
use sync_lsp::*;
|
||||
use task::{CacheTask, ExportUserConfig, FormatTask, FormatterConfig, UserActionTask};
|
||||
use task::{
|
||||
ExportConfig, ExportTask, ExportUserConfig, FormatTask, FormatterConfig, UserActionTask,
|
||||
};
|
||||
use tinymist_project::ProjectInsId;
|
||||
use tinymist_query::analysis::{Analysis, PeriscopeProvider};
|
||||
use tinymist_query::{
|
||||
to_typst_range, CompilerQueryRequest, CompilerQueryResponse, FoldRequestFeature,
|
||||
PositionEncoding, SyntaxRequest,
|
||||
OnExportRequest, PositionEncoding, ServerInfoResponse, SyntaxRequest,
|
||||
};
|
||||
use tinymist_query::{EntryResolver, PageSelection};
|
||||
use tinymist_query::{ExportKind, LocalContext, VersionedDocument};
|
||||
use tinymist_render::PeriscopeRenderer;
|
||||
use tinymist_std::Error;
|
||||
use tinymist_std::ImmutPath;
|
||||
use tokio::sync::mpsc;
|
||||
use typst::layout::Position as TypstPosition;
|
||||
use typst::{diag::FileResult, syntax::Source};
|
||||
|
||||
use crate::project::LspInterrupt;
|
||||
use crate::project::{CompileServerOpts, ProjectCompiler};
|
||||
use crate::stats::CompilerQueryStats;
|
||||
use crate::world::vfs::{notify::MemoryEvent, FileChangeSet};
|
||||
use crate::world::{ImmutDict, LspUniverseBuilder, TaskInputs};
|
||||
|
||||
use super::{init::*, *};
|
||||
use crate::actor::editor::EditorRequest;
|
||||
use crate::actor::typ_client::CompileClientActor;
|
||||
use crate::world::TaskInputs;
|
||||
|
||||
pub(crate) use futures::Future;
|
||||
|
||||
|
@ -50,6 +67,8 @@ fn as_path_pos(inp: TextDocumentPositionParams) -> (PathBuf, Position) {
|
|||
pub struct LanguageState {
|
||||
/// The lsp client
|
||||
pub client: TypedLspClient<Self>,
|
||||
/// The project state.
|
||||
pub project: Project,
|
||||
|
||||
// State to synchronize with the client.
|
||||
/// Whether the server has registered semantic tokens capabilities.
|
||||
|
@ -77,18 +96,12 @@ pub struct LanguageState {
|
|||
pub preview: tool::preview::PreviewState,
|
||||
/// The diagnostics sender to send diagnostics to `crate::actor::cluster`.
|
||||
pub editor_tx: mpsc::UnboundedSender<EditorRequest>,
|
||||
/// The primary compiler actor.
|
||||
pub primary: Option<CompileClientActor>,
|
||||
/// The compiler actors for tasks
|
||||
pub dedicates: Vec<CompileClientActor>,
|
||||
/// 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,
|
||||
/// The cache task running in backend
|
||||
pub cache: CacheTask,
|
||||
}
|
||||
|
||||
/// Getters and the main loop.
|
||||
|
@ -101,14 +114,25 @@ impl LanguageState {
|
|||
) -> Self {
|
||||
let formatter = FormatTask::new(config.formatter());
|
||||
|
||||
let default_path = config.compile.entry_resolver.resolve_default();
|
||||
let watchers = LspPreviewState::default();
|
||||
let handle = Self::server(
|
||||
&config,
|
||||
editor_tx.clone(),
|
||||
client.clone(),
|
||||
"primary".to_string(),
|
||||
config.compile.entry_resolver.resolve(default_path),
|
||||
config.compile.determine_inputs(),
|
||||
watchers.clone(),
|
||||
);
|
||||
|
||||
Self {
|
||||
client: client.clone(),
|
||||
project: handle,
|
||||
editor_tx,
|
||||
primary: None,
|
||||
dedicates: Vec::new(),
|
||||
memory_changes: HashMap::new(),
|
||||
#[cfg(feature = "preview")]
|
||||
preview: tool::preview::PreviewState::new(client.cast(|s| &mut s.preview)),
|
||||
preview: tool::preview::PreviewState::new(watchers, client.cast(|s| &mut s.preview)),
|
||||
ever_focusing_by_activities: false,
|
||||
ever_manual_focusing: false,
|
||||
sema_tokens_registered: false,
|
||||
|
@ -119,7 +143,6 @@ impl LanguageState {
|
|||
focusing: None,
|
||||
formatter,
|
||||
user_action: Default::default(),
|
||||
cache: CacheTask::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -139,7 +162,10 @@ impl LanguageState {
|
|||
service.config.compile.notify_status,
|
||||
);
|
||||
|
||||
service.restart_primary();
|
||||
let err = service.restart_primary();
|
||||
if let Err(err) = err {
|
||||
error!("could not restart primary: {err}");
|
||||
}
|
||||
|
||||
// Run the cluster in the background after we referencing it
|
||||
client.handle.spawn(editor_actor.run());
|
||||
|
@ -158,30 +184,8 @@ impl LanguageState {
|
|||
&self.config.compile
|
||||
}
|
||||
|
||||
/// Get the entry resolver.
|
||||
pub fn entry_resolver(&self) -> &EntryResolver {
|
||||
&self.compile_config().entry_resolver
|
||||
}
|
||||
|
||||
/// Get the primary compile server for those commands without task context.
|
||||
pub fn primary(&self) -> &CompileClientActor {
|
||||
self.primary.as_ref().expect("primary")
|
||||
}
|
||||
|
||||
/// Get the task-dedicated compile server.
|
||||
pub fn dedicate(&self, group: &str) -> Option<&CompileClientActor> {
|
||||
self.dedicates
|
||||
.iter()
|
||||
.find(|dedicate| dedicate.handle.diag_group == group)
|
||||
}
|
||||
|
||||
/// Get all compile servers in current state.
|
||||
pub fn servers_mut(&mut self) -> impl Iterator<Item = &mut CompileClientActor> {
|
||||
self.primary.iter_mut().chain(self.dedicates.iter_mut())
|
||||
}
|
||||
|
||||
/// Install handlers to the language server.
|
||||
pub fn install<T: Initializer<S = Self> + AddCommands>(
|
||||
pub fn install<T: Initializer<S = Self> + AddCommands + 'static>(
|
||||
provider: LspBuilder<T>,
|
||||
) -> LspBuilder<T> {
|
||||
type State = LanguageState;
|
||||
|
@ -197,6 +201,11 @@ impl LanguageState {
|
|||
// todo: .on_sync_mut::<notifs::Cancel>(handlers::handle_cancel)?
|
||||
let mut provider = provider
|
||||
.with_request::<Shutdown>(State::shutdown)
|
||||
// customized event
|
||||
.with_event(
|
||||
&LspInterrupt::Compile(ProjectInsId::default()),
|
||||
State::compile_interrupt::<T>,
|
||||
)
|
||||
// lantency sensitive
|
||||
.with_request_::<Completion>(State::completion)
|
||||
.with_request_::<SemanticTokensFullRequest>(State::semantic_tokens_full)
|
||||
|
@ -273,17 +282,20 @@ impl LanguageState {
|
|||
provider
|
||||
}
|
||||
|
||||
/// Get all sources in current state.
|
||||
pub fn vfs_snapshot(&self) -> FileChangeSet {
|
||||
FileChangeSet::new_inserts(
|
||||
self.memory_changes
|
||||
.iter()
|
||||
.map(|(path, meta)| {
|
||||
let content = meta.content.clone().text().as_bytes().into();
|
||||
(path.clone(), FileResult::Ok(content).into())
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
fn compile_interrupt<T: Initializer<S = Self>>(
|
||||
mut state: ServiceState<T, T::S>,
|
||||
params: LspInterrupt,
|
||||
) -> anyhow::Result<()> {
|
||||
let start = std::time::Instant::now();
|
||||
log::info!("incoming interrupt: {params:?}");
|
||||
let Some(ready) = state.ready() else {
|
||||
log::info!("interrupted on not ready server");
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
ready.project.interrupt(params);
|
||||
log::info!("interrupted in {:?}", start.elapsed());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -526,10 +538,6 @@ impl LanguageState {
|
|||
}
|
||||
}
|
||||
|
||||
for e in self.primary.iter_mut().chain(self.dedicates.iter_mut()) {
|
||||
e.sync_config(self.config.compile.clone());
|
||||
}
|
||||
|
||||
if config.compile.output_path != self.config.compile.output_path
|
||||
|| config.compile.export_pdf != self.config.compile.export_pdf
|
||||
{
|
||||
|
@ -538,16 +546,15 @@ impl LanguageState {
|
|||
mode: self.config.compile.export_pdf,
|
||||
};
|
||||
|
||||
self.primary
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.change_export_config(config.clone());
|
||||
self.change_export_config(config.clone());
|
||||
}
|
||||
|
||||
if 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();
|
||||
// todo: restart dedicates
|
||||
let err = self.restart_primary();
|
||||
if let Err(err) = err {
|
||||
error!("could not restart primary: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
if config.semantic_tokens != self.config.semantic_tokens {
|
||||
|
@ -836,11 +843,24 @@ impl LanguageState {
|
|||
|
||||
impl LanguageState {
|
||||
/// Focus main file to some path.
|
||||
pub fn do_change_entry(&mut self, new_entry: Option<ImmutPath>) -> Result<bool, Error> {
|
||||
self.primary
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.change_entry(new_entry.clone())
|
||||
pub fn change_entry(&mut self, path: Option<ImmutPath>) -> Result<bool, Error> {
|
||||
if path
|
||||
.as_deref()
|
||||
.is_some_and(|p| !p.is_absolute() && !p.starts_with("/untitled"))
|
||||
{
|
||||
return Err(error_once!("entry file must be absolute", path: path.unwrap().display()));
|
||||
}
|
||||
|
||||
let next_entry = self.entry_resolver().resolve(path);
|
||||
|
||||
info!("the entry file of TypstActor(primary) is changing to {next_entry:?}");
|
||||
|
||||
self.change_task(TaskInputs {
|
||||
entry: Some(next_entry.clone()),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Pin the entry to the given path
|
||||
|
@ -849,7 +869,7 @@ impl LanguageState {
|
|||
let entry = new_entry
|
||||
.or_else(|| self.entry_resolver().resolve_default())
|
||||
.or_else(|| self.focusing.clone());
|
||||
self.do_change_entry(entry).map(|_| ())
|
||||
self.change_entry(entry).map(|_| ())
|
||||
}
|
||||
|
||||
/// Updates the primary (focusing) entry
|
||||
|
@ -859,7 +879,7 @@ impl LanguageState {
|
|||
return Ok(false);
|
||||
}
|
||||
|
||||
self.do_change_entry(new_entry.clone())
|
||||
self.change_entry(new_entry.clone())
|
||||
}
|
||||
|
||||
/// This is used for tracking activating document status if a client is not
|
||||
|
@ -905,13 +925,295 @@ impl LanguageState {
|
|||
Ok(false) => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Snapshot the compiler thread for tasks
|
||||
pub fn snapshot(&mut self) -> ZResult<WorldSnapFut> {
|
||||
self.project.snapshot()
|
||||
}
|
||||
|
||||
/// Get the entry resolver.
|
||||
pub fn entry_resolver(&self) -> &EntryResolver {
|
||||
&self.compile_config().entry_resolver
|
||||
}
|
||||
|
||||
/// Snapshot the compiler thread for language queries
|
||||
pub fn query_snapshot(&mut self) -> ZResult<QuerySnapFut> {
|
||||
self.project.query_snapshot(None)
|
||||
}
|
||||
|
||||
/// Snapshot the compiler thread for language queries
|
||||
pub fn query_snapshot_with_stat(
|
||||
&mut self,
|
||||
q: &CompilerQueryRequest,
|
||||
) -> ZResult<QuerySnapWithStat> {
|
||||
let name: &'static str = q.into();
|
||||
let path = q.associated_path();
|
||||
let stat = self.project.stats.query_stat(path, name);
|
||||
let fut = self.project.query_snapshot(Some(q))?;
|
||||
Ok(QuerySnapWithStat { fut, stat })
|
||||
}
|
||||
|
||||
fn add_memory_changes(&mut self, event: MemoryEvent) {
|
||||
self.project.add_memory_changes(event);
|
||||
}
|
||||
|
||||
fn change_task(&mut self, task_inputs: TaskInputs) {
|
||||
self.project.change_task(task_inputs);
|
||||
}
|
||||
|
||||
pub(crate) fn change_export_config(&mut self, config: ExportUserConfig) {
|
||||
self.project.export.change_config(config);
|
||||
}
|
||||
|
||||
// pub async fn settle(&mut self) {
|
||||
// let _ = self.change_entry(None);
|
||||
// info!("TypstActor({}): settle requested", self.handle.diag_group);
|
||||
// match self.handle.settle().await {
|
||||
// Ok(()) => info!("TypstActor({}): settled", self.handle.diag_group),
|
||||
// Err(err) => error!(
|
||||
// "TypstActor({}): failed to settle: {err:#}",
|
||||
// self.handle.diag_group
|
||||
// ),
|
||||
// }
|
||||
// }
|
||||
|
||||
/// Get the current server info.
|
||||
pub fn collect_server_info(&mut self) -> QueryFuture {
|
||||
let dg = self.project.diag_group.clone();
|
||||
let api_stats = self.project.stats.report();
|
||||
let query_stats = self.project.analysis.report_query_stats();
|
||||
let alloc_stats = self.project.analysis.report_alloc_stats();
|
||||
|
||||
let snap = self.snapshot()?;
|
||||
just_future(async move {
|
||||
let snap = snap.receive().await?;
|
||||
let w = &snap.world;
|
||||
|
||||
let info = ServerInfoResponse {
|
||||
root: w.entry_state().root().map(|e| e.as_ref().to_owned()),
|
||||
font_paths: w.font_resolver.font_paths().to_owned(),
|
||||
inputs: w.inputs().as_ref().deref().clone(),
|
||||
stats: HashMap::from_iter([
|
||||
("api".to_owned(), api_stats),
|
||||
("query".to_owned(), query_stats),
|
||||
("alloc".to_owned(), alloc_stats),
|
||||
]),
|
||||
};
|
||||
|
||||
let info = Some(HashMap::from_iter([(dg, info)]));
|
||||
Ok(tinymist_query::CompilerQueryResponse::ServerInfo(info))
|
||||
})
|
||||
}
|
||||
|
||||
/// Export the current document.
|
||||
pub fn on_export(&mut self, req: OnExportRequest) -> QueryFuture {
|
||||
let OnExportRequest { path, kind, open } = req;
|
||||
|
||||
let snap = self.snapshot()?;
|
||||
let entry = self.entry_resolver().resolve(Some(path.as_path().into()));
|
||||
let export = self.project.export.factory.oneshot(snap, Some(entry), kind);
|
||||
just_future(async move {
|
||||
let res = export.await?;
|
||||
|
||||
// See https://github.com/Myriad-Dreamin/tinymist/issues/837
|
||||
// Also see https://github.com/Byron/open-rs/issues/105
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let do_open = ::open::that_detached;
|
||||
#[cfg(target_os = "windows")]
|
||||
fn do_open(path: impl AsRef<std::ffi::OsStr>) -> std::io::Result<()> {
|
||||
::open::with_detached(path, "explorer")
|
||||
}
|
||||
|
||||
if let Some(Some(path)) = open.then_some(res.as_ref()) {
|
||||
log::info!("open with system default apps: {path:?}");
|
||||
if let Err(e) = do_open(path) {
|
||||
log::error!("failed to open with system default apps: {e}");
|
||||
};
|
||||
}
|
||||
|
||||
log::info!("CompileActor: on export end: {path:?} as {res:?}");
|
||||
Ok(tinymist_query::CompilerQueryResponse::OnExport(res))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl LanguageState {
|
||||
/// Restart the primary server.
|
||||
pub fn restart_primary(&mut self) -> ZResult<ProjectInsId> {
|
||||
let entry = self.entry_resolver().resolve_default();
|
||||
let config = &self.config;
|
||||
|
||||
// todo: hot replacement
|
||||
#[cfg(feature = "preview")]
|
||||
self.preview.stop_all();
|
||||
|
||||
let new_project = Self::server(
|
||||
config,
|
||||
self.editor_tx.clone(),
|
||||
self.client.clone(),
|
||||
"primary".to_string(),
|
||||
config.compile.entry_resolver.resolve(entry),
|
||||
config.compile.determine_inputs(),
|
||||
self.preview.watchers.clone(),
|
||||
);
|
||||
|
||||
let mut old_project = std::mem::replace(&mut self.project, new_project);
|
||||
|
||||
rayon::spawn(move || {
|
||||
old_project.stop();
|
||||
});
|
||||
|
||||
Ok(self.project.state.primary.id.clone())
|
||||
}
|
||||
|
||||
/// Restart the server with the given group.
|
||||
pub fn restart_dedicate(
|
||||
&mut self,
|
||||
dedicate: &str,
|
||||
entry: Option<ImmutPath>,
|
||||
) -> ZResult<ProjectInsId> {
|
||||
let entry = self.config.compile.entry_resolver.resolve(entry);
|
||||
self.project.restart_dedicate(dedicate, entry)
|
||||
}
|
||||
|
||||
/// Create a new server for the given group.
|
||||
pub fn server(
|
||||
config: &Config,
|
||||
editor_tx: tokio::sync::mpsc::UnboundedSender<EditorRequest>,
|
||||
client: TypedLspClient<LanguageState>,
|
||||
diag_group: String,
|
||||
entry: EntryState,
|
||||
inputs: ImmutDict,
|
||||
preview: project::LspPreviewState,
|
||||
) -> Project {
|
||||
let compile_config = &config.compile;
|
||||
let const_config = &config.const_config;
|
||||
|
||||
// use codespan_reporting::term::Config;
|
||||
// Run Export actors before preparing cluster to avoid loss of events
|
||||
let export_config = ExportConfig {
|
||||
group: diag_group.clone(),
|
||||
editor_tx: Some(editor_tx.clone()),
|
||||
config: ExportUserConfig {
|
||||
output: compile_config.output_path.clone(),
|
||||
mode: compile_config.export_pdf,
|
||||
},
|
||||
kind: ExportKind::Pdf {
|
||||
creation_timestamp: config.compile.determine_creation_timestamp(),
|
||||
},
|
||||
count_words: config.compile.notify_status,
|
||||
};
|
||||
let export = ExportTask::new(client.handle.clone(), export_config);
|
||||
|
||||
log::info!(
|
||||
"TypstActor: creating server for {diag_group}, entry: {entry:?}, inputs: {inputs:?}"
|
||||
);
|
||||
|
||||
// Create the compile handler for client consuming results.
|
||||
let periscope_args = compile_config.periscope_args.clone();
|
||||
let handle = Arc::new(CompileHandlerImpl {
|
||||
#[cfg(feature = "preview")]
|
||||
preview,
|
||||
diag_group: diag_group.clone(),
|
||||
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 compile_config.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<dyn PeriscopeProvider + Send + Sync>
|
||||
}),
|
||||
tokens_caches: Arc::default(),
|
||||
workers: Default::default(),
|
||||
caches: Default::default(),
|
||||
analysis_rev_cache: Arc::default(),
|
||||
stats: Arc::default(),
|
||||
}),
|
||||
|
||||
notified_revision: parking_lot::Mutex::new(0),
|
||||
});
|
||||
|
||||
let font_resolver = compile_config.determine_fonts();
|
||||
let entry_ = entry.clone();
|
||||
let compile_handle = handle.clone();
|
||||
let cert_path = compile_config.determine_certification_path();
|
||||
let package = compile_config.determine_package_opts();
|
||||
|
||||
// todo: never fail?
|
||||
let default_fonts = Arc::new(LspUniverseBuilder::only_embedded_fonts().unwrap());
|
||||
let package_registry =
|
||||
LspUniverseBuilder::resolve_package(cert_path.clone(), Some(&package));
|
||||
let verse =
|
||||
LspUniverseBuilder::build(entry_.clone(), inputs, default_fonts, package_registry)
|
||||
.expect("incorrect options");
|
||||
|
||||
// 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 server = ProjectCompiler::new(
|
||||
verse,
|
||||
dep_tx,
|
||||
CompileServerOpts {
|
||||
handler: compile_handle,
|
||||
enable_watch: true,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
let font_client = client.clone();
|
||||
client.handle.spawn_blocking(move || {
|
||||
// Create the world
|
||||
let font_resolver = font_resolver.wait().clone();
|
||||
font_client.send_event(LspInterrupt::Font(font_resolver));
|
||||
});
|
||||
|
||||
// todo: restart loses the memory changes
|
||||
// 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));
|
||||
Project {
|
||||
diag_group,
|
||||
state: server,
|
||||
preview: Default::default(),
|
||||
analysis: handle.analysis.clone(),
|
||||
stats: CompilerQueryStats::default(),
|
||||
export: handle.export.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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<String> {
|
||||
self.0.render_marked(ctx, doc, pos)
|
||||
}
|
||||
}
|
||||
|
||||
impl LanguageState {
|
||||
fn update_source(&mut self, files: FileChangeSet) -> Result<(), Error> {
|
||||
for srv in self.servers_mut() {
|
||||
srv.add_memory_changes(MemoryEvent::Update(files.clone()));
|
||||
}
|
||||
self.add_memory_changes(MemoryEvent::Update(files.clone()));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -920,6 +1222,7 @@ impl LanguageState {
|
|||
pub fn create_source(&mut self, path: PathBuf, content: String) -> Result<(), Error> {
|
||||
let path: ImmutPath = path.into();
|
||||
|
||||
log::info!("create source: {path:?}");
|
||||
self.memory_changes.insert(
|
||||
path.clone(),
|
||||
MemoryFileMeta {
|
||||
|
@ -928,7 +1231,6 @@ impl LanguageState {
|
|||
);
|
||||
|
||||
let content: Bytes = content.as_bytes().into();
|
||||
log::info!("create source: {:?}", path);
|
||||
|
||||
// todo: is it safe to believe that the path is normalized?
|
||||
let files = FileChangeSet::new_inserts(vec![(path, FileResult::Ok(content).into())]);
|
||||
|
@ -1014,7 +1316,6 @@ impl LanguageState {
|
|||
pub fn query(&mut self, query: CompilerQueryRequest) -> QueryFuture {
|
||||
use CompilerQueryRequest::*;
|
||||
|
||||
let primary = || self.primary();
|
||||
let is_pinning = self.pinning;
|
||||
just_ok(match query {
|
||||
FoldingRange(req) => query_source!(self, FoldingRange, req)?,
|
||||
|
@ -1022,27 +1323,24 @@ impl LanguageState {
|
|||
DocumentSymbol(req) => query_source!(self, DocumentSymbol, req)?,
|
||||
OnEnter(req) => query_source!(self, OnEnter, req)?,
|
||||
ColorPresentation(req) => CompilerQueryResponse::ColorPresentation(req.request()),
|
||||
OnExport(req) => return primary().on_export(req),
|
||||
ServerInfo(_) => return primary().collect_server_info(),
|
||||
_ => return Self::query_on(primary(), is_pinning, query),
|
||||
OnExport(req) => return self.on_export(req),
|
||||
ServerInfo(_) => return self.collect_server_info(),
|
||||
// todo: query on dedicate projects
|
||||
_ => return self.query_on(is_pinning, query),
|
||||
})
|
||||
}
|
||||
|
||||
fn query_on(
|
||||
client: &CompileClientActor,
|
||||
is_pinning: bool,
|
||||
query: CompilerQueryRequest,
|
||||
) -> QueryFuture {
|
||||
fn query_on(&mut self, is_pinning: bool, query: CompilerQueryRequest) -> QueryFuture {
|
||||
use CompilerQueryRequest::*;
|
||||
type R = CompilerQueryResponse;
|
||||
assert!(query.fold_feature() != FoldRequestFeature::ContextFreeUnique);
|
||||
|
||||
let fut_stat = client.query_snapshot_with_stat(&query)?;
|
||||
let fut_stat = self.query_snapshot_with_stat(&query)?;
|
||||
let entry = query
|
||||
.associated_path()
|
||||
.map(|path| client.entry_resolver().resolve(Some(path.into())))
|
||||
.map(|path| self.entry_resolver().resolve(Some(path.into())))
|
||||
.or_else(|| {
|
||||
let root = client.entry_resolver().root(None)?;
|
||||
let root = self.entry_resolver().root(None)?;
|
||||
Some(EntryState::new_rooted_by_id(root, *DETACHED_ENTRY))
|
||||
});
|
||||
|
||||
|
|
|
@ -69,7 +69,7 @@ pub struct CompilerQueryStats {
|
|||
|
||||
impl CompilerQueryStats {
|
||||
/// Record a query.
|
||||
pub fn query_stat(&self, path: Option<&Path>, name: &'static str) -> QueryStatGuard {
|
||||
pub(crate) fn query_stat(&self, path: Option<&Path>, name: &'static str) -> QueryStatGuard {
|
||||
let stats = &self.query_stats;
|
||||
// let refs = stats.entry(path.clone()).or_default();
|
||||
let refs = stats
|
||||
|
|
|
@ -1,64 +0,0 @@
|
|||
//! The actor that handles cache evicting.
|
||||
|
||||
use std::{
|
||||
num::NonZeroUsize,
|
||||
sync::{atomic::AtomicUsize, Arc},
|
||||
};
|
||||
|
||||
use crate::world::vfs::SourceCache;
|
||||
|
||||
use super::{FutureFolder, SyncTaskFactory};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CacheUserConfig {
|
||||
pub max_age: usize,
|
||||
pub vfs_age: usize,
|
||||
}
|
||||
|
||||
impl Default for CacheUserConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
max_age: 30,
|
||||
vfs_age: 15,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct CacheTask {
|
||||
factory: SyncTaskFactory<CacheUserConfig>,
|
||||
cache_evict_folder: FutureFolder,
|
||||
revision: Arc<AtomicUsize>,
|
||||
}
|
||||
|
||||
impl CacheTask {
|
||||
pub fn new(c: CacheUserConfig) -> Self {
|
||||
Self {
|
||||
factory: SyncTaskFactory::new(c),
|
||||
cache_evict_folder: FutureFolder::default(),
|
||||
revision: Arc::new(AtomicUsize::default()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn evict(&self, rev: NonZeroUsize, source_cache: SourceCache) {
|
||||
let revision = self
|
||||
.revision
|
||||
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
let task = self.factory.task();
|
||||
self.cache_evict_folder.spawn(revision, || {
|
||||
Box::pin(async move {
|
||||
let _ = FutureFolder::compute(move |_| {
|
||||
// Evict compilation cache.
|
||||
let evict_start = std::time::Instant::now();
|
||||
comemo::evict(task.max_age);
|
||||
source_cache.evict(rev, task.vfs_age);
|
||||
let elapsed = evict_start.elapsed();
|
||||
log::info!("CacheEvictTask: evict cache in {elapsed:?}");
|
||||
})
|
||||
.await;
|
||||
|
||||
Some(())
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
|
@ -3,10 +3,10 @@
|
|||
use std::str::FromStr;
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
|
||||
use crate::world::TaskInputs;
|
||||
use crate::world::{EntryReader, EntryState};
|
||||
use crate::project::{CompiledArtifact, ExportSignal};
|
||||
use anyhow::{bail, Context};
|
||||
use reflexo_typst::TypstDatetime;
|
||||
use tinymist_project::{EntryReader, EntryState, TaskInputs};
|
||||
use tinymist_query::{ExportKind, PageSelection};
|
||||
use tokio::sync::mpsc;
|
||||
use typlite::Typlite;
|
||||
|
@ -20,13 +20,7 @@ use typst_pdf::PdfOptions;
|
|||
|
||||
use crate::tool::text::FullTextDigest;
|
||||
use crate::{
|
||||
actor::{
|
||||
editor::EditorRequest,
|
||||
typ_client::WorldSnapFut,
|
||||
typ_server::{CompiledArtifact, ExportSignal},
|
||||
},
|
||||
tool::word_count,
|
||||
world::LspCompilerFeat,
|
||||
actor::editor::EditorRequest, project::WorldSnapFut, tool::word_count, world::LspCompilerFeat,
|
||||
ExportMode, PathPattern,
|
||||
};
|
||||
|
||||
|
@ -41,18 +35,21 @@ pub struct ExportUserConfig {
|
|||
pub mode: ExportMode,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
#[derive(Clone)]
|
||||
pub struct ExportTask {
|
||||
pub handle: tokio::runtime::Handle,
|
||||
pub factory: SyncTaskFactory<ExportConfig>,
|
||||
export_folder: FutureFolder,
|
||||
count_word_folder: FutureFolder,
|
||||
}
|
||||
|
||||
impl ExportTask {
|
||||
pub fn new(data: ExportConfig) -> Self {
|
||||
pub fn new(handle: tokio::runtime::Handle, data: ExportConfig) -> Self {
|
||||
Self {
|
||||
handle,
|
||||
factory: SyncTaskFactory::new(data),
|
||||
..ExportTask::default()
|
||||
export_folder: FutureFolder::default(),
|
||||
count_word_folder: FutureFolder::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -81,7 +78,7 @@ impl SyncTaskFactory<ExportConfig> {
|
|||
..Default::default()
|
||||
});
|
||||
|
||||
let artifact = snap.compile().await;
|
||||
let artifact = snap.compile();
|
||||
export.do_export(&kind, artifact).await
|
||||
}
|
||||
}
|
||||
|
@ -128,29 +125,33 @@ impl ExportConfig {
|
|||
return None;
|
||||
}
|
||||
|
||||
t.export_folder.spawn(artifact.world.revision().get(), || {
|
||||
let fut = t.export_folder.spawn(artifact.world.revision().get(), || {
|
||||
let this = self.clone();
|
||||
let artifact = artifact.clone();
|
||||
Box::pin(async move {
|
||||
log_err(this.do_export(&this.kind, artifact).await);
|
||||
Some(())
|
||||
})
|
||||
});
|
||||
})?;
|
||||
|
||||
t.handle.spawn(fut);
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn signal_count_word(&self, artifact: &CompiledArtifact<LspCompilerFeat>, t: &ExportTask) {
|
||||
fn signal_count_word(
|
||||
&self,
|
||||
artifact: &CompiledArtifact<LspCompilerFeat>,
|
||||
t: &ExportTask,
|
||||
) -> Option<()> {
|
||||
if !self.count_words {
|
||||
return;
|
||||
return None;
|
||||
}
|
||||
|
||||
let Some(editor_tx) = self.editor_tx.clone() else {
|
||||
return;
|
||||
};
|
||||
let editor_tx = self.editor_tx.clone()?;
|
||||
let revision = artifact.world.revision().get();
|
||||
|
||||
t.count_word_folder.spawn(revision, || {
|
||||
let fut = t.count_word_folder.spawn(revision, || {
|
||||
let artifact = artifact.clone();
|
||||
let group = self.group.clone();
|
||||
Box::pin(async move {
|
||||
|
@ -165,7 +166,11 @@ impl ExportConfig {
|
|||
|
||||
Some(())
|
||||
})
|
||||
});
|
||||
})?;
|
||||
|
||||
t.handle.spawn(fut);
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
async fn do_export(
|
||||
|
|
|
@ -8,8 +8,6 @@ mod format;
|
|||
pub use format::*;
|
||||
mod user_action;
|
||||
pub use user_action::*;
|
||||
mod cache;
|
||||
pub use cache::*;
|
||||
|
||||
use std::{ops::DerefMut, pin::Pin, sync::Arc};
|
||||
|
||||
|
@ -64,7 +62,12 @@ impl FutureFolder {
|
|||
.map_err(|e| anyhow::anyhow!("compute error: {e:?}"))
|
||||
}
|
||||
|
||||
fn spawn(&self, revision: usize, fut: impl FnOnce() -> FoldFuture) {
|
||||
#[must_use]
|
||||
fn spawn(
|
||||
&self,
|
||||
revision: usize,
|
||||
fut: impl FnOnce() -> FoldFuture,
|
||||
) -> Option<impl Future<Output = ()> + Send + 'static> {
|
||||
let mut state = self.state.lock();
|
||||
let state = state.deref_mut();
|
||||
|
||||
|
@ -75,7 +78,7 @@ impl FutureFolder {
|
|||
*prev_revision = revision;
|
||||
}
|
||||
|
||||
return;
|
||||
return None;
|
||||
}
|
||||
next_update => {
|
||||
*next_update = Some((revision, fut()));
|
||||
|
@ -83,13 +86,13 @@ impl FutureFolder {
|
|||
}
|
||||
|
||||
if state.running {
|
||||
return;
|
||||
return None;
|
||||
}
|
||||
|
||||
state.running = true;
|
||||
|
||||
let state = self.state.clone();
|
||||
tokio::spawn(async move {
|
||||
Some(async move {
|
||||
loop {
|
||||
let fut = {
|
||||
let mut state = state.lock();
|
||||
|
@ -101,6 +104,6 @@ impl FutureFolder {
|
|||
};
|
||||
fut.await;
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -178,7 +178,7 @@ async fn trace_main(
|
|||
|
||||
let timings = writer.into_inner().unwrap();
|
||||
|
||||
let handle = &state.primary().handle;
|
||||
let handle = &state.project;
|
||||
let diagnostics =
|
||||
tinymist_query::convert_diagnostics(w, diags.iter(), handle.analysis.position_encoding);
|
||||
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
//! Document preview tool for Typst
|
||||
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use std::num::NonZeroUsize;
|
||||
use std::ops::DerefMut;
|
||||
use std::{collections::HashMap, net::SocketAddr, path::Path, sync::Arc};
|
||||
|
||||
use crate::world::vfs::{notify::MemoryEvent, FileChangeSet};
|
||||
use futures::{SinkExt, StreamExt, TryStreamExt};
|
||||
use hyper::service::service_fn;
|
||||
use hyper_tungstenite::{tungstenite::Message, HyperWebsocket, HyperWebsocketStream};
|
||||
|
@ -16,6 +18,7 @@ use serde::Serialize;
|
|||
use serde_json::Value as JsonValue;
|
||||
use sync_lsp::just_ok;
|
||||
use tinymist_assets::TYPST_PREVIEW_HTML;
|
||||
use tinymist_project::ProjectInsId;
|
||||
use tokio::sync::{mpsc, oneshot};
|
||||
use typst::layout::{Frame, FrameItem, Point, Position};
|
||||
use typst::syntax::{LinkedNode, Source, Span, SyntaxKind};
|
||||
|
@ -28,11 +31,15 @@ use typst_preview::{
|
|||
};
|
||||
use typst_shim::syntax::LinkedNodeExt;
|
||||
|
||||
use crate::project::{
|
||||
CompileHandlerImpl, CompileServerOpts, CompiledArtifact, LspInterrupt, ProjectClient,
|
||||
ProjectCompiler,
|
||||
};
|
||||
use crate::world::LspCompilerFeat;
|
||||
use crate::*;
|
||||
use actor::preview::{PreviewActor, PreviewRequest, PreviewTab};
|
||||
use actor::typ_client::CompileHandler;
|
||||
use actor::typ_server::{CompileServerActor, CompileServerOpts, CompiledArtifact};
|
||||
use project::world::vfs::{notify::MemoryEvent, FileChangeSet};
|
||||
use project::{watch_deps, LspPreviewState};
|
||||
|
||||
/// The preview's view of the compiled artifact.
|
||||
pub struct PreviewCompileView {
|
||||
|
@ -63,6 +70,7 @@ impl typst_preview::CompileView for PreviewCompileView {
|
|||
let Location::Src(loc) = loc;
|
||||
|
||||
let source_id = world.id_for_path(Path::new(&loc.filepath))?;
|
||||
|
||||
let source = world.source(source_id).ok()?;
|
||||
let cursor = source.line_column_to_byte(loc.pos.line, loc.pos.column)?;
|
||||
|
||||
|
@ -88,10 +96,10 @@ impl typst_preview::CompileView for PreviewCompileView {
|
|||
let Some(doc) = doc.as_deref() else {
|
||||
return vec![];
|
||||
};
|
||||
|
||||
let Some(source_id) = world.id_for_path(Path::new(&src_loc.filepath)) else {
|
||||
return vec![];
|
||||
};
|
||||
|
||||
let Some(source) = world.source(source_id).ok() else {
|
||||
return vec![];
|
||||
};
|
||||
|
@ -123,42 +131,6 @@ impl typst_preview::CompileView for PreviewCompileView {
|
|||
}
|
||||
}
|
||||
|
||||
impl EditorServer for CompileHandler {
|
||||
async fn update_memory_files(
|
||||
&self,
|
||||
files: MemoryFiles,
|
||||
reset_shadow: bool,
|
||||
) -> Result<(), Error> {
|
||||
// todo: is it safe to believe that the path is normalized?
|
||||
let files = FileChangeSet::new_inserts(
|
||||
files
|
||||
.files
|
||||
.into_iter()
|
||||
.map(|(path, content)| {
|
||||
let content = content.as_bytes().into();
|
||||
// todo: cloning PathBuf -> Arc<Path>
|
||||
(path.into(), Ok(content).into())
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
self.add_memory_changes(if reset_shadow {
|
||||
MemoryEvent::Sync(files)
|
||||
} else {
|
||||
MemoryEvent::Update(files)
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_shadow_files(&self, files: MemoryFilesShort) -> Result<(), Error> {
|
||||
// todo: is it safe to believe that the path is normalized?
|
||||
let files = FileChangeSet::new_removes(files.files.into_iter().map(From::from).collect());
|
||||
self.add_memory_changes(MemoryEvent::Update(files));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// CLI Arguments for the preview tool.
|
||||
#[derive(Debug, Clone, clap::Parser)]
|
||||
pub struct PreviewCliArgs {
|
||||
|
@ -218,11 +190,13 @@ pub struct PreviewState {
|
|||
client: TypedLspClient<PreviewState>,
|
||||
/// The backend running actor.
|
||||
preview_tx: mpsc::UnboundedSender<PreviewRequest>,
|
||||
/// the watchers for the preview
|
||||
pub(crate) watchers: LspPreviewState,
|
||||
}
|
||||
|
||||
impl PreviewState {
|
||||
/// Create a new preview state.
|
||||
pub fn new(client: TypedLspClient<PreviewState>) -> Self {
|
||||
pub fn new(watchers: LspPreviewState, client: TypedLspClient<PreviewState>) -> Self {
|
||||
let (preview_tx, preview_rx) = mpsc::unbounded_channel();
|
||||
|
||||
client.handle.spawn(
|
||||
|
@ -230,11 +204,29 @@ impl PreviewState {
|
|||
client: client.clone().to_untyped(),
|
||||
tabs: HashMap::default(),
|
||||
preview_rx,
|
||||
watchers: watchers.clone(),
|
||||
}
|
||||
.run(),
|
||||
);
|
||||
|
||||
Self { client, preview_tx }
|
||||
Self {
|
||||
client,
|
||||
preview_tx,
|
||||
watchers,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn stop_all(&mut self) {
|
||||
let watchers = std::mem::take(self.watchers.inner.lock().deref_mut());
|
||||
for (_, watcher) in watchers {
|
||||
let res = self.preview_tx.send(PreviewRequest::Kill(
|
||||
watcher.task_id().to_owned(),
|
||||
oneshot::channel().0,
|
||||
));
|
||||
if let Err(e) = res {
|
||||
log::error!("failed to send kill request({:?}): {e}", watcher.task_id());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -248,15 +240,78 @@ pub struct StartPreviewResponse {
|
|||
is_primary: bool,
|
||||
}
|
||||
|
||||
pub struct PreviewProjectHandler {
|
||||
pub project_id: ProjectInsId,
|
||||
client: Box<dyn ProjectClient>,
|
||||
}
|
||||
|
||||
impl PreviewProjectHandler {
|
||||
pub fn flush_compile(&self) {
|
||||
let _ = self.project_id;
|
||||
self.client
|
||||
.send_event(LspInterrupt::Compile(self.project_id.clone()));
|
||||
}
|
||||
|
||||
pub async fn settle(&self) -> Result<(), Error> {
|
||||
self.client
|
||||
.send_event(LspInterrupt::Settle(self.project_id.clone()));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl EditorServer for PreviewProjectHandler {
|
||||
async fn update_memory_files(
|
||||
&self,
|
||||
files: MemoryFiles,
|
||||
reset_shadow: bool,
|
||||
) -> Result<(), Error> {
|
||||
// todo: is it safe to believe that the path is normalized?
|
||||
let files = FileChangeSet::new_inserts(
|
||||
files
|
||||
.files
|
||||
.into_iter()
|
||||
.map(|(path, content)| {
|
||||
// todo: cloning PathBuf -> Arc<Path>
|
||||
(path.into(), Ok(content.as_bytes().into()).into())
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
|
||||
let intr = LspInterrupt::Memory(if reset_shadow {
|
||||
MemoryEvent::Sync(files)
|
||||
} else {
|
||||
MemoryEvent::Update(files)
|
||||
});
|
||||
self.client.send_event(intr);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_shadow_files(&self, files: MemoryFilesShort) -> Result<(), Error> {
|
||||
// todo: is it safe to believe that the path is normalized?
|
||||
let files = FileChangeSet::new_removes(files.files.into_iter().map(From::from).collect());
|
||||
self.client
|
||||
.send_event(LspInterrupt::Memory(MemoryEvent::Update(files)));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl PreviewState {
|
||||
/// Start a preview on a given compiler.
|
||||
pub fn start(
|
||||
&self,
|
||||
args: PreviewCliArgs,
|
||||
previewer: PreviewBuilder,
|
||||
compile_handler: Arc<CompileHandler>,
|
||||
// compile_handler: Arc<CompileHandler>,
|
||||
project_id: ProjectInsId,
|
||||
is_primary: bool,
|
||||
) -> SchedulableResponse<StartPreviewResponse> {
|
||||
let compile_handler = Arc::new(PreviewProjectHandler {
|
||||
project_id,
|
||||
client: Box::new(self.client.clone().to_untyped()),
|
||||
});
|
||||
|
||||
let task_id = args.preview.task_id.clone();
|
||||
log::info!("PreviewTask({task_id}): arguments: {args:#?}");
|
||||
|
||||
|
@ -502,6 +557,7 @@ pub async fn make_http_server(
|
|||
/// Entry point of the preview tool.
|
||||
pub async fn preview_main(args: PreviewCliArgs) -> anyhow::Result<()> {
|
||||
log::info!("Arguments: {args:#?}");
|
||||
let handle = tokio::runtime::Handle::current();
|
||||
|
||||
let static_file_host =
|
||||
if args.static_file_host == args.data_plane_host || !args.static_file_host.is_empty() {
|
||||
|
@ -517,37 +573,62 @@ pub async fn preview_main(args: PreviewCliArgs) -> anyhow::Result<()> {
|
|||
});
|
||||
|
||||
let verse = args.compile.resolve()?;
|
||||
let previewer = PreviewBuilder::new(args.preview);
|
||||
|
||||
let (service, handle) = {
|
||||
// type EditorSender = mpsc::UnboundedSender<EditorRequest>;
|
||||
let (editor_tx, mut editor_rx) = mpsc::unbounded_channel();
|
||||
let (intr_tx, intr_rx) = mpsc::unbounded_channel();
|
||||
let (intr_tx, intr_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
|
||||
let handle = Arc::new(CompileHandler {
|
||||
inner: Default::default(),
|
||||
diag_group: "main".to_owned(),
|
||||
intr_tx: intr_tx.clone(),
|
||||
// export_tx,
|
||||
export: Default::default(),
|
||||
editor_tx,
|
||||
analysis: Arc::default(),
|
||||
stats: Default::default(),
|
||||
notified_revision: parking_lot::Mutex::new(0),
|
||||
});
|
||||
// todo: unify filesystem watcher
|
||||
let (dep_tx, dep_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
let fs_intr_tx = intr_tx.clone();
|
||||
tokio::spawn(watch_deps(dep_rx, move |event| {
|
||||
fs_intr_tx.send_event(LspInterrupt::Fs(event));
|
||||
}));
|
||||
|
||||
// Consume editor_rx
|
||||
tokio::spawn(async move { while editor_rx.recv().await.is_some() {} });
|
||||
|
||||
let service = CompileServerActor::new_with(
|
||||
let preview_state = LspPreviewState::default();
|
||||
|
||||
// Create the actor
|
||||
let compile_handle = Arc::new(CompileHandlerImpl {
|
||||
preview: preview_state.clone(),
|
||||
diag_group: "main".to_owned(),
|
||||
export: crate::task::ExportTask::new(handle, Default::default()),
|
||||
editor_tx,
|
||||
client: Box::new(intr_tx.clone()),
|
||||
analysis: Arc::default(),
|
||||
|
||||
notified_revision: parking_lot::Mutex::new(0),
|
||||
});
|
||||
|
||||
let mut server = ProjectCompiler::new(
|
||||
verse,
|
||||
intr_tx,
|
||||
intr_rx,
|
||||
dep_tx,
|
||||
CompileServerOpts {
|
||||
compile_handle: handle.clone(),
|
||||
handler: compile_handle,
|
||||
enable_watch: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.with_watch(true);
|
||||
);
|
||||
let registered = preview_state.register(&server.primary.id, previewer.compile_watcher());
|
||||
if !registered {
|
||||
anyhow::bail!("failed to register preview");
|
||||
}
|
||||
|
||||
let handle = Arc::new(PreviewProjectHandler {
|
||||
project_id: server.primary.id.clone(),
|
||||
client: Box::new(intr_tx),
|
||||
});
|
||||
|
||||
let service = async move {
|
||||
let mut intr_rx = intr_rx;
|
||||
while let Some(intr) = intr_rx.recv().await {
|
||||
server.process(intr);
|
||||
}
|
||||
};
|
||||
|
||||
(service, handle)
|
||||
};
|
||||
|
@ -614,12 +695,9 @@ pub async fn preview_main(args: PreviewCliArgs) -> anyhow::Result<()> {
|
|||
let _ = srv.join.await;
|
||||
});
|
||||
|
||||
let previewer = PreviewBuilder::new(args.preview);
|
||||
let registered = handle.register_preview(previewer.compile_watcher());
|
||||
assert!(registered, "failed to register preview");
|
||||
let (websocket_tx, websocket_rx) = mpsc::unbounded_channel();
|
||||
let mut previewer = previewer.build(lsp_tx, handle.clone()).await;
|
||||
tokio::spawn(service.run());
|
||||
tokio::spawn(service);
|
||||
|
||||
bind_streams(&mut previewer, websocket_rx);
|
||||
|
||||
|
|
|
@ -47,7 +47,7 @@ macro_rules! get_arg_or_default {
|
|||
}
|
||||
pub(crate) use get_arg_or_default;
|
||||
|
||||
pub fn z_internal_error(msg: reflexo::Error) -> ResponseError {
|
||||
pub fn z_internal_error(msg: tinymist_std::Error) -> ResponseError {
|
||||
ResponseError {
|
||||
code: ErrorCode::InternalError as i32,
|
||||
message: format!("internal: {msg:?}"),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue