feat: make configuration items null-safe (#1988)
Some checks are pending
tinymist::auto_tag / auto-tag (push) Waiting to run
tinymist::gh_pages / build-gh-pages (push) Waiting to run

When passing configuration items with null values, the default
configurations are used. Note: I don't ensure this to be always true,
some configuration items may have different non-default behaviors when
accepting a null value now or in future. The `deserialize_null_default`
is taken from https://github.com/serde-rs/serde/issues/1098.

Configuration parsing changes:
+ some configurations only accepting boolean now coerce null to `false`
(default).
+ some configurations only accepting an object now coerce null to
default.
+ The `tinymist.preview.invertColors` now now coerces null to `"never"`
(default).
This commit is contained in:
Myriad-Dreamin 2025-08-05 10:31:52 +08:00 committed by GitHub
parent f33f612f43
commit fb1c8b3b35
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 87 additions and 32 deletions

View file

@ -70,17 +70,17 @@ type LspCompletion = CompletionItem;
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct CompletionFeat { pub struct CompletionFeat {
/// Whether to trigger completions on arguments (placeholders) of snippets. /// Whether to trigger completions on arguments (placeholders) of snippets.
#[serde(default)] #[serde(default, deserialize_with = "deserialize_null_default")]
pub trigger_on_snippet_placeholders: bool, pub trigger_on_snippet_placeholders: bool,
/// Whether supports trigger suggest completion, a.k.a. auto-completion. /// Whether supports trigger suggest completion, a.k.a. auto-completion.
#[serde(default)] #[serde(default, deserialize_with = "deserialize_null_default")]
pub trigger_suggest: bool, pub trigger_suggest: bool,
/// Whether supports trigger parameter hint, a.k.a. signature help. /// Whether supports trigger parameter hint, a.k.a. signature help.
#[serde(default)] #[serde(default, deserialize_with = "deserialize_null_default")]
pub trigger_parameter_hints: bool, pub trigger_parameter_hints: bool,
/// Whether supports trigger the command combining suggest and parameter /// Whether supports trigger the command combining suggest and parameter
/// hints. /// hints.
#[serde(default)] #[serde(default, deserialize_with = "deserialize_null_default")]
pub trigger_suggest_and_parameter_hints: bool, pub trigger_suggest_and_parameter_hints: bool,
/// The Way to complete symbols. /// The Way to complete symbols.
@ -995,6 +995,17 @@ fn is_arg_like_context(mut matching: &LinkedNode) -> bool {
// ctx.completions.push(compl); // ctx.completions.push(compl);
// } // }
fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
where
T: Default + Deserialize<'de>,
D: serde::Deserializer<'de>,
{
let opt = Option::deserialize(deserializer)?;
Ok(opt.unwrap_or_default())
}
// todo: doesn't complete parameter now, which is not good.
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::slice_at; use super::slice_at;
@ -1009,5 +1020,3 @@ mod tests {
} }
} }
} }
// todo: doesn't complete parameter now, which is not good.

View file

@ -862,19 +862,19 @@ pub enum SemanticTokensMode {
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct PreviewFeat { pub struct PreviewFeat {
/// The browsing preview options. /// The browsing preview options.
#[serde(default)] #[serde(default, deserialize_with = "deserialize_null_default")]
pub browsing: BrowsingPreviewOpts, pub browsing: BrowsingPreviewOpts,
/// The background preview options. /// The background preview options.
#[serde(default)] #[serde(default, deserialize_with = "deserialize_null_default")]
pub background: BackgroundPreviewOpts, pub background: BackgroundPreviewOpts,
/// When to refresh the preview. /// When to refresh the preview.
#[serde(default)] #[serde(default)]
pub refresh: Option<TaskWhen>, pub refresh: Option<TaskWhen>,
/// Whether to enable partial rendering. /// Whether to enable partial rendering.
#[serde(default)] #[serde(default, deserialize_with = "deserialize_null_default")]
pub partial_rendering: bool, pub partial_rendering: bool,
/// Invert colors for the preview. /// Invert colors for the preview.
#[serde(default)] #[serde(default, deserialize_with = "deserialize_null_default")]
pub invert_colors: PreviewInvertColors, pub invert_colors: PreviewInvertColors,
} }
@ -911,6 +911,7 @@ pub struct BrowsingPreviewOpts {
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct BackgroundPreviewOpts { pub struct BackgroundPreviewOpts {
/// Whether to run the preview in the background. /// Whether to run the preview in the background.
#[serde(default, deserialize_with = "deserialize_null_default")]
pub enabled: bool, pub enabled: bool,
/// The arguments for the background preview. /// The arguments for the background preview.
pub args: Option<Vec<String>>, pub args: Option<Vec<String>>,
@ -957,6 +958,15 @@ pub(crate) fn get_semantic_tokens_options() -> SemanticTokensOptions {
} }
} }
fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
where
T: Default + Deserialize<'de>,
D: serde::Deserializer<'de>,
{
let opt = Option::deserialize(deserializer)?;
Ok(opt.unwrap_or_default())
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@ -1106,33 +1116,69 @@ mod tests {
} }
#[test] #[test]
fn test_null_completion() { fn test_null_args() {
let mut config = Config::default(); fn test_good_config(path: &str) -> Config {
let update = json!({ let mut obj = json!(null);
"completion": null let path = path.split('.').collect::<Vec<_>>();
}); for p in path.iter().rev() {
obj = json!({ *p: obj });
good_config(&mut config, &update);
} }
#[test] let mut c = Config::default();
fn test_null_root() { good_config(&mut c, &obj);
let mut config = Config::default(); c
let update = json!({
"root": null
});
good_config(&mut config, &update);
} }
#[test] test_good_config("root");
fn test_null_extra_args() { test_good_config("rootPath");
let mut config = Config::default(); test_good_config("colorTheme");
let update = json!({ test_good_config("lint");
"typstExtraArgs": null test_good_config("customizedShowDocument");
}); test_good_config("projectResolution");
test_good_config("exportPdf");
test_good_config("exportTarget");
test_good_config("fontPaths");
test_good_config("formatterMode");
test_good_config("formatterPrintWidth");
test_good_config("formatterIndentSize");
test_good_config("formatterProseWrap");
test_good_config("outputPath");
test_good_config("semanticTokens");
test_good_config("delegateFsRequests");
test_good_config("supportHtmlInMarkdown");
test_good_config("supportExtendedCodeAction");
test_good_config("development");
test_good_config("systemFonts");
good_config(&mut config, &update); test_good_config("completion");
test_good_config("completion.triggerSuggest");
test_good_config("completion.triggerParameterHints");
test_good_config("completion.triggerSuggestAndParameterHints");
test_good_config("completion.triggerOnSnippetPlaceholders");
test_good_config("completion.symbol");
test_good_config("completion.postfix");
test_good_config("completion.postfixUfcs");
test_good_config("completion.postfixUfcsLeft");
test_good_config("completion.postfixUfcsRight");
test_good_config("completion.postfixSnippets");
test_good_config("lint");
test_good_config("lint.enabled");
test_good_config("lint.when");
test_good_config("preview");
test_good_config("preview.browsing");
test_good_config("preview.browsing.args");
test_good_config("preview.background");
test_good_config("preview.background.enabled");
test_good_config("preview.background.args");
test_good_config("preview.refresh");
test_good_config("preview.partialRendering");
let c = test_good_config("preview.invertColors");
assert_eq!(
c.preview.invert_colors,
PreviewInvertColors::Enum(PreviewInvertColor::Never)
);
} }
#[test] #[test]