1use lsp_types::{Location, Position};
2use serde_json::Value;
3use std::path::Path;
4
5use crate::lsp::client::uri_to_file_path;
6
7pub fn handle(args: &Value, project_root: &str) -> String {
8 let action = args
9 .get("action")
10 .and_then(Value::as_str)
11 .unwrap_or("references");
12
13 let Some(path) = args.get("path").and_then(Value::as_str) else {
14 return "ERROR: 'path' parameter is required.".to_string();
15 };
16
17 let line = args.get("line").and_then(Value::as_u64).unwrap_or(1) as u32;
18 let column = args.get("column").and_then(Value::as_u64).unwrap_or(0) as u32;
19
20 let abs_path = if Path::new(path).is_absolute() {
21 path.to_string()
22 } else {
23 format!("{project_root}/{path}")
24 };
25
26 let uri = match crate::lsp::router::open_file(&abs_path, project_root) {
27 Ok(u) => u,
28 Err(e) => return format!("ERROR: {e}"),
29 };
30
31 let position = Position::new(line.saturating_sub(1), column);
32
33 match action {
34 "rename" => handle_rename(args, &abs_path, project_root, &uri, position),
35 "references" => handle_references(&abs_path, project_root, &uri, position),
36 "definition" => handle_definition(&abs_path, project_root, &uri, position),
37 "implementations" => handle_implementations(&abs_path, project_root, &uri, position),
38 _ => format!(
39 "ERROR: Unknown action '{action}'. Available: rename, references, definition, implementations."
40 ),
41 }
42}
43
44fn handle_rename(
45 args: &Value,
46 file_path: &str,
47 project_root: &str,
48 uri: &lsp_types::Uri,
49 position: Position,
50) -> String {
51 let Some(new_name) = args.get("new_name").and_then(Value::as_str) else {
52 return "ERROR: 'new_name' parameter is required for rename.".to_string();
53 };
54
55 let result = crate::lsp::router::with_client(file_path, project_root, |client, _| {
56 client.rename(uri, position, new_name)
57 });
58
59 match result {
60 Ok(Some(edit)) => format_workspace_edit(&edit, project_root),
61 Ok(None) => "No rename edits returned by language server.".to_string(),
62 Err(e) => format!("ERROR: {e}"),
63 }
64}
65
66fn handle_references(
67 file_path: &str,
68 project_root: &str,
69 uri: &lsp_types::Uri,
70 position: Position,
71) -> String {
72 let result = crate::lsp::router::with_client(file_path, project_root, |client, _| {
73 client.references(uri, position)
74 });
75
76 match result {
77 Ok(locations) => format_locations(&locations, project_root),
78 Err(e) => format!("ERROR: {e}"),
79 }
80}
81
82fn handle_definition(
83 file_path: &str,
84 project_root: &str,
85 uri: &lsp_types::Uri,
86 position: Position,
87) -> String {
88 let result = crate::lsp::router::with_client(file_path, project_root, |client, _| {
89 client.definition(uri, position)
90 });
91
92 match result {
93 Ok(resp) => {
94 let locations = match resp {
95 lsp_types::GotoDefinitionResponse::Scalar(loc) => vec![loc],
96 lsp_types::GotoDefinitionResponse::Array(locs) => locs,
97 lsp_types::GotoDefinitionResponse::Link(links) => links
98 .into_iter()
99 .map(|l| Location {
100 uri: l.target_uri,
101 range: l.target_selection_range,
102 })
103 .collect(),
104 };
105 format_locations(&locations, project_root)
106 }
107 Err(e) => format!("ERROR: {e}"),
108 }
109}
110
111fn handle_implementations(
112 file_path: &str,
113 project_root: &str,
114 uri: &lsp_types::Uri,
115 position: Position,
116) -> String {
117 let result = crate::lsp::router::with_client(file_path, project_root, |client, _| {
118 client.implementations(uri, position)
119 });
120
121 match result {
122 Ok(locations) => format_locations(&locations, project_root),
123 Err(e) => format!("ERROR: {e}"),
124 }
125}
126
127fn format_locations(locations: &[Location], project_root: &str) -> String {
128 if locations.is_empty() {
129 return "No results found.".to_string();
130 }
131
132 let mut out = format!("{} location(s):\n", locations.len());
133 for loc in locations {
134 let path = uri_to_file_path(&loc.uri).map_or_else(
135 || loc.uri.as_str().to_string(),
136 |p| {
137 p.strip_prefix(project_root)
138 .map(|s| s.strip_prefix('/').unwrap_or(s).to_string())
139 .unwrap_or(p)
140 },
141 );
142
143 let line = loc.range.start.line + 1;
144 let col = loc.range.start.character;
145 out.push_str(&format!(" {path}:{line}:{col}\n"));
146 }
147 out
148}
149
150fn format_workspace_edit(edit: &lsp_types::WorkspaceEdit, project_root: &str) -> String {
151 let mut out = String::from("Rename edits:\n");
152 let mut file_count = 0;
153 let mut edit_count = 0;
154
155 if let Some(ref changes) = edit.changes {
156 for (uri, edits) in changes {
157 let path = uri_to_file_path(uri).map_or_else(
158 || uri.as_str().to_string(),
159 |p| {
160 p.strip_prefix(project_root)
161 .map(|s| s.strip_prefix('/').unwrap_or(s).to_string())
162 .unwrap_or(p)
163 },
164 );
165
166 file_count += 1;
167 out.push_str(&format!(" {path}: {} edit(s)\n", edits.len()));
168 for e in edits {
169 edit_count += 1;
170 let line = e.range.start.line + 1;
171 out.push_str(&format!(" L{line}: -> \"{}\"\n", e.new_text));
172 }
173 }
174 }
175
176 if let Some(ref doc_changes) = edit.document_changes {
177 match doc_changes {
178 lsp_types::DocumentChanges::Edits(edits) => {
179 for text_edit in edits {
180 let path = uri_to_file_path(&text_edit.text_document.uri)
181 .unwrap_or_else(|| text_edit.text_document.uri.as_str().to_string());
182 file_count += 1;
183 let edits_len = text_edit.edits.len();
184 edit_count += edits_len;
185 out.push_str(&format!(" {path}: {edits_len} edit(s)\n"));
186 }
187 }
188 lsp_types::DocumentChanges::Operations(ops) => {
189 for op in ops {
190 if let lsp_types::DocumentChangeOperation::Edit(text_edit) = op {
191 let path = uri_to_file_path(&text_edit.text_document.uri)
192 .unwrap_or_else(|| text_edit.text_document.uri.as_str().to_string());
193 file_count += 1;
194 let edits_len = text_edit.edits.len();
195 edit_count += edits_len;
196 out.push_str(&format!(" {path}: {edits_len} edit(s)\n"));
197 }
198 }
199 }
200 }
201 }
202
203 out.push_str(&format!(
204 "\nTotal: {edit_count} edit(s) across {file_count} file(s)."
205 ));
206 out
207}