Skip to content

Commit 7b8e275

Browse files
feat: browsing preview (#1234)
* feat: browsing preview * feat: add browsing-preview command * refactor: check pin status on main thread * feat: preview without specifying main * feat: implement tinymist.doStartBrowsingPreview * test: update snapshot
1 parent deb1425 commit 7b8e275

File tree

13 files changed

+231
-85
lines changed

13 files changed

+231
-85
lines changed

crates/tinymist/src/actor/preview.rs

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,11 @@ impl PreviewActor {
6262
log::warn!("PreviewTask({task_id}): failed to unregister preview");
6363
}
6464

65-
if !tab.is_primary {
66-
let h = tab.compile_handler.clone();
67-
let task_id = tab.task_id.clone();
68-
self.client.handle.spawn(async move {
69-
h.settle().await.log_error_with(|| {
70-
format!("PreviewTask({task_id}): failed to settle")
71-
});
65+
if tab.is_primary {
66+
tab.compile_handler.unpin_primary();
67+
} else {
68+
tab.compile_handler.settle().log_error_with(|| {
69+
format!("PreviewTask({}): failed to settle", tab.task_id)
7270
});
7371
}
7472

crates/tinymist/src/cmd.rs

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -282,8 +282,27 @@ impl ServerState {
282282
/// Start a preview instance.
283283
#[cfg(feature = "preview")]
284284
pub fn start_preview(
285+
&mut self,
286+
args: Vec<JsonValue>,
287+
) -> SchedulableResponse<crate::tool::preview::StartPreviewResponse> {
288+
self.start_preview_inner(args, false)
289+
}
290+
291+
/// Start a preview instance for browsing.
292+
#[cfg(feature = "preview")]
293+
pub fn browse_preview(
294+
&mut self,
295+
args: Vec<JsonValue>,
296+
) -> SchedulableResponse<crate::tool::preview::StartPreviewResponse> {
297+
self.start_preview_inner(args, true)
298+
}
299+
300+
/// Start a preview instance.
301+
#[cfg(feature = "preview")]
302+
pub fn start_preview_inner(
285303
&mut self,
286304
mut args: Vec<JsonValue>,
305+
browsing_preview: bool,
287306
) -> SchedulableResponse<crate::tool::preview::StartPreviewResponse> {
288307
use std::path::Path;
289308

@@ -299,18 +318,18 @@ impl ServerState {
299318
PreviewCliArgs::try_parse_from(cli_args).map_err(|e| invalid_params(e.to_string()))?;
300319

301320
// todo: preview specific arguments are not used
302-
let input = cli_args
303-
.compile
304-
.input
305-
.clone()
306-
.ok_or_else(|| internal_error("entry file must be provided"))?;
307-
let input = Path::new(&input);
308-
let entry = if input.is_absolute() {
309-
input.into()
310-
} else {
311-
// std::env::current_dir().unwrap().join(input)
312-
return Err(invalid_params("entry file must be absolute path"));
313-
};
321+
let entry = cli_args.compile.input.as_ref();
322+
let entry = entry
323+
.map(|input| {
324+
let input = Path::new(&input);
325+
if !input.is_absolute() {
326+
// std::env::current_dir().unwrap().join(input)
327+
return Err(invalid_params("entry file must be absolute path"));
328+
};
329+
330+
Ok(input.into())
331+
})
332+
.transpose()?;
314333

315334
let task_id = cli_args.preview.task_id.clone();
316335
if task_id == "primary" {
@@ -321,14 +340,20 @@ impl ServerState {
321340
let watcher = previewer.compile_watcher();
322341

323342
let primary = &mut self.project.compiler.primary;
324-
if !cli_args.not_as_primary && self.preview.watchers.register(&primary.id, watcher) {
343+
// todo: recover pin status reliably
344+
if !cli_args.not_as_primary
345+
&& (browsing_preview || entry.is_some())
346+
&& self.preview.watchers.register(&primary.id, watcher)
347+
{
325348
let id = primary.id.clone();
326-
// todo: recover pin status reliably
327-
self.pin_main_file(Some(entry))
328-
.map_err(|e| internal_error(format!("could not pin file: {e}")))?;
349+
350+
if let Some(entry) = entry {
351+
self.change_main_file(Some(entry)).map_err(internal_error)?;
352+
}
353+
self.set_pin_by_preview(true);
329354

330355
self.preview.start(cli_args, previewer, id, true)
331-
} else {
356+
} else if let Some(entry) = entry {
332357
let id = self
333358
.restart_dedicate(&task_id, Some(entry))
334359
.map_err(internal_error)?;
@@ -348,6 +373,8 @@ impl ServerState {
348373
}
349374

350375
self.preview.start(cli_args, previewer, id, false)
376+
} else {
377+
return Err(internal_error("entry file must be provided"));
351378
}
352379
}
353380

crates/tinymist/src/input.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,11 @@ impl ServerState {
103103
/// Main file mutations on the primary project (which is used for the language
104104
/// queries.)
105105
impl ServerState {
106+
/// Updates the `pinning_by_preview` status.
107+
pub fn set_pin_by_preview(&mut self, pin: bool) {
108+
self.pinning_by_preview = pin;
109+
}
110+
106111
/// Changes main file to the given path.
107112
pub fn change_main_file(&mut self, path: Option<ImmutPath>) -> Result<bool> {
108113
if path
@@ -124,7 +129,7 @@ impl ServerState {
124129

125130
/// Pins the main file to the given path
126131
pub fn pin_main_file(&mut self, new_entry: Option<ImmutPath>) -> Result<()> {
127-
self.pinning = new_entry.is_some();
132+
self.pinning_by_user = new_entry.is_some();
128133
let entry = new_entry
129134
.or_else(|| self.entry_resolver().resolve_default())
130135
.or_else(|| self.focusing.clone());
@@ -134,7 +139,7 @@ impl ServerState {
134139

135140
/// Focuses main file to the given path.
136141
pub fn focus_main_file(&mut self, new_entry: Option<ImmutPath>) -> Result<bool> {
137-
if self.pinning || self.config.compile.has_default_entry_path {
142+
if self.pinning_by_user || self.config.compile.has_default_entry_path {
138143
self.focusing = new_entry;
139144
return Ok(false);
140145
}

crates/tinymist/src/lsp_query.rs

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,6 @@ impl ServerState {
317317
pub fn query(&mut self, query: CompilerQueryRequest) -> QueryFuture {
318318
use CompilerQueryRequest::*;
319319

320-
let is_pinning = self.pinning;
321320
just_ok(match query {
322321
FoldingRange(req) => query_source!(self, FoldingRange, req)?,
323322
SelectionRange(req) => query_source!(self, SelectionRange, req)?,
@@ -327,34 +326,35 @@ impl ServerState {
327326
OnExport(req) => return self.on_export(req),
328327
ServerInfo(_) => return self.collect_server_info(),
329328
// todo: query on dedicate projects
330-
_ => return self.query_on(is_pinning, query),
329+
_ => return self.query_on(query),
331330
})
332331
}
333332

334-
fn query_on(&mut self, is_pinning: bool, query: CompilerQueryRequest) -> QueryFuture {
333+
fn query_on(&mut self, query: CompilerQueryRequest) -> QueryFuture {
335334
use CompilerQueryRequest::*;
336335
type R = CompilerQueryResponse;
337336
assert!(query.fold_feature() != FoldRequestFeature::ContextFreeUnique);
338337

339338
let (mut snap, stat) = self.query_snapshot_with_stat(&query)?;
340-
let input = query
341-
.associated_path()
342-
.map(|path| self.resolve_task(path.into()))
343-
.or_else(|| {
344-
let root = self.entry_resolver().root(None)?;
345-
Some(TaskInputs {
346-
entry: Some(EntryState::new_rooted_by_id(root, *DETACHED_ENTRY)),
347-
..Default::default()
348-
})
349-
});
339+
// todo: whether it is safe to inherit success_doc with changed entry
340+
if !self.is_pinning() {
341+
let input = query
342+
.associated_path()
343+
.map(|path| self.resolve_task(path.into()))
344+
.or_else(|| {
345+
let root = self.entry_resolver().root(None)?;
346+
Some(TaskInputs {
347+
entry: Some(EntryState::new_rooted_by_id(root, *DETACHED_ENTRY)),
348+
..Default::default()
349+
})
350+
});
351+
352+
if let Some(input) = input {
353+
snap = snap.task(input);
354+
}
355+
}
350356

351357
just_future(async move {
352-
// todo: whether it is safe to inherit success_doc with changed entry
353-
if !is_pinning {
354-
if let Some(input) = input {
355-
snap = snap.task(input);
356-
}
357-
}
358358
stat.snap();
359359

360360
if matches!(query, Completion(..)) {

crates/tinymist/src/project.rs

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,11 @@ use tokio::sync::mpsc;
3838
use typst::{diag::FileResult, foundations::Bytes, layout::Position as TypstPosition};
3939

4040
use super::ServerState;
41-
use crate::actor::editor::{CompileStatus, CompileStatusEnum, EditorRequest, ProjVersion};
4241
use crate::stats::{CompilerQueryStats, QueryStatGuard};
42+
use crate::{
43+
actor::editor::{CompileStatus, CompileStatusEnum, EditorRequest, ProjVersion},
44+
ServerEvent,
45+
};
4346
use crate::{task::ExportUserConfig, Config};
4447

4548
type EditorSender = mpsc::UnboundedSender<EditorRequest>;
@@ -347,19 +350,28 @@ pub struct CompileHandlerImpl {
347350
}
348351

349352
pub trait ProjectClient: Send + Sync + 'static {
350-
fn send_event(&self, event: LspInterrupt);
353+
fn interrupt(&self, event: LspInterrupt);
354+
fn server_event(&self, event: ServerEvent);
351355
}
352356

353357
impl ProjectClient for LspClient {
354-
fn send_event(&self, event: LspInterrupt) {
358+
fn interrupt(&self, event: LspInterrupt) {
359+
self.send_event(event);
360+
}
361+
362+
fn server_event(&self, event: ServerEvent) {
355363
self.send_event(event);
356364
}
357365
}
358366

359367
impl ProjectClient for mpsc::UnboundedSender<LspInterrupt> {
360-
fn send_event(&self, event: LspInterrupt) {
368+
fn interrupt(&self, event: LspInterrupt) {
361369
self.send(event).log_error("failed to send interrupt");
362370
}
371+
372+
fn server_event(&self, _event: ServerEvent) {
373+
log::warn!("ProjectClient: server_event is not implemented for mpsc::UnboundedSender<LspInterrupt>");
374+
}
363375
}
364376

365377
impl CompileHandlerImpl {
@@ -510,7 +522,7 @@ impl CompileHandler<LspCompilerFeat, ProjectInsStateExt> for CompileHandlerImpl
510522

511523
self.notify_diagnostics(snap);
512524

513-
self.client.send_event(LspInterrupt::Compiled(snap.clone()));
525+
self.client.interrupt(LspInterrupt::Compiled(snap.clone()));
514526
self.export.signal(snap);
515527

516528
self.editor_tx

crates/tinymist/src/server.rs

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,10 @@ pub struct ServerState {
6060
/// Whether the server has registered document formatter capabilities.
6161
pub formatter_registered: bool,
6262
/// Whether client is pinning a file.
63-
pub pinning: bool,
63+
pub pinning_by_user: bool,
64+
/// Whether client is pinning caused by preview, which has lower priority
65+
/// than pinning.
66+
pub pinning_by_preview: bool,
6467
/// The client focusing file.
6568
pub focusing: Option<ImmutPath>,
6669
/// The client ever focused implicitly by activities.
@@ -111,7 +114,8 @@ impl ServerState {
111114
formatter_registered: false,
112115
config,
113116

114-
pinning: false,
117+
pinning_by_user: false,
118+
pinning_by_preview: false,
115119
focusing: None,
116120
formatter,
117121
user_action: Default::default(),
@@ -133,6 +137,15 @@ impl ServerState {
133137
&self.compile_config().entry_resolver
134138
}
135139

140+
/// Whether the main file is pinning.
141+
pub fn is_pinning(&self) -> bool {
142+
self.pinning_by_user
143+
|| (self.pinning_by_preview && {
144+
let primary_verse = &self.project.compiler.primary.verse;
145+
!primary_verse.entry_state().is_inactive()
146+
})
147+
}
148+
136149
/// The entry point for the language server.
137150
pub fn main(client: TypedLspClient<Self>, config: Config, start: bool) -> Self {
138151
log::info!("LanguageState: initialized with config {config:?}");
@@ -171,6 +184,7 @@ impl ServerState {
171184
#[cfg(feature = "preview")]
172185
let provider = provider
173186
.with_command("tinymist.doStartPreview", State::start_preview)
187+
.with_command("tinymist.doStartBrowsingPreview", State::browse_preview)
174188
.with_command("tinymist.doKillPreview", State::kill_preview)
175189
.with_command("tinymist.scrollPreview", State::scroll_preview);
176190

@@ -182,6 +196,10 @@ impl ServerState {
182196
&LspInterrupt::Compile(ProjectInsId::default()),
183197
State::compile_interrupt::<T>,
184198
)
199+
.with_event(
200+
&ServerEvent::UnpinPrimaryByPreview,
201+
State::server_event::<T>,
202+
)
185203
// lantency sensitive
186204
.with_request_::<Completion>(State::completion)
187205
.with_request_::<SemanticTokensFullRequest>(State::semantic_tokens_full)
@@ -274,6 +292,33 @@ impl ServerState {
274292
// log::info!("interrupted in {:?}", _start.elapsed());
275293
Ok(())
276294
}
295+
296+
/// Handles the server events.
297+
fn server_event<T: Initializer<S = Self>>(
298+
mut state: ServiceState<T, T::S>,
299+
params: ServerEvent,
300+
) -> anyhow::Result<()> {
301+
let _start = std::time::Instant::now();
302+
// log::info!("incoming interrupt: {params:?}");
303+
let Some(ready) = state.ready() else {
304+
log::info!("server event sent to not ready server");
305+
return Ok(());
306+
};
307+
308+
match params {
309+
ServerEvent::UnpinPrimaryByPreview => {
310+
ready.set_pin_by_preview(false);
311+
}
312+
}
313+
314+
Ok(())
315+
}
316+
}
317+
318+
/// An event sent to the language server.
319+
pub enum ServerEvent {
320+
/// Updates the `pinning_by_preview` status to false.
321+
UnpinPrimaryByPreview,
277322
}
278323

279324
impl ServerState {

crates/tinymist/src/tool/preview.rs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -252,14 +252,18 @@ impl PreviewProjectHandler {
252252
pub fn flush_compile(&self) {
253253
let _ = self.project_id;
254254
self.client
255-
.send_event(LspInterrupt::Compile(self.project_id.clone()));
255+
.interrupt(LspInterrupt::Compile(self.project_id.clone()));
256256
}
257257

258-
pub async fn settle(&self) -> Result<(), Error> {
258+
pub fn settle(&self) -> Result<(), Error> {
259259
self.client
260-
.send_event(LspInterrupt::Settle(self.project_id.clone()));
260+
.interrupt(LspInterrupt::Settle(self.project_id.clone()));
261261
Ok(())
262262
}
263+
264+
pub fn unpin_primary(&self) {
265+
self.client.server_event(ServerEvent::UnpinPrimaryByPreview);
266+
}
263267
}
264268

265269
impl EditorServer for PreviewProjectHandler {
@@ -285,7 +289,7 @@ impl EditorServer for PreviewProjectHandler {
285289
} else {
286290
MemoryEvent::Update(files)
287291
});
288-
self.client.send_event(intr);
292+
self.client.interrupt(intr);
289293

290294
Ok(())
291295
}
@@ -294,7 +298,7 @@ impl EditorServer for PreviewProjectHandler {
294298
// todo: is it safe to believe that the path is normalized?
295299
let files = FileChangeSet::new_removes(files.files.into_iter().map(From::from).collect());
296300
self.client
297-
.send_event(LspInterrupt::Memory(MemoryEvent::Update(files)));
301+
.interrupt(LspInterrupt::Memory(MemoryEvent::Update(files)));
298302

299303
Ok(())
300304
}
@@ -630,7 +634,7 @@ pub async fn preview_main(args: PreviewCliArgs) -> Result<()> {
630634
let (dep_tx, dep_rx) = tokio::sync::mpsc::unbounded_channel();
631635
let fs_intr_tx = intr_tx.clone();
632636
tokio::spawn(watch_deps(dep_rx, move |event| {
633-
fs_intr_tx.send_event(LspInterrupt::Fs(event));
637+
fs_intr_tx.interrupt(LspInterrupt::Fs(event));
634638
}));
635639

636640
// Consume editor_rx

0 commit comments

Comments
 (0)