diff --git a/crates/ide-core/src/settings.rs b/crates/ide-core/src/settings.rs index ebb97835..5991a79f 100644 --- a/crates/ide-core/src/settings.rs +++ b/crates/ide-core/src/settings.rs @@ -157,6 +157,16 @@ pub struct LanguageServerSettings { pub languages: Vec, /// Files marking a workspace root for a server defined here (`pyproject.toml`). pub root_markers: Vec, + /// Replaces the definition's `features`: what the server is used for + /// (`code-action`, `hover`, …); an empty list means everything. + #[serde(skip_serializing_if = "Option::is_none")] + #[specta(optional)] + pub features: Option>, + /// Replaces the definition's `code-action-kinds` (`refactor`); an empty + /// list keeps every kind. + #[serde(skip_serializing_if = "Option::is_none")] + #[specta(optional)] + pub code_action_kinds: Option>, } impl Default for LanguageServerSettings { @@ -170,6 +180,8 @@ impl Default for LanguageServerSettings { settings: None, languages: Vec::new(), root_markers: Vec::new(), + features: None, + code_action_kinds: None, } } } diff --git a/crates/ide-language/src/lib.rs b/crates/ide-language/src/lib.rs index cef38d97..7054d781 100644 --- a/crates/ide-language/src/lib.rs +++ b/crates/ide-language/src/lib.rs @@ -131,6 +131,16 @@ pub struct LanguageServerConfig { /// basedpyright): only used while none of those is installed. #[serde(default)] pub fallback: bool, + /// What the server is used for (`completion`, `code-action`, `diagnostics`, + /// … `FEATURES` in `ide-lsp/src/caps.rs`); empty means everything. The other + /// capabilities it announces are ignored, so a refactoring-only server next + /// to the language's own one adds no completion, hover or formatting. + #[serde(default)] + pub features: Vec, + /// Code action kinds kept from the server, matched like LSP's `only` + /// (`refactor` keeps `refactor.extract`); empty keeps every kind. + #[serde(default)] + pub code_action_kinds: Vec, /// Newest Java release the server runs on (kotlin-language-server: 21). A /// newer `JAVA_HOME` is replaced by the newest installed JDK that fits. #[serde(default)] diff --git a/crates/ide-lsp/src/caps.rs b/crates/ide-lsp/src/caps.rs index da53501b..a20296ef 100644 --- a/crates/ide-lsp/src/caps.rs +++ b/crates/ide-lsp/src/caps.rs @@ -183,6 +183,8 @@ pub struct ServerCaps { /// `Some(include_text)` when the server wants `didSave`. pub save: Option, pub semantic_legend: Option<(Vec, Vec)>, + /// Applied to what the server announces, now and in later registrations. + pub restriction: Restriction, } /// LSP request method → server capability key, for features and dynamic registration. @@ -215,6 +217,104 @@ pub fn capability_key(method: &str) -> Option<&'static str> { }) } +/// Names for a server's `features` → the capability keys each one keeps. +pub const FEATURES: &[(&str, &[&str])] = &[ + ("completion", &["completionProvider"]), + ("inline-completion", &["inlineCompletionProvider"]), + ("hover", &["hoverProvider"]), + ("signature-help", &["signatureHelpProvider"]), + ("definition", &["definitionProvider"]), + ("declaration", &["declarationProvider"]), + ("type-definition", &["typeDefinitionProvider"]), + ("implementation", &["implementationProvider"]), + ("references", &["referencesProvider"]), + ("document-highlight", &["documentHighlightProvider"]), + ("document-symbol", &["documentSymbolProvider"]), + ("workspace-symbol", &["workspaceSymbolProvider"]), + ("code-action", &["codeActionProvider", "executeCommandProvider"]), + ("code-lens", &["codeLensProvider", "executeCommandProvider"]), + ("rename", &["renameProvider"]), + ( + "formatting", + &["documentFormattingProvider", "documentRangeFormattingProvider", "documentOnTypeFormattingProvider"], + ), + ("inlay-hint", &["inlayHintProvider"]), + ("semantic-tokens", &["semanticTokensProvider"]), + ("document-link", &["documentLinkProvider"]), + ("call-hierarchy", &["callHierarchyProvider"]), + ("type-hierarchy", &["typeHierarchyProvider"]), + ("folding-range", &["foldingRangeProvider"]), + ("selection-range", &["selectionRangeProvider"]), + ("diagnostics", &["diagnosticProvider"]), +]; + +/// A server's `features` and `code-action-kinds`: the part of its capabilities +/// the IDE uses. +#[derive(Clone, Debug, Default, PartialEq)] +pub struct Restriction { + /// Allowed `*Provider` keys; `None` allows all. + keys: Option>, + /// Code action kind prefixes kept; empty keeps all. + action_kinds: Vec, +} + +impl Restriction { + /// Unknown feature names are logged and ignored. + pub fn new(server: &str, features: &[String], action_kinds: &[String]) -> Self { + let keys = (!features.is_empty()).then(|| { + let mut keys = Vec::new(); + for name in features { + match FEATURES.iter().find(|(n, _)| n == name) { + Some((_, k)) => keys.extend_from_slice(k), + None => tracing::warn!("{server}: unknown feature `{name}` in `features`"), + } + } + keys + }); + Self { keys, action_kinds: action_kinds.to_vec() } + } + + fn allows(&self, key: &str) -> bool { + self.keys.as_ref().is_none_or(|k| k.contains(&key)) + } + + /// Whether the server's `publishDiagnostics` are shown. + pub fn diagnostics(&self) -> bool { + self.allows("diagnosticProvider") + } + + /// Whether a code action of `kind` is kept; one without a kind only when + /// every kind is. + pub fn keeps_action(&self, kind: Option<&str>) -> bool { + self.action_kinds.is_empty() || kind.is_some_and(|k| self.action_kinds.iter().any(|p| kind_within(k, p))) + } + + /// The kinds the server can still return: those it lists that are kept, + /// narrowed to the kept prefixes below them (`refactor` → `refactor.extract`). + fn advertised_kinds(&self, listed: Option>) -> Vec { + let Some(listed) = listed else { return self.action_kinds.clone() }; + let mut out = Vec::new(); + for k in &listed { + let narrowed: Vec<&String> = if self.keeps_action(Some(k)) { + vec![k] + } else { + self.action_kinds.iter().filter(|p| kind_within(p, k)).collect() + }; + for k in narrowed { + if !out.contains(k) { + out.push(k.clone()); + } + } + } + out + } +} + +/// `kind` is `prefix` or below it (`refactor.extract` within `refactor`). +fn kind_within(kind: &str, prefix: &str) -> bool { + kind.strip_prefix(prefix).is_some_and(|rest| rest.is_empty() || rest.starts_with('.')) +} + fn truthy(v: Option<&Value>) -> bool { !matches!(v, None | Some(Value::Null) | Some(Value::Bool(false))) } @@ -244,7 +344,34 @@ impl ServerCaps { }; (strings("tokenTypes"), strings("tokenModifiers")) }); - Self { raw, encoding, open_close, sync, save, semantic_legend } + Self { raw, encoding, open_close, sync, save, semantic_legend, restriction: Restriction::default() } + } + + /// Drops the capabilities outside the server's `features`, and code action + /// kinds outside its `code-action-kinds`. Document sync, workspace options + /// and `experimental` stay. + pub fn restrict(&mut self, restriction: Restriction) { + if let Some(o) = self.raw.as_object_mut() { + o.retain(|key, _| !key.ends_with("Provider") || restriction.allows(key)); + } + if self.has("codeActionProvider") && !restriction.action_kinds.is_empty() { + let kinds = restriction.advertised_kinds(self.code_action_kinds()); + if kinds.is_empty() { + // It lists only kinds that aren't kept: nothing to ask it for. + if let Some(o) = self.raw.as_object_mut() { + o.remove("codeActionProvider"); + } + } else { + if !self.raw["codeActionProvider"].is_object() { + self.raw["codeActionProvider"] = json!({}); + } + self.raw["codeActionProvider"]["codeActionKinds"] = json!(kinds); + } + } + if !restriction.allows("semanticTokensProvider") { + self.semantic_legend = None; + } + self.restriction = restriction; } /// Whether the server statically advertises the capability under `key`. @@ -325,11 +452,18 @@ impl ServerCaps { self.raw["workspace"]["textDocumentContent"] = opts.clone(); return; } + if let Some(key) = capability_key(method) + && !self.restriction.allows(key) + { + return; + } if method == "textDocument/semanticTokens" && let Some(opts) = options { self.raw["semanticTokensProvider"] = opts.clone(); + let restriction = std::mem::take(&mut self.restriction); *self = Self::parse(std::mem::take(&mut self.raw)); + self.restriction = restriction; return; } if method == "textDocument/didSave" { @@ -349,6 +483,11 @@ impl ServerCaps { && !self.has(key) { self.raw[key] = options.cloned().filter(|o| o.is_object()).unwrap_or(Value::Bool(true)); + if key == "codeActionProvider" { + // Registered kinds go through the same narrowing as announced ones. + let restriction = std::mem::take(&mut self.restriction); + self.restrict(restriction); + } } } @@ -426,4 +565,122 @@ mod tests { c.unregister("workspace/willRenameFiles"); assert!(c.file_operation("willRename").is_none()); } + + fn restriction(features: &[&str], kinds: &[&str]) -> Restriction { + let owned = |v: &[&str]| v.iter().map(|s| s.to_string()).collect::>(); + Restriction::new("test", &owned(features), &owned(kinds)) + } + + #[test] + fn features_drop_other_capabilities() { + let mut c = ServerCaps::parse(json!({ + "textDocumentSync": 2, + "completionProvider": {}, + "hoverProvider": true, + "documentFormattingProvider": true, + "foldingRangeProvider": true, + "codeActionProvider": { "resolveProvider": true }, + "executeCommandProvider": { "commands": ["rope.extract"] }, + "semanticTokensProvider": { "legend": { "tokenTypes": [], "tokenModifiers": [] }, "full": true }, + "workspace": { "fileOperations": { "willRename": { "filters": [] } } }, + "experimental": { "x": true } + })); + c.restrict(restriction(&["code-action", "nope"], &[])); + assert!(c.has("codeActionProvider") && c.has_sub("codeActionProvider", "resolveProvider")); + assert_eq!(c.commands(), ["rope.extract"]); + for key in ["completionProvider", "hoverProvider", "documentFormattingProvider", "foldingRangeProvider"] { + assert!(!c.has(key), "{key}"); + } + assert!(c.semantic_legend.is_none() && !c.semantic_tokens_full()); + assert_eq!((c.sync, c.open_close), (SyncKind::Incremental, true)); + assert!(c.file_operation("willRename").is_some() && c.raw["experimental"]["x"] == true); + assert!(!c.restriction.diagnostics()); + assert_eq!(c.code_action_kinds(), None, "no kind filter: the server may return any kind"); + + // Later registrations go through the same filter. + c.register("textDocument/hover", None); + c.register("textDocument/semanticTokens", Some(&json!({ "legend": {}, "full": true }))); + assert!(!c.has("hoverProvider") && !c.has("semanticTokensProvider")); + c.register("textDocument/didSave", None); + assert_eq!(c.save, Some(false)); + + let mut c = ServerCaps::parse(json!({ "hoverProvider": true, "semanticTokensProvider": { "full": true } })); + c.restrict(Restriction::default()); + assert!(c.has("hoverProvider") && c.restriction.diagnostics(), "no `features`: everything"); + c.restrict(restriction(&["semantic-tokens", "diagnostics"], &[])); + c.register("textDocument/semanticTokens", Some(&json!({ "legend": {}, "full": true }))); + assert!(!c.has("hoverProvider") && c.semantic_tokens_full() && c.restriction.diagnostics()); + c.register("textDocument/hover", None); + assert!(!c.has("hoverProvider"), "the restriction survives re-parsing semantic tokens"); + } + + #[test] + fn code_action_kinds_narrow_what_is_asked_and_kept() { + let r = restriction(&[], &["refactor", "source.fixAll"]); + assert!(r.keeps_action(Some("refactor")) && r.keeps_action(Some("refactor.extract.function"))); + assert!(r.keeps_action(Some("source.fixAll.ruff"))); + assert!(!r.keeps_action(Some("refactorx")) && !r.keeps_action(Some("source")) && !r.keeps_action(None)); + assert!(Restriction::default().keeps_action(None)); + + let kinds = |provider: Value| { + let mut c = ServerCaps::parse(json!({ "codeActionProvider": provider })); + c.restrict(r.clone()); + c.has("codeActionProvider").then(|| c.code_action_kinds().unwrap()) + }; + assert_eq!(kinds(json!(true)), Some(vec!["refactor".into(), "source.fixAll".into()])); + assert_eq!( + kinds(json!({ "codeActionKinds": ["quickfix", "refactor.extract", "source"] })), + Some(vec!["refactor.extract".into(), "source.fixAll".into()]), + ); + assert_eq!(kinds(json!({ "codeActionKinds": ["quickfix"] })), None, "nothing it lists is kept"); + + let mut c = ServerCaps::parse(json!({})); + c.restrict(r.clone()); + c.register("textDocument/codeAction", Some(&json!({ "codeActionKinds": ["quickfix", "refactor.inline"] }))); + assert_eq!(c.code_action_kinds(), Some(vec!["refactor.inline".into()])); + } + + #[test] + fn language_definitions_use_known_feature_names() { + for lang in ide_language::LanguageRegistry::load(None).unwrap().all() { + for server in &lang.language_servers { + for name in &server.features { + assert!(FEATURES.iter().any(|(n, _)| n == name), "{}: unknown feature `{name}`", server.name); + } + } + } + } + + #[test] + fn feature_names_cover_every_capability_the_ide_asks_for() { + let named: Vec<&str> = FEATURES.iter().flat_map(|(_, keys)| keys.iter().copied()).collect(); + for method in [ + "textDocument/completion", + "textDocument/hover", + "textDocument/signatureHelp", + "textDocument/definition", + "textDocument/declaration", + "textDocument/typeDefinition", + "textDocument/implementation", + "textDocument/references", + "textDocument/documentHighlight", + "textDocument/documentSymbol", + "workspace/symbol", + "textDocument/codeAction", + "workspace/executeCommand", + "textDocument/rename", + "textDocument/formatting", + "textDocument/rangeFormatting", + "textDocument/inlayHint", + "textDocument/inlineCompletion", + "textDocument/semanticTokens", + "textDocument/codeLens", + "textDocument/documentLink", + "textDocument/prepareCallHierarchy", + "textDocument/prepareTypeHierarchy", + ] { + let key = capability_key(method).unwrap(); + assert!(named.contains(&key), "{method} ({key}) has no feature name"); + } + } } diff --git a/crates/ide-lsp/src/handler.rs b/crates/ide-lsp/src/handler.rs index 11ab9696..006e4d09 100644 --- a/crates/ide-lsp/src/handler.rs +++ b/crates/ide-lsp/src/handler.rs @@ -111,6 +111,10 @@ impl ClientHandler { } fn publish_diagnostics(&self, mgr: &LspManager, params: Value) { + // Problems come only from servers used for `diagnostics`. + if self.server.running_gen(self.generation).is_some_and(|run| !run.caps.read().restriction.diagnostics()) { + return; + } let params: lsp::PublishDiagnosticsParams = match serde_json::from_value(params) { Ok(p) => p, Err(e) => { diff --git a/crates/ide-lsp/src/inspect.rs b/crates/ide-lsp/src/inspect.rs index 9fcf73e9..19c4e580 100644 --- a/crates/ide-lsp/src/inspect.rs +++ b/crates/ide-lsp/src/inspect.rs @@ -156,6 +156,7 @@ impl LspManager { .iter() .zip(ready) .filter_map(|(s, run)| Some(Target { server: s.clone(), run: run? })) + .filter(|t| t.caps().restriction.diagnostics()) .collect(); if targets.is_empty() { return Ok(None); diff --git a/crates/ide-lsp/src/lib.rs b/crates/ide-lsp/src/lib.rs index 91de3dd3..97a94fc2 100644 --- a/crates/ide-lsp/src/lib.rs +++ b/crates/ide-lsp/src/lib.rs @@ -570,7 +570,10 @@ impl LspManager { if let Some(info) = result.get("serverInfo") { tracing::info!("{} initialized: {info}", server.name); } - ServerCaps::parse(result.get("capabilities").cloned().unwrap_or_else(|| json!({}))) + let mut caps = ServerCaps::parse(result.get("capabilities").cloned().unwrap_or_else(|| json!({}))); + let cfg = server.effective_config(); + caps.restrict(caps::Restriction::new(&server.name, &cfg.features, &cfg.code_action_kinds)); + caps } Err(e) => { tracing::warn!("{}: initialize failed: {e}", server.name); @@ -1593,8 +1596,14 @@ impl LspManager { Ok(actions) => { let conv = self.conv(t.enc()); let acc = out.value.get_or_insert_with(Vec::new); + let caps = t.caps(); acc.extend( - actions.unwrap_or_default().into_iter().map(|a| convert::code_action(&conv, a, t.server.id)), + actions + .unwrap_or_default() + .into_iter() + .map(|a| convert::code_action(&conv, a, t.server.id)) + // Servers may ignore `only`, and the kinds kept for this one. + .filter(|a| caps.restriction.keeps_action(a.kind.as_deref())), ); } Err(e) => out.fail(e), diff --git a/crates/ide-lsp/tests/mock_server.rs b/crates/ide-lsp/tests/mock_server.rs index fef3fb36..c9889ae7 100644 --- a/crates/ide-lsp/tests/mock_server.rs +++ b/crates/ide-lsp/tests/mock_server.rs @@ -225,6 +225,8 @@ fn server_cfg(name: &str) -> LanguageServerConfig { install_hint: Some(format!("install {name}")), connect: None, fallback: false, + features: Vec::new(), + code_action_kinds: Vec::new(), max_java: None, launch_error: None, tsserver_project: false, @@ -1749,6 +1751,64 @@ async fn code_action_query_without_listed_kinds() { assert_eq!(q.advertised_kinds, None, "`codeActionProvider` without `codeActionKinds` may return any kind"); } +#[tokio::test(flavor = "multi_thread")] +async fn features_limit_what_a_server_is_used_for() { + // A refactoring-only server listed first, announcing everything the main one does. + let h = harness_configured("mock", "mk", &[("rope", incremental_caps()), ("main", incremental_caps())], |cfg| { + if cfg.name == "rope" { + cfg.features = vec!["code-action".into()]; + cfg.code_action_kinds = vec!["refactor".into()]; + } + }); + let (doc, path) = h.open("a.mk", "let x = 1;\n"); + for name in ["rope", "main"] { + h.wait_status(name, ServerStatus::Ready).await; + } + let (rope, main) = (h.launcher.state("rope"), h.launcher.state("main")); + for s in [&rope, &main] { + s.respond("textDocument/hover", json!({ "contents": "x" })); + s.respond("textDocument/formatting", json!([])); + } + + h.mgr.hover(doc, Pos::new(0, 4), Default::default()).await.unwrap(); + assert_eq!(main.requested("textDocument/hover").len(), 1); + h.mgr + .format(doc, None, FormatOptions { tab_size: 2, insert_spaces: true, on_save: false, tool: None }) + .await + .unwrap(); + assert_eq!(main.requested("textDocument/formatting").len(), 1); + assert!(rope.requested("textDocument/hover").is_empty() && rope.requested("textDocument/formatting").is_empty()); + + rope.respond( + "textDocument/codeAction", + json!([ + { "title": "Organize imports", "kind": "source.organizeImports" }, + { "title": "Extract variable", "kind": "refactor.extract.variable" }, + { "title": "Run", "command": "mock.run" } + ]), + ); + main.respond("textDocument/codeAction", json!([{ "title": "Fix", "kind": "quickfix" }])); + let q = h.mgr.code_action_query(doc, rng(0, 8, 0, 9), vec![], ide_core::CancellationToken::new()).await.unwrap(); + let mut titles: Vec<&str> = q.actions.iter().map(|a| a.title.as_str()).collect(); + titles.sort(); + assert_eq!(titles, ["Extract variable", "Fix"], "rope's other kinds and bare commands are dropped"); + + let diag = + json!([{ "range": {"start":{"line":0,"character":4},"end":{"line":0,"character":5}}, "message": "unused" }]); + rope.peer() + .notify("textDocument/publishDiagnostics", json!({ "uri": uri_of(&path), "diagnostics": diag })) + .unwrap(); + main.peer() + .notify("textDocument/publishDiagnostics", json!({ "uri": uri_of(&path), "diagnostics": diag })) + .unwrap(); + eventually(|| { + h.events.lock().iter().any(|e| matches!(e, IdeEvent::Diagnostics { source, .. } if source == "main")) + }) + .await; + tokio::time::sleep(Duration::from_millis(100)).await; + assert!(!h.events.lock().iter().any(|e| matches!(e, IdeEvent::Diagnostics { source, .. } if source == "rope"))); +} + #[tokio::test(flavor = "multi_thread")] async fn shutdown_all_stops_gracefully() { let h = harness(&[("mock", incremental_caps())]); diff --git a/crates/ide-lsp/tests/real_servers.rs b/crates/ide-lsp/tests/real_servers.rs index 3bd92275..35fa3314 100644 --- a/crates/ide-lsp/tests/real_servers.rs +++ b/crates/ide-lsp/tests/real_servers.rs @@ -129,6 +129,8 @@ async fn missing_binary_is_reported_with_install_hint() { install_hint: Some("npm i -g nope-ls".into()), connect: None, fallback: false, + features: Vec::new(), + code_action_kinds: Vec::new(), max_java: None, launch_error: None, tsserver_project: false, @@ -396,6 +398,8 @@ async fn typescript_language_server_end_to_end() { install_hint: None, connect: None, fallback: false, + features: Vec::new(), + code_action_kinds: Vec::new(), max_java: None, launch_error: None, tsserver_project: false, @@ -443,6 +447,8 @@ fn npm_server(tools: &Path, name: &str, bin: &str, init: Option LanguageServerConfi }), connect: None, fallback: false, + features: Vec::new(), + code_action_kinds: Vec::new(), max_java: None, launch_error: None, tsserver_project: false, @@ -562,11 +564,17 @@ pub fn server_launch_overrides(cfg: &mut LanguageServerConfig, s: &Settings) { } } -/// Environment, `initializationOptions` and `workspace/configuration` settings -/// from Settings › Language Servers; applied last so they win over derived values. +/// Environment, features, `initializationOptions` and `workspace/configuration` +/// settings from Settings › Language Servers; applied last so they win over derived values. pub fn server_protocol_overrides(cfg: &mut LanguageServerConfig, s: &Settings) { let Some(ls) = s.language_servers.get(&cfg.name) else { return }; cfg.env.extend(ls.env.iter().map(|(k, v)| (k.clone(), v.clone()))); + if let Some(features) = &ls.features { + cfg.features = features.clone(); + } + if let Some(kinds) = &ls.code_action_kinds { + cfg.code_action_kinds = kinds.clone(); + } if let Some(options) = &ls.initialization_options { merge_init_options(cfg, options.clone()); } diff --git a/crates/ide-sdk/src/tests.rs b/crates/ide-sdk/src/tests.rs index 3117fdda..b3ad69e0 100644 --- a/crates/ide-sdk/src/tests.rs +++ b/crates/ide-sdk/src/tests.rs @@ -33,6 +33,8 @@ fn server(name: &str) -> LanguageServerConfig { install_hint: None, connect: None, fallback: false, + features: Vec::new(), + code_action_kinds: Vec::new(), max_java: None, launch_error: None, tsserver_project: false, @@ -329,6 +331,26 @@ fn language_server_settings_disable_add_and_override() { let init = serde_json::to_value(ra.initialization_options.unwrap()).unwrap(); assert_eq!(init, json!({ "checkOnSave": false, "check": { "command": "clippy" } })); assert_eq!(ra.settings, Some(json!({ "rust-analyzer": { "cargo": { "features": "all" } } }))); + assert!(ra.features.is_empty() && ra.code_action_kinds.is_empty(), "no setting keeps the definition's"); + + // `features` / `codeActionKinds` replace the definition's; empty lifts them. + let mut pylsp = server("pylsp"); + pylsp.features = vec!["code-action".into()]; + pylsp.code_action_kinds = vec!["refactor".into()]; + s.language_servers.insert( + "pylsp".into(), + LanguageServerSettings { + features: Some(vec!["code-action".into(), "hover".into()]), + code_action_kinds: Some(vec![]), + ..Default::default() + }, + ); + server_protocol_overrides(&mut pylsp, &s); + assert_eq!(pylsp.features, ["code-action", "hover"]); + assert!(pylsp.code_action_kinds.is_empty()); + let parsed: LanguageServerSettings = + serde_json::from_value(json!({ "features": ["code-action"], "codeActionKinds": ["refactor"] })).unwrap(); + assert_eq!(parsed.code_action_kinds, Some(vec!["refactor".to_string()])); } #[test] diff --git a/src/bindings.ts b/src/bindings.ts index b8048c35..5e89fae4 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -4319,6 +4319,16 @@ export type LanguageServerSettings_Deserialize = { languages?: string[], /** Files marking a workspace root for a server defined here (`pyproject.toml`). */ rootMarkers?: string[], + /** + * Replaces the definition's `features`: what the server is used for + * (`code-action`, `hover`, …); an empty list means everything. + */ + features?: string[] | null, + /** + * Replaces the definition's `code-action-kinds` (`refactor`); an empty + * list keeps every kind. + */ + codeActionKinds?: string[] | null, }; export type LanguageServerSettings_Serialize = { @@ -4347,6 +4357,16 @@ export type LanguageServerSettings_Serialize = { languages: string[], /** Files marking a workspace root for a server defined here (`pyproject.toml`). */ rootMarkers: string[], + /** + * Replaces the definition's `features`: what the server is used for + * (`code-action`, `hover`, …); an empty list means everything. + */ + features?: string[] | null, + /** + * Replaces the definition's `code-action-kinds` (`refactor`); an empty + * list keeps every kind. + */ + codeActionKinds?: string[] | null, }; export type LanguagesSettings = {