feat: downgrade some config errors and show warnings (#1538)

This commit is contained in:
Myriad-Dreamin 2025-03-19 13:30:00 +08:00 committed by GitHub
parent 9b9a674118
commit 13fb22f4fa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 163 additions and 45 deletions

View file

@ -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> {

View file

@ -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();

View file

@ -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();
}
}
}

View file

@ -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();