feat(lsp): per-server features and code-action-kinds filter (#1099)

A [[language-servers]] entry (or languageServers.<name> in settings) can list features and code-action-kinds: capabilities outside features are dropped after initialize and from later registrations, publishDiagnostics and Inspect Code only use servers with diagnostics, and code actions are narrowed to the kept kinds (advertised kinds and answers both). No built-in definition uses them yet. First stage of #901: lets a refactoring-only server sit next to the language's own one.

Co-authored-by: andreabasile <7715018+andreabasile@users.noreply.github.com>
This commit is contained in:
Andrea Basile
2026-11-04 06:43:31 +08:00
committed by GitHub
co-authored by andreabasile
parent 63dd83c129
commit 4d224f80c9
11 changed files with 414 additions and 5 deletions
+12
View File
@@ -157,6 +157,16 @@ pub struct LanguageServerSettings {
pub languages: Vec<String>,
/// Files marking a workspace root for a server defined here (`pyproject.toml`).
pub root_markers: Vec<String>,
/// 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<Vec<String>>,
/// 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<Vec<String>>,
}
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,
}
}
}
+10
View File
@@ -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<String>,
/// 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<String>,
/// 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)]
+258 -1
View File
@@ -183,6 +183,8 @@ pub struct ServerCaps {
/// `Some(include_text)` when the server wants `didSave`.
pub save: Option<bool>,
pub semantic_legend: Option<(Vec<String>, Vec<String>)>,
/// 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<Vec<&'static str>>,
/// Code action kind prefixes kept; empty keeps all.
action_kinds: Vec<String>,
}
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<String>>) -> Vec<String> {
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::<Vec<_>>();
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");
}
}
}
+4
View File
@@ -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) => {
+1
View File
@@ -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);
+11 -2
View File
@@ -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),
+60
View File
@@ -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())]);
+6
View File
@@ -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<serde_json::Valu
install_hint: None,
connect: None,
fallback: false,
features: Vec::new(),
code_action_kinds: Vec::new(),
max_java: None,
launch_error: None,
tsserver_project: false,
+10 -2
View File
@@ -541,6 +541,8 @@ fn custom_server(name: &str, ls: &LanguageServerSettings) -> 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());
}
+22
View File
@@ -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]
+20
View File
@@ -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 = {