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