Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: browsing preview #1234

Merged
merged 6 commits into from
Feb 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 5 additions & 7 deletions crates/tinymist/src/actor/preview.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,11 @@ impl PreviewActor {
log::warn!("PreviewTask({task_id}): failed to unregister preview");
}

if !tab.is_primary {
let h = tab.compile_handler.clone();
let task_id = tab.task_id.clone();
self.client.handle.spawn(async move {
h.settle().await.log_error_with(|| {
format!("PreviewTask({task_id}): failed to settle")
});
if tab.is_primary {
tab.compile_handler.unpin_primary();
} else {
tab.compile_handler.settle().log_error_with(|| {
format!("PreviewTask({}): failed to settle", tab.task_id)
});
}

Expand Down
61 changes: 44 additions & 17 deletions crates/tinymist/src/cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -282,8 +282,27 @@ impl ServerState {
/// Start a preview instance.
#[cfg(feature = "preview")]
pub fn start_preview(
&mut self,
args: Vec<JsonValue>,
) -> SchedulableResponse<crate::tool::preview::StartPreviewResponse> {
self.start_preview_inner(args, false)
}

/// Start a preview instance for browsing.
#[cfg(feature = "preview")]
pub fn browse_preview(
&mut self,
args: Vec<JsonValue>,
) -> SchedulableResponse<crate::tool::preview::StartPreviewResponse> {
self.start_preview_inner(args, true)
}

/// Start a preview instance.
#[cfg(feature = "preview")]
pub fn start_preview_inner(
&mut self,
mut args: Vec<JsonValue>,
browsing_preview: bool,
) -> SchedulableResponse<crate::tool::preview::StartPreviewResponse> {
use std::path::Path;

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

// todo: preview specific arguments are not used
let input = cli_args
.compile
.input
.clone()
.ok_or_else(|| internal_error("entry file must be provided"))?;
let input = Path::new(&input);
let entry = if input.is_absolute() {
input.into()
} else {
// std::env::current_dir().unwrap().join(input)
return Err(invalid_params("entry file must be absolute path"));
};
let entry = cli_args.compile.input.as_ref();
let entry = entry
.map(|input| {
let input = Path::new(&input);
if !input.is_absolute() {
// std::env::current_dir().unwrap().join(input)
return Err(invalid_params("entry file must be absolute path"));
};

Ok(input.into())
})
.transpose()?;

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

let primary = &mut self.project.compiler.primary;
if !cli_args.not_as_primary && self.preview.watchers.register(&primary.id, watcher) {
// todo: recover pin status reliably
if !cli_args.not_as_primary
&& (browsing_preview || entry.is_some())
&& self.preview.watchers.register(&primary.id, watcher)
{
let id = primary.id.clone();
// todo: recover pin status reliably
self.pin_main_file(Some(entry))
.map_err(|e| internal_error(format!("could not pin file: {e}")))?;

if let Some(entry) = entry {
self.change_main_file(Some(entry)).map_err(internal_error)?;
}
self.set_pin_by_preview(true);

self.preview.start(cli_args, previewer, id, true)
} else {
} else if let Some(entry) = entry {
let id = self
.restart_dedicate(&task_id, Some(entry))
.map_err(internal_error)?;
Expand All @@ -348,6 +373,8 @@ impl ServerState {
}

self.preview.start(cli_args, previewer, id, false)
} else {
return Err(internal_error("entry file must be provided"));
}
}

Expand Down
9 changes: 7 additions & 2 deletions crates/tinymist/src/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@ impl ServerState {
/// Main file mutations on the primary project (which is used for the language
/// queries.)
impl ServerState {
/// Updates the `pinning_by_preview` status.
pub fn set_pin_by_preview(&mut self, pin: bool) {
self.pinning_by_preview = pin;
}

/// Changes main file to the given path.
pub fn change_main_file(&mut self, path: Option<ImmutPath>) -> Result<bool> {
if path
Expand All @@ -124,7 +129,7 @@ impl ServerState {

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

/// Focuses main file to the given path.
pub fn focus_main_file(&mut self, new_entry: Option<ImmutPath>) -> Result<bool> {
if self.pinning || self.config.compile.has_default_entry_path {
if self.pinning_by_user || self.config.compile.has_default_entry_path {
self.focusing = new_entry;
return Ok(false);
}
Expand Down
38 changes: 19 additions & 19 deletions crates/tinymist/src/lsp_query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,6 @@ impl ServerState {
pub fn query(&mut self, query: CompilerQueryRequest) -> QueryFuture {
use CompilerQueryRequest::*;

let is_pinning = self.pinning;
just_ok(match query {
FoldingRange(req) => query_source!(self, FoldingRange, req)?,
SelectionRange(req) => query_source!(self, SelectionRange, req)?,
Expand All @@ -327,34 +326,35 @@ impl ServerState {
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),
_ => return self.query_on(query),
})
}

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

let (mut snap, stat) = self.query_snapshot_with_stat(&query)?;
let input = query
.associated_path()
.map(|path| self.resolve_task(path.into()))
.or_else(|| {
let root = self.entry_resolver().root(None)?;
Some(TaskInputs {
entry: Some(EntryState::new_rooted_by_id(root, *DETACHED_ENTRY)),
..Default::default()
})
});
// todo: whether it is safe to inherit success_doc with changed entry
if !self.is_pinning() {
let input = query
.associated_path()
.map(|path| self.resolve_task(path.into()))
.or_else(|| {
let root = self.entry_resolver().root(None)?;
Some(TaskInputs {
entry: Some(EntryState::new_rooted_by_id(root, *DETACHED_ENTRY)),
..Default::default()
})
});

if let Some(input) = input {
snap = snap.task(input);
}
}

just_future(async move {
// todo: whether it is safe to inherit success_doc with changed entry
if !is_pinning {
if let Some(input) = input {
snap = snap.task(input);
}
}
stat.snap();

if matches!(query, Completion(..)) {
Expand Down
22 changes: 17 additions & 5 deletions crates/tinymist/src/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,11 @@ use tokio::sync::mpsc;
use typst::{diag::FileResult, foundations::Bytes, layout::Position as TypstPosition};

use super::ServerState;
use crate::actor::editor::{CompileStatus, CompileStatusEnum, EditorRequest, ProjVersion};
use crate::stats::{CompilerQueryStats, QueryStatGuard};
use crate::{
actor::editor::{CompileStatus, CompileStatusEnum, EditorRequest, ProjVersion},
ServerEvent,
};
use crate::{task::ExportUserConfig, Config};

type EditorSender = mpsc::UnboundedSender<EditorRequest>;
Expand Down Expand Up @@ -347,19 +350,28 @@ pub struct CompileHandlerImpl {
}

pub trait ProjectClient: Send + Sync + 'static {
fn send_event(&self, event: LspInterrupt);
fn interrupt(&self, event: LspInterrupt);
fn server_event(&self, event: ServerEvent);
}

impl ProjectClient for LspClient {
fn send_event(&self, event: LspInterrupt) {
fn interrupt(&self, event: LspInterrupt) {
self.send_event(event);
}

fn server_event(&self, event: ServerEvent) {
self.send_event(event);
}
}

impl ProjectClient for mpsc::UnboundedSender<LspInterrupt> {
fn send_event(&self, event: LspInterrupt) {
fn interrupt(&self, event: LspInterrupt) {
self.send(event).log_error("failed to send interrupt");
}

fn server_event(&self, _event: ServerEvent) {
log::warn!("ProjectClient: server_event is not implemented for mpsc::UnboundedSender<LspInterrupt>");
}
}

impl CompileHandlerImpl {
Expand Down Expand Up @@ -510,7 +522,7 @@ impl CompileHandler<LspCompilerFeat, ProjectInsStateExt> for CompileHandlerImpl

self.notify_diagnostics(snap);

self.client.send_event(LspInterrupt::Compiled(snap.clone()));
self.client.interrupt(LspInterrupt::Compiled(snap.clone()));
self.export.signal(snap);

self.editor_tx
Expand Down
49 changes: 47 additions & 2 deletions crates/tinymist/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,10 @@ pub struct ServerState {
/// Whether the server has registered document formatter capabilities.
pub formatter_registered: bool,
/// Whether client is pinning a file.
pub pinning: bool,
pub pinning_by_user: bool,
/// Whether client is pinning caused by preview, which has lower priority
/// than pinning.
pub pinning_by_preview: bool,
/// The client focusing file.
pub focusing: Option<ImmutPath>,
/// The client ever focused implicitly by activities.
Expand Down Expand Up @@ -111,7 +114,8 @@ impl ServerState {
formatter_registered: false,
config,

pinning: false,
pinning_by_user: false,
pinning_by_preview: false,
focusing: None,
formatter,
user_action: Default::default(),
Expand All @@ -133,6 +137,15 @@ impl ServerState {
&self.compile_config().entry_resolver
}

/// Whether the main file is pinning.
pub fn is_pinning(&self) -> bool {
self.pinning_by_user
|| (self.pinning_by_preview && {
let primary_verse = &self.project.compiler.primary.verse;
!primary_verse.entry_state().is_inactive()
})
}

/// The entry point for the language server.
pub fn main(client: TypedLspClient<Self>, config: Config, start: bool) -> Self {
log::info!("LanguageState: initialized with config {config:?}");
Expand Down Expand Up @@ -171,6 +184,7 @@ impl ServerState {
#[cfg(feature = "preview")]
let provider = provider
.with_command("tinymist.doStartPreview", State::start_preview)
.with_command("tinymist.doStartBrowsingPreview", State::browse_preview)
.with_command("tinymist.doKillPreview", State::kill_preview)
.with_command("tinymist.scrollPreview", State::scroll_preview);

Expand All @@ -182,6 +196,10 @@ impl ServerState {
&LspInterrupt::Compile(ProjectInsId::default()),
State::compile_interrupt::<T>,
)
.with_event(
&ServerEvent::UnpinPrimaryByPreview,
State::server_event::<T>,
)
// lantency sensitive
.with_request_::<Completion>(State::completion)
.with_request_::<SemanticTokensFullRequest>(State::semantic_tokens_full)
Expand Down Expand Up @@ -274,6 +292,33 @@ impl ServerState {
// log::info!("interrupted in {:?}", _start.elapsed());
Ok(())
}

/// Handles the server events.
fn server_event<T: Initializer<S = Self>>(
mut state: ServiceState<T, T::S>,
params: ServerEvent,
) -> anyhow::Result<()> {
let _start = std::time::Instant::now();
// log::info!("incoming interrupt: {params:?}");
let Some(ready) = state.ready() else {
log::info!("server event sent to not ready server");
return Ok(());
};

match params {
ServerEvent::UnpinPrimaryByPreview => {
ready.set_pin_by_preview(false);
}
}

Ok(())
}
}

/// An event sent to the language server.
pub enum ServerEvent {
/// Updates the `pinning_by_preview` status to false.
UnpinPrimaryByPreview,
}

impl ServerState {
Expand Down
16 changes: 10 additions & 6 deletions crates/tinymist/src/tool/preview.rs
Original file line number Diff line number Diff line change
Expand Up @@ -252,14 +252,18 @@ impl PreviewProjectHandler {
pub fn flush_compile(&self) {
let _ = self.project_id;
self.client
.send_event(LspInterrupt::Compile(self.project_id.clone()));
.interrupt(LspInterrupt::Compile(self.project_id.clone()));
}

pub async fn settle(&self) -> Result<(), Error> {
pub fn settle(&self) -> Result<(), Error> {
self.client
.send_event(LspInterrupt::Settle(self.project_id.clone()));
.interrupt(LspInterrupt::Settle(self.project_id.clone()));
Ok(())
}

pub fn unpin_primary(&self) {
self.client.server_event(ServerEvent::UnpinPrimaryByPreview);
}
}

impl EditorServer for PreviewProjectHandler {
Expand All @@ -285,7 +289,7 @@ impl EditorServer for PreviewProjectHandler {
} else {
MemoryEvent::Update(files)
});
self.client.send_event(intr);
self.client.interrupt(intr);

Ok(())
}
Expand All @@ -294,7 +298,7 @@ impl EditorServer for PreviewProjectHandler {
// 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)));
.interrupt(LspInterrupt::Memory(MemoryEvent::Update(files)));

Ok(())
}
Expand Down Expand Up @@ -630,7 +634,7 @@ pub async fn preview_main(args: PreviewCliArgs) -> Result<()> {
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));
fs_intr_tx.interrupt(LspInterrupt::Fs(event));
}));

// Consume editor_rx
Expand Down
Loading
Loading