Repository: denoland/deno
Pull request: feat(lsp): unstable setting as list

Server side change for `deno.unstable` setting as a list. Coupled with https://github.com/denoland/vscode_deno/pull/1159.
Closes https://github.com/denoland/vscode_deno/issues/1161.

Changed files:
filename: cli/lsp/config.rs
patch: @@ -43,6 +43,8 @@ use indexmap::IndexSet;
 use lsp_types::ClientCapabilities;
 use std::collections::BTreeMap;
 use std::collections::HashMap;
+use std::ops::Deref;
+use std::ops::DerefMut;
 use std::path::Path;
 use std::path::PathBuf;
 use std::sync::Arc;
@@ -70,6 +72,54 @@ fn is_true() -> bool {
   true
 }
 
+/// Wrapper that defaults if it fails to deserialize. Good for individual
+/// settings.
+#[derive(Debug, Default, Clone, Eq, PartialEq)]
+pub struct SafeValue<T> {
+  inner: T,
+}
+
+impl<'de, T: Default + for<'de2> Deserialize<'de2>> Deserialize<'de>
+  for SafeValue<T>
+{
+  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+  where
+    D: serde::Deserializer<'de>,
+  {
+    Ok(Self {
+      inner: Deserialize::deserialize(deserializer).unwrap_or_default(),
+    })
+  }
+}
+
+impl<T: Serialize> Serialize for SafeValue<T> {
+  fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+  where
+    S: serde::Serializer,
+  {
+    self.inner.serialize(serializer)
+  }
+}
+
+impl<T> Deref for SafeValue<T> {
+  type Target = T;
+  fn deref(&self) -> &Self::Target {
+    &self.inner
+  }
+}
+
+impl<T> DerefMut for SafeValue<T> {
+  fn deref_mut(&mut self) -> &mut T {
+    &mut self.inner
+  }
+}
+
+impl<T> SafeValue<T> {
+  pub fn as_deref(&self) -> &T {
+    &self.inner
+  }
+}
+
 #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
 #[serde(rename_all = "camelCase")]
 pub struct CodeLensSettings {
@@ -538,7 +588,7 @@ pub struct WorkspaceSettings {
   pub unsafely_ignore_certificate_errors: Option<Vec<String>>,
 
   #[serde(default)]
-  pub unstable: bool,
+  pub unstable: SafeValue<Vec<String>>,
 
   #[serde(default)]
   pub javascript: LanguageWorkspaceSettings,
@@ -568,7 +618,7 @@ impl Default for WorkspaceSettings {
       testing: Default::default(),
       tls_certificate: None,
       unsafely_ignore_certificate_errors: None,
-      unstable: false,
+      unstable: Default::default(),
       javascript: Default::default(),
       typescript: Default::default(),
     }
@@ -2141,7 +2191,7 @@ mod tests {
         },
         tls_certificate: None,
         unsafely_ignore_certificate_errors: None,
-        unstable: false,
+        unstable: Default::default(),
         javascript: LanguageWorkspaceSettings {
           inlay_hints: InlayHintsSettings {
             parameter_names: InlayHintsParamNamesOptions {

filename: cli/lsp/language_server.rs
patch: @@ -3859,7 +3859,11 @@ impl Inner {
 
   </details>
 "#,
-        serde_json::to_string_pretty(&workspace_settings).unwrap(),
+        serde_json::to_string_pretty(&workspace_settings)
+          .inspect_err(|e| {
+            dbg!(e);
+          })
+          .unwrap(),
         documents_specifiers.len(),
         documents_specifiers
           .into_iter()

filename: cli/lsp/repl.rs
patch: @@ -311,7 +311,7 @@ pub fn get_repl_workspace_settings() -> WorkspaceSettings {
     document_preload_limit: 0, // don't pre-load any modules as it's expensive and not useful for the repl
     tls_certificate: None,
     unsafely_ignore_certificate_errors: None,
-    unstable: false,
+    unstable: Default::default(),
     suggest: DenoCompletionSettings {
       imports: ImportCompletionSettings {
         auto_discover: false,

filename: cli/lsp/testing/execution.rs
patch: @@ -33,6 +33,7 @@ use deno_runtime::deno_permissions::Permissions;
 use deno_runtime::tokio_util::create_and_run_current_thread;
 use indexmap::IndexMap;
 use lsp_types::Uri;
+use std::borrow::Cow;
 use std::collections::HashMap;
 use std::collections::HashSet;
 use std::num::NonZeroUsize;
@@ -219,8 +220,9 @@ impl TestRun {
   ) -> Result<(), AnyError> {
     let args = self.get_args();
     lsp_log!("Executing test run with arguments: {}", args.join(" "));
-    let flags =
-      Arc::new(flags_from_vec(args.into_iter().map(From::from).collect())?);
+    let flags = Arc::new(flags_from_vec(
+      args.into_iter().map(|s| From::from(s.as_ref())).collect(),
+    )?);
     let factory = CliFactory::from_flags(flags);
     let cli_options = factory.cli_options()?;
     // Various test files should not share the same permissions in terms of
@@ -452,37 +454,42 @@ impl TestRun {
     Ok(())
   }
 
-  fn get_args(&self) -> Vec<&str> {
-    let mut args = vec!["deno", "test"];
+  fn get_args(&self) -> Vec<Cow<str>> {
+    let mut args = vec![Cow::Borrowed("deno"), Cow::Borrowed("test")];
     args.extend(
       self
         .workspace_settings
         .testing
         .args
         .iter()
-        .map(|s| s.as_str()),
+        .map(|s| Cow::Borrowed(s.as_str())),
     );
-    args.push("--trace-leaks");
-    if self.workspace_settings.unstable && !args.contains(&"--unstable") {
-      args.push("--unstable");
+    args.push(Cow::Borrowed("--trace-leaks"));
+    for unstable_feature in self.workspace_settings.unstable.as_deref() {
+      let flag = format!("--unstable-{unstable_feature}");
+      if !args.contains(&Cow::Borrowed(&flag)) {
+        args.push(Cow::Owned(flag));
+      }
     }
     if let Some(config) = &self.workspace_settings.config {
-      if !args.contains(&"--config") && !args.contains(&"-c") {
-        args.push("--config");
-        args.push(config.as_str());
+      if !args.contains(&Cow::Borrowed("--config"))
+        && !args.contains(&Cow::Borrowed("-c"))
+      {
+        args.push(Cow::Borrowed("--config"));
+        args.push(Cow::Borrowed(config.as_str()));
       }
     }
     if let Some(import_map) = &self.workspace_settings.import_map {
-      if !args.contains(&"--import-map") {
-        args.push("--import-map");
-        args.push(import_map.as_str());
+      if !args.contains(&Cow::Borrowed("--import-map")) {
+        args.push(Cow::Borrowed("--import-map"));
+        args.push(Cow::Borrowed(import_map.as_str()));
       }
     }
     if self.kind == lsp_custom::TestRunKind::Debug
-      && !args.contains(&"--inspect")
-      && !args.contains(&"--inspect-brk")
+      && !args.contains(&Cow::Borrowed("--inspect"))
+      && !args.contains(&Cow::Borrowed("--inspect-brk"))
     {
-      args.push("--inspect");
+      args.push(Cow::Borrowed("--inspect"));
     }
     args
   }

filename: tests/integration/lsp_tests.rs
patch: @@ -10907,7 +10907,7 @@ fn lsp_configuration_did_change() {
         },
       },
     },
-    "unstable": false,
+    "unstable": [],
   } }));
 
   let list = client.get_completion_list(
@@ -11851,6 +11851,8 @@ Deno.test({
   async fn(t) {
     console.log("test a");
     await t.step("step of test a", () => {});
+    const kv = await Deno.openKv();
+    kv.close();
   }
 });
 "#;
@@ -11860,6 +11862,12 @@ Deno.test({
 
   let mut client = context.new_lsp_command().build();
   client.initialize_default();
+  client.change_configuration(json!({
+    "deno": {
+      "enable": true,
+      "unstable": ["kv"],
+    },
+  }));
 
   client.did_open(json!({
     "textDocument": {
@@ -12380,7 +12388,7 @@ fn lsp_node_modules_dir() {
         "paths": true,
         "imports": {},
       },
-      "unstable": false,
+      "unstable": [],
     } }));
   };
   refresh_config(&mut client);
@@ -12498,7 +12506,7 @@ fn lsp_vendor_dir() {
       "paths": true,
       "imports": {},
     },
-    "unstable": false,
+    "unstable": [],
   } }));
 
   let diagnostics = client.read_diagnostics();