1use std::path::Path;
20
21use serde_json::{Value, json};
22
23use lattice_agent::EditorAccess;
24use lattice_agent::editor_access::{ordered, path_to_uri};
25use lattice_protocol::{Position, Selection};
26
27#[derive(Clone)]
32pub struct ReadContext {
33 pub editor: EditorAccess,
36 pub diagnostics: Option<lattice_lsp::modes::DiagnosticsQueryHandle>,
38}
39
40fn pos_json(p: Position) -> Value {
43 json!({ "line": p.line, "character": p.byte })
44}
45
46pub fn selection_result(
51 file_path: Option<&Path>,
52 selected_text: &str,
53 selection: Option<&Selection>,
54) -> Value {
55 let Some(sel) = selection else {
56 return json!({ "text": "", "filePath": Value::Null, "selection": Value::Null });
57 };
58 let (start, end) = ordered(sel);
59 json!({
60 "text": selected_text,
61 "filePath": file_path.map(|p| p.display().to_string()),
62 "fileUrl": file_path.map(path_to_uri),
63 "selection": {
64 "start": pos_json(start),
65 "end": pos_json(end),
66 "isEmpty": sel.is_cursor(),
67 }
68 })
69}
70
71pub fn open_editors_result(editors: &[(String, bool)]) -> Value {
74 let tabs: Vec<Value> = editors
75 .iter()
76 .map(|(path, active)| {
77 json!({
78 "filePath": path,
79 "uri": format!("file://{path}"),
80 "isActive": active,
81 })
82 })
83 .collect();
84 json!({ "editors": tabs })
85}
86
87pub fn workspace_folders_result(folders: &[String]) -> Value {
89 let list: Vec<Value> = folders
90 .iter()
91 .map(|f| {
92 json!({
93 "path": f,
94 "uri": format!("file://{f}"),
95 "name": Path::new(f).file_name().and_then(|n| n.to_str()).unwrap_or(f),
96 })
97 })
98 .collect();
99 json!({ "folders": list })
100}
101
102pub fn diagnostics_result(files: &[(String, Vec<lattice_lsp::Diagnostic>)]) -> Value {
105 let out: Vec<Value> = files
106 .iter()
107 .map(|(uri, diags)| {
108 json!({
109 "uri": uri,
110 "diagnostics": serde_json::to_value(diags).unwrap_or_else(|_| json!([])),
111 })
112 })
113 .collect();
114 json!({ "diagnostics": out })
115}
116
117pub fn dirty_result(is_dirty: bool) -> Value {
119 json!({ "isDirty": is_dirty })
120}
121
122pub fn get_current_selection(ctx: &ReadContext) -> Value {
127 match ctx.editor.current_selection() {
128 Some(sel) => selection_result(
129 sel.file_path.as_deref(),
130 &sel.selected_text,
131 sel.selection.as_ref(),
132 ),
133 None => selection_result(None, "", None),
134 }
135}
136
137pub fn get_open_editors(ctx: &ReadContext) -> Value {
140 let editors: Vec<(String, bool)> = ctx
141 .editor
142 .open_editors()
143 .into_iter()
144 .map(|e| (e.path, e.is_active))
145 .collect();
146 open_editors_result(&editors)
147}
148
149pub fn get_workspace_folders(ctx: &ReadContext) -> Value {
151 workspace_folders_result(ctx.editor.workspace_folders())
152}
153
154pub fn get_diagnostics(ctx: &ReadContext, arguments: &Value) -> Value {
156 let Some(dq) = ctx.diagnostics.as_ref() else {
157 return diagnostics_result(&[]);
158 };
159 let files: Vec<(String, Vec<lattice_lsp::Diagnostic>)> =
160 match arguments.get("uri").and_then(|v| v.as_str()) {
161 Some(uri) => vec![(uri.to_string(), dq.for_uri(uri))],
162 None => dq
163 .uris_with_diagnostics()
164 .into_iter()
165 .map(|uri| {
166 let diags = dq.for_uri(&uri);
167 (uri, diags)
168 })
169 .collect(),
170 };
171 diagnostics_result(&files)
172}
173
174pub fn check_document_dirty(ctx: &ReadContext, arguments: &Value) -> Value {
177 let Some(path) = arguments.get("filePath").and_then(|v| v.as_str()) else {
178 return dirty_result(false);
179 };
180 dirty_result(ctx.editor.document_dirty(path))
181}
182
183#[cfg(test)]
184mod tests {
185 #![allow(clippy::unwrap_used)]
186 use super::*;
187 use lattice_agent::EditorStateCache;
188 use lattice_protocol::Event;
189 use lattice_protocol::ids::DocumentId;
190 use std::path::PathBuf;
191 use std::sync::{Arc, Mutex};
192
193 fn ctx_with(state: EditorStateCache, workspace: Vec<String>) -> ReadContext {
194 ReadContext {
195 editor: EditorAccess::new(Arc::new(Mutex::new(state)), None, workspace, None),
196 diagnostics: None,
197 }
198 }
199
200 #[test]
201 fn selection_result_none_is_empty_not_error() {
202 let v = selection_result(None, "", None);
203 assert_eq!(v["text"], "");
204 assert!(v["selection"].is_null());
205 }
206
207 #[test]
208 fn selection_result_orders_start_before_end() {
209 let sel = Selection {
210 anchor: Position::new(5, 2),
211 head: Position::new(1, 0),
212 visual: None,
213 };
214 let v = selection_result(Some(Path::new("/a.rs")), "x", Some(&sel));
215 assert_eq!(v["selection"]["start"]["line"], 1);
216 assert_eq!(v["selection"]["end"]["line"], 5);
217 assert_eq!(v["filePath"], "/a.rs");
218 assert_eq!(v["fileUrl"], "file:///a.rs");
219 }
220
221 #[test]
222 fn open_editors_lists_only_file_buffers_and_marks_active() {
223 let mut s = EditorStateCache::default();
224 s.apply_event(&Event::DocumentOpened {
225 id: DocumentId::new(1),
226 path: Some(PathBuf::from("/a.rs")),
227 version: 1,
228 text: String::new(),
229 });
230 s.apply_event(&Event::DocumentOpened {
231 id: DocumentId::new(2),
232 path: None,
233 version: 1,
234 text: String::new(),
235 });
236 s.apply_event(&Event::SelectionsChanged {
237 id: DocumentId::new(1),
238 version: 2,
239 selections: lattice_protocol::SelectionSet::default(),
240 });
241 let v = get_open_editors(&ctx_with(s, vec![]));
242 let editors = v["editors"].as_array().unwrap();
243 assert_eq!(editors.len(), 1, "synthetic (no-path) buffer excluded");
244 assert_eq!(editors[0]["filePath"], "/a.rs");
245 assert_eq!(editors[0]["isActive"], true);
246 }
247
248 #[test]
249 fn workspace_folders_from_config() {
250 let v = get_workspace_folders(&ctx_with(
251 EditorStateCache::default(),
252 vec!["/work/project".to_string()],
253 ));
254 let folders = v["folders"].as_array().unwrap();
255 assert_eq!(folders.len(), 1);
256 assert_eq!(folders[0]["path"], "/work/project");
257 assert_eq!(folders[0]["name"], "project");
258 }
259
260 #[test]
261 fn diagnostics_with_no_service_is_empty_not_error() {
262 let v = get_diagnostics(&ctx_with(EditorStateCache::default(), vec![]), &json!({}));
263 assert_eq!(v["diagnostics"].as_array().map(|a| a.len()), Some(0));
264 }
265
266 #[test]
267 fn check_dirty_unknown_path_is_false() {
268 let v = check_document_dirty(
269 &ctx_with(EditorStateCache::default(), vec![]),
270 &json!({ "filePath": "/nope.rs" }),
271 );
272 assert_eq!(v["isDirty"], false);
273 }
274
275 #[test]
276 fn get_current_selection_no_active_is_empty() {
277 let v = get_current_selection(&ctx_with(EditorStateCache::default(), vec![]));
278 assert_eq!(v["text"], "");
279 assert!(v["selection"].is_null());
280 }
281}