1use anyhow::Result as AnyhowResult;
9use rust_i18n::t;
10
11use crate::services::async_bridge::AsyncMessage;
12use crate::view::prompt::PromptType;
13
14use super::Editor;
15
16impl Editor {
17 pub fn process_async_messages(&mut self) -> bool {
25 self.plugin_manager.check_thread_health();
28
29 let Some(bridge) = &self.async_bridge else {
30 return false;
31 };
32
33 let messages = {
34 let _s = tracing::info_span!("try_recv_all").entered();
35 bridge.try_recv_all()
36 };
37 let needs_render = !messages.is_empty();
38 tracing::trace!(
39 async_message_count = messages.len(),
40 "received async messages"
41 );
42
43 for message in messages {
44 match message {
45 AsyncMessage::LspDiagnostics {
46 uri,
47 diagnostics,
48 server_name,
49 } => {
50 self.handle_lsp_diagnostics(uri, diagnostics, server_name);
51 }
52 AsyncMessage::LspInitialized {
53 language,
54 server_name,
55 capabilities,
56 } => {
57 tracing::info!(
58 "LSP server '{}' initialized for language: {}",
59 server_name,
60 language
61 );
62 self.status_message = Some(format!("LSP ({}) ready", language));
63
64 if let Some(lsp) = &mut self.lsp {
66 lsp.set_server_capabilities(&language, &server_name, capabilities);
67 }
68
69 self.resend_did_open_for_language(&language);
71 self.request_semantic_tokens_for_language(&language);
72 self.request_folding_ranges_for_language(&language);
73 }
74 AsyncMessage::LspError {
75 language,
76 error,
77 stderr_log_path,
78 } => {
79 tracing::error!("LSP error for {}: {}", language, error);
80 self.status_message = Some(format!("LSP error ({}): {}", language, error));
81
82 let server_command = self
84 .config
85 .lsp
86 .get(&language)
87 .and_then(|configs| configs.as_slice().first())
88 .map(|c| c.command.clone())
89 .unwrap_or_else(|| "unknown".to_string());
90
91 let error_type = if error.contains("not found") || error.contains("NotFound") {
93 "not_found"
94 } else if error.contains("permission") || error.contains("PermissionDenied") {
95 "spawn_failed"
96 } else if error.contains("timeout") {
97 "timeout"
98 } else {
99 "spawn_failed"
100 }
101 .to_string();
102
103 self.plugin_manager.run_hook(
105 "lsp_server_error",
106 crate::services::plugins::hooks::HookArgs::LspServerError {
107 language: language.clone(),
108 server_command,
109 error_type,
110 message: error.clone(),
111 },
112 );
113
114 if let Some(log_path) = stderr_log_path {
117 let has_content = log_path.metadata().map(|m| m.len() > 0).unwrap_or(false);
118 if has_content {
119 tracing::info!("Opening LSP stderr log in background: {:?}", log_path);
120 match self.open_file_no_focus(&log_path) {
121 Ok(buffer_id) => {
122 self.mark_buffer_read_only(buffer_id, true);
123 self.status_message = Some(format!(
124 "LSP error ({}): {} - See stderr log",
125 language, error
126 ));
127 }
128 Err(e) => {
129 tracing::error!("Failed to open LSP stderr log: {}", e);
130 }
131 }
132 }
133 }
134 }
135 AsyncMessage::LspCompletion { request_id, items } => {
136 if let Err(e) = self.handle_completion_response(request_id, items) {
137 tracing::error!("Error handling completion response: {}", e);
138 }
139 }
140 AsyncMessage::LspGotoDefinition {
141 request_id,
142 locations,
143 } => {
144 if let Err(e) = self.handle_goto_definition_response(request_id, locations) {
145 tracing::error!("Error handling goto definition response: {}", e);
146 }
147 }
148 AsyncMessage::LspRename { request_id, result } => {
149 if let Err(e) = self.handle_rename_response(request_id, result) {
150 tracing::error!("Error handling rename response: {}", e);
151 }
152 }
153 AsyncMessage::LspHover {
154 request_id,
155 contents,
156 is_markdown,
157 range,
158 } => {
159 self.handle_hover_response(request_id, contents, is_markdown, range);
160 }
161 AsyncMessage::LspReferences {
162 request_id,
163 locations,
164 } => {
165 if let Err(e) = self.handle_references_response(request_id, locations) {
166 tracing::error!("Error handling references response: {}", e);
167 }
168 }
169 AsyncMessage::LspSignatureHelp {
170 request_id,
171 signature_help,
172 } => {
173 self.handle_signature_help_response(request_id, signature_help);
174 }
175 AsyncMessage::LspCodeActions {
176 request_id,
177 actions,
178 } => {
179 self.handle_code_actions_response(request_id, actions);
180 }
181 AsyncMessage::LspApplyEdit { edit, label } => {
182 tracing::info!("Applying workspace edit from server (label: {:?})", label);
183 match self.apply_workspace_edit(edit) {
184 Ok(n) => {
185 if let Some(label) = label {
186 self.set_status_message(
187 t!("lsp.code_action_applied", title = &label, count = n)
188 .to_string(),
189 );
190 }
191 }
192 Err(e) => {
193 tracing::error!("Failed to apply workspace edit: {}", e);
194 }
195 }
196 }
197 AsyncMessage::LspCodeActionResolved {
198 request_id: _,
199 action,
200 } => match action {
201 Ok(resolved) => {
202 self.execute_resolved_code_action(resolved);
203 }
204 Err(e) => {
205 tracing::warn!("codeAction/resolve failed: {}", e);
206 self.set_status_message(format!("Code action resolve failed: {e}"));
207 }
208 },
209 AsyncMessage::LspCompletionResolved {
210 request_id: _,
211 item,
212 } => {
213 if let Ok(resolved) = item {
214 self.handle_completion_resolved(resolved);
215 }
216 }
217 AsyncMessage::LspFormatting {
218 request_id: _,
219 uri,
220 edits,
221 } => {
222 if !edits.is_empty() {
223 if let Err(e) = self.apply_formatting_edits(&uri, edits) {
224 tracing::error!("Failed to apply formatting: {}", e);
225 }
226 }
227 }
228 AsyncMessage::LspPrepareRename {
229 request_id: _,
230 result,
231 } => {
232 self.handle_prepare_rename_response(result);
233 }
234 AsyncMessage::LspPulledDiagnostics {
235 request_id: _,
236 uri,
237 result_id,
238 diagnostics,
239 unchanged,
240 } => {
241 self.handle_lsp_pulled_diagnostics(uri, result_id, diagnostics, unchanged);
242 }
243 AsyncMessage::LspInlayHints {
244 request_id,
245 uri,
246 hints,
247 } => {
248 self.handle_lsp_inlay_hints(request_id, uri, hints);
249 }
250 AsyncMessage::LspFoldingRanges {
251 request_id,
252 uri,
253 ranges,
254 } => {
255 self.handle_lsp_folding_ranges(request_id, uri, ranges);
256 }
257 AsyncMessage::LspSemanticTokens {
258 request_id,
259 uri,
260 response,
261 } => {
262 self.handle_lsp_semantic_tokens(request_id, uri, response);
263 }
264 AsyncMessage::LspServerQuiescent { language } => {
265 self.handle_lsp_server_quiescent(language);
266 }
267 AsyncMessage::LspDiagnosticRefresh { language } => {
268 self.handle_lsp_diagnostic_refresh(language);
269 }
270 AsyncMessage::FileChanged { path } => {
271 self.handle_async_file_changed(path);
272 }
273 AsyncMessage::GitStatusChanged { status } => {
274 tracing::info!("Git status changed: {}", status);
275 }
277 AsyncMessage::FileExplorerInitialized(view) => {
278 self.handle_file_explorer_initialized(view);
279 }
280 AsyncMessage::FileExplorerToggleNode(node_id) => {
281 self.handle_file_explorer_toggle_node(node_id);
282 }
283 AsyncMessage::FileExplorerRefreshNode(node_id) => {
284 self.handle_file_explorer_refresh_node(node_id);
285 }
286 AsyncMessage::FileExplorerExpandedToPath(view) => {
287 self.handle_file_explorer_expanded_to_path(view);
288 }
289 AsyncMessage::Plugin(plugin_msg) => {
290 use fresh_core::api::{JsCallbackId, PluginAsyncMessage};
291 match plugin_msg {
292 PluginAsyncMessage::ProcessOutput {
293 process_id,
294 stdout,
295 stderr,
296 exit_code,
297 } => {
298 self.handle_plugin_process_output(
299 JsCallbackId::from(process_id),
300 stdout,
301 stderr,
302 exit_code,
303 );
304 }
305 PluginAsyncMessage::DelayComplete { callback_id } => {
306 self.plugin_manager.resolve_callback(
307 JsCallbackId::from(callback_id),
308 "null".to_string(),
309 );
310 }
311 PluginAsyncMessage::ProcessStdout { process_id, data } => {
312 self.plugin_manager.run_hook(
313 "onProcessStdout",
314 crate::services::plugins::hooks::HookArgs::ProcessOutput {
315 process_id,
316 data,
317 },
318 );
319 }
320 PluginAsyncMessage::ProcessStderr { process_id, data } => {
321 self.plugin_manager.run_hook(
322 "onProcessStderr",
323 crate::services::plugins::hooks::HookArgs::ProcessOutput {
324 process_id,
325 data,
326 },
327 );
328 }
329 PluginAsyncMessage::ProcessExit {
330 process_id,
331 callback_id,
332 exit_code,
333 } => {
334 self.background_process_handles.remove(&process_id);
335 let result = fresh_core::api::BackgroundProcessResult {
336 process_id,
337 exit_code,
338 };
339 self.plugin_manager.resolve_callback(
340 JsCallbackId::from(callback_id),
341 serde_json::to_string(&result).unwrap(),
342 );
343 }
344 PluginAsyncMessage::LspResponse {
345 language: _,
346 request_id,
347 result,
348 } => {
349 self.handle_plugin_lsp_response(request_id, result);
350 }
351 PluginAsyncMessage::PluginResponse(response) => {
352 self.handle_plugin_response(response);
353 }
354 PluginAsyncMessage::GrepStreamingProgress {
355 search_id,
356 matches_json,
357 } => {
358 tracing::info!(
359 "GrepStreamingProgress: search_id={} json_len={}",
360 search_id,
361 matches_json.len()
362 );
363 self.plugin_manager.call_streaming_callback(
364 JsCallbackId::from(search_id),
365 matches_json,
366 false,
367 );
368 }
369 PluginAsyncMessage::GrepStreamingComplete {
370 search_id: _,
371 callback_id,
372 total_matches,
373 truncated,
374 } => {
375 self.streaming_grep_cancellation = None;
376 self.plugin_manager.resolve_callback(
377 JsCallbackId::from(callback_id),
378 format!(
379 r#"{{"totalMatches":{},"truncated":{}}}"#,
380 total_matches, truncated
381 ),
382 );
383 }
384 }
385 }
386 AsyncMessage::LspProgress {
387 language,
388 token,
389 value,
390 } => {
391 self.handle_lsp_progress(language, token, value);
392 }
393 AsyncMessage::LspWindowMessage {
394 language,
395 message_type,
396 message,
397 } => {
398 self.handle_lsp_window_message(language, message_type, message);
399 }
400 AsyncMessage::LspLogMessage {
401 language,
402 message_type,
403 message,
404 } => {
405 self.handle_lsp_log_message(language, message_type, message);
406 }
407 AsyncMessage::LspStatusUpdate {
408 language,
409 server_name,
410 status,
411 message: _,
412 } => {
413 self.handle_lsp_status_update(language, server_name, status);
414 }
415 AsyncMessage::FileOpenDirectoryLoaded(result) => {
416 self.handle_file_open_directory_loaded(result);
417 }
418 AsyncMessage::FileOpenShortcutsLoaded(shortcuts) => {
419 self.handle_file_open_shortcuts_loaded(shortcuts);
420 }
421 AsyncMessage::TerminalOutput { terminal_id } => {
422 tracing::trace!("Terminal output received for {:?}", terminal_id);
424
425 if self.config.terminal.jump_to_end_on_output && !self.terminal_mode {
428 if let Some(&active_terminal_id) =
430 self.terminal_buffers.get(&self.active_buffer())
431 {
432 if active_terminal_id == terminal_id {
433 self.enter_terminal_mode();
434 }
435 }
436 }
437
438 if self.terminal_mode {
440 if let Some(handle) = self.terminal_manager.get(terminal_id) {
441 if let Ok(mut state) = handle.state.lock() {
442 state.scroll_to_bottom();
443 }
444 }
445 }
446 }
447 AsyncMessage::TerminalExited { terminal_id } => {
448 tracing::info!("Terminal {:?} exited", terminal_id);
449 if let Some((&buffer_id, _)) = self
451 .terminal_buffers
452 .iter()
453 .find(|(_, &tid)| tid == terminal_id)
454 {
455 if self.active_buffer() == buffer_id && self.terminal_mode {
457 self.terminal_mode = false;
458 self.key_context = crate::input::keybindings::KeyContext::Normal;
459 }
460
461 self.sync_terminal_to_buffer(buffer_id);
463
464 let exit_msg = "\n[Terminal process exited]\n";
466
467 if let Some(backing_path) =
468 self.terminal_backing_files.get(&terminal_id).cloned()
469 {
470 if let Ok(mut file) =
471 self.filesystem.open_file_for_append(&backing_path)
472 {
473 use std::io::Write;
474 if let Err(e) = file.write_all(exit_msg.as_bytes()) {
475 tracing::warn!("Failed to write terminal exit message: {}", e);
476 }
477 }
478
479 if let Err(e) = self.revert_buffer_by_id(buffer_id, &backing_path) {
481 tracing::warn!("Failed to revert terminal buffer: {}", e);
482 }
483 }
484
485 if let Some(state) = self.buffers.get_mut(&buffer_id) {
487 state.editing_disabled = true;
488 state.margins.configure_for_line_numbers(false);
489 state.buffer.set_modified(false);
490 }
491
492 self.terminal_buffers.remove(&buffer_id);
494
495 self.set_status_message(
496 t!("terminal.exited", id = terminal_id.0).to_string(),
497 );
498 }
499 self.terminal_manager.close(terminal_id);
500 }
501
502 AsyncMessage::LspServerRequest {
503 language,
504 server_command,
505 method,
506 params,
507 } => {
508 self.handle_lsp_server_request(language, server_command, method, params);
509 }
510 AsyncMessage::PluginLspResponse {
511 language: _,
512 request_id,
513 result,
514 } => {
515 self.handle_plugin_lsp_response(request_id, result);
516 }
517 AsyncMessage::PluginProcessOutput {
518 process_id,
519 stdout,
520 stderr,
521 exit_code,
522 } => {
523 self.handle_plugin_process_output(
524 fresh_core::api::JsCallbackId::from(process_id),
525 stdout,
526 stderr,
527 exit_code,
528 );
529 }
530 AsyncMessage::GrammarRegistryBuilt {
531 registry,
532 callback_ids,
533 } => {
534 tracing::info!(
535 "Background grammar build completed ({} syntaxes)",
536 registry.available_syntaxes().len()
537 );
538 let mut registry = registry;
544 std::sync::Arc::get_mut(&mut registry)
545 .expect("freshly-received grammar registry Arc must be uniquely owned")
546 .apply_language_config(&self.config.languages);
547 self.grammar_registry = registry;
548 self.grammar_build_in_progress = false;
549
550 let buffers_to_update: Vec<_> = self
552 .buffer_metadata
553 .iter()
554 .filter_map(|(id, meta)| meta.file_path().map(|p| (*id, p.to_path_buf())))
555 .collect();
556
557 for (buf_id, path) in buffers_to_update {
558 if let Some(state) = self.buffers.get_mut(&buf_id) {
559 let detected =
560 crate::primitives::detected_language::DetectedLanguage::from_path(
561 &path,
562 &self.grammar_registry,
563 &self.config.languages,
564 );
565
566 if detected.highlighter.has_highlighting()
567 || !state.highlighter.has_highlighting()
568 {
569 state.apply_language(detected);
570 }
571 }
572 }
573
574 #[cfg(feature = "plugins")]
576 for cb_id in callback_ids {
577 self.plugin_manager
578 .resolve_callback(cb_id, "null".to_string());
579 }
580
581 self.flush_pending_grammars();
583 }
584 AsyncMessage::QuickOpenFilesLoaded { files, complete } => {
585 if let Some((provider, _)) = self.quick_open_registry.get_provider_for_input("")
588 {
589 if let Some(fp) = provider
590 .as_any()
591 .downcast_ref::<crate::input::quick_open::providers::FileProvider>(
592 ) {
593 if complete {
594 fp.set_cache(files);
595 } else {
596 fp.set_partial_cache(files);
597 }
598 }
599 }
600 if let Some(prompt) = &self.prompt {
602 if prompt.prompt_type == PromptType::QuickOpen {
603 let input = prompt.input.clone();
604 self.update_quick_open_suggestions(&input);
605 }
606 }
607 }
608 }
609 }
610
611 #[cfg(feature = "plugins")]
614 {
615 let _s = tracing::info_span!("update_plugin_state_snapshot").entered();
616 self.update_plugin_state_snapshot();
617 }
618
619 let processed_any_commands = {
621 let _s = tracing::info_span!("process_plugin_commands").entered();
622 self.process_plugin_commands()
623 };
624
625 #[cfg(feature = "plugins")]
629 if processed_any_commands {
630 let _s = tracing::info_span!("update_plugin_state_snapshot_post").entered();
631 self.update_plugin_state_snapshot();
632 }
633
634 #[cfg(feature = "plugins")]
636 {
637 let _s = tracing::info_span!("process_pending_plugin_actions").entered();
638 self.process_pending_plugin_actions();
639 }
640
641 {
643 let _s = tracing::info_span!("process_pending_lsp_restarts").entered();
644 self.process_pending_lsp_restarts();
645 }
646
647 #[cfg(feature = "plugins")]
649 let plugin_render = {
650 let render = self.plugin_render_requested;
651 self.plugin_render_requested = false;
652 render
653 };
654 #[cfg(not(feature = "plugins"))]
655 let plugin_render = false;
656
657 if let Some(ref mut checker) = self.update_checker {
659 let _ = checker.poll_result();
661 }
662
663 let file_changes = {
665 let _s = tracing::info_span!("poll_file_changes").entered();
666 self.poll_file_changes()
667 };
668 let tree_changes = {
669 let _s = tracing::info_span!("poll_file_tree_changes").entered();
670 self.poll_file_tree_changes()
671 };
672
673 needs_render || processed_any_commands || plugin_render || file_changes || tree_changes
675 }
676}