feat: add capability to pin main

This commit is contained in:
Myriad-Dreamin 2024-03-08 18:18:34 +08:00
parent f6d473f320
commit 85cc29f884
4 changed files with 214 additions and 70 deletions

View file

@ -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)
}
}

View file

@ -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![]);

View file

@ -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(())
}
}