1use std::path::Path;
2
3use anyhow::Context;
4use serde_json::{json, Value};
5
6use crate::commands::find::resolve_symbol_location;
7use crate::lsp::client::{path_to_uri, LspClient};
8use crate::lsp::files::FileTracker;
9
10pub async fn handle_hover(
18 name: &str,
19 client: &mut LspClient,
20 file_tracker: &mut FileTracker,
21 project_root: &Path,
22) -> anyhow::Result<Value> {
23 let (abs_path, line, character) = resolve_symbol_location(name, client, project_root).await?;
24
25 file_tracker
26 .ensure_open(&abs_path, client.transport_mut())
27 .await?;
28
29 let uri = path_to_uri(&abs_path)?;
30 let params = json!({
31 "textDocument": { "uri": uri.as_str() },
32 "position": { "line": line, "character": character }
33 });
34
35 let request_id = client
36 .transport_mut()
37 .send_request("textDocument/hover", params)
38 .await?;
39
40 let response = client
41 .wait_for_response_public(request_id)
42 .await
43 .context("textDocument/hover request failed")?;
44
45 let hover_content = extract_hover_content(&response);
46 let rel_path = abs_path
47 .strip_prefix(project_root)
48 .unwrap_or(&abs_path)
49 .to_string_lossy()
50 .to_string();
51
52 Ok(json!({
53 "hover_content": hover_content,
54 "path": rel_path,
55 "line": line + 1,
56 }))
57}
58
59fn extract_hover_content(response: &Value) -> String {
61 let Some(contents) = response.get("contents") else {
62 return String::new();
63 };
64
65 if let Some(value) = contents.get("value").and_then(Value::as_str) {
67 return value.trim().to_string();
68 }
69
70 if let Some(s) = contents.as_str() {
72 return s.trim().to_string();
73 }
74
75 if let Some(arr) = contents.as_array() {
77 let parts: Vec<&str> = arr
78 .iter()
79 .filter_map(|v| {
80 v.as_str()
81 .or_else(|| v.get("value").and_then(Value::as_str))
82 })
83 .collect();
84 return parts.join("\n").trim().to_string();
85 }
86
87 String::new()
88}
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93 use serde_json::json;
94
95 #[test]
96 fn extract_markup_content() {
97 let resp = json!({ "contents": { "kind": "markdown", "value": "fn greet()" } });
98 assert_eq!(extract_hover_content(&resp), "fn greet()");
99 }
100
101 #[test]
102 fn extract_string_contents() {
103 let resp = json!({ "contents": "hello world" });
104 assert_eq!(extract_hover_content(&resp), "hello world");
105 }
106
107 #[test]
108 fn extract_array_contents() {
109 let resp = json!({ "contents": ["type A", { "language": "rust", "value": "fn a()" }] });
110 let out = extract_hover_content(&resp);
111 assert!(out.contains("type A"));
112 assert!(out.contains("fn a()"));
113 }
114
115 #[test]
116 fn extract_missing_contents() {
117 let resp = json!({ "range": {} });
118 assert_eq!(extract_hover_content(&resp), "");
119 }
120}