Skip to main content

lean_ctx/tools/
ctx_refactor.rs

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}