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(super) 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 =
159 self.authority.workspace_trust.level().as_str().to_string();
160 snapshot.env_active = self.authority.env_provider.is_active();
161
162 let mut session_infos: Vec<fresh_core::api::WindowInfo> = self
168 .windows
169 .values()
170 .map(|s| {
171 let slot = s.plugin_state.get("orchestrator");
172 let project_path = slot
179 .and_then(|m| m.get("project_path"))
180 .and_then(|v| v.as_str())
181 .filter(|p| !p.is_empty())
182 .map(std::path::PathBuf::from)
183 .unwrap_or_else(|| s.root.clone());
184 let shared_worktree = slot
185 .and_then(|m| m.get("shared_worktree"))
186 .and_then(|v| v.as_bool())
187 .unwrap_or(false);
188 fresh_core::api::WindowInfo {
189 id: s.id,
190 label: s.label.clone(),
191 root: normalize_plugin_path(s.root.clone()),
192 project_path: normalize_plugin_path(project_path),
193 shared_worktree,
194 }
195 })
196 .collect();
197 session_infos.sort_by_key(|s| s.id.0);
198 snapshot.windows = session_infos;
199 snapshot.active_window_id = self.active_window;
200
201 if !Arc::ptr_eq(&self.config, &self.config_snapshot_anchor) {
210 let json = serde_json::to_value(&*self.config).unwrap_or(serde_json::Value::Null);
211 self.config_cached_json = Arc::new(json);
212 self.config_snapshot_anchor = Arc::clone(&self.config);
213 }
214 snapshot.config = Arc::clone(&self.config_cached_json);
215
216 snapshot.user_config = Arc::clone(&self.user_config_raw);
219
220 for (plugin_name, state_map) in &self.plugin_global_state {
223 let entry = snapshot
224 .plugin_global_states
225 .entry(plugin_name.clone())
226 .or_default();
227 for (key, value) in state_map {
228 entry.entry(key.clone()).or_insert_with(|| value.clone());
229 }
230 }
231 }
232
233 pub fn handle_plugin_command(&mut self, command: PluginCommand) -> AnyhowResult<()> {
235 match command {
236 PluginCommand::InsertText {
238 buffer_id,
239 position,
240 text,
241 } => {
242 self.handle_insert_text(buffer_id, position, text);
243 }
244 PluginCommand::DeleteRange { buffer_id, range } => {
245 self.handle_delete_range(buffer_id, range);
246 }
247 PluginCommand::InsertAtCursor { text } => {
248 self.handle_insert_at_cursor(text);
249 }
250 PluginCommand::DeleteSelection => {
251 self.handle_delete_selection();
252 }
253
254 PluginCommand::AddOverlay {
256 buffer_id,
257 namespace,
258 range,
259 options,
260 } => {
261 self.handle_add_overlay(buffer_id, namespace, range, options);
262 }
263 PluginCommand::RemoveOverlay { buffer_id, handle } => {
264 self.handle_remove_overlay(buffer_id, handle);
265 }
266 PluginCommand::ClearAllOverlays { buffer_id } => {
267 self.handle_clear_all_overlays(buffer_id);
268 }
269 PluginCommand::ClearNamespace {
270 buffer_id,
271 namespace,
272 } => {
273 self.handle_clear_namespace(buffer_id, namespace);
274 }
275 PluginCommand::ClearOverlaysInRange {
276 buffer_id,
277 start,
278 end,
279 } => {
280 self.handle_clear_overlays_in_range(buffer_id, start, end);
281 }
282 PluginCommand::ClearOverlaysInRangeForNamespace {
283 buffer_id,
284 namespace,
285 start,
286 end,
287 } => {
288 self.handle_clear_overlays_in_range_for_namespace(buffer_id, namespace, start, end);
289 }
290
291 PluginCommand::AddVirtualText {
293 buffer_id,
294 virtual_text_id,
295 position,
296 text,
297 color,
298 use_bg,
299 before,
300 } => {
301 self.handle_add_virtual_text(
302 buffer_id,
303 virtual_text_id,
304 position,
305 text,
306 color,
307 use_bg,
308 before,
309 );
310 }
311 PluginCommand::AddVirtualTextStyled {
312 buffer_id,
313 virtual_text_id,
314 position,
315 text,
316 fg,
317 bg,
318 bold,
319 italic,
320 before,
321 } => {
322 self.handle_add_virtual_text_styled(
323 buffer_id,
324 virtual_text_id,
325 position,
326 text,
327 fg,
328 bg,
329 bold,
330 italic,
331 before,
332 );
333 }
334 PluginCommand::RemoveVirtualText {
335 buffer_id,
336 virtual_text_id,
337 } => {
338 self.handle_remove_virtual_text(buffer_id, virtual_text_id);
339 }
340 PluginCommand::RemoveVirtualTextsByPrefix { buffer_id, prefix } => {
341 self.handle_remove_virtual_texts_by_prefix(buffer_id, prefix);
342 }
343 PluginCommand::ClearVirtualTexts { buffer_id } => {
344 self.handle_clear_virtual_texts(buffer_id);
345 }
346 PluginCommand::AddVirtualLine {
347 buffer_id,
348 position,
349 text,
350 fg_color,
351 bg_color,
352 above,
353 namespace,
354 priority,
355 gutter_glyph,
356 gutter_color,
357 text_overlays,
358 } => {
359 self.handle_add_virtual_line(
360 buffer_id,
361 position,
362 text,
363 fg_color,
364 bg_color,
365 above,
366 namespace,
367 priority,
368 gutter_glyph,
369 gutter_color,
370 text_overlays,
371 );
372 }
373 PluginCommand::ClearVirtualTextNamespace {
374 buffer_id,
375 namespace,
376 } => {
377 self.handle_clear_virtual_text_namespace(buffer_id, namespace);
378 }
379
380 PluginCommand::AddConceal {
382 buffer_id,
383 namespace,
384 start,
385 end,
386 replacement,
387 } => {
388 self.handle_add_conceal(buffer_id, namespace, start, end, replacement);
389 }
390 PluginCommand::ClearConcealNamespace {
391 buffer_id,
392 namespace,
393 } => {
394 self.handle_clear_conceal_namespace(buffer_id, namespace);
395 }
396 PluginCommand::ClearConcealsInRange {
397 buffer_id,
398 start,
399 end,
400 } => {
401 self.handle_clear_conceals_in_range(buffer_id, start, end);
402 }
403
404 PluginCommand::AddFold {
405 buffer_id,
406 start,
407 end,
408 placeholder,
409 } => {
410 self.handle_add_fold(buffer_id, start, end, placeholder);
411 }
412 PluginCommand::ClearFolds { buffer_id } => {
413 self.handle_clear_folds(buffer_id);
414 }
415 PluginCommand::SetFoldingRanges { buffer_id, ranges } => {
416 self.handle_set_folding_ranges(buffer_id, ranges);
417 }
418
419 PluginCommand::AddSoftBreak {
421 buffer_id,
422 namespace,
423 position,
424 indent,
425 } => {
426 self.handle_add_soft_break(buffer_id, namespace, position, indent);
427 }
428 PluginCommand::ClearSoftBreakNamespace {
429 buffer_id,
430 namespace,
431 } => {
432 self.handle_clear_soft_break_namespace(buffer_id, namespace);
433 }
434 PluginCommand::ClearSoftBreaksInRange {
435 buffer_id,
436 start,
437 end,
438 } => {
439 self.handle_clear_soft_breaks_in_range(buffer_id, start, end);
440 }
441
442 PluginCommand::AddMenuItem {
444 menu_label,
445 item,
446 position,
447 } => {
448 self.handle_add_menu_item(menu_label, item, position);
449 }
450 PluginCommand::AddMenu { menu, position } => {
451 self.handle_add_menu(menu, position);
452 }
453 PluginCommand::RemoveMenuItem {
454 menu_label,
455 item_label,
456 } => {
457 self.handle_remove_menu_item(menu_label, item_label);
458 }
459 PluginCommand::RemoveMenu { menu_label } => {
460 self.handle_remove_menu(menu_label);
461 }
462
463 PluginCommand::FocusSplit { split_id } => {
465 self.handle_focus_split(split_id);
466 }
467 PluginCommand::SetSplitBuffer {
468 split_id,
469 buffer_id,
470 } => {
471 self.handle_set_split_buffer(split_id, buffer_id);
472 }
473 PluginCommand::SetSplitScroll { split_id, top_byte } => {
474 self.handle_set_split_scroll(split_id, top_byte);
475 }
476 PluginCommand::RequestHighlights {
477 buffer_id,
478 range,
479 request_id,
480 } => {
481 self.handle_request_highlights(buffer_id, range, request_id);
482 }
483 PluginCommand::CloseSplit { split_id } => {
484 self.handle_close_split(split_id);
485 }
486 PluginCommand::SetSplitRatio { split_id, ratio } => {
487 self.handle_set_split_ratio(split_id, ratio);
488 }
489 PluginCommand::SetSplitLabel { split_id, label } => {
490 self.handle_set_split_label(split_id, label);
491 }
492 PluginCommand::ClearSplitLabel { split_id } => {
493 self.handle_clear_split_label(split_id);
494 }
495 PluginCommand::GetSplitByLabel { label, request_id } => {
496 self.handle_get_split_by_label(label, request_id);
497 }
498 PluginCommand::DistributeSplitsEvenly { split_ids: _ } => {
499 self.handle_distribute_splits_evenly();
500 }
501 PluginCommand::SetBufferCursor {
502 buffer_id,
503 position,
504 } => {
505 self.handle_set_buffer_cursor(buffer_id, position);
506 }
507 PluginCommand::SetBufferShowCursors { buffer_id, show } => {
508 self.handle_set_buffer_show_cursors(buffer_id, show);
509 }
510
511 PluginCommand::SetLayoutHints {
513 buffer_id,
514 split_id,
515 range: _,
516 hints,
517 } => {
518 self.handle_set_layout_hints(buffer_id, split_id, hints);
519 }
520 PluginCommand::SetLineNumbers { buffer_id, enabled } => {
521 self.handle_set_line_numbers(buffer_id, enabled);
522 }
523 PluginCommand::SetViewMode { buffer_id, mode } => {
524 self.handle_set_view_mode(buffer_id, &mode);
525 }
526 PluginCommand::SetLineWrap {
527 buffer_id,
528 split_id,
529 enabled,
530 } => {
531 self.handle_set_line_wrap(buffer_id, split_id, enabled);
532 }
533 PluginCommand::SubmitViewTransform {
534 buffer_id,
535 split_id,
536 payload,
537 } => {
538 self.handle_submit_view_transform(buffer_id, split_id, payload);
539 }
540 PluginCommand::ClearViewTransform {
541 buffer_id: _,
542 split_id,
543 } => {
544 self.handle_clear_view_transform(split_id);
545 }
546 PluginCommand::SetViewState {
547 buffer_id,
548 key,
549 value,
550 } => {
551 self.handle_set_view_state(buffer_id, key, value);
552 }
553 PluginCommand::SetGlobalState {
554 plugin_name,
555 key,
556 value,
557 } => {
558 self.handle_set_global_state(plugin_name, key, value);
559 }
560 PluginCommand::SetWindowState {
561 plugin_name,
562 key,
563 value,
564 } => {
565 self.handle_set_session_state(plugin_name, key, value);
566 }
567 PluginCommand::RefreshLines { buffer_id } => {
568 self.handle_refresh_lines(buffer_id);
569 }
570 PluginCommand::RefreshAllLines => {
571 self.handle_refresh_all_lines();
572 }
573 PluginCommand::HookCompleted { .. } => {
574 }
576 PluginCommand::SetLineIndicator {
577 buffer_id,
578 line,
579 namespace,
580 symbol,
581 color,
582 priority,
583 } => {
584 self.handle_set_line_indicator(buffer_id, line, namespace, symbol, color, priority);
585 }
586 PluginCommand::SetLineIndicators {
587 buffer_id,
588 lines,
589 namespace,
590 symbol,
591 color,
592 priority,
593 } => {
594 self.handle_set_line_indicators(
595 buffer_id, lines, namespace, symbol, color, priority,
596 );
597 }
598 PluginCommand::ClearLineIndicators {
599 buffer_id,
600 namespace,
601 } => {
602 self.handle_clear_line_indicators(buffer_id, namespace);
603 }
604 PluginCommand::SetFileExplorerDecorations {
605 namespace,
606 decorations,
607 } => {
608 self.active_window_mut()
609 .handle_set_file_explorer_decorations(namespace, decorations);
610 }
611 PluginCommand::ClearFileExplorerDecorations { namespace } => {
612 self.active_window_mut()
613 .handle_clear_file_explorer_decorations(&namespace);
614 }
615
616 PluginCommand::SetStatus { message } => {
618 self.handle_set_status(message);
619 }
620 PluginCommand::ApplyTheme { theme_name } => {
621 self.apply_theme(&theme_name);
622 }
623 PluginCommand::OverrideThemeColors { overrides } => {
624 self.handle_override_theme_colors(overrides);
625 }
626 PluginCommand::ReloadConfig => {
627 self.reload_config();
628 }
629 PluginCommand::SetSetting { path, value, .. } => {
630 self.handle_set_setting(path, value);
631 }
632 PluginCommand::AddPluginConfigField {
633 plugin_name,
634 field_name,
635 field_schema,
636 } => {
637 self.handle_add_plugin_config_field(plugin_name, field_name, field_schema);
638 }
639 PluginCommand::ReloadThemes { apply_theme } => {
640 self.handle_reload_themes(apply_theme);
641 }
642 PluginCommand::RegisterGrammar {
643 language,
644 grammar_path,
645 extensions,
646 } => {
647 self.handle_register_grammar(language, grammar_path, extensions);
648 }
649 PluginCommand::RegisterLanguageConfig { language, config } => {
650 self.handle_register_language_config(language, config);
651 }
652 PluginCommand::RegisterLspServer { language, config } => {
653 self.handle_register_lsp_server(language, config);
654 }
655 PluginCommand::ReloadGrammars { callback_id } => {
656 self.handle_reload_grammars(callback_id);
657 }
658 PluginCommand::CancelPrompt => {
659 self.cancel_prompt();
660 }
661 PluginCommand::StartPrompt {
662 label,
663 prompt_type,
664 floating_overlay,
665 } => {
666 self.handle_start_prompt(label, prompt_type, floating_overlay);
667 }
668 PluginCommand::StartPromptWithInitial {
669 label,
670 prompt_type,
671 initial_value,
672 floating_overlay,
673 } => {
674 self.handle_start_prompt_with_initial(
675 label,
676 prompt_type,
677 initial_value,
678 floating_overlay,
679 );
680 }
681 PluginCommand::StartPromptAsync {
682 label,
683 initial_value,
684 callback_id,
685 } => {
686 self.handle_start_prompt_async(label, initial_value, callback_id);
687 }
688 PluginCommand::AwaitNextKey { callback_id } => {
689 self.handle_await_next_key(callback_id);
690 }
691 PluginCommand::SetKeyCaptureActive { active } => {
692 self.handle_set_key_capture_active(active);
693 }
694 PluginCommand::SetPromptSuggestions {
695 suggestions,
696 selected_index,
697 } => {
698 self.handle_set_prompt_suggestions(suggestions, selected_index);
699 }
700 PluginCommand::SetPromptInputSync { sync } => {
701 self.handle_set_prompt_input_sync(sync);
702 }
703 PluginCommand::SetPromptTitle { title } => {
704 self.handle_set_prompt_title(title);
705 }
706 PluginCommand::SetPromptFooter { footer } => {
707 self.handle_set_prompt_footer(footer);
708 }
709 PluginCommand::SetPromptToolbar { spec } => {
710 self.handle_set_prompt_toolbar(spec);
711 }
712 PluginCommand::ToggleOverlayToolbarWidget { key } => {
713 self.toggle_overlay_toolbar_widget(&key);
714 }
715 PluginCommand::SetPromptStatus { status } => {
716 self.handle_set_prompt_status(status);
717 }
718 PluginCommand::SetPromptSelectedIndex { index } => {
719 self.handle_set_prompt_selected_index(index);
720 }
721
722 PluginCommand::CreateWindow { root, label } => {
725 self.handle_create_window(root, label);
726 }
727 PluginCommand::CreateWindowWithTerminal {
728 root,
729 label,
730 cwd,
731 command,
732 title,
733 request_id,
734 } => {
735 self.handle_create_window_with_terminal(
736 root, label, cwd, command, title, request_id,
737 );
738 }
739 PluginCommand::SetActiveWindow { id } => {
740 self.set_active_window(id);
741 }
742 PluginCommand::SetActiveWindowAnimated { id, from_edge } => {
743 self.set_active_window_animated(id, &from_edge);
744 }
745 PluginCommand::CloseWindow { id } => {
746 let _ = self.close_window(id);
747 }
748 PluginCommand::PrewarmWindow { id } => {
749 self.prewarm_window(id);
750 }
751
752 PluginCommand::WatchPath {
754 path,
755 recursive,
756 request_id,
757 } => {
758 self.handle_watch_path(path, recursive, request_id);
759 }
760 PluginCommand::UnwatchPath { handle } => {
761 self.file_watcher_manager.unwatch(handle);
762 }
763
764 PluginCommand::PreviewWindowInRect { id } => {
765 self.handle_preview_window_in_rect(id);
766 }
767
768 PluginCommand::RegisterCommand { command } => {
770 self.handle_register_command(command);
771 }
772 PluginCommand::RegisterStatusBarElement {
773 plugin_name,
774 token_name,
775 title,
776 } => {
777 self.handle_register_status_bar_element(plugin_name, token_name, title);
778 }
779 PluginCommand::SetStatusBarValue {
780 buffer_id,
781 key,
782 value,
783 } => {
784 self.handle_set_status_bar_value(buffer_id, key, value);
785 }
786 PluginCommand::UnregisterCommand { name } => {
787 self.handle_unregister_command(name);
788 }
789 PluginCommand::DefineMode {
790 name,
791 bindings,
792 read_only,
793 allow_text_input,
794 inherit_normal_bindings,
795 plugin_name,
796 } => {
797 self.handle_define_mode(
798 name,
799 bindings,
800 read_only,
801 allow_text_input,
802 inherit_normal_bindings,
803 plugin_name,
804 );
805 }
806
807 PluginCommand::OpenFileInBackground { path, window_id } => {
809 self.handle_open_file_in_background_routed(path, window_id);
810 }
811 PluginCommand::OpenFileAtLocation { path, line, column } => {
812 return self.handle_open_file_at_location(path, line, column);
813 }
814 PluginCommand::OpenFileInSplit {
815 split_id,
816 path,
817 line,
818 column,
819 } => {
820 return self.handle_open_file_in_split(split_id, path, line, column);
821 }
822 PluginCommand::ShowBuffer { buffer_id } => {
823 self.handle_show_buffer(buffer_id);
824 }
825 PluginCommand::CloseBuffer { buffer_id } => {
826 self.handle_close_buffer(buffer_id);
827 }
828 PluginCommand::CloseOtherBuffersInSplit {
829 buffer_id,
830 split_id,
831 } => {
832 self.handle_close_other_buffers_in_split(buffer_id, split_id);
833 }
834 PluginCommand::CloseAllBuffersInSplit { split_id } => {
835 self.handle_close_all_buffers_in_split(split_id);
836 }
837 PluginCommand::CloseBuffersToRightInSplit {
838 buffer_id,
839 split_id,
840 } => {
841 self.handle_close_buffers_to_right_in_split(buffer_id, split_id);
842 }
843 PluginCommand::CloseBuffersToLeftInSplit {
844 buffer_id,
845 split_id,
846 } => {
847 self.handle_close_buffers_to_left_in_split(buffer_id, split_id);
848 }
849
850 PluginCommand::MoveTabLeft => {
851 self.handle_move_tab_left();
852 }
853 PluginCommand::MoveTabRight => {
854 self.handle_move_tab_right();
855 }
856
857 PluginCommand::StartAnimationArea { id, rect, kind } => {
859 self.handle_start_animation_area(id, rect, kind);
860 }
861 PluginCommand::StartAnimationVirtualBuffer {
862 id,
863 buffer_id,
864 kind,
865 } => {
866 self.handle_start_animation_virtual_buffer(id, buffer_id, kind);
867 }
868 PluginCommand::CancelAnimation { id } => {
869 self.handle_cancel_animation(id);
870 }
871
872 PluginCommand::SendLspRequest {
874 language,
875 method,
876 params,
877 request_id,
878 } => {
879 self.handle_send_lsp_request(language, method, params, request_id);
880 }
881
882 PluginCommand::SetClipboard { text } => {
884 self.handle_set_clipboard(text);
885 }
886
887 PluginCommand::SpawnProcess {
889 command,
890 args,
891 cwd,
892 stdout_to,
893 callback_id,
894 } => {
895 self.handle_spawn_process(command, args, cwd, stdout_to, callback_id);
896 }
897
898 PluginCommand::SpawnHostProcess {
899 command,
900 args,
901 cwd,
902 callback_id,
903 } => {
904 self.handle_spawn_host_process(command, args, cwd, callback_id);
905 }
906
907 PluginCommand::KillHostProcess { process_id } => {
908 self.handle_kill_host_process(process_id);
909 }
910
911 PluginCommand::SetAuthority { payload } => {
912 self.handle_set_authority(payload);
913 }
914
915 PluginCommand::AttachRemoteAgent {
916 payload,
917 request_id,
918 } => {
919 self.handle_attach_remote_agent(payload, request_id);
920 }
921
922 PluginCommand::CancelRemoteAttach => {
923 self.cancel_remote_attaches();
924 }
925
926 PluginCommand::ClearAuthority => {
927 self.handle_clear_authority();
928 }
929
930 PluginCommand::SetEnv { snippet, dir } => {
931 self.handle_set_env(snippet, dir);
932 }
933
934 PluginCommand::ClearEnv => {
935 self.handle_clear_env();
936 }
937
938 PluginCommand::SetRemoteIndicatorState { state } => {
939 self.handle_set_remote_indicator_state(state);
940 }
941
942 PluginCommand::ClearRemoteIndicatorState => {
943 self.remote_indicator_override = None;
944 }
945
946 PluginCommand::SpawnProcessWait {
947 process_id,
948 callback_id,
949 } => {
950 self.handle_spawn_process_wait(process_id, callback_id);
951 }
952
953 PluginCommand::Delay {
954 callback_id,
955 duration_ms,
956 } => {
957 self.handle_delay(callback_id, duration_ms);
958 }
959
960 PluginCommand::HttpFetch {
961 url,
962 target_path,
963 callback_id,
964 } => {
965 self.handle_http_fetch(url, target_path, callback_id);
966 }
967
968 PluginCommand::SpawnBackgroundProcess {
969 process_id,
970 command,
971 args,
972 cwd,
973 callback_id,
974 } => {
975 self.handle_spawn_background_process(process_id, command, args, cwd, callback_id);
976 }
977
978 PluginCommand::KillBackgroundProcess { process_id } => {
979 self.handle_kill_background_process(process_id);
980 }
981
982 PluginCommand::CreateVirtualBuffer {
984 name,
985 mode,
986 read_only,
987 } => {
988 self.handle_create_virtual_buffer(name, mode, read_only);
989 }
990 PluginCommand::CreateVirtualBufferWithContent {
991 name,
992 mode,
993 read_only,
994 entries,
995 show_line_numbers,
996 show_cursors,
997 editing_disabled,
998 hidden_from_tabs,
999 request_id,
1000 } => {
1001 self.handle_create_virtual_buffer_with_content(
1002 name,
1003 mode,
1004 read_only,
1005 entries,
1006 show_line_numbers,
1007 show_cursors,
1008 editing_disabled,
1009 hidden_from_tabs,
1010 request_id,
1011 );
1012 }
1013 PluginCommand::CreateVirtualBufferInSplit {
1014 name,
1015 mode,
1016 read_only,
1017 entries,
1018 ratio,
1019 direction,
1020 panel_id,
1021 show_line_numbers,
1022 show_cursors,
1023 editing_disabled,
1024 line_wrap,
1025 before,
1026 role,
1027 request_id,
1028 } => {
1029 self.handle_create_virtual_buffer_in_split(
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 }
1046 PluginCommand::SetVirtualBufferContent { buffer_id, entries } => {
1047 self.handle_set_virtual_buffer_content(buffer_id, entries);
1048 }
1049 PluginCommand::GetTextPropertiesAtCursor { buffer_id } => {
1050 self.handle_get_text_properties_at_cursor(buffer_id);
1051 }
1052 PluginCommand::CreateVirtualBufferInExistingSplit {
1053 name,
1054 mode,
1055 read_only,
1056 entries,
1057 split_id,
1058 show_line_numbers,
1059 show_cursors,
1060 editing_disabled,
1061 line_wrap,
1062 request_id,
1063 } => {
1064 self.handle_create_virtual_buffer_in_existing_split(
1065 name,
1066 mode,
1067 read_only,
1068 entries,
1069 split_id,
1070 show_line_numbers,
1071 show_cursors,
1072 editing_disabled,
1073 line_wrap,
1074 request_id,
1075 );
1076 }
1077
1078 PluginCommand::SetContext { name, active } => {
1080 self.handle_set_context(name, active);
1081 }
1082
1083 PluginCommand::SetReviewDiffHunks { hunks } => {
1085 self.handle_set_review_diff_hunks(hunks);
1086 }
1087
1088 PluginCommand::ExecuteAction { action_name } => {
1090 self.handle_execute_action(action_name);
1091 }
1092 PluginCommand::ExecuteActions { actions } => {
1093 self.handle_execute_actions(actions);
1094 }
1095 PluginCommand::GetBufferText {
1096 buffer_id,
1097 start,
1098 end,
1099 request_id,
1100 } => {
1101 self.handle_get_buffer_text(buffer_id, start, end, request_id);
1102 }
1103 PluginCommand::GetLineStartPosition {
1104 buffer_id,
1105 line,
1106 request_id,
1107 } => {
1108 self.handle_get_line_start_position(buffer_id, line, request_id);
1109 }
1110 PluginCommand::GetLineEndPosition {
1111 buffer_id,
1112 line,
1113 request_id,
1114 } => {
1115 self.handle_get_line_end_position(buffer_id, line, request_id);
1116 }
1117 PluginCommand::GetBufferLineCount {
1118 buffer_id,
1119 request_id,
1120 } => {
1121 self.handle_get_buffer_line_count(buffer_id, request_id);
1122 }
1123 PluginCommand::GetCompositeCursorInfo { request_id } => {
1124 self.handle_get_composite_cursor_info(request_id);
1125 }
1126 PluginCommand::OpenFileStreaming { path, request_id } => {
1127 self.handle_open_file_streaming(path, request_id);
1128 }
1129 PluginCommand::RefreshBufferFromDisk {
1130 buffer_id,
1131 request_id,
1132 } => {
1133 self.handle_refresh_buffer_from_disk(buffer_id, request_id);
1134 }
1135 PluginCommand::SetBufferGroupPanelBuffer {
1136 group_id,
1137 panel_name,
1138 buffer_id,
1139 request_id,
1140 } => {
1141 self.handle_set_buffer_group_panel_buffer(
1142 group_id, panel_name, buffer_id, request_id,
1143 );
1144 }
1145 PluginCommand::ScrollToLineCenter {
1146 split_id,
1147 buffer_id,
1148 line,
1149 } => {
1150 self.handle_scroll_to_line_center(split_id, buffer_id, line);
1151 }
1152 PluginCommand::ScrollBufferToLine { buffer_id, line } => {
1153 self.handle_scroll_buffer_to_line(buffer_id, line);
1154 }
1155 PluginCommand::SetEditorMode { mode } => {
1156 self.handle_set_editor_mode(mode);
1157 }
1158
1159 PluginCommand::ShowActionPopup {
1161 popup_id,
1162 title,
1163 message,
1164 actions,
1165 } => {
1166 self.handle_show_action_popup(popup_id, title, message, actions);
1167 }
1168
1169 PluginCommand::SetLspMenuContributions {
1170 plugin_id,
1171 language,
1172 items,
1173 } => {
1174 self.handle_set_lsp_menu_contributions(plugin_id, language, items);
1175 }
1176
1177 PluginCommand::DisableLspForLanguage { language } => {
1178 self.handle_disable_lsp_for_language(language);
1179 }
1180
1181 PluginCommand::RestartLspForLanguage { language } => {
1182 self.handle_restart_lsp_for_language(language);
1183 }
1184
1185 PluginCommand::SetLspRootUri { language, uri } => {
1186 self.handle_set_lsp_root_uri(language, uri);
1187 }
1188
1189 PluginCommand::CreateScrollSyncGroup {
1191 group_id,
1192 left_split,
1193 right_split,
1194 } => {
1195 self.handle_create_scroll_sync_group(group_id, left_split, right_split);
1196 }
1197 PluginCommand::SetScrollSyncAnchors { group_id, anchors } => {
1198 self.handle_set_scroll_sync_anchors(group_id, anchors);
1199 }
1200 PluginCommand::RemoveScrollSyncGroup { group_id } => {
1201 self.handle_remove_scroll_sync_group(group_id);
1202 }
1203
1204 PluginCommand::CreateCompositeBuffer {
1206 name,
1207 mode,
1208 layout,
1209 sources,
1210 hunks,
1211 initial_focus_hunk,
1212 request_id,
1213 } => {
1214 self.handle_create_composite_buffer(
1215 name,
1216 mode,
1217 layout,
1218 sources,
1219 hunks,
1220 initial_focus_hunk,
1221 request_id,
1222 );
1223 }
1224 PluginCommand::UpdateCompositeAlignment { buffer_id, hunks } => {
1225 self.handle_update_composite_alignment(buffer_id, hunks);
1226 }
1227 PluginCommand::CloseCompositeBuffer { buffer_id } => {
1228 self.active_window_mut().close_composite_buffer(buffer_id);
1229 }
1230 PluginCommand::FlushLayout => {
1231 self.flush_layout();
1232 }
1233 PluginCommand::CompositeNextHunk { buffer_id } => {
1234 self.handle_composite_next_hunk(buffer_id);
1235 }
1236 PluginCommand::CompositePrevHunk { buffer_id } => {
1237 self.handle_composite_prev_hunk(buffer_id);
1238 }
1239
1240 PluginCommand::CreateBufferGroup {
1242 name,
1243 mode,
1244 layout_json,
1245 request_id,
1246 } => {
1247 self.handle_create_buffer_group(name, mode, layout_json, request_id);
1248 }
1249 PluginCommand::SetPanelContent {
1250 group_id,
1251 panel_name,
1252 entries,
1253 } => {
1254 self.set_panel_content(group_id, panel_name, entries);
1255 }
1256 PluginCommand::CloseBufferGroup { group_id } => {
1257 self.close_buffer_group(group_id);
1258 }
1259 PluginCommand::FocusPanel {
1260 group_id,
1261 panel_name,
1262 } => {
1263 self.focus_panel(group_id, panel_name);
1264 }
1265
1266 PluginCommand::SaveBufferToPath { buffer_id, path } => {
1268 self.handle_save_buffer_to_path(buffer_id, path);
1269 }
1270
1271 #[cfg(feature = "plugins")]
1273 PluginCommand::LoadPlugin { path, callback_id } => {
1274 self.handle_load_plugin(path, callback_id);
1275 }
1276 #[cfg(feature = "plugins")]
1277 PluginCommand::UnloadPlugin { name, callback_id } => {
1278 self.handle_unload_plugin(name, callback_id);
1279 }
1280 #[cfg(feature = "plugins")]
1281 PluginCommand::ReloadPlugin { name, callback_id } => {
1282 self.handle_reload_plugin(name, callback_id);
1283 }
1284 #[cfg(feature = "plugins")]
1285 PluginCommand::ListPlugins { callback_id } => {
1286 self.handle_list_plugins(callback_id);
1287 }
1288 #[cfg(not(feature = "plugins"))]
1290 PluginCommand::LoadPlugin { .. }
1291 | PluginCommand::UnloadPlugin { .. }
1292 | PluginCommand::ReloadPlugin { .. }
1293 | PluginCommand::ListPlugins { .. } => {
1294 tracing::warn!("Plugin management commands require the 'plugins' feature");
1295 }
1296
1297 PluginCommand::CreateTerminal {
1299 cwd,
1300 direction,
1301 ratio,
1302 focus,
1303 persistent,
1304 window_id,
1305 command,
1306 title,
1307 request_id,
1308 } => {
1309 self.handle_create_terminal(
1310 cwd, direction, ratio, focus, persistent, window_id, command, title, request_id,
1311 );
1312 }
1313
1314 PluginCommand::SendTerminalInput { terminal_id, data } => {
1315 self.handle_send_terminal_input(terminal_id, data);
1316 }
1317
1318 PluginCommand::CloseTerminal { terminal_id } => {
1319 self.handle_close_terminal(terminal_id);
1320 }
1321
1322 PluginCommand::SignalWindow { id, signal } => {
1323 self.handle_signal_window(id, &signal);
1324 }
1325
1326 PluginCommand::GrepProject {
1327 pattern,
1328 fixed_string,
1329 case_sensitive,
1330 max_results,
1331 whole_words,
1332 callback_id,
1333 } => {
1334 self.handle_grep_project(
1335 pattern,
1336 fixed_string,
1337 case_sensitive,
1338 max_results,
1339 whole_words,
1340 callback_id,
1341 );
1342 }
1343
1344 PluginCommand::BeginSearch {
1345 pattern,
1346 fixed_string,
1347 case_sensitive,
1348 max_results,
1349 whole_words,
1350 source_buffer_id,
1351 handle_id,
1352 } => {
1353 self.handle_begin_search(
1354 pattern,
1355 fixed_string,
1356 case_sensitive,
1357 max_results,
1358 whole_words,
1359 source_buffer_id,
1360 handle_id,
1361 );
1362 }
1363
1364 PluginCommand::ReplaceInBuffer {
1365 file_path,
1366 buffer_id,
1367 matches,
1368 replacement,
1369 callback_id,
1370 } => {
1371 self.handle_replace_in_buffer(
1372 file_path,
1373 buffer_id,
1374 matches,
1375 replacement,
1376 callback_id,
1377 );
1378 }
1379
1380 PluginCommand::MountWidgetPanel {
1381 panel_id,
1382 buffer_id,
1383 spec,
1384 } => {
1385 self.handle_mount_widget_panel(panel_id, buffer_id, spec);
1386 }
1387
1388 PluginCommand::UpdateWidgetPanel { panel_id, spec } => {
1389 self.handle_update_widget_panel(panel_id, spec);
1390 }
1391
1392 PluginCommand::UnmountWidgetPanel { panel_id } => {
1393 self.handle_unmount_widget_panel(panel_id);
1394 }
1395
1396 PluginCommand::WidgetCommand { panel_id, action } => {
1397 self.handle_widget_command(panel_id, action);
1398 }
1399
1400 PluginCommand::WidgetMutate { panel_id, mutation } => {
1401 self.handle_widget_mutate(panel_id, mutation);
1402 }
1403
1404 PluginCommand::MountFloatingWidget {
1405 panel_id,
1406 spec,
1407 width_pct,
1408 height_pct,
1409 as_dock,
1410 } => {
1411 self.handle_mount_floating_widget(panel_id, spec, width_pct, height_pct, as_dock);
1412 }
1413
1414 PluginCommand::UpdateFloatingWidget { panel_id, spec } => {
1415 self.handle_update_floating_widget(panel_id, spec);
1416 }
1417
1418 PluginCommand::UnmountFloatingWidget { panel_id } => {
1419 self.handle_unmount_floating_widget(panel_id);
1420 }
1421
1422 PluginCommand::FloatingPanelControl { panel_id, op, arg } => {
1423 self.handle_floating_panel_control(panel_id, &op, arg);
1424 }
1425 }
1426 Ok(())
1427 }
1428
1429 fn handle_watch_path(&mut self, path: std::path::PathBuf, recursive: bool, request_id: u64) {
1432 let result = if let Some(ref bridge) = self.async_bridge {
1433 self.file_watcher_manager.watch(bridge, &path, recursive)
1434 } else {
1435 Err(
1436 "watchPath: no async bridge — file watching is unavailable in this build"
1437 .to_string(),
1438 )
1439 };
1440 self.last_watch_response_for_test = Some((request_id, result.clone()));
1441 self.send_plugin_response(fresh_core::api::PluginResponse::WatchPathRegistered {
1442 request_id,
1443 result,
1444 });
1445 }
1446
1447 fn handle_set_env(&mut self, snippet: String, dir: Option<String>) {
1448 use crate::services::workspace_trust::TrustLevel;
1452 if self.authority.workspace_trust.level() == TrustLevel::Trusted {
1453 self.authority
1454 .env_provider
1455 .set(snippet, dir.map(std::path::PathBuf::from));
1456 self.request_restart(self.working_dir().to_path_buf());
1458 } else {
1459 self.active_window_mut().status_message =
1460 Some("Workspace not trusted — cannot activate environment".to_string());
1461 }
1462 }
1463
1464 fn handle_clear_env(&mut self) {
1465 let was_active = self.authority.env_provider.is_active();
1466 self.authority.env_provider.clear();
1467 if was_active {
1468 self.request_restart(self.working_dir().to_path_buf());
1469 }
1470 }
1471
1472 fn handle_open_file_in_background_routed(
1473 &mut self,
1474 path: std::path::PathBuf,
1475 window_id: Option<fresh_core::WindowId>,
1476 ) {
1477 let route_to_inactive =
1478 window_id.filter(|&id| id != self.active_window && self.windows.contains_key(&id));
1479 if let Some(target) = route_to_inactive {
1480 self.handle_open_file_in_inactive_session(target, path);
1481 } else {
1482 self.handle_open_file_in_background(path);
1483 }
1484 }
1485
1486 fn handle_set_split_label(&mut self, split_id: SplitId, label: String) {
1489 self.windows
1490 .get_mut(&self.active_window)
1491 .and_then(|w| w.split_manager_mut())
1492 .expect("active window must have a populated split layout")
1493 .set_label(LeafId(split_id), label);
1494 }
1495
1496 fn handle_clear_split_label(&mut self, split_id: SplitId) {
1497 self.windows
1498 .get_mut(&self.active_window)
1499 .and_then(|w| w.split_manager_mut())
1500 .expect("active window must have a populated split layout")
1501 .clear_label(split_id);
1502 }
1503
1504 fn handle_reload_themes(&mut self, apply_theme: Option<String>) {
1505 self.reload_themes();
1506 if let Some(theme_name) = apply_theme {
1507 self.apply_theme(&theme_name);
1508 }
1509 }
1510
1511 fn handle_set_key_capture_active(&mut self, active: bool) {
1512 self.active_window_mut().key_capture_active = active;
1513 if !active {
1514 self.active_window_mut().pending_key_capture_buffer.clear();
1517 }
1518 }
1519
1520 fn handle_set_prompt_input_sync(&mut self, sync: bool) {
1521 if let Some(prompt) = &mut self.active_window_mut().prompt {
1522 prompt.sync_input_on_navigate = sync;
1523 }
1524 }
1525
1526 fn handle_set_prompt_title(&mut self, title: Vec<fresh_core::api::StyledText>) {
1527 if let Some(prompt) = &mut self.active_window_mut().prompt {
1528 prompt.title = title;
1529 }
1530 }
1531
1532 fn handle_set_prompt_footer(&mut self, footer: Vec<fresh_core::api::StyledText>) {
1533 if let Some(prompt) = &mut self.active_window_mut().prompt {
1534 prompt.footer = footer;
1535 }
1536 }
1537
1538 fn handle_set_prompt_toolbar(&mut self, spec: Option<fresh_core::api::WidgetSpec>) {
1539 if let Some(prompt) = &mut self.active_window_mut().prompt {
1540 prompt.toolbar_widget = spec;
1541 }
1542 }
1543
1544 fn handle_set_prompt_status(&mut self, status: String) {
1545 if let Some(prompt) = &mut self.active_window_mut().prompt {
1546 prompt.status = status;
1547 }
1548 }
1549
1550 fn handle_set_prompt_selected_index(&mut self, index: u32) {
1551 if let Some(prompt) = &mut self.active_window_mut().prompt {
1552 let len = prompt.suggestions.len();
1553 if len > 0 {
1554 prompt.selected_suggestion = Some((index as usize).min(len - 1));
1555 }
1556 }
1557 }
1558
1559 fn handle_create_window(&mut self, root: std::path::PathBuf, label: String) {
1560 if !root.is_absolute() {
1561 tracing::warn!(
1562 "CreateWindow rejected: root must be absolute, got {:?}",
1563 root
1564 );
1565 } else {
1566 let _ = self.create_window_at(root, label);
1567 }
1568 }
1569
1570 fn handle_preview_window_in_rect(&mut self, id: Option<fresh_core::WindowId>) {
1571 self.preview_window_id = match id {
1574 Some(sid) if sid != self.active_window && self.windows.contains_key(&sid) => Some(sid),
1575 _ => None,
1576 };
1577 }
1578
1579 fn handle_register_status_bar_element(
1580 &mut self,
1581 plugin_name: String,
1582 token_name: String,
1583 title: String,
1584 ) {
1585 if let Err(e) = self.register_status_bar_element(&plugin_name, &token_name, &title) {
1586 tracing::warn!("Failed to register statusbar element: {}", e);
1587 }
1588 }
1589
1590 fn handle_set_status_bar_value(&mut self, buffer_id: u64, key: String, value: String) {
1591 if let Err(e) =
1592 self.set_status_bar_value(fresh_core::BufferId(buffer_id as usize), &key, value)
1593 {
1594 tracing::debug!("Skipped statusbar value for stale buffer: {}", e);
1597 }
1598 }
1599
1600 fn handle_cancel_animation(&mut self, id: u64) {
1601 self.active_window_mut()
1602 .animations
1603 .cancel(crate::view::animation::AnimationId::from_raw(id));
1604 }
1605
1606 fn handle_clear_authority(&mut self) {
1607 tracing::info!("Plugin cleared authority; restoring local");
1608 self.clear_authority();
1609 }
1610
1611 fn handle_set_review_diff_hunks(&mut self, hunks: Vec<fresh_core::api::ReviewHunk>) {
1612 self.active_window_mut().review_hunks = hunks;
1613 tracing::debug!(
1614 "Set {} review hunks",
1615 self.active_window_mut().review_hunks.len()
1616 );
1617 }
1618
1619 fn handle_composite_next_hunk(&mut self, buffer_id: fresh_core::BufferId) {
1620 let split_id = self.split_manager().active_split();
1621 self.active_window_mut()
1622 .composite_next_hunk(split_id, buffer_id);
1623 }
1624
1625 fn handle_composite_prev_hunk(&mut self, buffer_id: fresh_core::BufferId) {
1626 let split_id = self.split_manager().active_split();
1627 self.active_window_mut()
1628 .composite_prev_hunk(split_id, buffer_id);
1629 }
1630
1631 fn configure_vbuf_display(
1637 &mut self,
1638 buffer_id: crate::model::event::BufferId,
1639 show_line_numbers: bool,
1640 show_cursors: bool,
1641 editing_disabled: bool,
1642 ) {
1643 if let Some(state) = self
1644 .windows
1645 .get_mut(&self.active_window)
1646 .map(|w| &mut w.buffers)
1647 .expect("active window present")
1648 .get_mut(&buffer_id)
1649 {
1650 state.margins.configure_for_line_numbers(show_line_numbers);
1651 state.show_cursors = show_cursors;
1652 state.editing_disabled = editing_disabled;
1653 }
1654 }
1655
1656 #[allow(clippy::too_many_arguments)]
1661 fn route_vbuf_to_existing_dock(
1662 &mut self,
1663 dock_leaf: crate::model::event::LeafId,
1664 name: String,
1665 mode: String,
1666 read_only: bool,
1667 entries: Vec<fresh_core::text_property::TextPropertyEntry>,
1668 panel_id: Option<&str>,
1669 show_line_numbers: bool,
1670 show_cursors: bool,
1671 editing_disabled: bool,
1672 request_id: Option<u64>,
1673 ) {
1674 let source_split_before_create = self.split_manager().active_split();
1677 let buffer_id =
1678 self.active_window_mut()
1679 .create_virtual_buffer(name.clone(), mode, read_only);
1680 self.configure_vbuf_display(buffer_id, show_line_numbers, show_cursors, editing_disabled);
1681 if let Some(pid) = panel_id {
1682 self.panel_ids_mut().insert(pid.to_string(), buffer_id);
1683 }
1684 if let Err(e) = self.set_virtual_buffer_content(buffer_id, entries) {
1685 tracing::error!("Failed to set virtual buffer content (dock route): {}", e);
1686 return;
1687 }
1688 self.split_manager_mut().set_active_split(dock_leaf);
1690 self.active_window_mut()
1691 .set_pane_buffer(dock_leaf, buffer_id);
1692 if dock_leaf != source_split_before_create {
1694 if let Some(source_view_state) = self
1695 .windows
1696 .get_mut(&self.active_window)
1697 .and_then(|w| w.split_view_states_mut())
1698 .expect("active window must have a populated split layout")
1699 .get_mut(&source_split_before_create)
1700 {
1701 source_view_state.remove_buffer(buffer_id);
1702 }
1703 }
1704 if let Some(req_id) = request_id {
1705 let result = fresh_core::api::VirtualBufferResult {
1706 buffer_id: buffer_id.0 as u64,
1707 split_id: Some(dock_leaf.0 .0 as u64),
1708 };
1709 self.plugin_manager.read().unwrap().resolve_callback(
1710 fresh_core::api::JsCallbackId::from(req_id),
1711 serde_json::to_string(&result).unwrap_or_default(),
1712 );
1713 }
1714 tracing::info!(
1715 "Routed virtual buffer '{}' into existing utility dock {:?}",
1716 name,
1717 dock_leaf
1718 );
1719 }
1720
1721 fn update_existing_vbuf_panel(
1724 &mut self,
1725 existing_buffer_id: crate::model::event::BufferId,
1726 entries: Vec<fresh_core::text_property::TextPropertyEntry>,
1727 request_id: Option<u64>,
1728 panel_name: &str,
1729 ) {
1730 match self.set_virtual_buffer_content(existing_buffer_id, entries) {
1731 Ok(()) => tracing::info!("Updated existing panel '{}' content", panel_name),
1732 Err(e) => tracing::error!("Failed to update panel content: {}", e),
1733 }
1734 let splits = self.split_manager().splits_for_buffer(existing_buffer_id);
1735 if let Some(&split_id) = splits.first() {
1736 self.split_manager_mut().set_active_split(split_id);
1737 self.active_window_mut()
1739 .set_pane_buffer(split_id, existing_buffer_id);
1740 tracing::debug!("Focused split {:?} containing panel buffer", split_id);
1741 }
1742 if let Some(req_id) = request_id {
1743 let result = fresh_core::api::VirtualBufferResult {
1744 buffer_id: existing_buffer_id.0 as u64,
1745 split_id: splits.first().map(|s| s.0 .0 as u64),
1746 };
1747 self.plugin_manager.read().unwrap().resolve_callback(
1748 fresh_core::api::JsCallbackId::from(req_id),
1749 serde_json::to_string(&result).unwrap_or_default(),
1750 );
1751 }
1752 }
1753
1754 fn handle_get_line_position(
1762 &mut self,
1763 buffer_id: crate::model::event::BufferId,
1764 line: u32,
1765 request_id: u64,
1766 want_end: bool,
1767 ) {
1768 let actual_buffer_id = self.resolve_buffer_id(buffer_id);
1769 let result = self
1770 .windows
1771 .get_mut(&self.active_window)
1772 .map(|w| &mut w.buffers)
1773 .expect("active window present")
1774 .get_mut(&actual_buffer_id)
1775 .and_then(|state| {
1776 let len = state.buffer.len();
1777 let content = state.get_text_range(0, len);
1778 buffer_line_byte_offset(&content, len, line as usize, want_end)
1779 });
1780 self.resolve_json_callback(request_id, result);
1781 }
1782
1783 fn handle_save_buffer_to_path(&mut self, buffer_id: BufferId, path: std::path::PathBuf) {
1785 if let Some(state) = self
1786 .windows
1787 .get_mut(&self.active_window)
1788 .map(|w| &mut w.buffers)
1789 .expect("active window present")
1790 .get_mut(&buffer_id)
1791 {
1792 match state.buffer.save_to_file(&path) {
1794 Ok(()) => {
1795 if let Err(e) = self.finalize_save(Some(path)) {
1798 tracing::warn!("Failed to finalize save: {}", e);
1799 }
1800 tracing::debug!("Saved buffer {:?} to path", buffer_id);
1801 }
1802 Err(e) => {
1803 self.handle_set_status(format!("Error saving: {}", e));
1804 tracing::error!("Failed to save buffer to path: {}", e);
1805 }
1806 }
1807 } else {
1808 self.handle_set_status(format!("Buffer {:?} not found", buffer_id));
1809 tracing::warn!("SaveBufferToPath: buffer {:?} not found", buffer_id);
1810 }
1811 }
1812
1813 #[cfg(feature = "plugins")]
1815 fn handle_load_plugin(&mut self, path: std::path::PathBuf, callback_id: JsCallbackId) {
1816 let load_result = self.plugin_manager.read().unwrap().load_plugin(&path);
1817 match load_result {
1818 Ok(()) => {
1819 tracing::info!("Loaded plugin from {:?}", path);
1820 self.plugin_manager
1821 .read()
1822 .unwrap()
1823 .resolve_callback(callback_id, "true".to_string());
1824 }
1825 Err(e) => {
1826 tracing::error!("Failed to load plugin from {:?}: {}", path, e);
1827 self.plugin_manager
1828 .read()
1829 .unwrap()
1830 .reject_callback(callback_id, format!("{}", e));
1831 }
1832 }
1833 }
1834
1835 #[cfg(feature = "plugins")]
1837 fn handle_unload_plugin(&mut self, name: String, callback_id: JsCallbackId) {
1838 let result = self.plugin_manager.write().unwrap().unload_plugin(&name);
1841 match result {
1842 Ok(()) => {
1843 tracing::info!("Unloaded plugin: {}", name);
1844 if let Ok(mut schemas) = self.plugin_schemas.write() {
1845 schemas.remove(&name);
1846 }
1847 self.plugin_manager
1848 .read()
1849 .unwrap()
1850 .resolve_callback(callback_id, "true".to_string());
1851 }
1852 Err(e) => {
1853 tracing::error!("Failed to unload plugin '{}': {}", name, e);
1854 self.plugin_manager
1855 .read()
1856 .unwrap()
1857 .reject_callback(callback_id, format!("{}", e));
1858 }
1859 }
1860 }
1861
1862 #[cfg(feature = "plugins")]
1864 fn handle_reload_plugin(&mut self, name: String, callback_id: JsCallbackId) {
1865 let path = self
1869 .plugin_manager
1870 .read()
1871 .unwrap()
1872 .list_plugins()
1873 .into_iter()
1874 .find(|p| p.name == name)
1875 .map(|p| p.path);
1876 let _ = path; let reload_result = self.plugin_manager.read().unwrap().reload_plugin(&name);
1878 match reload_result {
1879 Ok(()) => {
1880 tracing::info!("Reloaded plugin: {}", name);
1881 self.plugin_manager
1882 .read()
1883 .unwrap()
1884 .resolve_callback(callback_id, "true".to_string());
1885 }
1886 Err(e) => {
1887 tracing::error!("Failed to reload plugin '{}': {}", name, e);
1888 self.plugin_manager
1889 .read()
1890 .unwrap()
1891 .reject_callback(callback_id, format!("{}", e));
1892 }
1893 }
1894 }
1895
1896 #[cfg(feature = "plugins")]
1898 fn handle_list_plugins(&mut self, callback_id: JsCallbackId) {
1899 let plugins = self.plugin_manager.read().unwrap().list_plugins();
1900 let json_array: Vec<serde_json::Value> = plugins
1902 .iter()
1903 .map(|p| {
1904 serde_json::json!({
1905 "name": p.name,
1906 "path": p.path.to_string_lossy(),
1907 "enabled": p.enabled
1908 })
1909 })
1910 .collect();
1911 let json_str = serde_json::to_string(&json_array).unwrap_or_else(|_| "[]".to_string());
1912 self.plugin_manager
1913 .read()
1914 .unwrap()
1915 .resolve_callback(callback_id, json_str);
1916 }
1917
1918 fn handle_execute_action(&mut self, action_name: String) {
1920 use crate::input::keybindings::Action;
1921 use std::collections::HashMap;
1922
1923 if let Some(action) = Action::from_str(&action_name, &HashMap::new()) {
1925 if let Err(e) = self.handle_action(action) {
1927 tracing::warn!("Failed to execute action '{}': {}", action_name, e);
1928 } else {
1929 tracing::debug!("Executed action: {}", action_name);
1930 }
1931 } else {
1932 tracing::warn!("Unknown action: {}", action_name);
1933 }
1934 }
1935
1936 fn handle_execute_actions(&mut self, actions: Vec<fresh_core::api::ActionSpec>) {
1939 use crate::input::keybindings::Action;
1940 use std::collections::HashMap;
1941
1942 for action_spec in actions {
1943 if let Some(action) = Action::from_str(&action_spec.action, &HashMap::new()) {
1944 for _ in 0..action_spec.count {
1946 if let Err(e) = self.handle_action(action.clone()) {
1947 tracing::warn!("Failed to execute action '{}': {}", action_spec.action, e);
1948 return; }
1950 }
1951 tracing::debug!(
1952 "Executed action '{}' {} time(s)",
1953 action_spec.action,
1954 action_spec.count
1955 );
1956 } else {
1957 tracing::warn!("Unknown action: {}", action_spec.action);
1958 return; }
1960 }
1961 }
1962
1963 fn handle_get_buffer_text(
1968 &mut self,
1969 buffer_id: BufferId,
1970 start: usize,
1971 end: usize,
1972 request_id: u64,
1973 ) {
1974 let result = if let Some(state) = self
1975 .windows
1976 .get_mut(&self.active_window)
1977 .map(|w| &mut w.buffers)
1978 .expect("active window present")
1979 .get_mut(&buffer_id)
1980 {
1981 let (start, end) = clamp_buffer_text_range(start, end, state.buffer.len());
1989 Ok(state.get_text_range(start, end))
1990 } else {
1991 Err(format!("Buffer {:?} not found", buffer_id))
1992 };
1993
1994 let callback_id = fresh_core::api::JsCallbackId::from(request_id);
1996 match result {
1997 Ok(text) => {
1998 let json = serde_json::to_string(&text).unwrap_or_else(|_| "null".to_string());
2000 self.plugin_manager
2001 .read()
2002 .unwrap()
2003 .resolve_callback(callback_id, json);
2004 }
2005 Err(error) => {
2006 self.plugin_manager
2007 .read()
2008 .unwrap()
2009 .reject_callback(callback_id, error);
2010 }
2011 }
2012 }
2013
2014 fn handle_set_editor_mode(&mut self, mode: Option<String>) {
2016 self.active_window_mut().editor_mode = mode.clone();
2017 tracing::debug!("Set editor mode: {:?}", mode);
2018 }
2019
2020 fn resolve_buffer_id(&self, buffer_id: BufferId) -> BufferId {
2022 if buffer_id.0 == 0 {
2023 self.active_buffer()
2024 } else {
2025 buffer_id
2026 }
2027 }
2028
2029 fn resolve_json_callback<T: serde::Serialize>(&mut self, request_id: u64, value: T) {
2031 let callback_id = fresh_core::api::JsCallbackId::from(request_id);
2032 let json = serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string());
2033 self.plugin_manager
2034 .read()
2035 .unwrap()
2036 .resolve_callback(callback_id, json);
2037 }
2038
2039 fn handle_get_line_start_position(&mut self, buffer_id: BufferId, line: u32, request_id: u64) {
2041 self.handle_get_line_position(buffer_id, line, request_id, false);
2042 }
2043
2044 fn handle_get_line_end_position(&mut self, buffer_id: BufferId, line: u32, request_id: u64) {
2047 self.handle_get_line_position(buffer_id, line, request_id, true);
2048 }
2049
2050 fn handle_get_buffer_line_count(&mut self, buffer_id: BufferId, request_id: u64) {
2052 let actual_buffer_id = self.resolve_buffer_id(buffer_id);
2053
2054 let result = if let Some(state) = self
2055 .windows
2056 .get_mut(&self.active_window)
2057 .map(|w| &mut w.buffers)
2058 .expect("active window present")
2059 .get_mut(&actual_buffer_id)
2060 {
2061 let buffer_len = state.buffer.len();
2062 let content = state.get_text_range(0, buffer_len);
2063 let newlines = content.bytes().filter(|&b| b == b'\n').count();
2064 Some(if content.is_empty() {
2065 1
2066 } else {
2067 newlines + usize::from(!content.ends_with('\n'))
2068 })
2069 } else {
2070 None
2071 };
2072
2073 self.resolve_json_callback(request_id, result);
2074 }
2075
2076 fn handle_get_composite_cursor_info(&mut self, request_id: u64) {
2082 let info = self.active_window().active_composite_cursor_info();
2083 let value = info.map(|(focused_pane, pane_count, lines)| {
2084 serde_json::json!({
2085 "focusedPane": focused_pane,
2086 "paneCount": pane_count,
2087 "lines": lines,
2088 })
2089 });
2090 self.resolve_json_callback(request_id, value);
2091 }
2092
2093 fn handle_open_file_streaming(&mut self, path: std::path::PathBuf, request_id: u64) {
2110 if !self.authority.filesystem.exists(&path) {
2113 if let Some(parent) = path.parent() {
2114 if !parent.as_os_str().is_empty() {
2115 if let Err(e) = std::fs::create_dir_all(parent) {
2116 tracing::warn!(
2117 "openFileStreaming: failed to create parent dir {:?}: {}",
2118 parent,
2119 e
2120 );
2121 self.resolve_json_callback::<Option<u64>>(request_id, None);
2122 return;
2123 }
2124 }
2125 }
2126 if let Err(e) = std::fs::write(&path, b"") {
2127 tracing::warn!(
2128 "openFileStreaming: failed to create empty file at {:?}: {}",
2129 path,
2130 e
2131 );
2132 self.resolve_json_callback::<Option<u64>>(request_id, None);
2133 return;
2134 }
2135 }
2136
2137 let buffer_id = match self.open_file_no_focus(&path) {
2141 Ok(id) => id,
2142 Err(e) => {
2143 tracing::warn!(
2144 "openFileStreaming: open_file_no_focus failed for {:?}: {}",
2145 path,
2146 e
2147 );
2148 self.resolve_json_callback::<Option<u64>>(request_id, None);
2149 return;
2150 }
2151 };
2152
2153 if let Some(meta) = self.active_window_mut().buffer_metadata.get_mut(&buffer_id) {
2158 meta.hidden_from_tabs = true;
2159 meta.auto_revert_enabled = false;
2160 }
2161 let active_split = self
2162 .windows
2163 .get(&self.active_window)
2164 .and_then(|w| w.buffers.splits())
2165 .map(|(mgr, _)| mgr)
2166 .expect("active window must have a populated split layout")
2167 .active_split();
2168 if let Some(vs) = self
2169 .windows
2170 .get_mut(&self.active_window)
2171 .and_then(|w| w.split_view_states_mut())
2172 .expect("active window must have a populated split layout")
2173 .get_mut(&active_split)
2174 {
2175 use crate::view::split::TabTarget;
2176 vs.open_buffers
2177 .retain(|t| !matches!(t, TabTarget::Buffer(b) if *b == buffer_id));
2178 }
2179
2180 self.resolve_json_callback(request_id, Some(buffer_id.0));
2181 }
2182
2183 fn handle_set_buffer_group_panel_buffer(
2186 &mut self,
2187 group_id: usize,
2188 panel_name: String,
2189 buffer_id: BufferId,
2190 request_id: u64,
2191 ) {
2192 let actual_buffer_id = self.resolve_buffer_id(buffer_id);
2193 let ok = self.set_buffer_group_panel_buffer(group_id, panel_name, actual_buffer_id);
2194 self.resolve_json_callback(request_id, ok);
2195 }
2196
2197 fn handle_refresh_buffer_from_disk(&mut self, buffer_id: BufferId, request_id: u64) {
2201 let actual_buffer_id = self.resolve_buffer_id(buffer_id);
2202
2203 let path = self
2204 .windows
2205 .get(&self.active_window)
2206 .and_then(|w| w.buffers.splits())
2207 .map(|(_, _)| ())
2208 .and_then(|_| {
2209 self.windows
2210 .get(&self.active_window)?
2211 .buffers
2212 .get(&actual_buffer_id)?
2213 .buffer
2214 .file_path()
2215 .map(|p| p.to_path_buf())
2216 });
2217
2218 let Some(path) = path else {
2219 self.resolve_json_callback::<Option<usize>>(request_id, None);
2221 return;
2222 };
2223
2224 let new_size = match self.authority.filesystem.metadata(&path) {
2225 Ok(m) => m.size as usize,
2226 Err(_) => {
2227 self.resolve_json_callback::<Option<usize>>(request_id, None);
2228 return;
2229 }
2230 };
2231
2232 let new_total = if let Some(state) = self
2233 .windows
2234 .get_mut(&self.active_window)
2235 .map(|w| &mut w.buffers)
2236 .expect("active window present")
2237 .get_mut(&actual_buffer_id)
2238 {
2239 let old = state.buffer.total_bytes();
2240 if new_size > old {
2241 state.buffer.extend_streaming(&path, new_size);
2242 }
2243 state.buffer.total_bytes()
2244 } else {
2245 self.resolve_json_callback::<Option<usize>>(request_id, None);
2246 return;
2247 };
2248
2249 self.resolve_json_callback(request_id, Some(new_total));
2250 }
2251
2252 fn handle_scroll_to_line_center(
2254 &mut self,
2255 split_id: SplitId,
2256 buffer_id: BufferId,
2257 line: usize,
2258 ) {
2259 let actual_split_id = if split_id.0 == 0 {
2260 self.windows
2261 .get(&self.active_window)
2262 .and_then(|w| w.buffers.splits())
2263 .map(|(mgr, _)| mgr)
2264 .expect("active window must have a populated split layout")
2265 .active_split()
2266 } else {
2267 LeafId(split_id)
2268 };
2269 let actual_buffer_id = self.resolve_buffer_id(buffer_id);
2270
2271 let viewport_height = if let Some(view_state) = self
2273 .windows
2274 .get(&self.active_window)
2275 .and_then(|w| w.buffers.splits())
2276 .map(|(_, vs)| vs)
2277 .expect("active window must have a populated split layout")
2278 .get(&actual_split_id)
2279 {
2280 view_state.viewport.height as usize
2281 } else {
2282 return;
2283 };
2284
2285 let lines_above = viewport_height / 2;
2287 let target_line = line.saturating_sub(lines_above);
2288
2289 self.active_window_mut().scroll_split_viewport_to(
2290 actual_buffer_id,
2291 actual_split_id,
2292 target_line,
2293 true,
2294 );
2295 }
2296
2297 fn handle_scroll_buffer_to_line(&mut self, buffer_id: BufferId, line: usize) {
2307 if !self
2308 .windows
2309 .get(&self.active_window)
2310 .map(|w| &w.buffers)
2311 .expect("active window present")
2312 .contains_key(&buffer_id)
2313 {
2314 return;
2315 }
2316
2317 let mut target_leaves: Vec<LeafId> = Vec::new();
2319
2320 for leaf_id in self
2322 .windows
2323 .get(&self.active_window)
2324 .and_then(|w| w.buffers.splits())
2325 .map(|(mgr, _)| mgr)
2326 .expect("active window must have a populated split layout")
2327 .root()
2328 .leaf_split_ids()
2329 {
2330 if let Some(vs) = self
2331 .windows
2332 .get(&self.active_window)
2333 .and_then(|w| w.buffers.splits())
2334 .map(|(_, vs)| vs)
2335 .expect("active window must have a populated split layout")
2336 .get(&leaf_id)
2337 {
2338 if vs.active_buffer == buffer_id {
2339 target_leaves.push(leaf_id);
2340 }
2341 }
2342 }
2343
2344 for (_group_leaf_id, node) in self.active_window().grouped_subtrees.iter() {
2346 if let crate::view::split::SplitNode::Grouped { layout, .. } = node {
2347 for inner_leaf in layout.leaf_split_ids() {
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(&inner_leaf)
2355 {
2356 if vs.active_buffer == buffer_id && !target_leaves.contains(&inner_leaf) {
2357 target_leaves.push(inner_leaf);
2358 }
2359 }
2360 }
2361 }
2362 }
2363
2364 if target_leaves.is_empty() {
2365 return;
2366 }
2367
2368 self.active_window_mut()
2369 .scroll_buffer_to_line_in_splits(buffer_id, &target_leaves, line);
2370 }
2371
2372 fn handle_spawn_host_process(
2373 &mut self,
2374 command: String,
2375 args: Vec<String>,
2376 cwd: Option<String>,
2377 callback_id: JsCallbackId,
2378 ) {
2379 if let (Some(runtime), Some(bridge)) = (&self.tokio_runtime, &self.async_bridge) {
2394 use tokio::io::{AsyncReadExt, BufReader};
2395 use tokio::process::Command as TokioCommand;
2396
2397 let effective_cwd = cwd.or_else(|| {
2398 std::env::current_dir()
2399 .map(|p| p.to_string_lossy().to_string())
2400 .ok()
2401 });
2402 let sender = bridge.sender();
2403 let process_id = callback_id.as_u64();
2404
2405 if let crate::services::workspace_trust::SpawnDecision::Deny(reason) = self
2412 .authority
2413 .workspace_trust
2414 .decide(&command, effective_cwd.as_deref())
2415 {
2416 #[allow(clippy::let_underscore_must_use)]
2417 let _ = sender.send(AsyncMessage::PluginProcessOutput {
2418 process_id,
2419 stdout: String::new(),
2420 stderr: reason,
2421 exit_code: -1,
2422 });
2423 return;
2424 }
2425
2426 let (kill_tx, mut kill_rx) = tokio::sync::oneshot::channel::<()>();
2427 self.host_process_handles.insert(process_id, kill_tx);
2428
2429 runtime.spawn(async move {
2430 use crate::services::process_hidden::HideWindow;
2431 let mut cmd = TokioCommand::new(&command);
2432 cmd.args(&args);
2433 cmd.stdout(std::process::Stdio::piped());
2434 cmd.stderr(std::process::Stdio::piped());
2435 cmd.hide_window();
2436 if let Some(ref dir) = effective_cwd {
2437 cmd.current_dir(dir);
2438 }
2439 let mut child = match cmd.spawn() {
2440 Ok(c) => c,
2441 Err(e) => {
2442 #[allow(clippy::let_underscore_must_use)]
2443 let _ = sender.send(AsyncMessage::PluginProcessOutput {
2444 process_id,
2445 stdout: String::new(),
2446 stderr: e.to_string(),
2447 exit_code: -1,
2448 });
2449 return;
2450 }
2451 };
2452
2453 let stdout_pipe = child.stdout.take();
2459 let stderr_pipe = child.stderr.take();
2460
2461 let stdout_fut = async {
2462 let mut buf = String::new();
2463 if let Some(s) = stdout_pipe {
2464 #[allow(clippy::let_underscore_must_use)]
2465 let _ = BufReader::new(s).read_to_string(&mut buf).await;
2466 }
2467 buf
2468 };
2469 let stderr_fut = async {
2470 let mut buf = String::new();
2471 if let Some(s) = stderr_pipe {
2472 #[allow(clippy::let_underscore_must_use)]
2473 let _ = BufReader::new(s).read_to_string(&mut buf).await;
2474 }
2475 buf
2476 };
2477 let wait_fut = async {
2478 tokio::select! {
2479 status = child.wait() => {
2480 status.map(|s| s.code().unwrap_or(-1)).unwrap_or(-1)
2481 }
2482 _ = &mut kill_rx => {
2483 #[allow(clippy::let_underscore_must_use)]
2487 let _ = child.start_kill();
2488 child
2489 .wait()
2490 .await
2491 .map(|s| s.code().unwrap_or(-1))
2492 .unwrap_or(-1)
2493 }
2494 }
2495 };
2496 let (stdout, stderr, exit_code) = tokio::join!(stdout_fut, stderr_fut, wait_fut);
2497
2498 #[allow(clippy::let_underscore_must_use)]
2499 let _ = sender.send(AsyncMessage::PluginProcessOutput {
2500 process_id,
2501 stdout,
2502 stderr,
2503 exit_code,
2504 });
2505 });
2506 } else {
2507 self.plugin_manager
2508 .read()
2509 .unwrap()
2510 .reject_callback(callback_id, "Async runtime not available".to_string());
2511 }
2512 }
2513
2514 fn handle_spawn_background_process(
2515 &mut self,
2516 process_id: u64,
2517 command: String,
2518 args: Vec<String>,
2519 cwd: Option<String>,
2520 callback_id: JsCallbackId,
2521 ) {
2522 if let (Some(runtime), Some(bridge)) = (&self.tokio_runtime, &self.async_bridge) {
2524 use tokio::io::{AsyncBufReadExt, BufReader};
2525 use tokio::process::Command as TokioCommand;
2526
2527 let effective_cwd = cwd.unwrap_or_else(|| {
2528 std::env::current_dir()
2529 .map(|p| p.to_string_lossy().to_string())
2530 .unwrap_or_else(|_| ".".to_string())
2531 });
2532
2533 let sender = bridge.sender();
2534 let sender_stdout = sender.clone();
2535 let sender_stderr = sender.clone();
2536 let callback_id_u64 = callback_id.as_u64();
2537
2538 #[allow(clippy::let_underscore_must_use)]
2540 let handle = runtime.spawn(async move {
2541 use crate::services::process_hidden::HideWindow;
2542 let mut child = match TokioCommand::new(&command)
2543 .args(&args)
2544 .current_dir(&effective_cwd)
2545 .stdout(std::process::Stdio::piped())
2546 .stderr(std::process::Stdio::piped())
2547 .hide_window()
2548 .spawn()
2549 {
2550 Ok(child) => child,
2551 Err(e) => {
2552 let _ = sender.send(crate::services::async_bridge::AsyncMessage::Plugin(
2553 fresh_core::api::PluginAsyncMessage::ProcessExit {
2554 process_id,
2555 callback_id: callback_id_u64,
2556 exit_code: -1,
2557 },
2558 ));
2559 tracing::error!("Failed to spawn background process: {}", e);
2560 return;
2561 }
2562 };
2563
2564 let stdout = child.stdout.take();
2566 let stderr = child.stderr.take();
2567 let pid = process_id;
2568
2569 if let Some(stdout) = stdout {
2571 let sender = sender_stdout;
2572 tokio::spawn(async move {
2573 let reader = BufReader::new(stdout);
2574 let mut lines = reader.lines();
2575 while let Ok(Some(line)) = lines.next_line().await {
2576 let _ =
2577 sender.send(crate::services::async_bridge::AsyncMessage::Plugin(
2578 fresh_core::api::PluginAsyncMessage::ProcessStdout {
2579 process_id: pid,
2580 data: line + "\n",
2581 },
2582 ));
2583 }
2584 });
2585 }
2586
2587 if let Some(stderr) = stderr {
2589 let sender = sender_stderr;
2590 tokio::spawn(async move {
2591 let reader = BufReader::new(stderr);
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::ProcessStderr {
2597 process_id: pid,
2598 data: line + "\n",
2599 },
2600 ));
2601 }
2602 });
2603 }
2604
2605 let exit_code = match child.wait().await {
2607 Ok(status) => status.code().unwrap_or(-1),
2608 Err(_) => -1,
2609 };
2610
2611 let _ = sender.send(crate::services::async_bridge::AsyncMessage::Plugin(
2612 fresh_core::api::PluginAsyncMessage::ProcessExit {
2613 process_id,
2614 callback_id: callback_id_u64,
2615 exit_code,
2616 },
2617 ));
2618 });
2619
2620 self.background_process_handles
2622 .insert(process_id, handle.abort_handle());
2623 } else {
2624 self.plugin_manager
2626 .read()
2627 .unwrap()
2628 .reject_callback(callback_id, "Async runtime not available".to_string());
2629 }
2630 }
2631
2632 #[allow(clippy::too_many_arguments)]
2633 fn handle_create_virtual_buffer_with_content(
2634 &mut self,
2635 name: String,
2636 mode: String,
2637 read_only: bool,
2638 entries: Vec<fresh_core::text_property::TextPropertyEntry>,
2639 show_line_numbers: bool,
2640 show_cursors: bool,
2641 editing_disabled: bool,
2642 hidden_from_tabs: bool,
2643 request_id: Option<u64>,
2644 ) {
2645 let buffer_id =
2646 self.active_window_mut()
2647 .create_virtual_buffer(name.clone(), mode.clone(), read_only);
2648 tracing::info!(
2649 "Created virtual buffer '{}' with mode '{}' (id={:?})",
2650 name,
2651 mode,
2652 buffer_id
2653 );
2654
2655 self.configure_vbuf_display(buffer_id, show_line_numbers, show_cursors, editing_disabled);
2661 let active_split = self.split_manager().active_split();
2662 if let Some(view_state) = self
2663 .windows
2664 .get_mut(&self.active_window)
2665 .and_then(|w| w.split_view_states_mut())
2666 .expect("active window must have a populated split layout")
2667 .get_mut(&active_split)
2668 {
2669 view_state.ensure_buffer_state(buffer_id).show_line_numbers = show_line_numbers;
2670 }
2671
2672 if hidden_from_tabs {
2674 if let Some(meta) = self.active_window_mut().buffer_metadata.get_mut(&buffer_id) {
2675 meta.hidden_from_tabs = true;
2676 }
2677 }
2678
2679 match self.set_virtual_buffer_content(buffer_id, entries) {
2681 Ok(()) => {
2682 tracing::debug!("Set virtual buffer content for {:?}", buffer_id);
2683 self.set_active_buffer(buffer_id);
2685 tracing::debug!("Switched to virtual buffer {:?}", buffer_id);
2686
2687 if let Some(req_id) = request_id {
2689 tracing::info!(
2690 "CreateVirtualBufferWithContent: resolving callback for request_id={}, buffer_id={:?}",
2691 req_id,
2692 buffer_id
2693 );
2694 let result = fresh_core::api::VirtualBufferResult {
2696 buffer_id: buffer_id.0 as u64,
2697 split_id: None,
2698 };
2699 self.plugin_manager.read().unwrap().resolve_callback(
2700 fresh_core::api::JsCallbackId::from(req_id),
2701 serde_json::to_string(&result).unwrap_or_default(),
2702 );
2703 tracing::info!(
2704 "CreateVirtualBufferWithContent: resolve_callback sent for request_id={}",
2705 req_id
2706 );
2707 }
2708 }
2709 Err(e) => {
2710 tracing::error!("Failed to set virtual buffer content: {}", e);
2711 }
2712 }
2713 }
2714
2715 #[allow(clippy::too_many_arguments)]
2716 fn handle_create_virtual_buffer_in_split(
2717 &mut self,
2718 name: String,
2719 mode: String,
2720 read_only: bool,
2721 entries: Vec<fresh_core::text_property::TextPropertyEntry>,
2722 ratio: f32,
2723 direction: Option<String>,
2724 panel_id: Option<String>,
2725 show_line_numbers: bool,
2726 show_cursors: bool,
2727 editing_disabled: bool,
2728 line_wrap: Option<bool>,
2729 before: bool,
2730 role: Option<String>,
2731 request_id: Option<u64>,
2732 ) {
2733 let split_role: Option<crate::view::split::SplitRole> = match role.as_deref() {
2736 Some("utility_dock") => Some(crate::view::split::SplitRole::UtilityDock),
2737 _ => None,
2738 };
2739
2740 if let Some(dock_leaf) = split_role.and_then(|r| self.split_manager().find_leaf_by_role(r))
2744 {
2745 return self.route_vbuf_to_existing_dock(
2746 dock_leaf,
2747 name,
2748 mode,
2749 read_only,
2750 entries,
2751 panel_id.as_deref(),
2752 show_line_numbers,
2753 show_cursors,
2754 editing_disabled,
2755 request_id,
2756 );
2757 }
2760
2761 if let Some(pid) = panel_id.as_deref() {
2764 let maybe_existing = self.panel_ids().get(pid).copied();
2765 if let Some(existing_id) = maybe_existing {
2766 let buffer_alive = self
2767 .windows
2768 .get(&self.active_window)
2769 .map(|w| w.buffers.contains_key(&existing_id))
2770 .unwrap_or(false);
2771 if buffer_alive {
2772 return self.update_existing_vbuf_panel(existing_id, entries, request_id, pid);
2773 }
2774 tracing::warn!(
2776 "Removing stale panel_id '{}' pointing to non-existent buffer {:?}",
2777 pid,
2778 existing_id
2779 );
2780 self.panel_ids_mut().remove(pid);
2781 }
2782 }
2783
2784 let source_split_before_create = self.split_manager().active_split();
2791
2792 let buffer_id =
2793 self.active_window_mut()
2794 .create_virtual_buffer(name.clone(), mode.clone(), read_only);
2795 tracing::info!(
2796 "Created virtual buffer '{}' with mode '{}' in split (id={:?})",
2797 name,
2798 mode,
2799 buffer_id
2800 );
2801
2802 self.configure_vbuf_display(buffer_id, show_line_numbers, show_cursors, editing_disabled);
2803
2804 if let Some(pid) = panel_id {
2805 self.panel_ids_mut().insert(pid, buffer_id);
2806 }
2807
2808 if let Err(e) = self.set_virtual_buffer_content(buffer_id, entries) {
2809 tracing::error!("Failed to set virtual buffer content: {}", e);
2810 return;
2811 }
2812
2813 let split_dir = match direction.as_deref() {
2814 Some("vertical") => crate::model::event::SplitDirection::Vertical,
2815 _ => crate::model::event::SplitDirection::Horizontal,
2816 };
2817
2818 let split_result = if split_role == Some(crate::view::split::SplitRole::UtilityDock) {
2823 self.split_manager_mut()
2824 .split_root_positioned(split_dir, buffer_id, ratio, before)
2825 } else {
2826 self.split_manager_mut()
2827 .split_active_positioned(split_dir, buffer_id, ratio, before)
2828 };
2829
2830 let created_split_id = match split_result {
2831 Ok(new_split_id) => {
2832 if new_split_id != source_split_before_create {
2836 if let Some(src_vs) = self
2837 .windows
2838 .get_mut(&self.active_window)
2839 .and_then(|w| w.split_view_states_mut())
2840 .expect("active window must have a populated split layout")
2841 .get_mut(&source_split_before_create)
2842 {
2843 src_vs.remove_buffer(buffer_id);
2844 }
2845 }
2846
2847 let mut view_state = SplitViewState::with_buffer(
2848 self.terminal_width,
2849 self.terminal_height,
2850 buffer_id,
2851 );
2852 view_state.apply_config_defaults(
2853 self.config.editor.line_numbers,
2854 self.config.editor.highlight_current_line,
2855 line_wrap.unwrap_or_else(|| {
2856 self.active_window().resolve_line_wrap_for_buffer(buffer_id)
2857 }),
2858 self.config.editor.wrap_indent,
2859 self.active_window()
2860 .resolve_wrap_column_for_buffer(buffer_id),
2861 self.config.editor.rulers.clone(),
2862 self.config.editor.scroll_offset,
2863 );
2864 view_state.ensure_buffer_state(buffer_id).show_line_numbers = show_line_numbers;
2865 self.windows
2866 .get_mut(&self.active_window)
2867 .and_then(|w| w.split_view_states_mut())
2868 .expect("active window must have a populated split layout")
2869 .insert(new_split_id, view_state);
2870
2871 self.split_manager_mut().set_active_split(new_split_id);
2872
2873 if let Some(target_role) = split_role {
2877 self.split_manager_mut().clear_role(target_role);
2878 self.split_manager_mut()
2879 .set_leaf_role(new_split_id, Some(target_role));
2880 tracing::info!(
2881 "Tagged new dock leaf {:?} with role {:?}",
2882 new_split_id,
2883 target_role
2884 );
2885 }
2886
2887 tracing::info!(
2888 "Created {:?} split with virtual buffer {:?}",
2889 split_dir,
2890 buffer_id
2891 );
2892 Some(new_split_id)
2893 }
2894 Err(e) => {
2895 tracing::error!("Failed to create split: {}", e);
2896 self.set_active_buffer(buffer_id);
2897 None
2898 }
2899 };
2900
2901 if let Some(req_id) = request_id {
2902 tracing::trace!(
2903 "CreateVirtualBufferInSplit: resolving callback for request_id={}, \
2904 buffer_id={:?}, split_id={:?}",
2905 req_id,
2906 buffer_id,
2907 created_split_id
2908 );
2909 let result = fresh_core::api::VirtualBufferResult {
2910 buffer_id: buffer_id.0 as u64,
2911 split_id: created_split_id.map(|s| s.0 .0 as u64),
2912 };
2913 self.plugin_manager.read().unwrap().resolve_callback(
2914 fresh_core::api::JsCallbackId::from(req_id),
2915 serde_json::to_string(&result).unwrap_or_default(),
2916 );
2917 }
2918 }
2919
2920 #[allow(clippy::too_many_arguments)]
2921 fn handle_create_virtual_buffer_in_existing_split(
2922 &mut self,
2923 name: String,
2924 mode: String,
2925 read_only: bool,
2926 entries: Vec<fresh_core::text_property::TextPropertyEntry>,
2927 split_id: SplitId,
2928 show_line_numbers: bool,
2929 show_cursors: bool,
2930 editing_disabled: bool,
2931 line_wrap: Option<bool>,
2932 request_id: Option<u64>,
2933 ) {
2934 let buffer_id =
2936 self.active_window_mut()
2937 .create_virtual_buffer(name.clone(), mode.clone(), read_only);
2938 tracing::info!(
2939 "Created virtual buffer '{}' with mode '{}' for existing split {:?} (id={:?})",
2940 name,
2941 mode,
2942 split_id,
2943 buffer_id
2944 );
2945
2946 self.configure_vbuf_display(buffer_id, show_line_numbers, show_cursors, editing_disabled);
2947
2948 if let Err(e) = self.set_virtual_buffer_content(buffer_id, entries) {
2949 tracing::error!("Failed to set virtual buffer content: {}", e);
2950 return;
2951 }
2952
2953 let leaf_id = LeafId(split_id);
2956 self.windows
2957 .get_mut(&self.active_window)
2958 .and_then(|w| w.split_manager_mut())
2959 .expect("active window must have a populated split layout")
2960 .set_active_split(leaf_id);
2961 self.active_window_mut().set_pane_buffer(leaf_id, buffer_id);
2962
2963 if let Some(view_state) = self
2969 .windows
2970 .get_mut(&self.active_window)
2971 .and_then(|w| w.split_view_states_mut())
2972 .expect("active window must have a populated split layout")
2973 .get_mut(&leaf_id)
2974 {
2975 view_state.switch_buffer(buffer_id);
2976 view_state.add_buffer(buffer_id);
2977 view_state.ensure_buffer_state(buffer_id).show_line_numbers = show_line_numbers;
2978
2979 if let Some(wrap) = line_wrap {
2981 view_state.active_state_mut().viewport.line_wrap_enabled = wrap;
2982 }
2983 }
2984
2985 tracing::info!(
2986 "Displayed virtual buffer {:?} in split {:?}",
2987 buffer_id,
2988 split_id
2989 );
2990
2991 if let Some(req_id) = request_id {
2993 let result = fresh_core::api::VirtualBufferResult {
2994 buffer_id: buffer_id.0 as u64,
2995 split_id: Some(split_id.0 as u64),
2996 };
2997 self.plugin_manager.read().unwrap().resolve_callback(
2998 fresh_core::api::JsCallbackId::from(req_id),
2999 serde_json::to_string(&result).unwrap_or_default(),
3000 );
3001 }
3002 }
3003
3004 fn handle_show_action_popup(
3005 &mut self,
3006 popup_id: String,
3007 title: String,
3008 message: String,
3009 actions: Vec<fresh_core::api::ActionPopupAction>,
3010 ) {
3011 tracing::info!(
3012 "Action popup requested: id={}, title={}, actions={}",
3013 popup_id,
3014 title,
3015 actions.len()
3016 );
3017
3018 let items: Vec<crate::model::event::PopupListItemData> = actions
3020 .iter()
3021 .map(|action| crate::model::event::PopupListItemData {
3022 text: action.label.clone(),
3023 detail: None,
3024 icon: None,
3025 data: Some(action.id.clone()),
3026 })
3027 .collect();
3028
3029 drop(actions);
3034
3035 let popup_data = crate::model::event::PopupData {
3037 kind: crate::model::event::PopupKindHint::List,
3038 title: Some(title),
3039 description: Some(message),
3040 transient: false,
3041 content: crate::model::event::PopupContentData::List { items, selected: 0 },
3042 position: crate::model::event::PopupPositionData::BottomRight,
3043 width: 60,
3044 max_height: 15,
3045 bordered: true,
3046 };
3047
3048 let mut popup_obj = crate::state::convert_popup_data_to_popup(&popup_data);
3058 popup_obj.resolver = crate::view::popup::PopupResolver::PluginAction {
3059 popup_id: popup_id.clone(),
3060 };
3061
3062 {
3069 let theme = self.theme();
3070 popup_obj.background_style = ratatui::style::Style::default().bg(theme.popup_bg);
3071 popup_obj.border_style = ratatui::style::Style::default().fg(theme.popup_border_fg);
3072 }
3073
3074 while self
3086 .active_state()
3087 .popups
3088 .top()
3089 .is_some_and(|p| matches!(p.resolver, crate::view::popup::PopupResolver::LspStatus))
3090 {
3091 self.active_state_mut().popups.hide();
3092 }
3093
3094 let existing_idx = self.global_popups.all().iter().position(|p| {
3101 matches!(
3102 &p.resolver,
3103 crate::view::popup::PopupResolver::PluginAction { popup_id: id } if id == &popup_id,
3104 )
3105 });
3106 if let Some(idx) = existing_idx {
3107 if let Some(slot) = self.global_popups.get_mut(idx) {
3108 *slot = popup_obj;
3109 }
3110 } else {
3111 self.global_popups.show(popup_obj);
3112 }
3113 tracing::info!(
3114 "Action popup shown: id={}, stack_depth={}",
3115 popup_id,
3116 self.global_popups.all().len()
3117 );
3118 }
3119
3120 fn handle_set_lsp_menu_contributions(
3130 &mut self,
3131 plugin_id: String,
3132 language: String,
3133 items: Vec<fresh_core::api::LspMenuItem>,
3134 ) {
3135 let key = (language.clone(), plugin_id.clone());
3136 if items.is_empty() {
3137 self.active_window_mut().lsp_menu_contributions.remove(&key);
3138 } else {
3139 self.active_window_mut()
3140 .lsp_menu_contributions
3141 .insert(key, items);
3142 }
3143 self.refresh_lsp_status_popup_if_open();
3148 }
3149
3150 fn handle_create_window_with_terminal(
3151 &mut self,
3152 root: std::path::PathBuf,
3153 label: String,
3154 cwd: Option<String>,
3155 command: Option<Vec<String>>,
3156 title: Option<String>,
3157 request_id: u64,
3158 ) {
3159 let callback_id = JsCallbackId::from(request_id);
3160 if !root.is_absolute() {
3161 let msg = format!(
3162 "createWindowWithTerminal: root must be absolute, got {:?}",
3163 root
3164 );
3165 tracing::warn!("{}", msg);
3166 self.plugin_manager
3167 .read()
3168 .unwrap()
3169 .reject_callback(callback_id, msg);
3170 return;
3171 }
3172 let cwd_buf = cwd.map(std::path::PathBuf::from);
3173 match self.create_window_with_terminal(root, label, cwd_buf, command, title) {
3174 Ok((window_id, terminal_id, buffer_id)) => {
3175 let api_result = fresh_core::api::SessionWithTerminalResult {
3176 window_id: window_id.0,
3177 terminal_id: terminal_id.0 as u64,
3178 buffer_id: buffer_id.0 as u64,
3179 };
3180 self.plugin_manager.read().unwrap().resolve_callback(
3181 callback_id,
3182 serde_json::to_string(&api_result).unwrap_or_default(),
3183 );
3184 }
3185 Err(e) => {
3186 tracing::error!("createWindowWithTerminal failed: {e}");
3187 self.plugin_manager
3188 .read()
3189 .unwrap()
3190 .reject_callback(callback_id, format!("createWindowWithTerminal: {e}"));
3191 }
3192 }
3193 }
3194
3195 #[allow(clippy::too_many_arguments)]
3196 fn handle_create_terminal(
3197 &mut self,
3198 cwd: Option<String>,
3199 direction: Option<String>,
3200 ratio: Option<f32>,
3201 focus: Option<bool>,
3202 persistent: bool,
3203 target_session_id: Option<fresh_core::WindowId>,
3204 command: Option<Vec<String>>,
3205 title: Option<String>,
3206 request_id: u64,
3207 ) {
3208 let target_id = target_session_id
3215 .filter(|id| self.windows.contains_key(id))
3216 .unwrap_or(self.active_window);
3217 let is_active_target = target_id == self.active_window;
3218
3219 let cwd_buf = cwd.map(std::path::PathBuf::from);
3220 let split_direction = direction.as_deref().map(|d| match d {
3221 "horizontal" => crate::model::event::SplitDirection::Horizontal,
3222 _ => crate::model::event::SplitDirection::Vertical,
3223 });
3224
3225 let prev_active = if is_active_target {
3233 Some(self.active_window().active_buffer())
3234 } else {
3235 None
3236 };
3237
3238 let result = {
3239 let target = self
3240 .windows
3241 .get_mut(&target_id)
3242 .expect("target window present (existence checked above)");
3243 target.create_plugin_terminal(
3244 cwd_buf,
3245 split_direction,
3246 ratio,
3247 focus.unwrap_or(true),
3248 persistent,
3249 command,
3250 title.filter(|t| !t.is_empty()),
3251 )
3252 };
3253 match result {
3254 Ok((terminal_id, buffer_id, created_split_id)) => {
3255 if is_active_target {
3256 let new_active = self.active_window().active_buffer();
3257 if prev_active != Some(new_active) {
3258 #[cfg(feature = "plugins")]
3259 self.update_plugin_state_snapshot();
3260 #[cfg(feature = "plugins")]
3261 self.plugin_manager.read().unwrap().run_hook(
3262 "buffer_activated",
3263 crate::services::plugins::hooks::HookArgs::BufferActivated {
3264 buffer_id: new_active,
3265 },
3266 );
3267 }
3268 }
3269 let api_result = fresh_core::api::TerminalResult {
3270 buffer_id: buffer_id.0 as u64,
3271 terminal_id: terminal_id.0 as u64,
3272 split_id: created_split_id.map(|s| s.0 .0 as u64),
3273 };
3274 self.plugin_manager.read().unwrap().resolve_callback(
3275 fresh_core::api::JsCallbackId::from(request_id),
3276 serde_json::to_string(&api_result).unwrap_or_default(),
3277 );
3278 tracing::info!(
3279 "Plugin created terminal {:?} with buffer {:?} in window {:?}",
3280 terminal_id,
3281 buffer_id,
3282 target_id
3283 );
3284 }
3285 Err(e) => {
3286 tracing::error!("Failed to create terminal for plugin: {e}");
3287 self.plugin_manager.read().unwrap().reject_callback(
3288 fresh_core::api::JsCallbackId::from(request_id),
3289 format!("Failed to create terminal: {e}"),
3290 );
3291 }
3292 }
3293 }
3294
3295 fn handle_get_split_by_label(&mut self, label: String, request_id: u64) {
3298 let split_id = self
3299 .windows
3300 .get(&self.active_window)
3301 .and_then(|w| w.buffers.splits())
3302 .map(|(mgr, _)| mgr)
3303 .expect("active window must have a populated split layout")
3304 .find_split_by_label(&label);
3305 let callback_id = fresh_core::api::JsCallbackId::from(request_id);
3306 let json =
3307 serde_json::to_string(&split_id.map(|s| s.0 .0)).unwrap_or_else(|_| "null".to_string());
3308 self.plugin_manager
3309 .read()
3310 .unwrap()
3311 .resolve_callback(callback_id, json);
3312 }
3313
3314 fn handle_set_buffer_show_cursors(&mut self, buffer_id: BufferId, show: bool) {
3315 if let Some(state) = self
3316 .windows
3317 .get_mut(&self.active_window)
3318 .map(|w| &mut w.buffers)
3319 .expect("active window present")
3320 .get_mut(&buffer_id)
3321 {
3322 state.show_cursors = show;
3323 state.cursor_visibility_locked = true;
3326 } else {
3327 tracing::warn!("SetBufferShowCursors: buffer {:?} not found", buffer_id);
3328 }
3329 }
3330
3331 fn handle_override_theme_colors(
3332 &mut self,
3333 overrides: std::collections::HashMap<String, [u8; 3]>,
3334 ) {
3335 let pairs = overrides
3336 .into_iter()
3337 .map(|(k, [r, g, b])| (k, ratatui::style::Color::Rgb(r, g, b)));
3338 let applied = self.theme.write().unwrap().override_colors(pairs);
3339 if applied > 0 {
3340 self.reapply_all_overlays();
3343 }
3344 }
3345
3346 fn handle_await_next_key(&mut self, callback_id: fresh_core::api::JsCallbackId) {
3347 if let Some(payload) = self
3351 .active_window_mut()
3352 .pending_key_capture_buffer
3353 .pop_front()
3354 {
3355 let json = serde_json::to_string(&payload).unwrap_or_else(|_| "null".to_string());
3356 self.plugin_manager
3357 .read()
3358 .unwrap()
3359 .resolve_callback(callback_id, json);
3360 } else {
3361 self.active_window_mut()
3362 .pending_next_key_callbacks
3363 .push_back(callback_id);
3364 }
3365 }
3366
3367 fn handle_spawn_process(
3368 &mut self,
3369 command: String,
3370 args: Vec<String>,
3371 cwd: Option<String>,
3372 stdout_to: Option<std::path::PathBuf>,
3373 callback_id: fresh_core::api::JsCallbackId,
3374 ) {
3375 if let (Some(runtime), Some(bridge)) = (&self.tokio_runtime, &self.async_bridge) {
3376 let effective_cwd = cwd.or_else(|| {
3377 std::env::current_dir()
3378 .map(|p| p.to_string_lossy().to_string())
3379 .ok()
3380 });
3381 let sender = bridge.sender();
3382 let spawner = self.authority.process_spawner.clone();
3383
3384 let process_id = callback_id.as_u64();
3389 let (kill_tx, kill_rx) = tokio::sync::oneshot::channel::<()>();
3390 self.host_process_handles.insert(process_id, kill_tx);
3391
3392 runtime.spawn(async move {
3393 #[allow(clippy::let_underscore_must_use)]
3394 let outcome = spawner
3395 .spawn_cancellable(command, args, effective_cwd, stdout_to, kill_rx)
3396 .await;
3397 match outcome {
3398 Ok(result) => {
3399 #[allow(clippy::let_underscore_must_use)]
3400 let _ = sender.send(AsyncMessage::PluginProcessOutput {
3401 process_id,
3402 stdout: result.stdout,
3403 stderr: result.stderr,
3404 exit_code: result.exit_code,
3405 });
3406 }
3407 Err(e) => {
3408 #[allow(clippy::let_underscore_must_use)]
3409 let _ = sender.send(AsyncMessage::PluginProcessOutput {
3410 process_id,
3411 stdout: String::new(),
3412 stderr: e.to_string(),
3413 exit_code: -1,
3414 });
3415 }
3416 }
3417 });
3418 } else {
3419 self.plugin_manager
3420 .read()
3421 .unwrap()
3422 .reject_callback(callback_id, "Async runtime not available".to_string());
3423 }
3424 }
3425
3426 fn handle_kill_host_process(&mut self, process_id: u64) {
3427 if let Some(tx) = self.host_process_handles.remove(&process_id) {
3431 #[allow(clippy::let_underscore_must_use)]
3432 let _ = tx.send(());
3433 tracing::debug!("KillHostProcess: sent kill for process_id={}", process_id);
3434 } else {
3435 tracing::debug!(
3436 "KillHostProcess: unknown process_id={} (already exited?)",
3437 process_id
3438 );
3439 }
3440 }
3441
3442 fn handle_set_authority(&mut self, payload: serde_json::Value) {
3443 match serde_json::from_value::<crate::services::authority::AuthorityPayload>(payload) {
3446 Ok(parsed) => {
3447 let trust = std::sync::Arc::clone(&self.authority.workspace_trust);
3450 let env = std::sync::Arc::clone(&self.authority.env_provider);
3451 match crate::services::authority::Authority::from_plugin_payload(parsed, trust, env)
3452 {
3453 Ok(auth) => {
3454 tracing::info!("Plugin installed new authority");
3455 self.install_authority(auth);
3456 }
3457 Err(e) => {
3458 tracing::warn!("setAuthority: invalid payload: {}", e);
3459 self.set_status_message(format!("setAuthority rejected: {}", e));
3460 }
3461 }
3462 }
3463 Err(e) => {
3464 tracing::warn!("setAuthority: failed to parse payload: {}", e);
3465 self.set_status_message(format!("setAuthority rejected: {}", e));
3466 }
3467 }
3468 }
3469
3470 fn handle_attach_remote_agent(&mut self, payload: serde_json::Value, request_id: u64) {
3471 let spec =
3474 match serde_json::from_value::<crate::services::authority::RemoteAgentSpec>(payload) {
3475 Ok(spec) => spec,
3476 Err(e) => {
3477 tracing::warn!("attachRemoteAgent: invalid payload: {}", e);
3478 self.reject_remote_attach(request_id, format!("invalid attach spec: {e}"));
3479 return;
3480 }
3481 };
3482
3483 let runtime = self.tokio_runtime.clone();
3486 let sender = self.async_bridge.as_ref().map(|b| b.sender());
3487 let (Some(runtime), Some(sender)) = (runtime, sender) else {
3488 self.reject_remote_attach(request_id, "async runtime not available".to_string());
3489 return;
3490 };
3491
3492 self.remote_attach_inflight.insert(request_id);
3497 let (cancel_tx, cancel_rx) = tokio::sync::oneshot::channel::<()>();
3498 self.remote_attach_cancels.insert(request_id, cancel_tx);
3499
3500 let window_mode = spec.window;
3504 let window_label = spec.label.clone();
3505 let window_command = spec.command.clone();
3506 let trust = std::sync::Arc::clone(&self.authority.workspace_trust);
3507 let env = std::sync::Arc::clone(&self.authority.env_provider);
3508
3509 use crate::services::authority::RemoteTransportSpec;
3516 let base_env = spec.base_env.clone();
3517 let mode_for = |label: &str| {
3518 if window_mode {
3519 crate::services::async_bridge::RemoteAttachMode::Window {
3520 label: window_label.clone().unwrap_or_else(|| label.to_string()),
3521 command: window_command.clone(),
3522 }
3523 } else {
3524 crate::services::async_bridge::RemoteAttachMode::Restart
3525 }
3526 };
3527
3528 match spec.transport {
3529 RemoteTransportSpec::KubectlExec { .. } => {
3530 let (target, base_env) = spec.into_kube_target();
3531 let label = target.display();
3532 let workspace = target.workspace.clone().map(std::path::PathBuf::from);
3534 let mode = mode_for(&label);
3535 self.set_status_message(format!("Connecting to {label}…"));
3536 runtime.spawn(async move {
3537 let outcome = crate::services::authority::connect_kube_authority(
3538 target,
3539 base_env,
3540 trust,
3541 env,
3542 Some(cancel_rx),
3543 )
3544 .await;
3545 let msg = match outcome {
3546 Ok((authority, keepalive)) => AsyncMessage::RemoteAttachReady(
3547 crate::services::async_bridge::RemoteAttachReady {
3548 authority,
3549 keepalive: Box::new(keepalive),
3550 working_dir: workspace,
3551 mode,
3552 request_id,
3553 },
3554 ),
3555 Err(e) => AsyncMessage::RemoteAttachFailed {
3556 error: e.to_string(),
3557 request_id,
3558 },
3559 };
3560 #[allow(clippy::let_underscore_must_use)]
3561 let _ = sender.send(msg);
3562 });
3563 }
3564 RemoteTransportSpec::Ssh {
3565 user,
3566 host,
3567 port,
3568 identity_file,
3569 remote_path,
3570 extra_args,
3571 } => {
3572 let _ = base_env; let params = crate::services::remote::ConnectionParams {
3574 user: user.clone().filter(|u| !u.is_empty()),
3575 host: host.clone(),
3576 port,
3577 identity_file: identity_file.map(std::path::PathBuf::from),
3578 extra_args,
3579 };
3580 let target = params.ssh_target();
3582 let label = match port {
3583 Some(p) => format!("ssh:{target}:{p}"),
3584 None => format!("ssh:{target}"),
3585 };
3586 let workspace = remote_path.clone().map(std::path::PathBuf::from);
3587 let mode = mode_for(&label);
3588 self.set_status_message(format!("Connecting to {label}…"));
3589 runtime.spawn(async move {
3590 let outcome = crate::services::authority::connect_ssh_authority(
3591 params,
3592 remote_path,
3593 trust,
3594 env,
3595 Some(cancel_rx),
3596 )
3597 .await;
3598 let msg = match outcome {
3599 Ok((authority, keepalive)) => AsyncMessage::RemoteAttachReady(
3600 crate::services::async_bridge::RemoteAttachReady {
3601 authority,
3602 keepalive: Box::new(keepalive),
3603 working_dir: workspace,
3604 mode,
3605 request_id,
3606 },
3607 ),
3608 Err(e) => AsyncMessage::RemoteAttachFailed {
3609 error: e.to_string(),
3610 request_id,
3611 },
3612 };
3613 #[allow(clippy::let_underscore_must_use)]
3614 let _ = sender.send(msg);
3615 });
3616 }
3617 }
3618 }
3619
3620 fn handle_set_remote_indicator_state(&mut self, state: serde_json::Value) {
3621 match serde_json::from_value::<crate::view::ui::status_bar::RemoteIndicatorOverride>(state)
3624 {
3625 Ok(over) => {
3626 self.remote_indicator_override = Some(over);
3627 }
3628 Err(e) => {
3629 tracing::warn!("setRemoteIndicatorState: invalid payload: {}", e);
3630 self.set_status_message(format!("setRemoteIndicatorState rejected: {}", e));
3631 }
3632 }
3633 }
3634
3635 fn handle_spawn_process_wait(
3636 &mut self,
3637 process_id: u64,
3638 callback_id: fresh_core::api::JsCallbackId,
3639 ) {
3640 tracing::warn!(
3641 "SpawnProcessWait not fully implemented - process_id={}",
3642 process_id
3643 );
3644 self.plugin_manager.read().unwrap().reject_callback(
3645 callback_id,
3646 format!(
3647 "SpawnProcessWait not yet fully implemented for process_id={}",
3648 process_id
3649 ),
3650 );
3651 }
3652
3653 fn handle_delay(&mut self, callback_id: fresh_core::api::JsCallbackId, duration_ms: u64) {
3654 if let (Some(runtime), Some(bridge)) = (&self.tokio_runtime, &self.async_bridge) {
3655 let sender = bridge.sender();
3656 let callback_id_u64 = callback_id.as_u64();
3657 runtime.spawn(async move {
3658 tokio::time::sleep(tokio::time::Duration::from_millis(duration_ms)).await;
3659 #[allow(clippy::let_underscore_must_use)]
3660 let _ = sender.send(crate::services::async_bridge::AsyncMessage::Plugin(
3661 fresh_core::api::PluginAsyncMessage::DelayComplete {
3662 callback_id: callback_id_u64,
3663 },
3664 ));
3665 });
3666 } else {
3667 std::thread::sleep(std::time::Duration::from_millis(duration_ms));
3668 self.plugin_manager
3669 .read()
3670 .unwrap()
3671 .resolve_callback(callback_id, "null".to_string());
3672 }
3673 }
3674
3675 fn handle_http_fetch(
3676 &mut self,
3677 url: String,
3678 target_path: std::path::PathBuf,
3679 callback_id: fresh_core::api::JsCallbackId,
3680 ) {
3681 if let (Some(runtime), Some(bridge)) = (&self.tokio_runtime, &self.async_bridge) {
3682 let sender = bridge.sender();
3683 let process_id = callback_id.as_u64();
3684
3685 runtime.spawn(async move {
3686 let fetch = tokio::task::spawn_blocking(move || {
3687 crate::services::http::download_to_file(&url, &target_path)
3688 })
3689 .await;
3690
3691 let (stdout, stderr, exit_code) = match fetch {
3692 Ok(Ok(status)) => {
3693 if (200..300).contains(&status) {
3694 (String::new(), String::new(), 0)
3695 } else {
3696 (String::new(), format!("HTTP {}", status), i32::from(status))
3697 }
3698 }
3699 Ok(Err(e)) => (String::new(), e, -1),
3700 Err(e) => (String::new(), format!("fetch task failed: {}", e), -1),
3701 };
3702
3703 #[allow(clippy::let_underscore_must_use)]
3704 let _ = sender.send(AsyncMessage::PluginProcessOutput {
3705 process_id,
3706 stdout,
3707 stderr,
3708 exit_code,
3709 });
3710 });
3711 } else {
3712 self.plugin_manager
3713 .read()
3714 .unwrap()
3715 .reject_callback(callback_id, "Async runtime not available".to_string());
3716 }
3717 }
3718
3719 fn handle_kill_background_process(&mut self, process_id: u64) {
3720 if let Some(handle) = self.background_process_handles.remove(&process_id) {
3721 handle.abort();
3722 tracing::debug!("Killed background process {}", process_id);
3723 }
3724 }
3725
3726 fn handle_create_virtual_buffer(&mut self, name: String, mode: String, read_only: bool) {
3727 let buffer_id =
3728 self.active_window_mut()
3729 .create_virtual_buffer(name.clone(), mode.clone(), read_only);
3730 tracing::info!(
3731 "Created virtual buffer '{}' with mode '{}' (id={:?})",
3732 name,
3733 mode,
3734 buffer_id
3735 );
3736 }
3738
3739 fn handle_set_virtual_buffer_content(
3740 &mut self,
3741 buffer_id: BufferId,
3742 entries: Vec<fresh_core::text_property::TextPropertyEntry>,
3743 ) {
3744 match self.set_virtual_buffer_content(buffer_id, entries) {
3745 Ok(()) => {
3746 tracing::debug!("Set virtual buffer content for {:?}", buffer_id);
3747 }
3748 Err(e) => {
3749 tracing::error!("Failed to set virtual buffer content: {}", e);
3750 }
3751 }
3752 }
3753
3754 fn handle_mount_widget_panel(
3755 &mut self,
3756 panel_id: u64,
3757 buffer_id: BufferId,
3758 spec: fresh_core::api::WidgetSpec,
3759 ) {
3760 let prev = std::collections::HashMap::new();
3765 let prev_focus = String::new();
3766 let panel_width = self.widget_panel_width(buffer_id);
3767 let out = crate::widgets::render_spec(&spec, &prev, &prev_focus, panel_width);
3768 let focus_cursor = out.focus_cursor;
3769 self.widget_registry.mount(
3770 panel_id,
3771 buffer_id,
3772 spec,
3773 out.hits,
3774 out.instance_states,
3775 out.focus_key,
3776 out.tabbable,
3777 );
3778 let entries = out.entries;
3779 if let Err(e) = self.set_virtual_buffer_content(buffer_id, entries.clone()) {
3780 tracing::error!(
3781 "Failed to render mounted widget panel {} into {:?}: {}",
3782 panel_id,
3783 buffer_id,
3784 e
3785 );
3786 } else {
3787 tracing::debug!(
3788 "Mounted widget panel {} into buffer {:?}",
3789 panel_id,
3790 buffer_id
3791 );
3792 }
3793 self.apply_widget_focus_cursor(buffer_id, &entries, focus_cursor);
3794 }
3795
3796 fn handle_update_widget_panel(&mut self, panel_id: u64, spec: fresh_core::api::WidgetSpec) {
3797 let prev = match self.widget_registry.instance_states(panel_id) {
3798 Some(s) => s.clone(),
3799 None => {
3800 tracing::debug!(
3801 "UpdateWidgetPanel for unknown panel {} ignored (not mounted)",
3802 panel_id
3803 );
3804 return;
3805 }
3806 };
3807 let prev_focus = self
3808 .widget_registry
3809 .focus_key(panel_id)
3810 .map(|s| s.to_string())
3811 .unwrap_or_default();
3812 let buffer_id_for_width = self
3813 .widget_registry
3814 .buffer_and_spec(panel_id)
3815 .map(|(b, _)| b)
3816 .unwrap_or(BufferId(0));
3817 let panel_width = self.widget_panel_width(buffer_id_for_width);
3818 let out = crate::widgets::render_spec(&spec, &prev, &prev_focus, panel_width);
3819 let focus_cursor = out.focus_cursor;
3820 let entries = out.entries;
3821 match self.widget_registry.update(
3822 panel_id,
3823 spec,
3824 out.hits,
3825 out.instance_states,
3826 out.focus_key,
3827 out.tabbable,
3828 ) {
3829 Ok(buffer_id) => {
3830 if let Err(e) = self.set_virtual_buffer_content(buffer_id, entries.clone()) {
3831 tracing::error!("Failed to render updated widget panel {}: {}", panel_id, e);
3832 }
3833 self.apply_widget_focus_cursor(buffer_id, &entries, focus_cursor);
3834 }
3835 Err(()) => {
3836 tracing::debug!(
3837 "UpdateWidgetPanel for unknown panel {} ignored (not mounted)",
3838 panel_id
3839 );
3840 }
3841 }
3842 }
3843
3844 fn handle_widget_mutate(&mut self, panel_id: u64, mutation: fresh_core::api::WidgetMutation) {
3850 use fresh_core::api::WidgetMutation;
3851
3852 if self.widget_registry.get(panel_id).is_none() {
3854 tracing::debug!(
3855 "WidgetMutate for unknown panel {} ignored (not mounted)",
3856 panel_id
3857 );
3858 return;
3859 }
3860
3861 match mutation {
3862 WidgetMutation::SetValue {
3863 widget_key,
3864 value,
3865 cursor_byte,
3866 } => {
3867 if let Some(panel) = self.widget_registry.get_mut(panel_id) {
3874 let (scroll, multiline, completions, sel_idx, scroll_off) =
3882 match panel.instance_states.get(&widget_key) {
3883 Some(crate::widgets::WidgetInstanceState::Text {
3884 editor,
3885 scroll,
3886 completions,
3887 completion_selected_index,
3888 completion_scroll_offset,
3889 }) => (
3890 *scroll,
3891 editor.multiline,
3892 completions.clone(),
3893 *completion_selected_index,
3894 *completion_scroll_offset,
3895 ),
3896 _ => (0u32, true, Vec::new(), 0usize, 0u32),
3897 };
3898 let mut editor = if multiline {
3899 crate::primitives::text_edit::TextEdit::with_text(&value)
3900 } else {
3901 crate::primitives::text_edit::TextEdit::single_line_with_text(&value)
3902 };
3903 let target = match cursor_byte {
3904 Some(c) if c >= 0 => (c as usize).min(value.len()),
3905 _ => value.len(),
3906 };
3907 editor.set_cursor_from_flat(target);
3908 panel.instance_states.insert(
3909 widget_key,
3910 crate::widgets::WidgetInstanceState::Text {
3911 editor,
3912 scroll,
3913 completions,
3914 completion_selected_index: sel_idx,
3915 completion_scroll_offset: scroll_off,
3916 },
3917 );
3918 }
3919 }
3920 WidgetMutation::SetChecked {
3921 widget_key,
3922 checked,
3923 } => {
3924 if let Some(panel) = self.widget_registry.get_mut(panel_id) {
3928 crate::widgets::set_toggle_checked_in_spec(
3929 &mut panel.spec,
3930 &widget_key,
3931 checked,
3932 );
3933 }
3934 }
3935 WidgetMutation::SetSelectedIndex { widget_key, index } => {
3936 if let Some(panel) = self.widget_registry.get_mut(panel_id) {
3938 let (prev_scroll, prev_index, prev_item_height, prev_user_scrolled) =
3939 match panel.instance_states.get(&widget_key) {
3940 Some(crate::widgets::WidgetInstanceState::List {
3941 scroll_offset,
3942 selected_index,
3943 item_height,
3944 user_scrolled,
3945 }) => (
3946 *scroll_offset,
3947 *selected_index,
3948 *item_height,
3949 *user_scrolled,
3950 ),
3951 _ => (0, -1, 1, false),
3952 };
3953 let user_scrolled = prev_user_scrolled && index == prev_index;
3959 panel.instance_states.insert(
3960 widget_key,
3961 crate::widgets::WidgetInstanceState::List {
3962 scroll_offset: prev_scroll,
3963 selected_index: index,
3964 item_height: prev_item_height,
3965 user_scrolled,
3966 },
3967 );
3968 }
3969 }
3970 WidgetMutation::SetCompletions { widget_key, items } => {
3971 if let Some(panel) = self.widget_registry.get_mut(panel_id) {
3980 if let Some(crate::widgets::WidgetInstanceState::Text {
3981 completions,
3982 completion_selected_index,
3983 completion_scroll_offset,
3984 ..
3985 }) = panel.instance_states.get_mut(&widget_key)
3986 {
3987 *completions = items;
3988 *completion_selected_index = 0;
3989 *completion_scroll_offset = 0;
3990 }
3991 }
3992 }
3993 WidgetMutation::SetItems {
3994 widget_key,
3995 items,
3996 item_keys,
3997 } => {
3998 if let Some(panel) = self.widget_registry.get_mut(panel_id) {
4000 crate::widgets::set_list_items_in_spec(
4001 &mut panel.spec,
4002 &widget_key,
4003 items,
4004 item_keys,
4005 );
4006 }
4007 }
4008 WidgetMutation::SetExpandedKeys { widget_key, keys } => {
4009 if let Some(panel) = self.widget_registry.get_mut(panel_id) {
4011 let (prev_scroll, prev_sel) = match panel.instance_states.get(&widget_key) {
4012 Some(crate::widgets::WidgetInstanceState::Tree {
4013 scroll_offset,
4014 selected_index,
4015 ..
4016 }) => (*scroll_offset, *selected_index),
4017 _ => (0, -1),
4018 };
4019 let expanded: std::collections::HashSet<String> = keys.into_iter().collect();
4020 panel.instance_states.insert(
4021 widget_key,
4022 crate::widgets::WidgetInstanceState::Tree {
4023 scroll_offset: prev_scroll,
4024 selected_index: prev_sel,
4025 expanded_keys: expanded,
4026 },
4027 );
4028 }
4029 }
4030 WidgetMutation::SetCheckedKeys {
4031 widget_key,
4032 checked,
4033 keys,
4034 } => {
4035 if let Some(panel) = self.widget_registry.get_mut(panel_id) {
4043 crate::widgets::set_tree_checked_keys_in_spec(
4044 &mut panel.spec,
4045 &widget_key,
4046 checked,
4047 &keys,
4048 );
4049 }
4050 }
4051 WidgetMutation::AppendTreeNodes {
4052 widget_key,
4053 new_nodes,
4054 new_item_keys,
4055 } => {
4056 if let Some(panel) = self.widget_registry.get_mut(panel_id) {
4057 crate::widgets::append_tree_nodes_in_spec(
4058 &mut panel.spec,
4059 &widget_key,
4060 new_nodes,
4061 new_item_keys,
4062 );
4063 }
4064 }
4065 WidgetMutation::SetRawEntries {
4066 widget_key,
4067 entries,
4068 } => {
4069 if let Some(panel) = self.widget_registry.get_mut(panel_id) {
4070 crate::widgets::set_raw_entries_in_spec(&mut panel.spec, &widget_key, entries);
4071 }
4072 }
4073 WidgetMutation::SetFocusKey { widget_key } => {
4074 self.widget_registry.set_focus_key(panel_id, widget_key);
4079 }
4080 }
4081
4082 self.rerender_widget_panel(panel_id);
4086 }
4087
4088 fn handle_unmount_widget_panel(&mut self, panel_id: u64) {
4089 match self.widget_registry.unmount(panel_id) {
4090 Some(buffer_id) => {
4091 tracing::debug!(
4092 "Unmounted widget panel {} (was rendering into {:?})",
4093 panel_id,
4094 buffer_id
4095 );
4096 }
4101 None => {
4102 tracing::debug!("UnmountWidgetPanel for unknown panel {} ignored", panel_id);
4103 }
4104 }
4105 }
4106
4107 fn handle_mount_floating_widget(
4108 &mut self,
4109 panel_id: u64,
4110 spec: fresh_core::api::WidgetSpec,
4111 width_pct: u8,
4112 height_pct: u8,
4113 as_dock: bool,
4114 ) {
4115 let width_pct = width_pct.clamp(1, 100);
4116 let height_pct = height_pct.clamp(1, 100);
4117 let slot = if as_dock {
4120 super::PanelSlot::Dock
4121 } else {
4122 super::PanelSlot::Floating
4123 };
4124 let buffer_id = slot.buffer_id();
4125 if !as_dock && self.dock.as_ref().is_some_and(|f| f.focused) {
4132 self.blur_floating_panel(super::PanelSlot::Dock);
4133 }
4134 let placement = if as_dock {
4135 let width = self
4136 .dock_width
4137 .unwrap_or(32)
4138 .clamp(10, self.terminal_width.max(20).saturating_sub(20).max(10));
4139 super::PanelPlacement::LeftDock { width_cols: width }
4140 } else {
4141 super::PanelPlacement::Centered
4142 };
4143 if let Some(existing) = self.panel_opt_mut(slot).take() {
4144 if existing.panel_id != panel_id {
4145 let _ = self.widget_registry.unmount(existing.panel_id);
4146 }
4147 }
4148 *self.panel_opt_mut(slot) = Some(FloatingWidgetState {
4149 panel_id,
4150 width_pct,
4151 height_pct,
4152 placement,
4153 focused: true,
4154 entries: Vec::new(),
4155 focus_cursor: None,
4156 embeds: Vec::new(),
4157 overlays: Vec::new(),
4158 scroll_regions: Vec::new(),
4159 scrollbar_tracks: Vec::new(),
4160 scrollbar_mouse: Default::default(),
4161 scrollbar_drag_key: None,
4162 last_inner_rect: None,
4163 fullscreen: false,
4164 });
4165 let prev = std::collections::HashMap::new();
4166 let prev_focus = String::new();
4167 let panel_width = self.floating_panel_inner_width(slot);
4168 let out = crate::widgets::render_spec(&spec, &prev, &prev_focus, panel_width);
4169 let focus_cursor = out.focus_cursor;
4170 let entries = out.entries;
4171 let embeds = out.embeds;
4172 let overlays = out.overlays;
4173 let scroll_regions = out.scroll_regions;
4174 self.widget_registry.mount(
4175 panel_id,
4176 buffer_id,
4177 spec,
4178 out.hits,
4179 out.instance_states,
4180 out.focus_key,
4181 out.tabbable,
4182 );
4183 if let Some(fwp) = self.panel_mut(slot) {
4184 fwp.entries = entries;
4185 fwp.focus_cursor = focus_cursor;
4186 fwp.embeds = embeds;
4187 fwp.overlays = overlays;
4188 fwp.scroll_regions = scroll_regions;
4189 }
4190 tracing::debug!(
4191 "Mounted floating widget panel {} ({}%x{}%)",
4192 panel_id,
4193 width_pct,
4194 height_pct
4195 );
4196
4197 if as_dock {
4202 self.relayout();
4203 }
4204 }
4205
4206 fn handle_update_floating_widget(&mut self, panel_id: u64, spec: fresh_core::api::WidgetSpec) {
4207 let Some(slot) = self.slot_of_panel(panel_id) else {
4208 tracing::debug!(
4209 "UpdateFloatingWidget for unknown / mismatched panel {} ignored",
4210 panel_id
4211 );
4212 return;
4213 };
4214 let prev = self
4215 .widget_registry
4216 .instance_states(panel_id)
4217 .cloned()
4218 .unwrap_or_default();
4219 let prev_focus = self
4220 .widget_registry
4221 .focus_key(panel_id)
4222 .map(|s| s.to_string())
4223 .unwrap_or_default();
4224 let panel_width = self.floating_panel_inner_width(slot);
4225 let out = crate::widgets::render_spec(&spec, &prev, &prev_focus, panel_width);
4226 let focus_cursor = out.focus_cursor;
4227 let entries = out.entries;
4228 let embeds = out.embeds;
4229 let overlays = out.overlays;
4230 let scroll_regions = out.scroll_regions;
4231 if self
4232 .widget_registry
4233 .update(
4234 panel_id,
4235 spec,
4236 out.hits,
4237 out.instance_states,
4238 out.focus_key,
4239 out.tabbable,
4240 )
4241 .is_err()
4242 {
4243 tracing::debug!(
4244 "UpdateFloatingWidget for unknown panel {} ignored (not in registry)",
4245 panel_id
4246 );
4247 return;
4248 }
4249 if let Some(fwp) = self.panel_mut(slot) {
4250 fwp.entries = entries;
4251 fwp.focus_cursor = focus_cursor;
4252 fwp.embeds = embeds;
4253 fwp.overlays = overlays;
4254 fwp.scroll_regions = scroll_regions;
4255 }
4256 }
4257
4258 fn handle_unmount_floating_widget(&mut self, panel_id: u64) {
4259 let Some(slot) = self.slot_of_panel(panel_id) else {
4260 tracing::debug!(
4261 "UnmountFloatingWidget for unknown / mismatched panel {} ignored",
4262 panel_id
4263 );
4264 return;
4265 };
4266 *self.panel_opt_mut(slot) = None;
4267 let _ = self.widget_registry.unmount(panel_id);
4268 if slot == super::PanelSlot::Dock {
4279 self.request_full_redraw();
4280 }
4281 self.relayout();
4297 tracing::debug!("Unmounted floating widget panel {}", panel_id);
4298 }
4299
4300 fn handle_floating_panel_control(&mut self, panel_id: u64, op: &str, arg: f64) {
4303 let Some(slot) = self.slot_of_panel(panel_id) else {
4304 tracing::warn!("FloatingPanelControl for unknown/mismatched panel {panel_id} ignored");
4305 return;
4306 };
4307 if op == "blur" {
4310 self.blur_floating_panel(slot);
4311 return;
4312 }
4313 let max_cols = self.terminal_width.max(20).saturating_sub(20).max(10);
4318 let persisted = self.dock_width;
4319 let Some(fwp) = self.panel_mut(slot) else {
4320 return;
4321 };
4322 let geometry_changed = match op {
4325 "dock" => {
4326 let requested = persisted.unwrap_or(arg as u16);
4327 let width_cols = requested.clamp(10, max_cols);
4328 fwp.placement = super::PanelPlacement::LeftDock { width_cols };
4329 fwp.focused = true;
4330 true
4331 }
4332 "dock_width" => {
4338 if let super::PanelPlacement::LeftDock { .. } = fwp.placement {
4339 let requested = persisted.unwrap_or(arg as u16);
4340 let width_cols = requested.clamp(10, max_cols);
4341 fwp.placement = super::PanelPlacement::LeftDock { width_cols };
4342 true
4343 } else {
4344 false
4345 }
4346 }
4347 "center" => {
4348 fwp.placement = super::PanelPlacement::Centered;
4349 fwp.focused = true;
4350 true
4351 }
4352 "focus" => {
4353 fwp.focused = true;
4354 false
4355 }
4356 "fullscreen" => {
4362 fwp.fullscreen = arg != 0.0;
4363 false
4364 }
4365 other => {
4366 tracing::warn!("FloatingPanelControl: unknown op {other:?}");
4367 false
4368 }
4369 };
4370 if geometry_changed {
4374 self.relayout();
4375 }
4376 }
4377
4378 fn handle_get_text_properties_at_cursor(&self, buffer_id: BufferId) {
4379 if let Some(state) = self
4380 .windows
4381 .get(&self.active_window)
4382 .map(|w| &w.buffers)
4383 .expect("active window present")
4384 .get(&buffer_id)
4385 {
4386 let cursor_pos = self
4387 .windows
4388 .get(&self.active_window)
4389 .and_then(|w| w.buffers.splits())
4390 .map(|(_, vs)| vs)
4391 .expect("active window must have a populated split layout")
4392 .values()
4393 .find_map(|vs| vs.buffer_state(buffer_id))
4394 .map(|bs| bs.cursors.primary().position)
4395 .unwrap_or(0);
4396 let properties = state.text_properties.get_at(cursor_pos);
4397 tracing::debug!(
4398 "Text properties at cursor in {:?}: {} properties found",
4399 buffer_id,
4400 properties.len()
4401 );
4402 }
4404 }
4405
4406 fn handle_set_context(&mut self, name: String, active: bool) {
4407 if active {
4408 self.active_window_mut()
4409 .active_custom_contexts
4410 .insert(name.clone());
4411 tracing::debug!("Set custom context: {}", name);
4412 } else {
4413 self.active_window_mut()
4414 .active_custom_contexts
4415 .remove(&name);
4416 tracing::debug!("Unset custom context: {}", name);
4417 }
4418 }
4419
4420 fn handle_disable_lsp_for_language(&mut self, language: String) {
4421 tracing::info!("Disabling LSP for language: {}", language);
4422 let __active_id = self.active_window;
4423 if let Some(lsp) = self.windows.get_mut(&__active_id).map(|w| &mut w.lsp) {
4424 lsp.shutdown_server(&language);
4425 tracing::info!("Stopped LSP server for {}", language);
4426 }
4427 if let Some(lsp_configs) = self.config_mut().lsp.get_mut(&language) {
4428 for c in lsp_configs.as_mut_slice() {
4429 c.enabled = false;
4430 c.auto_start = false;
4431 }
4432 tracing::info!("Disabled LSP config for {}", language);
4433 }
4434 if let Err(e) = self.save_config() {
4435 tracing::error!("Failed to save config: {}", e);
4436 self.active_window_mut().status_message = Some(format!(
4437 "LSP disabled for {} (config save failed)",
4438 language
4439 ));
4440 } else {
4441 self.active_window_mut().status_message =
4442 Some(format!("LSP disabled for {}", language));
4443 }
4444 self.active_window_mut().warning_domains.lsp.clear();
4445 }
4446
4447 fn handle_restart_lsp_for_language(&mut self, language: String) {
4448 tracing::info!("Plugin restarting LSP for language: {}", language);
4449 let file_path = self
4450 .active_window()
4451 .buffer_metadata
4452 .get(&self.active_buffer())
4453 .and_then(|meta| meta.file_path().cloned());
4454 let __active_id = self.active_window;
4455 let success = if let Some(lsp) = self.windows.get_mut(&__active_id).map(|w| &mut w.lsp) {
4456 let (ok, msg) = lsp.manual_restart(&language, file_path.as_deref());
4457 self.active_window_mut().status_message = Some(msg);
4458 ok
4459 } else {
4460 self.active_window_mut().status_message = Some("No LSP manager available".to_string());
4461 false
4462 };
4463 if success {
4464 self.reopen_buffers_for_language(&language);
4465 }
4466 }
4467
4468 fn handle_set_lsp_root_uri(&mut self, language: String, uri: String) {
4469 tracing::info!("Plugin setting LSP root URI for {}: {}", language, uri);
4470 match uri.parse::<lsp_types::Uri>() {
4471 Ok(parsed_uri) => {
4472 let __active_id = self.active_window;
4473 if let Some(lsp) = self.windows.get_mut(&__active_id).map(|w| &mut w.lsp) {
4474 let restarted = lsp.set_language_root_uri(&language, parsed_uri);
4475 if restarted {
4476 self.active_window_mut().status_message = Some(format!(
4477 "LSP root updated for {} (restarting server)",
4478 language
4479 ));
4480 } else {
4481 self.active_window_mut().status_message =
4482 Some(format!("LSP root set for {}", language));
4483 }
4484 }
4485 }
4486 Err(e) => {
4487 tracing::error!("Invalid LSP root URI '{}': {}", uri, e);
4488 self.active_window_mut().status_message =
4489 Some(format!("Invalid LSP root URI: {}", e));
4490 }
4491 }
4492 }
4493
4494 fn handle_create_scroll_sync_group(
4495 &mut self,
4496 group_id: crate::view::scroll_sync::ScrollSyncGroupId,
4497 left_split: SplitId,
4498 right_split: SplitId,
4499 ) {
4500 let success = self
4501 .active_window_mut()
4502 .scroll_sync_manager
4503 .create_group_with_id(group_id, left_split, right_split);
4504 if success {
4505 tracing::debug!(
4506 "Created scroll sync group {} for splits {:?} and {:?}",
4507 group_id,
4508 left_split,
4509 right_split
4510 );
4511 } else {
4512 tracing::warn!(
4513 "Failed to create scroll sync group {} (ID already exists)",
4514 group_id
4515 );
4516 }
4517 }
4518
4519 fn handle_set_scroll_sync_anchors(
4520 &mut self,
4521 group_id: crate::view::scroll_sync::ScrollSyncGroupId,
4522 anchors: Vec<(usize, usize)>,
4523 ) {
4524 use crate::view::scroll_sync::SyncAnchor;
4525 let anchor_count = anchors.len();
4526 let sync_anchors: Vec<SyncAnchor> = anchors
4527 .into_iter()
4528 .map(|(left_line, right_line)| SyncAnchor {
4529 left_line,
4530 right_line,
4531 })
4532 .collect();
4533 self.active_window_mut()
4534 .scroll_sync_manager
4535 .set_anchors(group_id, sync_anchors);
4536 tracing::debug!(
4537 "Set {} anchors for scroll sync group {}",
4538 anchor_count,
4539 group_id
4540 );
4541 }
4542
4543 fn handle_remove_scroll_sync_group(
4544 &mut self,
4545 group_id: crate::view::scroll_sync::ScrollSyncGroupId,
4546 ) {
4547 if self
4548 .active_window_mut()
4549 .scroll_sync_manager
4550 .remove_group(group_id)
4551 {
4552 tracing::debug!("Removed scroll sync group {}", group_id);
4553 } else {
4554 tracing::warn!("Scroll sync group {} not found", group_id);
4555 }
4556 }
4557
4558 fn handle_create_buffer_group(
4559 &mut self,
4560 name: String,
4561 mode: String,
4562 layout_json: String,
4563 request_id: Option<u64>,
4564 ) {
4565 match self.create_buffer_group(name, mode, layout_json) {
4566 Ok(result) => {
4567 if let Some(req_id) = request_id {
4568 let json = serde_json::to_string(&result).unwrap_or_default();
4569 self.plugin_manager
4570 .read()
4571 .unwrap()
4572 .resolve_callback(fresh_core::api::JsCallbackId::from(req_id), json);
4573 }
4574 }
4575 Err(e) => {
4576 tracing::error!("Failed to create buffer group: {}", e);
4577 }
4578 }
4579 }
4580
4581 fn handle_send_terminal_input(
4582 &mut self,
4583 terminal_id: crate::services::terminal::TerminalId,
4584 data: String,
4585 ) {
4586 if let Some(handle) = self.active_window().terminal_manager.get(terminal_id) {
4587 handle.write(data.as_bytes());
4588 tracing::trace!(
4589 "Plugin sent {} bytes to terminal {:?}",
4590 data.len(),
4591 terminal_id
4592 );
4593 } else {
4594 tracing::warn!(
4595 "Plugin tried to send input to non-existent terminal {:?}",
4596 terminal_id
4597 );
4598 }
4599 }
4600
4601 fn handle_close_terminal(&mut self, terminal_id: crate::services::terminal::TerminalId) {
4602 let buffer_to_close = self
4603 .active_window()
4604 .terminal_buffers
4605 .iter()
4606 .find(|(_, &tid)| tid == terminal_id)
4607 .map(|(&bid, _)| bid);
4608 if let Some(buffer_id) = buffer_to_close {
4609 if let Err(e) = self.close_buffer(buffer_id) {
4610 tracing::warn!("Failed to close terminal buffer: {}", e);
4611 }
4612 tracing::info!("Plugin closed terminal {:?}", terminal_id);
4613 } else {
4614 self.active_window_mut().terminal_manager.close(terminal_id);
4615 tracing::info!("Plugin closed terminal {:?} (no buffer found)", terminal_id);
4616 }
4617 }
4618
4619 fn handle_signal_window(&mut self, id: fresh_core::WindowId, signal: &str) {
4627 let Some(window) = self.windows.get_mut(&id) else {
4628 tracing::warn!("Plugin SignalWindow targeted unknown window {:?}", id);
4629 return;
4630 };
4631 let results = window.process_groups.signal_all(signal);
4632 for (entry, result) in results {
4633 match result {
4634 Ok(true) => tracing::info!(
4635 "SignalWindow {:?}: {} → pid {} ({})",
4636 id,
4637 signal,
4638 entry.leader_pid,
4639 entry.label
4640 ),
4641 Ok(false) => tracing::debug!(
4642 "SignalWindow {:?}: pid {} ({}) already exited",
4643 id,
4644 entry.leader_pid,
4645 entry.label
4646 ),
4647 Err(e) => tracing::warn!(
4648 "SignalWindow {:?}: pid {} ({}): {}",
4649 id,
4650 entry.leader_pid,
4651 entry.label,
4652 e
4653 ),
4654 }
4655 }
4656 }
4657}
4658
4659fn clamp_buffer_text_range(start: usize, end: usize, len: usize) -> (usize, usize) {
4670 let end = end.min(len);
4671 let start = start.min(end);
4672 (start, end)
4673}
4674
4675#[cfg(test)]
4676mod tests {
4677 use tokio::io::{AsyncReadExt, BufReader};
4690 use tokio::process::Command as TokioCommand;
4691 use tokio::time::{timeout, Duration};
4692
4693 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
4704 async fn kill_via_oneshot_terminates_long_running_child() {
4705 let mut cmd = TokioCommand::new("sleep");
4706 cmd.args(["30"]);
4707 cmd.stdout(std::process::Stdio::piped());
4708 cmd.stderr(std::process::Stdio::piped());
4709
4710 let mut child = cmd.spawn().expect("spawn sh -c sleep 30");
4711 let pid = child.id().expect("child has a pid");
4712
4713 let (kill_tx, mut kill_rx) = tokio::sync::oneshot::channel::<()>();
4714 let stdout_pipe = child.stdout.take();
4715 let stderr_pipe = child.stderr.take();
4716
4717 let stdout_fut = async {
4718 let mut buf = String::new();
4719 if let Some(s) = stdout_pipe {
4720 #[allow(clippy::let_underscore_must_use)]
4721 let _ = BufReader::new(s).read_to_string(&mut buf).await;
4722 }
4723 buf
4724 };
4725 let stderr_fut = async {
4726 let mut buf = String::new();
4727 if let Some(s) = stderr_pipe {
4728 #[allow(clippy::let_underscore_must_use)]
4729 let _ = BufReader::new(s).read_to_string(&mut buf).await;
4730 }
4731 buf
4732 };
4733 let wait_fut = async {
4734 tokio::select! {
4735 status = child.wait() => {
4736 status.map(|s| s.code().unwrap_or(-1)).unwrap_or(-1)
4737 }
4738 _ = &mut kill_rx => {
4739 #[allow(clippy::let_underscore_must_use)]
4740 let _ = child.start_kill();
4741 child
4742 .wait()
4743 .await
4744 .map(|s| s.code().unwrap_or(-1))
4745 .unwrap_or(-1)
4746 }
4747 }
4748 };
4749
4750 tokio::time::sleep(Duration::from_millis(50)).await;
4755 kill_tx.send(()).expect("kill channel send");
4756
4757 let result = timeout(Duration::from_secs(5), async {
4758 tokio::join!(stdout_fut, stderr_fut, wait_fut)
4759 })
4760 .await;
4761
4762 let (_stdout, _stderr, exit_code) = result.expect(
4763 "kill path must resolve within 5s — if this times out the \
4764 select! arm order or kill-then-wait logic is broken",
4765 );
4766 assert_ne!(
4778 exit_code, 0,
4779 "killed child must exit non-success (got 0 — did the \
4780 kill arm fire too late, or did sleep somehow complete?)"
4781 );
4782
4783 #[cfg(unix)]
4792 {
4793 let still_alive = std::process::Command::new("kill")
4794 .args(["-0", &pid.to_string()])
4795 .status()
4796 .map(|s| s.success())
4797 .unwrap_or(false);
4798 assert!(
4799 !still_alive,
4800 "process {pid} must be reaped after wait() — a still-\
4801 alive check means the kill path leaked the child"
4802 );
4803 }
4804 #[cfg(not(unix))]
4805 {
4806 let _ = pid;
4809 }
4810 }
4811
4812 use super::clamp_buffer_text_range;
4813
4814 #[test]
4815 fn clamp_text_range_passes_through_in_bounds() {
4816 assert_eq!(clamp_buffer_text_range(0, 165, 165), (0, 165));
4817 assert_eq!(clamp_buffer_text_range(10, 50, 165), (10, 50));
4818 }
4819
4820 #[test]
4826 fn clamp_text_range_clamps_stale_end_past_buffer() {
4827 assert_eq!(clamp_buffer_text_range(0, 165_003, 165_002), (0, 165_002));
4828 }
4829
4830 #[test]
4831 fn clamp_text_range_pins_overlarge_start_to_empty() {
4832 assert_eq!(clamp_buffer_text_range(200, 250, 165), (165, 165));
4834 }
4835}
4836
4837impl Window {
4838 #[cfg(feature = "plugins")]
4853 pub(crate) fn populate_plugin_state_snapshot(
4854 &mut self,
4855 snapshot: &mut fresh_core::api::EditorStateSnapshot,
4856 ) {
4857 use fresh_core::api::{BufferInfo, CursorInfo, ViewportInfo};
4858
4859 let current_gen = self.resources.grammar_registry.catalog_gen();
4865 if snapshot.last_grammar_gen != current_gen {
4866 snapshot.available_grammars = self
4867 .resources
4868 .grammar_registry
4869 .available_grammar_info()
4870 .into_iter()
4871 .map(|g| fresh_core::api::GrammarInfoSnapshot {
4872 name: g.name,
4873 source: g.source.to_string(),
4874 file_extensions: g.file_extensions,
4875 short_name: g.short_name,
4876 })
4877 .collect();
4878 snapshot.last_grammar_gen = current_gen;
4879 }
4880
4881 snapshot.active_buffer_id = self.active_buffer();
4882
4883 let (mgr_ref, vs_ref) = self
4884 .buffers
4885 .splits()
4886 .expect("active window must have a populated split layout");
4887 let active_split = mgr_ref.active_split();
4888 snapshot.active_split_id = active_split.0 .0;
4889
4890 snapshot.buffers.clear();
4892 snapshot.buffer_saved_diffs.clear();
4893 snapshot.buffer_cursor_positions.clear();
4894 snapshot.buffer_text_properties.clear();
4895
4896 let active_vs_opt = vs_ref.get(&active_split);
4897 for (buffer_id, state) in &self.buffers {
4898 let is_virtual = self
4899 .buffer_metadata
4900 .get(buffer_id)
4901 .map(|m| m.is_virtual())
4902 .unwrap_or(false);
4903 let view_mode = active_vs_opt
4908 .and_then(|vs| vs.buffer_state(*buffer_id))
4909 .map(|bs| match bs.view_mode {
4910 crate::state::ViewMode::Source => "source",
4911 crate::state::ViewMode::PageView => "compose",
4912 })
4913 .unwrap_or("source");
4914 let compose_width = active_vs_opt
4915 .and_then(|vs| vs.buffer_state(*buffer_id))
4916 .and_then(|bs| bs.compose_width);
4917 let is_composing_in_any_split = vs_ref.values().any(|vs| {
4918 vs.buffer_state(*buffer_id)
4919 .map(|bs| matches!(bs.view_mode, crate::state::ViewMode::PageView))
4920 .unwrap_or(false)
4921 });
4922 let is_preview = self
4923 .buffer_metadata
4924 .get(buffer_id)
4925 .map(|m| m.is_preview)
4926 .unwrap_or(false);
4927 let splits: Vec<fresh_core::SplitId> = mgr_ref
4933 .splits_for_buffer(*buffer_id)
4934 .into_iter()
4935 .map(|leaf_id| leaf_id.0)
4936 .collect();
4937 let buffer_info = BufferInfo {
4938 id: *buffer_id,
4939 path: state.buffer.file_path().map(|p| p.to_path_buf()),
4940 modified: state.buffer.is_modified(),
4941 length: state.buffer.len(),
4942 is_virtual,
4943 view_mode: view_mode.to_string(),
4944 is_composing_in_any_split,
4945 compose_width,
4946 language: state.language.clone(),
4947 is_preview,
4948 splits,
4949 };
4950 snapshot.buffers.insert(*buffer_id, buffer_info);
4951
4952 let diff = {
4953 let diff = state.buffer.diff_since_saved();
4954 BufferSavedDiff {
4955 equal: diff.equal,
4956 byte_ranges: diff.byte_ranges.clone(),
4957 }
4958 };
4959 snapshot.buffer_saved_diffs.insert(*buffer_id, diff);
4960
4961 let is_hidden = self
4970 .buffer_metadata
4971 .get(buffer_id)
4972 .is_some_and(|m| m.hidden_from_tabs);
4973 let source_split = vs_ref.iter().find(|(split_id, vs)| {
4974 vs.keyed_states.contains_key(buffer_id)
4975 && !(is_hidden && self.grouped_subtrees.contains_key(split_id))
4976 });
4977 let cursor_pos = source_split
4978 .and_then(|(_, vs)| vs.buffer_state(*buffer_id))
4979 .map(|bs| bs.cursors.primary().position)
4980 .unwrap_or(0);
4981 tracing::trace!(
4982 "snapshot: buffer {:?} cursor_pos={} (from split {:?})",
4983 buffer_id,
4984 cursor_pos,
4985 source_split.map(|(id, _)| *id),
4986 );
4987 snapshot
4988 .buffer_cursor_positions
4989 .insert(*buffer_id, cursor_pos);
4990
4991 if !state.text_properties.is_empty() {
4993 snapshot
4994 .buffer_text_properties
4995 .insert(*buffer_id, state.text_properties.all().to_vec());
4996 }
4997 }
4998
4999 let active_buf_id = snapshot.active_buffer_id;
5010 let active_split_id = self.effective_active_pair().0;
5011 self.buffers
5012 .with_all_mut(|buffers_mut, mgr, vs_map| {
5013 let _ = mgr; if let Some(active_vs) = vs_map.get(&active_split_id) {
5015 let active_cursors = &active_vs.cursors;
5017 let primary = active_cursors.primary();
5018 let primary_position = primary.position;
5019 let primary_selection = primary.selection_range();
5020
5021 let line_of = |offset: usize| -> Option<usize> {
5027 buffers_mut.get(&active_buf_id).and_then(|state| {
5028 if state.buffer.line_count().is_some() {
5029 Some(state.buffer.get_line_number(offset))
5030 } else {
5031 None
5032 }
5033 })
5034 };
5035
5036 snapshot.primary_cursor = Some(CursorInfo {
5037 position: primary_position,
5038 selection: primary_selection.clone(),
5039 line: line_of(primary_position),
5040 });
5041
5042 snapshot.all_cursors = active_cursors
5043 .iter()
5044 .map(|(_, cursor)| CursorInfo {
5045 position: cursor.position,
5046 selection: cursor.selection_range(),
5047 line: line_of(cursor.position),
5048 })
5049 .collect();
5050
5051 if let Some(range) = primary_selection {
5053 if let Some(active_state) = buffers_mut.get_mut(&active_buf_id) {
5054 snapshot.selected_text =
5055 Some(active_state.get_text_range(range.start, range.end));
5056 }
5057 }
5058
5059 let top_line = buffers_mut.get(&active_buf_id).and_then(|state| {
5061 if state.buffer.line_count().is_some() {
5062 Some(state.buffer.get_line_number(active_vs.viewport.top_byte))
5063 } else {
5064 None
5065 }
5066 });
5067 snapshot.viewport = Some(ViewportInfo {
5068 top_byte: active_vs.viewport.top_byte,
5069 top_line,
5070 left_column: active_vs.viewport.left_column,
5071 width: active_vs.viewport.width,
5072 height: active_vs.viewport.height,
5073 });
5074 } else {
5075 snapshot.primary_cursor = None;
5076 snapshot.all_cursors.clear();
5077 snapshot.viewport = None;
5078 snapshot.selected_text = None;
5079 }
5080
5081 snapshot.splits.clear();
5083 for (leaf_id, vs) in vs_map.iter() {
5084 let buf_id = vs.active_buffer;
5085 let top_line = buffers_mut.get(&buf_id).and_then(|state| {
5086 if state.buffer.line_count().is_some() {
5087 Some(state.buffer.get_line_number(vs.viewport.top_byte))
5088 } else {
5089 None
5090 }
5091 });
5092 snapshot.splits.push(fresh_core::api::SplitSnapshot {
5093 split_id: leaf_id.0 .0,
5094 buffer_id: buf_id,
5095 viewport: ViewportInfo {
5096 top_byte: vs.viewport.top_byte,
5097 top_line,
5098 left_column: vs.viewport.left_column,
5099 width: vs.viewport.width,
5100 height: vs.viewport.height,
5101 },
5102 });
5103 }
5104 })
5105 .expect("active window must have a populated split layout");
5106
5107 snapshot.active_session_plugin_states = self.plugin_state.clone();
5113 snapshot.diagnostics = Arc::clone(&self.stored_diagnostics);
5118 snapshot.folding_ranges = Arc::clone(&self.stored_folding_ranges);
5119
5120 snapshot.editor_mode = self.editor_mode.clone();
5122
5123 let active_split_id_u64 = active_split_id.0 .0;
5128 let split_changed = snapshot.plugin_view_states_split != active_split_id_u64;
5129 if split_changed {
5130 snapshot.plugin_view_states.clear();
5131 snapshot.plugin_view_states_split = active_split_id_u64;
5132 }
5133
5134 {
5136 let open_bids: Vec<_> = snapshot.buffers.keys().copied().collect();
5137 snapshot
5138 .plugin_view_states
5139 .retain(|bid, _| open_bids.contains(bid));
5140 }
5141
5142 if let Some(vs_map) = self.buffers.split_view_states() {
5144 if let Some(active_vs) = vs_map.get(&active_split_id) {
5145 for (buffer_id, buf_state) in &active_vs.keyed_states {
5146 if !buf_state.plugin_state.is_empty() {
5147 let entry = snapshot.plugin_view_states.entry(*buffer_id).or_default();
5148 for (key, value) in &buf_state.plugin_state {
5149 entry.entry(key.clone()).or_insert_with(|| value.clone());
5150 }
5151 }
5152 }
5153 }
5154 }
5155 }
5156}
5157
5158