mirror of
https://github.com/Myriad-Dreamin/tinymist.git
synced 2025-08-03 17:58:17 +00:00
feat: downgrade some config errors and show warnings (#1538)
This commit is contained in:
parent
9b9a674118
commit
13fb22f4fa
5 changed files with 163 additions and 45 deletions
|
@ -1,5 +1,5 @@
|
|||
mod takable;
|
||||
use std::{path::Path, sync::Arc};
|
||||
use std::{borrow::Cow, path::Path, sync::Arc};
|
||||
|
||||
pub use takable::*;
|
||||
|
||||
|
@ -23,6 +23,8 @@ pub type ImmutStr = Arc<str>;
|
|||
pub type ImmutBytes = Arc<[u8]>;
|
||||
/// An immutable path.
|
||||
pub type ImmutPath = Arc<Path>;
|
||||
/// A copy-on-write static string.
|
||||
pub type CowStr = Cow<'static, str>;
|
||||
|
||||
/// A trait for converting an `Arc<T>` into `Self`.
|
||||
pub trait FromArc<T> {
|
||||
|
|
|
@ -7,6 +7,7 @@ use itertools::Itertools;
|
|||
use lsp_types::*;
|
||||
use once_cell::sync::{Lazy, OnceCell};
|
||||
use reflexo::error::IgnoreLogging;
|
||||
use reflexo::CowStr;
|
||||
use reflexo_typst::{ImmutPath, TypstDict};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Map, Value as JsonValue};
|
||||
|
@ -113,6 +114,9 @@ pub struct Config {
|
|||
pub formatter_print_width: Option<u32>,
|
||||
/// Sets the indent size (using space) for the formatter.
|
||||
pub formatter_indent_size: Option<u32>,
|
||||
|
||||
/// The warnings during configuration update.
|
||||
pub warnings: Vec<CowStr>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
|
@ -263,24 +267,40 @@ impl Config {
|
|||
serde_json::to_string(update).unwrap_or_else(|e| e.to_string())
|
||||
);
|
||||
|
||||
macro_rules! assign_config {
|
||||
($( $field_path:ident ).+ := $bind:literal?: $ty:ty) => {
|
||||
let v = try_deserialize::<$ty>(update, $bind);
|
||||
self.$($field_path).+ = v.unwrap_or_default();
|
||||
};
|
||||
($( $field_path:ident ).+ := $bind:literal: $ty:ty = $default_value:expr) => {
|
||||
let v = try_deserialize::<$ty>(update, $bind);
|
||||
self.$($field_path).+ = v.unwrap_or_else(|| $default_value);
|
||||
self.warnings.clear();
|
||||
|
||||
macro_rules! try_deserialize {
|
||||
($ty:ty, $key:expr) => {
|
||||
update.get($key).and_then(|v| {
|
||||
<$ty>::deserialize(v)
|
||||
.inspect_err(|err| {
|
||||
// Only ignore null returns. Some editors may send null values when
|
||||
// the configuration is not set, e.g. Zed.
|
||||
if v.is_null() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.warnings.push(tinymist_l10n::t!(
|
||||
"tinymist.config.deserializeError",
|
||||
"failed to deserialize \"{key}\": {err}",
|
||||
key = $key.debug_l10n(),
|
||||
err = err.debug_l10n(),
|
||||
));
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
fn try_deserialize<T: serde::de::DeserializeOwned>(
|
||||
map: &Map<String, JsonValue>,
|
||||
key: &str,
|
||||
) -> Option<T> {
|
||||
T::deserialize(map.get(key)?)
|
||||
.inspect_err(|e| log::warn!("failed to deserialize {key:?}: {e}"))
|
||||
.ok()
|
||||
macro_rules! assign_config {
|
||||
($( $field_path:ident ).+ := $bind:literal?: $ty:ty) => {
|
||||
let v = try_deserialize!($ty, $bind);
|
||||
self.$($field_path).+ = v.unwrap_or_default();
|
||||
};
|
||||
($( $field_path:ident ).+ := $bind:literal: $ty:ty = $default_value:expr) => {
|
||||
let v = try_deserialize!($ty, $bind);
|
||||
self.$($field_path).+ = v.unwrap_or_else(|| $default_value);
|
||||
};
|
||||
}
|
||||
|
||||
assign_config!(color_theme := "colorTheme"?: Option<String>);
|
||||
|
@ -306,11 +326,13 @@ impl Config {
|
|||
Some("enable") => true,
|
||||
Some("disable") | None => false,
|
||||
Some(value) => {
|
||||
tinymist_l10n::bail!(
|
||||
self.warnings.push(tinymist_l10n::t!(
|
||||
"tinymist.config.badCompileStatus",
|
||||
"compileStatus must be either `\"enable\"` or `\"disable\"`, got {value}",
|
||||
value = value.debug_l10n(),
|
||||
);
|
||||
));
|
||||
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -321,11 +343,12 @@ impl Config {
|
|||
Some(periscope_args) => match serde_json::from_value(periscope_args.clone()) {
|
||||
Ok(args) => Some(args),
|
||||
Err(err) => {
|
||||
tinymist_l10n::bail!(
|
||||
self.warnings.push(tinymist_l10n::t!(
|
||||
"tinymist.config.badHoverPeriscope",
|
||||
"failed to parse hoverPeriscope: {err}",
|
||||
err = err.debug_l10n(),
|
||||
);
|
||||
));
|
||||
None
|
||||
}
|
||||
},
|
||||
};
|
||||
|
@ -335,9 +358,9 @@ impl Config {
|
|||
}
|
||||
}
|
||||
|
||||
fn invalid_extra_args(args: &impl fmt::Debug, err: impl std::error::Error) -> Result<()> {
|
||||
fn invalid_extra_args(args: &impl fmt::Debug, err: impl std::error::Error) -> CowStr {
|
||||
log::warn!("failed to parse typstExtraArgs: {err}, args: {args:?}");
|
||||
tinymist_l10n::bail!(
|
||||
tinymist_l10n::t!(
|
||||
"tinymist.config.badTypstExtraArgs",
|
||||
"failed to parse typstExtraArgs: {err}, args: {args}",
|
||||
err = err.debug_l10n(),
|
||||
|
@ -349,16 +372,35 @@ impl Config {
|
|||
let raw_args = || update.get("typstExtraArgs");
|
||||
let typst_args: Vec<String> = match raw_args().cloned().map(serde_json::from_value) {
|
||||
Some(Ok(args)) => args,
|
||||
Some(Err(err)) => return invalid_extra_args(&raw_args(), err),
|
||||
// Even if the list is none, it should be parsed since we have env vars to retrieve.
|
||||
None => Vec::new(),
|
||||
};
|
||||
Some(Err(err)) => {
|
||||
self.warnings.push(invalid_extra_args(&raw_args(), err));
|
||||
None
|
||||
}
|
||||
// Even if the list is none, it should be parsed since we have env vars to
|
||||
// retrieve.
|
||||
None => None,
|
||||
}
|
||||
.unwrap_or_default();
|
||||
let empty_typst_args = typst_args.is_empty();
|
||||
|
||||
let args = match CompileOnceArgs::try_parse_from(
|
||||
Some("typst-cli".to_owned()).into_iter().chain(typst_args),
|
||||
) {
|
||||
Ok(args) => args,
|
||||
Err(e) => return invalid_extra_args(&raw_args(), e),
|
||||
Err(err) => {
|
||||
self.warnings.push(invalid_extra_args(&raw_args(), err));
|
||||
|
||||
if empty_typst_args {
|
||||
CompileOnceArgs::default()
|
||||
} else {
|
||||
// Still try to parse the arguments to get the environment variables.
|
||||
CompileOnceArgs::try_parse_from(Some("typst-cli".to_owned()))
|
||||
.inspect_err(|err| {
|
||||
log::error!("failed to make default typstExtraArgs: {err}");
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// todo: the command.root may be not absolute
|
||||
|
@ -796,6 +838,11 @@ mod tests {
|
|||
temp_env::with_vars_unset(Vec::<String>::new(), || config.update(update))
|
||||
}
|
||||
|
||||
fn good_config(config: &mut Config, update: &JsonValue) {
|
||||
update_config(config, update).expect("not good");
|
||||
assert!(config.warnings.is_empty(), "{:?}", config.warnings);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_encoding() {
|
||||
let cc = ConstConfig::default();
|
||||
|
@ -817,7 +864,7 @@ mod tests {
|
|||
"typstExtraArgs": ["--root", root_path]
|
||||
});
|
||||
|
||||
update_config(&mut config, &update).unwrap();
|
||||
good_config(&mut config, &update);
|
||||
|
||||
// Nix specifies this environment variable when testing.
|
||||
let has_source_date_epoch = std::env::var("SOURCE_DATE_EPOCH").is_ok();
|
||||
|
@ -856,7 +903,7 @@ mod tests {
|
|||
}
|
||||
});
|
||||
|
||||
update_config(&mut config, &update).unwrap();
|
||||
good_config(&mut config, &update);
|
||||
|
||||
assert_eq!(config.export_pdf, TaskWhen::OnType);
|
||||
}
|
||||
|
@ -877,7 +924,7 @@ mod tests {
|
|||
// assert!(timestamp(|_| {}).is_none());
|
||||
// assert!(timestamp(|config| {
|
||||
// let update = json!({});
|
||||
// update_config(&mut config, &update).unwrap();
|
||||
// good_config(&mut config, &update);
|
||||
// })
|
||||
// .is_none());
|
||||
|
||||
|
@ -885,7 +932,7 @@ mod tests {
|
|||
let update = json!({
|
||||
"typstExtraArgs": ["--creation-timestamp", "1234"]
|
||||
});
|
||||
update_config(config, &update).unwrap();
|
||||
good_config(config, &update);
|
||||
});
|
||||
assert!(args_timestamp.is_some());
|
||||
|
||||
|
@ -905,7 +952,37 @@ mod tests {
|
|||
"typstExtraArgs": []
|
||||
});
|
||||
|
||||
update_config(&mut config, &update).unwrap();
|
||||
good_config(&mut config, &update);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_null_completion() {
|
||||
let mut config = Config::default();
|
||||
let update = json!({
|
||||
"completion": null
|
||||
});
|
||||
|
||||
good_config(&mut config, &update);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_null_root() {
|
||||
let mut config = Config::default();
|
||||
let update = json!({
|
||||
"root": null
|
||||
});
|
||||
|
||||
good_config(&mut config, &update);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_null_extra_args() {
|
||||
let mut config = Config::default();
|
||||
let update = json!({
|
||||
"typstExtraArgs": null
|
||||
});
|
||||
|
||||
good_config(&mut config, &update);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -913,7 +990,7 @@ mod tests {
|
|||
fn opts(update: Option<&JsonValue>) -> CompileFontArgs {
|
||||
let mut config = Config::default();
|
||||
if let Some(update) = update {
|
||||
update_config(&mut config, update).unwrap();
|
||||
good_config(&mut config, update);
|
||||
}
|
||||
|
||||
config.font_opts()
|
||||
|
@ -988,13 +1065,10 @@ mod tests {
|
|||
let update = json!({
|
||||
"typstExtraArgs": ["main.typ", "main.typ"]
|
||||
});
|
||||
|
||||
let err = format!("{}", update_config(&mut config, &update).unwrap_err());
|
||||
assert!(err.contains("typstExtraArgs"), "unexpected error: {err}");
|
||||
assert!(
|
||||
err.contains(r#"String("main.typ")"#),
|
||||
"unexpected error: {err}"
|
||||
);
|
||||
update_config(&mut config, &update).unwrap();
|
||||
let warns = format!("{:?}", config.warnings);
|
||||
assert!(warns.contains("typstExtraArgs"), "warns: {warns}");
|
||||
assert!(warns.contains(r#"String(\"main.typ\")"#), "warns: {warns}");
|
||||
}
|
||||
{
|
||||
let mut config = Config::default();
|
||||
|
|
|
@ -199,6 +199,10 @@ impl ServerState {
|
|||
return;
|
||||
};
|
||||
let _ = this.on_changed_configuration(Config::values_to_map(resp));
|
||||
|
||||
if !this.config.warnings.is_empty() {
|
||||
this.show_config_warnings();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ use std::ops::Deref;
|
|||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use lsp_types::request::ShowMessageRequest;
|
||||
use lsp_types::*;
|
||||
use sync_ls::*;
|
||||
use tinymist_query::{LspWorldExt, OnExportRequest, ServerInfoResponse};
|
||||
|
@ -162,27 +163,31 @@ impl ServerState {
|
|||
// Bootstrap server
|
||||
let (editor_tx, editor_rx) = mpsc::unbounded_channel();
|
||||
|
||||
let mut service = ServerState::new(client.clone(), config, editor_tx);
|
||||
let mut server = ServerState::new(client.clone(), config, editor_tx);
|
||||
|
||||
if !server.config.warnings.is_empty() {
|
||||
server.show_config_warnings();
|
||||
}
|
||||
|
||||
if start {
|
||||
let editor_actor = EditorActor::new(
|
||||
client.clone().to_untyped(),
|
||||
editor_rx,
|
||||
service.config.notify_status,
|
||||
server.config.notify_status,
|
||||
);
|
||||
|
||||
service
|
||||
server
|
||||
.reload_projects()
|
||||
.log_error("could not restart primary");
|
||||
|
||||
#[cfg(feature = "preview")]
|
||||
service.background_preview();
|
||||
server.background_preview();
|
||||
|
||||
// Run the cluster in the background after we referencing it
|
||||
client.handle.spawn(editor_actor.run());
|
||||
}
|
||||
|
||||
service
|
||||
server
|
||||
}
|
||||
|
||||
/// Installs LSP handlers to the language server.
|
||||
|
@ -357,6 +362,31 @@ pub enum ServerEvent {
|
|||
}
|
||||
|
||||
impl ServerState {
|
||||
/// Shows the configuration warnings to the client.
|
||||
pub fn show_config_warnings(&mut self) {
|
||||
if !self.config.warnings.is_empty() {
|
||||
for warning in self.config.warnings.iter() {
|
||||
self.client.send_lsp_request::<ShowMessageRequest>(
|
||||
ShowMessageRequestParams {
|
||||
typ: MessageType::WARNING,
|
||||
message: tinymist_l10n::t!(
|
||||
"tinymist.config.badServerConfig",
|
||||
"bad server configuration: {warning}",
|
||||
warning = warning.as_ref().into()
|
||||
)
|
||||
.into(),
|
||||
actions: None,
|
||||
},
|
||||
|_s, r| {
|
||||
if let Some(err) = r.error {
|
||||
log::error!("failed to send warning message: {err:?}");
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the current server info.
|
||||
pub fn collect_server_info(&mut self) -> QueryFuture {
|
||||
let dg = self.project.primary_id().to_string();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue