refactor: rearrange state methods (#1252)

This commit is contained in:
Myriad-Dreamin 2025-02-02 16:24:12 +08:00 committed by GitHub
parent 4ce405a89b
commit 5069a89d5d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 445 additions and 453 deletions

View file

@ -330,7 +330,7 @@ pub mod prelude {
use crate::Error;
pub use super::{IgnoreLogging, WithContext, WithContextUntyped};
pub use crate::Result;
pub use crate::{bail, Result};
pub fn map_string_err<T: ToString>(loc: &'static str) -> impl Fn(T) -> Error {
move |e| Error::new(loc, e.to_string().to_error_kind(), None)

View file

@ -22,7 +22,7 @@ use world::TaskInputs;
use super::state::*;
use super::*;
use crate::state::query::{run_query, LspClientExt};
use crate::state::lsp_query::{run_query, LspClientExt};
use crate::tool::package::InitTask;
/// See [`ProjectTask`].

View file

@ -38,7 +38,7 @@ pub use world::{CompileFontArgs, CompileOnceArgs, CompilePackageArgs};
use lsp_server::ResponseError;
use serde_json::from_value;
use state::query::QueryFuture;
use state::lsp_query::QueryFuture;
use sync_lsp::*;
use tinymist_std::error::Result;
use utils::*;

View file

@ -2,7 +2,7 @@ mod server;
pub use server::*;
pub(crate) mod input;
pub(crate) mod lsp;
pub(crate) mod lsp_query;
pub mod project;
pub(crate) mod protocol;
pub(crate) mod query;
pub(crate) mod route;

View file

@ -1,145 +1,17 @@
use std::path::PathBuf;
use lsp_types::request::WorkspaceConfiguration;
use lsp_types::*;
use once_cell::sync::OnceCell;
use reflexo_typst::Bytes;
use serde_json::{Map, Value as JsonValue};
use sync_lsp::*;
use tinymist_project::{Interrupt, ProjectResolutionKind};
use tinymist_query::{to_typst_range, PositionEncoding};
use tinymist_std::error::{prelude::*, IgnoreLogging};
use tinymist_std::error::prelude::*;
use tinymist_std::ImmutPath;
use typst::{diag::FileResult, syntax::Source};
use crate::route::ProjectResolution;
use crate::task::FormatterConfig;
use crate::world::vfs::{notify::MemoryEvent, FileChangeSet};
use crate::world::TaskInputs;
use crate::{init::*, *};
/// LSP Document Synchronization
impl ServerState {
pub(crate) fn did_open(&mut self, params: DidOpenTextDocumentParams) -> LspResult<()> {
log::info!("did open {:?}", params.text_document.uri);
let path = as_path_(params.text_document.uri);
let text = params.text_document.text;
self.create_source(path.clone(), text)
.map_err(|e| invalid_params(e.to_string()))?;
// Focus after opening
self.implicit_focus_entry(|| Some(path.as_path().into()), 'o');
Ok(())
}
pub(crate) fn did_close(&mut self, params: DidCloseTextDocumentParams) -> LspResult<()> {
let path = as_path_(params.text_document.uri);
self.remove_source(path.clone())
.map_err(|e| invalid_params(e.to_string()))?;
Ok(())
}
pub(crate) fn did_change(&mut self, params: DidChangeTextDocumentParams) -> LspResult<()> {
let path = as_path_(params.text_document.uri);
let changes = params.content_changes;
self.edit_source(path.clone(), changes, self.const_config().position_encoding)
.map_err(|e| invalid_params(e.to_string()))?;
Ok(())
}
pub(crate) fn did_save(&mut self, _params: DidSaveTextDocumentParams) -> LspResult<()> {
Ok(())
}
}
/// LSP Configuration Synchronization
impl ServerState {
pub(crate) fn on_changed_configuration(
&mut self,
values: Map<String, JsonValue>,
) -> LspResult<()> {
let old_config = self.config.clone();
match self.config.update_by_map(&values) {
Ok(()) => {}
Err(err) => {
self.config = old_config;
log::error!("error applying new settings: {err}");
return Err(invalid_params(format!(
"error applying new settings: {err}"
)));
}
}
let new_export_config = self.config.export();
if old_config.export() != new_export_config {
self.change_export_config(new_export_config);
}
if old_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()
.log_error("could not restart primary");
}
if old_config.semantic_tokens != self.config.semantic_tokens {
self.enable_sema_token_caps(self.config.semantic_tokens == SemanticTokensMode::Enable)
.log_error("could not change semantic tokens config");
}
let new_formatter_config = self.config.formatter();
if !old_config.formatter().eq(&new_formatter_config) {
let enabled = !matches!(new_formatter_config.config, FormatterConfig::Disable);
self.enable_formatter_caps(enabled)
.log_error("could not change formatter config");
self.formatter.change_config(new_formatter_config);
}
log::info!("new settings applied");
Ok(())
}
pub(crate) fn did_change_configuration(
&mut self,
params: DidChangeConfigurationParams,
) -> LspResult<()> {
// For some clients, we don't get the actual changed configuration and need to
// poll for it https://github.com/microsoft/language-server-protocol/issues/676
if let JsonValue::Object(settings) = params.settings {
return self.on_changed_configuration(settings);
};
self.client.send_request::<WorkspaceConfiguration>(
ConfigurationParams {
items: Config::get_items(),
},
Self::workspace_configuration_callback,
);
Ok(())
}
fn workspace_configuration_callback(this: &mut ServerState, resp: lsp_server::Response) {
if let Some(err) = resp.error {
log::error!("failed to request configuration: {err:?}");
return;
}
let Some(result) = resp.result else {
log::error!("no configuration returned");
return;
};
let Some(resp) = serde_json::from_value::<Vec<JsonValue>>(result)
.log_error("could not parse configuration")
else {
return;
};
let _ = this.on_changed_configuration(Config::values_to_map(resp));
}
}
use crate::*;
/// In memory source file management.
impl ServerState {

View file

@ -1,10 +1,204 @@
use lsp_types::request::WorkspaceConfiguration;
use lsp_types::*;
use once_cell::sync::OnceCell;
use request::{RegisterCapability, UnregisterCapability};
use serde_json::{Map, Value as JsonValue};
use sync_lsp::*;
use tinymist_std::error::{prelude::*, IgnoreLogging};
use crate::task::FormatterConfig;
use crate::{init::*, *};
/// Trait implemented by language server backends.
///
/// This interface allows servers adhering to the [Language Server Protocol] to
/// be implemented in a safe and easily testable way without exposing the
/// low-level implementation details.
///
/// [Language Server Protocol]: https://microsoft.github.io/language-server-protocol/
impl ServerState {
/// The [`initialized`] notification is sent from the client to the server
/// after the client received the result of the initialize request but
/// before the client sends anything else.
///
/// [`initialized`]: https://microsoft.github.io/language-server-protocol/specification#initialized
///
/// The server can use the `initialized` notification, for example, to
/// dynamically register capabilities with the client.
pub(crate) fn initialized(&mut self, _params: InitializedParams) -> LspResult<()> {
if self.const_config().tokens_dynamic_registration
&& self.config.semantic_tokens == SemanticTokensMode::Enable
{
self.enable_sema_token_caps(true)
.log_error("could not register semantic tokens for initialization");
}
if self.const_config().doc_fmt_dynamic_registration
&& self.config.formatter_mode != FormatterMode::Disable
{
self.enable_formatter_caps(true)
.log_error("could not register formatter for initialization");
}
if self.const_config().cfg_change_registration {
log::trace!("setting up to request config change notifications");
const CONFIG_REGISTRATION_ID: &str = "config";
const CONFIG_METHOD_ID: &str = "workspace/didChangeConfiguration";
self.register_capability(vec![Registration {
id: CONFIG_REGISTRATION_ID.to_owned(),
method: CONFIG_METHOD_ID.to_owned(),
register_options: None,
}])
.log_error("could not register to watch config changes");
}
log::info!("server initialized");
Ok(())
}
/// The [`shutdown`] request asks the server to gracefully shut down, but to
/// not exit.
///
/// [`shutdown`]: https://microsoft.github.io/language-server-protocol/specification#shutdown
///
/// This request is often later followed by an [`exit`] notification, which
/// will cause the server to exit immediately.
///
/// [`exit`]: https://microsoft.github.io/language-server-protocol/specification#exit
///
/// This method is guaranteed to only execute once. If the client sends this
/// request to the server again, the server will respond with JSON-RPC
/// error code `-32600` (invalid request).
pub(crate) fn shutdown(&mut self, _params: ()) -> SchedulableResponse<()> {
just_ok(())
}
}
/// LSP Document Synchronization
impl ServerState {
pub(crate) fn did_open(&mut self, params: DidOpenTextDocumentParams) -> LspResult<()> {
log::info!("did open {:?}", params.text_document.uri);
let path = as_path_(params.text_document.uri);
let text = params.text_document.text;
self.create_source(path.clone(), text)
.map_err(|e| invalid_params(e.to_string()))?;
// Focus after opening
self.implicit_focus_entry(|| Some(path.as_path().into()), 'o');
Ok(())
}
pub(crate) fn did_close(&mut self, params: DidCloseTextDocumentParams) -> LspResult<()> {
let path = as_path_(params.text_document.uri);
self.remove_source(path.clone())
.map_err(|e| invalid_params(e.to_string()))?;
Ok(())
}
pub(crate) fn did_change(&mut self, params: DidChangeTextDocumentParams) -> LspResult<()> {
let path = as_path_(params.text_document.uri);
let changes = params.content_changes;
self.edit_source(path.clone(), changes, self.const_config().position_encoding)
.map_err(|e| invalid_params(e.to_string()))?;
Ok(())
}
pub(crate) fn did_save(&mut self, _params: DidSaveTextDocumentParams) -> LspResult<()> {
Ok(())
}
}
/// LSP Configuration Synchronization
impl ServerState {
pub(crate) fn on_changed_configuration(
&mut self,
values: Map<String, JsonValue>,
) -> LspResult<()> {
let old_config = self.config.clone();
match self.config.update_by_map(&values) {
Ok(()) => {}
Err(err) => {
self.config = old_config;
log::error!("error applying new settings: {err}");
return Err(invalid_params(format!(
"error applying new settings: {err}"
)));
}
}
let new_export_config = self.config.export();
if old_config.export() != new_export_config {
self.change_export_config(new_export_config);
}
if old_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()
.log_error("could not restart primary");
}
if old_config.semantic_tokens != self.config.semantic_tokens {
self.enable_sema_token_caps(self.config.semantic_tokens == SemanticTokensMode::Enable)
.log_error("could not change semantic tokens config");
}
let new_formatter_config = self.config.formatter();
if !old_config.formatter().eq(&new_formatter_config) {
let enabled = !matches!(new_formatter_config.config, FormatterConfig::Disable);
self.enable_formatter_caps(enabled)
.log_error("could not change formatter config");
self.formatter.change_config(new_formatter_config);
}
log::info!("new settings applied");
Ok(())
}
pub(crate) fn did_change_configuration(
&mut self,
params: DidChangeConfigurationParams,
) -> LspResult<()> {
// For some clients, we don't get the actual changed configuration and need to
// poll for it https://github.com/microsoft/language-server-protocol/issues/676
if let JsonValue::Object(settings) = params.settings {
return self.on_changed_configuration(settings);
};
self.client.send_request::<WorkspaceConfiguration>(
ConfigurationParams {
items: Config::get_items(),
},
Self::workspace_configuration_callback,
);
Ok(())
}
fn workspace_configuration_callback(this: &mut ServerState, resp: lsp_server::Response) {
if let Some(err) = resp.error {
log::error!("failed to request configuration: {err:?}");
return;
}
let Some(result) = resp.result else {
log::error!("no configuration returned");
return;
};
let Some(resp) = serde_json::from_value::<Vec<JsonValue>>(result)
.log_error("could not parse configuration")
else {
return;
};
let _ = this.on_changed_configuration(Config::values_to_map(resp));
}
}
impl ServerState {
// todo: handle error
pub(crate) fn register_capability(&self, registrations: Vec<Registration>) -> Result<()> {
@ -122,70 +316,3 @@ impl ServerState {
}
}
}
/// Trait implemented by language server backends.
///
/// This interface allows servers adhering to the [Language Server Protocol] to
/// be implemented in a safe and easily testable way without exposing the
/// low-level implementation details.
///
/// [Language Server Protocol]: https://microsoft.github.io/language-server-protocol/
impl ServerState {
/// The [`initialized`] notification is sent from the client to the server
/// after the client received the result of the initialize request but
/// before the client sends anything else.
///
/// [`initialized`]: https://microsoft.github.io/language-server-protocol/specification#initialized
///
/// The server can use the `initialized` notification, for example, to
/// dynamically register capabilities with the client.
pub(crate) fn initialized(&mut self, _params: InitializedParams) -> LspResult<()> {
if self.const_config().tokens_dynamic_registration
&& self.config.semantic_tokens == SemanticTokensMode::Enable
{
self.enable_sema_token_caps(true)
.log_error("could not register semantic tokens for initialization");
}
if self.const_config().doc_fmt_dynamic_registration
&& self.config.formatter_mode != FormatterMode::Disable
{
self.enable_formatter_caps(true)
.log_error("could not register formatter for initialization");
}
if self.const_config().cfg_change_registration {
log::trace!("setting up to request config change notifications");
const CONFIG_REGISTRATION_ID: &str = "config";
const CONFIG_METHOD_ID: &str = "workspace/didChangeConfiguration";
self.register_capability(vec![Registration {
id: CONFIG_REGISTRATION_ID.to_owned(),
method: CONFIG_METHOD_ID.to_owned(),
register_options: None,
}])
.log_error("could not register to watch config changes");
}
log::info!("server initialized");
Ok(())
}
/// The [`shutdown`] request asks the server to gracefully shut down, but to
/// not exit.
///
/// [`shutdown`]: https://microsoft.github.io/language-server-protocol/specification#shutdown
///
/// This request is often later followed by an [`exit`] notification, which
/// will cause the server to exit immediately.
///
/// [`exit`]: https://microsoft.github.io/language-server-protocol/specification#exit
///
/// This method is guaranteed to only execute once. If the client sends this
/// request to the server again, the server will respond with JSON-RPC
/// error code `-32600` (invalid request).
pub(crate) fn shutdown(&mut self, _params: ()) -> SchedulableResponse<()> {
just_ok(())
}
}

View file

@ -50,7 +50,7 @@ macro_rules! run_query {
}
pub(crate) use run_query;
/// Standard Language Features
/// LSP Standard Language Features
impl ServerState {
pub(crate) fn goto_definition(
&mut self,

View file

@ -26,26 +26,212 @@ use std::sync::Arc;
use parking_lot::Mutex;
use reflexo::{hash::FxHashMap, path::unix_slash};
use reflexo_typst::CompileReport;
use sync_lsp::LspClient;
use sync_lsp::{LspClient, TypedLspClient};
use tinymist_project::vfs::{FileChangeSet, MemoryEvent};
use tinymist_query::{
analysis::{Analysis, AnalysisRevLock, LocalContextGuard},
CompilerQueryRequest, CompilerQueryResponse, DiagnosticsMap, SemanticRequest, StatefulRequest,
VersionedDocument,
};
use tinymist_std::{
bail,
error::{prelude::*, IgnoreLogging},
analysis::{Analysis, AnalysisRevLock, LocalContextGuard, PeriscopeProvider},
CompilerQueryRequest, CompilerQueryResponse, DiagnosticsMap, LocalContext, SemanticRequest,
StatefulRequest, VersionedDocument,
};
use tinymist_render::PeriscopeRenderer;
use tinymist_std::{error::prelude::*, ImmutPath};
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::{task::ExportUserConfig, Config};
type EditorSender = mpsc::UnboundedSender<EditorRequest>;
/// LSP project compiler.
pub type LspProjectCompiler = ProjectCompiler<LspCompilerFeat, ProjectInsStateExt>;
/// Getters and the main loop.
impl ServerState {
/// Changes the export configuration.
pub fn change_export_config(&mut self, config: ExportUserConfig) {
self.project.export.change_config(config);
}
/// Snapshot the compiler thread for tasks
pub fn snapshot(&mut self) -> Result<LspCompileSnapshot> {
self.project.snapshot()
}
/// Snapshot the compiler thread for language queries
pub fn query_snapshot(&mut self) -> Result<LspQuerySnapshot> {
self.project.query_snapshot(None)
}
/// Snapshot the compiler thread for language queries
pub fn query_snapshot_with_stat(
&mut self,
q: &CompilerQueryRequest,
) -> Result<QuerySnapWithStat> {
let name: &'static str = q.into();
let path = q.associated_path();
let stat = self.project.stats.query_stat(path, name);
let snap = self.project.query_snapshot(Some(q))?;
Ok((snap, stat))
}
/// Restart the primary server.
pub fn restart_primary(&mut self) -> Result<ProjectInsId> {
// todo: hot replacement
#[cfg(feature = "preview")]
self.preview.stop_all();
let watchers = self.preview.watchers.clone();
let editor_tx = self.editor_tx.clone();
let new_project = Self::project(&self.config, editor_tx, self.client.clone(), watchers);
let mut old_project = std::mem::replace(&mut self.project, new_project);
let snapshot = FileChangeSet::new_inserts(
self.memory_changes
.iter()
.map(|(path, content)| {
let content = Bytes::from(content.clone().text().as_bytes());
(path.clone(), FileResult::Ok(content).into())
})
.collect(),
);
self.project
.interrupt(Interrupt::Memory(MemoryEvent::Update(snapshot)));
rayon::spawn(move || {
old_project.stop();
});
Ok(self.project.primary_id().clone())
}
/// Restart the server with the given group.
pub fn restart_dedicate(
&mut self,
dedicate: &str,
entry: Option<ImmutPath>,
) -> Result<ProjectInsId> {
let entry = self.config.compile.entry_resolver.resolve(entry);
self.project.restart_dedicate(dedicate, entry)
}
// pub async fn settle(&mut self) {
// let _ = self.change_entry(None);
// log::info!("TypstActor({}): settle requested", self.handle.diag_group);
// match self.handle.settle().await {
// Ok(()) => log::info!("TypstActor({}): settled",
// self.handle.diag_group), Err(err) => error!(
// "TypstActor({}): failed to settle: {err:#}",
// self.handle.diag_group
// ),
// }
// }
/// Create a fresh [`ProjectState`].
pub fn project(
config: &Config,
editor_tx: tokio::sync::mpsc::UnboundedSender<EditorRequest>,
client: TypedLspClient<ServerState>,
preview: ProjectPreviewState,
) -> ProjectState {
let const_config = &config.const_config;
// Run Export actors before preparing cluster to avoid loss of events
let export = crate::task::ExportTask::new(
client.handle.clone(),
Some(editor_tx.clone()),
config.export(),
);
// Create the compile handler for client consuming results.
let periscope_args = config.compile.periscope_args.clone();
let handle = Arc::new(CompileHandlerImpl {
#[cfg(feature = "preview")]
preview,
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 config.compile.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: Mutex::default(),
});
let default_path = config.compile.entry_resolver.resolve_default();
let entry = config.compile.entry_resolver.resolve(default_path);
let inputs = config.compile.determine_inputs();
let cert_path = config.compile.determine_certification_path();
let package = config.compile.determine_package_opts();
log::info!("ServerState: creating ProjectState, entry: {entry:?}, inputs: {inputs:?}");
// todo: never fail?
let embedded_fonts = Arc::new(LspUniverseBuilder::only_embedded_fonts().unwrap());
let package_registry =
LspUniverseBuilder::resolve_package(cert_path.clone(), Some(&package));
let verse = LspUniverseBuilder::build(entry, inputs, embedded_fonts, package_registry);
// 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 compile_handle = handle.clone();
let compiler = ProjectCompiler::new(
verse,
dep_tx,
CompileServerOpts {
handler: compile_handle,
enable_watch: true,
..Default::default()
},
);
// Delayed Loads fonts
let font_client = client.clone();
let font_resolver = config.compile.determine_fonts();
client.handle.spawn_blocking(move || {
// Refresh fonts
font_client.send_event(LspInterrupt::Font(font_resolver.wait().clone()));
});
ProjectState {
compiler,
preview: Default::default(),
analysis: handle.analysis.clone(),
stats: CompilerQueryStats::default(),
export: handle.export.clone(),
}
}
}
#[derive(Default)]
pub struct ProjectInsStateExt {
pub is_compiling: bool,
@ -109,6 +295,20 @@ impl ProjectState {
}
}
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)
}
}
#[derive(Default, Clone)]
pub struct ProjectPreviewState {
#[cfg(feature = "preview")]

View file

@ -4,35 +4,23 @@ use std::path::{Path, PathBuf};
use std::sync::Arc;
use lsp_types::*;
use parking_lot::Mutex;
use sync_lsp::*;
use task::ExportUserConfig;
use tinymist_project::{EntryResolver, Interrupt, LspCompileSnapshot, ProjectInsId};
use tinymist_query::analysis::{Analysis, PeriscopeProvider};
use tinymist_query::{
CompilerQueryRequest, LocalContext, LspWorldExt, OnExportRequest, ServerInfoResponse,
VersionedDocument,
};
use tinymist_render::PeriscopeRenderer;
use tinymist_project::{EntryResolver, LspCompileSnapshot, ProjectInsId};
use tinymist_query::{LspWorldExt, OnExportRequest, ServerInfoResponse};
use tinymist_std::error::prelude::*;
use tinymist_std::ImmutPath;
use tokio::sync::mpsc;
use typst::diag::FileResult;
use typst::layout::Position as TypstPosition;
use typst::syntax::Source;
use crate::actor::editor::{EditorActor, EditorRequest};
use crate::project::{
update_lock, watch_deps, CompileHandlerImpl, CompileServerOpts, LspInterrupt, LspQuerySnapshot,
ProjectCompiler, ProjectPreviewState, ProjectState, QuerySnapWithStat,
update_lock, LspInterrupt, ProjectPreviewState, ProjectState,
PROJECT_ROUTE_USER_ACTION_PRIORITY,
};
use crate::route::ProjectRouteState;
use crate::state::query::OnEnter;
use crate::stats::CompilerQueryStats;
use crate::state::lsp_query::OnEnter;
use crate::task::{ExportTask, FormatTask, UserActionTask};
use crate::vfs::{Bytes, FileChangeSet, MemoryEvent};
use crate::world::{LspUniverseBuilder, TaskInputs};
use crate::world::TaskInputs;
use crate::{init::*, *};
pub(crate) use futures::Future;
@ -53,10 +41,21 @@ pub(crate) fn as_path_pos(inp: TextDocumentPositionParams) -> (PathBuf, Position
pub struct ServerState {
/// The lsp client
pub client: TypedLspClient<Self>,
// State
/// The project route state.
pub route: ProjectRouteState,
/// The project state.
pub project: ProjectState,
/// The preview state.
#[cfg(feature = "preview")]
pub preview: tool::preview::PreviewState,
/// 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,
// State to synchronize with the client.
/// Whether the server has registered semantic tokens capabilities.
@ -75,21 +74,10 @@ pub struct ServerState {
// Configurations
/// User configuration from the editor.
pub config: Config,
// Resources
/// Source synchronized with client
pub memory_changes: HashMap<Arc<Path>, Source>,
/// The preview state.
#[cfg(feature = "preview")]
pub preview: tool::preview::PreviewState,
/// The diagnostics sender to send diagnostics to `crate::actor::cluster`.
pub editor_tx: mpsc::UnboundedSender<EditorRequest>,
/// 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,
}
/// Getters and the main loop.
@ -126,6 +114,21 @@ impl ServerState {
}
}
/// Gets the const configuration.
pub fn const_config(&self) -> &ConstConfig {
&self.config.const_config
}
/// Gets the compile configuration.
pub fn compile_config(&self) -> &CompileConfig {
&self.config.compile
}
/// Gets the entry resolver.
pub fn entry_resolver(&self) -> &EntryResolver {
&self.compile_config().entry_resolver
}
/// 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:?}");
@ -153,44 +156,7 @@ impl ServerState {
service
}
/// Get the const configuration.
pub fn const_config(&self) -> &ConstConfig {
&self.config.const_config
}
/// Get the compile configuration.
pub fn compile_config(&self) -> &CompileConfig {
&self.config.compile
}
/// Get the entry resolver.
pub fn entry_resolver(&self) -> &EntryResolver {
&self.compile_config().entry_resolver
}
/// Snapshot the compiler thread for tasks
pub fn snapshot(&mut self) -> Result<LspCompileSnapshot> {
self.project.snapshot()
}
/// Snapshot the compiler thread for language queries
pub fn query_snapshot(&mut self) -> Result<LspQuerySnapshot> {
self.project.query_snapshot(None)
}
/// Snapshot the compiler thread for language queries
pub fn query_snapshot_with_stat(
&mut self,
q: &CompilerQueryRequest,
) -> Result<QuerySnapWithStat> {
let name: &'static str = q.into();
let path = q.associated_path();
let stat = self.project.stats.query_stat(path, name);
let snap = self.project.query_snapshot(Some(q))?;
Ok((snap, stat))
}
/// Install handlers to the language server.
/// Installs handlers to the language server.
pub fn install<T: Initializer<S = Self> + AddCommands + 'static>(
provider: LspBuilder<T>,
) -> LspBuilder<T> {
@ -288,6 +254,7 @@ impl ServerState {
provider
}
/// Handles the project interrupts.
fn compile_interrupt<T: Initializer<S = Self>>(
mut state: ServiceState<T, T::S>,
params: LspInterrupt,
@ -306,7 +273,7 @@ impl ServerState {
}
impl ServerState {
/// Get the current server info.
/// Gets the current server info.
pub fn collect_server_info(&mut self) -> QueryFuture {
let dg = self.project.primary_id().to_string();
let api_stats = self.project.stats.report();
@ -333,167 +300,7 @@ impl ServerState {
})
}
/// Restart the primary server.
pub fn restart_primary(&mut self) -> Result<ProjectInsId> {
// todo: hot replacement
#[cfg(feature = "preview")]
self.preview.stop_all();
let watchers = self.preview.watchers.clone();
let editor_tx = self.editor_tx.clone();
let new_project = Self::project(&self.config, editor_tx, self.client.clone(), watchers);
let mut old_project = std::mem::replace(&mut self.project, new_project);
let snapshot = FileChangeSet::new_inserts(
self.memory_changes
.iter()
.map(|(path, content)| {
let content = Bytes::from(content.clone().text().as_bytes());
(path.clone(), FileResult::Ok(content).into())
})
.collect(),
);
self.project
.interrupt(Interrupt::Memory(MemoryEvent::Update(snapshot)));
rayon::spawn(move || {
old_project.stop();
});
Ok(self.project.primary_id().clone())
}
/// Restart the server with the given group.
pub fn restart_dedicate(
&mut self,
dedicate: &str,
entry: Option<ImmutPath>,
) -> Result<ProjectInsId> {
let entry = self.config.compile.entry_resolver.resolve(entry);
self.project.restart_dedicate(dedicate, entry)
}
// pub async fn settle(&mut self) {
// let _ = self.change_entry(None);
// log::info!("TypstActor({}): settle requested", self.handle.diag_group);
// match self.handle.settle().await {
// Ok(()) => log::info!("TypstActor({}): settled",
// self.handle.diag_group), Err(err) => error!(
// "TypstActor({}): failed to settle: {err:#}",
// self.handle.diag_group
// ),
// }
// }
/// Create a fresh [`ProjectState`].
pub fn project(
config: &Config,
editor_tx: tokio::sync::mpsc::UnboundedSender<EditorRequest>,
client: TypedLspClient<ServerState>,
preview: project::ProjectPreviewState,
) -> ProjectState {
let const_config = &config.const_config;
// Run Export actors before preparing cluster to avoid loss of events
let export = ExportTask::new(
client.handle.clone(),
Some(editor_tx.clone()),
config.export(),
);
// Create the compile handler for client consuming results.
let periscope_args = config.compile.periscope_args.clone();
let handle = Arc::new(CompileHandlerImpl {
#[cfg(feature = "preview")]
preview,
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 config.compile.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: Mutex::default(),
});
let default_path = config.compile.entry_resolver.resolve_default();
let entry = config.compile.entry_resolver.resolve(default_path);
let inputs = config.compile.determine_inputs();
let cert_path = config.compile.determine_certification_path();
let package = config.compile.determine_package_opts();
log::info!("ServerState: creating ProjectState, entry: {entry:?}, inputs: {inputs:?}");
// todo: never fail?
let embedded_fonts = Arc::new(LspUniverseBuilder::only_embedded_fonts().unwrap());
let package_registry =
LspUniverseBuilder::resolve_package(cert_path.clone(), Some(&package));
let verse = LspUniverseBuilder::build(entry, inputs, embedded_fonts, package_registry);
// 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 compile_handle = handle.clone();
let compiler = ProjectCompiler::new(
verse,
dep_tx,
CompileServerOpts {
handler: compile_handle,
enable_watch: true,
..Default::default()
},
);
// Delayed Loads fonts
let font_client = client.clone();
let font_resolver = config.compile.determine_fonts();
client.handle.spawn_blocking(move || {
// Refresh fonts
font_client.send_event(LspInterrupt::Font(font_resolver.wait().clone()));
});
ProjectState {
compiler,
preview: Default::default(),
analysis: handle.analysis.clone(),
stats: CompilerQueryStats::default(),
export: handle.export.clone(),
}
}
}
impl ServerState {
pub(crate) fn change_export_config(&mut self, config: ExportUserConfig) {
self.project.export.change_config(config);
}
/// Export the current document.
/// Exports the current document.
pub fn on_export(&mut self, req: OnExportRequest) -> QueryFuture {
let OnExportRequest { path, task, open } = req;
let entry = self.entry_resolver().resolve(Some(path.as_path().into()));
@ -547,20 +354,6 @@ impl ServerState {
}
}
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)
}
}
#[test]
fn test_as_path() {
use reflexo::path::PathClean;