use std::collections::HashMap;
use std::io::{BufRead, BufReader, Read, Write};
use std::path::{Path, PathBuf};
use std::process::{Child, ChildStdin, Command, Stdio};
use std::sync::{Arc, Mutex};
use std::thread::JoinHandle;
use serde_json::json;
use super::{
CodeAction, CodeCommand, Diagnostic, DocumentSymbol, LspEvent, Pos, Range, ServerConfig,
Severity, parse_diagnostic, path_to_uri, uri_to_path,
};
type Pending = Arc<Mutex<HashMap<i64, (String, Option<PathBuf>, Option<String>)>>>;
type Sink = Arc<Mutex<ChildStdin>>;
#[derive(Debug, Default, Clone)]
struct SemState {
result_id: Option<String>,
raw_data: Vec<u32>,
}
type SemStates = Arc<Mutex<HashMap<PathBuf, SemState>>>;
#[derive(Debug, Clone)]
struct SemServerCaps {
supports_full: bool,
supports_delta: bool,
supports_range: bool,
}
impl Default for SemServerCaps {
fn default() -> Self {
Self {
supports_full: true,
supports_delta: false,
supports_range: false,
}
}
}
type SemCaps = Arc<Mutex<SemServerCaps>>;
pub struct LspClient {
name: String,
child: Child,
stdin: Sink,
reader: Option<JoinHandle<()>>,
next_id: i64,
pending: Pending,
sem_states: SemStates,
sem_caps: SemCaps,
versions: HashMap<PathBuf, i64>,
}
impl LspClient {
pub fn spawn(
sc: &ServerConfig,
root: &Path,
tx: std::sync::mpsc::Sender<LspEvent>,
) -> Result<Self, String> {
let mut child = Command::new(&sc.cmd)
.args(&sc.args)
.current_dir(root)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.map_err(|e| format!("spawn {}: {e}", sc.cmd))?;
let stdin = Arc::new(Mutex::new(child.stdin.take().ok_or("no stdin")?));
let stdout = child.stdout.take().ok_or("no stdout")?;
let pending: Pending = Arc::new(Mutex::new(HashMap::new()));
let sem_states: SemStates = Arc::new(Mutex::new(HashMap::new()));
let sem_caps: SemCaps = Arc::new(Mutex::new(SemServerCaps::default()));
let r_pending = Arc::clone(&pending);
let r_stdin = Arc::clone(&stdin);
let r_sem_states = Arc::clone(&sem_states);
let r_sem_caps = Arc::clone(&sem_caps);
let reader = std::thread::Builder::new()
.name(format!("mnml-lsp-{}", sc.name))
.spawn(move || reader_loop(stdout, tx, r_pending, r_stdin, r_sem_states, r_sem_caps))
.map_err(|e| format!("reader thread: {e}"))?;
let mut c = LspClient {
name: sc.name.clone(),
child,
stdin,
reader: Some(reader),
next_id: 1,
pending,
sem_states,
sem_caps,
versions: HashMap::new(),
};
let root_uri = path_to_uri(root);
let initialization_options =
if sc.cmd.contains("typescript-language-server") || sc.name.contains("typescript") {
json!({
"preferences": {
"includeInlayParameterNameHints": "all",
"includeInlayParameterNameHintsWhenArgumentMatchesName": false,
"includeInlayFunctionParameterTypeHints": true,
"includeInlayVariableTypeHints": true,
"includeInlayVariableTypeHintsWhenTypeMatchesName": false,
"includeInlayPropertyDeclarationTypeHints": true,
"includeInlayFunctionLikeReturnTypeHints": true,
"includeInlayEnumMemberValueHints": true,
}
})
} else {
serde_json::Value::Null
};
c.request(
"initialize",
json!({
"processId": std::process::id(),
"clientInfo": { "name": "mnml" },
"rootUri": root_uri,
"workspaceFolders": [ { "uri": root_uri, "name": "workspace" } ],
"initializationOptions": initialization_options,
"capabilities": {
"window": {
"workDoneProgress": true
},
"textDocument": {
"synchronization": {
"didSave": true,
"willSave": true,
"willSaveWaitUntil": true
},
"publishDiagnostics": {},
"hover": { "contentFormat": ["markdown", "plaintext"] },
"definition": { "linkSupport": true },
"declaration": { "linkSupport": true },
"typeDefinition": { "linkSupport": true },
"implementation": { "linkSupport": true },
"references": {},
"documentHighlight": {},
"callHierarchy": { "dynamicRegistration": false },
"typeHierarchy": { "dynamicRegistration": false },
"rename": {},
"completion": {
"completionItem": {
"snippetSupport": false,
"documentationFormat": ["markdown", "plaintext"],
"resolveSupport": {
"properties": ["documentation", "detail"]
}
}
},
"signatureHelp": {
"signatureInformation": {
"parameterInformation": { "labelOffsetSupport": true }
}
},
"codeAction": {
"codeActionLiteralSupport": {
"codeActionKind": {
"valueSet": [
"", "quickfix", "refactor",
"refactor.extract", "refactor.inline", "refactor.rewrite",
"source", "source.organizeImports"
]
}
},
"resolveSupport": {
"properties": ["edit", "command"]
}
},
"inlayHint": {
"dynamicRegistration": false,
"resolveSupport": { "properties": ["label.tooltip", "label.location"] }
},
"semanticTokens": {
"dynamicRegistration": false,
"requests": { "full": { "delta": true }, "range": 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"]
},
"codeLens": {
"dynamicRegistration": false,
"resolveSupport": { "properties": ["command"] }
},
"onTypeFormatting": { "dynamicRegistration": false },
"foldingRange": {
"dynamicRegistration": false,
"lineFoldingOnly": true
},
"colorProvider": {
"dynamicRegistration": false
},
"documentSymbol": {
"hierarchicalDocumentSymbolSupport": true,
"symbolKind": {
"valueSet": [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
21, 22, 23, 24, 25, 26
]
}
}
}
}
}),
);
c.notify("initialized", json!({}));
Ok(c)
}
pub fn is_open(&self, path: &Path) -> bool {
self.versions.contains_key(path)
}
pub fn did_open(&mut self, path: &Path, language_id: &str, text: &str) {
if self.versions.contains_key(path) {
return;
}
self.versions.insert(path.to_path_buf(), 1);
self.notify(
"textDocument/didOpen",
json!({ "textDocument": {
"uri": path_to_uri(path), "languageId": language_id, "version": 1, "text": text
}}),
);
}
pub fn did_change(&mut self, path: &Path, text: &str) {
let Some(v) = self.versions.get_mut(path) else {
return;
};
*v += 1;
let version = *v;
self.notify(
"textDocument/didChange",
json!({
"textDocument": { "uri": path_to_uri(path), "version": version },
"contentChanges": [ { "text": text } ]
}),
);
}
pub fn did_save(&mut self, path: &Path, text: &str) {
if !self.versions.contains_key(path) {
return;
}
self.notify(
"textDocument/didSave",
json!({ "textDocument": { "uri": path_to_uri(path) }, "text": text }),
);
}
pub fn did_close(&mut self, path: &Path) {
if self.versions.remove(path).is_none() {
return;
}
if let Ok(mut s) = self.sem_states.lock() {
s.remove(path);
}
self.notify(
"textDocument/didClose",
json!({ "textDocument": { "uri": path_to_uri(path) } }),
);
}
pub fn request_text_position(&mut self, method: &str, path: &Path, line: u32, character: u32) {
self.request(
method,
json!({
"textDocument": { "uri": path_to_uri(path) },
"position": { "line": line, "character": character }
}),
);
}
pub fn completion_resolve(&mut self, item: serde_json::Value, label: &str) {
self.request_with_context(
"completionItem/resolve",
item,
None,
Some(label.to_string()),
);
}
pub fn code_action_resolve(&mut self, action: serde_json::Value) {
self.request("codeAction/resolve", action);
}
pub fn code_lens_resolve(&mut self, lens: serde_json::Value, lens_index: usize) {
self.request_with_context("codeLens/resolve", lens, None, Some(lens_index.to_string()));
}
pub fn references(&mut self, path: &Path, line: u32, character: u32) {
self.request(
"textDocument/references",
json!({
"textDocument": { "uri": path_to_uri(path) },
"position": { "line": line, "character": character },
"context": { "includeDeclaration": true }
}),
);
}
pub fn rename(&mut self, path: &Path, line: u32, character: u32, new_name: &str) {
self.request(
"textDocument/rename",
json!({
"textDocument": { "uri": path_to_uri(path) },
"position": { "line": line, "character": character },
"newName": new_name
}),
);
}
pub fn document_symbol(&mut self, path: &Path) {
self.request_with_path(
"textDocument/documentSymbol",
json!({ "textDocument": { "uri": path_to_uri(path) } }),
Some(path),
);
}
pub fn workspace_symbol(&mut self, query: &str) {
self.request("workspace/symbol", json!({ "query": query }));
}
pub fn code_action(&mut self, path: &Path, range: Range, diagnostics: &[Diagnostic]) {
self.code_action_inner(path, range, diagnostics, None);
}
pub fn code_action_with_only(
&mut self,
path: &Path,
range: Range,
diagnostics: &[Diagnostic],
only: &[String],
) {
self.code_action_inner(path, range, diagnostics, Some(only));
}
fn code_action_inner(
&mut self,
path: &Path,
range: Range,
diagnostics: &[Diagnostic],
only: Option<&[String]>,
) {
let diags_json: Vec<serde_json::Value> = diagnostics
.iter()
.map(|d| {
let sev = match d.severity {
Severity::Error => 1,
Severity::Warning => 2,
Severity::Info => 3,
Severity::Hint => 4,
};
let mut v = json!({
"range": {
"start": { "line": d.range.start.line, "character": d.range.start.character },
"end": { "line": d.range.end.line, "character": d.range.end.character }
},
"severity": sev,
"message": d.message,
});
if let Some(src) = &d.source {
v["source"] = json!(src);
}
v
})
.collect();
let mut context = json!({ "diagnostics": diags_json });
if let Some(only) = only {
context["only"] = json!(only);
}
self.request(
"textDocument/codeAction",
json!({
"textDocument": { "uri": path_to_uri(path) },
"range": {
"start": { "line": range.start.line, "character": range.start.character },
"end": { "line": range.end.line, "character": range.end.character }
},
"context": context,
}),
);
}
pub fn execute_command(&mut self, cmd: &CodeCommand) {
self.request(
"workspace/executeCommand",
json!({
"command": cmd.command,
"arguments": cmd.arguments,
}),
);
}
pub fn code_lens(&mut self, path: &Path) {
self.request_with_path(
"textDocument/codeLens",
json!({ "textDocument": { "uri": path_to_uri(path) } }),
Some(path),
);
}
pub fn inlay_hint(&mut self, path: &Path, line_count: u32) {
self.request_with_path(
"textDocument/inlayHint",
json!({
"textDocument": { "uri": path_to_uri(path) },
"range": {
"start": { "line": 0, "character": 0 },
"end": { "line": line_count.saturating_sub(1), "character": 0 }
}
}),
Some(path),
);
}
pub fn semantic_tokens(&mut self, path: &Path, line_count: u32, viewport: Option<(u32, u32)>) {
let caps = self.sem_caps.lock().map(|c| c.clone()).unwrap_or_default();
if let Some((s, e)) = viewport
&& caps.supports_range
{
self.semantic_tokens_range(path, s, e);
return;
}
let prev_id = self
.sem_states
.lock()
.ok()
.and_then(|s| s.get(path).and_then(|st| st.result_id.clone()));
if caps.supports_delta
&& let Some(id) = prev_id
{
self.semantic_tokens_delta(path, &id);
} else if caps.supports_full {
self.semantic_tokens_full(path);
} else if caps.supports_range {
self.semantic_tokens_range(path, 0, line_count);
}
}
pub fn semantic_tokens_full(&mut self, path: &Path) {
self.request_with_path(
"textDocument/semanticTokens/full",
json!({ "textDocument": { "uri": path_to_uri(path) } }),
Some(path),
);
}
pub fn semantic_tokens_delta(&mut self, path: &Path, previous_result_id: &str) {
self.request_with_path(
"textDocument/semanticTokens/full/delta",
json!({
"textDocument": { "uri": path_to_uri(path) },
"previousResultId": previous_result_id,
}),
Some(path),
);
}
pub fn semantic_tokens_range(&mut self, path: &Path, start_line: u32, end_line: u32) {
self.request_with_path(
"textDocument/semanticTokens/range",
json!({
"textDocument": { "uri": path_to_uri(path) },
"range": {
"start": { "line": start_line, "character": 0 },
"end": { "line": end_line, "character": 0 }
}
}),
Some(path),
);
}
pub fn document_link(&mut self, path: &Path) {
self.request_with_path(
"textDocument/documentLink",
json!({ "textDocument": { "uri": path_to_uri(path) } }),
Some(path),
);
}
pub fn folding_range(&mut self, path: &Path) {
self.request_with_path(
"textDocument/foldingRange",
json!({ "textDocument": { "uri": path_to_uri(path) } }),
Some(path),
);
}
pub fn selection_range(&mut self, path: &Path, line: u32, character: u32) {
self.request_with_path(
"textDocument/selectionRange",
json!({
"textDocument": { "uri": path_to_uri(path) },
"positions": [{ "line": line, "character": character }]
}),
Some(path),
);
}
pub fn document_color(&mut self, path: &Path) {
self.request_with_path(
"textDocument/documentColor",
json!({ "textDocument": { "uri": path_to_uri(path) } }),
Some(path),
);
}
pub fn document_highlight(&mut self, path: &Path, line: u32, character: u32) {
self.request_with_path(
"textDocument/documentHighlight",
json!({
"textDocument": { "uri": path_to_uri(path) },
"position": { "line": line, "character": character }
}),
Some(path),
);
}
pub fn prepare_call_hierarchy(
&mut self,
path: &Path,
line: u32,
character: u32,
direction: super::CallHierarchyDirection,
) {
let dir = match direction {
super::CallHierarchyDirection::Incoming => "i",
super::CallHierarchyDirection::Outgoing => "o",
};
self.request_with_context(
"textDocument/prepareCallHierarchy",
json!({
"textDocument": { "uri": path_to_uri(path) },
"position": { "line": line, "character": character }
}),
Some(path),
Some(dir.to_string()),
);
}
pub fn call_hierarchy_calls(
&mut self,
item: &super::CallHierarchyItem,
direction: super::CallHierarchyDirection,
) {
let (method, tag) = match direction {
super::CallHierarchyDirection::Incoming => {
("callHierarchy/incomingCalls", format!("i:{}", item.name))
}
super::CallHierarchyDirection::Outgoing => {
("callHierarchy/outgoingCalls", format!("o:{}", item.name))
}
};
self.request_with_context(
method,
json!({ "item": item.raw }),
Some(&item.path),
Some(tag),
);
}
pub fn prepare_type_hierarchy(
&mut self,
path: &Path,
line: u32,
character: u32,
direction: super::TypeHierarchyDirection,
) {
let dir = match direction {
super::TypeHierarchyDirection::Supertypes => "s",
super::TypeHierarchyDirection::Subtypes => "b",
};
self.request_with_context(
"textDocument/prepareTypeHierarchy",
json!({
"textDocument": { "uri": path_to_uri(path) },
"position": { "line": line, "character": character }
}),
Some(path),
Some(dir.to_string()),
);
}
pub fn type_hierarchy_types(
&mut self,
item: &super::CallHierarchyItem,
direction: super::TypeHierarchyDirection,
) {
let (method, tag) = match direction {
super::TypeHierarchyDirection::Supertypes => {
("typeHierarchy/supertypes", format!("s:{}", item.name))
}
super::TypeHierarchyDirection::Subtypes => {
("typeHierarchy/subtypes", format!("b:{}", item.name))
}
};
self.request_with_context(
method,
json!({ "item": item.raw }),
Some(&item.path),
Some(tag),
);
}
pub fn on_type_formatting(
&mut self,
path: &Path,
line: u32,
character: u32,
trigger: char,
tab_size: u32,
insert_spaces: bool,
) {
let mut buf = [0u8; 4];
let ch = trigger.encode_utf8(&mut buf).to_string();
self.request_with_path(
"textDocument/onTypeFormatting",
json!({
"textDocument": { "uri": path_to_uri(path) },
"position": { "line": line, "character": character },
"ch": ch,
"options": {
"tabSize": tab_size,
"insertSpaces": insert_spaces,
"trimTrailingWhitespace": true,
"insertFinalNewline": true,
}
}),
Some(path),
);
}
pub fn will_save_wait_until(&mut self, path: &Path) {
self.request_with_path(
"textDocument/willSaveWaitUntil",
json!({
"textDocument": { "uri": path_to_uri(path) },
"reason": 1
}),
Some(path),
);
}
pub fn formatting(&mut self, path: &Path, tab_size: u32, insert_spaces: bool) {
self.request_with_path(
"textDocument/formatting",
json!({
"textDocument": { "uri": path_to_uri(path) },
"options": {
"tabSize": tab_size,
"insertSpaces": insert_spaces,
"trimTrailingWhitespace": true,
"insertFinalNewline": true,
}
}),
Some(path),
);
}
fn request(&mut self, method: &str, params: serde_json::Value) {
self.request_with_path(method, params, None);
}
fn request_with_path(&mut self, method: &str, params: serde_json::Value, path: Option<&Path>) {
self.request_with_context(method, params, path, None);
}
fn request_with_context(
&mut self,
method: &str,
params: serde_json::Value,
path: Option<&Path>,
opaque: Option<String>,
) {
let id = self.next_id;
self.next_id += 1;
if let Ok(mut p) = self.pending.lock() {
p.insert(
id,
(method.to_string(), path.map(|p| p.to_path_buf()), opaque),
);
}
self.send(&json!({ "jsonrpc": "2.0", "id": id, "method": method, "params": params }));
}
fn notify(&mut self, method: &str, params: serde_json::Value) {
self.send(&json!({ "jsonrpc": "2.0", "method": method, "params": params }));
}
fn send(&self, msg: &serde_json::Value) {
if let Ok(mut w) = self.stdin.lock() {
let _ = write_message(&mut *w, msg);
}
}
}
impl Drop for LspClient {
fn drop(&mut self) {
self.send(&json!({ "jsonrpc": "2.0", "id": -1, "method": "shutdown" }));
self.send(&json!({ "jsonrpc": "2.0", "method": "exit" }));
let _ = self.child.kill();
let _ = self.child.wait();
if let Some(j) = self.reader.take() {
let _ = j.join();
}
let _ = &self.name; }
}
fn write_message(w: &mut impl Write, msg: &serde_json::Value) -> std::io::Result<()> {
let body = serde_json::to_vec(msg).unwrap_or_default();
write!(w, "Content-Length: {}\r\n\r\n", body.len())?;
w.write_all(&body)?;
w.flush()
}
fn reader_loop(
stdout: impl Read,
tx: std::sync::mpsc::Sender<LspEvent>,
pending: Pending,
stdin: Sink,
sem_states: SemStates,
sem_caps: SemCaps,
) {
let mut r = BufReader::new(stdout);
let mut sem_legend: Vec<String> = Vec::new();
let mut sem_mod_legend: Vec<String> = Vec::new();
loop {
let mut len: Option<usize> = None;
loop {
let mut line = String::new();
match r.read_line(&mut line) {
Ok(0) => return, Ok(_) => {}
Err(_) => return,
}
let trimmed = line.trim_end_matches(['\r', '\n']);
if trimmed.is_empty() {
break;
}
if let Some(v) = trimmed
.split_once(':')
.filter(|(k, _)| k.eq_ignore_ascii_case("content-length"))
.and_then(|(_, v)| v.trim().parse::<usize>().ok())
{
len = Some(v);
}
}
let Some(len) = len else { continue };
let mut buf = vec![0u8; len];
if r.read_exact(&mut buf).is_err() {
return;
}
let Ok(v) = serde_json::from_slice::<serde_json::Value>(&buf) else {
continue;
};
handle_message(
&v,
&tx,
&pending,
&stdin,
&mut sem_legend,
&mut sem_mod_legend,
&sem_states,
&sem_caps,
);
}
}
#[allow(clippy::too_many_arguments)]
fn handle_message(
v: &serde_json::Value,
tx: &std::sync::mpsc::Sender<LspEvent>,
pending: &Pending,
stdin: &Sink,
sem_legend: &mut Vec<String>,
sem_mod_legend: &mut Vec<String>,
sem_states: &SemStates,
sem_caps: &SemCaps,
) {
if let (Some(id), Some(method)) = (v.get("id"), v.get("method").and_then(|m| m.as_str())) {
if method == "workspace/applyEdit"
&& let Some(edit) = v.get("params").and_then(|p| p.get("edit"))
{
let edits = parse_workspace_edit(edit);
let label = v
.get("params")
.and_then(|p| p.get("label"))
.and_then(|l| l.as_str())
.map(String::from);
if !edits.is_empty() {
let _ = tx.send(LspEvent::ApplyEdit { label, edits });
}
if let Ok(mut w) = stdin.lock() {
let _ = write_message(
&mut *w,
&json!({ "jsonrpc": "2.0", "id": id, "result": { "applied": true } }),
);
}
return;
}
if let Ok(mut w) = stdin.lock() {
let _ = write_message(
&mut *w,
&json!({ "jsonrpc": "2.0", "id": id, "result": null }),
);
}
return;
}
if let Some(method) = v.get("method").and_then(|m| m.as_str()) {
if method == "textDocument/publishDiagnostics"
&& let Some(params) = v.get("params")
&& let Some(uri) = params.get("uri").and_then(|u| u.as_str())
&& let Some(path) = uri_to_path(uri)
{
let diags = params
.get("diagnostics")
.and_then(|d| d.as_array())
.map(|a| a.iter().filter_map(parse_diagnostic).collect())
.unwrap_or_default();
let _ = tx.send(LspEvent::Diagnostics { path, diags });
}
if method == "window/showMessage"
&& let Some(params) = v.get("params")
&& let Some(m) = params.get("message").and_then(|m| m.as_str())
{
let msg_type = params.get("type").and_then(|t| t.as_i64()).unwrap_or(1);
if msg_type == 1 {
let _ = tx.send(LspEvent::Message(format!("LSP: {m}")));
}
}
if method == "$/progress"
&& let Some(params) = v.get("params")
&& let Some(token) = params.get("token").and_then(|t| {
t.as_str()
.map(String::from)
.or_else(|| t.as_i64().map(|n| n.to_string()))
})
&& let Some(value) = params.get("value")
&& let Some(kind) = value.get("kind").and_then(|k| k.as_str())
{
let title = value
.get("title")
.and_then(|s| s.as_str())
.or_else(|| value.get("message").and_then(|s| s.as_str()))
.unwrap_or("")
.to_string();
match kind {
"begin" => {
let _ = tx.send(LspEvent::ProgressBegin { token, title });
}
"report" => {
let _ = tx.send(LspEvent::ProgressReport { token, title });
}
"end" => {
let _ = tx.send(LspEvent::ProgressEnd { token });
}
_ => {}
}
}
return;
}
if let Some(id) = v.get("id").and_then(|i| i.as_i64()) {
let pend = pending.lock().ok().and_then(|mut p| p.remove(&id));
let Some((method, req_path, req_opaque)) = pend else {
return;
};
let Some(result) = v.get("result") else {
return;
}; match method.as_str() {
"textDocument/definition"
| "textDocument/declaration"
| "textDocument/typeDefinition"
| "textDocument/implementation" => {
if let Some((path, line, ch)) = first_location(result) {
let _ = tx.send(LspEvent::GotoDefinition {
path,
line,
character: ch,
});
}
}
"textDocument/hover" => {
if let Some(text) = hover_text(result) {
let _ = tx.send(LspEvent::Hover { text });
}
}
"textDocument/references" => {
let locs: Vec<(PathBuf, u32, u32)> = result
.as_array()
.map(|a| a.iter().filter_map(first_location).collect())
.unwrap_or_default();
if !locs.is_empty() {
let _ = tx.send(LspEvent::References(locs));
}
}
"textDocument/rename" => {
let edits = parse_workspace_edit(result);
if !edits.is_empty() {
let _ = tx.send(LspEvent::Rename(edits));
}
}
"textDocument/completion" => {
let items = parse_completion(result);
if !items.is_empty() {
let _ = tx.send(LspEvent::Completion(items));
}
}
"completionItem/resolve" => {
let (detail, documentation) = parse_completion_resolve(result);
if let Some(label) = req_opaque
&& (detail.is_some() || documentation.is_some())
{
let _ = tx.send(LspEvent::CompletionResolve {
label,
detail,
documentation,
});
}
}
"textDocument/formatting" | "textDocument/onTypeFormatting" => {
let edits = parse_text_edits(result);
if let (false, Some(path)) = (edits.is_empty(), req_path) {
let _ = tx.send(LspEvent::Formatting { path, edits });
}
}
"textDocument/willSaveWaitUntil" => {
let edits = parse_text_edits(result);
if let Some(path) = req_path {
let _ = tx.send(LspEvent::WillSaveWaitUntil { path, edits });
}
}
"textDocument/codeAction" => {
let actions = parse_code_actions(result);
let _ = tx.send(LspEvent::CodeAction(actions));
}
"codeAction/resolve" => {
let (edit, command) = parse_code_action_resolve(result);
let _ = tx.send(LspEvent::CodeActionResolve { edit, command });
}
"textDocument/documentSymbol" => {
let symbols = parse_document_symbols(result);
let _ = tx.send(LspEvent::DocumentSymbols(symbols));
}
"workspace/symbol" => {
let symbols = parse_workspace_symbols(result);
if !symbols.is_empty() {
let _ = tx.send(LspEvent::WorkspaceSymbols(symbols));
}
}
"textDocument/signatureHelp" => {
if let Some(sh) = parse_signature_help(result) {
let _ = tx.send(LspEvent::SignatureHelp(sh));
}
}
"initialize" => {
let provider = result
.get("capabilities")
.and_then(|c| c.get("semanticTokensProvider"));
let legend = provider.and_then(|p| p.get("legend"));
if let Some(types) = legend
.and_then(|l| l.get("tokenTypes"))
.and_then(|t| t.as_array())
{
sem_legend.clear();
for v in types {
if let Some(s) = v.as_str() {
sem_legend.push(s.to_string());
}
}
}
if let Some(mods) = legend
.and_then(|l| l.get("tokenModifiers"))
.and_then(|m| m.as_array())
{
sem_mod_legend.clear();
for v in mods {
if let Some(s) = v.as_str() {
sem_mod_legend.push(s.to_string());
}
}
}
let new_caps = parse_semantic_tokens_caps(provider);
if let Ok(mut caps) = sem_caps.lock() {
*caps = new_caps;
}
}
"textDocument/semanticTokens/full" => {
if let Some(path) = req_path {
let raw = extract_semantic_data(result);
let result_id = result
.get("resultId")
.and_then(|v| v.as_str())
.map(String::from);
let tokens = parse_semantic_tokens_from_raw(&raw, sem_legend, sem_mod_legend);
if let Ok(mut s) = sem_states.lock() {
s.insert(
path.clone(),
SemState {
result_id,
raw_data: raw,
},
);
}
let _ = tx.send(LspEvent::SemanticTokens { path, tokens });
}
}
"textDocument/semanticTokens/range" => {
if let Some(path) = req_path {
let raw = extract_semantic_data(result);
let tokens = parse_semantic_tokens_from_raw(&raw, sem_legend, sem_mod_legend);
if let Ok(mut s) = sem_states.lock() {
s.remove(&path);
}
let _ = tx.send(LspEvent::SemanticTokens { path, tokens });
}
}
"textDocument/semanticTokens/full/delta" => {
if let Some(path) = req_path {
let result_id = result
.get("resultId")
.and_then(|v| v.as_str())
.map(String::from);
if result.get("data").is_some() {
let raw = extract_semantic_data(result);
let tokens =
parse_semantic_tokens_from_raw(&raw, sem_legend, sem_mod_legend);
if let Ok(mut s) = sem_states.lock() {
s.insert(
path.clone(),
SemState {
result_id,
raw_data: raw,
},
);
}
let _ = tx.send(LspEvent::SemanticTokens { path, tokens });
} else if let Some(edits) = parse_semantic_token_edits(result) {
let merged = sem_states.lock().ok().map(|mut s| {
let entry = s.entry(path.clone()).or_default();
let ok = apply_semantic_token_edits(&mut entry.raw_data, edits);
if !ok {
s.remove(&path);
None
} else {
entry.result_id = result_id;
Some(entry.raw_data.clone())
}
});
let Some(Some(raw)) = merged else {
return;
};
let tokens =
parse_semantic_tokens_from_raw(&raw, sem_legend, sem_mod_legend);
let _ = tx.send(LspEvent::SemanticTokens { path, tokens });
}
}
}
"textDocument/inlayHint" => {
if let Some(path) = req_path {
let hints = parse_inlay_hints(result);
let _ = tx.send(LspEvent::InlayHints { path, hints });
}
}
"textDocument/codeLens" => {
if let Some(path) = req_path {
let lenses = parse_code_lenses(result);
let _ = tx.send(LspEvent::CodeLens { path, lenses });
}
}
"codeLens/resolve" => {
if let (Some(path), Some(opaque)) = (req_path, req_opaque)
&& let Ok(lens_index) = opaque.parse::<usize>()
{
let lenses = parse_code_lenses(&serde_json::Value::Array(vec![result.clone()]));
if let Some(lens) = lenses.into_iter().next() {
let _ = tx.send(LspEvent::CodeLensResolve {
path,
lens_index,
lens,
});
}
}
}
"textDocument/documentLink" => {
if let Some(path) = req_path {
let links = parse_document_links(result);
if !links.is_empty() {
let _ = tx.send(LspEvent::DocumentLinks { path, links });
}
}
}
"textDocument/foldingRange" => {
if let Some(path) = req_path {
let ranges = parse_folding_ranges(result);
let _ = tx.send(LspEvent::FoldingRanges { path, ranges });
}
}
"textDocument/selectionRange" => {
if let Some(path) = req_path {
let ranges = parse_selection_ranges(result);
let _ = tx.send(LspEvent::SelectionRanges { path, ranges });
}
}
"textDocument/documentColor" => {
if let Some(path) = req_path {
let colors = parse_document_color(result);
let _ = tx.send(LspEvent::DocumentColor { path, colors });
}
}
"textDocument/documentHighlight" => {
if let Some(path) = req_path {
let ranges = parse_document_highlights(result);
let _ = tx.send(LspEvent::DocumentHighlights { path, ranges });
}
}
"textDocument/prepareCallHierarchy" => {
let direction = match req_opaque.as_deref() {
Some("o") => super::CallHierarchyDirection::Outgoing,
_ => super::CallHierarchyDirection::Incoming,
};
let items = parse_call_hierarchy_items(result);
let _ = tx.send(LspEvent::CallHierarchyPrepared { direction, items });
}
"callHierarchy/incomingCalls" | "callHierarchy/outgoingCalls" => {
let (direction, origin_name) = match req_opaque.as_deref() {
Some(s) if s.starts_with("o:") => {
(super::CallHierarchyDirection::Outgoing, s[2..].to_string())
}
Some(s) if s.starts_with("i:") => {
(super::CallHierarchyDirection::Incoming, s[2..].to_string())
}
_ => (super::CallHierarchyDirection::Incoming, String::new()),
};
let hits = parse_call_hierarchy_calls(result, direction);
let _ = tx.send(LspEvent::CallHierarchyCalls {
direction,
origin_name,
hits,
});
}
"textDocument/prepareTypeHierarchy" => {
let direction = match req_opaque.as_deref() {
Some("b") => super::TypeHierarchyDirection::Subtypes,
_ => super::TypeHierarchyDirection::Supertypes,
};
let items = parse_call_hierarchy_items(result);
let _ = tx.send(LspEvent::TypeHierarchyPrepared { direction, items });
}
"typeHierarchy/supertypes" | "typeHierarchy/subtypes" => {
let (direction, origin_name) = match req_opaque.as_deref() {
Some(s) if s.starts_with("b:") => {
(super::TypeHierarchyDirection::Subtypes, s[2..].to_string())
}
Some(s) if s.starts_with("s:") => (
super::TypeHierarchyDirection::Supertypes,
s[2..].to_string(),
),
_ => (super::TypeHierarchyDirection::Supertypes, String::new()),
};
let hits = parse_type_hierarchy_types(result);
let _ = tx.send(LspEvent::TypeHierarchyTypes {
direction,
origin_name,
hits,
});
}
_ => {}
}
}
}
pub fn parse_call_hierarchy_items(result: &serde_json::Value) -> Vec<super::CallHierarchyItem> {
let arr = match result.as_array() {
Some(a) => a,
None => return Vec::new(),
};
let mut out = Vec::with_capacity(arr.len());
for it in arr {
let Some(name) = it.get("name").and_then(|v| v.as_str()) else {
continue;
};
let kind = it.get("kind").and_then(|v| v.as_u64()).unwrap_or(0) as u32;
let Some(uri) = it.get("uri").and_then(|v| v.as_str()) else {
continue;
};
let Some(path) = uri_to_path(uri) else {
continue;
};
let line = it
.get("selectionRange")
.and_then(|r| r.get("start"))
.and_then(|s| s.get("line"))
.and_then(|n| n.as_u64())
.unwrap_or(0) as u32;
let character = it
.get("selectionRange")
.and_then(|r| r.get("start"))
.and_then(|s| s.get("character"))
.and_then(|n| n.as_u64())
.unwrap_or(0) as u32;
out.push(super::CallHierarchyItem {
name: name.to_string(),
kind,
path,
line,
character,
raw: it.clone(),
});
}
out
}
pub fn parse_type_hierarchy_types(result: &serde_json::Value) -> Vec<super::CallHit> {
let arr = match result.as_array() {
Some(a) => a,
None => return Vec::new(),
};
let mut out = Vec::new();
for it in arr {
let Some(name) = it.get("name").and_then(|v| v.as_str()) else {
continue;
};
let Some(uri) = it.get("uri").and_then(|v| v.as_str()) else {
continue;
};
let Some(path) = uri_to_path(uri) else {
continue;
};
let line = it
.get("selectionRange")
.and_then(|r| r.get("start"))
.and_then(|s| s.get("line"))
.and_then(|n| n.as_u64())
.unwrap_or(0) as u32;
let character = it
.get("selectionRange")
.and_then(|r| r.get("start"))
.and_then(|s| s.get("character"))
.and_then(|n| n.as_u64())
.unwrap_or(0) as u32;
out.push(super::CallHit {
name: name.to_string(),
path,
line,
character,
});
}
out
}
pub fn parse_call_hierarchy_calls(
result: &serde_json::Value,
direction: super::CallHierarchyDirection,
) -> Vec<super::CallHit> {
let arr = match result.as_array() {
Some(a) => a,
None => return Vec::new(),
};
let mut out = Vec::new();
for it in arr {
let (item_key, range_key) = match direction {
super::CallHierarchyDirection::Incoming => ("from", "fromRanges"),
super::CallHierarchyDirection::Outgoing => ("to", "fromRanges"),
};
let Some(item) = it.get(item_key) else {
continue;
};
let Some(name) = item.get("name").and_then(|v| v.as_str()) else {
continue;
};
let Some(uri) = item.get("uri").and_then(|v| v.as_str()) else {
continue;
};
let Some(path) = uri_to_path(uri) else {
continue;
};
let pos = match direction {
super::CallHierarchyDirection::Incoming => it
.get(range_key)
.and_then(|a| a.as_array())
.and_then(|a| a.first())
.and_then(|r| r.get("start")),
super::CallHierarchyDirection::Outgoing => {
item.get("selectionRange").and_then(|r| r.get("start"))
}
};
let (Some(line), Some(character)) = (
pos.and_then(|p| p.get("line")).and_then(|n| n.as_u64()),
pos.and_then(|p| p.get("character"))
.and_then(|n| n.as_u64()),
) else {
continue;
};
out.push(super::CallHit {
name: name.to_string(),
path,
line: line as u32,
character: character as u32,
});
}
out
}
pub fn parse_document_highlights(result: &serde_json::Value) -> Vec<(u32, u32, u32, u32)> {
let arr = match result.as_array() {
Some(a) => a,
None => return Vec::new(),
};
let mut out = Vec::with_capacity(arr.len());
for it in arr {
let Some(range) = it.get("range") else {
continue;
};
let (Some(s_line), Some(s_char), Some(e_line), Some(e_char)) = (
range
.get("start")
.and_then(|s| s.get("line"))
.and_then(|n| n.as_u64()),
range
.get("start")
.and_then(|s| s.get("character"))
.and_then(|n| n.as_u64()),
range
.get("end")
.and_then(|e| e.get("line"))
.and_then(|n| n.as_u64()),
range
.get("end")
.and_then(|e| e.get("character"))
.and_then(|n| n.as_u64()),
) else {
continue;
};
if s_line != e_line {
continue;
}
out.push((s_line as u32, s_char as u32, e_line as u32, e_char as u32));
}
out
}
pub fn parse_document_color(result: &serde_json::Value) -> Vec<super::ColorDecoration> {
let arr = match result.as_array() {
Some(a) => a,
None => return Vec::new(),
};
let mut out = Vec::with_capacity(arr.len());
for it in arr {
let Some(range) = it.get("range") else {
continue;
};
let (Some(s_line), Some(s_char), Some(e_line), Some(e_char)) = (
range
.get("start")
.and_then(|s| s.get("line"))
.and_then(|n| n.as_u64()),
range
.get("start")
.and_then(|s| s.get("character"))
.and_then(|n| n.as_u64()),
range
.get("end")
.and_then(|e| e.get("line"))
.and_then(|n| n.as_u64()),
range
.get("end")
.and_then(|e| e.get("character"))
.and_then(|n| n.as_u64()),
) else {
continue;
};
if s_line != e_line {
continue;
}
let Some(color) = it.get("color") else {
continue;
};
let r = color.get("red").and_then(|n| n.as_f64()).unwrap_or(0.0);
let g = color.get("green").and_then(|n| n.as_f64()).unwrap_or(0.0);
let b = color.get("blue").and_then(|n| n.as_f64()).unwrap_or(0.0);
let r8 = (r.clamp(0.0, 1.0) * 255.0).round() as u32;
let g8 = (g.clamp(0.0, 1.0) * 255.0).round() as u32;
let b8 = (b.clamp(0.0, 1.0) * 255.0).round() as u32;
out.push(super::ColorDecoration {
line: s_line as u32,
start_char: s_char as u32,
end_char: e_char as u32,
rgb: (r8 << 16) | (g8 << 8) | b8,
});
}
out
}
pub fn parse_selection_ranges(result: &serde_json::Value) -> Vec<(u32, u32, u32, u32)> {
let arr = match result.as_array() {
Some(a) => a,
None => return Vec::new(),
};
let Some(first) = arr.first() else {
return Vec::new();
};
let mut out = Vec::new();
let mut cur = Some(first);
while let Some(node) = cur {
let Some(range) = node.get("range") else {
break;
};
let (Some(s_line), Some(s_char)) = (
range
.get("start")
.and_then(|s| s.get("line"))
.and_then(|n| n.as_u64()),
range
.get("start")
.and_then(|s| s.get("character"))
.and_then(|n| n.as_u64()),
) else {
break;
};
let (Some(e_line), Some(e_char)) = (
range
.get("end")
.and_then(|e| e.get("line"))
.and_then(|n| n.as_u64()),
range
.get("end")
.and_then(|e| e.get("character"))
.and_then(|n| n.as_u64()),
) else {
break;
};
out.push((s_line as u32, s_char as u32, e_line as u32, e_char as u32));
cur = node.get("parent");
}
out
}
pub fn parse_folding_ranges(result: &serde_json::Value) -> Vec<(u32, u32)> {
let arr = match result.as_array() {
Some(a) => a,
None => return Vec::new(),
};
let mut out = Vec::with_capacity(arr.len());
for it in arr {
let Some(start) = it
.get("startLine")
.and_then(|v| v.as_u64())
.map(|n| n as u32)
else {
continue;
};
let Some(end) = it.get("endLine").and_then(|v| v.as_u64()).map(|n| n as u32) else {
continue;
};
if end <= start {
continue;
}
out.push((start, end));
}
out
}
fn parse_document_links(result: &serde_json::Value) -> Vec<super::DocumentLink> {
let arr = match result.as_array() {
Some(a) => a,
None => return Vec::new(),
};
let mut out = Vec::with_capacity(arr.len());
for it in arr {
let target = match it.get("target").and_then(|t| t.as_str()) {
Some(t) => t.to_string(),
None => continue,
};
let Some(range) = it.get("range") else {
continue;
};
let Some(start) = range.get("start") else {
continue;
};
let Some(end) = range.get("end") else {
continue;
};
let (Some(s_line), Some(s_char)) = (
start.get("line").and_then(|l| l.as_u64()),
start.get("character").and_then(|c| c.as_u64()),
) else {
continue;
};
let (Some(e_line), Some(e_char)) = (
end.get("line").and_then(|l| l.as_u64()),
end.get("character").and_then(|c| c.as_u64()),
) else {
continue;
};
if s_line != e_line {
continue;
}
out.push(super::DocumentLink {
line: s_line as u32,
start_char: s_char as u32,
end_char: e_char as u32,
target,
});
}
out
}
fn first_location(result: &serde_json::Value) -> Option<(PathBuf, u32, u32)> {
let one = match result {
serde_json::Value::Array(a) => a.first()?,
other => other,
};
let uri = one
.get("uri")
.or_else(|| one.get("targetUri"))
.and_then(|u| u.as_str())?;
let range = one
.get("range")
.or_else(|| one.get("targetSelectionRange"))?;
let start = range.get("start")?;
Some((
uri_to_path(uri)?,
start.get("line")?.as_u64()? as u32,
start.get("character")?.as_u64()? as u32,
))
}
fn parse_workspace_edit(result: &serde_json::Value) -> Vec<(PathBuf, Vec<(Range, String)>)> {
let mut out: Vec<(PathBuf, Vec<(Range, String)>)> = Vec::new();
let mut push = |uri: &str, edits: &serde_json::Value| {
let Some(path) = uri_to_path(uri) else { return };
let mut parsed: Vec<(Range, String)> = Vec::new();
if let Some(arr) = edits.as_array() {
for e in arr {
if let Some(te) = parse_text_edit(e) {
parsed.push(te);
}
}
}
if !parsed.is_empty() {
out.push((path, parsed));
}
};
if let Some(changes) = result.get("changes").and_then(|c| c.as_object()) {
for (uri, edits) in changes {
push(uri, edits);
}
}
if let Some(dcs) = result.get("documentChanges").and_then(|d| d.as_array()) {
for dc in dcs {
if dc.get("kind").is_some() {
continue;
}
if let (Some(uri), Some(edits)) = (
dc.get("textDocument")
.and_then(|t| t.get("uri"))
.and_then(|u| u.as_str()),
dc.get("edits"),
) {
push(uri, edits);
}
}
}
out
}
fn parse_completion(result: &serde_json::Value) -> Vec<super::CompletionItemTuple> {
let arr = match result {
serde_json::Value::Array(a) => a,
serde_json::Value::Object(o) => match o.get("items").and_then(|i| i.as_array()) {
Some(a) => a,
None => return Vec::new(),
},
_ => return Vec::new(),
};
let mut out = Vec::with_capacity(arr.len());
for it in arr {
let Some(label) = it.get("label").and_then(|l| l.as_str()) else {
continue;
};
let is_snippet = it.get("insertTextFormat").and_then(|f| f.as_u64()) == Some(2);
let insert = it
.get("insertText")
.and_then(|t| t.as_str())
.or_else(|| {
it.get("textEdit")
.and_then(|e| e.get("newText"))
.and_then(|t| t.as_str())
})
.unwrap_or(label)
.to_string();
let detail = it
.get("detail")
.and_then(|d| d.as_str())
.map(str::to_string);
let documentation = parse_completion_doc(it.get("documentation"));
let kind = it
.get("kind")
.and_then(|k| k.as_u64())
.filter(|&k| (1..=25).contains(&k))
.map(|k| k as u8)
.unwrap_or(0);
out.push((
label.to_string(),
insert,
detail,
documentation,
it.clone(),
is_snippet,
kind,
));
}
out
}
fn parse_completion_doc(v: Option<&serde_json::Value>) -> Option<String> {
v.and_then(|d| match d {
serde_json::Value::String(s) => Some(s.clone()),
serde_json::Value::Object(o) => o.get("value").and_then(|v| v.as_str()).map(String::from),
_ => None,
})
}
pub(crate) fn parse_completion_resolve(
result: &serde_json::Value,
) -> (Option<String>, Option<String>) {
let detail = result
.get("detail")
.and_then(|d| d.as_str())
.map(String::from);
let documentation = parse_completion_doc(result.get("documentation"));
(detail, documentation)
}
fn parse_text_edits(result: &serde_json::Value) -> Vec<(Range, String)> {
result
.as_array()
.map(|a| a.iter().filter_map(parse_text_edit).collect())
.unwrap_or_default()
}
fn parse_text_edit(v: &serde_json::Value) -> Option<(Range, String)> {
let r = v.get("range")?;
let pos = |k: &str| -> Option<Pos> {
let p = r.get(k)?;
Some(Pos {
line: p.get("line")?.as_u64()? as u32,
character: p.get("character")?.as_u64()? as u32,
})
};
let new_text = v.get("newText").and_then(|t| t.as_str())?.to_string();
Some((
Range {
start: pos("start")?,
end: pos("end")?,
},
new_text,
))
}
fn parse_code_actions(result: &serde_json::Value) -> Vec<CodeAction> {
let arr = match result.as_array() {
Some(a) => a,
None => return Vec::new(),
};
let mut out = Vec::with_capacity(arr.len());
for it in arr {
let title = match it.get("title").and_then(|t| t.as_str()) {
Some(t) => t.to_string(),
None => continue,
};
if it.get("disabled").is_some() {
continue;
}
let kind = it.get("kind").and_then(|k| k.as_str()).map(str::to_string);
let edit = it
.get("edit")
.map(parse_workspace_edit)
.filter(|e| !(e.is_empty() && it.get("edit").map(|j| j.is_null()).unwrap_or(true)));
let command = match it.get("command") {
Some(serde_json::Value::Object(o)) => {
o.get("command")
.and_then(|c| c.as_str())
.map(|c| CodeCommand {
command: c.to_string(),
arguments: o
.get("arguments")
.and_then(|a| a.as_array())
.cloned()
.unwrap_or_default(),
})
}
Some(serde_json::Value::String(s)) => Some(CodeCommand {
command: s.clone(),
arguments: it
.get("arguments")
.and_then(|a| a.as_array())
.cloned()
.unwrap_or_default(),
}),
_ => None,
};
out.push(CodeAction {
title,
kind,
edit,
command,
raw: Some(it.clone()),
});
}
out
}
pub(crate) fn parse_code_action_resolve(
result: &serde_json::Value,
) -> (Option<super::WorkspaceEdit>, Option<super::CodeCommand>) {
let edit = result
.get("edit")
.map(parse_workspace_edit)
.filter(|e| !(e.is_empty() && result.get("edit").map(|j| j.is_null()).unwrap_or(true)));
let command = match result.get("command") {
Some(serde_json::Value::Object(o)) => {
o.get("command")
.and_then(|c| c.as_str())
.map(|c| super::CodeCommand {
command: c.to_string(),
arguments: o
.get("arguments")
.and_then(|a| a.as_array())
.cloned()
.unwrap_or_default(),
})
}
Some(serde_json::Value::String(s)) => Some(super::CodeCommand {
command: s.clone(),
arguments: result
.get("arguments")
.and_then(|a| a.as_array())
.cloned()
.unwrap_or_default(),
}),
_ => None,
};
(edit, command)
}
fn parse_document_symbols(result: &serde_json::Value) -> Vec<DocumentSymbol> {
let Some(arr) = result.as_array() else {
return Vec::new();
};
if arr.is_empty() {
return Vec::new();
}
let is_hierarchical = arr.iter().any(|v| v.get("range").is_some());
let mut out = Vec::new();
if is_hierarchical {
for v in arr {
walk_doc_symbol(v, 0, &mut out);
}
} else {
for v in arr {
if let Some(s) = parse_symbol_information(v) {
out.push(s);
}
}
}
out
}
fn walk_doc_symbol(v: &serde_json::Value, depth: u32, out: &mut Vec<DocumentSymbol>) {
let Some(name) = v.get("name").and_then(|n| n.as_str()) else {
return;
};
let kind = symbol_kind_label(v.get("kind").and_then(|k| k.as_u64()).unwrap_or(0));
let pos = v
.get("selectionRange")
.or_else(|| v.get("range"))
.and_then(|r| r.get("start"))
.and_then(|s| {
Some((
s.get("line")?.as_u64()? as u32,
s.get("character")?.as_u64()? as u32,
))
});
let (line, character) = pos.unwrap_or((0, 0));
out.push(DocumentSymbol {
name: name.to_string(),
kind,
line,
character,
depth,
});
if let Some(children) = v.get("children").and_then(|c| c.as_array()) {
for child in children {
walk_doc_symbol(child, depth + 1, out);
}
}
}
fn parse_workspace_symbols(result: &serde_json::Value) -> Vec<crate::lsp::WorkspaceSymbol> {
let Some(arr) = result.as_array() else {
return Vec::new();
};
let mut out: Vec<crate::lsp::WorkspaceSymbol> = Vec::new();
for v in arr {
let Some(name) = v.get("name").and_then(|n| n.as_str()) else {
continue;
};
let kind = symbol_kind_label(v.get("kind").and_then(|k| k.as_u64()).unwrap_or(0));
let container = v
.get("containerName")
.and_then(|c| c.as_str())
.filter(|s| !s.is_empty())
.map(str::to_string);
let loc = v.get("location");
let uri = loc
.and_then(|l| l.get("uri"))
.and_then(|u| u.as_str())
.or_else(|| v.get("uri").and_then(|u| u.as_str()));
let Some(uri) = uri else { continue };
let Some(path) = uri_to_path(uri) else {
continue;
};
let (line, character) = loc
.and_then(|l| l.get("range"))
.and_then(|r| r.get("start"))
.and_then(|s| {
Some((
s.get("line")?.as_u64()? as u32,
s.get("character")?.as_u64()? as u32,
))
})
.unwrap_or((0, 0));
out.push(crate::lsp::WorkspaceSymbol {
name: name.to_string(),
kind,
path,
line,
character,
container,
});
}
out
}
pub fn parse_code_lenses(result: &serde_json::Value) -> Vec<crate::lsp::CodeLens> {
let mut out = Vec::new();
let Some(arr) = result.as_array() else {
return out;
};
for lens in arr {
let Some(range) = lens.get("range") else {
continue;
};
let Some(start) = range.get("start") else {
continue;
};
let line = start.get("line").and_then(|v| v.as_u64()).unwrap_or(0) as u32;
let cmd_obj = lens.get("command");
let title = cmd_obj
.and_then(|c| c.get("title"))
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let command_str = cmd_obj
.and_then(|c| c.get("command"))
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
.map(|s| s.to_string());
let arguments = cmd_obj
.and_then(|c| c.get("arguments"))
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
let command = command_str.map(|c| crate::lsp::CodeCommand {
command: c,
arguments,
});
let raw = if command.is_none() {
Some(lens.clone())
} else {
None
};
if !title.is_empty() {
out.push(crate::lsp::CodeLens {
line,
title,
command,
raw,
});
}
}
out
}
#[allow(dead_code)]
pub fn parse_semantic_tokens(
result: &serde_json::Value,
legend: &[String],
mod_legend: &[String],
) -> Vec<crate::lsp::SemanticToken> {
let raw = extract_semantic_data(result);
parse_semantic_tokens_from_raw(&raw, legend, mod_legend)
}
pub(crate) fn extract_semantic_data(result: &serde_json::Value) -> Vec<u32> {
let Some(arr) = result.get("data").and_then(|d| d.as_array()) else {
return Vec::new();
};
arr.iter().map(|v| v.as_u64().unwrap_or(0) as u32).collect()
}
pub(crate) fn parse_semantic_tokens_from_raw(
data: &[u32],
legend: &[String],
mod_legend: &[String],
) -> Vec<crate::lsp::SemanticToken> {
if !data.len().is_multiple_of(5) || legend.is_empty() {
return Vec::new();
}
let mut out = Vec::with_capacity(data.len() / 5);
let mut line: u32 = 0;
let mut start: u32 = 0;
for chunk in data.as_chunks::<5>().0 {
let delta_line = chunk[0];
let delta_start = chunk[1];
let length = chunk[2];
let type_idx = chunk[3] as usize;
let mod_bits = chunk[4];
line += delta_line;
if delta_line == 0 {
start += delta_start;
} else {
start = delta_start;
}
if length == 0 {
continue;
}
let type_name = legend
.get(type_idx)
.cloned()
.unwrap_or_else(|| String::from("unknown"));
let modifiers = decode_modifier_bits(mod_bits, mod_legend);
out.push(crate::lsp::SemanticToken {
line,
start_char: start,
length,
type_name,
modifiers,
});
}
out
}
fn parse_semantic_tokens_caps(provider: Option<&serde_json::Value>) -> SemServerCaps {
let Some(provider) = provider else {
return SemServerCaps {
supports_full: false,
supports_delta: false,
supports_range: false,
};
};
let requests = provider.get("requests");
let full = requests.and_then(|r| r.get("full"));
let supports_full = match full {
Some(serde_json::Value::Bool(b)) => *b,
Some(serde_json::Value::Object(_)) => true,
_ => requests.is_none(),
};
let supports_delta = match full {
Some(serde_json::Value::Object(o)) => {
o.get("delta").and_then(|v| v.as_bool()).unwrap_or(false)
}
_ => false,
};
let supports_range = match requests.and_then(|r| r.get("range")) {
Some(serde_json::Value::Bool(b)) => *b,
Some(serde_json::Value::Object(_)) => true,
_ => false,
};
SemServerCaps {
supports_full,
supports_delta,
supports_range,
}
}
pub(crate) fn decode_modifier_bits(bits: u32, mod_legend: &[String]) -> Vec<String> {
if bits == 0 || mod_legend.is_empty() {
return Vec::new();
}
let mut out = Vec::new();
for (i, name) in mod_legend.iter().enumerate() {
if bits & (1 << i) != 0 {
out.push(name.clone());
}
}
out
}
pub(crate) fn parse_semantic_token_edits(
result: &serde_json::Value,
) -> Option<Vec<SemanticTokenEdit>> {
let arr = result.get("edits")?.as_array()?;
let mut out = Vec::with_capacity(arr.len());
for e in arr {
let start = e.get("start").and_then(|v| v.as_u64()).unwrap_or(0) as usize;
let delete_count = e.get("deleteCount").and_then(|v| v.as_u64()).unwrap_or(0) as usize;
let data: Vec<u32> = e
.get("data")
.and_then(|d| d.as_array())
.map(|a| a.iter().map(|v| v.as_u64().unwrap_or(0) as u32).collect())
.unwrap_or_default();
out.push(SemanticTokenEdit {
start,
delete_count,
data,
});
}
Some(out)
}
#[derive(Debug, Clone)]
pub(crate) struct SemanticTokenEdit {
pub start: usize,
pub delete_count: usize,
pub data: Vec<u32>,
}
pub(crate) fn apply_semantic_token_edits(
raw: &mut Vec<u32>,
edits: Vec<SemanticTokenEdit>,
) -> bool {
let mut edits = edits;
edits.sort_by_key(|e| std::cmp::Reverse(e.start));
for e in edits {
if e.start > raw.len() || e.start.saturating_add(e.delete_count) > raw.len() {
return false;
}
raw.splice(e.start..e.start + e.delete_count, e.data);
}
true
}
pub fn parse_inlay_hints(result: &serde_json::Value) -> Vec<crate::lsp::InlayHint> {
let mut out = Vec::new();
let Some(arr) = result.as_array() else {
return out;
};
for hint in arr {
let Some(pos) = hint.get("position") else {
continue;
};
let line = pos.get("line").and_then(|v| v.as_u64()).unwrap_or(0) as u32;
let character = pos.get("character").and_then(|v| v.as_u64()).unwrap_or(0) as u32;
let label = match hint.get("label") {
Some(serde_json::Value::String(s)) => s.clone(),
Some(serde_json::Value::Array(parts)) => parts
.iter()
.filter_map(|p| p.get("value").and_then(|v| v.as_str()))
.collect::<Vec<_>>()
.join(""),
_ => continue,
};
if !label.is_empty() {
out.push(crate::lsp::InlayHint {
line,
character,
label,
});
}
}
out
}
fn parse_signature_help(result: &serde_json::Value) -> Option<crate::lsp::SignatureHelp> {
if result.is_null() {
return None;
}
let sigs_v = result.get("signatures").and_then(|s| s.as_array())?;
if sigs_v.is_empty() {
return None;
}
let active_signature = result
.get("activeSignature")
.and_then(|v| v.as_u64())
.map(|n| n as usize)
.unwrap_or(0);
let top_active_param = result
.get("activeParameter")
.and_then(|v| v.as_u64())
.map(|n| n as usize);
let mut sigs: Vec<crate::lsp::SignatureInfo> = Vec::with_capacity(sigs_v.len());
for s in sigs_v {
let label = s
.get("label")
.and_then(|l| l.as_str())
.unwrap_or("")
.to_string();
if label.is_empty() {
continue;
}
let active_parameter = s
.get("activeParameter")
.and_then(|v| v.as_u64())
.map(|n| n as usize)
.or(top_active_param);
let mut parameters: Vec<(usize, usize)> = Vec::new();
if let Some(ps) = s.get("parameters").and_then(|p| p.as_array()) {
for p in ps {
if let Some((s_off, e_off)) = parse_param_label(p, &label) {
parameters.push((s_off, e_off));
}
}
}
sigs.push(crate::lsp::SignatureInfo {
label,
parameters,
active_parameter,
});
}
if sigs.is_empty() {
return None;
}
let active_signature = active_signature.min(sigs.len().saturating_sub(1));
Some(crate::lsp::SignatureHelp {
signatures: sigs,
active_signature,
})
}
fn parse_param_label(p: &serde_json::Value, sig_label: &str) -> Option<(usize, usize)> {
let lbl = p.get("label")?;
if let Some(arr) = lbl.as_array()
&& arr.len() == 2
{
let s = arr[0].as_u64()? as usize;
let e = arr[1].as_u64()? as usize;
if e <= sig_label.chars().count() && s <= e {
return Some((s, e));
}
}
if let Some(sub) = lbl.as_str()
&& let Some(off) = sig_label.find(sub)
{
let start_char = sig_label[..off].chars().count();
let end_char = start_char + sub.chars().count();
return Some((start_char, end_char));
}
None
}
fn parse_symbol_information(v: &serde_json::Value) -> Option<DocumentSymbol> {
let name = v.get("name").and_then(|n| n.as_str())?;
let kind = symbol_kind_label(v.get("kind").and_then(|k| k.as_u64()).unwrap_or(0));
let r = v
.get("location")
.and_then(|l| l.get("range"))
.and_then(|r| r.get("start"))?;
let line = r.get("line")?.as_u64()? as u32;
let character = r.get("character")?.as_u64()? as u32;
Some(DocumentSymbol {
name: name.to_string(),
kind,
line,
character,
depth: 0,
})
}
fn symbol_kind_label(k: u64) -> &'static str {
match k {
1 => "file",
2 => "module",
3 => "namespace",
4 => "package",
5 => "class",
6 => "method",
7 => "property",
8 => "field",
9 => "ctor",
10 => "enum",
11 => "interface",
12 => "fn",
13 => "var",
14 => "const",
15 => "string",
16 => "num",
17 => "bool",
18 => "array",
19 => "obj",
20 => "key",
21 => "null",
22 => "variant",
23 => "struct",
24 => "event",
25 => "op",
26 => "type",
_ => "?",
}
}
fn hover_text(result: &serde_json::Value) -> Option<String> {
let c = result.get("contents")?;
fn one(v: &serde_json::Value) -> Option<String> {
match v {
serde_json::Value::String(s) => Some(s.clone()),
serde_json::Value::Object(o) => {
o.get("value").and_then(|v| v.as_str()).map(str::to_string)
}
_ => None,
}
}
let text = match c {
serde_json::Value::Array(a) => a.iter().filter_map(one).collect::<Vec<_>>().join("\n\n"),
other => one(other)?,
};
let text = text.trim();
(!text.is_empty()).then(|| text.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_semantic_tokens_decodes_delta_form() {
let legend: Vec<String> = ["keyword", "function", "variable", "string"]
.iter()
.map(|&s| s.to_string())
.collect();
let reply = json!({
"data": [
0, 0, 3, 0, 0,
0, 4, 5, 1, 0,
1, 2, 1, 2, 0
]
});
let tokens = parse_semantic_tokens(&reply, &legend, &[]);
assert_eq!(tokens.len(), 3);
assert_eq!(tokens[0].line, 0);
assert_eq!(tokens[0].start_char, 0);
assert_eq!(tokens[0].length, 3);
assert_eq!(tokens[0].type_name, "keyword");
assert_eq!(tokens[1].line, 0);
assert_eq!(tokens[1].start_char, 4);
assert_eq!(tokens[1].type_name, "function");
assert_eq!(tokens[2].line, 1);
assert_eq!(tokens[2].start_char, 2);
assert_eq!(tokens[2].type_name, "variable");
assert!(tokens.iter().all(|t| t.modifiers.is_empty()));
}
#[test]
fn parse_semantic_tokens_handles_empty_or_malformed() {
let legend = vec!["keyword".to_string()];
assert!(parse_semantic_tokens(&json!({}), &legend, &[]).is_empty());
assert!(parse_semantic_tokens(&json!({"data": [1, 2, 3]}), &legend, &[]).is_empty());
assert!(parse_semantic_tokens(&json!({"data": [0, 0, 1, 0, 0]}), &[], &[]).is_empty());
}
#[test]
fn parse_semantic_tokens_drops_zero_length() {
let legend = vec!["keyword".to_string()];
let reply = json!({ "data": [0, 0, 0, 0, 0, 0, 5, 3, 0, 0] });
let tokens = parse_semantic_tokens(&reply, &legend, &[]);
assert_eq!(tokens.len(), 1);
assert_eq!(tokens[0].start_char, 5);
assert_eq!(tokens[0].length, 3);
}
#[test]
fn decode_modifier_bits_picks_set_bits() {
let legend = vec![
"declaration".to_string(),
"definition".to_string(),
"readonly".to_string(),
"static".to_string(),
"deprecated".to_string(),
"abstract".to_string(),
];
let mods = decode_modifier_bits(0b0001_0100, &legend);
assert_eq!(mods, vec!["readonly".to_string(), "deprecated".to_string()]);
assert!(decode_modifier_bits(0, &legend).is_empty());
assert!(decode_modifier_bits(0xff, &[]).is_empty());
let mods = decode_modifier_bits(0b1000_0000, &legend);
assert!(mods.is_empty());
}
#[test]
fn parse_semantic_tokens_caps_picks_range_when_full_absent() {
let provider = json!({ "requests": { "range": true } });
let caps = parse_semantic_tokens_caps(Some(&provider));
assert!(!caps.supports_full);
assert!(caps.supports_range);
}
#[test]
fn parse_semantic_tokens_caps_handles_common_shapes() {
let caps = parse_semantic_tokens_caps(None);
assert!(!caps.supports_full);
assert!(!caps.supports_delta);
assert!(!caps.supports_range);
let provider = json!({
"legend": { "tokenTypes": [], "tokenModifiers": [] },
"requests": { "full": { "delta": true }, "range": true }
});
let caps = parse_semantic_tokens_caps(Some(&provider));
assert!(caps.supports_full);
assert!(caps.supports_delta);
assert!(caps.supports_range);
let provider = json!({
"legend": { "tokenTypes": [], "tokenModifiers": [] },
"requests": { "full": true }
});
let caps = parse_semantic_tokens_caps(Some(&provider));
assert!(caps.supports_full);
assert!(!caps.supports_delta);
assert!(!caps.supports_range);
let provider = json!({
"legend": { "tokenTypes": [], "tokenModifiers": [] },
"requests": { "range": true }
});
let caps = parse_semantic_tokens_caps(Some(&provider));
assert!(!caps.supports_full);
assert!(!caps.supports_delta);
assert!(caps.supports_range);
let provider = json!({
"legend": { "tokenTypes": [], "tokenModifiers": [] }
});
let caps = parse_semantic_tokens_caps(Some(&provider));
assert!(caps.supports_full);
assert!(!caps.supports_delta);
assert!(!caps.supports_range);
}
#[test]
fn parse_semantic_tokens_resolves_modifiers_when_legend_supplied() {
let legend = vec!["function".to_string()];
let mod_legend = vec![
"declaration".to_string(),
"readonly".to_string(),
"deprecated".to_string(),
];
let reply = json!({ "data": [0, 0, 3, 0, 0b0000_0101] });
let tokens = parse_semantic_tokens(&reply, &legend, &mod_legend);
assert_eq!(tokens.len(), 1);
assert_eq!(
tokens[0].modifiers,
vec!["declaration".to_string(), "deprecated".to_string()]
);
}
#[test]
fn semantic_token_edits_splice_in_descending_order() {
let mut raw: Vec<u32> = (0..25).collect();
let edits = vec![
SemanticTokenEdit {
start: 5,
delete_count: 5,
data: vec![99, 99, 99],
},
SemanticTokenEdit {
start: 15,
delete_count: 0,
data: vec![55, 55],
},
];
assert!(apply_semantic_token_edits(&mut raw, edits));
assert_eq!(
raw,
vec![
0, 1, 2, 3, 4, 99, 99, 99, 10, 11, 12, 13, 14, 55, 55, 15, 16, 17, 18, 19, 20, 21,
22, 23, 24
]
);
}
#[test]
fn semantic_token_edits_reject_out_of_bounds() {
let mut raw: Vec<u32> = vec![0, 1, 2, 3, 4];
let edits = vec![SemanticTokenEdit {
start: 10,
delete_count: 0,
data: vec![99],
}];
assert!(!apply_semantic_token_edits(&mut raw, edits));
}
#[test]
fn parse_semantic_token_edits_picks_up_edits_array() {
let reply = json!({
"resultId": "abc",
"edits": [
{ "start": 0, "deleteCount": 5, "data": [0, 0, 3, 0, 0] },
{ "start": 10, "deleteCount": 0 }
]
});
let edits = parse_semantic_token_edits(&reply).expect("edits");
assert_eq!(edits.len(), 2);
assert_eq!(edits[0].start, 0);
assert_eq!(edits[0].delete_count, 5);
assert_eq!(edits[0].data, vec![0, 0, 3, 0, 0]);
assert_eq!(edits[1].start, 10);
assert_eq!(edits[1].delete_count, 0);
assert!(edits[1].data.is_empty());
}
#[test]
fn parse_semantic_token_edits_none_when_data_form() {
let reply = json!({ "resultId": "abc", "data": [0, 0, 1, 0, 0] });
assert!(parse_semantic_token_edits(&reply).is_none());
}
#[test]
fn delta_then_decode_round_trips_through_cache() {
let legend: Vec<String> = ["keyword", "function", "variable"]
.iter()
.map(|&s| s.to_string())
.collect();
let mut raw: Vec<u32> = vec![0, 0, 3, 0, 0];
let edits = vec![SemanticTokenEdit {
start: 5,
delete_count: 0,
data: vec![1, 2, 5, 1, 0],
}];
assert!(apply_semantic_token_edits(&mut raw, edits));
let tokens = parse_semantic_tokens_from_raw(&raw, &legend, &[]);
assert_eq!(tokens.len(), 2);
assert_eq!(tokens[0].type_name, "keyword");
assert_eq!(tokens[1].line, 1);
assert_eq!(tokens[1].start_char, 2);
assert_eq!(tokens[1].length, 5);
assert_eq!(tokens[1].type_name, "function");
}
#[test]
fn first_location_handles_shapes() {
let loc = json!({"uri": "file:///x.rs", "range": {"start": {"line": 2, "character": 4}, "end": {"line": 2, "character": 9}}});
assert_eq!(
first_location(&loc).unwrap(),
(PathBuf::from("/x.rs"), 2, 4)
);
let arr = json!([loc]);
assert_eq!(first_location(&arr).unwrap().1, 2);
let link = json!([{"targetUri": "file:///y.rs", "targetSelectionRange": {"start": {"line": 7, "character": 0}, "end": {"line": 7, "character": 3}}}]);
assert_eq!(
first_location(&link).unwrap(),
(PathBuf::from("/y.rs"), 7, 0)
);
assert!(first_location(&json!(null)).is_none());
}
#[test]
fn parse_workspace_edit_handles_both_shapes() {
let te = |l: u64, c0: u64, c1: u64, t: &str| json!({"range": {"start": {"line": l, "character": c0}, "end": {"line": l, "character": c1}}, "newText": t});
let we = json!({"changes": {"file:///a.rs": [te(1, 4, 7, "bar")]}});
let got = parse_workspace_edit(&we);
assert_eq!(got.len(), 1);
assert_eq!(got[0].0, PathBuf::from("/a.rs"));
assert_eq!(got[0].1[0].1, "bar");
let we2 = json!({"documentChanges": [
{"kind": "create", "uri": "file:///new.rs"},
{"textDocument": {"uri": "file:///b.rs", "version": 3}, "edits": [te(0, 0, 3, "baz")]}
]});
let got2 = parse_workspace_edit(&we2);
assert_eq!(got2.len(), 1);
assert_eq!(got2[0].0, PathBuf::from("/b.rs"));
assert_eq!(got2[0].1[0].0.start.line, 0);
assert!(parse_workspace_edit(&json!(null)).is_empty());
}
#[test]
fn parse_code_lenses_keeps_those_with_titles() {
let reply = json!([
{
"range": { "start": {"line": 5, "character": 0}, "end": {"line": 5, "character": 0} },
"command": { "title": "5 references", "command": "rust-analyzer.showReferences", "arguments": [42] }
},
{
"range": { "start": {"line": 10, "character": 0}, "end": {"line": 10, "character": 0} }
}
]);
let lenses = parse_code_lenses(&reply);
assert_eq!(lenses.len(), 1);
assert_eq!(lenses[0].line, 5);
assert_eq!(lenses[0].title, "5 references");
let cmd = lenses[0].command.as_ref().expect("command captured");
assert_eq!(cmd.command, "rust-analyzer.showReferences");
assert_eq!(cmd.arguments.len(), 1);
assert_eq!(cmd.arguments[0], 42);
}
#[test]
fn parse_code_lenses_keeps_stub_with_raw() {
let reply = json!([{
"range": { "start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 0} },
"command": { "title": "Run | Debug" },
"data": { "kind": "test", "id": 42 }
}]);
let lenses = parse_code_lenses(&reply);
assert_eq!(lenses.len(), 1);
assert!(lenses[0].command.is_none());
let raw = lenses[0].raw.as_ref().expect("raw retained for resolve");
assert_eq!(raw["data"]["id"], 42);
}
#[test]
fn parse_code_lenses_eager_drops_raw() {
let reply = json!([{
"range": { "start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 0} },
"command": { "title": "5 references", "command": "rust-analyzer.showRefs" }
}]);
let lenses = parse_code_lenses(&reply);
assert_eq!(lenses.len(), 1);
assert!(lenses[0].command.is_some());
assert!(lenses[0].raw.is_none());
}
#[test]
fn parse_inlay_hints_handles_string_and_part_labels() {
let reply = json!([
{
"position": { "line": 0, "character": 5 },
"label": ": i32"
},
{
"position": { "line": 1, "character": 10 },
"label": [{"value": ": "}, {"value": "String"}]
},
{
"position": { "line": 2, "character": 0 }
}
]);
let hints = parse_inlay_hints(&reply);
assert_eq!(hints.len(), 2);
assert_eq!(hints[0].label, ": i32");
assert_eq!(hints[0].line, 0);
assert_eq!(hints[0].character, 5);
assert_eq!(hints[1].label, ": String");
assert_eq!(hints[1].line, 1);
}
#[test]
fn parse_completion_handles_list_and_array() {
let cl = json!({"isIncomplete": false, "items": [
{"label": "push", "insertText": "push", "detail": "fn(&mut self, T)"},
{"label": "println!", "insertText": "println!($0)", "insertTextFormat": 2},
{"label": "len"}
]});
let got = parse_completion(&cl);
assert_eq!(got.len(), 3);
assert_eq!(got[0].0, "push");
assert_eq!(got[0].1, "push");
assert_eq!(got[0].2, Some("fn(&mut self, T)".to_string()));
assert_eq!(got[0].3, None);
assert_eq!(got[1].1, "println!($0)");
assert!(got[1].5, "println! should be flagged is_snippet");
assert!(!got[0].5, "push should not be flagged is_snippet");
assert_eq!(got[2].0, "len");
assert_eq!(got[2].1, "len");
assert_eq!(got[2].2, None);
assert_eq!(got[2].3, None);
let arr = json!([{"label": "x", "textEdit": {"newText": "x_edited"}}]);
assert_eq!(parse_completion(&arr)[0].1, "x_edited");
assert!(parse_completion(&json!(null)).is_empty());
}
#[test]
fn parse_code_actions_handles_both_shapes() {
let we = json!({"changes": {"file:///x.rs": [
{"range": {"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 3}}, "newText": "foo"}
]}});
let r = json!([
{"title": "Quick fix", "kind": "quickfix", "edit": we, "command": {"title": "c", "command": "do.fix", "arguments": [1, 2]}},
{"title": "Only cmd", "command": {"title": "c", "command": "do.run"}},
{"title": "Legacy", "command": "old.cmd", "arguments": ["a"]},
{"title": "Disabled", "disabled": {"reason": "nope"}, "command": {"command": "x"}},
{"title": "Stub"}
]);
let got = parse_code_actions(&r);
assert_eq!(got.len(), 4);
assert_eq!(got[0].title, "Quick fix");
assert_eq!(got[0].kind.as_deref(), Some("quickfix"));
assert!(got[0].edit.is_some());
assert_eq!(got[0].command.as_ref().unwrap().command, "do.fix");
assert_eq!(got[0].command.as_ref().unwrap().arguments.len(), 2);
assert!(got[1].edit.is_none());
assert_eq!(got[1].command.as_ref().unwrap().command, "do.run");
assert_eq!(got[2].command.as_ref().unwrap().command, "old.cmd");
assert_eq!(got[2].command.as_ref().unwrap().arguments[0], json!("a"));
assert_eq!(got[3].title, "Stub");
assert!(got[3].edit.is_none() && got[3].command.is_none());
assert!(parse_code_actions(&json!(null)).is_empty());
assert!(parse_code_actions(&json!({})).is_empty());
}
#[test]
fn parse_document_symbols_handles_both_shapes() {
let r = json!([
{
"name": "App", "kind": 23,
"range": {"start": {"line": 10, "character": 0}, "end": {"line": 100, "character": 0}},
"selectionRange": {"start": {"line": 10, "character": 7}, "end": {"line": 10, "character": 10}},
"children": [
{
"name": "new", "kind": 12,
"range": {"start": {"line": 15, "character": 4}, "end": {"line": 20, "character": 5}},
"selectionRange": {"start": {"line": 15, "character": 11}, "end": {"line": 15, "character": 14}}
}
]
}
]);
let got = parse_document_symbols(&r);
assert_eq!(got.len(), 2);
assert_eq!(got[0].name, "App");
assert_eq!(got[0].kind, "struct");
assert_eq!(got[0].depth, 0);
assert_eq!((got[0].line, got[0].character), (10, 7));
assert_eq!(got[1].name, "new");
assert_eq!(got[1].kind, "fn");
assert_eq!(got[1].depth, 1);
assert_eq!((got[1].line, got[1].character), (15, 11));
let r2 = json!([
{"name": "main", "kind": 12,
"location": {"uri": "file:///x.rs", "range": {"start": {"line": 0, "character": 3}, "end": {"line": 0, "character": 7}}}}
]);
let got2 = parse_document_symbols(&r2);
assert_eq!(got2.len(), 1);
assert_eq!(got2[0].name, "main");
assert_eq!(got2[0].kind, "fn");
assert_eq!((got2[0].line, got2[0].character), (0, 3));
assert!(parse_document_symbols(&json!(null)).is_empty());
assert!(parse_document_symbols(&json!([])).is_empty());
}
#[test]
fn parse_workspace_symbols_handles_both_shapes() {
let r = json!([
{"name": "foo", "kind": 12, "containerName": "mod_a",
"location": {
"uri": "file:///proj/src/lib.rs",
"range": {"start": {"line": 10, "character": 4},
"end": {"line": 10, "character": 7}}
}},
{"name": "Bar", "kind": 5, "location": {"uri": "file:///proj/src/types.rs"}}
]);
let got = parse_workspace_symbols(&r);
assert_eq!(got.len(), 2);
assert_eq!(got[0].name, "foo");
assert_eq!(got[0].kind, "fn");
assert_eq!(got[0].line, 10);
assert_eq!(got[0].character, 4);
assert_eq!(got[0].container.as_deref(), Some("mod_a"));
assert_eq!(got[1].name, "Bar");
assert_eq!(got[1].kind, "class");
assert_eq!((got[1].line, got[1].character), (0, 0));
assert!(got[1].container.is_none());
assert!(parse_workspace_symbols(&json!(null)).is_empty());
assert!(parse_workspace_symbols(&json!([])).is_empty());
}
#[test]
fn hover_text_flattens() {
assert_eq!(
hover_text(&json!({"contents": "hi"})).as_deref(),
Some("hi")
);
assert_eq!(
hover_text(&json!({"contents": {"kind": "markdown", "value": "**x**"}})).as_deref(),
Some("**x**")
);
assert_eq!(
hover_text(&json!({"contents": ["a", {"value": "b"}]})).as_deref(),
Some("a\n\nb")
);
assert!(hover_text(&json!({"contents": ""})).is_none());
}
}