feat: support formatters (#113)

* feat: supports formatter

* feat: supports dynamic configuration and typstfmt

* fix: test
This commit is contained in:
Myriad-Dreamin 2024-03-28 16:28:00 +08:00 committed by GitHub
parent 0114bf4a3b
commit d4dda9e06f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 663 additions and 216 deletions

471
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -53,15 +53,17 @@ reflexo = { version = "0.5.0-rc2", default-features = false, features = [
typst-ts-core = { version = "0.5.0-rc2", default-features = false } typst-ts-core = { version = "0.5.0-rc2", default-features = false }
typst-ts-compiler = { version = "0.5.0-rc2" } typst-ts-compiler = { version = "0.5.0-rc2" }
typst-preview = { version = "0.11.3" } typst-preview = { version = "0.11.3" }
typstyle = "0.11.4"
typstfmt_lib = { git = "https://github.com/astrale-sharp/typstfmt", tag = "0.2.7" }
lsp-server = "0.7.6" lsp-server = "0.7.6"
lsp-types = { version = "=0.95.0", features = ["proposed"] } lsp-types = { version = "=0.95.0", features = ["proposed"] }
crossbeam-channel = "0.5.12" crossbeam-channel = "0.5.12"
clap = { version = "4.4", features = ["derive", "env", "unicode", "wrap_help"] } clap = { version = "4.5", features = ["derive", "env", "unicode", "wrap_help"] }
clap_builder = { version = "4", features = ["string"] } clap_builder = { version = "4", features = ["string"] }
clap_complete = "4.4" clap_complete = "4.5"
clap_complete_fig = "4.4" clap_complete_fig = "4.5"
clap_mangen = { version = "0.2.15" } clap_mangen = { version = "0.2.15" }
vergen = { version = "8.2.5", features = [ vergen = { version = "8.2.5", features = [
"build", "build",

View file

@ -109,6 +109,7 @@ pub trait StatefulRequest {
#[allow(missing_docs)] #[allow(missing_docs)]
mod polymorphic { mod polymorphic {
use lsp_types::TextEdit;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::prelude::*; use super::prelude::*;
@ -150,6 +151,12 @@ mod polymorphic {
pub path: PathBuf, pub path: PathBuf,
} }
#[derive(Debug, Clone)]
pub struct FormattingRequest {
/// The path of the document to get semantic tokens for.
pub path: PathBuf,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum FoldRequestFeature { pub enum FoldRequestFeature {
PinnedFirst, PinnedFirst,
@ -176,6 +183,7 @@ mod polymorphic {
Symbol(SymbolRequest), Symbol(SymbolRequest),
SemanticTokensFull(SemanticTokensFullRequest), SemanticTokensFull(SemanticTokensFullRequest),
SemanticTokensDelta(SemanticTokensDeltaRequest), SemanticTokensDelta(SemanticTokensDeltaRequest),
Formatting(FormattingRequest),
FoldingRange(FoldingRangeRequest), FoldingRange(FoldingRangeRequest),
SelectionRange(SelectionRangeRequest), SelectionRange(SelectionRangeRequest),
} }
@ -200,6 +208,7 @@ mod polymorphic {
CompilerQueryRequest::Symbol(..) => Mergable, CompilerQueryRequest::Symbol(..) => Mergable,
CompilerQueryRequest::SemanticTokensFull(..) => ContextFreeUnique, CompilerQueryRequest::SemanticTokensFull(..) => ContextFreeUnique,
CompilerQueryRequest::SemanticTokensDelta(..) => ContextFreeUnique, CompilerQueryRequest::SemanticTokensDelta(..) => ContextFreeUnique,
CompilerQueryRequest::Formatting(..) => ContextFreeUnique,
CompilerQueryRequest::FoldingRange(..) => ContextFreeUnique, CompilerQueryRequest::FoldingRange(..) => ContextFreeUnique,
CompilerQueryRequest::SelectionRange(..) => ContextFreeUnique, CompilerQueryRequest::SelectionRange(..) => ContextFreeUnique,
} }
@ -223,6 +232,7 @@ mod polymorphic {
CompilerQueryRequest::Symbol(..) => return None, CompilerQueryRequest::Symbol(..) => return None,
CompilerQueryRequest::SemanticTokensFull(req) => &req.path, CompilerQueryRequest::SemanticTokensFull(req) => &req.path,
CompilerQueryRequest::SemanticTokensDelta(req) => &req.path, CompilerQueryRequest::SemanticTokensDelta(req) => &req.path,
CompilerQueryRequest::Formatting(req) => &req.path,
CompilerQueryRequest::FoldingRange(req) => &req.path, CompilerQueryRequest::FoldingRange(req) => &req.path,
CompilerQueryRequest::SelectionRange(req) => &req.path, CompilerQueryRequest::SelectionRange(req) => &req.path,
}) })
@ -247,6 +257,7 @@ mod polymorphic {
Symbol(Option<Vec<SymbolInformation>>), Symbol(Option<Vec<SymbolInformation>>),
SemanticTokensFull(Option<SemanticTokensResult>), SemanticTokensFull(Option<SemanticTokensResult>),
SemanticTokensDelta(Option<SemanticTokensFullDeltaResult>), SemanticTokensDelta(Option<SemanticTokensFullDeltaResult>),
Formatting(Option<Vec<TextEdit>>),
FoldingRange(Option<Vec<FoldingRange>>), FoldingRange(Option<Vec<FoldingRange>>),
SelectionRange(Option<Vec<SelectionRange>>), SelectionRange(Option<Vec<SelectionRange>>),
} }

View file

@ -42,6 +42,9 @@ typst-render.workspace = true
typst-timing.workspace = true typst-timing.workspace = true
typst-assets = { workspace = true, features = ["fonts"] } typst-assets = { workspace = true, features = ["fonts"] }
typstyle.workspace = true
typstfmt_lib.workspace = true
typst-ts-core = { workspace = true, default-features = false, features = [ typst-ts-core = { workspace = true, default-features = false, features = [
"flat-vector", "flat-vector",
"vector-bbox", "vector-bbox",

View file

@ -1,6 +1,7 @@
//! Bootstrap actors for Tinymist. //! Bootstrap actors for Tinymist.
pub mod cluster; pub mod cluster;
mod formatting;
pub mod render; pub mod render;
pub mod typ_client; pub mod typ_client;
pub mod typ_server; pub mod typ_server;
@ -14,6 +15,7 @@ use typst_ts_compiler::{
use typst_ts_core::config::compiler::EntryState; use typst_ts_core::config::compiler::EntryState;
use self::{ use self::{
formatting::run_format_thread,
render::{ExportActor, ExportConfig}, render::{ExportActor, ExportConfig},
typ_client::{CompileClientActor, CompileDriver, CompileHandler}, typ_client::{CompileClientActor, CompileDriver, CompileHandler},
typ_server::CompileServerActor, typ_server::CompileServerActor,
@ -24,6 +26,8 @@ use crate::{
TypstLanguageServer, TypstLanguageServer,
}; };
pub use formatting::{FormattingConfig, FormattingRequest};
type CompileDriverInner = CompileDriverImpl<LspWorld>; type CompileDriverInner = CompileDriverImpl<LspWorld>;
impl CompileServer { impl CompileServer {
@ -113,4 +117,20 @@ impl TypstLanguageServer {
) -> CompileClientActor { ) -> CompileClientActor {
self.primary.server(diag_group, entry, inputs) self.primary.server(diag_group, entry, inputs)
} }
pub fn run_format_thread(&mut self) {
if self.format_thread.is_some() {
log::error!("formatting thread already started");
return;
}
let (tx_req, rx_req) = crossbeam_channel::unbounded();
self.format_thread = Some(tx_req.clone());
let client = self.client.clone();
let mode = self.config.formatter;
std::thread::spawn(move || {
run_format_thread(FormattingConfig { mode, width: 120 }, rx_req, client)
});
}
} }

View file

@ -0,0 +1,91 @@
use lsp_server::RequestId;
use lsp_types::{Position, Range, TextEdit};
use typst::syntax::Source;
use crate::{result_to_response_, FormatterMode, LspHost, LspResult, TypstLanguageServer};
#[derive(Debug, Clone)]
pub struct FormattingConfig {
pub mode: FormatterMode,
pub width: u32,
}
pub enum FormattingRequest {
ChangeConfig(FormattingConfig),
Formatting((RequestId, Source)),
}
pub fn run_format_thread(
init_c: FormattingConfig,
rx_req: crossbeam_channel::Receiver<FormattingRequest>,
client: LspHost<TypstLanguageServer>,
) {
type FmtFn = Box<dyn Fn(Source) -> LspResult<Option<Vec<TextEdit>>>>;
let compile = |c: FormattingConfig| -> FmtFn {
log::info!("formatting thread with config: {c:#?}");
match c.mode {
FormatterMode::Typstyle => {
let cw = c.width as usize;
let f: FmtFn = Box::new(move |e: Source| {
let res = typstyle_core::pretty_print(e.text(), cw);
Ok(Some(vec![TextEdit {
new_text: res,
range: Range::new(
Position {
line: 0,
character: 0,
},
Position {
line: u32::MAX,
character: u32::MAX,
},
),
}]))
});
f
}
FormatterMode::Typstfmt => {
let config = typstfmt_lib::Config {
max_line_length: 120,
..typstfmt_lib::Config::default()
};
let f: FmtFn = Box::new(move |e: Source| {
let res = typstfmt_lib::format(e.text(), config);
Ok(Some(vec![TextEdit {
new_text: res,
range: Range::new(
Position {
line: 0,
character: 0,
},
Position {
line: u32::MAX,
character: u32::MAX,
},
),
}]))
});
f
}
FormatterMode::Disable => {
let f: FmtFn = Box::new(|_| Ok(None));
f
}
}
};
let mut f: FmtFn = compile(init_c);
while let Ok(req) = rx_req.recv() {
match req {
FormattingRequest::ChangeConfig(c) => f = compile(c),
FormattingRequest::Formatting((id, source)) => {
let res = f(source);
if let Ok(response) = result_to_response_(id, res) {
client.respond(response);
}
}
}
}
log::info!("formatting thread did shut down");
}

View file

@ -37,7 +37,7 @@ use crossbeam_channel::select;
use crossbeam_channel::Receiver; use crossbeam_channel::Receiver;
use futures::future::BoxFuture; use futures::future::BoxFuture;
use log::{error, info, trace, warn}; use log::{error, info, trace, warn};
use lsp_server::{ErrorCode, Message, Notification, Request, ResponseError}; use lsp_server::{ErrorCode, Message, Notification, Request, RequestId, ResponseError};
use lsp_types::notification::Notification as NotificationTrait; use lsp_types::notification::Notification as NotificationTrait;
use lsp_types::request::{GotoDeclarationParams, GotoDeclarationResponse, WorkspaceConfiguration}; use lsp_types::request::{GotoDeclarationParams, GotoDeclarationResponse, WorkspaceConfiguration};
use lsp_types::*; use lsp_types::*;
@ -60,6 +60,7 @@ use typst_ts_core::{error::prelude::*, ImmutPath};
use super::lsp_init::*; use super::lsp_init::*;
use crate::actor::render::ExportConfig; use crate::actor::render::ExportConfig;
use crate::actor::typ_client::CompileClientActor; use crate::actor::typ_client::CompileClientActor;
use crate::actor::{FormattingConfig, FormattingRequest};
use crate::compiler::{CompileServer, CompileServerArgs}; use crate::compiler::{CompileServer, CompileServerArgs};
use crate::compiler_init::CompilerConstConfig; use crate::compiler_init::CompilerConstConfig;
use crate::harness::{InitializedLspDriver, LspHost}; use crate::harness::{InitializedLspDriver, LspHost};
@ -83,14 +84,20 @@ impl fmt::Display for Event {
} }
} }
struct Cancelled; pub(crate) struct Cancelled;
type LspMethod<Res> = fn(srv: &mut TypstLanguageServer, args: JsonValue) -> LspResult<Res>; type LspMethod<Res> = fn(srv: &mut TypstLanguageServer, args: JsonValue) -> LspResult<Res>;
type LspHandler<Req, Res> = fn(srv: &mut TypstLanguageServer, args: Req) -> LspResult<Res>; type LspHandler<Req, Res> = fn(srv: &mut TypstLanguageServer, args: Req) -> LspResult<Res>;
/// Returns Ok(Some()) -> Already responded
/// Returns Ok(None) -> Need to respond none
/// Returns Err(..) -> Need t o respond error
type LspRawHandler =
fn(srv: &mut TypstLanguageServer, args: (RequestId, JsonValue)) -> LspResult<Option<()>>;
type ExecuteCmdMap = HashMap<&'static str, LspHandler<Vec<JsonValue>, JsonValue>>; type ExecuteCmdMap = HashMap<&'static str, LspHandler<Vec<JsonValue>, JsonValue>>;
type NotifyCmdMap = HashMap<&'static str, LspMethod<()>>; type NotifyCmdMap = HashMap<&'static str, LspMethod<()>>;
type RegularCmdMap = HashMap<&'static str, LspMethod<JsonValue>>; type RegularCmdMap = HashMap<&'static str, LspRawHandler>;
macro_rules! exec_fn { macro_rules! exec_fn {
($ty: ty, Self::$method: ident, $($arg_key:ident),+ $(,)?) => {{ ($ty: ty, Self::$method: ident, $($arg_key:ident),+ $(,)?) => {{
@ -99,15 +106,44 @@ macro_rules! exec_fn {
}}; }};
} }
macro_rules! request_fn_ {
($desc: ty, Self::$method: ident) => {
(<$desc>::METHOD, {
const E: LspRawHandler = |this, (req_id, req)| {
let req: <$desc as lsp_types::request::Request>::Params =
serde_json::from_value(req).unwrap(); // todo: soft unwrap
this.$method(req_id, req)
};
E
})
};
}
macro_rules! request_fn { macro_rules! request_fn {
($desc: ty, Self::$method: ident) => { ($desc: ty, Self::$method: ident) => {
(<$desc>::METHOD, { (<$desc>::METHOD, {
const E: LspMethod<JsonValue> = |this, req| { const E: LspRawHandler = |this, (req_id, req)| {
let req: <$desc as lsp_types::request::Request>::Params = let req: <$desc as lsp_types::request::Request>::Params =
serde_json::from_value(req).unwrap(); // todo: soft unwrap serde_json::from_value(req).unwrap(); // todo: soft unwrap
let res = this.$method(req)?; let res = this
let res = serde_json::to_value(res).unwrap(); // todo: soft unwrap .$method(req)
Ok(res) .map(|res| serde_json::to_value(res).unwrap()); // todo: soft unwrap
if let Ok(response) = result_to_response(req_id, res) {
this.client.respond(response);
}
// todo: cancellation
// Err(e) => match e.downcast::<Cancelled>() {
// Ok(cancelled) => return Err(cancelled),
// Err(e) => lsp_server::Response::new_err(
// id,
// lsp_server::ErrorCode::InternalError as i32,
// e.to_string(),
// ),
// },
Ok(Some(()))
}; };
E E
}) })
@ -152,6 +188,7 @@ pub struct TypstLanguageServer {
/// Whether the server is shutting down. /// Whether the server is shutting down.
pub shutdown_requested: bool, pub shutdown_requested: bool,
pub sema_tokens_registered: Option<bool>, pub sema_tokens_registered: Option<bool>,
pub formatter_registered: Option<bool>,
// Configurations // Configurations
/// User configuration from the editor. /// User configuration from the editor.
@ -175,6 +212,7 @@ pub struct TypstLanguageServer {
pub primary: CompileServer, pub primary: CompileServer,
pub main: Option<CompileClientActor>, pub main: Option<CompileClientActor>,
pub tokens_ctx: SemanticTokenContext, pub tokens_ctx: SemanticTokenContext,
pub format_thread: Option<crossbeam_channel::Sender<FormattingRequest>>,
} }
/// Getters and the main loop. /// Getters and the main loop.
@ -200,6 +238,7 @@ impl TypstLanguageServer {
}), }),
shutdown_requested: false, shutdown_requested: false,
sema_tokens_registered: None, sema_tokens_registered: None,
formatter_registered: None,
config: Default::default(), config: Default::default(),
const_config: args.const_config, const_config: args.const_config,
compile_opts: args.compile_opts, compile_opts: args.compile_opts,
@ -212,6 +251,7 @@ impl TypstLanguageServer {
pinning: false, pinning: false,
main: None, main: None,
tokens_ctx, tokens_ctx,
format_thread: None,
} }
} }
@ -238,6 +278,7 @@ impl TypstLanguageServer {
request_fn!(SemanticTokensFullDeltaRequest, Self::semantic_tokens_full_delta), request_fn!(SemanticTokensFullDeltaRequest, Self::semantic_tokens_full_delta),
request_fn!(DocumentSymbolRequest, Self::document_symbol), request_fn!(DocumentSymbolRequest, Self::document_symbol),
// Sync for low latency // Sync for low latency
request_fn_!(Formatting, Self::formatting),
request_fn!(SelectionRangeRequest, Self::selection_range), request_fn!(SelectionRangeRequest, Self::selection_range),
// latency insensitive // latency insensitive
request_fn!(InlayHintRequest, Self::inlay_hint), request_fn!(InlayHintRequest, Self::inlay_hint),
@ -287,6 +328,15 @@ impl InitializedLspDriver for TypstLanguageServer {
} }
} }
if self.const_config().doc_fmt_dynamic_registration
&& self.config.formatter != FormatterMode::Disable
{
let err = self.react_formatter_changes(true);
if let Err(err) = err {
error!("could not register formatter for initialization: {err}");
}
}
if self.const_config().cfg_change_registration { if self.const_config().cfg_change_registration {
trace!("setting up to request config change notifications"); trace!("setting up to request config change notifications");
@ -394,30 +444,13 @@ impl TypstLanguageServer {
return; return;
}; };
let result = handler(self, req.params); let res = handler(self, (req.id.clone(), req.params));
if matches!(res, Ok(Some(()))) {
if let Ok(response) = result_to_response(req.id, result) { return;
self.client.respond(response);
} }
// todo: cancellation if let Ok(response) = result_to_response_(req.id, res) {
// Err(e) => match e.downcast::<Cancelled>() { self.client.respond(response);
// Ok(cancelled) => return Err(cancelled),
// Err(e) => lsp_server::Response::new_err(
// id,
// lsp_server::ErrorCode::InternalError as i32,
// e.to_string(),
// ),
// },
fn result_to_response(
id: lsp_server::RequestId,
result: Result<JsonValue, ResponseError>,
) -> Result<lsp_server::Response, Cancelled> {
let res = match result {
Ok(resp) => lsp_server::Response::new_ok(id, resp),
Err(e) => lsp_server::Response::new_err(id, e.code, e.message),
};
Ok(res)
} }
} }
@ -481,6 +514,53 @@ impl TypstLanguageServer {
res res
} }
fn react_formatter_changes(&mut self, enable: bool) -> anyhow::Result<()> {
if !self.const_config().doc_fmt_dynamic_registration {
trace!("skip dynamic register formatter by config");
return Ok(());
}
const FORMATTING_REGISTRATION_ID: &str = "formatting";
const DOCUMENT_FORMATTING_METHOD_ID: &str = "textDocument/formatting";
pub fn get_formatting_registration() -> Registration {
Registration {
id: FORMATTING_REGISTRATION_ID.to_owned(),
method: DOCUMENT_FORMATTING_METHOD_ID.to_owned(),
register_options: None,
}
}
pub fn get_formatting_unregistration() -> Unregistration {
Unregistration {
id: FORMATTING_REGISTRATION_ID.to_owned(),
method: DOCUMENT_FORMATTING_METHOD_ID.to_owned(),
}
}
let res = match (enable, self.formatter_registered) {
(true, None | Some(false)) => {
trace!("registering formatter");
self.client
.register_capability(vec![get_formatting_registration()])
.context("could not register formatter")
}
(false, Some(true)) => {
trace!("unregistering formatter");
self.client
.unregister_capability(vec![get_formatting_unregistration()])
.context("could not unregister formatter")
}
(true, Some(true)) | (false, None | Some(false)) => Ok(()),
};
if res.is_ok() {
self.formatter_registered = Some(enable);
}
res
}
} }
/// Trait implemented by language server backends. /// Trait implemented by language server backends.
@ -823,6 +903,22 @@ impl TypstLanguageServer {
} }
} }
if config.formatter != self.config.formatter {
let err = self.react_formatter_changes(self.config.formatter != FormatterMode::Disable);
if let Err(err) = err {
error!("could not change formatter config: {err}");
}
if let Some(f) = &self.format_thread {
let err = f.send(FormattingRequest::ChangeConfig(FormattingConfig {
mode: self.config.formatter,
width: 120,
}));
if let Err(err) = err {
error!("could not change formatter config: {err}");
}
}
}
// todo: watch changes of the root path // todo: watch changes of the root path
Ok(()) Ok(())
@ -929,6 +1025,26 @@ impl TypstLanguageServer {
run_query!(self.SemanticTokensDelta(path, previous_result_id)) run_query!(self.SemanticTokensDelta(path, previous_result_id))
} }
fn formatting(
&self,
req_id: RequestId,
params: DocumentFormattingParams,
) -> LspResult<Option<()>> {
if matches!(self.config.formatter, FormatterMode::Disable) {
return Ok(None);
}
let path = as_path(params.text_document);
self.query_source(&path, |source| {
if let Some(f) = &self.format_thread {
f.send(FormattingRequest::Formatting((req_id, source.clone())))?;
}
Ok(Some(()))
})
.map_err(|e| internal_error(format!("could not format document: {e}")))
}
fn inlay_hint(&self, params: InlayHintParams) -> LspResult<Option<Vec<InlayHint>>> { fn inlay_hint(&self, params: InlayHintParams) -> LspResult<Option<Vec<InlayHint>>> {
let path = as_path(params.text_document); let path = as_path(params.text_document);
let range = params.range; let range = params.range;
@ -998,3 +1114,31 @@ pub fn method_not_found() -> ResponseError {
data: None, data: None,
} }
} }
pub(crate) fn result_to_response_<T: Serialize>(
id: lsp_server::RequestId,
result: Result<T, ResponseError>,
) -> Result<lsp_server::Response, Cancelled> {
let res = match result {
Ok(resp) => {
let resp = serde_json::to_value(resp);
match resp {
Ok(resp) => lsp_server::Response::new_ok(id, resp),
Err(e) => return result_to_response(id, Err(internal_error(e.to_string()))),
}
}
Err(e) => lsp_server::Response::new_err(id, e.code, e.message),
};
Ok(res)
}
fn result_to_response(
id: lsp_server::RequestId,
result: Result<JsonValue, ResponseError>,
) -> Result<lsp_server::Response, Cancelled> {
let res = match result {
Ok(resp) => lsp_server::Response::new_ok(id, resp),
Err(e) => lsp_server::Response::new_err(id, e.code, e.message),
};
Ok(res)
}

View file

@ -24,15 +24,17 @@ use crate::{
// LocationLink[] even if the client does not report the // LocationLink[] even if the client does not report the
// textDocument.definition.linkSupport capability. // textDocument.definition.linkSupport capability.
/// The mode of the experimental formatter. /// The mode of the formatter.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub enum ExperimentalFormatterMode { pub enum FormatterMode {
/// Disable the experimental formatter. /// Disable the formatter.
#[default] #[default]
Disable, Disable,
/// Enable the experimental formatter. /// Use `typstyle` formatter.
Enable, Typstyle,
/// Use `typstfmt` formatter.
Typstfmt,
} }
/// The mode of PDF/SVG/PNG export. /// The mode of PDF/SVG/PNG export.
@ -83,7 +85,7 @@ const CONFIG_ITEMS: &[&str] = &[
"exportPdf", "exportPdf",
"rootPath", "rootPath",
"semanticTokens", "semanticTokens",
"experimentalFormatterMode", "formatterMode",
"typstExtraArgs", "typstExtraArgs",
]; ];
@ -95,7 +97,7 @@ pub struct Config {
/// Dynamic configuration for semantic tokens. /// Dynamic configuration for semantic tokens.
pub semantic_tokens: SemanticTokensMode, pub semantic_tokens: SemanticTokensMode,
/// Dynamic configuration for the experimental formatter. /// Dynamic configuration for the experimental formatter.
pub formatter: ExperimentalFormatterMode, pub formatter: FormatterMode,
} }
impl Config { impl Config {
@ -153,8 +155,8 @@ impl Config {
} }
let formatter = update let formatter = update
.get("experimentalFormatterMode") .get("formatterMode")
.map(ExperimentalFormatterMode::deserialize) .map(FormatterMode::deserialize)
.and_then(Result::ok); .and_then(Result::ok);
if let Some(formatter) = formatter { if let Some(formatter) = formatter {
self.formatter = formatter; self.formatter = formatter;
@ -354,6 +356,8 @@ impl Init {
service.primary.config = config.compile.clone(); service.primary.config = config.compile.clone();
service.config = config; service.config = config;
service.run_format_thread();
let cluster_actor = CompileClusterActor { let cluster_actor = CompileClusterActor {
host: self.host.clone(), host: self.host.clone(),
diag_rx, diag_rx,
@ -383,8 +387,11 @@ impl Init {
_ => None, _ => None,
}; };
// if !cc.doc_fmt_dynamic_registration
let document_formatting_provider = match service.config.formatter { let document_formatting_provider = match service.config.formatter {
ExperimentalFormatterMode::Enable if !cc.doc_fmt_dynamic_registration => { FormatterMode::Typstyle | FormatterMode::Typstfmt
if !cc.doc_fmt_dynamic_registration =>
{
Some(OneOf::Left(true)) Some(OneOf::Left(true))
} }
_ => None, _ => None,
@ -480,7 +487,7 @@ mod tests {
"exportPdf": "onSave", "exportPdf": "onSave",
"rootPath": root_path, "rootPath": root_path,
"semanticTokens": "enable", "semanticTokens": "enable",
"experimentalFormatterMode": "enable", "formatterMode": "typstyle",
"typstExtraArgs": ["--root", root_path] "typstExtraArgs": ["--root", root_path]
}); });
@ -490,7 +497,7 @@ mod tests {
assert_eq!(config.compile.export_pdf, ExportMode::OnSave); assert_eq!(config.compile.export_pdf, ExportMode::OnSave);
assert_eq!(config.compile.root_path, Some(PathBuf::from(root_path))); assert_eq!(config.compile.root_path, Some(PathBuf::from(root_path)));
assert_eq!(config.semantic_tokens, SemanticTokensMode::Enable); assert_eq!(config.semantic_tokens, SemanticTokensMode::Enable);
assert_eq!(config.formatter, ExperimentalFormatterMode::Enable); assert_eq!(config.formatter, FormatterMode::Typstyle);
assert_eq!( assert_eq!(
config.compile.typst_extra_args, config.compile.typst_extra_args,
Some(CompileExtraOpts { Some(CompileExtraOpts {

View file

@ -1,6 +1,6 @@
//! Bootstrap actors for Tinymist. //! Bootstrap actors for Tinymist.
use std::path::PathBuf; use std::path::{Path, PathBuf};
use ::typst::{diag::FileResult, syntax::Source}; use ::typst::{diag::FileResult, syntax::Source};
use anyhow::anyhow; use anyhow::anyhow;
@ -162,16 +162,13 @@ macro_rules! run_query {
} }
macro_rules! query_source { macro_rules! query_source {
($self:ident, $method:ident, $req:expr) => {{ ($self:ident, $method:ident, $req:expr) => {
let path: ImmutPath = $req.path.clone().into(); $self.query_source(&$req.path.clone(), |source| {
let snapshot = $self.memory_changes.get(&path);
let snapshot = snapshot.ok_or_else(|| anyhow!("file missing {:?}", path))?;
let source = snapshot.content.clone();
let enc = $self.const_config.position_encoding; let enc = $self.const_config.position_encoding;
let res = $req.request(&source, enc); let res = $req.request(&source, enc);
Ok(CompilerQueryResponse::$method(res)) Ok(CompilerQueryResponse::$method(res))
}}; })
};
} }
macro_rules! query_tokens_cache { macro_rules! query_tokens_cache {
@ -201,6 +198,18 @@ macro_rules! query_world {
} }
impl TypstLanguageServer { impl TypstLanguageServer {
pub fn query_source<T>(
&self,
p: &Path,
f: impl FnOnce(Source) -> anyhow::Result<T>,
) -> anyhow::Result<T> {
let path: ImmutPath = p.into();
let snapshot = self.memory_changes.get(&path);
let snapshot = snapshot.ok_or_else(|| anyhow!("file missing {:?}", path))?;
let source = snapshot.content.clone();
f(source)
}
pub fn query(&self, query: CompilerQueryRequest) -> anyhow::Result<CompilerQueryResponse> { pub fn query(&self, query: CompilerQueryRequest) -> anyhow::Result<CompilerQueryResponse> {
use CompilerQueryRequest::*; use CompilerQueryRequest::*;
@ -254,6 +263,7 @@ impl TypstLanguageServer {
FoldingRange(..) FoldingRange(..)
| SelectionRange(..) | SelectionRange(..)
| SemanticTokensDelta(..) | SemanticTokensDelta(..)
| Formatting(..)
| DocumentSymbol(..) | DocumentSymbol(..)
| SemanticTokensFull(..) => unreachable!(), | SemanticTokensFull(..) => unreachable!(),
} }

View file

@ -121,18 +121,20 @@
"default": "off", "default": "off",
"description": "Traces the communication between VS Code and the language server." "description": "Traces the communication between VS Code and the language server."
}, },
"tinymist.experimentalFormatterMode": { "tinymist.formatterMode": {
"title": "Enable Experimental Formatter", "title": "Enable Experimental Formatter",
"description": "The extension can format Typst files using typstfmt (experimental).", "description": "The extension can format Typst files using typstfmt or typstyle.",
"type": "string", "type": "string",
"default": "disable", "default": "disable",
"enum": [ "enum": [
"disable", "disable",
"enable" "typstyle",
"typstfmt"
], ],
"enumDescriptions": [ "enumDescriptions": [
"Formatter is not activated.", "Formatter is not activated.",
"Experimental formatter is activated." "Use typstyle formatter.",
"Use typstfmt formatter."
] ]
} }
} }