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-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",
|
||||||
|
|
|
@ -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>>),
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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)
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
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 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)
|
||||||
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 enc = $self.const_config.position_encoding;
|
||||||
let snapshot = snapshot.ok_or_else(|| anyhow!("file missing {:?}", path))?;
|
let res = $req.request(&source, enc);
|
||||||
let source = snapshot.content.clone();
|
Ok(CompilerQueryResponse::$method(res))
|
||||||
|
})
|
||||||
let enc = $self.const_config.position_encoding;
|
};
|
||||||
let res = $req.request(&source, enc);
|
|
||||||
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!(),
|
||||||
}
|
}
|
||||||
|
|
|
@ -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."
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue