mirror of
https://github.com/Myriad-Dreamin/tinymist.git
synced 2025-07-19 02:35:00 +00:00
feat: support formatters (#113)
* feat: supports formatter * feat: supports dynamic configuration and typstfmt * fix: test
This commit is contained in:
parent
0114bf4a3b
commit
d4dda9e06f
10 changed files with 663 additions and 216 deletions
471
Cargo.lock
generated
471
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -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-compiler = { version = "0.5.0-rc2" }
|
||||
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-types = { version = "=0.95.0", features = ["proposed"] }
|
||||
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_complete = "4.4"
|
||||
clap_complete_fig = "4.4"
|
||||
clap_complete = "4.5"
|
||||
clap_complete_fig = "4.5"
|
||||
clap_mangen = { version = "0.2.15" }
|
||||
vergen = { version = "8.2.5", features = [
|
||||
"build",
|
||||
|
|
|
@ -109,6 +109,7 @@ pub trait StatefulRequest {
|
|||
|
||||
#[allow(missing_docs)]
|
||||
mod polymorphic {
|
||||
use lsp_types::TextEdit;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::prelude::*;
|
||||
|
@ -150,6 +151,12 @@ mod polymorphic {
|
|||
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)]
|
||||
pub enum FoldRequestFeature {
|
||||
PinnedFirst,
|
||||
|
@ -176,6 +183,7 @@ mod polymorphic {
|
|||
Symbol(SymbolRequest),
|
||||
SemanticTokensFull(SemanticTokensFullRequest),
|
||||
SemanticTokensDelta(SemanticTokensDeltaRequest),
|
||||
Formatting(FormattingRequest),
|
||||
FoldingRange(FoldingRangeRequest),
|
||||
SelectionRange(SelectionRangeRequest),
|
||||
}
|
||||
|
@ -200,6 +208,7 @@ mod polymorphic {
|
|||
CompilerQueryRequest::Symbol(..) => Mergable,
|
||||
CompilerQueryRequest::SemanticTokensFull(..) => ContextFreeUnique,
|
||||
CompilerQueryRequest::SemanticTokensDelta(..) => ContextFreeUnique,
|
||||
CompilerQueryRequest::Formatting(..) => ContextFreeUnique,
|
||||
CompilerQueryRequest::FoldingRange(..) => ContextFreeUnique,
|
||||
CompilerQueryRequest::SelectionRange(..) => ContextFreeUnique,
|
||||
}
|
||||
|
@ -223,6 +232,7 @@ mod polymorphic {
|
|||
CompilerQueryRequest::Symbol(..) => return None,
|
||||
CompilerQueryRequest::SemanticTokensFull(req) => &req.path,
|
||||
CompilerQueryRequest::SemanticTokensDelta(req) => &req.path,
|
||||
CompilerQueryRequest::Formatting(req) => &req.path,
|
||||
CompilerQueryRequest::FoldingRange(req) => &req.path,
|
||||
CompilerQueryRequest::SelectionRange(req) => &req.path,
|
||||
})
|
||||
|
@ -247,6 +257,7 @@ mod polymorphic {
|
|||
Symbol(Option<Vec<SymbolInformation>>),
|
||||
SemanticTokensFull(Option<SemanticTokensResult>),
|
||||
SemanticTokensDelta(Option<SemanticTokensFullDeltaResult>),
|
||||
Formatting(Option<Vec<TextEdit>>),
|
||||
FoldingRange(Option<Vec<FoldingRange>>),
|
||||
SelectionRange(Option<Vec<SelectionRange>>),
|
||||
}
|
||||
|
|
|
@ -42,6 +42,9 @@ typst-render.workspace = true
|
|||
typst-timing.workspace = true
|
||||
typst-assets = { workspace = true, features = ["fonts"] }
|
||||
|
||||
typstyle.workspace = true
|
||||
typstfmt_lib.workspace = true
|
||||
|
||||
typst-ts-core = { workspace = true, default-features = false, features = [
|
||||
"flat-vector",
|
||||
"vector-bbox",
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
//! Bootstrap actors for Tinymist.
|
||||
|
||||
pub mod cluster;
|
||||
mod formatting;
|
||||
pub mod render;
|
||||
pub mod typ_client;
|
||||
pub mod typ_server;
|
||||
|
@ -14,6 +15,7 @@ use typst_ts_compiler::{
|
|||
use typst_ts_core::config::compiler::EntryState;
|
||||
|
||||
use self::{
|
||||
formatting::run_format_thread,
|
||||
render::{ExportActor, ExportConfig},
|
||||
typ_client::{CompileClientActor, CompileDriver, CompileHandler},
|
||||
typ_server::CompileServerActor,
|
||||
|
@ -24,6 +26,8 @@ use crate::{
|
|||
TypstLanguageServer,
|
||||
};
|
||||
|
||||
pub use formatting::{FormattingConfig, FormattingRequest};
|
||||
|
||||
type CompileDriverInner = CompileDriverImpl<LspWorld>;
|
||||
|
||||
impl CompileServer {
|
||||
|
@ -113,4 +117,20 @@ impl TypstLanguageServer {
|
|||
) -> CompileClientActor {
|
||||
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)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
91
crates/tinymist/src/actor/formatting.rs
Normal file
91
crates/tinymist/src/actor/formatting.rs
Normal 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");
|
||||
}
|
|
@ -37,7 +37,7 @@ use crossbeam_channel::select;
|
|||
use crossbeam_channel::Receiver;
|
||||
use futures::future::BoxFuture;
|
||||
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::request::{GotoDeclarationParams, GotoDeclarationResponse, WorkspaceConfiguration};
|
||||
use lsp_types::*;
|
||||
|
@ -60,6 +60,7 @@ use typst_ts_core::{error::prelude::*, ImmutPath};
|
|||
use super::lsp_init::*;
|
||||
use crate::actor::render::ExportConfig;
|
||||
use crate::actor::typ_client::CompileClientActor;
|
||||
use crate::actor::{FormattingConfig, FormattingRequest};
|
||||
use crate::compiler::{CompileServer, CompileServerArgs};
|
||||
use crate::compiler_init::CompilerConstConfig;
|
||||
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 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 NotifyCmdMap = HashMap<&'static str, LspMethod<()>>;
|
||||
type RegularCmdMap = HashMap<&'static str, LspMethod<JsonValue>>;
|
||||
type RegularCmdMap = HashMap<&'static str, LspRawHandler>;
|
||||
|
||||
macro_rules! exec_fn {
|
||||
($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 {
|
||||
($desc: ty, Self::$method: ident) => {
|
||||
(<$desc>::METHOD, {
|
||||
const E: LspMethod<JsonValue> = |this, req| {
|
||||
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
|
||||
let res = this.$method(req)?;
|
||||
let res = serde_json::to_value(res).unwrap(); // todo: soft unwrap
|
||||
Ok(res)
|
||||
let res = this
|
||||
.$method(req)
|
||||
.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
|
||||
})
|
||||
|
@ -152,6 +188,7 @@ pub struct TypstLanguageServer {
|
|||
/// Whether the server is shutting down.
|
||||
pub shutdown_requested: bool,
|
||||
pub sema_tokens_registered: Option<bool>,
|
||||
pub formatter_registered: Option<bool>,
|
||||
|
||||
// Configurations
|
||||
/// User configuration from the editor.
|
||||
|
@ -175,6 +212,7 @@ pub struct TypstLanguageServer {
|
|||
pub primary: CompileServer,
|
||||
pub main: Option<CompileClientActor>,
|
||||
pub tokens_ctx: SemanticTokenContext,
|
||||
pub format_thread: Option<crossbeam_channel::Sender<FormattingRequest>>,
|
||||
}
|
||||
|
||||
/// Getters and the main loop.
|
||||
|
@ -200,6 +238,7 @@ impl TypstLanguageServer {
|
|||
}),
|
||||
shutdown_requested: false,
|
||||
sema_tokens_registered: None,
|
||||
formatter_registered: None,
|
||||
config: Default::default(),
|
||||
const_config: args.const_config,
|
||||
compile_opts: args.compile_opts,
|
||||
|
@ -212,6 +251,7 @@ impl TypstLanguageServer {
|
|||
pinning: false,
|
||||
main: None,
|
||||
tokens_ctx,
|
||||
format_thread: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -238,6 +278,7 @@ impl TypstLanguageServer {
|
|||
request_fn!(SemanticTokensFullDeltaRequest, Self::semantic_tokens_full_delta),
|
||||
request_fn!(DocumentSymbolRequest, Self::document_symbol),
|
||||
// Sync for low latency
|
||||
request_fn_!(Formatting, Self::formatting),
|
||||
request_fn!(SelectionRangeRequest, Self::selection_range),
|
||||
// latency insensitive
|
||||
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 {
|
||||
trace!("setting up to request config change notifications");
|
||||
|
||||
|
@ -394,30 +444,13 @@ impl TypstLanguageServer {
|
|||
return;
|
||||
};
|
||||
|
||||
let result = handler(self, req.params);
|
||||
|
||||
if let Ok(response) = result_to_response(req.id, result) {
|
||||
self.client.respond(response);
|
||||
let res = handler(self, (req.id.clone(), req.params));
|
||||
if matches!(res, Ok(Some(()))) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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(),
|
||||
// ),
|
||||
// },
|
||||
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)
|
||||
if let Ok(response) = result_to_response_(req.id, res) {
|
||||
self.client.respond(response);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -481,6 +514,53 @@ impl TypstLanguageServer {
|
|||
|
||||
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.
|
||||
|
@ -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
|
||||
|
||||
Ok(())
|
||||
|
@ -929,6 +1025,26 @@ impl TypstLanguageServer {
|
|||
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>>> {
|
||||
let path = as_path(params.text_document);
|
||||
let range = params.range;
|
||||
|
@ -998,3 +1114,31 @@ pub fn method_not_found() -> ResponseError {
|
|||
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)
|
||||
}
|
||||
|
|
|
@ -24,15 +24,17 @@ use crate::{
|
|||
// LocationLink[] even if the client does not report the
|
||||
// textDocument.definition.linkSupport capability.
|
||||
|
||||
/// The mode of the experimental formatter.
|
||||
/// The mode of the formatter.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum ExperimentalFormatterMode {
|
||||
/// Disable the experimental formatter.
|
||||
pub enum FormatterMode {
|
||||
/// Disable the formatter.
|
||||
#[default]
|
||||
Disable,
|
||||
/// Enable the experimental formatter.
|
||||
Enable,
|
||||
/// Use `typstyle` formatter.
|
||||
Typstyle,
|
||||
/// Use `typstfmt` formatter.
|
||||
Typstfmt,
|
||||
}
|
||||
|
||||
/// The mode of PDF/SVG/PNG export.
|
||||
|
@ -83,7 +85,7 @@ const CONFIG_ITEMS: &[&str] = &[
|
|||
"exportPdf",
|
||||
"rootPath",
|
||||
"semanticTokens",
|
||||
"experimentalFormatterMode",
|
||||
"formatterMode",
|
||||
"typstExtraArgs",
|
||||
];
|
||||
|
||||
|
@ -95,7 +97,7 @@ pub struct Config {
|
|||
/// Dynamic configuration for semantic tokens.
|
||||
pub semantic_tokens: SemanticTokensMode,
|
||||
/// Dynamic configuration for the experimental formatter.
|
||||
pub formatter: ExperimentalFormatterMode,
|
||||
pub formatter: FormatterMode,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
|
@ -153,8 +155,8 @@ impl Config {
|
|||
}
|
||||
|
||||
let formatter = update
|
||||
.get("experimentalFormatterMode")
|
||||
.map(ExperimentalFormatterMode::deserialize)
|
||||
.get("formatterMode")
|
||||
.map(FormatterMode::deserialize)
|
||||
.and_then(Result::ok);
|
||||
if let Some(formatter) = formatter {
|
||||
self.formatter = formatter;
|
||||
|
@ -354,6 +356,8 @@ impl Init {
|
|||
service.primary.config = config.compile.clone();
|
||||
service.config = config;
|
||||
|
||||
service.run_format_thread();
|
||||
|
||||
let cluster_actor = CompileClusterActor {
|
||||
host: self.host.clone(),
|
||||
diag_rx,
|
||||
|
@ -383,8 +387,11 @@ impl Init {
|
|||
_ => None,
|
||||
};
|
||||
|
||||
// if !cc.doc_fmt_dynamic_registration
|
||||
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))
|
||||
}
|
||||
_ => None,
|
||||
|
@ -480,7 +487,7 @@ mod tests {
|
|||
"exportPdf": "onSave",
|
||||
"rootPath": root_path,
|
||||
"semanticTokens": "enable",
|
||||
"experimentalFormatterMode": "enable",
|
||||
"formatterMode": "typstyle",
|
||||
"typstExtraArgs": ["--root", root_path]
|
||||
});
|
||||
|
||||
|
@ -490,7 +497,7 @@ mod tests {
|
|||
assert_eq!(config.compile.export_pdf, ExportMode::OnSave);
|
||||
assert_eq!(config.compile.root_path, Some(PathBuf::from(root_path)));
|
||||
assert_eq!(config.semantic_tokens, SemanticTokensMode::Enable);
|
||||
assert_eq!(config.formatter, ExperimentalFormatterMode::Enable);
|
||||
assert_eq!(config.formatter, FormatterMode::Typstyle);
|
||||
assert_eq!(
|
||||
config.compile.typst_extra_args,
|
||||
Some(CompileExtraOpts {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
//! Bootstrap actors for Tinymist.
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use ::typst::{diag::FileResult, syntax::Source};
|
||||
use anyhow::anyhow;
|
||||
|
@ -162,16 +162,13 @@ macro_rules! run_query {
|
|||
}
|
||||
|
||||
macro_rules! query_source {
|
||||
($self:ident, $method:ident, $req:expr) => {{
|
||||
let path: ImmutPath = $req.path.clone().into();
|
||||
let snapshot = $self.memory_changes.get(&path);
|
||||
let snapshot = snapshot.ok_or_else(|| anyhow!("file missing {:?}", path))?;
|
||||
let source = snapshot.content.clone();
|
||||
|
||||
($self:ident, $method:ident, $req:expr) => {
|
||||
$self.query_source(&$req.path.clone(), |source| {
|
||||
let enc = $self.const_config.position_encoding;
|
||||
let res = $req.request(&source, enc);
|
||||
Ok(CompilerQueryResponse::$method(res))
|
||||
}};
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! query_tokens_cache {
|
||||
|
@ -201,6 +198,18 @@ macro_rules! query_world {
|
|||
}
|
||||
|
||||
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> {
|
||||
use CompilerQueryRequest::*;
|
||||
|
||||
|
@ -254,6 +263,7 @@ impl TypstLanguageServer {
|
|||
FoldingRange(..)
|
||||
| SelectionRange(..)
|
||||
| SemanticTokensDelta(..)
|
||||
| Formatting(..)
|
||||
| DocumentSymbol(..)
|
||||
| SemanticTokensFull(..) => unreachable!(),
|
||||
}
|
||||
|
|
|
@ -121,18 +121,20 @@
|
|||
"default": "off",
|
||||
"description": "Traces the communication between VS Code and the language server."
|
||||
},
|
||||
"tinymist.experimentalFormatterMode": {
|
||||
"tinymist.formatterMode": {
|
||||
"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",
|
||||
"default": "disable",
|
||||
"enum": [
|
||||
"disable",
|
||||
"enable"
|
||||
"typstyle",
|
||||
"typstfmt"
|
||||
],
|
||||
"enumDescriptions": [
|
||||
"Formatter is not activated.",
|
||||
"Experimental formatter is activated."
|
||||
"Use typstyle formatter.",
|
||||
"Use typstfmt formatter."
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue