From fb1c8b3b35637ab0223abd714bb33984fc58c000 Mon Sep 17 00:00:00 2001 From: Myriad-Dreamin <35292584+Myriad-Dreamin@users.noreply.github.com> Date: Tue, 5 Aug 2025 10:31:52 +0800 Subject: [PATCH] feat: make configuration items null-safe (#1988) 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). --- .../tinymist-query/src/analysis/completion.rs | 21 ++-- crates/tinymist/src/config.rs | 98 ++++++++++++++----- 2 files changed, 87 insertions(+), 32 deletions(-) diff --git a/crates/tinymist-query/src/analysis/completion.rs b/crates/tinymist-query/src/analysis/completion.rs index 08c15ce4..d6e21e44 100644 --- a/crates/tinymist-query/src/analysis/completion.rs +++ b/crates/tinymist-query/src/analysis/completion.rs @@ -70,17 +70,17 @@ type LspCompletion = CompletionItem; #[serde(rename_all = "camelCase")] pub struct CompletionFeat { /// Whether to trigger completions on arguments (placeholders) of snippets. - #[serde(default)] + #[serde(default, deserialize_with = "deserialize_null_default")] pub trigger_on_snippet_placeholders: bool, /// Whether supports trigger suggest completion, a.k.a. auto-completion. - #[serde(default)] + #[serde(default, deserialize_with = "deserialize_null_default")] pub trigger_suggest: bool, /// Whether supports trigger parameter hint, a.k.a. signature help. - #[serde(default)] + #[serde(default, deserialize_with = "deserialize_null_default")] pub trigger_parameter_hints: bool, /// Whether supports trigger the command combining suggest and parameter /// hints. - #[serde(default)] + #[serde(default, deserialize_with = "deserialize_null_default")] pub trigger_suggest_and_parameter_hints: bool, /// The Way to complete symbols. @@ -995,6 +995,17 @@ fn is_arg_like_context(mut matching: &LinkedNode) -> bool { // ctx.completions.push(compl); // } +fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result +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)] mod tests { use super::slice_at; @@ -1009,5 +1020,3 @@ mod tests { } } } - -// todo: doesn't complete parameter now, which is not good. diff --git a/crates/tinymist/src/config.rs b/crates/tinymist/src/config.rs index 6ed65013..6859f022 100644 --- a/crates/tinymist/src/config.rs +++ b/crates/tinymist/src/config.rs @@ -862,19 +862,19 @@ pub enum SemanticTokensMode { #[serde(rename_all = "camelCase")] pub struct PreviewFeat { /// The browsing preview options. - #[serde(default)] + #[serde(default, deserialize_with = "deserialize_null_default")] pub browsing: BrowsingPreviewOpts, /// The background preview options. - #[serde(default)] + #[serde(default, deserialize_with = "deserialize_null_default")] pub background: BackgroundPreviewOpts, /// When to refresh the preview. #[serde(default)] pub refresh: Option, /// Whether to enable partial rendering. - #[serde(default)] + #[serde(default, deserialize_with = "deserialize_null_default")] pub partial_rendering: bool, /// Invert colors for the preview. - #[serde(default)] + #[serde(default, deserialize_with = "deserialize_null_default")] pub invert_colors: PreviewInvertColors, } @@ -911,6 +911,7 @@ pub struct BrowsingPreviewOpts { #[serde(rename_all = "camelCase")] pub struct BackgroundPreviewOpts { /// Whether to run the preview in the background. + #[serde(default, deserialize_with = "deserialize_null_default")] pub enabled: bool, /// The arguments for the background preview. pub args: Option>, @@ -957,6 +958,15 @@ pub(crate) fn get_semantic_tokens_options() -> SemanticTokensOptions { } } +fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result +where + T: Default + Deserialize<'de>, + D: serde::Deserializer<'de>, +{ + let opt = Option::deserialize(deserializer)?; + Ok(opt.unwrap_or_default()) +} + #[cfg(test)] mod tests { use super::*; @@ -1106,33 +1116,69 @@ mod tests { } #[test] - fn test_null_completion() { - let mut config = Config::default(); - let update = json!({ - "completion": null - }); + fn test_null_args() { + fn test_good_config(path: &str) -> Config { + let mut obj = json!(null); + let path = path.split('.').collect::>(); + for p in path.iter().rev() { + obj = json!({ *p: obj }); + } - good_config(&mut config, &update); - } + let mut c = Config::default(); + good_config(&mut c, &obj); + c + } - #[test] - fn test_null_root() { - let mut config = Config::default(); - let update = json!({ - "root": null - }); + test_good_config("root"); + test_good_config("rootPath"); + test_good_config("colorTheme"); + test_good_config("lint"); + 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] - fn test_null_extra_args() { - let mut config = Config::default(); - let update = json!({ - "typstExtraArgs": null - }); + test_good_config("lint"); + test_good_config("lint.enabled"); + test_good_config("lint.when"); - good_config(&mut config, &update); + 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]