//! Helper functions for LSP URI/path conversion, parameter building, and //! response parsing. use std::path::Path; use serde_json::Value as JsonValue; use crate::lsp_client::{LspCompletionItem, LspHoverResult, LspLocation, LspSymbol}; use crate::lsp_process::LspProcessError; pub(super) fn canonicalize_root(path: &Path) -> Result { path.canonicalize() .map_err(|e| LspProcessError::InvalidPath(format!("{}: {e}", path.display()))) .map(|p| p.to_string_lossy().into_owned()) } pub(super) fn path_to_uri(path: &str) -> String { let canonical = std::path::Path::new(path); if canonical.is_absolute() { format!("file://{path}") } else { let resolved = std::env::current_dir().map_or_else(|_| canonical.to_path_buf(), |d| d.join(path)); let canonicalized = resolved .canonicalize() .unwrap_or(resolved) .to_string_lossy() .into_owned(); format!("file://{canonicalized}") } } pub(super) fn text_document_position_params(uri: &str, line: u32, character: u32) -> JsonValue { serde_json::json!({ "textDocument": { "uri": uri }, "position": { "line": line, "character": character } }) } pub(super) fn uri_to_path(uri: &str) -> String { uri.strip_prefix("file://").unwrap_or(uri).to_owned() } pub(super) fn language_id_for_path(path: &str) -> String { let ext = std::path::Path::new(path) .extension() .and_then(|e| e.to_str()) .unwrap_or(""); match ext { "rs" => "rust", "ts" => "typescript", "tsx" => "typescriptreact", "js" => "javascript", "jsx" => "javascriptreact", "py" => "python", "go" => "go", "java" => "java", "c" | "h" => "c", "cpp" | "hpp" | "cc" => "cpp", "rb" => "ruby", "lua" => "lua", _ => ext, } .to_owned() } pub(super) fn severity_name(code: u64) -> String { match code { 1 => "error".to_owned(), 2 => "warning".to_owned(), 3 => "info".to_owned(), 4 => "hint".to_owned(), _ => format!("unknown({code})"), } } pub(super) fn parse_hover(value: &JsonValue) -> Option { let contents = value.get("contents")?; // MarkupContent: { kind, value } if let (Some(kind), Some(val)) = (contents.get("kind"), contents.get("value")) { let language = if kind.as_str() == Some("plaintext") { None } else { Some(kind.as_str().unwrap_or("markdown").to_owned()) }; return Some(LspHoverResult { content: val.as_str().unwrap_or("").to_owned(), language, }); } // MarkedString object: { language, value } if let (Some(lang), Some(val)) = (contents.get("language"), contents.get("value")) { return Some(LspHoverResult { content: val.as_str().unwrap_or("").to_owned(), language: Some(lang.as_str().unwrap_or("").to_owned()), }); } // Plain string MarkedString if let Some(s) = contents.as_str() { return Some(LspHoverResult { content: s.to_owned(), language: None, }); } // Array of MarkedString if let Some(arr) = contents.as_array() { let parts: Vec<&str> = arr .iter() .filter_map(|item| { if let Some(s) = item.as_str() { Some(s) } else { item.get("value").and_then(JsonValue::as_str) } }) .collect(); if parts.is_empty() { return None; } return Some(LspHoverResult { content: parts.join("\n"), language: None, }); } None } #[allow(clippy::cast_possible_truncation)] pub(super) fn parse_locations(value: &JsonValue) -> Vec { let Some(locations) = value.as_array() else { return Vec::new(); }; locations .iter() .filter_map(|loc| { let uri = loc.get("uri")?.as_str()?; let path = uri_to_path(uri); let range = loc.get("range")?; let start = range.get("start")?; let end = range.get("end")?; Some(LspLocation { path, line: start.get("line")?.as_u64()? as u32, character: start.get("character")?.as_u64()? as u32, end_line: end .get("line") .and_then(JsonValue::as_u64) .map(|v| v as u32), end_character: end .get("character") .and_then(JsonValue::as_u64) .map(|v| v as u32), preview: None, }) }) .collect() } fn extract_symbols(items: &[JsonValue], path: &str, out: &mut Vec) { for item in items { let name = item.get("name").and_then(JsonValue::as_str).unwrap_or(""); let kind = item .get("kind") .and_then(JsonValue::as_u64) .map_or_else(|| "Unknown".into(), symbol_kind_name); let (sym_path, line, character) = if let Some(range) = item.get("range") { let start = range.get("start"); ( path.to_owned(), u32::try_from( start .and_then(|s| s.get("line")) .and_then(JsonValue::as_u64) .unwrap_or(0), ) .unwrap_or(0), u32::try_from( start .and_then(|s| s.get("character")) .and_then(JsonValue::as_u64) .unwrap_or(0), ) .unwrap_or(0), ) } else { (path.to_owned(), 0, 0) }; out.push(LspSymbol { name: name.to_owned(), kind: kind.clone(), path: sym_path, line, character, }); if let Some(children) = item.get("children").and_then(JsonValue::as_array) { extract_symbols(children, path, out); } } } pub(super) fn parse_symbols(value: &JsonValue, default_path: &str) -> Vec { let Some(items) = value.as_array() else { return Vec::new(); }; let mut result = Vec::new(); extract_symbols(items, default_path, &mut result); result } pub(super) fn parse_completions(value: &JsonValue) -> Vec { let Some(items) = value.as_array() else { return Vec::new(); }; items .iter() .map(|item| LspCompletionItem { label: item .get("label") .and_then(JsonValue::as_str) .unwrap_or("") .to_owned(), kind: item .get("kind") .and_then(JsonValue::as_u64) .map(completion_kind_name), detail: item .get("detail") .and_then(JsonValue::as_str) .map(str::to_owned), insert_text: item .get("insertText") .and_then(JsonValue::as_str) .map(str::to_owned), }) .collect() } pub(super) fn symbol_kind_name(kind: u64) -> String { match kind { 1 => "File".into(), 2 => "Module".into(), 3 => "Namespace".into(), 4 => "Package".into(), 5 => "Class".into(), 6 => "Method".into(), 7 => "Property".into(), 8 => "Field".into(), 9 => "Constructor".into(), 10 => "Enum".into(), 11 => "Interface".into(), 12 => "Function".into(), 13 => "Variable".into(), 14 => "Constant".into(), 15 => "String".into(), 16 => "Number".into(), 17 => "Boolean".into(), 18 => "Array".into(), 19 => "Object".into(), 20 => "Key".into(), 21 => "Null".into(), 22 => "EnumMember".into(), 23 => "Struct".into(), 24 => "Event".into(), 25 => "Operator".into(), 26 => "TypeParameter".into(), _ => format!("Unknown({kind})"), } } pub(super) fn completion_kind_name(kind: u64) -> String { match kind { 1 => "Text".into(), 2 => "Method".into(), 3 => "Function".into(), 4 => "Constructor".into(), 5 => "Field".into(), 6 => "Variable".into(), 7 => "Class".into(), 8 => "Interface".into(), 9 => "Module".into(), 10 => "Property".into(), 11 => "Unit".into(), 12 => "Value".into(), 13 => "Enum".into(), 14 => "Keyword".into(), 15 => "Snippet".into(), 16 => "Color".into(), 17 => "File".into(), 18 => "Reference".into(), 19 => "Folder".into(), 20 => "EnumMember".into(), 21 => "Constant".into(), 22 => "Struct".into(), 23 => "Event".into(), 24 => "Operator".into(), 25 => "TypeParameter".into(), _ => format!("Unknown({kind})"), } } #[allow(clippy::cast_possible_truncation)] pub(super) fn parse_code_actions(value: &JsonValue) -> Vec { let Some(items) = value.as_array() else { return Vec::new(); }; items .iter() .filter_map(|item| { // Code actions can be Command or CodeAction objects; we only parse CodeAction let title = item.get("title")?.as_str()?.to_owned(); let kind = item .get("kind") .and_then(JsonValue::as_str) .map(str::to_owned); let is_preferred = item .get("isPreferred") .and_then(JsonValue::as_bool) .unwrap_or(false); let edit = item.get("edit").and_then(parse_workspace_edit); let command = item.get("command").and_then(parse_command); Some(crate::lsp_client::LspCodeAction { title, kind, is_preferred, edit, command, }) }) .collect() } pub(super) fn parse_workspace_edit( value: &JsonValue, ) -> Option { let changes = if let Some(changes_map) = value.get("changes").and_then(JsonValue::as_object) { changes_map .iter() .filter_map(|(uri, edits)| { let path = uri_to_path(uri); let edit_list = edits.as_array()?; let text_edits: Vec = edit_list .iter() .filter_map(|e| { let new_text = e.get("newText")?.as_str()?.to_owned(); let range = e.get("range")?; let start = range.get("start")?; let end = range.get("end")?; Some(crate::lsp_client::LspTextEdit { new_text, start_line: start.get("line")?.as_u64()? as u32, start_character: start.get("character")?.as_u64()? as u32, end_line: end.get("line")?.as_u64()? as u32, end_character: end.get("character")?.as_u64()? as u32, }) }) .collect(); if text_edits.is_empty() { None } else { Some(crate::lsp_client::LspFileEdit { path, edits: text_edits, }) } }) .collect() } else { Vec::new() }; if changes.is_empty() { None } else { Some(crate::lsp_client::LspWorkspaceEdit { changes }) } } pub(super) fn parse_command(value: &JsonValue) -> Option { let title = value.get("title")?.as_str()?.to_owned(); let command = value.get("command")?.as_str()?.to_owned(); let arguments = value .get("arguments") .and_then(JsonValue::as_array) .cloned() .unwrap_or_default(); Some(crate::lsp_client::LspCommand { title, command, arguments, }) } #[allow(clippy::cast_possible_truncation)] pub(super) fn parse_signature_help( value: &JsonValue, ) -> Option { let signatures_arr = value.get("signatures")?.as_array()?; let signatures: Vec = signatures_arr .iter() .filter_map(|sig| { let label = sig.get("label")?.as_str()?.to_owned(); let documentation = sig .get("documentation") .and_then(|d| { d.get("value") .and_then(JsonValue::as_str) .or_else(|| d.as_str()) }) .map(str::to_owned); let parameters = sig .get("parameters") .and_then(JsonValue::as_array) .map(|arr| { arr.iter() .map(|p| { let plabel = p .get("label") .and_then(|l| { l.as_str() .or_else(|| l.get("value").and_then(JsonValue::as_str)) }) .unwrap_or("") .to_owned(); let pdoc = p .get("documentation") .and_then(|d| { d.get("value") .and_then(JsonValue::as_str) .or_else(|| d.as_str()) }) .map(str::to_owned); crate::lsp_client::LspParameterInfo { label: plabel, documentation: pdoc, } }) .collect() }) .unwrap_or_default(); let active_parameter = sig .get("activeParameter") .and_then(JsonValue::as_u64) .map(|v| v as u32); Some(crate::lsp_client::LspSignatureInformation { label, documentation, parameters, active_parameter, }) }) .collect(); let active_signature = value .get("activeSignature") .and_then(JsonValue::as_u64) .map(|v| v as u32); let active_parameter = value .get("activeParameter") .and_then(JsonValue::as_u64) .map(|v| v as u32); Some(crate::lsp_client::LspSignatureHelpResult { signatures, active_signature, active_parameter, }) } #[allow(clippy::cast_possible_truncation)] pub(super) fn parse_code_lens(value: &JsonValue) -> Vec { let Some(items) = value.as_array() else { return Vec::new(); }; items .iter() .filter_map(|item| { let range = item.get("range")?; let start = range.get("start")?; let line = start.get("line")?.as_u64()? as u32; let character = start.get("character")?.as_u64()? as u32; let command = item.get("command").and_then(parse_command); let data = item.get("data").cloned(); Some(crate::lsp_client::LspCodeLens { line, character, command, data, }) }) .collect() } pub(super) fn parse_workspace_symbols(value: &JsonValue) -> Vec { let Some(items) = value.as_array() else { return Vec::new(); }; items .iter() .filter_map(|item| { let name = item.get("name")?.as_str()?.to_owned(); let kind = item .get("kind") .and_then(JsonValue::as_u64) .map_or_else(|| "Unknown".into(), symbol_kind_name); let path = item .get("location") .and_then(|l| l.get("uri")) .and_then(JsonValue::as_str) .map(uri_to_path) .or_else(|| item.get("uri").and_then(JsonValue::as_str).map(uri_to_path)) .unwrap_or_default(); let line = item .get("location") .and_then(|l| l.get("range")) .and_then(|r| r.get("start")) .and_then(|s| s.get("line")) .and_then(JsonValue::as_u64) .map_or(0, |v| v as u32); let character = item .get("location") .and_then(|l| l.get("range")) .and_then(|r| r.get("start")) .and_then(|s| s.get("character")) .and_then(JsonValue::as_u64) .map_or(0, |v| v as u32); Some(crate::lsp_client::LspSymbol { name, kind, path, line, character, }) }) .collect() } pub(super) fn rename_params(uri: &str, line: u32, character: u32, new_name: &str) -> JsonValue { serde_json::json!({ "textDocument": { "uri": uri }, "position": { "line": line, "character": character }, "newName": new_name }) } pub(super) fn workspace_symbol_params(query: &str) -> JsonValue { serde_json::json!({ "query": query }) }