mirror of
https://github.com/Myriad-Dreamin/tinymist.git
synced 2025-08-04 10:18:16 +00:00
feat: add capability to pin main
This commit is contained in:
parent
f6d473f320
commit
85cc29f884
4 changed files with 214 additions and 70 deletions
|
@ -3,11 +3,11 @@ use std::{borrow::Cow, path::PathBuf, sync::Arc};
|
|||
use parking_lot::Mutex;
|
||||
use tinymist_query::DiagnosticsMap;
|
||||
use tokio::sync::{broadcast, mpsc, watch};
|
||||
use typst_ts_core::{config::CompileOpts, TypstDocument};
|
||||
use typst_ts_core::config::CompileOpts;
|
||||
|
||||
use self::{
|
||||
render::{PdfExportActor, RenderActorRequest},
|
||||
typst::{create_server, CompileCluster, CompileDriver},
|
||||
render::PdfExportActor,
|
||||
typst::{create_server, CompileCluster, CompileDriver, CompileHandler, CompileNode},
|
||||
};
|
||||
use crate::{ConstConfig, LspHost};
|
||||
|
||||
|
@ -19,39 +19,15 @@ struct Repr {
|
|||
|
||||
diag_tx: mpsc::UnboundedSender<(String, DiagnosticsMap)>,
|
||||
diag_rx: Option<mpsc::UnboundedReceiver<(String, DiagnosticsMap)>>,
|
||||
doc_tx: Option<watch::Sender<Option<Arc<TypstDocument>>>>,
|
||||
doc_rx: watch::Receiver<Option<Arc<TypstDocument>>>,
|
||||
render_tx: broadcast::Sender<RenderActorRequest>,
|
||||
}
|
||||
|
||||
pub struct ActorFactory(Arc<Mutex<Repr>>);
|
||||
|
||||
impl ActorFactory {
|
||||
pub fn new(config: ConstConfig) -> Self {
|
||||
let (diag_tx, diag_rx) = mpsc::unbounded_channel();
|
||||
let (doc_sender, doc_rx) = watch::channel(None);
|
||||
impl Repr {
|
||||
fn server(&mut self, name: String, roots: Vec<PathBuf>) -> CompileNode<CompileHandler> {
|
||||
let (doc_tx, doc_rx) = watch::channel(None);
|
||||
let (render_tx, _) = broadcast::channel(10);
|
||||
|
||||
Self(Arc::new(Mutex::new(Repr {
|
||||
config,
|
||||
diag_tx,
|
||||
diag_rx: Some(diag_rx),
|
||||
doc_tx: Some(doc_sender),
|
||||
doc_rx,
|
||||
render_tx,
|
||||
})))
|
||||
}
|
||||
|
||||
pub async fn pdf_export_actor(&self) {
|
||||
let this = self.0.lock();
|
||||
tokio::spawn(PdfExportActor::new(this.doc_rx.clone(), this.render_tx.subscribe()).run());
|
||||
}
|
||||
|
||||
pub fn prepare_cluster(&self, host: LspHost, roots: Vec<PathBuf>) -> CompileCluster {
|
||||
let mut this = self.0.lock();
|
||||
|
||||
let doc_tx = this.doc_tx.take().expect("doc_sender is poisoned");
|
||||
let diag_rx = this.diag_rx.take().expect("diag_rx is poisoned");
|
||||
// Run the PDF export actor before preparing cluster to avoid loss of events
|
||||
tokio::spawn(PdfExportActor::new(doc_rx.clone(), render_tx.subscribe()).run());
|
||||
|
||||
let opts = CompileOpts {
|
||||
root_dir: roots.first().cloned().unwrap_or_default(),
|
||||
|
@ -60,15 +36,48 @@ impl ActorFactory {
|
|||
with_embedded_fonts: typst_assets::fonts().map(Cow::Borrowed).collect(),
|
||||
..CompileOpts::default()
|
||||
};
|
||||
let primary = create_server(
|
||||
"primary".to_owned(),
|
||||
&this.config,
|
||||
create_server(
|
||||
name,
|
||||
&self.config,
|
||||
CompileDriver::new(roots.clone(), opts),
|
||||
this.diag_tx.clone(),
|
||||
self.diag_tx.clone(),
|
||||
doc_tx,
|
||||
this.render_tx.clone(),
|
||||
);
|
||||
render_tx,
|
||||
)
|
||||
}
|
||||
|
||||
CompileCluster::new(host, &this.config, primary, diag_rx)
|
||||
pub fn prepare_cluster(
|
||||
&mut self,
|
||||
fac: ActorFactory,
|
||||
host: LspHost,
|
||||
roots: Vec<PathBuf>,
|
||||
) -> CompileCluster {
|
||||
let diag_rx = self.diag_rx.take().expect("diag_rx is poisoned");
|
||||
|
||||
let primary = self.server("primary".to_owned(), roots.clone());
|
||||
CompileCluster::new(fac, host, roots, &self.config, primary, diag_rx)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ActorFactory(Arc<Mutex<Repr>>);
|
||||
|
||||
impl ActorFactory {
|
||||
pub fn new(config: ConstConfig) -> Self {
|
||||
let (diag_tx, diag_rx) = mpsc::unbounded_channel();
|
||||
|
||||
Self(Arc::new(Mutex::new(Repr {
|
||||
config,
|
||||
diag_tx,
|
||||
diag_rx: Some(diag_rx),
|
||||
})))
|
||||
}
|
||||
|
||||
fn server(&self, name: String, roots: Vec<PathBuf>) -> CompileNode<CompileHandler> {
|
||||
self.0.lock().server(name, roots)
|
||||
}
|
||||
|
||||
pub fn prepare_cluster(&self, host: LspHost, roots: Vec<PathBuf>) -> CompileCluster {
|
||||
self.0.lock().prepare_cluster(self.clone(), host, roots)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,30 +39,41 @@ use crate::actor::render::RenderActorRequest;
|
|||
use crate::ConstConfig;
|
||||
use crate::LspHost;
|
||||
|
||||
use super::ActorFactory;
|
||||
|
||||
type CompileService<H> = CompileActor<Reporter<CompileExporter<CompileDriver>, H>>;
|
||||
type CompileClient<H> = TsCompileClient<CompileService<H>>;
|
||||
type Node = CompileNode<CompileHandler>;
|
||||
|
||||
type DiagnosticsSender = mpsc::UnboundedSender<(String, DiagnosticsMap)>;
|
||||
|
||||
pub struct CompileCluster {
|
||||
roots: Vec<PathBuf>,
|
||||
actor_factory: ActorFactory,
|
||||
position_encoding: PositionEncoding,
|
||||
memory_changes: RwLock<HashMap<Arc<Path>, MemoryFileMeta>>,
|
||||
primary: CompileNode<CompileHandler>,
|
||||
primary: Node,
|
||||
main: Mutex<Option<Node>>,
|
||||
pub tokens_cache: SemanticTokenCache,
|
||||
actor: Option<CompileClusterActor>,
|
||||
}
|
||||
|
||||
impl CompileCluster {
|
||||
pub fn new(
|
||||
actor_factory: ActorFactory,
|
||||
host: LspHost,
|
||||
roots: Vec<PathBuf>,
|
||||
cfg: &ConstConfig,
|
||||
primary: CompileNode<CompileHandler>,
|
||||
primary: Node,
|
||||
diag_rx: mpsc::UnboundedReceiver<(String, DiagnosticsMap)>,
|
||||
) -> Self {
|
||||
Self {
|
||||
roots,
|
||||
actor_factory,
|
||||
position_encoding: cfg.position_encoding,
|
||||
memory_changes: RwLock::new(HashMap::new()),
|
||||
primary,
|
||||
main: Mutex::new(None),
|
||||
tokens_cache: Default::default(),
|
||||
actor: Some(CompileClusterActor {
|
||||
host,
|
||||
|
@ -77,6 +88,45 @@ impl CompileCluster {
|
|||
let actor = self.actor.take().expect("actor is poisoned");
|
||||
(self, actor)
|
||||
}
|
||||
|
||||
pub async fn pin_main(&self, new_entry: Option<Url>) -> Result<(), Error> {
|
||||
let mut m = self.main.lock().await;
|
||||
match (new_entry, m.is_some()) {
|
||||
(Some(new_entry), true) => {
|
||||
let path = new_entry
|
||||
.to_file_path()
|
||||
.map_err(|_| error_once!("invalid url"))?;
|
||||
let path = path.as_path().into();
|
||||
|
||||
m.as_mut().unwrap().change_entry(path).await
|
||||
}
|
||||
(Some(new_entry), false) => {
|
||||
let path = new_entry
|
||||
.to_file_path()
|
||||
.map_err(|_| error_once!("invalid url"))?;
|
||||
let path = path.as_path().into();
|
||||
|
||||
let main_node = self
|
||||
.actor_factory
|
||||
.server("main".to_owned(), self.roots.clone());
|
||||
main_node.change_entry(path).await?;
|
||||
|
||||
// todo: disable primary watch
|
||||
|
||||
*m = Some(main_node);
|
||||
Ok(())
|
||||
}
|
||||
(None, true) => {
|
||||
// todo: unpin main
|
||||
warn!("unpin main is not implemented yet");
|
||||
|
||||
// todo: enable primary watch
|
||||
|
||||
Ok(())
|
||||
}
|
||||
(None, false) => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_server(
|
||||
|
@ -86,7 +136,7 @@ pub fn create_server(
|
|||
diag_tx: DiagnosticsSender,
|
||||
doc_sender: watch::Sender<Option<Arc<TypstDocument>>>,
|
||||
render_tx: broadcast::Sender<RenderActorRequest>,
|
||||
) -> CompileNode<CompileHandler> {
|
||||
) -> Node {
|
||||
let root = compiler_driver.inner.world.root.as_ref().to_owned();
|
||||
let handler: CompileHandler = compiler_driver.handler.clone();
|
||||
|
||||
|
@ -128,6 +178,7 @@ impl CompileClusterActor {
|
|||
loop {
|
||||
tokio::select! {
|
||||
Some((group, diagnostics)) = self.diag_rx.recv() => {
|
||||
debug!("received diagnostics from {}: diag({:#?})", group, diagnostics.len());
|
||||
self.publish(group, diagnostics).await;
|
||||
}
|
||||
}
|
||||
|
@ -191,6 +242,20 @@ struct MemoryFileMeta {
|
|||
}
|
||||
|
||||
impl CompileCluster {
|
||||
async fn update_source(&self, files: FileChangeSet) -> Result<(), Error> {
|
||||
let primary = Some(&self.primary);
|
||||
let main = self.main.lock().await;
|
||||
let main = main.as_ref();
|
||||
let clients_to_notify = (primary.iter()).chain(main.iter());
|
||||
|
||||
for client in clients_to_notify {
|
||||
let iw = client.inner.lock().await;
|
||||
iw.add_memory_changes(MemoryEvent::Update(files.clone()));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn create_source(&self, path: PathBuf, content: String) -> Result<(), Error> {
|
||||
let now = Time::now();
|
||||
let path: ImmutPath = path.into();
|
||||
|
@ -207,10 +272,8 @@ impl CompileCluster {
|
|||
|
||||
// todo: is it safe to believe that the path is normalized?
|
||||
let files = FileChangeSet::new_inserts(vec![(path, FileResult::Ok((now, content)).into())]);
|
||||
let iw = self.primary.inner.lock().await;
|
||||
iw.add_memory_changes(MemoryEvent::Update(files));
|
||||
|
||||
Ok(())
|
||||
self.update_source(files).await
|
||||
}
|
||||
|
||||
pub async fn remove_source(&self, path: PathBuf) -> Result<(), Error> {
|
||||
|
@ -220,11 +283,8 @@ impl CompileCluster {
|
|||
|
||||
// todo: is it safe to believe that the path is normalized?
|
||||
let files = FileChangeSet::new_removes(vec![path]);
|
||||
// todo: change focus
|
||||
let iw = self.primary.inner.lock().await;
|
||||
iw.add_memory_changes(MemoryEvent::Update(files));
|
||||
|
||||
Ok(())
|
||||
self.update_source(files).await
|
||||
}
|
||||
|
||||
pub async fn edit_source(
|
||||
|
@ -263,10 +323,8 @@ impl CompileCluster {
|
|||
drop(memory_changes);
|
||||
|
||||
let files = FileChangeSet::new_inserts(vec![(path.clone(), snapshot)]);
|
||||
let iw = self.primary.inner.lock().await;
|
||||
iw.add_memory_changes(MemoryEvent::Update(files));
|
||||
|
||||
Ok(())
|
||||
self.update_source(files).await
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -509,32 +567,47 @@ impl<H: CompilationHandle> CompileNode<H> {
|
|||
return Err(error_once!("entry file must be absolute", path: path.display()));
|
||||
}
|
||||
|
||||
// todo: more robust rollback logic
|
||||
let entry = self.entry.clone();
|
||||
let should_change = {
|
||||
let mut entry = entry.lock().unwrap();
|
||||
let should_change = entry.as_ref().map(|e| e != &path).unwrap_or(true);
|
||||
let prev = entry.clone();
|
||||
*entry = Some(path.clone());
|
||||
should_change
|
||||
|
||||
should_change.then_some(prev)
|
||||
};
|
||||
|
||||
if should_change {
|
||||
if let Some(prev) = should_change {
|
||||
let next = path.clone();
|
||||
|
||||
debug!(
|
||||
"the entry file of TypstActor({}) is changed to {}",
|
||||
self.diag_group,
|
||||
path.display()
|
||||
next.display()
|
||||
);
|
||||
|
||||
self.steal_async(move |compiler, _| {
|
||||
let root = compiler.compiler.world().workspace_root();
|
||||
if !path.starts_with(&root) {
|
||||
warn!("entry file is not in workspace root {}", path.display());
|
||||
return;
|
||||
let res = self
|
||||
.steal_async(move |compiler, _| {
|
||||
let root = compiler.compiler.world().workspace_root();
|
||||
if !path.starts_with(&root) {
|
||||
warn!("entry file is not in workspace root {}", path.display());
|
||||
return;
|
||||
}
|
||||
|
||||
let driver = &mut compiler.compiler.compiler.inner.compiler;
|
||||
driver.set_entry_file(path.as_ref().to_owned());
|
||||
})
|
||||
.await;
|
||||
|
||||
if res.is_err() {
|
||||
let mut entry = entry.lock().unwrap();
|
||||
if *entry == Some(next) {
|
||||
*entry = prev;
|
||||
}
|
||||
|
||||
let driver = &mut compiler.compiler.compiler.inner.compiler;
|
||||
driver.set_entry_file(path.as_ref().to_owned());
|
||||
})
|
||||
.await?;
|
||||
return res;
|
||||
}
|
||||
|
||||
// todo: trigger recompile
|
||||
let files = FileChangeSet::new_inserts(vec![]);
|
||||
|
|
|
@ -105,9 +105,6 @@ impl LanguageServer for TypstServer {
|
|||
// Bootstrap actors
|
||||
let actor_factory = actor::ActorFactory::new(cc);
|
||||
|
||||
// Run the PDF export actor before preparing cluster to avoid loss of events
|
||||
actor_factory.pdf_export_actor().await;
|
||||
|
||||
// Bootstrap the cluster
|
||||
let cluster = actor_factory.prepare_cluster(self.client.clone(), params.root_paths());
|
||||
let (cluster, cluster_bg) = cluster.split();
|
||||
|
@ -454,6 +451,9 @@ impl LanguageServer for TypstServer {
|
|||
Some(LspCommand::ClearCache) => {
|
||||
self.command_clear_cache(arguments).await?;
|
||||
}
|
||||
Some(LspCommand::PinMain) => {
|
||||
self.command_pin_main(arguments).await?;
|
||||
}
|
||||
None => {
|
||||
error!("asked to execute unknown command");
|
||||
return Err(jsonrpc::Error::method_not_found());
|
||||
|
@ -467,6 +467,7 @@ impl LanguageServer for TypstServer {
|
|||
pub enum LspCommand {
|
||||
ExportPdf,
|
||||
ClearCache,
|
||||
PinMain,
|
||||
}
|
||||
|
||||
impl From<LspCommand> for String {
|
||||
|
@ -474,6 +475,7 @@ impl From<LspCommand> for String {
|
|||
match command {
|
||||
LspCommand::ExportPdf => "tinymist.doPdfExport".to_string(),
|
||||
LspCommand::ClearCache => "tinymist.doClearCache".to_string(),
|
||||
LspCommand::PinMain => "tinymist.doPinMain".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -483,12 +485,17 @@ impl LspCommand {
|
|||
match command {
|
||||
"tinymist.doPdfExport" => Some(Self::ExportPdf),
|
||||
"tinymist.doClearCache" => Some(Self::ClearCache),
|
||||
"tinymist.doPinMain" => Some(Self::PinMain),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn all_as_string() -> Vec<String> {
|
||||
vec![Self::ExportPdf.into(), Self::ClearCache.into()]
|
||||
vec![
|
||||
Self::ExportPdf.into(),
|
||||
Self::ClearCache.into(),
|
||||
Self::PinMain.into(),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -525,9 +532,37 @@ impl TypstServer {
|
|||
|
||||
// self.typst(|_| comemo::evict(0)).await;
|
||||
|
||||
// Ok(())
|
||||
Ok(())
|
||||
}
|
||||
|
||||
todo!()
|
||||
/// Pin main file to some path.
|
||||
pub async fn command_pin_main(&self, arguments: Vec<JsonValue>) -> jsonrpc::Result<()> {
|
||||
if arguments.is_empty() {
|
||||
return Err(jsonrpc::Error::invalid_params("Missing file URI argument"));
|
||||
}
|
||||
let Some(file_uri) = arguments.first().and_then(|v| v.as_str()) else {
|
||||
return Err(jsonrpc::Error::invalid_params(
|
||||
"Missing file URI as first argument",
|
||||
));
|
||||
};
|
||||
let file_uri = if file_uri == "detached" {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
Url::parse(file_uri)
|
||||
.map_err(|_| jsonrpc::Error::invalid_params("Parameter is not a valid URI"))?,
|
||||
)
|
||||
};
|
||||
|
||||
let update_result = self.universe().pin_main(file_uri.clone()).await;
|
||||
|
||||
update_result.map_err(|err| {
|
||||
error!("could not set main file: {err}");
|
||||
jsonrpc::Error::internal_error()
|
||||
})?;
|
||||
|
||||
info!("main file pinned: {main_url:?}", main_url = file_uri);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue