1use std::sync::Arc;
15
16use anyhow::Result as AnyhowResult;
17
18use fresh_core::api::{BufferSavedDiff, JsCallbackId, PluginCommand};
19
20use crate::model::event::{BufferId, LeafId, SplitId};
21use crate::services::async_bridge::AsyncMessage;
22use crate::view::split::SplitViewState;
23
24use super::window::Window;
25use super::{Editor, FloatingWidgetState};
26
27fn normalize_plugin_path(path: std::path::PathBuf) -> std::path::PathBuf {
43 #[cfg(windows)]
44 {
45 let canonical = canonicalize_deepest_existing(&path);
46 let s = canonical.to_string_lossy();
47 if let Some(stripped) = s.strip_prefix(r"\\?\") {
48 return std::path::PathBuf::from(stripped);
49 }
50 return canonical;
51 }
52 #[cfg(not(windows))]
53 path
54}
55
56#[cfg(windows)]
57fn canonicalize_deepest_existing(path: &std::path::Path) -> std::path::PathBuf {
58 if let Ok(c) = path.canonicalize() {
59 return c;
60 }
61 let mut tail: Vec<&std::ffi::OsStr> = Vec::new();
65 let mut ancestor = path;
66 loop {
67 let Some(parent) = ancestor.parent() else {
68 return path.to_path_buf();
69 };
70 if let Some(name) = ancestor.file_name() {
71 tail.push(name);
72 }
73 if let Ok(c) = parent.canonicalize() {
74 let mut out = c;
75 for name in tail.iter().rev() {
76 out.push(name);
77 }
78 return out;
79 }
80 ancestor = parent;
81 }
82}
83
84fn buffer_line_byte_offset(
89 content: &str,
90 buffer_len: usize,
91 line: usize,
92 want_end: bool,
93) -> Option<usize> {
94 if !want_end && line == 0 {
95 return Some(0);
96 }
97 let mut current_line = 0usize;
98 for (byte_idx, c) in content.char_indices() {
99 if c == '\n' {
100 if want_end && current_line == line {
101 return Some(byte_idx);
102 }
103 current_line += 1;
104 if !want_end && current_line == line {
105 return Some(byte_idx + 1);
106 }
107 }
108 }
109 if want_end && current_line == line {
110 Some(buffer_len)
111 } else {
112 None
113 }
114}
115
116impl Editor {
117 #[cfg(feature = "plugins")]
126 pub fn update_plugin_state_snapshot(&mut self) {
127 let Some(snapshot_handle) = self.plugin_manager.read().unwrap().state_snapshot_handle()
128 else {
129 return;
130 };
131 let mut snapshot = snapshot_handle.write().unwrap();
132
133 self.active_window_mut()
134 .populate_plugin_state_snapshot(&mut snapshot);
135
136 snapshot.clipboard = self.clipboard.get_internal().to_string();
140 snapshot.working_dir = self.working_dir().to_path_buf();
141
142 snapshot.terminal_width = self.terminal_width;
146 snapshot.terminal_height = self.terminal_height;
147
148 snapshot.authority_label = self.authority().display_label.clone();
155
156 snapshot.workspace_trust_level = self
159 .authority()
160 .workspace_trust
161 .level()
162 .as_str()
163 .to_string();
164 snapshot.env_active = self.authority().env_provider.is_active();
165
166 let mut session_infos: Vec<fresh_core::api::WindowInfo> = self
172 .windows
173 .values()
174 .map(|s| {
175 let slot = s.plugin_state.get("orchestrator");
176 let project_path = slot
183 .and_then(|m| m.get("project_path"))
184 .and_then(|v| v.as_str())
185 .filter(|p| !p.is_empty())
186 .map(std::path::PathBuf::from)
187 .unwrap_or_else(|| s.root.clone());
188 let shared_worktree = slot
189 .and_then(|m| m.get("shared_worktree"))
190 .and_then(|v| v.as_bool())
191 .unwrap_or(false);
192 fresh_core::api::WindowInfo {
193 id: s.id,
194 label: s.label.clone(),
195 root: normalize_plugin_path(s.root.clone()),
196 project_path: normalize_plugin_path(project_path),
197 shared_worktree,
198 }
199 })
200 .collect();
201 session_infos.sort_by_key(|s| s.id.0);
202 snapshot.windows = session_infos;
203 snapshot.active_window_id = self.active_window;
204
205 if !Arc::ptr_eq(&self.config, &self.config_snapshot_anchor) {
214 let json = serde_json::to_value(&*self.config).unwrap_or(serde_json::Value::Null);
215 self.config_cached_json = Arc::new(json);
216 self.config_snapshot_anchor = Arc::clone(&self.config);
217 }
218 snapshot.config = Arc::clone(&self.config_cached_json);
219
220 snapshot.user_config = Arc::clone(&self.user_config_raw);
223
224 for (plugin_name, state_map) in &self.plugin_global_state {
227 let entry = snapshot
228 .plugin_global_states
229 .entry(plugin_name.clone())
230 .or_default();
231 for (key, value) in state_map {
232 entry.entry(key.clone()).or_insert_with(|| value.clone());
233 }
234 }
235 }
236
237 pub fn handle_plugin_command(&mut self, command: PluginCommand) -> AnyhowResult<()> {
239 match command {
240 PluginCommand::InsertText {
242 buffer_id,
243 position,
244 text,
245 } => {
246 self.handle_insert_text(buffer_id, position, text);
247 }
248 PluginCommand::DeleteRange { buffer_id, range } => {
249 self.handle_delete_range(buffer_id, range);
250 }
251 PluginCommand::InsertAtCursor { text } => {
252 self.handle_insert_at_cursor(text);
253 }
254 PluginCommand::DeleteSelection => {
255 self.handle_delete_selection();
256 }
257
258 PluginCommand::AddOverlay {
260 buffer_id,
261 namespace,
262 range,
263 options,
264 } => {
265 self.handle_add_overlay(buffer_id, namespace, range, options);
266 }
267 PluginCommand::RemoveOverlay { buffer_id, handle } => {
268 self.handle_remove_overlay(buffer_id, handle);
269 }
270 PluginCommand::ClearAllOverlays { buffer_id } => {
271 self.handle_clear_all_overlays(buffer_id);
272 }
273 PluginCommand::ClearNamespace {
274 buffer_id,
275 namespace,
276 } => {
277 self.handle_clear_namespace(buffer_id, namespace);
278 }
279 PluginCommand::ClearOverlaysInRange {
280 buffer_id,
281 start,
282 end,
283 } => {
284 self.handle_clear_overlays_in_range(buffer_id, start, end);
285 }
286 PluginCommand::ClearOverlaysInRangeForNamespace {
287 buffer_id,
288 namespace,
289 start,
290 end,
291 } => {
292 self.handle_clear_overlays_in_range_for_namespace(buffer_id, namespace, start, end);
293 }
294
295 PluginCommand::AddVirtualText {
297 buffer_id,
298 virtual_text_id,
299 position,
300 text,
301 color,
302 use_bg,
303 before,
304 } => {
305 self.handle_add_virtual_text(
306 buffer_id,
307 virtual_text_id,
308 position,
309 text,
310 color,
311 use_bg,
312 before,
313 );
314 }
315 PluginCommand::AddVirtualTextStyled {
316 buffer_id,
317 virtual_text_id,
318 position,
319 text,
320 fg,
321 bg,
322 bold,
323 italic,
324 before,
325 } => {
326 self.handle_add_virtual_text_styled(
327 buffer_id,
328 virtual_text_id,
329 position,
330 text,
331 fg,
332 bg,
333 bold,
334 italic,
335 before,
336 );
337 }
338 PluginCommand::RemoveVirtualText {
339 buffer_id,
340 virtual_text_id,
341 } => {
342 self.handle_remove_virtual_text(buffer_id, virtual_text_id);
343 }
344 PluginCommand::RemoveVirtualTextsByPrefix { buffer_id, prefix } => {
345 self.handle_remove_virtual_texts_by_prefix(buffer_id, prefix);
346 }
347 PluginCommand::ClearVirtualTexts { buffer_id } => {
348 self.handle_clear_virtual_texts(buffer_id);
349 }
350 PluginCommand::AddVirtualLine {
351 buffer_id,
352 position,
353 text,
354 fg_color,
355 bg_color,
356 above,
357 namespace,
358 priority,
359 gutter_glyph,
360 gutter_color,
361 text_overlays,
362 } => {
363 self.handle_add_virtual_line(
364 buffer_id,
365 position,
366 text,
367 fg_color,
368 bg_color,
369 above,
370 namespace,
371 priority,
372 gutter_glyph,
373 gutter_color,
374 text_overlays,
375 );
376 }
377 PluginCommand::ClearVirtualTextNamespace {
378 buffer_id,
379 namespace,
380 } => {
381 self.handle_clear_virtual_text_namespace(buffer_id, namespace);
382 }
383
384 PluginCommand::AddConceal {
386 buffer_id,
387 namespace,
388 start,
389 end,
390 replacement,
391 } => {
392 self.handle_add_conceal(buffer_id, namespace, start, end, replacement);
393 }
394 PluginCommand::ClearConcealNamespace {
395 buffer_id,
396 namespace,
397 } => {
398 self.handle_clear_conceal_namespace(buffer_id, namespace);
399 }
400 PluginCommand::ClearConcealsInRange {
401 buffer_id,
402 start,
403 end,
404 } => {
405 self.handle_clear_conceals_in_range(buffer_id, start, end);
406 }
407
408 PluginCommand::AddFold {
409 buffer_id,
410 start,
411 end,
412 placeholder,
413 } => {
414 self.handle_add_fold(buffer_id, start, end, placeholder);
415 }
416 PluginCommand::ClearFolds { buffer_id } => {
417 self.handle_clear_folds(buffer_id);
418 }
419 PluginCommand::SetFoldingRanges { buffer_id, ranges } => {
420 self.handle_set_folding_ranges(buffer_id, ranges);
421 }
422
423 PluginCommand::AddSoftBreak {
425 buffer_id,
426 namespace,
427 position,
428 indent,
429 } => {
430 self.handle_add_soft_break(buffer_id, namespace, position, indent);
431 }
432 PluginCommand::ClearSoftBreakNamespace {
433 buffer_id,
434 namespace,
435 } => {
436 self.handle_clear_soft_break_namespace(buffer_id, namespace);
437 }
438 PluginCommand::ClearSoftBreaksInRange {
439 buffer_id,
440 start,
441 end,
442 } => {
443 self.handle_clear_soft_breaks_in_range(buffer_id, start, end);
444 }
445
446 PluginCommand::AddMenuItem {
448 menu_label,
449 item,
450 position,
451 } => {
452 self.handle_add_menu_item(menu_label, item, position);
453 }
454 PluginCommand::AddMenu { menu, position } => {
455 self.handle_add_menu(menu, position);
456 }
457 PluginCommand::RemoveMenuItem {
458 menu_label,
459 item_label,
460 } => {
461 self.handle_remove_menu_item(menu_label, item_label);
462 }
463 PluginCommand::RemoveMenu { menu_label } => {
464 self.handle_remove_menu(menu_label);
465 }
466
467 PluginCommand::FocusSplit { split_id } => {
469 self.handle_focus_split(split_id);
470 }
471 PluginCommand::SetSplitBuffer {
472 split_id,
473 buffer_id,
474 } => {
475 self.handle_set_split_buffer(split_id, buffer_id);
476 }
477 PluginCommand::SetSplitScroll { split_id, top_byte } => {
478 self.handle_set_split_scroll(split_id, top_byte);
479 }
480 PluginCommand::RequestHighlights {
481 buffer_id,
482 range,
483 request_id,
484 } => {
485 self.handle_request_highlights(buffer_id, range, request_id);
486 }
487 PluginCommand::CloseSplit { split_id } => {
488 self.handle_close_split(split_id);
489 }
490 PluginCommand::SetSplitRatio { split_id, ratio } => {
491 self.handle_set_split_ratio(split_id, ratio);
492 }
493 PluginCommand::SetSplitLabel { split_id, label } => {
494 self.handle_set_split_label(split_id, label);
495 }
496 PluginCommand::ClearSplitLabel { split_id } => {
497 self.handle_clear_split_label(split_id);
498 }
499 PluginCommand::GetSplitByLabel { label, request_id } => {
500 self.handle_get_split_by_label(label, request_id);
501 }
502 PluginCommand::DistributeSplitsEvenly { split_ids: _ } => {
503 self.handle_distribute_splits_evenly();
504 }
505 PluginCommand::SetBufferCursor {
506 buffer_id,
507 position,
508 } => {
509 self.handle_set_buffer_cursor(buffer_id, position);
510 }
511 PluginCommand::SetBufferShowCursors { buffer_id, show } => {
512 self.handle_set_buffer_show_cursors(buffer_id, show);
513 }
514
515 PluginCommand::SetLayoutHints {
517 buffer_id,
518 split_id,
519 range: _,
520 hints,
521 } => {
522 self.handle_set_layout_hints(buffer_id, split_id, hints);
523 }
524 PluginCommand::SetLineNumbers { buffer_id, enabled } => {
525 self.handle_set_line_numbers(buffer_id, enabled);
526 }
527 PluginCommand::SetViewMode { buffer_id, mode } => {
528 self.handle_set_view_mode(buffer_id, &mode);
529 }
530 PluginCommand::SetLineWrap {
531 buffer_id,
532 split_id,
533 enabled,
534 } => {
535 self.handle_set_line_wrap(buffer_id, split_id, enabled);
536 }
537 PluginCommand::SubmitViewTransform {
538 buffer_id,
539 split_id,
540 payload,
541 } => {
542 self.handle_submit_view_transform(buffer_id, split_id, payload);
543 }
544 PluginCommand::ClearViewTransform {
545 buffer_id: _,
546 split_id,
547 } => {
548 self.handle_clear_view_transform(split_id);
549 }
550 PluginCommand::SetViewState {
551 buffer_id,
552 key,
553 value,
554 } => {
555 self.handle_set_view_state(buffer_id, key, value);
556 }
557 PluginCommand::SetGlobalState {
558 plugin_name,
559 key,
560 value,
561 } => {
562 self.handle_set_global_state(plugin_name, key, value);
563 }
564 PluginCommand::SetWindowState {
565 plugin_name,
566 key,
567 value,
568 } => {
569 self.handle_set_session_state(plugin_name, key, value);
570 }
571 PluginCommand::RefreshLines { buffer_id } => {
572 self.handle_refresh_lines(buffer_id);
573 }
574 PluginCommand::RefreshAllLines => {
575 self.handle_refresh_all_lines();
576 }
577 PluginCommand::HookCompleted { .. } => {
578 }
580 PluginCommand::SetLineIndicator {
581 buffer_id,
582 line,
583 namespace,
584 symbol,
585 color,
586 priority,
587 } => {
588 self.handle_set_line_indicator(buffer_id, line, namespace, symbol, color, priority);
589 }
590 PluginCommand::SetLineIndicators {
591 buffer_id,
592 lines,
593 namespace,
594 symbol,
595 color,
596 priority,
597 } => {
598 self.handle_set_line_indicators(
599 buffer_id, lines, namespace, symbol, color, priority,
600 );
601 }
602 PluginCommand::ClearLineIndicators {
603 buffer_id,
604 namespace,
605 } => {
606 self.handle_clear_line_indicators(buffer_id, namespace);
607 }
608 PluginCommand::SetFileExplorerDecorations {
609 namespace,
610 decorations,
611 } => {
612 self.active_window_mut()
613 .handle_set_file_explorer_decorations(namespace, decorations);
614 }
615 PluginCommand::ClearFileExplorerDecorations { namespace } => {
616 self.active_window_mut()
617 .handle_clear_file_explorer_decorations(&namespace);
618 }
619 PluginCommand::SetFileExplorerSlots { namespace, slots } => {
620 self.active_window_mut()
621 .handle_set_file_explorer_slots(namespace, slots);
622 }
623 PluginCommand::ClearFileExplorerSlots { namespace } => {
624 self.active_window_mut()
625 .handle_clear_file_explorer_slots(&namespace);
626 }
627
628 PluginCommand::SetStatus { message } => {
630 self.handle_set_status(message);
631 }
632 PluginCommand::ApplyTheme { theme_name } => {
633 self.apply_theme(&theme_name);
634 }
635 PluginCommand::OverrideThemeColors { overrides } => {
636 self.handle_override_theme_colors(overrides);
637 }
638 PluginCommand::ReloadConfig => {
639 self.reload_config();
640 }
641 PluginCommand::SetSetting { path, value, .. } => {
642 self.handle_set_setting(path, value);
643 }
644 PluginCommand::AddPluginConfigField {
645 plugin_name,
646 field_name,
647 field_schema,
648 } => {
649 self.handle_add_plugin_config_field(plugin_name, field_name, field_schema);
650 }
651 PluginCommand::ReloadThemes { apply_theme } => {
652 self.handle_reload_themes(apply_theme);
653 }
654 PluginCommand::RegisterGrammar {
655 language,
656 grammar_path,
657 extensions,
658 } => {
659 self.handle_register_grammar(language, grammar_path, extensions);
660 }
661 PluginCommand::RegisterLanguageConfig { language, config } => {
662 self.handle_register_language_config(language, config);
663 }
664 PluginCommand::RegisterLspServer { language, config } => {
665 self.handle_register_lsp_server(language, config);
666 }
667 PluginCommand::ReloadGrammars { callback_id } => {
668 self.handle_reload_grammars(callback_id);
669 }
670 PluginCommand::CancelPrompt => {
671 self.cancel_prompt();
672 }
673 PluginCommand::StartPrompt {
674 label,
675 prompt_type,
676 floating_overlay,
677 } => {
678 self.handle_start_prompt(label, prompt_type, floating_overlay);
679 }
680 PluginCommand::StartPromptWithInitial {
681 label,
682 prompt_type,
683 initial_value,
684 floating_overlay,
685 } => {
686 self.handle_start_prompt_with_initial(
687 label,
688 prompt_type,
689 initial_value,
690 floating_overlay,
691 );
692 }
693 PluginCommand::StartPromptAsync {
694 label,
695 initial_value,
696 callback_id,
697 } => {
698 self.handle_start_prompt_async(label, initial_value, callback_id);
699 }
700 PluginCommand::AwaitNextKey { callback_id } => {
701 self.handle_await_next_key(callback_id);
702 }
703 PluginCommand::SetKeyCaptureActive { active } => {
704 self.handle_set_key_capture_active(active);
705 }
706 PluginCommand::SetPromptSuggestions {
707 suggestions,
708 selected_index,
709 } => {
710 self.handle_set_prompt_suggestions(suggestions, selected_index);
711 }
712 PluginCommand::SetPromptInputSync { sync } => {
713 self.handle_set_prompt_input_sync(sync);
714 }
715 PluginCommand::SetPromptTitle { title } => {
716 self.handle_set_prompt_title(title);
717 }
718 PluginCommand::SetPromptFooter { footer } => {
719 self.handle_set_prompt_footer(footer);
720 }
721 PluginCommand::SetPromptToolbar { spec } => {
722 self.handle_set_prompt_toolbar(spec);
723 }
724 PluginCommand::ToggleOverlayToolbarWidget { key } => {
725 self.toggle_overlay_toolbar_widget(&key);
726 }
727 PluginCommand::SetPromptStatus { status } => {
728 self.handle_set_prompt_status(status);
729 }
730 PluginCommand::SetPromptSelectedIndex { index } => {
731 self.handle_set_prompt_selected_index(index);
732 }
733
734 PluginCommand::CreateWindow { root, label } => {
737 self.handle_create_window(root, label);
738 }
739 PluginCommand::CreateWindowWithTerminal {
740 root,
741 label,
742 cwd,
743 command,
744 title,
745 resume,
746 request_id,
747 } => {
748 self.handle_create_window_with_terminal(
749 root, label, cwd, command, title, resume, request_id,
750 );
751 }
752 PluginCommand::SetActiveWindow { id } => {
753 self.set_active_window(id);
754 }
755 PluginCommand::SetActiveWindowAnimated { id, from_edge } => {
756 self.set_active_window_animated(id, &from_edge);
757 }
758 PluginCommand::SetWindowCycleOrder { ids } => {
759 self.window_cycle_order = if ids.is_empty() { None } else { Some(ids) };
760 }
761 PluginCommand::CloseWindow { id } => {
762 let _ = self.close_window(id);
763 }
764 PluginCommand::PrewarmWindow { id } => {
765 self.prewarm_window(id);
766 }
767
768 PluginCommand::WatchPath {
770 path,
771 recursive,
772 request_id,
773 } => {
774 self.handle_watch_path(path, recursive, request_id);
775 }
776 PluginCommand::UnwatchPath { handle } => {
777 self.file_watcher_manager.unwatch(handle);
778 }
779
780 PluginCommand::PreviewWindowInRect { id } => {
781 self.handle_preview_window_in_rect(id);
782 }
783
784 PluginCommand::RegisterCommand { command } => {
786 self.handle_register_command(command);
787 }
788 PluginCommand::RegisterStatusBarElement {
789 plugin_name,
790 token_name,
791 title,
792 } => {
793 self.handle_register_status_bar_element(plugin_name, token_name, title);
794 }
795 PluginCommand::SetStatusBarValue {
796 buffer_id,
797 key,
798 value,
799 } => {
800 self.handle_set_status_bar_value(buffer_id, key, value);
801 }
802 PluginCommand::UnregisterCommand { name } => {
803 self.handle_unregister_command(name);
804 }
805 PluginCommand::DefineMode {
806 name,
807 bindings,
808 read_only,
809 allow_text_input,
810 inherit_normal_bindings,
811 plugin_name,
812 } => {
813 self.handle_define_mode(
814 name,
815 bindings,
816 read_only,
817 allow_text_input,
818 inherit_normal_bindings,
819 plugin_name,
820 );
821 }
822
823 PluginCommand::OpenFileInBackground { path, window_id } => {
825 self.handle_open_file_in_background_routed(path, window_id);
826 }
827 PluginCommand::OpenFileAtLocation { path, line, column } => {
828 return self.handle_open_file_at_location(path, line, column);
829 }
830 PluginCommand::OpenFileInSplit {
831 split_id,
832 path,
833 line,
834 column,
835 } => {
836 return self.handle_open_file_in_split(split_id, path, line, column);
837 }
838 PluginCommand::ShowBuffer { buffer_id } => {
839 self.handle_show_buffer(buffer_id);
840 }
841 PluginCommand::CloseBuffer { buffer_id } => {
842 self.handle_close_buffer(buffer_id);
843 }
844 PluginCommand::CloseOtherBuffersInSplit {
845 buffer_id,
846 split_id,
847 } => {
848 self.handle_close_other_buffers_in_split(buffer_id, split_id);
849 }
850 PluginCommand::CloseAllBuffersInSplit { split_id } => {
851 self.handle_close_all_buffers_in_split(split_id);
852 }
853 PluginCommand::CloseBuffersToRightInSplit {
854 buffer_id,
855 split_id,
856 } => {
857 self.handle_close_buffers_to_right_in_split(buffer_id, split_id);
858 }
859 PluginCommand::CloseBuffersToLeftInSplit {
860 buffer_id,
861 split_id,
862 } => {
863 self.handle_close_buffers_to_left_in_split(buffer_id, split_id);
864 }
865
866 PluginCommand::MoveTabLeft => {
867 self.handle_move_tab_left();
868 }
869 PluginCommand::MoveTabRight => {
870 self.handle_move_tab_right();
871 }
872
873 PluginCommand::StartAnimationArea { id, rect, kind } => {
875 self.handle_start_animation_area(id, rect, kind);
876 }
877 PluginCommand::StartAnimationVirtualBuffer {
878 id,
879 buffer_id,
880 kind,
881 } => {
882 self.handle_start_animation_virtual_buffer(id, buffer_id, kind);
883 }
884 PluginCommand::CancelAnimation { id } => {
885 self.handle_cancel_animation(id);
886 }
887
888 PluginCommand::SendLspRequest {
890 language,
891 method,
892 params,
893 request_id,
894 } => {
895 self.handle_send_lsp_request(language, method, params, request_id);
896 }
897
898 PluginCommand::SetClipboard { text } => {
900 self.handle_set_clipboard(text);
901 }
902
903 PluginCommand::SpawnProcess {
905 command,
906 args,
907 cwd,
908 stdout_to,
909 callback_id,
910 } => {
911 self.handle_spawn_process(command, args, cwd, stdout_to, callback_id);
912 }
913
914 PluginCommand::SpawnHostProcess {
915 command,
916 args,
917 cwd,
918 callback_id,
919 } => {
920 self.handle_spawn_host_process(command, args, cwd, callback_id);
921 }
922
923 PluginCommand::KillHostProcess { process_id } => {
924 self.handle_kill_host_process(process_id);
925 }
926
927 PluginCommand::SetAuthority { payload } => {
928 self.handle_set_authority(payload);
929 }
930
931 PluginCommand::AttachRemoteAgent {
932 payload,
933 request_id,
934 } => {
935 self.handle_attach_remote_agent(payload, request_id);
936 }
937
938 PluginCommand::CancelRemoteAttach => {
939 self.cancel_remote_attaches();
940 }
941
942 PluginCommand::ClearAuthority => {
943 self.handle_clear_authority();
944 }
945
946 PluginCommand::SetEnv { snippet, dir } => {
947 self.handle_set_env(snippet, dir);
948 }
949
950 PluginCommand::ClearEnv => {
951 self.handle_clear_env();
952 }
953
954 PluginCommand::SetRemoteIndicatorState { state } => {
955 self.handle_set_remote_indicator_state(state);
956 }
957
958 PluginCommand::ClearRemoteIndicatorState => {
959 self.remote_indicator_override = None;
960 }
961
962 PluginCommand::SpawnProcessWait {
963 process_id,
964 callback_id,
965 } => {
966 self.handle_spawn_process_wait(process_id, callback_id);
967 }
968
969 PluginCommand::Delay {
970 callback_id,
971 duration_ms,
972 } => {
973 self.handle_delay(callback_id, duration_ms);
974 }
975
976 PluginCommand::HttpFetch {
977 url,
978 target_path,
979 callback_id,
980 } => {
981 self.handle_http_fetch(url, target_path, callback_id);
982 }
983
984 PluginCommand::SpawnBackgroundProcess {
985 process_id,
986 command,
987 args,
988 cwd,
989 callback_id,
990 } => {
991 self.handle_spawn_background_process(process_id, command, args, cwd, callback_id);
992 }
993
994 PluginCommand::KillBackgroundProcess { process_id } => {
995 self.handle_kill_background_process(process_id);
996 }
997
998 PluginCommand::CreateVirtualBuffer {
1000 name,
1001 mode,
1002 read_only,
1003 } => {
1004 self.handle_create_virtual_buffer(name, mode, read_only);
1005 }
1006 PluginCommand::CreateVirtualBufferWithContent {
1007 name,
1008 mode,
1009 read_only,
1010 entries,
1011 show_line_numbers,
1012 show_cursors,
1013 editing_disabled,
1014 hidden_from_tabs,
1015 request_id,
1016 } => {
1017 self.handle_create_virtual_buffer_with_content(
1018 name,
1019 mode,
1020 read_only,
1021 entries,
1022 show_line_numbers,
1023 show_cursors,
1024 editing_disabled,
1025 hidden_from_tabs,
1026 request_id,
1027 );
1028 }
1029 PluginCommand::CreateVirtualBufferInSplit {
1030 name,
1031 mode,
1032 read_only,
1033 entries,
1034 ratio,
1035 direction,
1036 panel_id,
1037 show_line_numbers,
1038 show_cursors,
1039 editing_disabled,
1040 line_wrap,
1041 before,
1042 role,
1043 request_id,
1044 } => {
1045 self.handle_create_virtual_buffer_in_split(
1046 name,
1047 mode,
1048 read_only,
1049 entries,
1050 ratio,
1051 direction,
1052 panel_id,
1053 show_line_numbers,
1054 show_cursors,
1055 editing_disabled,
1056 line_wrap,
1057 before,
1058 role,
1059 request_id,
1060 );
1061 }
1062 PluginCommand::SetVirtualBufferContent { buffer_id, entries } => {
1063 self.handle_set_virtual_buffer_content(buffer_id, entries);
1064 }
1065 PluginCommand::GetTextPropertiesAtCursor { buffer_id } => {
1066 self.handle_get_text_properties_at_cursor(buffer_id);
1067 }
1068 PluginCommand::CreateVirtualBufferInExistingSplit {
1069 name,
1070 mode,
1071 read_only,
1072 entries,
1073 split_id,
1074 show_line_numbers,
1075 show_cursors,
1076 editing_disabled,
1077 line_wrap,
1078 request_id,
1079 } => {
1080 self.handle_create_virtual_buffer_in_existing_split(
1081 name,
1082 mode,
1083 read_only,
1084 entries,
1085 split_id,
1086 show_line_numbers,
1087 show_cursors,
1088 editing_disabled,
1089 line_wrap,
1090 request_id,
1091 );
1092 }
1093
1094 PluginCommand::SetContext { name, active } => {
1096 self.handle_set_context(name, active);
1097 }
1098
1099 PluginCommand::SetReviewDiffHunks { hunks } => {
1101 self.handle_set_review_diff_hunks(hunks);
1102 }
1103
1104 PluginCommand::ExecuteAction { action_name } => {
1106 self.handle_execute_action(action_name);
1107 }
1108 PluginCommand::ExecuteActions { actions } => {
1109 self.handle_execute_actions(actions);
1110 }
1111 PluginCommand::GetBufferText {
1112 buffer_id,
1113 start,
1114 end,
1115 request_id,
1116 } => {
1117 self.handle_get_buffer_text(buffer_id, start, end, request_id);
1118 }
1119 PluginCommand::GetLineStartPosition {
1120 buffer_id,
1121 line,
1122 request_id,
1123 } => {
1124 self.handle_get_line_start_position(buffer_id, line, request_id);
1125 }
1126 PluginCommand::GetLineEndPosition {
1127 buffer_id,
1128 line,
1129 request_id,
1130 } => {
1131 self.handle_get_line_end_position(buffer_id, line, request_id);
1132 }
1133 PluginCommand::GetBufferLineCount {
1134 buffer_id,
1135 request_id,
1136 } => {
1137 self.handle_get_buffer_line_count(buffer_id, request_id);
1138 }
1139 PluginCommand::GetCompositeCursorInfo { request_id } => {
1140 self.handle_get_composite_cursor_info(request_id);
1141 }
1142 PluginCommand::OpenFileStreaming { path, request_id } => {
1143 self.handle_open_file_streaming(path, request_id);
1144 }
1145 PluginCommand::RefreshBufferFromDisk {
1146 buffer_id,
1147 request_id,
1148 } => {
1149 self.handle_refresh_buffer_from_disk(buffer_id, request_id);
1150 }
1151 PluginCommand::SetBufferGroupPanelBuffer {
1152 group_id,
1153 panel_name,
1154 buffer_id,
1155 request_id,
1156 } => {
1157 self.handle_set_buffer_group_panel_buffer(
1158 group_id, panel_name, buffer_id, request_id,
1159 );
1160 }
1161 PluginCommand::ScrollToLineCenter {
1162 split_id,
1163 buffer_id,
1164 line,
1165 } => {
1166 self.handle_scroll_to_line_center(split_id, buffer_id, line);
1167 }
1168 PluginCommand::ScrollBufferToLine { buffer_id, line } => {
1169 self.handle_scroll_buffer_to_line(buffer_id, line);
1170 }
1171 PluginCommand::SetEditorMode { mode } => {
1172 self.handle_set_editor_mode(mode);
1173 }
1174
1175 PluginCommand::ShowActionPopup {
1177 popup_id,
1178 title,
1179 message,
1180 actions,
1181 } => {
1182 self.handle_show_action_popup(popup_id, title, message, actions);
1183 }
1184
1185 PluginCommand::SetLspMenuContributions {
1186 plugin_id,
1187 language,
1188 items,
1189 } => {
1190 self.handle_set_lsp_menu_contributions(plugin_id, language, items);
1191 }
1192
1193 PluginCommand::DisableLspForLanguage { language } => {
1194 self.handle_disable_lsp_for_language(language);
1195 }
1196
1197 PluginCommand::RestartLspForLanguage { language } => {
1198 self.handle_restart_lsp_for_language(language);
1199 }
1200
1201 PluginCommand::SetLspRootUri { language, uri } => {
1202 self.handle_set_lsp_root_uri(language, uri);
1203 }
1204
1205 PluginCommand::CreateScrollSyncGroup {
1207 group_id,
1208 left_split,
1209 right_split,
1210 } => {
1211 self.handle_create_scroll_sync_group(group_id, left_split, right_split);
1212 }
1213 PluginCommand::SetScrollSyncAnchors { group_id, anchors } => {
1214 self.handle_set_scroll_sync_anchors(group_id, anchors);
1215 }
1216 PluginCommand::RemoveScrollSyncGroup { group_id } => {
1217 self.handle_remove_scroll_sync_group(group_id);
1218 }
1219
1220 PluginCommand::CreateCompositeBuffer {
1222 name,
1223 mode,
1224 layout,
1225 sources,
1226 hunks,
1227 initial_focus_hunk,
1228 request_id,
1229 } => {
1230 self.handle_create_composite_buffer(
1231 name,
1232 mode,
1233 layout,
1234 sources,
1235 hunks,
1236 initial_focus_hunk,
1237 request_id,
1238 );
1239 }
1240 PluginCommand::UpdateCompositeAlignment { buffer_id, hunks } => {
1241 self.handle_update_composite_alignment(buffer_id, hunks);
1242 }
1243 PluginCommand::CloseCompositeBuffer { buffer_id } => {
1244 self.active_window_mut().close_composite_buffer(buffer_id);
1245 }
1246 PluginCommand::FlushLayout => {
1247 self.flush_layout();
1248 }
1249 PluginCommand::CompositeNextHunk { buffer_id } => {
1250 self.handle_composite_next_hunk(buffer_id);
1251 }
1252 PluginCommand::CompositePrevHunk { buffer_id } => {
1253 self.handle_composite_prev_hunk(buffer_id);
1254 }
1255
1256 PluginCommand::CreateBufferGroup {
1258 name,
1259 mode,
1260 layout_json,
1261 request_id,
1262 } => {
1263 self.handle_create_buffer_group(name, mode, layout_json, request_id);
1264 }
1265 PluginCommand::SetPanelContent {
1266 group_id,
1267 panel_name,
1268 entries,
1269 } => {
1270 self.set_panel_content(group_id, panel_name, entries);
1271 }
1272 PluginCommand::CloseBufferGroup { group_id } => {
1273 self.close_buffer_group(group_id);
1274 }
1275 PluginCommand::FocusPanel {
1276 group_id,
1277 panel_name,
1278 } => {
1279 self.focus_panel(group_id, panel_name);
1280 }
1281
1282 PluginCommand::SaveBufferToPath { buffer_id, path } => {
1284 self.handle_save_buffer_to_path(buffer_id, path);
1285 }
1286
1287 #[cfg(feature = "plugins")]
1289 PluginCommand::LoadPlugin { path, callback_id } => {
1290 self.handle_load_plugin(path, callback_id);
1291 }
1292 #[cfg(feature = "plugins")]
1293 PluginCommand::UnloadPlugin { name, callback_id } => {
1294 self.handle_unload_plugin(name, callback_id);
1295 }
1296 #[cfg(feature = "plugins")]
1297 PluginCommand::ReloadPlugin { name, callback_id } => {
1298 self.handle_reload_plugin(name, callback_id);
1299 }
1300 #[cfg(feature = "plugins")]
1301 PluginCommand::ListPlugins { callback_id } => {
1302 self.handle_list_plugins(callback_id);
1303 }
1304 #[cfg(not(feature = "plugins"))]
1306 PluginCommand::LoadPlugin { .. }
1307 | PluginCommand::UnloadPlugin { .. }
1308 | PluginCommand::ReloadPlugin { .. }
1309 | PluginCommand::ListPlugins { .. } => {
1310 tracing::warn!("Plugin management commands require the 'plugins' feature");
1311 }
1312
1313 PluginCommand::CreateTerminal {
1315 cwd,
1316 direction,
1317 ratio,
1318 focus,
1319 persistent,
1320 window_id,
1321 command,
1322 title,
1323 request_id,
1324 } => {
1325 self.handle_create_terminal(
1326 cwd, direction, ratio, focus, persistent, window_id, command, title, request_id,
1327 );
1328 }
1329
1330 PluginCommand::SendTerminalInput { terminal_id, data } => {
1331 self.handle_send_terminal_input(terminal_id, data);
1332 }
1333
1334 PluginCommand::CloseTerminal { terminal_id } => {
1335 self.handle_close_terminal(terminal_id);
1336 }
1337
1338 PluginCommand::SignalWindow { id, signal } => {
1339 self.handle_signal_window(id, &signal);
1340 }
1341
1342 PluginCommand::GrepProject {
1343 pattern,
1344 fixed_string,
1345 case_sensitive,
1346 max_results,
1347 whole_words,
1348 callback_id,
1349 } => {
1350 self.handle_grep_project(
1351 pattern,
1352 fixed_string,
1353 case_sensitive,
1354 max_results,
1355 whole_words,
1356 callback_id,
1357 );
1358 }
1359
1360 PluginCommand::BeginSearch {
1361 pattern,
1362 fixed_string,
1363 case_sensitive,
1364 max_results,
1365 whole_words,
1366 source_buffer_id,
1367 handle_id,
1368 } => {
1369 self.handle_begin_search(
1370 pattern,
1371 fixed_string,
1372 case_sensitive,
1373 max_results,
1374 whole_words,
1375 source_buffer_id,
1376 handle_id,
1377 );
1378 }
1379
1380 PluginCommand::ReplaceInBuffer {
1381 file_path,
1382 buffer_id,
1383 matches,
1384 replacement,
1385 callback_id,
1386 } => {
1387 self.handle_replace_in_buffer(
1388 file_path,
1389 buffer_id,
1390 matches,
1391 replacement,
1392 callback_id,
1393 );
1394 }
1395
1396 PluginCommand::MountWidgetPanel {
1397 panel_id,
1398 buffer_id,
1399 spec,
1400 } => {
1401 self.handle_mount_widget_panel(panel_id, buffer_id, spec);
1402 }
1403
1404 PluginCommand::UpdateWidgetPanel { panel_id, spec } => {
1405 self.handle_update_widget_panel(panel_id, spec);
1406 }
1407
1408 PluginCommand::UnmountWidgetPanel { panel_id } => {
1409 self.handle_unmount_widget_panel(panel_id);
1410 }
1411
1412 PluginCommand::WidgetCommand { panel_id, action } => {
1413 self.handle_widget_command(panel_id, action);
1414 }
1415
1416 PluginCommand::WidgetMutate { panel_id, mutation } => {
1417 self.handle_widget_mutate(panel_id, mutation);
1418 }
1419
1420 PluginCommand::MountFloatingWidget {
1421 panel_id,
1422 spec,
1423 width_pct,
1424 height_pct,
1425 as_dock,
1426 } => {
1427 self.handle_mount_floating_widget(panel_id, spec, width_pct, height_pct, as_dock);
1428 }
1429
1430 PluginCommand::UpdateFloatingWidget { panel_id, spec } => {
1431 self.handle_update_floating_widget(panel_id, spec);
1432 }
1433
1434 PluginCommand::UnmountFloatingWidget { panel_id } => {
1435 self.handle_unmount_floating_widget(panel_id);
1436 }
1437
1438 PluginCommand::FloatingPanelControl { panel_id, op, arg } => {
1439 self.handle_floating_panel_control(panel_id, &op, arg);
1440 }
1441 }
1442 Ok(())
1443 }
1444
1445 fn handle_watch_path(&mut self, path: std::path::PathBuf, recursive: bool, request_id: u64) {
1448 let result = if let Some(ref bridge) = self.async_bridge {
1449 self.file_watcher_manager.watch(bridge, &path, recursive)
1450 } else {
1451 Err(
1452 "watchPath: no async bridge — file watching is unavailable in this build"
1453 .to_string(),
1454 )
1455 };
1456 self.last_watch_response_for_test = Some((request_id, result.clone()));
1457 self.send_plugin_response(fresh_core::api::PluginResponse::WatchPathRegistered {
1458 request_id,
1459 result,
1460 });
1461 }
1462
1463 fn handle_set_env(&mut self, snippet: String, dir: Option<String>) {
1464 use crate::services::workspace_trust::TrustLevel;
1468 if self.authority().workspace_trust.level() == TrustLevel::Trusted {
1469 self.authority()
1470 .env_provider
1471 .set(snippet, dir.map(std::path::PathBuf::from));
1472 self.request_restart(self.working_dir().to_path_buf());
1474 } else {
1475 self.active_window_mut().status_message =
1476 Some("Workspace not trusted — cannot activate environment".to_string());
1477 }
1478 }
1479
1480 fn handle_clear_env(&mut self) {
1481 let was_active = self.authority().env_provider.is_active();
1482 self.authority().env_provider.clear();
1483 if was_active {
1484 self.request_restart(self.working_dir().to_path_buf());
1485 }
1486 }
1487
1488 fn handle_open_file_in_background_routed(
1489 &mut self,
1490 path: std::path::PathBuf,
1491 window_id: Option<fresh_core::WindowId>,
1492 ) {
1493 let route_to_inactive =
1494 window_id.filter(|&id| id != self.active_window && self.windows.contains_key(&id));
1495 if let Some(target) = route_to_inactive {
1496 self.handle_open_file_in_inactive_session(target, path);
1497 } else {
1498 self.handle_open_file_in_background(path);
1499 }
1500 }
1501
1502 fn handle_set_split_label(&mut self, split_id: SplitId, label: String) {
1505 self.windows
1506 .get_mut(&self.active_window)
1507 .and_then(|w| w.split_manager_mut())
1508 .expect("active window must have a populated split layout")
1509 .set_label(LeafId(split_id), label);
1510 }
1511
1512 fn handle_clear_split_label(&mut self, split_id: SplitId) {
1513 self.windows
1514 .get_mut(&self.active_window)
1515 .and_then(|w| w.split_manager_mut())
1516 .expect("active window must have a populated split layout")
1517 .clear_label(split_id);
1518 }
1519
1520 fn handle_reload_themes(&mut self, apply_theme: Option<String>) {
1521 self.reload_themes();
1522 if let Some(theme_name) = apply_theme {
1523 self.apply_theme(&theme_name);
1524 }
1525 }
1526
1527 fn handle_set_key_capture_active(&mut self, active: bool) {
1528 self.active_window_mut().key_capture_active = active;
1529 if !active {
1530 self.active_window_mut().pending_key_capture_buffer.clear();
1533 }
1534 }
1535
1536 fn handle_set_prompt_input_sync(&mut self, sync: bool) {
1537 if let Some(prompt) = &mut self.active_window_mut().prompt {
1538 prompt.sync_input_on_navigate = sync;
1539 }
1540 }
1541
1542 fn handle_set_prompt_title(&mut self, title: Vec<fresh_core::api::StyledText>) {
1543 if let Some(prompt) = &mut self.active_window_mut().prompt {
1544 prompt.title = title;
1545 }
1546 }
1547
1548 fn handle_set_prompt_footer(&mut self, footer: Vec<fresh_core::api::StyledText>) {
1549 if let Some(prompt) = &mut self.active_window_mut().prompt {
1550 prompt.footer = footer;
1551 }
1552 }
1553
1554 fn handle_set_prompt_toolbar(&mut self, spec: Option<fresh_core::api::WidgetSpec>) {
1555 if let Some(prompt) = &mut self.active_window_mut().prompt {
1556 prompt.toolbar_widget = spec;
1557 }
1558 }
1559
1560 fn handle_set_prompt_status(&mut self, status: String) {
1561 if let Some(prompt) = &mut self.active_window_mut().prompt {
1562 prompt.status = status;
1563 }
1564 }
1565
1566 fn handle_set_prompt_selected_index(&mut self, index: u32) {
1567 if let Some(prompt) = &mut self.active_window_mut().prompt {
1568 let len = prompt.suggestions.len();
1569 if len > 0 {
1570 prompt.selected_suggestion = Some((index as usize).min(len - 1));
1571 }
1572 }
1573 }
1574
1575 fn handle_create_window(&mut self, root: std::path::PathBuf, label: String) {
1576 if !root.is_absolute() {
1577 tracing::warn!(
1578 "CreateWindow rejected: root must be absolute, got {:?}",
1579 root
1580 );
1581 } else {
1582 let _ = self.create_window_at(root, label);
1583 }
1584 }
1585
1586 fn handle_preview_window_in_rect(&mut self, id: Option<fresh_core::WindowId>) {
1587 self.preview_window_id = match id {
1590 Some(sid) if sid != self.active_window && self.windows.contains_key(&sid) => Some(sid),
1591 _ => None,
1592 };
1593 }
1594
1595 fn handle_register_status_bar_element(
1596 &mut self,
1597 plugin_name: String,
1598 token_name: String,
1599 title: String,
1600 ) {
1601 if let Err(e) = self.register_status_bar_element(&plugin_name, &token_name, &title) {
1602 tracing::warn!("Failed to register statusbar element: {}", e);
1603 }
1604 }
1605
1606 fn handle_set_status_bar_value(&mut self, buffer_id: u64, key: String, value: String) {
1607 if let Err(e) =
1608 self.set_status_bar_value(fresh_core::BufferId(buffer_id as usize), &key, value)
1609 {
1610 tracing::debug!("Skipped statusbar value for stale buffer: {}", e);
1613 }
1614 }
1615
1616 fn handle_cancel_animation(&mut self, id: u64) {
1617 self.active_window_mut()
1618 .animations
1619 .cancel(crate::view::animation::AnimationId::from_raw(id));
1620 }
1621
1622 fn handle_clear_authority(&mut self) {
1623 tracing::info!("Plugin cleared authority; restoring local");
1624 self.clear_authority();
1625 }
1626
1627 fn handle_set_review_diff_hunks(&mut self, hunks: Vec<fresh_core::api::ReviewHunk>) {
1628 self.active_window_mut().review_hunks = hunks;
1629 tracing::debug!(
1630 "Set {} review hunks",
1631 self.active_window_mut().review_hunks.len()
1632 );
1633 }
1634
1635 fn handle_composite_next_hunk(&mut self, buffer_id: fresh_core::BufferId) {
1636 let split_id = self.active_window().effective_active_pair().0;
1639 self.active_window_mut()
1640 .composite_next_hunk(split_id, buffer_id);
1641 }
1642
1643 fn handle_composite_prev_hunk(&mut self, buffer_id: fresh_core::BufferId) {
1644 let split_id = self.active_window().effective_active_pair().0;
1645 self.active_window_mut()
1646 .composite_prev_hunk(split_id, buffer_id);
1647 }
1648
1649 fn configure_vbuf_display(
1655 &mut self,
1656 buffer_id: crate::model::event::BufferId,
1657 show_line_numbers: bool,
1658 show_cursors: bool,
1659 editing_disabled: bool,
1660 ) {
1661 if let Some(state) = self
1662 .windows
1663 .get_mut(&self.active_window)
1664 .map(|w| &mut w.buffers)
1665 .expect("active window present")
1666 .get_mut(&buffer_id)
1667 {
1668 state.margins.configure_for_line_numbers(show_line_numbers);
1669 state.show_cursors = show_cursors;
1670 state.editing_disabled = editing_disabled;
1671 }
1672 }
1673
1674 #[allow(clippy::too_many_arguments)]
1679 fn route_vbuf_to_existing_dock(
1680 &mut self,
1681 dock_leaf: crate::model::event::LeafId,
1682 name: String,
1683 mode: String,
1684 read_only: bool,
1685 entries: Vec<fresh_core::text_property::TextPropertyEntry>,
1686 panel_id: Option<&str>,
1687 show_line_numbers: bool,
1688 show_cursors: bool,
1689 editing_disabled: bool,
1690 request_id: Option<u64>,
1691 ) {
1692 let source_split_before_create = self.split_manager().active_split();
1695 let buffer_id =
1696 self.active_window_mut()
1697 .create_virtual_buffer(name.clone(), mode, read_only);
1698 self.configure_vbuf_display(buffer_id, show_line_numbers, show_cursors, editing_disabled);
1699 if let Some(pid) = panel_id {
1700 self.panel_ids_mut().insert(pid.to_string(), buffer_id);
1701 }
1702 if let Err(e) = self.set_virtual_buffer_content(buffer_id, entries) {
1703 tracing::error!("Failed to set virtual buffer content (dock route): {}", e);
1704 return;
1705 }
1706 self.split_manager_mut().set_active_split(dock_leaf);
1708 self.active_window_mut()
1709 .set_pane_buffer(dock_leaf, buffer_id);
1710 if dock_leaf != source_split_before_create {
1712 if let Some(source_view_state) = self
1713 .windows
1714 .get_mut(&self.active_window)
1715 .and_then(|w| w.split_view_states_mut())
1716 .expect("active window must have a populated split layout")
1717 .get_mut(&source_split_before_create)
1718 {
1719 source_view_state.remove_buffer(buffer_id);
1720 }
1721 }
1722 if let Some(req_id) = request_id {
1723 let result = fresh_core::api::VirtualBufferResult {
1724 buffer_id: buffer_id.0 as u64,
1725 split_id: Some(dock_leaf.0 .0 as u64),
1726 };
1727 self.plugin_manager.read().unwrap().resolve_callback(
1728 fresh_core::api::JsCallbackId::from(req_id),
1729 serde_json::to_string(&result).unwrap_or_default(),
1730 );
1731 }
1732 tracing::info!(
1733 "Routed virtual buffer '{}' into existing utility dock {:?}",
1734 name,
1735 dock_leaf
1736 );
1737 }
1738
1739 fn update_existing_vbuf_panel(
1742 &mut self,
1743 existing_buffer_id: crate::model::event::BufferId,
1744 entries: Vec<fresh_core::text_property::TextPropertyEntry>,
1745 request_id: Option<u64>,
1746 panel_name: &str,
1747 ) {
1748 match self.set_virtual_buffer_content(existing_buffer_id, entries) {
1749 Ok(()) => tracing::info!("Updated existing panel '{}' content", panel_name),
1750 Err(e) => tracing::error!("Failed to update panel content: {}", e),
1751 }
1752 let splits = self.split_manager().splits_for_buffer(existing_buffer_id);
1753 if let Some(&split_id) = splits.first() {
1754 self.split_manager_mut().set_active_split(split_id);
1755 self.active_window_mut()
1757 .set_pane_buffer(split_id, existing_buffer_id);
1758 tracing::debug!("Focused split {:?} containing panel buffer", split_id);
1759 }
1760 if let Some(req_id) = request_id {
1761 let result = fresh_core::api::VirtualBufferResult {
1762 buffer_id: existing_buffer_id.0 as u64,
1763 split_id: splits.first().map(|s| s.0 .0 as u64),
1764 };
1765 self.plugin_manager.read().unwrap().resolve_callback(
1766 fresh_core::api::JsCallbackId::from(req_id),
1767 serde_json::to_string(&result).unwrap_or_default(),
1768 );
1769 }
1770 }
1771
1772 fn handle_get_line_position(
1780 &mut self,
1781 buffer_id: crate::model::event::BufferId,
1782 line: u32,
1783 request_id: u64,
1784 want_end: bool,
1785 ) {
1786 let actual_buffer_id = self.resolve_buffer_id(buffer_id);
1787 let result = self
1788 .windows
1789 .get_mut(&self.active_window)
1790 .map(|w| &mut w.buffers)
1791 .expect("active window present")
1792 .get_mut(&actual_buffer_id)
1793 .and_then(|state| {
1794 let len = state.buffer.len();
1795 let content = state.get_text_range(0, len);
1796 buffer_line_byte_offset(&content, len, line as usize, want_end)
1797 });
1798 self.resolve_json_callback(request_id, result);
1799 }
1800
1801 fn handle_save_buffer_to_path(&mut self, buffer_id: BufferId, path: std::path::PathBuf) {
1803 if let Some(state) = self
1804 .windows
1805 .get_mut(&self.active_window)
1806 .map(|w| &mut w.buffers)
1807 .expect("active window present")
1808 .get_mut(&buffer_id)
1809 {
1810 match state.buffer.save_to_file(&path) {
1812 Ok(()) => {
1813 if let Err(e) = self.finalize_save(Some(path)) {
1816 tracing::warn!("Failed to finalize save: {}", e);
1817 }
1818 tracing::debug!("Saved buffer {:?} to path", buffer_id);
1819 }
1820 Err(e) => {
1821 self.handle_set_status(format!("Error saving: {}", e));
1822 tracing::error!("Failed to save buffer to path: {}", e);
1823 }
1824 }
1825 } else {
1826 self.handle_set_status(format!("Buffer {:?} not found", buffer_id));
1827 tracing::warn!("SaveBufferToPath: buffer {:?} not found", buffer_id);
1828 }
1829 }
1830
1831 #[cfg(feature = "plugins")]
1833 fn handle_load_plugin(&mut self, path: std::path::PathBuf, callback_id: JsCallbackId) {
1834 let load_result = self.plugin_manager.read().unwrap().load_plugin(&path);
1835 match load_result {
1836 Ok(()) => {
1837 tracing::info!("Loaded plugin from {:?}", path);
1838 self.plugin_manager
1839 .read()
1840 .unwrap()
1841 .resolve_callback(callback_id, "true".to_string());
1842 }
1843 Err(e) => {
1844 tracing::error!("Failed to load plugin from {:?}: {}", path, e);
1845 self.plugin_manager
1846 .read()
1847 .unwrap()
1848 .reject_callback(callback_id, format!("{}", e));
1849 }
1850 }
1851 }
1852
1853 #[cfg(feature = "plugins")]
1855 fn handle_unload_plugin(&mut self, name: String, callback_id: JsCallbackId) {
1856 let result = self.plugin_manager.write().unwrap().unload_plugin(&name);
1859 match result {
1860 Ok(()) => {
1861 tracing::info!("Unloaded plugin: {}", name);
1862 if let Ok(mut schemas) = self.plugin_schemas.write() {
1863 schemas.remove(&name);
1864 }
1865 self.plugin_manager
1866 .read()
1867 .unwrap()
1868 .resolve_callback(callback_id, "true".to_string());
1869 }
1870 Err(e) => {
1871 tracing::error!("Failed to unload plugin '{}': {}", name, e);
1872 self.plugin_manager
1873 .read()
1874 .unwrap()
1875 .reject_callback(callback_id, format!("{}", e));
1876 }
1877 }
1878 }
1879
1880 #[cfg(feature = "plugins")]
1882 fn handle_reload_plugin(&mut self, name: String, callback_id: JsCallbackId) {
1883 let path = self
1887 .plugin_manager
1888 .read()
1889 .unwrap()
1890 .list_plugins()
1891 .into_iter()
1892 .find(|p| p.name == name)
1893 .map(|p| p.path);
1894 let _ = path; let reload_result = self.plugin_manager.read().unwrap().reload_plugin(&name);
1896 match reload_result {
1897 Ok(()) => {
1898 tracing::info!("Reloaded plugin: {}", name);
1899 self.plugin_manager
1900 .read()
1901 .unwrap()
1902 .resolve_callback(callback_id, "true".to_string());
1903 }
1904 Err(e) => {
1905 tracing::error!("Failed to reload plugin '{}': {}", name, e);
1906 self.plugin_manager
1907 .read()
1908 .unwrap()
1909 .reject_callback(callback_id, format!("{}", e));
1910 }
1911 }
1912 }
1913
1914 #[cfg(feature = "plugins")]
1916 fn handle_list_plugins(&mut self, callback_id: JsCallbackId) {
1917 let plugins = self.plugin_manager.read().unwrap().list_plugins();
1918 let json_array: Vec<serde_json::Value> = plugins
1920 .iter()
1921 .map(|p| {
1922 serde_json::json!({
1923 "name": p.name,
1924 "path": p.path.to_string_lossy(),
1925 "enabled": p.enabled
1926 })
1927 })
1928 .collect();
1929 let json_str = serde_json::to_string(&json_array).unwrap_or_else(|_| "[]".to_string());
1930 self.plugin_manager
1931 .read()
1932 .unwrap()
1933 .resolve_callback(callback_id, json_str);
1934 }
1935
1936 fn handle_execute_action(&mut self, action_name: String) {
1938 use crate::input::keybindings::Action;
1939 use std::collections::HashMap;
1940
1941 if let Some(action) = Action::from_str(&action_name, &HashMap::new()) {
1943 if let Err(e) = self.handle_action(action) {
1945 tracing::warn!("Failed to execute action '{}': {}", action_name, e);
1946 } else {
1947 tracing::debug!("Executed action: {}", action_name);
1948 }
1949 } else {
1950 tracing::warn!("Unknown action: {}", action_name);
1951 }
1952 }
1953
1954 fn handle_execute_actions(&mut self, actions: Vec<fresh_core::api::ActionSpec>) {
1957 use crate::input::keybindings::Action;
1958 use std::collections::HashMap;
1959
1960 for action_spec in actions {
1961 if let Some(action) = Action::from_str(&action_spec.action, &HashMap::new()) {
1962 for _ in 0..action_spec.count {
1964 if let Err(e) = self.handle_action(action.clone()) {
1965 tracing::warn!("Failed to execute action '{}': {}", action_spec.action, e);
1966 return; }
1968 }
1969 tracing::debug!(
1970 "Executed action '{}' {} time(s)",
1971 action_spec.action,
1972 action_spec.count
1973 );
1974 } else {
1975 tracing::warn!("Unknown action: {}", action_spec.action);
1976 return; }
1978 }
1979 }
1980
1981 fn handle_get_buffer_text(
1986 &mut self,
1987 buffer_id: BufferId,
1988 start: usize,
1989 end: usize,
1990 request_id: u64,
1991 ) {
1992 let result = if let Some(state) = self
1993 .windows
1994 .get_mut(&self.active_window)
1995 .map(|w| &mut w.buffers)
1996 .expect("active window present")
1997 .get_mut(&buffer_id)
1998 {
1999 let (start, end) = clamp_buffer_text_range(start, end, state.buffer.len());
2007 Ok(state.get_text_range(start, end))
2008 } else {
2009 Err(format!("Buffer {:?} not found", buffer_id))
2010 };
2011
2012 let callback_id = fresh_core::api::JsCallbackId::from(request_id);
2014 match result {
2015 Ok(text) => {
2016 let json = serde_json::to_string(&text).unwrap_or_else(|_| "null".to_string());
2018 self.plugin_manager
2019 .read()
2020 .unwrap()
2021 .resolve_callback(callback_id, json);
2022 }
2023 Err(error) => {
2024 self.plugin_manager
2025 .read()
2026 .unwrap()
2027 .reject_callback(callback_id, error);
2028 }
2029 }
2030 }
2031
2032 fn handle_set_editor_mode(&mut self, mode: Option<String>) {
2034 self.active_window_mut().editor_mode = mode.clone();
2035 tracing::debug!("Set editor mode: {:?}", mode);
2036 }
2037
2038 fn resolve_buffer_id(&self, buffer_id: BufferId) -> BufferId {
2040 if buffer_id.0 == 0 {
2041 self.active_buffer()
2042 } else {
2043 buffer_id
2044 }
2045 }
2046
2047 fn resolve_json_callback<T: serde::Serialize>(&mut self, request_id: u64, value: T) {
2049 let callback_id = fresh_core::api::JsCallbackId::from(request_id);
2050 let json = serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string());
2051 self.plugin_manager
2052 .read()
2053 .unwrap()
2054 .resolve_callback(callback_id, json);
2055 }
2056
2057 fn handle_get_line_start_position(&mut self, buffer_id: BufferId, line: u32, request_id: u64) {
2059 self.handle_get_line_position(buffer_id, line, request_id, false);
2060 }
2061
2062 fn handle_get_line_end_position(&mut self, buffer_id: BufferId, line: u32, request_id: u64) {
2065 self.handle_get_line_position(buffer_id, line, request_id, true);
2066 }
2067
2068 fn handle_get_buffer_line_count(&mut self, buffer_id: BufferId, request_id: u64) {
2070 let actual_buffer_id = self.resolve_buffer_id(buffer_id);
2071
2072 let result = if let Some(state) = self
2073 .windows
2074 .get_mut(&self.active_window)
2075 .map(|w| &mut w.buffers)
2076 .expect("active window present")
2077 .get_mut(&actual_buffer_id)
2078 {
2079 let buffer_len = state.buffer.len();
2080 let content = state.get_text_range(0, buffer_len);
2081 let newlines = content.bytes().filter(|&b| b == b'\n').count();
2082 Some(if content.is_empty() {
2083 1
2084 } else {
2085 newlines + usize::from(!content.ends_with('\n'))
2086 })
2087 } else {
2088 None
2089 };
2090
2091 self.resolve_json_callback(request_id, result);
2092 }
2093
2094 fn handle_get_composite_cursor_info(&mut self, request_id: u64) {
2100 let info = self.active_window().active_composite_cursor_info();
2101 let value = info.map(|(focused_pane, pane_count, lines)| {
2102 serde_json::json!({
2103 "focusedPane": focused_pane,
2104 "paneCount": pane_count,
2105 "lines": lines,
2106 })
2107 });
2108 self.resolve_json_callback(request_id, value);
2109 }
2110
2111 fn handle_open_file_streaming(&mut self, path: std::path::PathBuf, request_id: u64) {
2128 if !self.authority().filesystem.exists(&path) {
2131 if let Some(parent) = path.parent() {
2132 if !parent.as_os_str().is_empty() {
2133 if let Err(e) = std::fs::create_dir_all(parent) {
2134 tracing::warn!(
2135 "openFileStreaming: failed to create parent dir {:?}: {}",
2136 parent,
2137 e
2138 );
2139 self.resolve_json_callback::<Option<u64>>(request_id, None);
2140 return;
2141 }
2142 }
2143 }
2144 if let Err(e) = std::fs::write(&path, b"") {
2145 tracing::warn!(
2146 "openFileStreaming: failed to create empty file at {:?}: {}",
2147 path,
2148 e
2149 );
2150 self.resolve_json_callback::<Option<u64>>(request_id, None);
2151 return;
2152 }
2153 }
2154
2155 let buffer_id = match self.open_file_no_focus(&path) {
2159 Ok(id) => id,
2160 Err(e) => {
2161 tracing::warn!(
2162 "openFileStreaming: open_file_no_focus failed for {:?}: {}",
2163 path,
2164 e
2165 );
2166 self.resolve_json_callback::<Option<u64>>(request_id, None);
2167 return;
2168 }
2169 };
2170
2171 if let Some(meta) = self.active_window_mut().buffer_metadata.get_mut(&buffer_id) {
2176 meta.hidden_from_tabs = true;
2177 meta.auto_revert_enabled = false;
2178 }
2179 let active_split = self
2180 .windows
2181 .get(&self.active_window)
2182 .and_then(|w| w.buffers.splits())
2183 .map(|(mgr, _)| mgr)
2184 .expect("active window must have a populated split layout")
2185 .active_split();
2186 if let Some(vs) = self
2187 .windows
2188 .get_mut(&self.active_window)
2189 .and_then(|w| w.split_view_states_mut())
2190 .expect("active window must have a populated split layout")
2191 .get_mut(&active_split)
2192 {
2193 use crate::view::split::TabTarget;
2194 vs.open_buffers
2195 .retain(|t| !matches!(t, TabTarget::Buffer(b) if *b == buffer_id));
2196 }
2197
2198 self.resolve_json_callback(request_id, Some(buffer_id.0));
2199 }
2200
2201 fn handle_set_buffer_group_panel_buffer(
2204 &mut self,
2205 group_id: usize,
2206 panel_name: String,
2207 buffer_id: BufferId,
2208 request_id: u64,
2209 ) {
2210 let actual_buffer_id = self.resolve_buffer_id(buffer_id);
2211 let ok = self.set_buffer_group_panel_buffer(group_id, panel_name, actual_buffer_id);
2212 self.resolve_json_callback(request_id, ok);
2213 }
2214
2215 fn handle_refresh_buffer_from_disk(&mut self, buffer_id: BufferId, request_id: u64) {
2219 let actual_buffer_id = self.resolve_buffer_id(buffer_id);
2220
2221 let path = self
2222 .windows
2223 .get(&self.active_window)
2224 .and_then(|w| w.buffers.splits())
2225 .map(|(_, _)| ())
2226 .and_then(|_| {
2227 self.windows
2228 .get(&self.active_window)?
2229 .buffers
2230 .get(&actual_buffer_id)?
2231 .buffer
2232 .file_path()
2233 .map(|p| p.to_path_buf())
2234 });
2235
2236 let Some(path) = path else {
2237 self.resolve_json_callback::<Option<usize>>(request_id, None);
2239 return;
2240 };
2241
2242 let new_size = match self.authority().filesystem.metadata(&path) {
2243 Ok(m) => m.size as usize,
2244 Err(_) => {
2245 self.resolve_json_callback::<Option<usize>>(request_id, None);
2246 return;
2247 }
2248 };
2249
2250 let new_total = if let Some(state) = self
2251 .windows
2252 .get_mut(&self.active_window)
2253 .map(|w| &mut w.buffers)
2254 .expect("active window present")
2255 .get_mut(&actual_buffer_id)
2256 {
2257 let old = state.buffer.total_bytes();
2258 if new_size > old {
2259 state.buffer.extend_streaming(&path, new_size);
2260 }
2261 state.buffer.total_bytes()
2262 } else {
2263 self.resolve_json_callback::<Option<usize>>(request_id, None);
2264 return;
2265 };
2266
2267 self.resolve_json_callback(request_id, Some(new_total));
2268 }
2269
2270 fn handle_scroll_to_line_center(
2272 &mut self,
2273 split_id: SplitId,
2274 buffer_id: BufferId,
2275 line: usize,
2276 ) {
2277 let actual_split_id = if split_id.0 == 0 {
2278 self.windows
2279 .get(&self.active_window)
2280 .and_then(|w| w.buffers.splits())
2281 .map(|(mgr, _)| mgr)
2282 .expect("active window must have a populated split layout")
2283 .active_split()
2284 } else {
2285 LeafId(split_id)
2286 };
2287 let actual_buffer_id = self.resolve_buffer_id(buffer_id);
2288
2289 let viewport_height = if let Some(view_state) = self
2291 .windows
2292 .get(&self.active_window)
2293 .and_then(|w| w.buffers.splits())
2294 .map(|(_, vs)| vs)
2295 .expect("active window must have a populated split layout")
2296 .get(&actual_split_id)
2297 {
2298 view_state.viewport.height as usize
2299 } else {
2300 return;
2301 };
2302
2303 let lines_above = viewport_height / 2;
2305 let target_line = line.saturating_sub(lines_above);
2306
2307 self.active_window_mut().scroll_split_viewport_to(
2308 actual_buffer_id,
2309 actual_split_id,
2310 target_line,
2311 true,
2312 );
2313 }
2314
2315 fn handle_scroll_buffer_to_line(&mut self, buffer_id: BufferId, line: usize) {
2325 if !self
2326 .windows
2327 .get(&self.active_window)
2328 .map(|w| &w.buffers)
2329 .expect("active window present")
2330 .contains_key(&buffer_id)
2331 {
2332 return;
2333 }
2334
2335 let mut target_leaves: Vec<LeafId> = Vec::new();
2337
2338 for leaf_id in self
2340 .windows
2341 .get(&self.active_window)
2342 .and_then(|w| w.buffers.splits())
2343 .map(|(mgr, _)| mgr)
2344 .expect("active window must have a populated split layout")
2345 .root()
2346 .leaf_split_ids()
2347 {
2348 if let Some(vs) = self
2349 .windows
2350 .get(&self.active_window)
2351 .and_then(|w| w.buffers.splits())
2352 .map(|(_, vs)| vs)
2353 .expect("active window must have a populated split layout")
2354 .get(&leaf_id)
2355 {
2356 if vs.active_buffer == buffer_id {
2357 target_leaves.push(leaf_id);
2358 }
2359 }
2360 }
2361
2362 for (_group_leaf_id, node) in self.active_window().grouped_subtrees.iter() {
2364 if let crate::view::split::SplitNode::Grouped { layout, .. } = node {
2365 for inner_leaf in layout.leaf_split_ids() {
2366 if let Some(vs) = self
2367 .windows
2368 .get(&self.active_window)
2369 .and_then(|w| w.buffers.splits())
2370 .map(|(_, vs)| vs)
2371 .expect("active window must have a populated split layout")
2372 .get(&inner_leaf)
2373 {
2374 if vs.active_buffer == buffer_id && !target_leaves.contains(&inner_leaf) {
2375 target_leaves.push(inner_leaf);
2376 }
2377 }
2378 }
2379 }
2380 }
2381
2382 if target_leaves.is_empty() {
2383 return;
2384 }
2385
2386 self.active_window_mut()
2387 .scroll_buffer_to_line_in_splits(buffer_id, &target_leaves, line);
2388 }
2389
2390 fn handle_spawn_host_process(
2391 &mut self,
2392 command: String,
2393 args: Vec<String>,
2394 cwd: Option<String>,
2395 callback_id: JsCallbackId,
2396 ) {
2397 if let (Some(runtime), Some(bridge)) = (&self.tokio_runtime, &self.async_bridge) {
2412 use tokio::io::{AsyncReadExt, BufReader};
2413 use tokio::process::Command as TokioCommand;
2414
2415 let effective_cwd = cwd.or_else(|| {
2416 std::env::current_dir()
2417 .map(|p| p.to_string_lossy().to_string())
2418 .ok()
2419 });
2420 let sender = bridge.sender();
2421 let process_id = callback_id.as_u64();
2422
2423 if let crate::services::workspace_trust::SpawnDecision::Deny(reason) = self
2430 .authority()
2431 .workspace_trust
2432 .decide(&command, effective_cwd.as_deref())
2433 {
2434 #[allow(clippy::let_underscore_must_use)]
2435 let _ = sender.send(AsyncMessage::PluginProcessOutput {
2436 process_id,
2437 stdout: String::new(),
2438 stderr: reason,
2439 exit_code: -1,
2440 });
2441 return;
2442 }
2443
2444 let (kill_tx, mut kill_rx) = tokio::sync::oneshot::channel::<()>();
2445 self.host_process_handles.insert(process_id, kill_tx);
2446
2447 runtime.spawn(async move {
2448 use crate::services::process_hidden::HideWindow;
2449 let mut cmd = TokioCommand::new(&command);
2450 cmd.args(&args);
2451 cmd.stdout(std::process::Stdio::piped());
2452 cmd.stderr(std::process::Stdio::piped());
2453 cmd.hide_window();
2454 if let Some(ref dir) = effective_cwd {
2455 cmd.current_dir(dir);
2456 }
2457 let mut child = match cmd.spawn() {
2458 Ok(c) => c,
2459 Err(e) => {
2460 #[allow(clippy::let_underscore_must_use)]
2461 let _ = sender.send(AsyncMessage::PluginProcessOutput {
2462 process_id,
2463 stdout: String::new(),
2464 stderr: e.to_string(),
2465 exit_code: -1,
2466 });
2467 return;
2468 }
2469 };
2470
2471 let stdout_pipe = child.stdout.take();
2477 let stderr_pipe = child.stderr.take();
2478
2479 let stdout_fut = async {
2480 let mut buf = String::new();
2481 if let Some(s) = stdout_pipe {
2482 #[allow(clippy::let_underscore_must_use)]
2483 let _ = BufReader::new(s).read_to_string(&mut buf).await;
2484 }
2485 buf
2486 };
2487 let stderr_fut = async {
2488 let mut buf = String::new();
2489 if let Some(s) = stderr_pipe {
2490 #[allow(clippy::let_underscore_must_use)]
2491 let _ = BufReader::new(s).read_to_string(&mut buf).await;
2492 }
2493 buf
2494 };
2495 let wait_fut = async {
2496 tokio::select! {
2497 status = child.wait() => {
2498 status.map(|s| s.code().unwrap_or(-1)).unwrap_or(-1)
2499 }
2500 _ = &mut kill_rx => {
2501 #[allow(clippy::let_underscore_must_use)]
2505 let _ = child.start_kill();
2506 child
2507 .wait()
2508 .await
2509 .map(|s| s.code().unwrap_or(-1))
2510 .unwrap_or(-1)
2511 }
2512 }
2513 };
2514 let (stdout, stderr, exit_code) = tokio::join!(stdout_fut, stderr_fut, wait_fut);
2515
2516 #[allow(clippy::let_underscore_must_use)]
2517 let _ = sender.send(AsyncMessage::PluginProcessOutput {
2518 process_id,
2519 stdout,
2520 stderr,
2521 exit_code,
2522 });
2523 });
2524 } else {
2525 self.plugin_manager
2526 .read()
2527 .unwrap()
2528 .reject_callback(callback_id, "Async runtime not available".to_string());
2529 }
2530 }
2531
2532 fn handle_spawn_background_process(
2533 &mut self,
2534 process_id: u64,
2535 command: String,
2536 args: Vec<String>,
2537 cwd: Option<String>,
2538 callback_id: JsCallbackId,
2539 ) {
2540 if let (Some(runtime), Some(bridge)) = (&self.tokio_runtime, &self.async_bridge) {
2542 use tokio::io::{AsyncBufReadExt, BufReader};
2543 use tokio::process::Command as TokioCommand;
2544
2545 let effective_cwd = cwd.unwrap_or_else(|| {
2546 std::env::current_dir()
2547 .map(|p| p.to_string_lossy().to_string())
2548 .unwrap_or_else(|_| ".".to_string())
2549 });
2550
2551 let sender = bridge.sender();
2552 let sender_stdout = sender.clone();
2553 let sender_stderr = sender.clone();
2554 let callback_id_u64 = callback_id.as_u64();
2555
2556 #[allow(clippy::let_underscore_must_use)]
2558 let handle = runtime.spawn(async move {
2559 use crate::services::process_hidden::HideWindow;
2560 let mut child = match TokioCommand::new(&command)
2561 .args(&args)
2562 .current_dir(&effective_cwd)
2563 .stdout(std::process::Stdio::piped())
2564 .stderr(std::process::Stdio::piped())
2565 .hide_window()
2566 .spawn()
2567 {
2568 Ok(child) => child,
2569 Err(e) => {
2570 let _ = sender.send(crate::services::async_bridge::AsyncMessage::Plugin(
2571 fresh_core::api::PluginAsyncMessage::ProcessExit {
2572 process_id,
2573 callback_id: callback_id_u64,
2574 exit_code: -1,
2575 },
2576 ));
2577 tracing::error!("Failed to spawn background process: {}", e);
2578 return;
2579 }
2580 };
2581
2582 let stdout = child.stdout.take();
2584 let stderr = child.stderr.take();
2585 let pid = process_id;
2586
2587 if let Some(stdout) = stdout {
2589 let sender = sender_stdout;
2590 tokio::spawn(async move {
2591 let reader = BufReader::new(stdout);
2592 let mut lines = reader.lines();
2593 while let Ok(Some(line)) = lines.next_line().await {
2594 let _ =
2595 sender.send(crate::services::async_bridge::AsyncMessage::Plugin(
2596 fresh_core::api::PluginAsyncMessage::ProcessStdout {
2597 process_id: pid,
2598 data: line + "\n",
2599 },
2600 ));
2601 }
2602 });
2603 }
2604
2605 if let Some(stderr) = stderr {
2607 let sender = sender_stderr;
2608 tokio::spawn(async move {
2609 let reader = BufReader::new(stderr);
2610 let mut lines = reader.lines();
2611 while let Ok(Some(line)) = lines.next_line().await {
2612 let _ =
2613 sender.send(crate::services::async_bridge::AsyncMessage::Plugin(
2614 fresh_core::api::PluginAsyncMessage::ProcessStderr {
2615 process_id: pid,
2616 data: line + "\n",
2617 },
2618 ));
2619 }
2620 });
2621 }
2622
2623 let exit_code = match child.wait().await {
2625 Ok(status) => status.code().unwrap_or(-1),
2626 Err(_) => -1,
2627 };
2628
2629 let _ = sender.send(crate::services::async_bridge::AsyncMessage::Plugin(
2630 fresh_core::api::PluginAsyncMessage::ProcessExit {
2631 process_id,
2632 callback_id: callback_id_u64,
2633 exit_code,
2634 },
2635 ));
2636 });
2637
2638 self.background_process_handles
2640 .insert(process_id, handle.abort_handle());
2641 } else {
2642 self.plugin_manager
2644 .read()
2645 .unwrap()
2646 .reject_callback(callback_id, "Async runtime not available".to_string());
2647 }
2648 }
2649
2650 #[allow(clippy::too_many_arguments)]
2651 fn handle_create_virtual_buffer_with_content(
2652 &mut self,
2653 name: String,
2654 mode: String,
2655 read_only: bool,
2656 entries: Vec<fresh_core::text_property::TextPropertyEntry>,
2657 show_line_numbers: bool,
2658 show_cursors: bool,
2659 editing_disabled: bool,
2660 hidden_from_tabs: bool,
2661 request_id: Option<u64>,
2662 ) {
2663 let buffer_id = if hidden_from_tabs {
2668 self.active_window_mut().create_virtual_buffer_detached(
2669 name.clone(),
2670 mode.clone(),
2671 read_only,
2672 )
2673 } else {
2674 self.active_window_mut()
2675 .create_virtual_buffer(name.clone(), mode.clone(), read_only)
2676 };
2677 tracing::info!(
2678 "Created virtual buffer '{}' with mode '{}' (id={:?}, detached={})",
2679 name,
2680 mode,
2681 buffer_id,
2682 hidden_from_tabs
2683 );
2684
2685 self.configure_vbuf_display(buffer_id, show_line_numbers, show_cursors, editing_disabled);
2691 if !hidden_from_tabs {
2692 let active_split = self.split_manager().active_split();
2693 if let Some(view_state) = self
2694 .windows
2695 .get_mut(&self.active_window)
2696 .and_then(|w| w.split_view_states_mut())
2697 .expect("active window must have a populated split layout")
2698 .get_mut(&active_split)
2699 {
2700 view_state.ensure_buffer_state(buffer_id).show_line_numbers = show_line_numbers;
2701 }
2702 } else if let Some(meta) = self.active_window_mut().buffer_metadata.get_mut(&buffer_id) {
2703 meta.hidden_from_tabs = true;
2704 }
2705
2706 match self.set_virtual_buffer_content(buffer_id, entries) {
2708 Ok(()) => {
2709 tracing::debug!("Set virtual buffer content for {:?}", buffer_id);
2710 if !hidden_from_tabs {
2713 self.set_active_buffer(buffer_id);
2714 tracing::debug!("Switched to virtual buffer {:?}", buffer_id);
2715 }
2716
2717 if let Some(req_id) = request_id {
2719 tracing::info!(
2720 "CreateVirtualBufferWithContent: resolving callback for request_id={}, buffer_id={:?}",
2721 req_id,
2722 buffer_id
2723 );
2724 let result = fresh_core::api::VirtualBufferResult {
2726 buffer_id: buffer_id.0 as u64,
2727 split_id: None,
2728 };
2729 self.plugin_manager.read().unwrap().resolve_callback(
2730 fresh_core::api::JsCallbackId::from(req_id),
2731 serde_json::to_string(&result).unwrap_or_default(),
2732 );
2733 tracing::info!(
2734 "CreateVirtualBufferWithContent: resolve_callback sent for request_id={}",
2735 req_id
2736 );
2737 }
2738 }
2739 Err(e) => {
2740 tracing::error!("Failed to set virtual buffer content: {}", e);
2741 }
2742 }
2743 }
2744
2745 #[allow(clippy::too_many_arguments)]
2746 fn handle_create_virtual_buffer_in_split(
2747 &mut self,
2748 name: String,
2749 mode: String,
2750 read_only: bool,
2751 entries: Vec<fresh_core::text_property::TextPropertyEntry>,
2752 ratio: f32,
2753 direction: Option<String>,
2754 panel_id: Option<String>,
2755 show_line_numbers: bool,
2756 show_cursors: bool,
2757 editing_disabled: bool,
2758 line_wrap: Option<bool>,
2759 before: bool,
2760 role: Option<String>,
2761 request_id: Option<u64>,
2762 ) {
2763 let split_role: Option<crate::view::split::SplitRole> = match role.as_deref() {
2766 Some("utility_dock") => Some(crate::view::split::SplitRole::UtilityDock),
2767 _ => None,
2768 };
2769
2770 if let Some(dock_leaf) = split_role.and_then(|r| self.split_manager().find_leaf_by_role(r))
2774 {
2775 return self.route_vbuf_to_existing_dock(
2776 dock_leaf,
2777 name,
2778 mode,
2779 read_only,
2780 entries,
2781 panel_id.as_deref(),
2782 show_line_numbers,
2783 show_cursors,
2784 editing_disabled,
2785 request_id,
2786 );
2787 }
2790
2791 if let Some(pid) = panel_id.as_deref() {
2794 let maybe_existing = self.panel_ids().get(pid).copied();
2795 if let Some(existing_id) = maybe_existing {
2796 let buffer_alive = self
2797 .windows
2798 .get(&self.active_window)
2799 .map(|w| w.buffers.contains_key(&existing_id))
2800 .unwrap_or(false);
2801 if buffer_alive {
2802 return self.update_existing_vbuf_panel(existing_id, entries, request_id, pid);
2803 }
2804 tracing::warn!(
2806 "Removing stale panel_id '{}' pointing to non-existent buffer {:?}",
2807 pid,
2808 existing_id
2809 );
2810 self.panel_ids_mut().remove(pid);
2811 }
2812 }
2813
2814 let source_split_before_create = self.split_manager().active_split();
2821
2822 let buffer_id =
2823 self.active_window_mut()
2824 .create_virtual_buffer(name.clone(), mode.clone(), read_only);
2825 tracing::info!(
2826 "Created virtual buffer '{}' with mode '{}' in split (id={:?})",
2827 name,
2828 mode,
2829 buffer_id
2830 );
2831
2832 self.configure_vbuf_display(buffer_id, show_line_numbers, show_cursors, editing_disabled);
2833
2834 if let Some(pid) = panel_id {
2835 self.panel_ids_mut().insert(pid, buffer_id);
2836 }
2837
2838 if let Err(e) = self.set_virtual_buffer_content(buffer_id, entries) {
2839 tracing::error!("Failed to set virtual buffer content: {}", e);
2840 return;
2841 }
2842
2843 let split_dir = match direction.as_deref() {
2844 Some("vertical") => crate::model::event::SplitDirection::Vertical,
2845 _ => crate::model::event::SplitDirection::Horizontal,
2846 };
2847
2848 let split_result = if split_role == Some(crate::view::split::SplitRole::UtilityDock) {
2853 self.split_manager_mut()
2854 .split_root_positioned(split_dir, buffer_id, ratio, before)
2855 } else {
2856 self.split_manager_mut()
2857 .split_active_positioned(split_dir, buffer_id, ratio, before)
2858 };
2859
2860 let created_split_id = match split_result {
2861 Ok(new_split_id) => {
2862 if new_split_id != source_split_before_create {
2866 if let Some(src_vs) = self
2867 .windows
2868 .get_mut(&self.active_window)
2869 .and_then(|w| w.split_view_states_mut())
2870 .expect("active window must have a populated split layout")
2871 .get_mut(&source_split_before_create)
2872 {
2873 src_vs.remove_buffer(buffer_id);
2874 }
2875 }
2876
2877 let mut view_state = SplitViewState::with_buffer(
2878 self.terminal_width,
2879 self.terminal_height,
2880 buffer_id,
2881 );
2882 view_state.apply_config_defaults(
2883 self.config.editor.line_numbers,
2884 self.config.editor.highlight_current_line,
2885 line_wrap.unwrap_or_else(|| {
2886 self.active_window().resolve_line_wrap_for_buffer(buffer_id)
2887 }),
2888 self.config.editor.wrap_indent,
2889 self.active_window()
2890 .resolve_wrap_column_for_buffer(buffer_id),
2891 self.config.editor.rulers.clone(),
2892 self.config.editor.scroll_offset,
2893 );
2894 view_state.ensure_buffer_state(buffer_id).show_line_numbers = show_line_numbers;
2895 self.windows
2896 .get_mut(&self.active_window)
2897 .and_then(|w| w.split_view_states_mut())
2898 .expect("active window must have a populated split layout")
2899 .insert(new_split_id, view_state);
2900
2901 self.split_manager_mut().set_active_split(new_split_id);
2902
2903 if let Some(target_role) = split_role {
2907 self.split_manager_mut().clear_role(target_role);
2908 self.split_manager_mut()
2909 .set_leaf_role(new_split_id, Some(target_role));
2910 tracing::info!(
2911 "Tagged new dock leaf {:?} with role {:?}",
2912 new_split_id,
2913 target_role
2914 );
2915 }
2916
2917 tracing::info!(
2918 "Created {:?} split with virtual buffer {:?}",
2919 split_dir,
2920 buffer_id
2921 );
2922 Some(new_split_id)
2923 }
2924 Err(e) => {
2925 tracing::error!("Failed to create split: {}", e);
2926 self.set_active_buffer(buffer_id);
2927 None
2928 }
2929 };
2930
2931 if let Some(req_id) = request_id {
2932 tracing::trace!(
2933 "CreateVirtualBufferInSplit: resolving callback for request_id={}, \
2934 buffer_id={:?}, split_id={:?}",
2935 req_id,
2936 buffer_id,
2937 created_split_id
2938 );
2939 let result = fresh_core::api::VirtualBufferResult {
2940 buffer_id: buffer_id.0 as u64,
2941 split_id: created_split_id.map(|s| s.0 .0 as u64),
2942 };
2943 self.plugin_manager.read().unwrap().resolve_callback(
2944 fresh_core::api::JsCallbackId::from(req_id),
2945 serde_json::to_string(&result).unwrap_or_default(),
2946 );
2947 }
2948 }
2949
2950 #[allow(clippy::too_many_arguments)]
2951 fn handle_create_virtual_buffer_in_existing_split(
2952 &mut self,
2953 name: String,
2954 mode: String,
2955 read_only: bool,
2956 entries: Vec<fresh_core::text_property::TextPropertyEntry>,
2957 split_id: SplitId,
2958 show_line_numbers: bool,
2959 show_cursors: bool,
2960 editing_disabled: bool,
2961 line_wrap: Option<bool>,
2962 request_id: Option<u64>,
2963 ) {
2964 let buffer_id =
2966 self.active_window_mut()
2967 .create_virtual_buffer(name.clone(), mode.clone(), read_only);
2968 tracing::info!(
2969 "Created virtual buffer '{}' with mode '{}' for existing split {:?} (id={:?})",
2970 name,
2971 mode,
2972 split_id,
2973 buffer_id
2974 );
2975
2976 self.configure_vbuf_display(buffer_id, show_line_numbers, show_cursors, editing_disabled);
2977
2978 if let Err(e) = self.set_virtual_buffer_content(buffer_id, entries) {
2979 tracing::error!("Failed to set virtual buffer content: {}", e);
2980 return;
2981 }
2982
2983 let leaf_id = LeafId(split_id);
2986 self.windows
2987 .get_mut(&self.active_window)
2988 .and_then(|w| w.split_manager_mut())
2989 .expect("active window must have a populated split layout")
2990 .set_active_split(leaf_id);
2991 self.active_window_mut().set_pane_buffer(leaf_id, buffer_id);
2992
2993 if let Some(view_state) = self
2999 .windows
3000 .get_mut(&self.active_window)
3001 .and_then(|w| w.split_view_states_mut())
3002 .expect("active window must have a populated split layout")
3003 .get_mut(&leaf_id)
3004 {
3005 view_state.switch_buffer(buffer_id);
3006 view_state.add_buffer(buffer_id);
3007 view_state.ensure_buffer_state(buffer_id).show_line_numbers = show_line_numbers;
3008
3009 if let Some(wrap) = line_wrap {
3011 view_state.active_state_mut().viewport.line_wrap_enabled = wrap;
3012 }
3013 }
3014
3015 tracing::info!(
3016 "Displayed virtual buffer {:?} in split {:?}",
3017 buffer_id,
3018 split_id
3019 );
3020
3021 if let Some(req_id) = request_id {
3023 let result = fresh_core::api::VirtualBufferResult {
3024 buffer_id: buffer_id.0 as u64,
3025 split_id: Some(split_id.0 as u64),
3026 };
3027 self.plugin_manager.read().unwrap().resolve_callback(
3028 fresh_core::api::JsCallbackId::from(req_id),
3029 serde_json::to_string(&result).unwrap_or_default(),
3030 );
3031 }
3032 }
3033
3034 fn handle_show_action_popup(
3035 &mut self,
3036 popup_id: String,
3037 title: String,
3038 message: String,
3039 actions: Vec<fresh_core::api::ActionPopupAction>,
3040 ) {
3041 tracing::info!(
3042 "Action popup requested: id={}, title={}, actions={}",
3043 popup_id,
3044 title,
3045 actions.len()
3046 );
3047
3048 let items: Vec<crate::model::event::PopupListItemData> = actions
3050 .iter()
3051 .map(|action| crate::model::event::PopupListItemData {
3052 text: action.label.clone(),
3053 detail: None,
3054 icon: None,
3055 data: Some(action.id.clone()),
3056 })
3057 .collect();
3058
3059 drop(actions);
3064
3065 let popup_data = crate::model::event::PopupData {
3067 kind: crate::model::event::PopupKindHint::List,
3068 title: Some(title),
3069 description: Some(message),
3070 transient: false,
3071 content: crate::model::event::PopupContentData::List { items, selected: 0 },
3072 position: crate::model::event::PopupPositionData::BottomRight,
3073 width: 60,
3074 max_height: 15,
3075 bordered: true,
3076 };
3077
3078 let mut popup_obj = crate::state::convert_popup_data_to_popup(&popup_data);
3088 popup_obj.resolver = crate::view::popup::PopupResolver::PluginAction {
3089 popup_id: popup_id.clone(),
3090 };
3091
3092 {
3099 let theme = self.theme();
3100 popup_obj.background_style = ratatui::style::Style::default().bg(theme.popup_bg);
3101 popup_obj.border_style = ratatui::style::Style::default().fg(theme.popup_border_fg);
3102 }
3103
3104 while self
3116 .active_state()
3117 .popups
3118 .top()
3119 .is_some_and(|p| matches!(p.resolver, crate::view::popup::PopupResolver::LspStatus))
3120 {
3121 self.active_state_mut().popups.hide();
3122 }
3123
3124 let existing_idx = self.global_popups.all().iter().position(|p| {
3131 matches!(
3132 &p.resolver,
3133 crate::view::popup::PopupResolver::PluginAction { popup_id: id } if id == &popup_id,
3134 )
3135 });
3136 if let Some(idx) = existing_idx {
3137 if let Some(slot) = self.global_popups.get_mut(idx) {
3138 *slot = popup_obj;
3139 }
3140 } else {
3141 self.global_popups.show(popup_obj);
3142 }
3143 tracing::info!(
3144 "Action popup shown: id={}, stack_depth={}",
3145 popup_id,
3146 self.global_popups.all().len()
3147 );
3148 }
3149
3150 fn handle_set_lsp_menu_contributions(
3160 &mut self,
3161 plugin_id: String,
3162 language: String,
3163 items: Vec<fresh_core::api::LspMenuItem>,
3164 ) {
3165 let key = (language.clone(), plugin_id.clone());
3166 if items.is_empty() {
3167 self.active_window_mut().lsp_menu_contributions.remove(&key);
3168 } else {
3169 self.active_window_mut()
3170 .lsp_menu_contributions
3171 .insert(key, items);
3172 }
3173 self.refresh_lsp_status_popup_if_open();
3178 }
3179
3180 #[allow(clippy::too_many_arguments)]
3181 fn handle_create_window_with_terminal(
3182 &mut self,
3183 root: std::path::PathBuf,
3184 label: String,
3185 cwd: Option<String>,
3186 command: Option<Vec<String>>,
3187 title: Option<String>,
3188 resume: Option<Vec<String>>,
3189 request_id: u64,
3190 ) {
3191 let callback_id = JsCallbackId::from(request_id);
3192 if !root.is_absolute() {
3193 let msg = format!(
3194 "createWindowWithTerminal: root must be absolute, got {:?}",
3195 root
3196 );
3197 tracing::warn!("{}", msg);
3198 self.plugin_manager
3199 .read()
3200 .unwrap()
3201 .reject_callback(callback_id, msg);
3202 return;
3203 }
3204 let cwd_buf = cwd.map(std::path::PathBuf::from);
3205 let new_authority = self.local_session_authority(&root);
3213 match self.create_window_with_terminal(
3214 root,
3215 label,
3216 cwd_buf,
3217 command,
3218 title,
3219 new_authority,
3220 resume,
3221 ) {
3222 Ok((window_id, terminal_id, buffer_id)) => {
3223 let api_result = fresh_core::api::SessionWithTerminalResult {
3224 window_id: window_id.0,
3225 terminal_id: terminal_id.0 as u64,
3226 buffer_id: buffer_id.0 as u64,
3227 };
3228 self.plugin_manager.read().unwrap().resolve_callback(
3229 callback_id,
3230 serde_json::to_string(&api_result).unwrap_or_default(),
3231 );
3232 }
3233 Err(e) => {
3234 tracing::error!("createWindowWithTerminal failed: {e}");
3235 self.plugin_manager
3236 .read()
3237 .unwrap()
3238 .reject_callback(callback_id, format!("createWindowWithTerminal: {e}"));
3239 }
3240 }
3241 }
3242
3243 #[allow(clippy::too_many_arguments)]
3244 fn handle_create_terminal(
3245 &mut self,
3246 cwd: Option<String>,
3247 direction: Option<String>,
3248 ratio: Option<f32>,
3249 focus: Option<bool>,
3250 persistent: bool,
3251 target_session_id: Option<fresh_core::WindowId>,
3252 command: Option<Vec<String>>,
3253 title: Option<String>,
3254 request_id: u64,
3255 ) {
3256 let target_id = target_session_id
3263 .filter(|id| self.windows.contains_key(id))
3264 .unwrap_or(self.active_window);
3265 let is_active_target = target_id == self.active_window;
3266
3267 let cwd_buf = cwd.map(std::path::PathBuf::from);
3268 let split_direction = direction.as_deref().map(|d| match d {
3269 "horizontal" => crate::model::event::SplitDirection::Horizontal,
3270 _ => crate::model::event::SplitDirection::Vertical,
3271 });
3272
3273 let prev_active = if is_active_target {
3281 Some(self.active_window().active_buffer())
3282 } else {
3283 None
3284 };
3285
3286 let result = {
3287 let target = self
3288 .windows
3289 .get_mut(&target_id)
3290 .expect("target window present (existence checked above)");
3291 target.create_plugin_terminal(
3292 cwd_buf,
3293 split_direction,
3294 ratio,
3295 focus.unwrap_or(true),
3296 persistent,
3297 command,
3298 title.filter(|t| !t.is_empty()),
3299 )
3300 };
3301 match result {
3302 Ok((terminal_id, buffer_id, created_split_id)) => {
3303 if is_active_target {
3304 let new_active = self.active_window().active_buffer();
3305 if prev_active != Some(new_active) {
3306 #[cfg(feature = "plugins")]
3307 self.update_plugin_state_snapshot();
3308 #[cfg(feature = "plugins")]
3309 self.plugin_manager.read().unwrap().run_hook(
3310 "buffer_activated",
3311 crate::services::plugins::hooks::HookArgs::BufferActivated {
3312 buffer_id: new_active,
3313 },
3314 );
3315 }
3316 }
3317 let api_result = fresh_core::api::TerminalResult {
3318 buffer_id: buffer_id.0 as u64,
3319 terminal_id: terminal_id.0 as u64,
3320 split_id: created_split_id.map(|s| s.0 .0 as u64),
3321 };
3322 self.plugin_manager.read().unwrap().resolve_callback(
3323 fresh_core::api::JsCallbackId::from(request_id),
3324 serde_json::to_string(&api_result).unwrap_or_default(),
3325 );
3326 tracing::info!(
3327 "Plugin created terminal {:?} with buffer {:?} in window {:?}",
3328 terminal_id,
3329 buffer_id,
3330 target_id
3331 );
3332 }
3333 Err(e) => {
3334 tracing::error!("Failed to create terminal for plugin: {e}");
3335 self.plugin_manager.read().unwrap().reject_callback(
3336 fresh_core::api::JsCallbackId::from(request_id),
3337 format!("Failed to create terminal: {e}"),
3338 );
3339 }
3340 }
3341 }
3342
3343 fn handle_get_split_by_label(&mut self, label: String, request_id: u64) {
3346 let split_id = self
3347 .windows
3348 .get(&self.active_window)
3349 .and_then(|w| w.buffers.splits())
3350 .map(|(mgr, _)| mgr)
3351 .expect("active window must have a populated split layout")
3352 .find_split_by_label(&label);
3353 let callback_id = fresh_core::api::JsCallbackId::from(request_id);
3354 let json =
3355 serde_json::to_string(&split_id.map(|s| s.0 .0)).unwrap_or_else(|_| "null".to_string());
3356 self.plugin_manager
3357 .read()
3358 .unwrap()
3359 .resolve_callback(callback_id, json);
3360 }
3361
3362 fn handle_set_buffer_show_cursors(&mut self, buffer_id: BufferId, show: bool) {
3363 if let Some(state) = self
3364 .windows
3365 .get_mut(&self.active_window)
3366 .map(|w| &mut w.buffers)
3367 .expect("active window present")
3368 .get_mut(&buffer_id)
3369 {
3370 state.show_cursors = show;
3371 state.cursor_visibility_locked = true;
3374 } else {
3375 tracing::warn!("SetBufferShowCursors: buffer {:?} not found", buffer_id);
3376 }
3377 }
3378
3379 fn handle_override_theme_colors(
3380 &mut self,
3381 overrides: std::collections::HashMap<String, [u8; 3]>,
3382 ) {
3383 let pairs = overrides
3384 .into_iter()
3385 .map(|(k, [r, g, b])| (k, ratatui::style::Color::Rgb(r, g, b)));
3386 let applied = self.theme.write().unwrap().override_colors(pairs);
3387 if applied > 0 {
3388 self.reapply_all_overlays();
3391 }
3392 }
3393
3394 fn handle_await_next_key(&mut self, callback_id: fresh_core::api::JsCallbackId) {
3395 if let Some(payload) = self
3399 .active_window_mut()
3400 .pending_key_capture_buffer
3401 .pop_front()
3402 {
3403 let json = serde_json::to_string(&payload).unwrap_or_else(|_| "null".to_string());
3404 self.plugin_manager
3405 .read()
3406 .unwrap()
3407 .resolve_callback(callback_id, json);
3408 } else {
3409 self.active_window_mut()
3410 .pending_next_key_callbacks
3411 .push_back(callback_id);
3412 }
3413 }
3414
3415 fn handle_spawn_process(
3416 &mut self,
3417 command: String,
3418 args: Vec<String>,
3419 cwd: Option<String>,
3420 stdout_to: Option<std::path::PathBuf>,
3421 callback_id: fresh_core::api::JsCallbackId,
3422 ) {
3423 if let (Some(runtime), Some(bridge)) = (&self.tokio_runtime, &self.async_bridge) {
3424 let effective_cwd = cwd.or_else(|| {
3425 std::env::current_dir()
3426 .map(|p| p.to_string_lossy().to_string())
3427 .ok()
3428 });
3429 let sender = bridge.sender();
3430 let spawner = self.authority().process_spawner.clone();
3431
3432 let process_id = callback_id.as_u64();
3437 let (kill_tx, kill_rx) = tokio::sync::oneshot::channel::<()>();
3438 self.host_process_handles.insert(process_id, kill_tx);
3439
3440 runtime.spawn(async move {
3441 #[allow(clippy::let_underscore_must_use)]
3442 let outcome = spawner
3443 .spawn_cancellable(command, args, effective_cwd, stdout_to, kill_rx)
3444 .await;
3445 match outcome {
3446 Ok(result) => {
3447 #[allow(clippy::let_underscore_must_use)]
3448 let _ = sender.send(AsyncMessage::PluginProcessOutput {
3449 process_id,
3450 stdout: result.stdout,
3451 stderr: result.stderr,
3452 exit_code: result.exit_code,
3453 });
3454 }
3455 Err(e) => {
3456 #[allow(clippy::let_underscore_must_use)]
3457 let _ = sender.send(AsyncMessage::PluginProcessOutput {
3458 process_id,
3459 stdout: String::new(),
3460 stderr: e.to_string(),
3461 exit_code: -1,
3462 });
3463 }
3464 }
3465 });
3466 } else {
3467 self.plugin_manager
3468 .read()
3469 .unwrap()
3470 .reject_callback(callback_id, "Async runtime not available".to_string());
3471 }
3472 }
3473
3474 fn handle_kill_host_process(&mut self, process_id: u64) {
3475 if let Some(tx) = self.host_process_handles.remove(&process_id) {
3479 #[allow(clippy::let_underscore_must_use)]
3480 let _ = tx.send(());
3481 tracing::debug!("KillHostProcess: sent kill for process_id={}", process_id);
3482 } else {
3483 tracing::debug!(
3484 "KillHostProcess: unknown process_id={} (already exited?)",
3485 process_id
3486 );
3487 }
3488 }
3489
3490 fn handle_set_authority(&mut self, payload: serde_json::Value) {
3491 match serde_json::from_value::<crate::services::authority::AuthorityPayload>(payload) {
3494 Ok(parsed) => {
3495 let trust = std::sync::Arc::clone(&self.authority().workspace_trust);
3498 let env = std::sync::Arc::clone(&self.authority().env_provider);
3499 let spec = crate::services::authority::SessionAuthoritySpec::Plugin(parsed.clone());
3505 match crate::services::authority::Authority::from_plugin_payload(parsed, trust, env)
3506 {
3507 Ok(auth) => {
3508 tracing::info!("Plugin installed new authority");
3509 self.active_window_mut().authority_spec = spec;
3510 self.install_authority(auth);
3511 }
3512 Err(e) => {
3513 tracing::warn!("setAuthority: invalid payload: {}", e);
3514 self.set_status_message(format!("setAuthority rejected: {}", e));
3515 }
3516 }
3517 }
3518 Err(e) => {
3519 tracing::warn!("setAuthority: failed to parse payload: {}", e);
3520 self.set_status_message(format!("setAuthority rejected: {}", e));
3521 }
3522 }
3523 }
3524
3525 pub(crate) fn reconnect_dormant_session_if_needed(&mut self, window_id: fresh_core::WindowId) {
3533 if self.session_keepalives.contains_key(&window_id) {
3536 return;
3537 }
3538 let Some(spec) = self
3539 .windows
3540 .get(&window_id)
3541 .map(|w| w.authority_spec.clone())
3542 else {
3543 return;
3544 };
3545 match spec {
3546 crate::services::authority::SessionAuthoritySpec::Local => {}
3547 crate::services::authority::SessionAuthoritySpec::RemoteAgent(agent_spec) => {
3548 let request_id = u64::MAX - window_id.0 as u64;
3552 if self.remote_attach_inflight.contains(&request_id) {
3553 return;
3554 }
3555 self.start_remote_connect(agent_spec, Some(window_id), request_id);
3556 }
3557 crate::services::authority::SessionAuthoritySpec::Plugin(_) => {
3558 tracing::debug!(
3562 "dormant container session {window_id}: reattach is plugin-driven (TODO)"
3563 );
3564 }
3565 }
3566 }
3567
3568 fn handle_attach_remote_agent(&mut self, payload: serde_json::Value, request_id: u64) {
3569 let spec =
3572 match serde_json::from_value::<crate::services::authority::RemoteAgentSpec>(payload) {
3573 Ok(spec) => spec,
3574 Err(e) => {
3575 tracing::warn!("attachRemoteAgent: invalid payload: {}", e);
3576 self.reject_remote_attach(request_id, format!("invalid attach spec: {e}"));
3577 return;
3578 }
3579 };
3580 self.start_remote_connect(spec, None, request_id);
3583 }
3584
3585 pub(crate) fn start_remote_connect(
3592 &mut self,
3593 spec: crate::services::authority::RemoteAgentSpec,
3594 reconnect_window: Option<fresh_core::WindowId>,
3595 request_id: u64,
3596 ) {
3597 let runtime = self.tokio_runtime.clone();
3600 let sender = self.async_bridge.as_ref().map(|b| b.sender());
3601 let (Some(runtime), Some(sender)) = (runtime, sender) else {
3602 self.reject_remote_attach(request_id, "async runtime not available".to_string());
3603 return;
3604 };
3605
3606 self.remote_attach_inflight.insert(request_id);
3611 let (cancel_tx, cancel_rx) = tokio::sync::oneshot::channel::<()>();
3612 self.remote_attach_cancels.insert(request_id, cancel_tx);
3613
3614 let window_mode = spec.window;
3618 let window_label = spec.label.clone();
3619 let window_command = spec.command.clone();
3620 let trust = std::sync::Arc::new(crate::services::workspace_trust::WorkspaceTrust::new(
3627 None,
3628 self.authority().workspace_trust.level(),
3629 ));
3630 let env = std::sync::Arc::new(crate::services::env_provider::EnvProvider::inactive());
3631
3632 use crate::services::authority::RemoteTransportSpec;
3639 let base_env = spec.base_env.clone();
3640 let session_spec =
3643 crate::services::authority::SessionAuthoritySpec::RemoteAgent(spec.clone());
3644 let mode_for = |label: &str| {
3645 if let Some(window_id) = reconnect_window {
3646 crate::services::async_bridge::RemoteAttachMode::Reconnect { window_id }
3647 } else if window_mode {
3648 crate::services::async_bridge::RemoteAttachMode::Window {
3649 label: window_label.clone().unwrap_or_else(|| label.to_string()),
3650 command: window_command.clone(),
3651 }
3652 } else {
3653 crate::services::async_bridge::RemoteAttachMode::Restart
3654 }
3655 };
3656
3657 match spec.transport {
3658 RemoteTransportSpec::KubectlExec { .. } => {
3659 let (target, base_env) = spec.into_kube_target();
3660 let label = target.display();
3661 let workspace = target.workspace.clone().map(std::path::PathBuf::from);
3663 let mode = mode_for(&label);
3664 self.set_status_message(format!("Connecting to {label}…"));
3665 runtime.spawn(async move {
3666 let outcome = crate::services::authority::connect_kube_authority(
3667 target,
3668 base_env,
3669 trust,
3670 env,
3671 Some(cancel_rx),
3672 )
3673 .await;
3674 let msg = match outcome {
3675 Ok((authority, keepalive)) => AsyncMessage::RemoteAttachReady(
3676 crate::services::async_bridge::RemoteAttachReady {
3677 authority,
3678 keepalive: Box::new(keepalive),
3679 working_dir: workspace,
3680 mode,
3681 spec: session_spec,
3682 request_id,
3683 },
3684 ),
3685 Err(e) => AsyncMessage::RemoteAttachFailed {
3686 error: e.to_string(),
3687 request_id,
3688 },
3689 };
3690 #[allow(clippy::let_underscore_must_use)]
3691 let _ = sender.send(msg);
3692 });
3693 }
3694 RemoteTransportSpec::Ssh {
3695 user,
3696 host,
3697 port,
3698 identity_file,
3699 remote_path,
3700 extra_args,
3701 } => {
3702 let _ = base_env; let params = crate::services::remote::ConnectionParams {
3704 user: user.clone().filter(|u| !u.is_empty()),
3705 host: host.clone(),
3706 port,
3707 identity_file: identity_file.map(std::path::PathBuf::from),
3708 extra_args,
3709 };
3710 let target = params.ssh_target();
3712 let label = match port {
3713 Some(p) => format!("ssh:{target}:{p}"),
3714 None => format!("ssh:{target}"),
3715 };
3716 let workspace = remote_path.clone().map(std::path::PathBuf::from);
3717 let mode = mode_for(&label);
3718 self.set_status_message(format!("Connecting to {label}…"));
3719 runtime.spawn(async move {
3720 let outcome = crate::services::authority::connect_ssh_authority(
3721 params,
3722 remote_path,
3723 trust,
3724 env,
3725 Some(cancel_rx),
3726 )
3727 .await;
3728 let msg = match outcome {
3729 Ok((authority, keepalive)) => AsyncMessage::RemoteAttachReady(
3730 crate::services::async_bridge::RemoteAttachReady {
3731 authority,
3732 keepalive: Box::new(keepalive),
3733 working_dir: workspace,
3734 mode,
3735 spec: session_spec,
3736 request_id,
3737 },
3738 ),
3739 Err(e) => AsyncMessage::RemoteAttachFailed {
3740 error: e.to_string(),
3741 request_id,
3742 },
3743 };
3744 #[allow(clippy::let_underscore_must_use)]
3745 let _ = sender.send(msg);
3746 });
3747 }
3748 }
3749 }
3750
3751 fn handle_set_remote_indicator_state(&mut self, state: serde_json::Value) {
3752 match serde_json::from_value::<crate::view::ui::status_bar::RemoteIndicatorOverride>(state)
3755 {
3756 Ok(over) => {
3757 self.remote_indicator_override = Some(over);
3758 }
3759 Err(e) => {
3760 tracing::warn!("setRemoteIndicatorState: invalid payload: {}", e);
3761 self.set_status_message(format!("setRemoteIndicatorState rejected: {}", e));
3762 }
3763 }
3764 }
3765
3766 fn handle_spawn_process_wait(
3767 &mut self,
3768 process_id: u64,
3769 callback_id: fresh_core::api::JsCallbackId,
3770 ) {
3771 tracing::warn!(
3772 "SpawnProcessWait not fully implemented - process_id={}",
3773 process_id
3774 );
3775 self.plugin_manager.read().unwrap().reject_callback(
3776 callback_id,
3777 format!(
3778 "SpawnProcessWait not yet fully implemented for process_id={}",
3779 process_id
3780 ),
3781 );
3782 }
3783
3784 fn handle_delay(&mut self, callback_id: fresh_core::api::JsCallbackId, duration_ms: u64) {
3785 if let (Some(runtime), Some(bridge)) = (&self.tokio_runtime, &self.async_bridge) {
3786 let sender = bridge.sender();
3787 let callback_id_u64 = callback_id.as_u64();
3788 runtime.spawn(async move {
3789 tokio::time::sleep(tokio::time::Duration::from_millis(duration_ms)).await;
3790 #[allow(clippy::let_underscore_must_use)]
3791 let _ = sender.send(crate::services::async_bridge::AsyncMessage::Plugin(
3792 fresh_core::api::PluginAsyncMessage::DelayComplete {
3793 callback_id: callback_id_u64,
3794 },
3795 ));
3796 });
3797 } else {
3798 std::thread::sleep(std::time::Duration::from_millis(duration_ms));
3799 self.plugin_manager
3800 .read()
3801 .unwrap()
3802 .resolve_callback(callback_id, "null".to_string());
3803 }
3804 }
3805
3806 fn handle_http_fetch(
3807 &mut self,
3808 url: String,
3809 target_path: std::path::PathBuf,
3810 callback_id: fresh_core::api::JsCallbackId,
3811 ) {
3812 if let (Some(runtime), Some(bridge)) = (&self.tokio_runtime, &self.async_bridge) {
3813 let sender = bridge.sender();
3814 let process_id = callback_id.as_u64();
3815
3816 runtime.spawn(async move {
3817 let fetch = tokio::task::spawn_blocking(move || {
3818 crate::services::http::download_to_file(&url, &target_path)
3819 })
3820 .await;
3821
3822 let (stdout, stderr, exit_code) = match fetch {
3823 Ok(Ok(status)) => {
3824 if (200..300).contains(&status) {
3825 (String::new(), String::new(), 0)
3826 } else {
3827 (String::new(), format!("HTTP {}", status), i32::from(status))
3828 }
3829 }
3830 Ok(Err(e)) => (String::new(), e, -1),
3831 Err(e) => (String::new(), format!("fetch task failed: {}", e), -1),
3832 };
3833
3834 #[allow(clippy::let_underscore_must_use)]
3835 let _ = sender.send(AsyncMessage::PluginProcessOutput {
3836 process_id,
3837 stdout,
3838 stderr,
3839 exit_code,
3840 });
3841 });
3842 } else {
3843 self.plugin_manager
3844 .read()
3845 .unwrap()
3846 .reject_callback(callback_id, "Async runtime not available".to_string());
3847 }
3848 }
3849
3850 fn handle_kill_background_process(&mut self, process_id: u64) {
3851 if let Some(handle) = self.background_process_handles.remove(&process_id) {
3852 handle.abort();
3853 tracing::debug!("Killed background process {}", process_id);
3854 }
3855 }
3856
3857 fn handle_create_virtual_buffer(&mut self, name: String, mode: String, read_only: bool) {
3858 let buffer_id =
3859 self.active_window_mut()
3860 .create_virtual_buffer(name.clone(), mode.clone(), read_only);
3861 tracing::info!(
3862 "Created virtual buffer '{}' with mode '{}' (id={:?})",
3863 name,
3864 mode,
3865 buffer_id
3866 );
3867 }
3869
3870 fn handle_set_virtual_buffer_content(
3871 &mut self,
3872 buffer_id: BufferId,
3873 entries: Vec<fresh_core::text_property::TextPropertyEntry>,
3874 ) {
3875 match self.set_virtual_buffer_content(buffer_id, entries) {
3876 Ok(()) => {
3877 tracing::debug!("Set virtual buffer content for {:?}", buffer_id);
3878 }
3879 Err(e) => {
3880 tracing::error!("Failed to set virtual buffer content: {}", e);
3881 }
3882 }
3883 }
3884
3885 fn handle_mount_widget_panel(
3886 &mut self,
3887 panel_id: u64,
3888 buffer_id: BufferId,
3889 spec: fresh_core::api::WidgetSpec,
3890 ) {
3891 let prev = std::collections::HashMap::new();
3896 let prev_focus = String::new();
3897 let panel_width = self.widget_panel_width(buffer_id);
3898 let out = crate::widgets::render_spec(&spec, &prev, &prev_focus, panel_width);
3899 let focus_cursor = out.focus_cursor;
3900 self.widget_registry.mount(
3901 panel_id,
3902 buffer_id,
3903 spec,
3904 out.hits,
3905 out.instance_states,
3906 out.focus_key,
3907 out.tabbable,
3908 );
3909 let entries = out.entries;
3910 if let Err(e) = self.set_virtual_buffer_content(buffer_id, entries.clone()) {
3911 tracing::error!(
3912 "Failed to render mounted widget panel {} into {:?}: {}",
3913 panel_id,
3914 buffer_id,
3915 e
3916 );
3917 } else {
3918 tracing::debug!(
3919 "Mounted widget panel {} into buffer {:?}",
3920 panel_id,
3921 buffer_id
3922 );
3923 }
3924 self.apply_widget_focus_cursor(buffer_id, &entries, focus_cursor);
3925 }
3926
3927 fn handle_update_widget_panel(&mut self, panel_id: u64, spec: fresh_core::api::WidgetSpec) {
3928 let prev = match self.widget_registry.instance_states(panel_id) {
3929 Some(s) => s.clone(),
3930 None => {
3931 tracing::debug!(
3932 "UpdateWidgetPanel for unknown panel {} ignored (not mounted)",
3933 panel_id
3934 );
3935 return;
3936 }
3937 };
3938 let prev_focus = self
3939 .widget_registry
3940 .focus_key(panel_id)
3941 .map(|s| s.to_string())
3942 .unwrap_or_default();
3943 let buffer_id_for_width = self
3944 .widget_registry
3945 .buffer_and_spec(panel_id)
3946 .map(|(b, _)| b)
3947 .unwrap_or(BufferId(0));
3948 let panel_width = self.widget_panel_width(buffer_id_for_width);
3949 let out = crate::widgets::render_spec(&spec, &prev, &prev_focus, panel_width);
3950 let focus_cursor = out.focus_cursor;
3951 let entries = out.entries;
3952 match self.widget_registry.update(
3953 panel_id,
3954 spec,
3955 out.hits,
3956 out.instance_states,
3957 out.focus_key,
3958 out.tabbable,
3959 ) {
3960 Ok(buffer_id) => {
3961 if let Err(e) = self.set_virtual_buffer_content(buffer_id, entries.clone()) {
3962 tracing::error!("Failed to render updated widget panel {}: {}", panel_id, e);
3963 }
3964 self.apply_widget_focus_cursor(buffer_id, &entries, focus_cursor);
3965 }
3966 Err(()) => {
3967 tracing::debug!(
3968 "UpdateWidgetPanel for unknown panel {} ignored (not mounted)",
3969 panel_id
3970 );
3971 }
3972 }
3973 }
3974
3975 fn handle_widget_mutate(&mut self, panel_id: u64, mutation: fresh_core::api::WidgetMutation) {
3981 use fresh_core::api::WidgetMutation;
3982
3983 if self.widget_registry.get(panel_id).is_none() {
3985 tracing::debug!(
3986 "WidgetMutate for unknown panel {} ignored (not mounted)",
3987 panel_id
3988 );
3989 return;
3990 }
3991
3992 match mutation {
3993 WidgetMutation::SetValue {
3994 widget_key,
3995 value,
3996 cursor_byte,
3997 } => {
3998 if let Some(panel) = self.widget_registry.get_mut(panel_id) {
4005 let (scroll, multiline, completions, sel_idx, scroll_off) =
4013 match panel.instance_states.get(&widget_key) {
4014 Some(crate::widgets::WidgetInstanceState::Text {
4015 editor,
4016 scroll,
4017 completions,
4018 completion_selected_index,
4019 completion_scroll_offset,
4020 }) => (
4021 *scroll,
4022 editor.multiline,
4023 completions.clone(),
4024 *completion_selected_index,
4025 *completion_scroll_offset,
4026 ),
4027 _ => (0u32, true, Vec::new(), 0usize, 0u32),
4028 };
4029 let mut editor = if multiline {
4030 crate::primitives::text_edit::TextEdit::with_text(&value)
4031 } else {
4032 crate::primitives::text_edit::TextEdit::single_line_with_text(&value)
4033 };
4034 let target = match cursor_byte {
4035 Some(c) if c >= 0 => (c as usize).min(value.len()),
4036 _ => value.len(),
4037 };
4038 editor.set_cursor_from_flat(target);
4039 panel.instance_states.insert(
4040 widget_key,
4041 crate::widgets::WidgetInstanceState::Text {
4042 editor,
4043 scroll,
4044 completions,
4045 completion_selected_index: sel_idx,
4046 completion_scroll_offset: scroll_off,
4047 },
4048 );
4049 }
4050 }
4051 WidgetMutation::SetChecked {
4052 widget_key,
4053 checked,
4054 } => {
4055 if let Some(panel) = self.widget_registry.get_mut(panel_id) {
4059 crate::widgets::set_toggle_checked_in_spec(
4060 &mut panel.spec,
4061 &widget_key,
4062 checked,
4063 );
4064 }
4065 }
4066 WidgetMutation::SetSelectedIndex { widget_key, index } => {
4067 if let Some(panel) = self.widget_registry.get_mut(panel_id) {
4069 let (prev_scroll, prev_index, prev_item_height, prev_user_scrolled) =
4070 match panel.instance_states.get(&widget_key) {
4071 Some(crate::widgets::WidgetInstanceState::List {
4072 scroll_offset,
4073 selected_index,
4074 item_height,
4075 user_scrolled,
4076 }) => (
4077 *scroll_offset,
4078 *selected_index,
4079 *item_height,
4080 *user_scrolled,
4081 ),
4082 _ => (0, -1, 1, false),
4083 };
4084 let user_scrolled = prev_user_scrolled && index == prev_index;
4090 panel.instance_states.insert(
4091 widget_key,
4092 crate::widgets::WidgetInstanceState::List {
4093 scroll_offset: prev_scroll,
4094 selected_index: index,
4095 item_height: prev_item_height,
4096 user_scrolled,
4097 },
4098 );
4099 }
4100 }
4101 WidgetMutation::SetCompletions { widget_key, items } => {
4102 if let Some(panel) = self.widget_registry.get_mut(panel_id) {
4111 if let Some(crate::widgets::WidgetInstanceState::Text {
4112 completions,
4113 completion_selected_index,
4114 completion_scroll_offset,
4115 ..
4116 }) = panel.instance_states.get_mut(&widget_key)
4117 {
4118 *completions = items;
4119 *completion_selected_index = 0;
4120 *completion_scroll_offset = 0;
4121 }
4122 }
4123 }
4124 WidgetMutation::SetItems {
4125 widget_key,
4126 items,
4127 item_keys,
4128 } => {
4129 if let Some(panel) = self.widget_registry.get_mut(panel_id) {
4131 crate::widgets::set_list_items_in_spec(
4132 &mut panel.spec,
4133 &widget_key,
4134 items,
4135 item_keys,
4136 );
4137 }
4138 }
4139 WidgetMutation::SetExpandedKeys { widget_key, keys } => {
4140 if let Some(panel) = self.widget_registry.get_mut(panel_id) {
4142 let (prev_scroll, prev_sel) = match panel.instance_states.get(&widget_key) {
4143 Some(crate::widgets::WidgetInstanceState::Tree {
4144 scroll_offset,
4145 selected_index,
4146 ..
4147 }) => (*scroll_offset, *selected_index),
4148 _ => (0, -1),
4149 };
4150 let expanded: std::collections::HashSet<String> = keys.into_iter().collect();
4151 panel.instance_states.insert(
4152 widget_key,
4153 crate::widgets::WidgetInstanceState::Tree {
4154 scroll_offset: prev_scroll,
4155 selected_index: prev_sel,
4156 expanded_keys: expanded,
4157 },
4158 );
4159 }
4160 }
4161 WidgetMutation::SetCheckedKeys {
4162 widget_key,
4163 checked,
4164 keys,
4165 } => {
4166 if let Some(panel) = self.widget_registry.get_mut(panel_id) {
4174 crate::widgets::set_tree_checked_keys_in_spec(
4175 &mut panel.spec,
4176 &widget_key,
4177 checked,
4178 &keys,
4179 );
4180 }
4181 }
4182 WidgetMutation::AppendTreeNodes {
4183 widget_key,
4184 new_nodes,
4185 new_item_keys,
4186 } => {
4187 if let Some(panel) = self.widget_registry.get_mut(panel_id) {
4188 crate::widgets::append_tree_nodes_in_spec(
4189 &mut panel.spec,
4190 &widget_key,
4191 new_nodes,
4192 new_item_keys,
4193 );
4194 }
4195 }
4196 WidgetMutation::SetRawEntries {
4197 widget_key,
4198 entries,
4199 } => {
4200 if let Some(panel) = self.widget_registry.get_mut(panel_id) {
4201 crate::widgets::set_raw_entries_in_spec(&mut panel.spec, &widget_key, entries);
4202 }
4203 }
4204 WidgetMutation::SetFocusKey { widget_key } => {
4205 self.widget_registry.set_focus_key(panel_id, widget_key);
4210 }
4211 }
4212
4213 self.rerender_widget_panel(panel_id);
4217 }
4218
4219 fn handle_unmount_widget_panel(&mut self, panel_id: u64) {
4220 match self.widget_registry.unmount(panel_id) {
4221 Some(buffer_id) => {
4222 tracing::debug!(
4223 "Unmounted widget panel {} (was rendering into {:?})",
4224 panel_id,
4225 buffer_id
4226 );
4227 }
4232 None => {
4233 tracing::debug!("UnmountWidgetPanel for unknown panel {} ignored", panel_id);
4234 }
4235 }
4236 }
4237
4238 fn handle_mount_floating_widget(
4239 &mut self,
4240 panel_id: u64,
4241 spec: fresh_core::api::WidgetSpec,
4242 width_pct: u8,
4243 height_pct: u8,
4244 as_dock: bool,
4245 ) {
4246 let width_pct = width_pct.clamp(1, 100);
4247 let height_pct = height_pct.clamp(1, 100);
4248 let slot = if as_dock {
4251 super::PanelSlot::Dock
4252 } else {
4253 super::PanelSlot::Floating
4254 };
4255 let buffer_id = slot.buffer_id();
4256 if !as_dock && self.dock.as_ref().is_some_and(|f| f.focused) {
4263 self.blur_floating_panel(super::PanelSlot::Dock);
4264 }
4265 let placement = if as_dock {
4266 let width = self
4267 .dock_width
4268 .unwrap_or(32)
4269 .clamp(10, self.terminal_width.max(20).saturating_sub(20).max(10));
4270 super::PanelPlacement::LeftDock { width_cols: width }
4271 } else {
4272 super::PanelPlacement::Centered
4273 };
4274 if let Some(existing) = self.panel_opt_mut(slot).take() {
4275 if existing.panel_id != panel_id {
4276 let _ = self.widget_registry.unmount(existing.panel_id);
4277 }
4278 }
4279 *self.panel_opt_mut(slot) = Some(FloatingWidgetState {
4280 panel_id,
4281 width_pct,
4282 height_pct,
4283 placement,
4284 focused: true,
4285 entries: Vec::new(),
4286 focus_cursor: None,
4287 embeds: Vec::new(),
4288 overlays: Vec::new(),
4289 scroll_regions: Vec::new(),
4290 scrollbar_tracks: Vec::new(),
4291 scrollbar_mouse: Default::default(),
4292 scrollbar_drag_key: None,
4293 last_inner_rect: None,
4294 scrollbar_hover_zones: Vec::new(),
4295 scrollbar_zone_hovered: false,
4296 fullscreen: false,
4297 });
4298 let prev = std::collections::HashMap::new();
4299 let prev_focus = String::new();
4300 let panel_width = self.floating_panel_inner_width(slot);
4301 let out = crate::widgets::render_spec(&spec, &prev, &prev_focus, panel_width);
4302 let focus_cursor = out.focus_cursor;
4303 let entries = out.entries;
4304 let embeds = out.embeds;
4305 let overlays = out.overlays;
4306 let scroll_regions = out.scroll_regions;
4307 self.widget_registry.mount(
4308 panel_id,
4309 buffer_id,
4310 spec,
4311 out.hits,
4312 out.instance_states,
4313 out.focus_key,
4314 out.tabbable,
4315 );
4316 if let Some(fwp) = self.panel_mut(slot) {
4317 fwp.entries = entries;
4318 fwp.focus_cursor = focus_cursor;
4319 fwp.embeds = embeds;
4320 fwp.overlays = overlays;
4321 fwp.scroll_regions = scroll_regions;
4322 }
4323 tracing::debug!(
4324 "Mounted floating widget panel {} ({}%x{}%)",
4325 panel_id,
4326 width_pct,
4327 height_pct
4328 );
4329
4330 if as_dock {
4335 self.relayout();
4336 }
4337 }
4338
4339 fn handle_update_floating_widget(&mut self, panel_id: u64, spec: fresh_core::api::WidgetSpec) {
4340 let Some(slot) = self.slot_of_panel(panel_id) else {
4341 tracing::debug!(
4342 "UpdateFloatingWidget for unknown / mismatched panel {} ignored",
4343 panel_id
4344 );
4345 return;
4346 };
4347 let prev = self
4348 .widget_registry
4349 .instance_states(panel_id)
4350 .cloned()
4351 .unwrap_or_default();
4352 let prev_focus = self
4353 .widget_registry
4354 .focus_key(panel_id)
4355 .map(|s| s.to_string())
4356 .unwrap_or_default();
4357 let panel_width = self.floating_panel_inner_width(slot);
4358 let out = crate::widgets::render_spec(&spec, &prev, &prev_focus, panel_width);
4359 let focus_cursor = out.focus_cursor;
4360 let entries = out.entries;
4361 let embeds = out.embeds;
4362 let overlays = out.overlays;
4363 let scroll_regions = out.scroll_regions;
4364 if self
4365 .widget_registry
4366 .update(
4367 panel_id,
4368 spec,
4369 out.hits,
4370 out.instance_states,
4371 out.focus_key,
4372 out.tabbable,
4373 )
4374 .is_err()
4375 {
4376 tracing::debug!(
4377 "UpdateFloatingWidget for unknown panel {} ignored (not in registry)",
4378 panel_id
4379 );
4380 return;
4381 }
4382 if let Some(fwp) = self.panel_mut(slot) {
4383 fwp.entries = entries;
4384 fwp.focus_cursor = focus_cursor;
4385 fwp.embeds = embeds;
4386 fwp.overlays = overlays;
4387 fwp.scroll_regions = scroll_regions;
4388 }
4389 }
4390
4391 fn handle_unmount_floating_widget(&mut self, panel_id: u64) {
4392 let Some(slot) = self.slot_of_panel(panel_id) else {
4393 tracing::debug!(
4394 "UnmountFloatingWidget for unknown / mismatched panel {} ignored",
4395 panel_id
4396 );
4397 return;
4398 };
4399 *self.panel_opt_mut(slot) = None;
4400 let _ = self.widget_registry.unmount(panel_id);
4401 if slot == super::PanelSlot::Dock {
4412 self.request_full_redraw();
4413 }
4414 self.relayout();
4430 tracing::debug!("Unmounted floating widget panel {}", panel_id);
4431 }
4432
4433 fn handle_floating_panel_control(&mut self, panel_id: u64, op: &str, arg: f64) {
4436 let Some(slot) = self.slot_of_panel(panel_id) else {
4437 tracing::warn!("FloatingPanelControl for unknown/mismatched panel {panel_id} ignored");
4438 return;
4439 };
4440 if op == "blur" {
4443 self.blur_floating_panel(slot);
4444 return;
4445 }
4446 let max_cols = self.terminal_width.max(20).saturating_sub(20).max(10);
4451 let persisted = self.dock_width;
4452 let Some(fwp) = self.panel_mut(slot) else {
4453 return;
4454 };
4455 let geometry_changed = match op {
4458 "dock" => {
4459 let requested = persisted.unwrap_or(arg as u16);
4460 let width_cols = requested.clamp(10, max_cols);
4461 fwp.placement = super::PanelPlacement::LeftDock { width_cols };
4462 fwp.focused = true;
4463 true
4464 }
4465 "dock_width" => {
4471 if let super::PanelPlacement::LeftDock { .. } = fwp.placement {
4472 let requested = persisted.unwrap_or(arg as u16);
4473 let width_cols = requested.clamp(10, max_cols);
4474 fwp.placement = super::PanelPlacement::LeftDock { width_cols };
4475 true
4476 } else {
4477 false
4478 }
4479 }
4480 "center" => {
4481 fwp.placement = super::PanelPlacement::Centered;
4482 fwp.focused = true;
4483 true
4484 }
4485 "anchor" => {
4491 let packed = arg.max(0.0) as u64;
4492 let x = (packed & 0xFFFF) as u16;
4493 let y = ((packed >> 16) & 0xFFFF) as u16;
4494 fwp.placement = super::PanelPlacement::Anchored { x, y };
4495 fwp.focused = true;
4496 fwp.fullscreen = false;
4497 false
4498 }
4499 "focus" => {
4500 fwp.focused = true;
4501 false
4502 }
4503 "fullscreen" => {
4509 fwp.fullscreen = arg != 0.0;
4510 false
4511 }
4512 other => {
4513 tracing::warn!("FloatingPanelControl: unknown op {other:?}");
4514 false
4515 }
4516 };
4517 if geometry_changed {
4521 self.relayout();
4522 }
4523 }
4524
4525 fn handle_get_text_properties_at_cursor(&self, buffer_id: BufferId) {
4526 if let Some(state) = self
4527 .windows
4528 .get(&self.active_window)
4529 .map(|w| &w.buffers)
4530 .expect("active window present")
4531 .get(&buffer_id)
4532 {
4533 let cursor_pos = self
4534 .windows
4535 .get(&self.active_window)
4536 .and_then(|w| w.buffers.splits())
4537 .map(|(_, vs)| vs)
4538 .expect("active window must have a populated split layout")
4539 .values()
4540 .find_map(|vs| vs.buffer_state(buffer_id))
4541 .map(|bs| bs.cursors.primary().position)
4542 .unwrap_or(0);
4543 let properties = state.text_properties.get_at(cursor_pos);
4544 tracing::debug!(
4545 "Text properties at cursor in {:?}: {} properties found",
4546 buffer_id,
4547 properties.len()
4548 );
4549 }
4551 }
4552
4553 fn handle_set_context(&mut self, name: String, active: bool) {
4554 if active {
4555 self.active_window_mut()
4556 .active_custom_contexts
4557 .insert(name.clone());
4558 tracing::debug!("Set custom context: {}", name);
4559 } else {
4560 self.active_window_mut()
4561 .active_custom_contexts
4562 .remove(&name);
4563 tracing::debug!("Unset custom context: {}", name);
4564 }
4565 }
4566
4567 fn handle_disable_lsp_for_language(&mut self, language: String) {
4568 tracing::info!("Disabling LSP for language: {}", language);
4569 let __active_id = self.active_window;
4570 if let Some(lsp) = self.windows.get_mut(&__active_id).map(|w| &mut w.lsp) {
4571 lsp.shutdown_server(&language);
4572 tracing::info!("Stopped LSP server for {}", language);
4573 }
4574 if let Some(lsp_configs) = self.config_mut().lsp.get_mut(&language) {
4575 for c in lsp_configs.as_mut_slice() {
4576 c.enabled = false;
4577 c.auto_start = false;
4578 }
4579 tracing::info!("Disabled LSP config for {}", language);
4580 }
4581 if let Err(e) = self.save_config() {
4582 tracing::error!("Failed to save config: {}", e);
4583 self.active_window_mut().status_message = Some(format!(
4584 "LSP disabled for {} (config save failed)",
4585 language
4586 ));
4587 } else {
4588 self.active_window_mut().status_message =
4589 Some(format!("LSP disabled for {}", language));
4590 }
4591 self.active_window_mut().warning_domains.lsp.clear();
4592 }
4593
4594 fn handle_restart_lsp_for_language(&mut self, language: String) {
4595 tracing::info!("Plugin restarting LSP for language: {}", language);
4596 let file_path = self
4597 .active_window()
4598 .buffer_metadata
4599 .get(&self.active_buffer())
4600 .and_then(|meta| meta.file_path().cloned());
4601 let __active_id = self.active_window;
4602 let success = if let Some(lsp) = self.windows.get_mut(&__active_id).map(|w| &mut w.lsp) {
4603 let (ok, msg) = lsp.manual_restart(&language, file_path.as_deref());
4604 self.active_window_mut().status_message = Some(msg);
4605 ok
4606 } else {
4607 self.active_window_mut().status_message = Some("No LSP manager available".to_string());
4608 false
4609 };
4610 if success {
4611 self.reopen_buffers_for_language(&language);
4612 }
4613 }
4614
4615 fn handle_set_lsp_root_uri(&mut self, language: String, uri: String) {
4616 tracing::info!("Plugin setting LSP root URI for {}: {}", language, uri);
4617 match uri.parse::<lsp_types::Uri>() {
4618 Ok(parsed_uri) => {
4619 let __active_id = self.active_window;
4620 if let Some(lsp) = self.windows.get_mut(&__active_id).map(|w| &mut w.lsp) {
4621 let restarted = lsp.set_language_root_uri(&language, parsed_uri);
4622 if restarted {
4623 self.active_window_mut().status_message = Some(format!(
4624 "LSP root updated for {} (restarting server)",
4625 language
4626 ));
4627 } else {
4628 self.active_window_mut().status_message =
4629 Some(format!("LSP root set for {}", language));
4630 }
4631 }
4632 }
4633 Err(e) => {
4634 tracing::error!("Invalid LSP root URI '{}': {}", uri, e);
4635 self.active_window_mut().status_message =
4636 Some(format!("Invalid LSP root URI: {}", e));
4637 }
4638 }
4639 }
4640
4641 fn handle_create_scroll_sync_group(
4642 &mut self,
4643 group_id: crate::view::scroll_sync::ScrollSyncGroupId,
4644 left_split: SplitId,
4645 right_split: SplitId,
4646 ) {
4647 let success = self
4648 .active_window_mut()
4649 .scroll_sync_manager
4650 .create_group_with_id(group_id, left_split, right_split);
4651 if success {
4652 tracing::debug!(
4653 "Created scroll sync group {} for splits {:?} and {:?}",
4654 group_id,
4655 left_split,
4656 right_split
4657 );
4658 } else {
4659 tracing::warn!(
4660 "Failed to create scroll sync group {} (ID already exists)",
4661 group_id
4662 );
4663 }
4664 }
4665
4666 fn handle_set_scroll_sync_anchors(
4667 &mut self,
4668 group_id: crate::view::scroll_sync::ScrollSyncGroupId,
4669 anchors: Vec<(usize, usize)>,
4670 ) {
4671 use crate::view::scroll_sync::SyncAnchor;
4672 let anchor_count = anchors.len();
4673 let sync_anchors: Vec<SyncAnchor> = anchors
4674 .into_iter()
4675 .map(|(left_line, right_line)| SyncAnchor {
4676 left_line,
4677 right_line,
4678 })
4679 .collect();
4680 self.active_window_mut()
4681 .scroll_sync_manager
4682 .set_anchors(group_id, sync_anchors);
4683 tracing::debug!(
4684 "Set {} anchors for scroll sync group {}",
4685 anchor_count,
4686 group_id
4687 );
4688 }
4689
4690 fn handle_remove_scroll_sync_group(
4691 &mut self,
4692 group_id: crate::view::scroll_sync::ScrollSyncGroupId,
4693 ) {
4694 if self
4695 .active_window_mut()
4696 .scroll_sync_manager
4697 .remove_group(group_id)
4698 {
4699 tracing::debug!("Removed scroll sync group {}", group_id);
4700 } else {
4701 tracing::warn!("Scroll sync group {} not found", group_id);
4702 }
4703 }
4704
4705 fn handle_create_buffer_group(
4706 &mut self,
4707 name: String,
4708 mode: String,
4709 layout_json: String,
4710 request_id: Option<u64>,
4711 ) {
4712 match self.create_buffer_group(name, mode, layout_json) {
4713 Ok(result) => {
4714 if let Some(req_id) = request_id {
4715 let json = serde_json::to_string(&result).unwrap_or_default();
4716 self.plugin_manager
4717 .read()
4718 .unwrap()
4719 .resolve_callback(fresh_core::api::JsCallbackId::from(req_id), json);
4720 }
4721 }
4722 Err(e) => {
4723 tracing::error!("Failed to create buffer group: {}", e);
4724 }
4725 }
4726 }
4727
4728 fn handle_send_terminal_input(
4729 &mut self,
4730 terminal_id: crate::services::terminal::TerminalId,
4731 data: String,
4732 ) {
4733 if let Some(handle) = self.active_window().terminal_manager.get(terminal_id) {
4734 handle.write(data.as_bytes());
4735 tracing::trace!(
4736 "Plugin sent {} bytes to terminal {:?}",
4737 data.len(),
4738 terminal_id
4739 );
4740 } else {
4741 tracing::warn!(
4742 "Plugin tried to send input to non-existent terminal {:?}",
4743 terminal_id
4744 );
4745 }
4746 }
4747
4748 fn handle_close_terminal(&mut self, terminal_id: crate::services::terminal::TerminalId) {
4749 let buffer_to_close = self
4750 .active_window()
4751 .terminal_buffers
4752 .iter()
4753 .find(|(_, &tid)| tid == terminal_id)
4754 .map(|(&bid, _)| bid);
4755 if let Some(buffer_id) = buffer_to_close {
4756 if let Err(e) = self.close_buffer(buffer_id) {
4757 tracing::warn!("Failed to close terminal buffer: {}", e);
4758 }
4759 tracing::info!("Plugin closed terminal {:?}", terminal_id);
4760 } else {
4761 self.active_window_mut().terminal_manager.close(terminal_id);
4762 tracing::info!("Plugin closed terminal {:?} (no buffer found)", terminal_id);
4763 }
4764 }
4765
4766 fn handle_signal_window(&mut self, id: fresh_core::WindowId, signal: &str) {
4774 let Some(window) = self.windows.get_mut(&id) else {
4775 tracing::warn!("Plugin SignalWindow targeted unknown window {:?}", id);
4776 return;
4777 };
4778 let results = window.process_groups.signal_all(signal);
4779 for (entry, result) in results {
4780 match result {
4781 Ok(true) => tracing::info!(
4782 "SignalWindow {:?}: {} → pid {} ({})",
4783 id,
4784 signal,
4785 entry.leader_pid,
4786 entry.label
4787 ),
4788 Ok(false) => tracing::debug!(
4789 "SignalWindow {:?}: pid {} ({}) already exited",
4790 id,
4791 entry.leader_pid,
4792 entry.label
4793 ),
4794 Err(e) => tracing::warn!(
4795 "SignalWindow {:?}: pid {} ({}): {}",
4796 id,
4797 entry.leader_pid,
4798 entry.label,
4799 e
4800 ),
4801 }
4802 }
4803 }
4804}
4805
4806fn clamp_buffer_text_range(start: usize, end: usize, len: usize) -> (usize, usize) {
4817 let end = end.min(len);
4818 let start = start.min(end);
4819 (start, end)
4820}
4821
4822#[cfg(test)]
4823mod tests {
4824 use tokio::io::{AsyncReadExt, BufReader};
4837 use tokio::process::Command as TokioCommand;
4838 use tokio::time::{timeout, Duration};
4839
4840 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
4851 async fn kill_via_oneshot_terminates_long_running_child() {
4852 let mut cmd = TokioCommand::new("sleep");
4853 cmd.args(["30"]);
4854 cmd.stdout(std::process::Stdio::piped());
4855 cmd.stderr(std::process::Stdio::piped());
4856
4857 let mut child = cmd.spawn().expect("spawn sh -c sleep 30");
4858 let pid = child.id().expect("child has a pid");
4859
4860 let (kill_tx, mut kill_rx) = tokio::sync::oneshot::channel::<()>();
4861 let stdout_pipe = child.stdout.take();
4862 let stderr_pipe = child.stderr.take();
4863
4864 let stdout_fut = async {
4865 let mut buf = String::new();
4866 if let Some(s) = stdout_pipe {
4867 #[allow(clippy::let_underscore_must_use)]
4868 let _ = BufReader::new(s).read_to_string(&mut buf).await;
4869 }
4870 buf
4871 };
4872 let stderr_fut = async {
4873 let mut buf = String::new();
4874 if let Some(s) = stderr_pipe {
4875 #[allow(clippy::let_underscore_must_use)]
4876 let _ = BufReader::new(s).read_to_string(&mut buf).await;
4877 }
4878 buf
4879 };
4880 let wait_fut = async {
4881 tokio::select! {
4882 status = child.wait() => {
4883 status.map(|s| s.code().unwrap_or(-1)).unwrap_or(-1)
4884 }
4885 _ = &mut kill_rx => {
4886 #[allow(clippy::let_underscore_must_use)]
4887 let _ = child.start_kill();
4888 child
4889 .wait()
4890 .await
4891 .map(|s| s.code().unwrap_or(-1))
4892 .unwrap_or(-1)
4893 }
4894 }
4895 };
4896
4897 tokio::time::sleep(Duration::from_millis(50)).await;
4902 kill_tx.send(()).expect("kill channel send");
4903
4904 let result = timeout(Duration::from_secs(5), async {
4905 tokio::join!(stdout_fut, stderr_fut, wait_fut)
4906 })
4907 .await;
4908
4909 let (_stdout, _stderr, exit_code) = result.expect(
4910 "kill path must resolve within 5s — if this times out the \
4911 select! arm order or kill-then-wait logic is broken",
4912 );
4913 assert_ne!(
4925 exit_code, 0,
4926 "killed child must exit non-success (got 0 — did the \
4927 kill arm fire too late, or did sleep somehow complete?)"
4928 );
4929
4930 #[cfg(unix)]
4939 {
4940 let still_alive = std::process::Command::new("kill")
4941 .args(["-0", &pid.to_string()])
4942 .status()
4943 .map(|s| s.success())
4944 .unwrap_or(false);
4945 assert!(
4946 !still_alive,
4947 "process {pid} must be reaped after wait() — a still-\
4948 alive check means the kill path leaked the child"
4949 );
4950 }
4951 #[cfg(not(unix))]
4952 {
4953 let _ = pid;
4956 }
4957 }
4958
4959 use super::clamp_buffer_text_range;
4960
4961 #[test]
4962 fn clamp_text_range_passes_through_in_bounds() {
4963 assert_eq!(clamp_buffer_text_range(0, 165, 165), (0, 165));
4964 assert_eq!(clamp_buffer_text_range(10, 50, 165), (10, 50));
4965 }
4966
4967 #[test]
4973 fn clamp_text_range_clamps_stale_end_past_buffer() {
4974 assert_eq!(clamp_buffer_text_range(0, 165_003, 165_002), (0, 165_002));
4975 }
4976
4977 #[test]
4978 fn clamp_text_range_pins_overlarge_start_to_empty() {
4979 assert_eq!(clamp_buffer_text_range(200, 250, 165), (165, 165));
4981 }
4982}
4983
4984impl Window {
4985 #[cfg(feature = "plugins")]
5000 pub(crate) fn populate_plugin_state_snapshot(
5001 &mut self,
5002 snapshot: &mut fresh_core::api::EditorStateSnapshot,
5003 ) {
5004 use fresh_core::api::{BufferInfo, CursorInfo, ViewportInfo};
5005
5006 let current_gen = self.resources.grammar_registry.catalog_gen();
5012 if snapshot.last_grammar_gen != current_gen {
5013 snapshot.available_grammars = self
5014 .resources
5015 .grammar_registry
5016 .available_grammar_info()
5017 .into_iter()
5018 .map(|g| fresh_core::api::GrammarInfoSnapshot {
5019 name: g.name,
5020 source: g.source.to_string(),
5021 file_extensions: g.file_extensions,
5022 short_name: g.short_name,
5023 })
5024 .collect();
5025 snapshot.last_grammar_gen = current_gen;
5026 }
5027
5028 snapshot.active_buffer_id = self.active_buffer();
5029
5030 let (mgr_ref, vs_ref) = self
5031 .buffers
5032 .splits()
5033 .expect("active window must have a populated split layout");
5034 let active_split = mgr_ref.active_split();
5035 snapshot.active_split_id = active_split.0 .0;
5036
5037 snapshot.buffers.clear();
5039 snapshot.buffer_saved_diffs.clear();
5040 snapshot.buffer_cursor_positions.clear();
5041 snapshot.buffer_text_properties.clear();
5042
5043 let active_vs_opt = vs_ref.get(&active_split);
5044 for (buffer_id, state) in &self.buffers {
5045 let is_virtual = self
5046 .buffer_metadata
5047 .get(buffer_id)
5048 .map(|m| m.is_virtual())
5049 .unwrap_or(false);
5050 let view_mode = active_vs_opt
5055 .and_then(|vs| vs.buffer_state(*buffer_id))
5056 .map(|bs| match bs.view_mode {
5057 crate::state::ViewMode::Source => "source",
5058 crate::state::ViewMode::PageView => "compose",
5059 })
5060 .unwrap_or("source");
5061 let compose_width = active_vs_opt
5062 .and_then(|vs| vs.buffer_state(*buffer_id))
5063 .and_then(|bs| bs.compose_width);
5064 let is_composing_in_any_split = vs_ref.values().any(|vs| {
5065 vs.buffer_state(*buffer_id)
5066 .map(|bs| matches!(bs.view_mode, crate::state::ViewMode::PageView))
5067 .unwrap_or(false)
5068 });
5069 let is_preview = self
5070 .buffer_metadata
5071 .get(buffer_id)
5072 .map(|m| m.is_preview)
5073 .unwrap_or(false);
5074 let splits: Vec<fresh_core::SplitId> = mgr_ref
5080 .splits_for_buffer(*buffer_id)
5081 .into_iter()
5082 .map(|leaf_id| leaf_id.0)
5083 .collect();
5084 let buffer_info = BufferInfo {
5085 id: *buffer_id,
5086 path: state.buffer.file_path().map(|p| p.to_path_buf()),
5087 modified: state.buffer.is_modified(),
5088 length: state.buffer.len(),
5089 is_virtual,
5090 view_mode: view_mode.to_string(),
5091 is_composing_in_any_split,
5092 compose_width,
5093 language: state.language.clone(),
5094 is_preview,
5095 splits,
5096 };
5097 snapshot.buffers.insert(*buffer_id, buffer_info);
5098
5099 let diff = {
5100 let diff = state.buffer.diff_since_saved();
5101 BufferSavedDiff {
5102 equal: diff.equal,
5103 byte_ranges: diff.byte_ranges.clone(),
5104 }
5105 };
5106 snapshot.buffer_saved_diffs.insert(*buffer_id, diff);
5107
5108 let is_hidden = self
5117 .buffer_metadata
5118 .get(buffer_id)
5119 .is_some_and(|m| m.hidden_from_tabs);
5120 let source_split = vs_ref.iter().find(|(split_id, vs)| {
5121 vs.keyed_states.contains_key(buffer_id)
5122 && !(is_hidden && self.grouped_subtrees.contains_key(split_id))
5123 });
5124 let cursor_pos = source_split
5125 .and_then(|(_, vs)| vs.buffer_state(*buffer_id))
5126 .map(|bs| bs.cursors.primary().position)
5127 .unwrap_or(0);
5128 tracing::trace!(
5129 "snapshot: buffer {:?} cursor_pos={} (from split {:?})",
5130 buffer_id,
5131 cursor_pos,
5132 source_split.map(|(id, _)| *id),
5133 );
5134 snapshot
5135 .buffer_cursor_positions
5136 .insert(*buffer_id, cursor_pos);
5137
5138 if !state.text_properties.is_empty() {
5140 snapshot
5141 .buffer_text_properties
5142 .insert(*buffer_id, state.text_properties.all().to_vec());
5143 }
5144 }
5145
5146 let active_buf_id = snapshot.active_buffer_id;
5157 let active_split_id = self.effective_active_pair().0;
5158 self.buffers
5159 .with_all_mut(|buffers_mut, mgr, vs_map| {
5160 let _ = mgr; if let Some(active_vs) = vs_map.get(&active_split_id) {
5162 let active_cursors = &active_vs.cursors;
5164 let primary = active_cursors.primary();
5165 let primary_position = primary.position;
5166 let primary_selection = primary.selection_range();
5167
5168 let line_of = |offset: usize| -> Option<usize> {
5174 buffers_mut.get(&active_buf_id).and_then(|state| {
5175 if state.buffer.line_count().is_some() {
5176 Some(state.buffer.get_line_number(offset))
5177 } else {
5178 None
5179 }
5180 })
5181 };
5182
5183 snapshot.primary_cursor = Some(CursorInfo {
5184 position: primary_position,
5185 selection: primary_selection.clone(),
5186 line: line_of(primary_position),
5187 });
5188
5189 snapshot.all_cursors = active_cursors
5190 .iter()
5191 .map(|(_, cursor)| CursorInfo {
5192 position: cursor.position,
5193 selection: cursor.selection_range(),
5194 line: line_of(cursor.position),
5195 })
5196 .collect();
5197
5198 if let Some(range) = primary_selection {
5200 if let Some(active_state) = buffers_mut.get_mut(&active_buf_id) {
5201 snapshot.selected_text =
5202 Some(active_state.get_text_range(range.start, range.end));
5203 }
5204 }
5205
5206 let top_line = buffers_mut.get(&active_buf_id).and_then(|state| {
5208 if state.buffer.line_count().is_some() {
5209 Some(state.buffer.get_line_number(active_vs.viewport.top_byte))
5210 } else {
5211 None
5212 }
5213 });
5214 snapshot.viewport = Some(ViewportInfo {
5215 top_byte: active_vs.viewport.top_byte,
5216 top_line,
5217 left_column: active_vs.viewport.left_column,
5218 width: active_vs.viewport.width,
5219 height: active_vs.viewport.height,
5220 });
5221 } else {
5222 snapshot.primary_cursor = None;
5223 snapshot.all_cursors.clear();
5224 snapshot.viewport = None;
5225 snapshot.selected_text = None;
5226 }
5227
5228 snapshot.splits.clear();
5230 for (leaf_id, vs) in vs_map.iter() {
5231 let buf_id = vs.active_buffer;
5232 let top_line = buffers_mut.get(&buf_id).and_then(|state| {
5233 if state.buffer.line_count().is_some() {
5234 Some(state.buffer.get_line_number(vs.viewport.top_byte))
5235 } else {
5236 None
5237 }
5238 });
5239 snapshot.splits.push(fresh_core::api::SplitSnapshot {
5240 split_id: leaf_id.0 .0,
5241 buffer_id: buf_id,
5242 viewport: ViewportInfo {
5243 top_byte: vs.viewport.top_byte,
5244 top_line,
5245 left_column: vs.viewport.left_column,
5246 width: vs.viewport.width,
5247 height: vs.viewport.height,
5248 },
5249 });
5250 }
5251 })
5252 .expect("active window must have a populated split layout");
5253
5254 snapshot.active_session_plugin_states = self.plugin_state.clone();
5260 snapshot.diagnostics = Arc::clone(&self.stored_diagnostics);
5265 snapshot.folding_ranges = Arc::clone(&self.stored_folding_ranges);
5266
5267 snapshot.editor_mode = self.editor_mode.clone();
5269
5270 let active_split_id_u64 = active_split_id.0 .0;
5275 let split_changed = snapshot.plugin_view_states_split != active_split_id_u64;
5276 if split_changed {
5277 snapshot.plugin_view_states.clear();
5278 snapshot.plugin_view_states_split = active_split_id_u64;
5279 }
5280
5281 {
5283 let open_bids: Vec<_> = snapshot.buffers.keys().copied().collect();
5284 snapshot
5285 .plugin_view_states
5286 .retain(|bid, _| open_bids.contains(bid));
5287 }
5288
5289 if let Some(vs_map) = self.buffers.split_view_states() {
5291 if let Some(active_vs) = vs_map.get(&active_split_id) {
5292 for (buffer_id, buf_state) in &active_vs.keyed_states {
5293 if !buf_state.plugin_state.is_empty() {
5294 let entry = snapshot.plugin_view_states.entry(*buffer_id).or_default();
5295 for (key, value) in &buf_state.plugin_state {
5296 entry.entry(key.clone()).or_insert_with(|| value.clone());
5297 }
5298 }
5299 }
5300 }
5301 }
5302
5303 snapshot.has_active_search = self.search_state.is_some();
5305 }
5306}
5307
5308