1use crate::diagnostics_scheduler::{diagnostics_schedule_event, run_diagnostics_schedule_effects};
2use crate::lsp_output::ScheduledLspOutput;
3use crate::{
4 CANCEL_REQUEST_METHOD, CASCADE_AT_POSITION_REQUEST, DEBUG_STATE_REQUEST,
5 EXPLAIN_HOVER_TRACE_REQUEST, EXPLAIN_REQUEST, LspDeferredDiagnosticsDispatchV0,
6 LspQueryReadView, LspQuerySnapshotV0, LspShellState, LspWorkspaceIndexJobV0,
7 LspWorkspaceIndexResultV0, REQUEST_CANCELLED_ERROR_CODE, RUNTIME_LOOP_PROBE_REQUEST,
8 SDK_WORKFLOW_REQUEST, SOURCE_DIAGNOSTICS_REQUEST, STYLE_CONTEXT_INDEX_REQUEST,
9 STYLE_DIAGNOSTICS_REQUEST, STYLE_HOVER_CANDIDATES_REQUEST, apply_diagnostic_settings,
10 apply_feature_settings, apply_resolution_settings, current_node_lsp_capability_contract,
11 did_change_text_document, did_change_watched_files, did_change_workspace_folders,
12 did_close_text_document, did_open_text_document, index_workspace_style_files,
13 initialize_workspace_folders, prepare_background_workspace_index_job,
14 refresh_source_indexes_for_resolution_settings_change, resolve_cascade_at_position,
15 resolve_lsp_code_actions, resolve_lsp_code_lens, resolve_lsp_completion,
16 resolve_lsp_definition, resolve_lsp_explain, resolve_lsp_hover, resolve_lsp_hover_trace,
17 resolve_lsp_prepare_rename, resolve_lsp_references, resolve_lsp_rename,
18 resolve_source_diagnostics, resolve_style_context_index, resolve_style_diagnostics,
19 resolve_style_hover_candidates,
20};
21use serde_json::{Value, json};
22use std::time::{SystemTime, UNIX_EPOCH};
23
24pub fn handle_lsp_message(state: &mut LspShellState, message: Value) -> Option<Value> {
25 let method = message.get("method").and_then(Value::as_str);
26 let id = message.get("id").cloned();
27
28 if method == Some(CANCEL_REQUEST_METHOD) && id.is_none() {
29 cancel_lsp_request(state, message.get("params"));
30 return None;
31 }
32
33 if let Some(request_id) = id.as_ref()
34 && take_cancelled_request(state, request_id)
35 {
36 return Some(cancelled_request_response(request_id.clone()));
37 }
38
39 match (method, id) {
40 (Some("initialize"), Some(request_id)) => {
41 initialize_workspace_folders(state, message.get("params"));
42 Some(json!({
43 "jsonrpc": "2.0",
44 "id": request_id,
45 "result": {
46 "capabilities": current_node_lsp_capability_contract(),
47 "serverInfo": {
48 "name": "omena-css-rust",
49 },
50 },
51 }))
52 }
53 (Some("initialized"), None) => {
54 index_workspace_style_files(state);
55 None
56 }
57 (Some("textDocument/didOpen"), None) => {
58 did_open_text_document(state, message.get("params"));
59 None
60 }
61 (Some("textDocument/didChange"), None) => {
62 did_change_text_document(state, message.get("params"));
63 None
64 }
65 (Some("textDocument/didClose"), None) => {
66 did_close_text_document(state, message.get("params"));
67 None
68 }
69 (Some("workspace/didChangeWorkspaceFolders"), None) => {
70 did_change_workspace_folders(state, message.get("params"), true);
71 None
72 }
73 (Some("workspace/didChangeConfiguration"), None) => {
74 did_change_configuration(state, message.get("params"));
75 None
76 }
77 (Some("workspace/didChangeWatchedFiles"), None) => {
78 did_change_watched_files(state, message.get("params"));
79 None
80 }
81 (Some("textDocument/hover"), Some(request_id)) => Some(json!({
82 "jsonrpc": "2.0",
83 "id": request_id,
84 "result": if state.features.hover { resolve_lsp_hover(state, message.get("params")) } else { Value::Null },
85 })),
86 (Some("textDocument/definition"), Some(request_id)) => Some(json!({
87 "jsonrpc": "2.0",
88 "id": request_id,
89 "result": if state.features.definition { resolve_lsp_definition(state, message.get("params")) } else { Value::Null },
90 })),
91 (Some("textDocument/references"), Some(request_id)) => Some(json!({
92 "jsonrpc": "2.0",
93 "id": request_id,
94 "result": if state.features.references { resolve_lsp_references(state, message.get("params")) } else { Value::Null },
95 })),
96 (Some("textDocument/completion"), Some(request_id)) => Some(json!({
97 "jsonrpc": "2.0",
98 "id": request_id,
99 "result": if state.features.completion { resolve_lsp_completion(state, message.get("params")) } else { Value::Null },
100 })),
101 (Some("textDocument/codeAction"), Some(request_id)) => Some(json!({
102 "jsonrpc": "2.0",
103 "id": request_id,
104 "result": resolve_lsp_code_actions(state, message.get("params")),
105 })),
106 (Some("textDocument/documentColor"), Some(request_id)) => Some(json!({
107 "jsonrpc": "2.0",
108 "id": request_id,
109 "result": crate::color_provider::resolve_lsp_document_color(state, message.get("params")),
110 })),
111 (Some("textDocument/colorPresentation"), Some(request_id)) => Some(json!({
112 "jsonrpc": "2.0",
113 "id": request_id,
114 "result": crate::color_provider::resolve_lsp_color_presentation(message.get("params")),
115 })),
116 (Some("textDocument/documentLink"), Some(request_id)) => Some(json!({
117 "jsonrpc": "2.0",
118 "id": request_id,
119 "result": crate::document_links::resolve_lsp_document_links(state, message.get("params")),
120 })),
121 (Some("workspace/symbol"), Some(request_id)) => Some(json!({
122 "jsonrpc": "2.0",
123 "id": request_id,
124 "result": crate::workspace_symbols::resolve_lsp_workspace_symbols(state, message.get("params")),
125 })),
126 (Some("textDocument/codeLens"), Some(request_id)) => Some(json!({
127 "jsonrpc": "2.0",
128 "id": request_id,
129 "result": if state.features.references { resolve_lsp_code_lens(state, message.get("params")) } else { Value::Null },
130 })),
131 (Some("textDocument/prepareRename"), Some(request_id)) => Some(json!({
132 "jsonrpc": "2.0",
133 "id": request_id,
134 "result": if state.features.rename { resolve_lsp_prepare_rename(state, message.get("params")) } else { Value::Null },
135 })),
136 (Some("textDocument/rename"), Some(request_id)) => Some(json!({
137 "jsonrpc": "2.0",
138 "id": request_id,
139 "result": if state.features.rename { resolve_lsp_rename(state, message.get("params")) } else { Value::Null },
140 })),
141 (Some(DEBUG_STATE_REQUEST), Some(request_id)) => Some(json!({
142 "jsonrpc": "2.0",
143 "id": request_id,
144 "result": state.snapshot(),
145 })),
146 (Some(RUNTIME_LOOP_PROBE_REQUEST), Some(request_id)) => Some(json!({
147 "jsonrpc": "2.0",
148 "id": request_id,
149 "result": {
150 "now": current_time_millis(),
151 },
152 })),
153 (Some(STYLE_HOVER_CANDIDATES_REQUEST), Some(request_id)) => Some(json!({
154 "jsonrpc": "2.0",
155 "id": request_id,
156 "result": resolve_style_hover_candidates(state, message.get("params")),
157 })),
158 (Some(STYLE_DIAGNOSTICS_REQUEST), Some(request_id)) => Some(json!({
159 "jsonrpc": "2.0",
160 "id": request_id,
161 "result": resolve_style_diagnostics(state, message.get("params")),
162 })),
163 (Some(SOURCE_DIAGNOSTICS_REQUEST), Some(request_id)) => Some(json!({
164 "jsonrpc": "2.0",
165 "id": request_id,
166 "result": resolve_source_diagnostics(state, message.get("params")),
167 })),
168 (Some(CASCADE_AT_POSITION_REQUEST), Some(request_id)) => Some(json!({
169 "jsonrpc": "2.0",
170 "id": request_id,
171 "result": resolve_cascade_at_position(state, message.get("params")),
172 })),
173 (Some(STYLE_CONTEXT_INDEX_REQUEST), Some(request_id)) => Some(json!({
174 "jsonrpc": "2.0",
175 "id": request_id,
176 "result": resolve_style_context_index(state, message.get("params")),
177 })),
178 (Some(EXPLAIN_HOVER_TRACE_REQUEST), Some(request_id)) => Some(json!({
179 "jsonrpc": "2.0",
180 "id": request_id,
181 "result": resolve_lsp_hover_trace(state, message.get("params")),
182 })),
183 (Some(EXPLAIN_REQUEST), Some(request_id)) => Some(json!({
184 "jsonrpc": "2.0",
185 "id": request_id,
186 "result": resolve_lsp_explain(state, message.get("params")),
187 })),
188 (Some(SDK_WORKFLOW_REQUEST), Some(request_id)) => {
189 match crate::sdk_workflow::resolve_lsp_sdk_workflow(state, message.get("params")) {
190 Ok(result) => Some(json!({
191 "jsonrpc": "2.0",
192 "id": request_id,
193 "result": result,
194 })),
195 Err(error) => Some(json!({
196 "jsonrpc": "2.0",
197 "id": request_id,
198 "error": {
199 "code": -32001,
200 "message": error.message.clone(),
201 "data": omena_query::OmenaSdkErrorEnvelopeV0 { error },
202 },
203 })),
204 }
205 }
206 (Some("shutdown"), Some(request_id)) => {
207 state.shutdown_requested = true;
208 Some(json!({
209 "jsonrpc": "2.0",
210 "id": request_id,
211 "result": null,
212 }))
213 }
214 (Some("exit"), None) => {
215 state.should_exit = true;
216 None
217 }
218 (Some(_), Some(request_id)) => Some(json!({
219 "jsonrpc": "2.0",
220 "id": request_id,
221 "error": {
222 "code": -32601,
223 "message": "Method not found",
224 },
225 })),
226 (Some(_), None) => None,
227 (None, Some(request_id)) => {
228 if take_server_progress_response(state, &request_id) {
229 None
230 } else {
231 Some(json!({
232 "jsonrpc": "2.0",
233 "id": request_id,
234 "error": {
235 "code": -32600,
236 "message": "Invalid Request",
237 },
238 }))
239 }
240 }
241 (None, None) => None,
242 }
243}
244
245fn did_change_configuration(state: &mut LspShellState, params: Option<&Value>) {
246 state.configuration_change_count += 1;
247 let Some(settings) = params
248 .and_then(|value| value.get("settings"))
249 .and_then(|value| value.get("omena"))
250 else {
251 return;
252 };
253 apply_feature_settings(state, settings.get("features"));
254 if apply_diagnostic_settings(state, settings.get("diagnostics")) {
255 state
260 .tide_ledger
261 .advance(&[crate::tide::TideInputKindV0::DiagnosticSettings]);
262 let tick = state.tide_tick;
263 state
264 .tide_republish_lane
265 .deposit(crate::tide::TideRepublishDemandV0::All, tick);
266 }
267 if apply_resolution_settings(state, settings.get("resolution")) {
268 state
269 .tide_ledger
270 .advance(&[crate::tide::TideInputKindV0::ResolutionSettings]);
271 refresh_source_indexes_for_resolution_settings_change(state);
272 }
273}
274
275fn cancel_lsp_request(state: &mut LspShellState, params: Option<&Value>) {
276 let Some(id) = params.and_then(|value| value.get("id")) else {
277 return;
278 };
279 if let Some(key) = request_id_key(id)
280 && !state.in_flight_requests.cancel(key.as_str())
281 {
282 state.cancelled_request_ids.cancel(key);
283 }
284}
285
286fn take_cancelled_request(state: &mut LspShellState, request_id: &Value) -> bool {
287 request_id_key(request_id).is_some_and(|key| {
288 state
289 .cancelled_request_ids
290 .take_cancelled_result(key.as_str())
291 .is_err()
292 })
293}
294
295fn take_server_progress_response(state: &mut LspShellState, request_id: &Value) -> bool {
296 request_id
297 .as_str()
298 .is_some_and(|id| state.take_server_progress_response(id))
299}
300
301fn request_id_key(id: &Value) -> Option<String> {
302 if let Some(value) = id.as_str() {
303 return Some(format!("s:{value}"));
304 }
305 if id.is_number() {
306 return Some(format!("n:{id}"));
307 }
308 None
309}
310
311fn cancelled_request_response(request_id: Value) -> Value {
312 json!({
313 "jsonrpc": "2.0",
314 "id": request_id,
315 "error": {
316 "code": REQUEST_CANCELLED_ERROR_CODE,
317 "message": "Request cancelled",
318 },
319 })
320}
321
322pub fn handle_lsp_message_outputs(state: &mut LspShellState, message: Value) -> Vec<Value> {
323 handle_lsp_message_scheduled_outputs(state, message)
324 .into_iter()
325 .map(ScheduledLspOutput::into_value)
326 .collect()
327}
328
329#[derive(Debug)]
339pub enum LspLoopTurnV0 {
340 Outputs(Vec<ScheduledLspOutput>),
341 OutputsAndDeferredDiagnostics {
342 outputs: Vec<ScheduledLspOutput>,
343 deferred_diagnostics: Vec<LspDeferredDiagnosticsDispatchV0>,
344 workspace_index_jobs: Vec<LspWorkspaceIndexJobV0>,
345 },
346 DispatchQuery(Box<LspQueryDispatchV0>),
349}
350
351#[derive(Debug)]
354pub struct LspQueryDispatchV0 {
355 pub snapshot: LspQuerySnapshotV0,
356 pub message: Value,
357 pub(crate) completion: Option<crate::state::LspDispatchedRequestToken>,
358}
359
360pub fn handle_lsp_message_scheduled_outputs_or_dispatch(
369 state: &mut LspShellState,
370 message: Value,
371) -> LspLoopTurnV0 {
372 if let Some(request_id) = dispatchable_query_request_id(&message) {
373 if take_cancelled_request(state, &request_id) {
374 return LspLoopTurnV0::Outputs(vec![ScheduledLspOutput::immediate(
375 cancelled_request_response(request_id),
376 )]);
377 }
378 if message.get("method").and_then(Value::as_str) == Some("textDocument/documentColor")
385 && !crate::color_provider::document_has_color_reference_candidates(
386 state,
387 message.get("params"),
388 )
389 {
390 return LspLoopTurnV0::Outputs(vec![ScheduledLspOutput::immediate(json!({
391 "jsonrpc": "2.0",
392 "id": request_id,
393 "result": json!([]),
394 }))]);
395 }
396 let Some(request_key) = request_id_key(&request_id) else {
397 return LspLoopTurnV0::Outputs(vec![ScheduledLspOutput::immediate(json!({
398 "jsonrpc": "2.0",
399 "id": request_id,
400 "error": {
401 "code": -32600,
402 "message": "Invalid Request",
403 },
404 }))]);
405 };
406 let completion = state.in_flight_requests.register(request_key);
407 return LspLoopTurnV0::DispatchQuery(Box::new(LspQueryDispatchV0 {
408 snapshot: state.query_snapshot(),
409 message,
410 completion: Some(completion),
411 }));
412 }
413 let effects = handle_lsp_message_scheduled_effects_with_deferral(state, message, true, true);
414 if effects.deferred_diagnostics.is_empty() && effects.workspace_index_jobs.is_empty() {
415 LspLoopTurnV0::Outputs(effects.outputs)
416 } else {
417 LspLoopTurnV0::OutputsAndDeferredDiagnostics {
418 outputs: effects.outputs,
419 deferred_diagnostics: effects.deferred_diagnostics,
420 workspace_index_jobs: effects.workspace_index_jobs,
421 }
422 }
423}
424
425pub fn dispatched_query_internal_error_response(dispatch: &LspQueryDispatchV0) -> Option<Value> {
433 let request_id = dispatchable_query_request_id(&dispatch.message)?;
434 Some(json!({
435 "jsonrpc": "2.0",
436 "id": request_id,
437 "error": {
438 "code": -32603,
439 "message": "internal error while resolving the dispatched query",
440 },
441 }))
442}
443
444fn dispatchable_query_request_id(message: &Value) -> Option<Value> {
445 let method = message.get("method").and_then(Value::as_str)?;
446 if method != "textDocument/hover"
447 && method != "textDocument/definition"
448 && method != "textDocument/documentColor"
449 && method != "textDocument/documentLink"
450 && method != "workspace/symbol"
451 && method != "textDocument/codeLens"
452 {
453 return None;
454 }
455 message.get("id").cloned()
456}
457
458pub fn dispatched_query_is_heavy(dispatch: &LspQueryDispatchV0) -> bool {
466 matches!(
467 dispatch.message.get("method").and_then(Value::as_str),
468 Some("textDocument/codeLens") | Some(HOVER_SUBSTRATE_WARMUP_METHOD)
469 )
470}
471
472pub const HOVER_SUBSTRATE_WARMUP_METHOD: &str = "omena/internalWarmHoverSubstrate";
484
485pub fn hover_substrate_warmup_dispatch(state: &LspShellState) -> Option<Box<LspQueryDispatchV0>> {
489 let document = state
490 .open_document_uris
491 .iter()
492 .filter_map(|file_id| state.document_for_file_id(*file_id))
493 .find(|document| {
494 crate::protocol::is_style_document_uri(document.uri.as_str())
495 && !document.style_candidates.is_empty()
496 })?;
497 let candidate = document.style_candidates.first()?;
498 let message = json!({
499 "jsonrpc": "2.0",
500 "method": HOVER_SUBSTRATE_WARMUP_METHOD,
501 "params": {
502 "textDocument": { "uri": document.uri },
503 "position": candidate.range.start,
504 },
505 });
506 Some(Box::new(LspQueryDispatchV0 {
507 snapshot: state.query_snapshot(),
508 message,
509 completion: None,
510 }))
511}
512
513pub fn complete_dispatched_query_response(
520 dispatch: &LspQueryDispatchV0,
521 response: Option<Value>,
522) -> Option<Value> {
523 let Some(completion) = dispatch.completion.as_ref() else {
524 return response;
525 };
526 match completion.complete() {
527 crate::state::LspDispatchedRequestCompletion::Result => response,
528 crate::state::LspDispatchedRequestCompletion::Cancelled => {
529 dispatchable_query_request_id(&dispatch.message).map(cancelled_request_response)
530 }
531 crate::state::LspDispatchedRequestCompletion::AlreadyCompleted => None,
532 }
533}
534
535pub fn resolve_dispatched_query_response(dispatch: &LspQueryDispatchV0) -> Option<Value> {
536 if dispatch.message.get("method").and_then(Value::as_str) == Some(HOVER_SUBSTRATE_WARMUP_METHOD)
537 {
538 let started = std::time::Instant::now();
539 let state: &dyn LspQueryReadView = &dispatch.snapshot;
540 let _ = resolve_lsp_hover(state, dispatch.message.get("params"));
541 crate::loop_trace!(
542 "hover-warmup done took_ms={}",
543 started.elapsed().as_millis()
544 );
545 return None;
546 }
547 let request_id = dispatchable_query_request_id(&dispatch.message)?;
548 let method = dispatch.message.get("method").and_then(Value::as_str)?;
549 let params = dispatch.message.get("params");
550 let state: &dyn LspQueryReadView = &dispatch.snapshot;
551 let result = match method {
552 "textDocument/hover" => {
553 if state.query_features().hover {
554 resolve_lsp_hover(state, params)
555 } else {
556 Value::Null
557 }
558 }
559 "textDocument/definition" => {
560 if state.query_features().definition {
561 resolve_lsp_definition(state, params)
562 } else {
563 Value::Null
564 }
565 }
566 "textDocument/documentLink" => {
568 crate::document_links::resolve_lsp_document_links(state, params)
569 }
570 "workspace/symbol" => {
572 crate::workspace_symbols::resolve_lsp_workspace_symbols(state, params)
573 }
574 "textDocument/documentColor" => {
577 let started = std::time::Instant::now();
578 let result = crate::color_provider::resolve_lsp_document_color(state, params);
579 crate::loop_trace!(
580 "document-color dispatched took_ms={}",
581 started.elapsed().as_millis()
582 );
583 result
584 }
585 "textDocument/codeLens" => {
591 if state.query_features().references {
592 let started = std::time::Instant::now();
593 let result = resolve_lsp_code_lens(state, params);
594 crate::loop_trace!(
595 "code-lens dispatched took_ms={}",
596 started.elapsed().as_millis()
597 );
598 result
599 } else {
600 Value::Null
601 }
602 }
603 _ => return None,
604 };
605 Some(json!({
606 "jsonrpc": "2.0",
607 "id": request_id,
608 "result": result,
609 }))
610}
611
612pub fn handle_lsp_message_scheduled_outputs(
613 state: &mut LspShellState,
614 message: Value,
615) -> Vec<ScheduledLspOutput> {
616 handle_lsp_message_scheduled_effects_with_deferral(state, message, false, false).outputs
617}
618
619fn handle_lsp_message_scheduled_effects_with_deferral(
620 state: &mut LspShellState,
621 message: Value,
622 enable_deferred_style_diagnostics: bool,
623 enable_background_workspace_index: bool,
624) -> LspScheduledEffectsV0 {
625 let method = message
626 .get("method")
627 .and_then(Value::as_str)
628 .map(str::to_string);
629 let document_uri = message
630 .get("params")
631 .and_then(|value| value.get("textDocument"))
632 .and_then(|value| value.get("uri"))
633 .and_then(Value::as_str)
634 .map(str::to_string);
635 let watched_file_uris = watched_file_uris_from_message(&message);
636 let content_changed = if method.as_deref() == Some("textDocument/didOpen") {
640 let incoming_text = message
641 .pointer("/params/textDocument/text")
642 .and_then(Value::as_str);
643 match (
644 document_uri.as_deref().and_then(|uri| state.document(uri)),
645 incoming_text,
646 ) {
647 (Some(existing), Some(text)) => existing.text != text,
648 _ => true,
649 }
650 } else {
651 true
652 };
653 let diagnostics_event = diagnostics_schedule_event(
654 method.as_deref(),
655 document_uri,
656 watched_file_uris,
657 content_changed,
658 );
659 let mut effects = LspScheduledEffectsV0::default();
660
661 let response = if enable_background_workspace_index {
662 handle_lsp_message_for_background_workspace_index(state, &message, &mut effects)
663 } else {
664 handle_lsp_message(state, message)
665 };
666
667 if let Some(response) = response {
668 effects
669 .outputs
670 .push(ScheduledLspOutput::immediate(response));
671 }
672
673 if let Some(event) = diagnostics_event {
674 let diagnostics_effects = if enable_deferred_style_diagnostics {
675 run_diagnostics_schedule_effects(state, event)
676 } else {
677 crate::diagnostics_scheduler::DiagnosticsScheduleEffectsV0::from_outputs(
678 crate::diagnostics_scheduler::run_diagnostics_schedule(state, event),
679 )
680 };
681 effects.outputs.extend(diagnostics_effects.outputs);
682 effects
683 .deferred_diagnostics
684 .extend(diagnostics_effects.deferred_diagnostics);
685 }
686
687 effects
688}
689
690#[derive(Debug, Default)]
691pub struct LspScheduledEffectsV0 {
692 pub outputs: Vec<ScheduledLspOutput>,
693 pub deferred_diagnostics: Vec<LspDeferredDiagnosticsDispatchV0>,
694 pub workspace_index_jobs: Vec<LspWorkspaceIndexJobV0>,
695}
696
697impl From<Vec<ScheduledLspOutput>> for LspScheduledEffectsV0 {
698 fn from(outputs: Vec<ScheduledLspOutput>) -> Self {
699 Self {
700 outputs,
701 deferred_diagnostics: Vec::new(),
702 workspace_index_jobs: Vec::new(),
703 }
704 }
705}
706
707fn handle_lsp_message_for_background_workspace_index(
708 state: &mut LspShellState,
709 message: &Value,
710 effects: &mut LspScheduledEffectsV0,
711) -> Option<Value> {
712 match message.get("method").and_then(Value::as_str) {
713 Some("initialized") if message.get("id").is_none() => {
714 let mut job = prepare_background_workspace_index_job(state);
715 effects
716 .outputs
717 .extend(workspace_index_progress_begin_outputs(state, &mut job));
718 effects.workspace_index_jobs.push(job);
719 None
720 }
721 Some("workspace/didChangeWorkspaceFolders") if message.get("id").is_none() => {
722 let added_workspace_folder =
723 did_change_workspace_folders(state, message.get("params"), false);
724 if added_workspace_folder {
725 let mut job = prepare_background_workspace_index_job(state);
726 effects
727 .outputs
728 .extend(workspace_index_progress_begin_outputs(state, &mut job));
729 effects.workspace_index_jobs.push(job);
730 }
731 None
732 }
733 _ => handle_lsp_message(state, message.clone()),
734 }
735}
736
737fn workspace_index_progress_begin_outputs(
738 state: &mut LspShellState,
739 job: &mut LspWorkspaceIndexJobV0,
740) -> Vec<ScheduledLspOutput> {
741 if !state.client_supports_work_done_progress {
742 return Vec::new();
743 }
744
745 let (id, token) = state.allocate_work_done_progress_request();
746 job.progress_token = Some(token.clone());
747 vec![
748 ScheduledLspOutput::immediate(json!({
749 "jsonrpc": "2.0",
750 "id": id,
751 "method": "window/workDoneProgress/create",
752 "params": {
753 "token": token,
754 },
755 })),
756 ScheduledLspOutput::immediate(json!({
757 "jsonrpc": "2.0",
758 "method": "$/progress",
759 "params": {
760 "token": token,
761 "value": {
762 "kind": "begin",
763 "title": "Omena CSS workspace index",
764 "message": "Scanning workspace files",
765 },
766 },
767 })),
768 ]
769}
770
771pub fn workspace_index_progress_end_output(
772 result: &LspWorkspaceIndexResultV0,
773) -> Option<ScheduledLspOutput> {
774 let token = result.progress_token.as_deref()?;
775 let message = if result.exhausted && result.pending_file_count > 0 {
776 format!(
777 "Workspace index updated; continuing with {} remaining files in the background",
778 result.pending_file_count
779 )
780 } else {
781 "Workspace index updated".to_string()
782 };
783 Some(ScheduledLspOutput::immediate(json!({
784 "jsonrpc": "2.0",
785 "method": "$/progress",
786 "params": {
787 "token": token,
788 "value": {
789 "kind": "end",
790 "message": message,
791 },
792 },
793 })))
794}
795
796pub(crate) fn current_time_millis() -> u128 {
797 SystemTime::now()
798 .duration_since(UNIX_EPOCH)
799 .map_or(0, |duration| duration.as_millis())
800}
801
802fn watched_file_uris_from_message(message: &Value) -> Vec<String> {
803 message
804 .get("params")
805 .and_then(|value| value.get("changes"))
806 .and_then(Value::as_array)
807 .map(|changes| {
808 changes
809 .iter()
810 .filter_map(|change| change.get("uri").and_then(Value::as_str))
811 .map(str::to_string)
812 .collect()
813 })
814 .unwrap_or_default()
815}