1pub(crate) mod code_actions;
11pub(crate) mod completion;
12pub(crate) mod diagnostics;
13pub(crate) mod handlers;
14pub(crate) mod index_worker;
15pub(crate) mod positions;
16pub mod protocol;
17pub(crate) mod semantic_tokens;
18pub(crate) mod state;
19
20use handlers::build_catalog_snapshot;
21use ontocore_catalog::OntologyCatalog;
22
23pub fn catalog_snapshot_json(
25 catalog: &OntologyCatalog,
26) -> Result<serde_json::Value, serde_json::Error> {
27 serde_json::to_value(build_catalog_snapshot(catalog))
28}
29
30#[cfg(test)]
31mod handlers_test;
32
33use crate::positions::utf16_offset_to_byte;
34use crossbeam_channel::Sender;
35use diagnostics::publish_diagnostics_for_state;
36use handlers::{
37 handle_custom_request, handle_initialize, handle_standard_request, StandardRequestOutcome,
38};
39use index_worker::IndexWorker;
40use lsp_server::{Connection, Message, Notification, Request, RequestId, Response, ResponseError};
41use lsp_types::{
42 notification::Notification as _,
43 notification::{
44 DidChangeTextDocument, DidCloseTextDocument, DidOpenTextDocument, Exit, Initialized,
45 },
46 InitializeParams,
47};
48use serde_json::Value;
49use state::{canonical_roots_match, resolve_workspace_folder_uri, ServerState};
50use std::sync::atomic::{AtomicBool, Ordering};
51use std::sync::{Arc, Mutex};
52use std::thread;
53use std::time::{Duration, Instant};
54
55struct PendingReindex {
56 workspace: std::path::PathBuf,
57 scheduled_at: Instant,
58}
59
60pub fn run() -> Result<(), Box<dyn std::error::Error>> {
61 let (connection, io_threads) = Connection::stdio();
62 let state = ServerState::new();
63 let pending_reindex: Arc<Mutex<Option<PendingReindex>>> = Arc::new(Mutex::new(None));
64 let pending_diagnostic_publish = Arc::new(AtomicBool::new(false));
65
66 let index_worker = IndexWorker::spawn(state.clone(), connection.sender.clone());
67
68 let timer_worker = index_worker.clone();
69 let timer_pending = Arc::clone(&pending_reindex);
70 thread::spawn(move || loop {
71 thread::sleep(Duration::from_millis(100));
72 if let Some(workspace) = take_due_reindex_workspace(&timer_pending) {
73 timer_worker.enqueue(workspace);
74 }
75 });
76
77 for msg in &connection.receiver {
78 match msg {
79 Message::Request(req) => {
80 if connection.handle_shutdown(&req)? {
81 break;
82 }
83 if let Some(resp) =
84 handle_lsp_request(&state, &index_worker, &pending_diagnostic_publish, req)
85 {
86 connection.sender.send(Message::Response(resp))?;
87 }
88 publish_pending_diagnostics(
89 &connection.sender,
90 &state,
91 &pending_diagnostic_publish,
92 );
93 }
94 Message::Notification(notif) => {
95 if notif.method == Exit::METHOD {
96 break;
97 }
98 handle_notification(&state, &pending_reindex, notif);
99 publish_pending_diagnostics(
100 &connection.sender,
101 &state,
102 &pending_diagnostic_publish,
103 );
104 }
105 Message::Response(_) => {}
106 }
107 }
108
109 drop(connection);
110 io_threads.join()?;
111 Ok(())
112}
113
114fn handle_lsp_request(
115 state: &ServerState,
116 index_worker: &IndexWorker,
117 pending_diagnostic_publish: &Arc<AtomicBool>,
118 req: Request,
119) -> Option<Response> {
120 let id = req.id.clone();
121
122 if req.method == "initialize" {
123 let params: InitializeParams = match parse_params(Some(req.params)) {
124 Ok(p) => p,
125 Err(e) => return Some(error_response(id, e)),
126 };
127 let result = handle_initialize(state, params);
128 if state.with_catalog(|_| ()).is_some() {
129 pending_diagnostic_publish.store(true, Ordering::SeqCst);
130 }
131 return Some(ok_response(id, result));
132 }
133
134 if req.method == "shutdown" {
135 return Some(ok_response(id, Value::Null));
136 }
137
138 if req.method.starts_with("ontocore/") {
139 return match handle_custom_request(state, index_worker, &req.method, Some(req.params)) {
140 Ok(result) => {
141 if req.method == "ontocore/indexWorkspace" {
142 pending_diagnostic_publish.store(true, Ordering::SeqCst);
143 }
144 Some(ok_response(id, result))
145 }
146 Err(err) => Some(ontocore_error_response(id, err)),
147 };
148 }
149
150 match handle_standard_request(state, &req.method, Some(req.params)) {
151 StandardRequestOutcome::Ok(result) => Some(ok_response(id, result)),
152 StandardRequestOutcome::MethodNotFound => Some(error_response(
153 id,
154 ResponseError {
155 code: -32601,
156 message: format!("Method not found: {}", req.method),
157 data: None,
158 },
159 )),
160 StandardRequestOutcome::InvalidParams(err) => Some(error_response(id, err)),
161 StandardRequestOutcome::LspError(err) => Some(ontocore_error_response(id, err)),
162 }
163}
164
165fn handle_notification(
166 state: &ServerState,
167 pending_reindex: &Arc<Mutex<Option<PendingReindex>>>,
168 notif: Notification,
169) {
170 match notif.method.as_str() {
171 Initialized::METHOD => {
172 if let Some(workspace) = state.effective_index_root() {
173 schedule_reindex(pending_reindex, workspace, Duration::from_millis(500));
174 }
175 }
176 "workspace/didChangeWatchedFiles" => {
177 if let Some(workspace) = state.effective_index_root() {
178 schedule_reindex(pending_reindex, workspace, Duration::from_millis(500));
179 }
180 }
181 DidOpenTextDocument::METHOD => {
182 if let Ok(params) =
183 serde_json::from_value::<lsp_types::DidOpenTextDocumentParams>(notif.params)
184 {
185 if let Ok(path) = document_path_from_uri(state, ¶ms.text_document.uri) {
186 if let Err(err) =
187 state.set_document_text(path.clone(), params.text_document.text)
188 {
189 eprintln!("ontocore-lsp: rejected open document: {err}");
190 } else {
191 state.set_document_version(path, params.text_document.version);
192 }
193 }
194 }
195 if let Some(workspace) = state.effective_index_root() {
196 schedule_reindex(pending_reindex, workspace, Duration::from_millis(750));
197 }
198 }
199 DidChangeTextDocument::METHOD => {
200 if let Ok(params) =
201 serde_json::from_value::<lsp_types::DidChangeTextDocumentParams>(notif.params)
202 {
203 if let Ok(path) = document_path_from_uri(state, ¶ms.text_document.uri) {
204 if let Some(text) = merged_document_text(state, &path, ¶ms) {
205 if let Err(err) = state.set_document_text(path.clone(), text) {
206 eprintln!("ontocore-lsp: rejected document change: {err}");
207 } else {
208 state.set_document_version(path, params.text_document.version);
209 if let Some(workspace) = state.effective_index_root() {
210 schedule_reindex(
211 pending_reindex,
212 workspace,
213 Duration::from_millis(750),
214 );
215 }
216 }
217 } else {
218 eprintln!(
219 "ontocore-lsp: rejected document change: invalid edit range for {}",
220 params.text_document.uri.as_str()
221 );
222 }
223 }
224 }
225 }
226 DidCloseTextDocument::METHOD => {
227 if let Ok(params) =
228 serde_json::from_value::<lsp_types::DidCloseTextDocumentParams>(notif.params)
229 {
230 if let Ok(path) = document_path_from_uri(state, ¶ms.text_document.uri) {
231 state.remove_document(&path);
232 }
233 }
234 if let Some(workspace) = state.effective_index_root() {
235 schedule_reindex(pending_reindex, workspace, Duration::from_millis(750));
236 }
237 }
238 "workspace/didChangeWorkspaceFolders" => {
239 if let Ok(params) =
240 serde_json::from_value::<lsp_types::DidChangeWorkspaceFoldersParams>(notif.params)
241 {
242 let mut roots = state.workspace_roots();
243 for removed in params.event.removed {
244 if let Ok(path) = resolve_workspace_folder_uri(removed.uri.as_str(), &roots) {
245 roots.retain(|r| !canonical_roots_match(r, &path));
246 }
247 }
248 for added in params.event.added {
249 if let Ok(path) = resolve_workspace_folder_uri(added.uri.as_str(), &roots) {
250 if !roots.iter().any(|r| canonical_roots_match(r, &path)) {
251 roots.push(path);
252 }
253 }
254 }
255 if roots.is_empty() {
256 state.clear_workspace_state();
257 eprintln!("ontocore-lsp: all workspace folders removed");
258 } else if let Err(err) = state.set_workspace_roots(roots) {
259 eprintln!("ontocore-lsp: failed to update workspace roots: {err}");
260 } else if let Some(workspace) = state.effective_index_root() {
261 schedule_reindex(pending_reindex, workspace, Duration::from_millis(500));
262 }
263 }
264 }
265 _ => {}
266 }
267}
268
269fn publish_pending_diagnostics(
270 sender: &Sender<Message>,
271 state: &ServerState,
272 pending: &Arc<AtomicBool>,
273) {
274 if !pending.swap(false, Ordering::SeqCst) {
275 return;
276 }
277 publish_diagnostics_for_state(sender, state);
278}
279
280fn document_path_from_uri(
281 state: &ServerState,
282 uri: &lsp_types::Uri,
283) -> Result<std::path::PathBuf, String> {
284 state.resolve_lsp_document_uri(uri.as_str())
285}
286
287fn merged_document_text(
288 state: &ServerState,
289 path: &std::path::Path,
290 params: &lsp_types::DidChangeTextDocumentParams,
291) -> Option<String> {
292 let mut text = state.document_text(path)?;
293 for change in ¶ms.content_changes {
294 if let Some(range) = &change.range {
295 text = apply_text_change(&text, range, &change.text)?;
296 } else {
297 text = change.text.clone();
298 }
299 }
300 Some(text)
301}
302
303fn position_to_byte(text: &str, pos: lsp_types::Position) -> Option<usize> {
305 let bytes = text.as_bytes();
306 let mut line = 0u32;
307 let mut i = 0usize;
308 while line < pos.line {
309 if i >= bytes.len() {
310 return None;
311 }
312 if bytes[i] == b'\n' {
313 line += 1;
314 i += 1;
315 } else if bytes[i] == b'\r' {
316 line += 1;
317 i += 1;
318 if i < bytes.len() && bytes[i] == b'\n' {
319 i += 1;
320 }
321 } else {
322 i += 1;
323 }
324 }
325 let line_start = i;
326 let mut line_end = i;
327 while line_end < bytes.len() && bytes[line_end] != b'\n' && bytes[line_end] != b'\r' {
328 line_end += 1;
329 }
330 let line_str = text.get(line_start..line_end)?;
331 let col = utf16_offset_to_byte(line_str, pos.character).min(line_str.len());
332 let offset = line_start + col;
333 if !text.is_char_boundary(offset) {
334 return None;
335 }
336 Some(offset)
337}
338
339fn apply_text_change(text: &str, range: &lsp_types::Range, new_text: &str) -> Option<String> {
340 let start = position_to_byte(text, range.start)?;
341 let end = position_to_byte(text, range.end)?;
342 if start > end {
343 eprintln!("ontocore-lsp: text change range inverted (start={start}, end={end})");
344 return None;
345 }
346 let mut result = String::with_capacity(text.len() - (end - start) + new_text.len());
347 result.push_str(&text[..start]);
348 result.push_str(new_text);
349 result.push_str(&text[end..]);
350 Some(result)
351}
352
353fn schedule_reindex(
354 pending: &Arc<Mutex<Option<PendingReindex>>>,
355 workspace: std::path::PathBuf,
356 delay: Duration,
357) {
358 if let Ok(mut guard) = pending.lock() {
359 *guard = Some(PendingReindex { workspace, scheduled_at: Instant::now() + delay });
360 }
361}
362
363fn take_due_reindex_workspace(
364 pending: &Arc<Mutex<Option<PendingReindex>>>,
365) -> Option<std::path::PathBuf> {
366 let Ok(mut guard) = pending.lock() else {
367 return None;
368 };
369 let entry = guard.as_ref()?;
370 if Instant::now() < entry.scheduled_at {
371 return None;
372 }
373 let workspace = entry.workspace.clone();
374 *guard = None;
375 Some(workspace)
376}
377
378fn parse_params<T: serde::de::DeserializeOwned>(params: Option<Value>) -> Result<T, ResponseError> {
379 serde_json::from_value(params.unwrap_or(Value::Null)).map_err(|e| ResponseError {
380 code: -32602,
381 message: format!("invalid params: {e}"),
382 data: None,
383 })
384}
385
386fn ok_response(id: RequestId, result: impl serde::Serialize) -> Response {
387 Response { id, result: Some(serde_json::to_value(result).unwrap_or(Value::Null)), error: None }
388}
389
390fn error_response(id: RequestId, error: ResponseError) -> Response {
391 Response { id, result: None, error: Some(error) }
392}
393
394fn ontocore_error_response(id: RequestId, err: protocol::LspErrorPayload) -> Response {
395 Response {
396 id,
397 result: None,
398 error: Some(ResponseError {
399 code: -32000,
400 message: err.message.clone(),
401 data: Some(serde_json::to_value(err).unwrap_or(Value::Null)),
402 }),
403 }
404}
405
406#[cfg(test)]
407mod debounce_tests {
408 use super::*;
409 use std::path::PathBuf;
410
411 #[test]
412 fn take_due_clears_pending_after_delay() {
413 let pending: Arc<Mutex<Option<PendingReindex>>> = Arc::new(Mutex::new(None));
414 let ws = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../fixtures");
415 schedule_reindex(&pending, ws.clone(), Duration::from_millis(30));
416 assert!(pending.lock().unwrap().is_some());
417 thread::sleep(Duration::from_millis(60));
418 let taken = take_due_reindex_workspace(&pending);
419 assert_eq!(taken, Some(ws));
420 assert!(pending.lock().unwrap().is_none());
421 }
422}