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")]
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<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)]
mod tests {
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")]
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<TaskWhen>,
/// 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<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)]
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
});
good_config(&mut config, &update);
fn test_null_args() {
fn test_good_config(path: &str) -> Config {
let mut obj = json!(null);
let path = path.split('.').collect::<Vec<_>>();
for p in path.iter().rev() {
obj = json!({ *p: obj });
}
#[test]
fn test_null_root() {
let mut config = Config::default();
let update = json!({
"root": null
});
good_config(&mut config, &update);
let mut c = Config::default();
good_config(&mut c, &obj);
c
}
#[test]
fn test_null_extra_args() {
let mut config = Config::default();
let update = json!({
"typstExtraArgs": 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_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]