дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 19:25:54 +02:00
rust-analyzer reports its own analysis (a missing field, a wrong type) only to clients that pull diagnostics; it pushes just `cargo check`'s, so in the editor a Rust type error showed up only after saving. The client now advertises `textDocument/diagnostic` and pulls an open file's diagnostics from servers with a `diagnosticProvider`: when it opens or its server starts, after edits (once typing pauses), on save (and other open files for servers with inter-file dependencies) and on `workspace/diagnostic/refresh`. Pulled and pushed diagnostics are kept apart and shown, and given to code actions, together; a newer pull supersedes an older reply, a server-cancelled pull is retried, and closing a file drops what was pulled.
688 рядки
30 KiB
Rust
688 рядки
30 KiB
Rust
//! Client capabilities we advertise and the server capabilities we consult.
|
|
|
|
use crate::text::Encoding;
|
|
use serde_json::{Value, json};
|
|
|
|
pub fn client_capabilities() -> Value {
|
|
let symbol_kinds: Vec<u32> = (1..=26).collect();
|
|
let completion_kinds: Vec<u32> = (1..=25).collect();
|
|
json!({
|
|
"general": {
|
|
"positionEncodings": ["utf-16"],
|
|
"staleRequestSupport": {
|
|
"cancel": true,
|
|
"retryOnContentModified": [
|
|
"textDocument/semanticTokens/full",
|
|
"textDocument/inlayHint",
|
|
"textDocument/codeLens"
|
|
]
|
|
},
|
|
"markdown": { "parser": "marked", "version": "1.1.0" }
|
|
},
|
|
"window": {
|
|
"workDoneProgress": true,
|
|
"showMessage": { "messageActionItem": { "additionalPropertiesSupport": false } },
|
|
"showDocument": { "support": false }
|
|
},
|
|
"workspace": {
|
|
"applyEdit": true,
|
|
"workspaceEdit": {
|
|
"documentChanges": true,
|
|
"resourceOperations": ["create", "rename", "delete"],
|
|
"failureHandling": "abort",
|
|
"normalizesLineEndings": true,
|
|
"changeAnnotationSupport": { "groupsOnLabel": false }
|
|
},
|
|
"workspaceFolders": true,
|
|
"fileOperations": { "dynamicRegistration": true, "willRename": true, "didRename": true },
|
|
"configuration": true,
|
|
"didChangeConfiguration": { "dynamicRegistration": false },
|
|
"didChangeWatchedFiles": { "dynamicRegistration": true, "relativePatternSupport": true },
|
|
"symbol": {
|
|
"dynamicRegistration": false,
|
|
"symbolKind": { "valueSet": symbol_kinds },
|
|
"tagSupport": { "valueSet": [1] },
|
|
"resolveSupport": { "properties": ["location.range"] }
|
|
},
|
|
"executeCommand": { "dynamicRegistration": false },
|
|
"semanticTokens": { "refreshSupport": true },
|
|
"codeLens": { "refreshSupport": true },
|
|
"inlayHint": { "refreshSupport": true },
|
|
"diagnostics": { "refreshSupport": true },
|
|
"textDocumentContent": { "dynamicRegistration": true }
|
|
},
|
|
"textDocument": {
|
|
"synchronization": {
|
|
"dynamicRegistration": false,
|
|
"willSave": false,
|
|
"willSaveWaitUntil": false,
|
|
"didSave": true
|
|
},
|
|
"completion": {
|
|
"dynamicRegistration": false,
|
|
"contextSupport": true,
|
|
"insertTextMode": 1,
|
|
"completionItem": {
|
|
"snippetSupport": true,
|
|
"commitCharactersSupport": true,
|
|
"documentationFormat": ["markdown", "plaintext"],
|
|
"deprecatedSupport": true,
|
|
"preselectSupport": true,
|
|
"tagSupport": { "valueSet": [1] },
|
|
"insertReplaceSupport": true,
|
|
"resolveSupport": {
|
|
"properties": ["documentation", "detail", "additionalTextEdits", "command"]
|
|
},
|
|
"insertTextModeSupport": { "valueSet": [1] },
|
|
"labelDetailsSupport": true
|
|
},
|
|
"completionItemKind": { "valueSet": completion_kinds },
|
|
"completionList": {
|
|
"itemDefaults": ["commitCharacters", "editRange", "insertTextFormat", "insertTextMode", "data"]
|
|
}
|
|
},
|
|
"hover": { "dynamicRegistration": false, "contentFormat": ["markdown", "plaintext"] },
|
|
"signatureHelp": {
|
|
"dynamicRegistration": false,
|
|
"contextSupport": true,
|
|
"signatureInformation": {
|
|
"documentationFormat": ["markdown", "plaintext"],
|
|
"parameterInformation": { "labelOffsetSupport": true },
|
|
"activeParameterSupport": true
|
|
}
|
|
},
|
|
"declaration": { "dynamicRegistration": false, "linkSupport": true },
|
|
"definition": { "dynamicRegistration": false, "linkSupport": true },
|
|
"typeDefinition": { "dynamicRegistration": false, "linkSupport": true },
|
|
"implementation": { "dynamicRegistration": false, "linkSupport": true },
|
|
"references": { "dynamicRegistration": false },
|
|
"documentHighlight": { "dynamicRegistration": false },
|
|
"documentSymbol": {
|
|
"dynamicRegistration": false,
|
|
"symbolKind": { "valueSet": symbol_kinds },
|
|
"hierarchicalDocumentSymbolSupport": true,
|
|
"tagSupport": { "valueSet": [1] },
|
|
"labelSupport": true
|
|
},
|
|
"codeAction": {
|
|
"dynamicRegistration": false,
|
|
"codeActionLiteralSupport": {
|
|
"codeActionKind": {
|
|
"valueSet": [
|
|
"", "quickfix", "refactor", "refactor.extract", "refactor.inline",
|
|
"refactor.rewrite", "source", "source.organizeImports", "source.fixAll"
|
|
]
|
|
}
|
|
},
|
|
"isPreferredSupport": true,
|
|
"disabledSupport": true,
|
|
"dataSupport": true,
|
|
"resolveSupport": { "properties": ["edit", "command"] },
|
|
"honorsChangeAnnotations": false
|
|
},
|
|
"codeLens": { "dynamicRegistration": false },
|
|
"documentLink": { "dynamicRegistration": false, "tooltipSupport": true },
|
|
"formatting": { "dynamicRegistration": false },
|
|
"rangeFormatting": { "dynamicRegistration": false },
|
|
"rename": {
|
|
"dynamicRegistration": false,
|
|
"prepareSupport": true,
|
|
"prepareSupportDefaultBehavior": 1,
|
|
"honorsChangeAnnotations": false
|
|
},
|
|
"diagnostic": { "dynamicRegistration": false, "relatedDocumentSupport": false },
|
|
"publishDiagnostics": {
|
|
"relatedInformation": true,
|
|
"tagSupport": { "valueSet": [1, 2] },
|
|
"versionSupport": true,
|
|
"codeDescriptionSupport": true,
|
|
"dataSupport": true
|
|
},
|
|
"semanticTokens": {
|
|
"dynamicRegistration": false,
|
|
"requests": { "range": false, "full": true },
|
|
"tokenTypes": [
|
|
"namespace", "type", "class", "enum", "interface", "struct", "typeParameter",
|
|
"parameter", "variable", "property", "enumMember", "event", "function", "method",
|
|
"macro", "keyword", "modifier", "comment", "string", "number", "regexp",
|
|
"operator", "decorator"
|
|
],
|
|
"tokenModifiers": [
|
|
"declaration", "definition", "readonly", "static", "deprecated", "abstract",
|
|
"async", "modification", "documentation", "defaultLibrary"
|
|
],
|
|
"formats": ["relative"],
|
|
"overlappingTokenSupport": false,
|
|
"multilineTokenSupport": false,
|
|
"serverCancelSupport": true,
|
|
"augmentsSyntaxTokens": true
|
|
},
|
|
"inlayHint": { "dynamicRegistration": false },
|
|
"inlineCompletion": { "dynamicRegistration": true },
|
|
"callHierarchy": { "dynamicRegistration": false },
|
|
"typeHierarchy": { "dynamicRegistration": false }
|
|
},
|
|
"experimental": {}
|
|
})
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub enum SyncKind {
|
|
None,
|
|
Full,
|
|
Incremental,
|
|
}
|
|
|
|
/// Parsed view of `InitializeResult.capabilities`, kept as raw JSON so unusual
|
|
/// server payloads never fail deserialization.
|
|
#[derive(Clone, Debug)]
|
|
pub struct ServerCaps {
|
|
pub raw: Value,
|
|
pub encoding: Encoding,
|
|
pub open_close: bool,
|
|
pub sync: SyncKind,
|
|
/// `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.
|
|
pub fn capability_key(method: &str) -> Option<&'static str> {
|
|
Some(match method {
|
|
"textDocument/completion" => "completionProvider",
|
|
"textDocument/hover" => "hoverProvider",
|
|
"textDocument/signatureHelp" => "signatureHelpProvider",
|
|
"textDocument/definition" => "definitionProvider",
|
|
"textDocument/declaration" => "declarationProvider",
|
|
"textDocument/typeDefinition" => "typeDefinitionProvider",
|
|
"textDocument/implementation" => "implementationProvider",
|
|
"textDocument/references" => "referencesProvider",
|
|
"textDocument/documentHighlight" => "documentHighlightProvider",
|
|
"textDocument/documentSymbol" => "documentSymbolProvider",
|
|
"workspace/symbol" => "workspaceSymbolProvider",
|
|
"textDocument/codeAction" => "codeActionProvider",
|
|
"workspace/executeCommand" => "executeCommandProvider",
|
|
"textDocument/rename" => "renameProvider",
|
|
"textDocument/formatting" => "documentFormattingProvider",
|
|
"textDocument/rangeFormatting" => "documentRangeFormattingProvider",
|
|
"textDocument/inlayHint" => "inlayHintProvider",
|
|
"textDocument/inlineCompletion" => "inlineCompletionProvider",
|
|
"textDocument/semanticTokens" | "textDocument/semanticTokens/full" => "semanticTokensProvider",
|
|
"textDocument/codeLens" => "codeLensProvider",
|
|
"textDocument/documentLink" => "documentLinkProvider",
|
|
"textDocument/prepareCallHierarchy" => "callHierarchyProvider",
|
|
"textDocument/prepareTypeHierarchy" => "typeHierarchyProvider",
|
|
_ => return None,
|
|
})
|
|
}
|
|
|
|
/// 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)))
|
|
}
|
|
|
|
impl ServerCaps {
|
|
pub fn parse(raw: Value) -> Self {
|
|
let encoding = Encoding::parse(raw.get("positionEncoding").and_then(Value::as_str));
|
|
let (open_close, sync, save) = match raw.get("textDocumentSync") {
|
|
Some(Value::Number(n)) => (true, sync_kind(n.as_u64()), None),
|
|
Some(Value::Object(o)) => {
|
|
let save = match o.get("save") {
|
|
Some(Value::Bool(true)) => Some(false),
|
|
Some(Value::Object(s)) => Some(s.get("includeText").and_then(Value::as_bool).unwrap_or(false)),
|
|
_ => None,
|
|
};
|
|
let sync = sync_kind(o.get("change").and_then(Value::as_u64));
|
|
(o.get("openClose").and_then(Value::as_bool).unwrap_or(false), sync, save)
|
|
}
|
|
_ => (false, SyncKind::None, None),
|
|
};
|
|
let semantic_legend = raw.pointer("/semanticTokensProvider/legend").map(|l| {
|
|
let strings = |key: &str| -> Vec<String> {
|
|
l.get(key)
|
|
.and_then(Value::as_array)
|
|
.map(|a| a.iter().filter_map(|s| s.as_str().map(str::to_owned)).collect())
|
|
.unwrap_or_default()
|
|
};
|
|
(strings("tokenTypes"), strings("tokenModifiers"))
|
|
});
|
|
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`.
|
|
pub fn has(&self, key: &str) -> bool {
|
|
truthy(self.raw.get(key))
|
|
}
|
|
|
|
/// `provider.sub == true`, e.g. `completionProvider.resolveProvider`.
|
|
pub fn has_sub(&self, key: &str, sub: &str) -> bool {
|
|
self.raw.get(key).and_then(|v| v.get(sub)).and_then(Value::as_bool).unwrap_or(false)
|
|
}
|
|
|
|
pub fn semantic_tokens_full(&self) -> bool {
|
|
match self.raw.get("semanticTokensProvider") {
|
|
Some(Value::Object(o)) => truthy(o.get("full")),
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
pub fn completion_triggers(&self) -> Vec<String> {
|
|
self.raw
|
|
.pointer("/completionProvider/triggerCharacters")
|
|
.and_then(Value::as_array)
|
|
.map(|a| a.iter().filter_map(|s| s.as_str().map(str::to_owned)).collect())
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
/// `codeActionProvider.codeActionKinds`; `None` when the server does not list them.
|
|
pub fn code_action_kinds(&self) -> Option<Vec<String>> {
|
|
self.raw
|
|
.pointer("/codeActionProvider/codeActionKinds")
|
|
.and_then(Value::as_array)
|
|
.map(|a| a.iter().filter_map(|s| s.as_str().map(str::to_owned)).collect())
|
|
}
|
|
|
|
pub fn commands(&self) -> Vec<String> {
|
|
self.raw
|
|
.pointer("/executeCommandProvider/commands")
|
|
.and_then(Value::as_array)
|
|
.map(|a| a.iter().filter_map(|s| s.as_str().map(str::to_owned)).collect())
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
/// URI schemes served by `workspace/textDocumentContent` (LSP 3.18).
|
|
pub fn content_schemes(&self) -> Vec<String> {
|
|
self.raw
|
|
.pointer("/workspace/textDocumentContent/schemes")
|
|
.and_then(Value::as_array)
|
|
.map(|a| a.iter().filter_map(|s| s.as_str().map(str::to_owned)).collect())
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
/// `workspace.fileOperations.<op>` registration options (`op`: `willRename`, `didRename`).
|
|
pub fn file_operation(&self, op: &str) -> Option<&Value> {
|
|
self.raw.pointer(&format!("/workspace/fileOperations/{op}")).filter(|v| v.is_object())
|
|
}
|
|
|
|
/// Applies a `client/registerCapability` registration (best effort).
|
|
pub fn register(&mut self, method: &str, options: Option<&Value>) {
|
|
if let Some(op) = file_operation_key(method)
|
|
&& let Some(opts) = options
|
|
{
|
|
if !self.raw.get("workspace").is_some_and(Value::is_object) {
|
|
self.raw["workspace"] = json!({});
|
|
}
|
|
if !self.raw["workspace"].get("fileOperations").is_some_and(Value::is_object) {
|
|
self.raw["workspace"]["fileOperations"] = json!({});
|
|
}
|
|
self.raw["workspace"]["fileOperations"][op] = opts.clone();
|
|
return;
|
|
}
|
|
if method == "workspace/textDocumentContent"
|
|
&& let Some(opts) = options
|
|
{
|
|
if !self.raw.get("workspace").is_some_and(Value::is_object) {
|
|
self.raw["workspace"] = json!({});
|
|
}
|
|
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" {
|
|
let include = options.and_then(|o| o.get("includeText")).and_then(Value::as_bool).unwrap_or(false);
|
|
self.save = Some(include);
|
|
return;
|
|
}
|
|
if method == "textDocument/didChange" {
|
|
self.sync = sync_kind(options.and_then(|o| o.get("syncKind")).and_then(Value::as_u64));
|
|
return;
|
|
}
|
|
if method == "textDocument/didOpen" || method == "textDocument/didClose" {
|
|
self.open_close = true;
|
|
return;
|
|
}
|
|
if let Some(key) = capability_key(method)
|
|
&& !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);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn unregister(&mut self, method: &str) {
|
|
if let Some(op) = file_operation_key(method) {
|
|
if let Some(ops) = self.raw.pointer_mut("/workspace/fileOperations").and_then(Value::as_object_mut) {
|
|
ops.remove(op);
|
|
}
|
|
return;
|
|
}
|
|
if let Some(key) = capability_key(method)
|
|
&& let Some(o) = self.raw.as_object_mut()
|
|
{
|
|
o.remove(key);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn file_operation_key(method: &str) -> Option<&'static str> {
|
|
match method {
|
|
"workspace/willRenameFiles" => Some("willRename"),
|
|
"workspace/didRenameFiles" => Some("didRename"),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn sync_kind(n: Option<u64>) -> SyncKind {
|
|
match n {
|
|
Some(1) => SyncKind::Full,
|
|
Some(2) => SyncKind::Incremental,
|
|
_ => SyncKind::None,
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn parses_sync_and_features() {
|
|
let c = ServerCaps::parse(json!({
|
|
"positionEncoding": "utf-8",
|
|
"textDocumentSync": { "openClose": true, "change": 2, "save": { "includeText": true } },
|
|
"completionProvider": { "resolveProvider": true, "triggerCharacters": [".", ":"] },
|
|
"hoverProvider": true,
|
|
"definitionProvider": false,
|
|
"semanticTokensProvider": { "legend": { "tokenTypes": ["a"], "tokenModifiers": ["m"] }, "full": { "delta": true } }
|
|
}));
|
|
assert_eq!(c.encoding, Encoding::Utf8);
|
|
assert_eq!(c.sync, SyncKind::Incremental);
|
|
assert!(c.open_close);
|
|
assert_eq!(c.save, Some(true));
|
|
assert!(c.has("hoverProvider") && !c.has("definitionProvider") && !c.has("renameProvider"));
|
|
assert!(c.has_sub("completionProvider", "resolveProvider"));
|
|
assert_eq!(c.completion_triggers(), vec![".", ":"]);
|
|
assert!(c.semantic_tokens_full());
|
|
assert_eq!(c.semantic_legend, Some((vec!["a".into()], vec!["m".into()])));
|
|
|
|
let c = ServerCaps::parse(json!({ "textDocumentSync": 1 }));
|
|
assert_eq!((c.sync, c.open_close, c.save), (SyncKind::Full, true, None));
|
|
}
|
|
|
|
#[test]
|
|
fn dynamic_registration() {
|
|
let mut c = ServerCaps::parse(json!({}));
|
|
c.register("textDocument/formatting", None);
|
|
assert!(c.has("documentFormattingProvider"));
|
|
c.unregister("textDocument/formatting");
|
|
assert!(!c.has("documentFormattingProvider"));
|
|
c.register("textDocument/didChange", Some(&json!({ "syncKind": 2 })));
|
|
assert_eq!(c.sync, SyncKind::Incremental);
|
|
assert!(c.file_operation("willRename").is_none());
|
|
c.register("workspace/willRenameFiles", Some(&json!({ "filters": [] })));
|
|
assert!(c.file_operation("willRename").is_some());
|
|
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");
|
|
}
|
|
}
|
|
}
|