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, FLOATING_PANEL_BUFFER_ID};
26
27fn buffer_line_byte_offset(
32 content: &str,
33 buffer_len: usize,
34 line: usize,
35 want_end: bool,
36) -> Option<usize> {
37 if !want_end && line == 0 {
38 return Some(0);
39 }
40 let mut current_line = 0usize;
41 for (byte_idx, c) in content.char_indices() {
42 if c == '\n' {
43 if want_end && current_line == line {
44 return Some(byte_idx);
45 }
46 current_line += 1;
47 if !want_end && current_line == line {
48 return Some(byte_idx + 1);
49 }
50 }
51 }
52 if want_end && current_line == line {
53 Some(buffer_len)
54 } else {
55 None
56 }
57}
58
59fn find_scrollable_widget_key(spec: &fresh_core::api::WidgetSpec) -> Option<String> {
67 use fresh_core::api::WidgetSpec;
68 match spec {
69 WidgetSpec::Tree { key: Some(k), .. } | WidgetSpec::List { key: Some(k), .. }
70 if !k.is_empty() =>
71 {
72 return Some(k.clone());
73 }
74 _ => {}
75 }
76 spec.children().find_map(find_scrollable_widget_key)
77}
78
79fn collect_visible_tree_indices(
80 nodes: &[fresh_core::api::TreeNode],
81 item_keys: &[String],
82 expanded: &std::collections::HashSet<String>,
83) -> Vec<usize> {
84 let mut ancestor_open: Vec<bool> = Vec::new();
85 let mut visible: Vec<usize> = Vec::with_capacity(nodes.len());
86 for (i, node) in nodes.iter().enumerate() {
87 let depth = node.depth as usize;
88 ancestor_open.truncate(depth);
89 if ancestor_open.iter().all(|open| *open) {
90 visible.push(i);
91 }
92 let key = item_keys.get(i).cloned().unwrap_or_default();
93 let is_open = if node.has_children {
94 !key.is_empty() && expanded.contains(&key)
95 } else {
96 true
97 };
98 ancestor_open.push(is_open);
99 }
100 visible
101}
102
103impl Editor {
104 #[cfg(feature = "plugins")]
113 pub(super) fn update_plugin_state_snapshot(&mut self) {
114 let Some(snapshot_handle) = self.plugin_manager.read().unwrap().state_snapshot_handle()
115 else {
116 return;
117 };
118 let mut snapshot = snapshot_handle.write().unwrap();
119
120 self.active_window_mut()
121 .populate_plugin_state_snapshot(&mut snapshot);
122
123 snapshot.clipboard = self.clipboard.get_internal().to_string();
127 snapshot.working_dir = self.working_dir.clone();
128
129 snapshot.terminal_width = self.terminal_width;
133 snapshot.terminal_height = self.terminal_height;
134
135 snapshot.authority_label = self.authority.display_label.clone();
142
143 let mut session_infos: Vec<fresh_core::api::WindowInfo> = self
149 .windows
150 .values()
151 .map(|s| {
152 let slot = s.plugin_state.get("orchestrator");
153 let project_path = slot
154 .and_then(|m| m.get("project_path"))
155 .and_then(|v| v.as_str())
156 .map(std::path::PathBuf::from);
157 let shared_worktree = slot
158 .and_then(|m| m.get("shared_worktree"))
159 .and_then(|v| v.as_bool())
160 .unwrap_or(false);
161 fresh_core::api::WindowInfo {
162 id: s.id,
163 label: s.label.clone(),
164 root: s.root.clone(),
165 project_path,
166 shared_worktree,
167 }
168 })
169 .collect();
170 session_infos.sort_by_key(|s| s.id.0);
171 snapshot.windows = session_infos;
172 snapshot.active_window_id = self.active_window;
173
174 if !Arc::ptr_eq(&self.config, &self.config_snapshot_anchor) {
183 let json = serde_json::to_value(&*self.config).unwrap_or(serde_json::Value::Null);
184 self.config_cached_json = Arc::new(json);
185 self.config_snapshot_anchor = Arc::clone(&self.config);
186 }
187 snapshot.config = Arc::clone(&self.config_cached_json);
188
189 snapshot.user_config = Arc::clone(&self.user_config_raw);
192
193 for (plugin_name, state_map) in &self.plugin_global_state {
196 let entry = snapshot
197 .plugin_global_states
198 .entry(plugin_name.clone())
199 .or_default();
200 for (key, value) in state_map {
201 entry.entry(key.clone()).or_insert_with(|| value.clone());
202 }
203 }
204 }
205
206 pub fn handle_plugin_command(&mut self, command: PluginCommand) -> AnyhowResult<()> {
208 match command {
209 PluginCommand::InsertText {
211 buffer_id,
212 position,
213 text,
214 } => {
215 self.handle_insert_text(buffer_id, position, text);
216 }
217 PluginCommand::DeleteRange { buffer_id, range } => {
218 self.handle_delete_range(buffer_id, range);
219 }
220 PluginCommand::InsertAtCursor { text } => {
221 self.handle_insert_at_cursor(text);
222 }
223 PluginCommand::DeleteSelection => {
224 self.handle_delete_selection();
225 }
226
227 PluginCommand::AddOverlay {
229 buffer_id,
230 namespace,
231 range,
232 options,
233 } => {
234 self.handle_add_overlay(buffer_id, namespace, range, options);
235 }
236 PluginCommand::RemoveOverlay { buffer_id, handle } => {
237 self.handle_remove_overlay(buffer_id, handle);
238 }
239 PluginCommand::ClearAllOverlays { buffer_id } => {
240 self.handle_clear_all_overlays(buffer_id);
241 }
242 PluginCommand::ClearNamespace {
243 buffer_id,
244 namespace,
245 } => {
246 self.handle_clear_namespace(buffer_id, namespace);
247 }
248 PluginCommand::ClearOverlaysInRange {
249 buffer_id,
250 start,
251 end,
252 } => {
253 self.handle_clear_overlays_in_range(buffer_id, start, end);
254 }
255
256 PluginCommand::AddVirtualText {
258 buffer_id,
259 virtual_text_id,
260 position,
261 text,
262 color,
263 use_bg,
264 before,
265 } => {
266 self.handle_add_virtual_text(
267 buffer_id,
268 virtual_text_id,
269 position,
270 text,
271 color,
272 use_bg,
273 before,
274 );
275 }
276 PluginCommand::AddVirtualTextStyled {
277 buffer_id,
278 virtual_text_id,
279 position,
280 text,
281 fg,
282 bg,
283 bold,
284 italic,
285 before,
286 } => {
287 self.handle_add_virtual_text_styled(
288 buffer_id,
289 virtual_text_id,
290 position,
291 text,
292 fg,
293 bg,
294 bold,
295 italic,
296 before,
297 );
298 }
299 PluginCommand::RemoveVirtualText {
300 buffer_id,
301 virtual_text_id,
302 } => {
303 self.handle_remove_virtual_text(buffer_id, virtual_text_id);
304 }
305 PluginCommand::RemoveVirtualTextsByPrefix { buffer_id, prefix } => {
306 self.handle_remove_virtual_texts_by_prefix(buffer_id, prefix);
307 }
308 PluginCommand::ClearVirtualTexts { buffer_id } => {
309 self.handle_clear_virtual_texts(buffer_id);
310 }
311 PluginCommand::AddVirtualLine {
312 buffer_id,
313 position,
314 text,
315 fg_color,
316 bg_color,
317 above,
318 namespace,
319 priority,
320 gutter_glyph,
321 gutter_color,
322 text_overlays,
323 } => {
324 self.handle_add_virtual_line(
325 buffer_id,
326 position,
327 text,
328 fg_color,
329 bg_color,
330 above,
331 namespace,
332 priority,
333 gutter_glyph,
334 gutter_color,
335 text_overlays,
336 );
337 }
338 PluginCommand::ClearVirtualTextNamespace {
339 buffer_id,
340 namespace,
341 } => {
342 self.handle_clear_virtual_text_namespace(buffer_id, namespace);
343 }
344
345 PluginCommand::AddConceal {
347 buffer_id,
348 namespace,
349 start,
350 end,
351 replacement,
352 } => {
353 self.handle_add_conceal(buffer_id, namespace, start, end, replacement);
354 }
355 PluginCommand::ClearConcealNamespace {
356 buffer_id,
357 namespace,
358 } => {
359 self.handle_clear_conceal_namespace(buffer_id, namespace);
360 }
361 PluginCommand::ClearConcealsInRange {
362 buffer_id,
363 start,
364 end,
365 } => {
366 self.handle_clear_conceals_in_range(buffer_id, start, end);
367 }
368
369 PluginCommand::AddFold {
370 buffer_id,
371 start,
372 end,
373 placeholder,
374 } => {
375 self.handle_add_fold(buffer_id, start, end, placeholder);
376 }
377 PluginCommand::ClearFolds { buffer_id } => {
378 self.handle_clear_folds(buffer_id);
379 }
380 PluginCommand::SetFoldingRanges { buffer_id, ranges } => {
381 self.handle_set_folding_ranges(buffer_id, ranges);
382 }
383
384 PluginCommand::AddSoftBreak {
386 buffer_id,
387 namespace,
388 position,
389 indent,
390 } => {
391 self.handle_add_soft_break(buffer_id, namespace, position, indent);
392 }
393 PluginCommand::ClearSoftBreakNamespace {
394 buffer_id,
395 namespace,
396 } => {
397 self.handle_clear_soft_break_namespace(buffer_id, namespace);
398 }
399 PluginCommand::ClearSoftBreaksInRange {
400 buffer_id,
401 start,
402 end,
403 } => {
404 self.handle_clear_soft_breaks_in_range(buffer_id, start, end);
405 }
406
407 PluginCommand::AddMenuItem {
409 menu_label,
410 item,
411 position,
412 } => {
413 self.handle_add_menu_item(menu_label, item, position);
414 }
415 PluginCommand::AddMenu { menu, position } => {
416 self.handle_add_menu(menu, position);
417 }
418 PluginCommand::RemoveMenuItem {
419 menu_label,
420 item_label,
421 } => {
422 self.handle_remove_menu_item(menu_label, item_label);
423 }
424 PluginCommand::RemoveMenu { menu_label } => {
425 self.handle_remove_menu(menu_label);
426 }
427
428 PluginCommand::FocusSplit { split_id } => {
430 self.handle_focus_split(split_id);
431 }
432 PluginCommand::SetSplitBuffer {
433 split_id,
434 buffer_id,
435 } => {
436 self.handle_set_split_buffer(split_id, buffer_id);
437 }
438 PluginCommand::SetSplitScroll { split_id, top_byte } => {
439 self.handle_set_split_scroll(split_id, top_byte);
440 }
441 PluginCommand::RequestHighlights {
442 buffer_id,
443 range,
444 request_id,
445 } => {
446 self.handle_request_highlights(buffer_id, range, request_id);
447 }
448 PluginCommand::CloseSplit { split_id } => {
449 self.handle_close_split(split_id);
450 }
451 PluginCommand::SetSplitRatio { split_id, ratio } => {
452 self.handle_set_split_ratio(split_id, ratio);
453 }
454 PluginCommand::SetSplitLabel { split_id, label } => {
455 self.windows
456 .get_mut(&self.active_window)
457 .and_then(|w| w.split_manager_mut())
458 .expect("active window must have a populated split layout")
459 .set_label(LeafId(split_id), label);
460 }
461 PluginCommand::ClearSplitLabel { split_id } => {
462 self.windows
463 .get_mut(&self.active_window)
464 .and_then(|w| w.split_manager_mut())
465 .expect("active window must have a populated split layout")
466 .clear_label(split_id);
467 }
468 PluginCommand::GetSplitByLabel { label, request_id } => {
469 self.handle_get_split_by_label(label, request_id);
470 }
471 PluginCommand::DistributeSplitsEvenly { split_ids: _ } => {
472 self.handle_distribute_splits_evenly();
473 }
474 PluginCommand::SetBufferCursor {
475 buffer_id,
476 position,
477 } => {
478 self.handle_set_buffer_cursor(buffer_id, position);
479 }
480 PluginCommand::SetBufferShowCursors { buffer_id, show } => {
481 self.handle_set_buffer_show_cursors(buffer_id, show);
482 }
483
484 PluginCommand::SetLayoutHints {
486 buffer_id,
487 split_id,
488 range: _,
489 hints,
490 } => {
491 self.handle_set_layout_hints(buffer_id, split_id, hints);
492 }
493 PluginCommand::SetLineNumbers { buffer_id, enabled } => {
494 self.handle_set_line_numbers(buffer_id, enabled);
495 }
496 PluginCommand::SetViewMode { buffer_id, mode } => {
497 self.handle_set_view_mode(buffer_id, &mode);
498 }
499 PluginCommand::SetLineWrap {
500 buffer_id,
501 split_id,
502 enabled,
503 } => {
504 self.handle_set_line_wrap(buffer_id, split_id, enabled);
505 }
506 PluginCommand::SubmitViewTransform {
507 buffer_id,
508 split_id,
509 payload,
510 } => {
511 self.handle_submit_view_transform(buffer_id, split_id, payload);
512 }
513 PluginCommand::ClearViewTransform {
514 buffer_id: _,
515 split_id,
516 } => {
517 self.handle_clear_view_transform(split_id);
518 }
519 PluginCommand::SetViewState {
520 buffer_id,
521 key,
522 value,
523 } => {
524 self.handle_set_view_state(buffer_id, key, value);
525 }
526 PluginCommand::SetGlobalState {
527 plugin_name,
528 key,
529 value,
530 } => {
531 self.handle_set_global_state(plugin_name, key, value);
532 }
533 PluginCommand::SetWindowState {
534 plugin_name,
535 key,
536 value,
537 } => {
538 self.handle_set_session_state(plugin_name, key, value);
539 }
540 PluginCommand::RefreshLines { buffer_id } => {
541 self.handle_refresh_lines(buffer_id);
542 }
543 PluginCommand::RefreshAllLines => {
544 self.handle_refresh_all_lines();
545 }
546 PluginCommand::HookCompleted { .. } => {
547 }
549 PluginCommand::SetLineIndicator {
550 buffer_id,
551 line,
552 namespace,
553 symbol,
554 color,
555 priority,
556 } => {
557 self.handle_set_line_indicator(buffer_id, line, namespace, symbol, color, priority);
558 }
559 PluginCommand::SetLineIndicators {
560 buffer_id,
561 lines,
562 namespace,
563 symbol,
564 color,
565 priority,
566 } => {
567 self.handle_set_line_indicators(
568 buffer_id, lines, namespace, symbol, color, priority,
569 );
570 }
571 PluginCommand::ClearLineIndicators {
572 buffer_id,
573 namespace,
574 } => {
575 self.handle_clear_line_indicators(buffer_id, namespace);
576 }
577 PluginCommand::SetFileExplorerDecorations {
578 namespace,
579 decorations,
580 } => {
581 self.active_window_mut()
582 .handle_set_file_explorer_decorations(namespace, decorations);
583 }
584 PluginCommand::ClearFileExplorerDecorations { namespace } => {
585 self.active_window_mut()
586 .handle_clear_file_explorer_decorations(&namespace);
587 }
588
589 PluginCommand::SetStatus { message } => {
591 self.handle_set_status(message);
592 }
593 PluginCommand::ApplyTheme { theme_name } => {
594 self.apply_theme(&theme_name);
595 }
596 PluginCommand::OverrideThemeColors { overrides } => {
597 self.handle_override_theme_colors(overrides);
598 }
599 PluginCommand::ReloadConfig => {
600 self.reload_config();
601 }
602 PluginCommand::SetSetting { path, value, .. } => {
603 self.handle_set_setting(path, value);
604 }
605 PluginCommand::AddPluginConfigField {
606 plugin_name,
607 field_name,
608 field_schema,
609 } => {
610 self.handle_add_plugin_config_field(plugin_name, field_name, field_schema);
611 }
612 PluginCommand::ReloadThemes { apply_theme } => {
613 self.reload_themes();
614 if let Some(theme_name) = apply_theme {
615 self.apply_theme(&theme_name);
616 }
617 }
618 PluginCommand::RegisterGrammar {
619 language,
620 grammar_path,
621 extensions,
622 } => {
623 self.handle_register_grammar(language, grammar_path, extensions);
624 }
625 PluginCommand::RegisterLanguageConfig { language, config } => {
626 self.handle_register_language_config(language, config);
627 }
628 PluginCommand::RegisterLspServer { language, config } => {
629 self.handle_register_lsp_server(language, config);
630 }
631 PluginCommand::ReloadGrammars { callback_id } => {
632 self.handle_reload_grammars(callback_id);
633 }
634 PluginCommand::StartPrompt {
635 label,
636 prompt_type,
637 floating_overlay,
638 } => {
639 self.handle_start_prompt(label, prompt_type, floating_overlay);
640 }
641 PluginCommand::StartPromptWithInitial {
642 label,
643 prompt_type,
644 initial_value,
645 floating_overlay,
646 } => {
647 self.handle_start_prompt_with_initial(
648 label,
649 prompt_type,
650 initial_value,
651 floating_overlay,
652 );
653 }
654 PluginCommand::StartPromptAsync {
655 label,
656 initial_value,
657 callback_id,
658 } => {
659 self.handle_start_prompt_async(label, initial_value, callback_id);
660 }
661 PluginCommand::AwaitNextKey { callback_id } => {
662 self.handle_await_next_key(callback_id);
663 }
664 PluginCommand::SetKeyCaptureActive { active } => {
665 self.active_window_mut().key_capture_active = active;
666 if !active {
667 self.active_window_mut().pending_key_capture_buffer.clear();
671 }
672 }
673 PluginCommand::SetPromptSuggestions { suggestions } => {
674 self.handle_set_prompt_suggestions(suggestions);
675 }
676 PluginCommand::SetPromptInputSync { sync } => {
677 if let Some(prompt) = &mut self.active_window_mut().prompt {
678 prompt.sync_input_on_navigate = sync;
679 }
680 }
681 PluginCommand::SetPromptTitle { title } => {
682 if let Some(prompt) = &mut self.active_window_mut().prompt {
683 prompt.title = title;
684 }
685 }
686 PluginCommand::SetPromptFooter { footer } => {
687 if let Some(prompt) = &mut self.active_window_mut().prompt {
688 prompt.footer = footer;
689 }
690 }
691 PluginCommand::SetPromptSelectedIndex { index } => {
692 if let Some(prompt) = &mut self.active_window_mut().prompt {
693 let len = prompt.suggestions.len();
694 if len > 0 {
695 let clamped = (index as usize).min(len - 1);
696 prompt.selected_suggestion = Some(clamped);
697 }
698 }
699 }
700
701 PluginCommand::CreateWindow { root, label } => {
704 if !root.is_absolute() {
705 tracing::warn!(
706 "CreateWindow rejected: root must be absolute, got {:?}",
707 root
708 );
709 } else {
710 let _ = self.create_window_at(root, label);
711 }
712 }
713 PluginCommand::SetActiveWindow { id } => {
714 self.set_active_window(id);
715 }
716 PluginCommand::CloseWindow { id } => {
717 let _ = self.close_window(id);
718 }
719 PluginCommand::PrewarmWindow { id } => {
720 self.prewarm_window(id);
721 }
722
723 PluginCommand::WatchPath {
725 path,
726 recursive,
727 request_id,
728 } => {
729 let result = if let Some(ref bridge) = self.async_bridge {
730 self.file_watcher_manager.watch(bridge, &path, recursive)
731 } else {
732 Err(
733 "watchPath: no async bridge — file watching is unavailable in this build"
734 .to_string(),
735 )
736 };
737 self.last_watch_response_for_test = Some((request_id, result.clone()));
738 self.send_plugin_response(fresh_core::api::PluginResponse::WatchPathRegistered {
739 request_id,
740 result,
741 });
742 }
743 PluginCommand::UnwatchPath { handle } => {
744 self.file_watcher_manager.unwatch(handle);
745 }
746
747 PluginCommand::PreviewWindowInRect { id } => {
748 self.preview_window_id = match id {
752 Some(sid) if sid != self.active_window && self.windows.contains_key(&sid) => {
753 Some(sid)
754 }
755 _ => None,
756 };
757 }
758
759 PluginCommand::RegisterCommand { command } => {
761 self.handle_register_command(command);
762 }
763 PluginCommand::RegisterStatusBarElement {
764 plugin_name,
765 token_name,
766 title,
767 } => {
768 if let Err(e) = self.register_status_bar_element(&plugin_name, &token_name, &title)
769 {
770 tracing::warn!("Failed to register statusbar element: {}", e);
771 }
772 }
773 PluginCommand::SetStatusBarValue {
774 buffer_id,
775 key,
776 value,
777 } => {
778 if let Err(e) =
779 self.set_status_bar_value(fresh_core::BufferId(buffer_id as usize), &key, value)
780 {
781 tracing::warn!("Failed to set statusbar value: {}", e);
782 }
783 }
784 PluginCommand::UnregisterCommand { name } => {
785 self.handle_unregister_command(name);
786 }
787 PluginCommand::DefineMode {
788 name,
789 bindings,
790 read_only,
791 allow_text_input,
792 inherit_normal_bindings,
793 plugin_name,
794 } => {
795 self.handle_define_mode(
796 name,
797 bindings,
798 read_only,
799 allow_text_input,
800 inherit_normal_bindings,
801 plugin_name,
802 );
803 }
804
805 PluginCommand::OpenFileInBackground { path, window_id } => {
807 let route_to_inactive = match window_id {
808 Some(id) if id != self.active_window && self.windows.contains_key(&id) => {
809 Some(id)
810 }
811 _ => None,
812 };
813 if let Some(target) = route_to_inactive {
814 self.handle_open_file_in_inactive_session(target, path);
815 } else {
816 self.handle_open_file_in_background(path);
817 }
818 }
819 PluginCommand::OpenFileAtLocation { path, line, column } => {
820 return self.handle_open_file_at_location(path, line, column);
821 }
822 PluginCommand::OpenFileInSplit {
823 split_id,
824 path,
825 line,
826 column,
827 } => {
828 return self.handle_open_file_in_split(split_id, path, line, column);
829 }
830 PluginCommand::ShowBuffer { buffer_id } => {
831 self.handle_show_buffer(buffer_id);
832 }
833 PluginCommand::CloseBuffer { buffer_id } => {
834 self.handle_close_buffer(buffer_id);
835 }
836 PluginCommand::CloseOtherBuffersInSplit {
837 buffer_id,
838 split_id,
839 } => {
840 self.handle_close_other_buffers_in_split(buffer_id, split_id);
841 }
842 PluginCommand::CloseAllBuffersInSplit { split_id } => {
843 self.handle_close_all_buffers_in_split(split_id);
844 }
845 PluginCommand::CloseBuffersToRightInSplit {
846 buffer_id,
847 split_id,
848 } => {
849 self.handle_close_buffers_to_right_in_split(buffer_id, split_id);
850 }
851 PluginCommand::CloseBuffersToLeftInSplit {
852 buffer_id,
853 split_id,
854 } => {
855 self.handle_close_buffers_to_left_in_split(buffer_id, split_id);
856 }
857
858 PluginCommand::MoveTabLeft => {
859 self.handle_move_tab_left();
860 }
861 PluginCommand::MoveTabRight => {
862 self.handle_move_tab_right();
863 }
864
865 PluginCommand::StartAnimationArea { id, rect, kind } => {
867 self.handle_start_animation_area(id, rect, kind);
868 }
869 PluginCommand::StartAnimationVirtualBuffer {
870 id,
871 buffer_id,
872 kind,
873 } => {
874 self.handle_start_animation_virtual_buffer(id, buffer_id, kind);
875 }
876 PluginCommand::CancelAnimation { id } => {
877 self.active_window_mut()
878 .animations
879 .cancel(crate::view::animation::AnimationId::from_raw(id));
880 }
881
882 PluginCommand::SendLspRequest {
884 language,
885 method,
886 params,
887 request_id,
888 } => {
889 self.handle_send_lsp_request(language, method, params, request_id);
890 }
891
892 PluginCommand::SetClipboard { text } => {
894 self.handle_set_clipboard(text);
895 }
896
897 PluginCommand::SpawnProcess {
899 command,
900 args,
901 cwd,
902 stdout_to,
903 callback_id,
904 } => {
905 self.handle_spawn_process(command, args, cwd, stdout_to, callback_id);
906 }
907
908 PluginCommand::SpawnHostProcess {
909 command,
910 args,
911 cwd,
912 callback_id,
913 } => {
914 self.handle_spawn_host_process(command, args, cwd, callback_id);
915 }
916
917 PluginCommand::KillHostProcess { process_id } => {
918 self.handle_kill_host_process(process_id);
919 }
920
921 PluginCommand::SetAuthority { payload } => {
922 self.handle_set_authority(payload);
923 }
924
925 PluginCommand::ClearAuthority => {
926 tracing::info!("Plugin cleared authority; restoring local");
927 self.clear_authority();
928 }
929
930 PluginCommand::SetRemoteIndicatorState { state } => {
931 self.handle_set_remote_indicator_state(state);
932 }
933
934 PluginCommand::ClearRemoteIndicatorState => {
935 self.remote_indicator_override = None;
936 }
937
938 PluginCommand::SpawnProcessWait {
939 process_id,
940 callback_id,
941 } => {
942 self.handle_spawn_process_wait(process_id, callback_id);
943 }
944
945 PluginCommand::Delay {
946 callback_id,
947 duration_ms,
948 } => {
949 self.handle_delay(callback_id, duration_ms);
950 }
951
952 PluginCommand::SpawnBackgroundProcess {
953 process_id,
954 command,
955 args,
956 cwd,
957 callback_id,
958 } => {
959 self.handle_spawn_background_process(process_id, command, args, cwd, callback_id);
960 }
961
962 PluginCommand::KillBackgroundProcess { process_id } => {
963 self.handle_kill_background_process(process_id);
964 }
965
966 PluginCommand::CreateVirtualBuffer {
968 name,
969 mode,
970 read_only,
971 } => {
972 self.handle_create_virtual_buffer(name, mode, read_only);
973 }
974 PluginCommand::CreateVirtualBufferWithContent {
975 name,
976 mode,
977 read_only,
978 entries,
979 show_line_numbers,
980 show_cursors,
981 editing_disabled,
982 hidden_from_tabs,
983 request_id,
984 } => {
985 self.handle_create_virtual_buffer_with_content(
986 name,
987 mode,
988 read_only,
989 entries,
990 show_line_numbers,
991 show_cursors,
992 editing_disabled,
993 hidden_from_tabs,
994 request_id,
995 );
996 }
997 PluginCommand::CreateVirtualBufferInSplit {
998 name,
999 mode,
1000 read_only,
1001 entries,
1002 ratio,
1003 direction,
1004 panel_id,
1005 show_line_numbers,
1006 show_cursors,
1007 editing_disabled,
1008 line_wrap,
1009 before,
1010 role,
1011 request_id,
1012 } => {
1013 self.handle_create_virtual_buffer_in_split(
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 }
1030 PluginCommand::SetVirtualBufferContent { buffer_id, entries } => {
1031 self.handle_set_virtual_buffer_content(buffer_id, entries);
1032 }
1033 PluginCommand::GetTextPropertiesAtCursor { buffer_id } => {
1034 self.handle_get_text_properties_at_cursor(buffer_id);
1035 }
1036 PluginCommand::CreateVirtualBufferInExistingSplit {
1037 name,
1038 mode,
1039 read_only,
1040 entries,
1041 split_id,
1042 show_line_numbers,
1043 show_cursors,
1044 editing_disabled,
1045 line_wrap,
1046 request_id,
1047 } => {
1048 self.handle_create_virtual_buffer_in_existing_split(
1049 name,
1050 mode,
1051 read_only,
1052 entries,
1053 split_id,
1054 show_line_numbers,
1055 show_cursors,
1056 editing_disabled,
1057 line_wrap,
1058 request_id,
1059 );
1060 }
1061
1062 PluginCommand::SetContext { name, active } => {
1064 self.handle_set_context(name, active);
1065 }
1066
1067 PluginCommand::SetReviewDiffHunks { hunks } => {
1069 self.active_window_mut().review_hunks = hunks;
1070 tracing::debug!(
1071 "Set {} review hunks",
1072 self.active_window_mut().review_hunks.len()
1073 );
1074 }
1075
1076 PluginCommand::ExecuteAction { action_name } => {
1078 self.handle_execute_action(action_name);
1079 }
1080 PluginCommand::ExecuteActions { actions } => {
1081 self.handle_execute_actions(actions);
1082 }
1083 PluginCommand::GetBufferText {
1084 buffer_id,
1085 start,
1086 end,
1087 request_id,
1088 } => {
1089 self.handle_get_buffer_text(buffer_id, start, end, request_id);
1090 }
1091 PluginCommand::GetLineStartPosition {
1092 buffer_id,
1093 line,
1094 request_id,
1095 } => {
1096 self.handle_get_line_start_position(buffer_id, line, request_id);
1097 }
1098 PluginCommand::GetLineEndPosition {
1099 buffer_id,
1100 line,
1101 request_id,
1102 } => {
1103 self.handle_get_line_end_position(buffer_id, line, request_id);
1104 }
1105 PluginCommand::GetBufferLineCount {
1106 buffer_id,
1107 request_id,
1108 } => {
1109 self.handle_get_buffer_line_count(buffer_id, request_id);
1110 }
1111 PluginCommand::OpenFileStreaming { path, request_id } => {
1112 self.handle_open_file_streaming(path, request_id);
1113 }
1114 PluginCommand::RefreshBufferFromDisk {
1115 buffer_id,
1116 request_id,
1117 } => {
1118 self.handle_refresh_buffer_from_disk(buffer_id, request_id);
1119 }
1120 PluginCommand::SetBufferGroupPanelBuffer {
1121 group_id,
1122 panel_name,
1123 buffer_id,
1124 request_id,
1125 } => {
1126 self.handle_set_buffer_group_panel_buffer(
1127 group_id, panel_name, buffer_id, request_id,
1128 );
1129 }
1130 PluginCommand::ScrollToLineCenter {
1131 split_id,
1132 buffer_id,
1133 line,
1134 } => {
1135 self.handle_scroll_to_line_center(split_id, buffer_id, line);
1136 }
1137 PluginCommand::ScrollBufferToLine { buffer_id, line } => {
1138 self.handle_scroll_buffer_to_line(buffer_id, line);
1139 }
1140 PluginCommand::SetEditorMode { mode } => {
1141 self.handle_set_editor_mode(mode);
1142 }
1143
1144 PluginCommand::ShowActionPopup {
1146 popup_id,
1147 title,
1148 message,
1149 actions,
1150 } => {
1151 self.handle_show_action_popup(popup_id, title, message, actions);
1152 }
1153
1154 PluginCommand::SetLspMenuContributions {
1155 plugin_id,
1156 language,
1157 items,
1158 } => {
1159 self.handle_set_lsp_menu_contributions(plugin_id, language, items);
1160 }
1161
1162 PluginCommand::DisableLspForLanguage { language } => {
1163 self.handle_disable_lsp_for_language(language);
1164 }
1165
1166 PluginCommand::RestartLspForLanguage { language } => {
1167 self.handle_restart_lsp_for_language(language);
1168 }
1169
1170 PluginCommand::SetLspRootUri { language, uri } => {
1171 self.handle_set_lsp_root_uri(language, uri);
1172 }
1173
1174 PluginCommand::CreateScrollSyncGroup {
1176 group_id,
1177 left_split,
1178 right_split,
1179 } => {
1180 self.handle_create_scroll_sync_group(group_id, left_split, right_split);
1181 }
1182 PluginCommand::SetScrollSyncAnchors { group_id, anchors } => {
1183 self.handle_set_scroll_sync_anchors(group_id, anchors);
1184 }
1185 PluginCommand::RemoveScrollSyncGroup { group_id } => {
1186 self.handle_remove_scroll_sync_group(group_id);
1187 }
1188
1189 PluginCommand::CreateCompositeBuffer {
1191 name,
1192 mode,
1193 layout,
1194 sources,
1195 hunks,
1196 initial_focus_hunk,
1197 request_id,
1198 } => {
1199 self.handle_create_composite_buffer(
1200 name,
1201 mode,
1202 layout,
1203 sources,
1204 hunks,
1205 initial_focus_hunk,
1206 request_id,
1207 );
1208 }
1209 PluginCommand::UpdateCompositeAlignment { buffer_id, hunks } => {
1210 self.handle_update_composite_alignment(buffer_id, hunks);
1211 }
1212 PluginCommand::CloseCompositeBuffer { buffer_id } => {
1213 self.active_window_mut().close_composite_buffer(buffer_id);
1214 }
1215 PluginCommand::FlushLayout => {
1216 self.flush_layout();
1217 }
1218 PluginCommand::CompositeNextHunk { buffer_id } => {
1219 let split_id = self
1220 .windows
1221 .get(&self.active_window)
1222 .and_then(|w| w.buffers.splits())
1223 .map(|(mgr, _)| mgr)
1224 .expect("active window must have a populated split layout")
1225 .active_split();
1226 self.active_window_mut()
1227 .composite_next_hunk(split_id, buffer_id);
1228 }
1229 PluginCommand::CompositePrevHunk { buffer_id } => {
1230 let split_id = self
1231 .windows
1232 .get(&self.active_window)
1233 .and_then(|w| w.buffers.splits())
1234 .map(|(mgr, _)| mgr)
1235 .expect("active window must have a populated split layout")
1236 .active_split();
1237 self.active_window_mut()
1238 .composite_prev_hunk(split_id, buffer_id);
1239 }
1240
1241 PluginCommand::CreateBufferGroup {
1243 name,
1244 mode,
1245 layout_json,
1246 request_id,
1247 } => {
1248 self.handle_create_buffer_group(name, mode, layout_json, request_id);
1249 }
1250 PluginCommand::SetPanelContent {
1251 group_id,
1252 panel_name,
1253 entries,
1254 } => {
1255 self.set_panel_content(group_id, panel_name, entries);
1256 }
1257 PluginCommand::CloseBufferGroup { group_id } => {
1258 self.close_buffer_group(group_id);
1259 }
1260 PluginCommand::FocusPanel {
1261 group_id,
1262 panel_name,
1263 } => {
1264 self.focus_panel(group_id, panel_name);
1265 }
1266
1267 PluginCommand::SaveBufferToPath { buffer_id, path } => {
1269 self.handle_save_buffer_to_path(buffer_id, path);
1270 }
1271
1272 #[cfg(feature = "plugins")]
1274 PluginCommand::LoadPlugin { path, callback_id } => {
1275 self.handle_load_plugin(path, callback_id);
1276 }
1277 #[cfg(feature = "plugins")]
1278 PluginCommand::UnloadPlugin { name, callback_id } => {
1279 self.handle_unload_plugin(name, callback_id);
1280 }
1281 #[cfg(feature = "plugins")]
1282 PluginCommand::ReloadPlugin { name, callback_id } => {
1283 self.handle_reload_plugin(name, callback_id);
1284 }
1285 #[cfg(feature = "plugins")]
1286 PluginCommand::ListPlugins { callback_id } => {
1287 self.handle_list_plugins(callback_id);
1288 }
1289 #[cfg(not(feature = "plugins"))]
1291 PluginCommand::LoadPlugin { .. }
1292 | PluginCommand::UnloadPlugin { .. }
1293 | PluginCommand::ReloadPlugin { .. }
1294 | PluginCommand::ListPlugins { .. } => {
1295 tracing::warn!("Plugin management commands require the 'plugins' feature");
1296 }
1297
1298 PluginCommand::CreateTerminal {
1300 cwd,
1301 direction,
1302 ratio,
1303 focus,
1304 persistent,
1305 window_id,
1306 command,
1307 title,
1308 request_id,
1309 } => {
1310 self.handle_create_terminal(
1311 cwd, direction, ratio, focus, persistent, window_id, command, title, request_id,
1312 );
1313 }
1314
1315 PluginCommand::SendTerminalInput { terminal_id, data } => {
1316 self.handle_send_terminal_input(terminal_id, data);
1317 }
1318
1319 PluginCommand::CloseTerminal { terminal_id } => {
1320 self.handle_close_terminal(terminal_id);
1321 }
1322
1323 PluginCommand::SignalWindow { id, signal } => {
1324 self.handle_signal_window(id, &signal);
1325 }
1326
1327 PluginCommand::GrepProject {
1328 pattern,
1329 fixed_string,
1330 case_sensitive,
1331 max_results,
1332 whole_words,
1333 callback_id,
1334 } => {
1335 self.handle_grep_project(
1336 pattern,
1337 fixed_string,
1338 case_sensitive,
1339 max_results,
1340 whole_words,
1341 callback_id,
1342 );
1343 }
1344
1345 PluginCommand::BeginSearch {
1346 pattern,
1347 fixed_string,
1348 case_sensitive,
1349 max_results,
1350 whole_words,
1351 handle_id,
1352 } => {
1353 self.handle_begin_search(
1354 pattern,
1355 fixed_string,
1356 case_sensitive,
1357 max_results,
1358 whole_words,
1359 handle_id,
1360 );
1361 }
1362
1363 PluginCommand::ReplaceInBuffer {
1364 file_path,
1365 matches,
1366 replacement,
1367 callback_id,
1368 } => {
1369 self.handle_replace_in_buffer(file_path, matches, replacement, callback_id);
1370 }
1371
1372 PluginCommand::MountWidgetPanel {
1373 panel_id,
1374 buffer_id,
1375 spec,
1376 } => {
1377 self.handle_mount_widget_panel(panel_id, buffer_id, spec);
1378 }
1379
1380 PluginCommand::UpdateWidgetPanel { panel_id, spec } => {
1381 self.handle_update_widget_panel(panel_id, spec);
1382 }
1383
1384 PluginCommand::UnmountWidgetPanel { panel_id } => {
1385 self.handle_unmount_widget_panel(panel_id);
1386 }
1387
1388 PluginCommand::WidgetCommand { panel_id, action } => {
1389 self.handle_widget_command(panel_id, action);
1390 }
1391
1392 PluginCommand::WidgetMutate { panel_id, mutation } => {
1393 self.handle_widget_mutate(panel_id, mutation);
1394 }
1395
1396 PluginCommand::MountFloatingWidget {
1397 panel_id,
1398 spec,
1399 width_pct,
1400 height_pct,
1401 } => {
1402 self.handle_mount_floating_widget(panel_id, spec, width_pct, height_pct);
1403 }
1404
1405 PluginCommand::UpdateFloatingWidget { panel_id, spec } => {
1406 self.handle_update_floating_widget(panel_id, spec);
1407 }
1408
1409 PluginCommand::UnmountFloatingWidget { panel_id } => {
1410 self.handle_unmount_floating_widget(panel_id);
1411 }
1412 }
1413 Ok(())
1414 }
1415
1416 fn handle_save_buffer_to_path(&mut self, buffer_id: BufferId, path: std::path::PathBuf) {
1418 if let Some(state) = self
1419 .windows
1420 .get_mut(&self.active_window)
1421 .map(|w| &mut w.buffers)
1422 .expect("active window present")
1423 .get_mut(&buffer_id)
1424 {
1425 match state.buffer.save_to_file(&path) {
1427 Ok(()) => {
1428 if let Err(e) = self.finalize_save(Some(path)) {
1431 tracing::warn!("Failed to finalize save: {}", e);
1432 }
1433 tracing::debug!("Saved buffer {:?} to path", buffer_id);
1434 }
1435 Err(e) => {
1436 self.handle_set_status(format!("Error saving: {}", e));
1437 tracing::error!("Failed to save buffer to path: {}", e);
1438 }
1439 }
1440 } else {
1441 self.handle_set_status(format!("Buffer {:?} not found", buffer_id));
1442 tracing::warn!("SaveBufferToPath: buffer {:?} not found", buffer_id);
1443 }
1444 }
1445
1446 #[cfg(feature = "plugins")]
1448 fn handle_load_plugin(&mut self, path: std::path::PathBuf, callback_id: JsCallbackId) {
1449 let load_result = self.plugin_manager.read().unwrap().load_plugin(&path);
1450 match load_result {
1451 Ok(()) => {
1452 tracing::info!("Loaded plugin from {:?}", path);
1453 self.plugin_manager
1454 .read()
1455 .unwrap()
1456 .resolve_callback(callback_id, "true".to_string());
1457 }
1458 Err(e) => {
1459 tracing::error!("Failed to load plugin from {:?}: {}", path, e);
1460 self.plugin_manager
1461 .read()
1462 .unwrap()
1463 .reject_callback(callback_id, format!("{}", e));
1464 }
1465 }
1466 }
1467
1468 #[cfg(feature = "plugins")]
1470 fn handle_unload_plugin(&mut self, name: String, callback_id: JsCallbackId) {
1471 let result = self.plugin_manager.write().unwrap().unload_plugin(&name);
1474 match result {
1475 Ok(()) => {
1476 tracing::info!("Unloaded plugin: {}", name);
1477 if let Ok(mut schemas) = self.plugin_schemas.write() {
1478 schemas.remove(&name);
1479 }
1480 self.plugin_manager
1481 .read()
1482 .unwrap()
1483 .resolve_callback(callback_id, "true".to_string());
1484 }
1485 Err(e) => {
1486 tracing::error!("Failed to unload plugin '{}': {}", name, e);
1487 self.plugin_manager
1488 .read()
1489 .unwrap()
1490 .reject_callback(callback_id, format!("{}", e));
1491 }
1492 }
1493 }
1494
1495 #[cfg(feature = "plugins")]
1497 fn handle_reload_plugin(&mut self, name: String, callback_id: JsCallbackId) {
1498 let path = self
1502 .plugin_manager
1503 .read()
1504 .unwrap()
1505 .list_plugins()
1506 .into_iter()
1507 .find(|p| p.name == name)
1508 .map(|p| p.path);
1509 let _ = path; let reload_result = self.plugin_manager.read().unwrap().reload_plugin(&name);
1511 match reload_result {
1512 Ok(()) => {
1513 tracing::info!("Reloaded plugin: {}", name);
1514 self.plugin_manager
1515 .read()
1516 .unwrap()
1517 .resolve_callback(callback_id, "true".to_string());
1518 }
1519 Err(e) => {
1520 tracing::error!("Failed to reload plugin '{}': {}", name, e);
1521 self.plugin_manager
1522 .read()
1523 .unwrap()
1524 .reject_callback(callback_id, format!("{}", e));
1525 }
1526 }
1527 }
1528
1529 #[cfg(feature = "plugins")]
1531 fn handle_list_plugins(&mut self, callback_id: JsCallbackId) {
1532 let plugins = self.plugin_manager.read().unwrap().list_plugins();
1533 let json_array: Vec<serde_json::Value> = plugins
1535 .iter()
1536 .map(|p| {
1537 serde_json::json!({
1538 "name": p.name,
1539 "path": p.path.to_string_lossy(),
1540 "enabled": p.enabled
1541 })
1542 })
1543 .collect();
1544 let json_str = serde_json::to_string(&json_array).unwrap_or_else(|_| "[]".to_string());
1545 self.plugin_manager
1546 .read()
1547 .unwrap()
1548 .resolve_callback(callback_id, json_str);
1549 }
1550
1551 fn handle_execute_action(&mut self, action_name: String) {
1553 use crate::input::keybindings::Action;
1554 use std::collections::HashMap;
1555
1556 if let Some(action) = Action::from_str(&action_name, &HashMap::new()) {
1558 if let Err(e) = self.handle_action(action) {
1560 tracing::warn!("Failed to execute action '{}': {}", action_name, e);
1561 } else {
1562 tracing::debug!("Executed action: {}", action_name);
1563 }
1564 } else {
1565 tracing::warn!("Unknown action: {}", action_name);
1566 }
1567 }
1568
1569 fn handle_execute_actions(&mut self, actions: Vec<fresh_core::api::ActionSpec>) {
1572 use crate::input::keybindings::Action;
1573 use std::collections::HashMap;
1574
1575 for action_spec in actions {
1576 if let Some(action) = Action::from_str(&action_spec.action, &HashMap::new()) {
1577 for _ in 0..action_spec.count {
1579 if let Err(e) = self.handle_action(action.clone()) {
1580 tracing::warn!("Failed to execute action '{}': {}", action_spec.action, e);
1581 return; }
1583 }
1584 tracing::debug!(
1585 "Executed action '{}' {} time(s)",
1586 action_spec.action,
1587 action_spec.count
1588 );
1589 } else {
1590 tracing::warn!("Unknown action: {}", action_spec.action);
1591 return; }
1593 }
1594 }
1595
1596 fn handle_get_buffer_text(
1598 &mut self,
1599 buffer_id: BufferId,
1600 start: usize,
1601 end: usize,
1602 request_id: u64,
1603 ) {
1604 let result = if let Some(state) = self
1605 .windows
1606 .get_mut(&self.active_window)
1607 .map(|w| &mut w.buffers)
1608 .expect("active window present")
1609 .get_mut(&buffer_id)
1610 {
1611 let len = state.buffer.len();
1613 if start <= end && end <= len {
1614 Ok(state.get_text_range(start, end))
1615 } else {
1616 Err(format!(
1617 "Invalid range {}..{} for buffer of length {}",
1618 start, end, len
1619 ))
1620 }
1621 } else {
1622 Err(format!("Buffer {:?} not found", buffer_id))
1623 };
1624
1625 let callback_id = fresh_core::api::JsCallbackId::from(request_id);
1627 match result {
1628 Ok(text) => {
1629 let json = serde_json::to_string(&text).unwrap_or_else(|_| "null".to_string());
1631 self.plugin_manager
1632 .read()
1633 .unwrap()
1634 .resolve_callback(callback_id, json);
1635 }
1636 Err(error) => {
1637 self.plugin_manager
1638 .read()
1639 .unwrap()
1640 .reject_callback(callback_id, error);
1641 }
1642 }
1643 }
1644
1645 fn handle_set_editor_mode(&mut self, mode: Option<String>) {
1647 self.active_window_mut().editor_mode = mode.clone();
1648 tracing::debug!("Set editor mode: {:?}", mode);
1649 }
1650
1651 fn resolve_buffer_id(&self, buffer_id: BufferId) -> BufferId {
1653 if buffer_id.0 == 0 {
1654 self.active_buffer()
1655 } else {
1656 buffer_id
1657 }
1658 }
1659
1660 fn resolve_json_callback<T: serde::Serialize>(&mut self, request_id: u64, value: T) {
1662 let callback_id = fresh_core::api::JsCallbackId::from(request_id);
1663 let json = serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string());
1664 self.plugin_manager
1665 .read()
1666 .unwrap()
1667 .resolve_callback(callback_id, json);
1668 }
1669
1670 fn handle_get_line_start_position(&mut self, buffer_id: BufferId, line: u32, request_id: u64) {
1672 let actual_buffer_id = self.resolve_buffer_id(buffer_id);
1673 let result = self
1674 .windows
1675 .get_mut(&self.active_window)
1676 .map(|w| &mut w.buffers)
1677 .expect("active window present")
1678 .get_mut(&actual_buffer_id)
1679 .and_then(|state| {
1680 let len = state.buffer.len();
1681 let content = state.get_text_range(0, len);
1682 buffer_line_byte_offset(&content, len, line as usize, false)
1683 });
1684 self.resolve_json_callback(request_id, result);
1685 }
1686
1687 fn handle_get_line_end_position(&mut self, buffer_id: BufferId, line: u32, request_id: u64) {
1690 let actual_buffer_id = self.resolve_buffer_id(buffer_id);
1691 let result = self
1692 .windows
1693 .get_mut(&self.active_window)
1694 .map(|w| &mut w.buffers)
1695 .expect("active window present")
1696 .get_mut(&actual_buffer_id)
1697 .and_then(|state| {
1698 let len = state.buffer.len();
1699 let content = state.get_text_range(0, len);
1700 buffer_line_byte_offset(&content, len, line as usize, true)
1701 });
1702 self.resolve_json_callback(request_id, result);
1703 }
1704
1705 fn handle_get_buffer_line_count(&mut self, buffer_id: BufferId, request_id: u64) {
1707 let actual_buffer_id = self.resolve_buffer_id(buffer_id);
1708
1709 let result = if let Some(state) = self
1710 .windows
1711 .get_mut(&self.active_window)
1712 .map(|w| &mut w.buffers)
1713 .expect("active window present")
1714 .get_mut(&actual_buffer_id)
1715 {
1716 let buffer_len = state.buffer.len();
1717 let content = state.get_text_range(0, buffer_len);
1718
1719 if content.is_empty() {
1721 Some(1) } else {
1723 let newline_count = content.chars().filter(|&c| c == '\n').count();
1724 let ends_with_newline = content.ends_with('\n');
1726 if ends_with_newline {
1727 Some(newline_count)
1728 } else {
1729 Some(newline_count + 1)
1730 }
1731 }
1732 } else {
1733 None
1734 };
1735
1736 self.resolve_json_callback(request_id, result);
1737 }
1738
1739 fn handle_open_file_streaming(&mut self, path: std::path::PathBuf, request_id: u64) {
1756 if !self.authority.filesystem.exists(&path) {
1759 if let Some(parent) = path.parent() {
1760 if !parent.as_os_str().is_empty() {
1761 if let Err(e) = std::fs::create_dir_all(parent) {
1762 tracing::warn!(
1763 "openFileStreaming: failed to create parent dir {:?}: {}",
1764 parent,
1765 e
1766 );
1767 self.resolve_json_callback::<Option<u64>>(request_id, None);
1768 return;
1769 }
1770 }
1771 }
1772 if let Err(e) = std::fs::write(&path, b"") {
1773 tracing::warn!(
1774 "openFileStreaming: failed to create empty file at {:?}: {}",
1775 path,
1776 e
1777 );
1778 self.resolve_json_callback::<Option<u64>>(request_id, None);
1779 return;
1780 }
1781 }
1782
1783 let buffer_id = match self.open_file_no_focus(&path) {
1787 Ok(id) => id,
1788 Err(e) => {
1789 tracing::warn!(
1790 "openFileStreaming: open_file_no_focus failed for {:?}: {}",
1791 path,
1792 e
1793 );
1794 self.resolve_json_callback::<Option<u64>>(request_id, None);
1795 return;
1796 }
1797 };
1798
1799 if let Some(meta) = self.active_window_mut().buffer_metadata.get_mut(&buffer_id) {
1804 meta.hidden_from_tabs = true;
1805 meta.auto_revert_enabled = false;
1806 }
1807 let active_split = self
1808 .windows
1809 .get(&self.active_window)
1810 .and_then(|w| w.buffers.splits())
1811 .map(|(mgr, _)| mgr)
1812 .expect("active window must have a populated split layout")
1813 .active_split();
1814 if let Some(vs) = self
1815 .windows
1816 .get_mut(&self.active_window)
1817 .and_then(|w| w.split_view_states_mut())
1818 .expect("active window must have a populated split layout")
1819 .get_mut(&active_split)
1820 {
1821 use crate::view::split::TabTarget;
1822 vs.open_buffers
1823 .retain(|t| !matches!(t, TabTarget::Buffer(b) if *b == buffer_id));
1824 }
1825
1826 self.resolve_json_callback(request_id, Some(buffer_id.0));
1827 }
1828
1829 fn handle_set_buffer_group_panel_buffer(
1832 &mut self,
1833 group_id: usize,
1834 panel_name: String,
1835 buffer_id: BufferId,
1836 request_id: u64,
1837 ) {
1838 let actual_buffer_id = self.resolve_buffer_id(buffer_id);
1839 let ok = self.set_buffer_group_panel_buffer(group_id, panel_name, actual_buffer_id);
1840 self.resolve_json_callback(request_id, ok);
1841 }
1842
1843 fn handle_refresh_buffer_from_disk(&mut self, buffer_id: BufferId, request_id: u64) {
1847 let actual_buffer_id = self.resolve_buffer_id(buffer_id);
1848
1849 let path = self
1850 .windows
1851 .get(&self.active_window)
1852 .and_then(|w| w.buffers.splits())
1853 .map(|(_, _)| ())
1854 .and_then(|_| {
1855 self.windows
1856 .get(&self.active_window)?
1857 .buffers
1858 .get(&actual_buffer_id)?
1859 .buffer
1860 .file_path()
1861 .map(|p| p.to_path_buf())
1862 });
1863
1864 let Some(path) = path else {
1865 self.resolve_json_callback::<Option<usize>>(request_id, None);
1867 return;
1868 };
1869
1870 let new_size = match self.authority.filesystem.metadata(&path) {
1871 Ok(m) => m.size as usize,
1872 Err(_) => {
1873 self.resolve_json_callback::<Option<usize>>(request_id, None);
1874 return;
1875 }
1876 };
1877
1878 let new_total = if let Some(state) = self
1879 .windows
1880 .get_mut(&self.active_window)
1881 .map(|w| &mut w.buffers)
1882 .expect("active window present")
1883 .get_mut(&actual_buffer_id)
1884 {
1885 let old = state.buffer.total_bytes();
1886 if new_size > old {
1887 state.buffer.extend_streaming(&path, new_size);
1888 }
1889 state.buffer.total_bytes()
1890 } else {
1891 self.resolve_json_callback::<Option<usize>>(request_id, None);
1892 return;
1893 };
1894
1895 self.resolve_json_callback(request_id, Some(new_total));
1896 }
1897
1898 fn handle_scroll_to_line_center(
1900 &mut self,
1901 split_id: SplitId,
1902 buffer_id: BufferId,
1903 line: usize,
1904 ) {
1905 let actual_split_id = if split_id.0 == 0 {
1906 self.windows
1907 .get(&self.active_window)
1908 .and_then(|w| w.buffers.splits())
1909 .map(|(mgr, _)| mgr)
1910 .expect("active window must have a populated split layout")
1911 .active_split()
1912 } else {
1913 LeafId(split_id)
1914 };
1915 let actual_buffer_id = self.resolve_buffer_id(buffer_id);
1916
1917 let viewport_height = if let Some(view_state) = self
1919 .windows
1920 .get(&self.active_window)
1921 .and_then(|w| w.buffers.splits())
1922 .map(|(_, vs)| vs)
1923 .expect("active window must have a populated split layout")
1924 .get(&actual_split_id)
1925 {
1926 view_state.viewport.height as usize
1927 } else {
1928 return;
1929 };
1930
1931 let lines_above = viewport_height / 2;
1933 let target_line = line.saturating_sub(lines_above);
1934
1935 self.active_window_mut().scroll_split_viewport_to(
1936 actual_buffer_id,
1937 actual_split_id,
1938 target_line,
1939 true,
1940 );
1941 }
1942
1943 fn handle_scroll_buffer_to_line(&mut self, buffer_id: BufferId, line: usize) {
1953 if !self
1954 .windows
1955 .get(&self.active_window)
1956 .map(|w| &w.buffers)
1957 .expect("active window present")
1958 .contains_key(&buffer_id)
1959 {
1960 return;
1961 }
1962
1963 let mut target_leaves: Vec<LeafId> = Vec::new();
1965
1966 for leaf_id in self
1968 .windows
1969 .get(&self.active_window)
1970 .and_then(|w| w.buffers.splits())
1971 .map(|(mgr, _)| mgr)
1972 .expect("active window must have a populated split layout")
1973 .root()
1974 .leaf_split_ids()
1975 {
1976 if let Some(vs) = self
1977 .windows
1978 .get(&self.active_window)
1979 .and_then(|w| w.buffers.splits())
1980 .map(|(_, vs)| vs)
1981 .expect("active window must have a populated split layout")
1982 .get(&leaf_id)
1983 {
1984 if vs.active_buffer == buffer_id {
1985 target_leaves.push(leaf_id);
1986 }
1987 }
1988 }
1989
1990 for (_group_leaf_id, node) in self.active_window().grouped_subtrees.iter() {
1992 if let crate::view::split::SplitNode::Grouped { layout, .. } = node {
1993 for inner_leaf in layout.leaf_split_ids() {
1994 if let Some(vs) = self
1995 .windows
1996 .get(&self.active_window)
1997 .and_then(|w| w.buffers.splits())
1998 .map(|(_, vs)| vs)
1999 .expect("active window must have a populated split layout")
2000 .get(&inner_leaf)
2001 {
2002 if vs.active_buffer == buffer_id && !target_leaves.contains(&inner_leaf) {
2003 target_leaves.push(inner_leaf);
2004 }
2005 }
2006 }
2007 }
2008 }
2009
2010 if target_leaves.is_empty() {
2011 return;
2012 }
2013
2014 self.active_window_mut()
2015 .scroll_buffer_to_line_in_splits(buffer_id, &target_leaves, line);
2016 }
2017
2018 fn handle_spawn_host_process(
2019 &mut self,
2020 command: String,
2021 args: Vec<String>,
2022 cwd: Option<String>,
2023 callback_id: JsCallbackId,
2024 ) {
2025 if let (Some(runtime), Some(bridge)) = (&self.tokio_runtime, &self.async_bridge) {
2040 use tokio::io::{AsyncReadExt, BufReader};
2041 use tokio::process::Command as TokioCommand;
2042
2043 let effective_cwd = cwd.or_else(|| {
2044 std::env::current_dir()
2045 .map(|p| p.to_string_lossy().to_string())
2046 .ok()
2047 });
2048 let sender = bridge.sender();
2049 let process_id = callback_id.as_u64();
2050
2051 let (kill_tx, mut kill_rx) = tokio::sync::oneshot::channel::<()>();
2052 self.host_process_handles.insert(process_id, kill_tx);
2053
2054 runtime.spawn(async move {
2055 use crate::services::process_hidden::HideWindow;
2056 let mut cmd = TokioCommand::new(&command);
2057 cmd.args(&args);
2058 cmd.stdout(std::process::Stdio::piped());
2059 cmd.stderr(std::process::Stdio::piped());
2060 cmd.hide_window();
2061 if let Some(ref dir) = effective_cwd {
2062 cmd.current_dir(dir);
2063 }
2064 let mut child = match cmd.spawn() {
2065 Ok(c) => c,
2066 Err(e) => {
2067 #[allow(clippy::let_underscore_must_use)]
2068 let _ = sender.send(AsyncMessage::PluginProcessOutput {
2069 process_id,
2070 stdout: String::new(),
2071 stderr: e.to_string(),
2072 exit_code: -1,
2073 });
2074 return;
2075 }
2076 };
2077
2078 let stdout_pipe = child.stdout.take();
2084 let stderr_pipe = child.stderr.take();
2085
2086 let stdout_fut = async {
2087 let mut buf = String::new();
2088 if let Some(s) = stdout_pipe {
2089 #[allow(clippy::let_underscore_must_use)]
2090 let _ = BufReader::new(s).read_to_string(&mut buf).await;
2091 }
2092 buf
2093 };
2094 let stderr_fut = async {
2095 let mut buf = String::new();
2096 if let Some(s) = stderr_pipe {
2097 #[allow(clippy::let_underscore_must_use)]
2098 let _ = BufReader::new(s).read_to_string(&mut buf).await;
2099 }
2100 buf
2101 };
2102 let wait_fut = async {
2103 tokio::select! {
2104 status = child.wait() => {
2105 status.map(|s| s.code().unwrap_or(-1)).unwrap_or(-1)
2106 }
2107 _ = &mut kill_rx => {
2108 #[allow(clippy::let_underscore_must_use)]
2112 let _ = child.start_kill();
2113 child
2114 .wait()
2115 .await
2116 .map(|s| s.code().unwrap_or(-1))
2117 .unwrap_or(-1)
2118 }
2119 }
2120 };
2121 let (stdout, stderr, exit_code) = tokio::join!(stdout_fut, stderr_fut, wait_fut);
2122
2123 #[allow(clippy::let_underscore_must_use)]
2124 let _ = sender.send(AsyncMessage::PluginProcessOutput {
2125 process_id,
2126 stdout,
2127 stderr,
2128 exit_code,
2129 });
2130 });
2131 } else {
2132 self.plugin_manager
2133 .read()
2134 .unwrap()
2135 .reject_callback(callback_id, "Async runtime not available".to_string());
2136 }
2137 }
2138
2139 fn handle_spawn_background_process(
2140 &mut self,
2141 process_id: u64,
2142 command: String,
2143 args: Vec<String>,
2144 cwd: Option<String>,
2145 callback_id: JsCallbackId,
2146 ) {
2147 if let (Some(runtime), Some(bridge)) = (&self.tokio_runtime, &self.async_bridge) {
2149 use tokio::io::{AsyncBufReadExt, BufReader};
2150 use tokio::process::Command as TokioCommand;
2151
2152 let effective_cwd = cwd.unwrap_or_else(|| {
2153 std::env::current_dir()
2154 .map(|p| p.to_string_lossy().to_string())
2155 .unwrap_or_else(|_| ".".to_string())
2156 });
2157
2158 let sender = bridge.sender();
2159 let sender_stdout = sender.clone();
2160 let sender_stderr = sender.clone();
2161 let callback_id_u64 = callback_id.as_u64();
2162
2163 #[allow(clippy::let_underscore_must_use)]
2165 let handle = runtime.spawn(async move {
2166 use crate::services::process_hidden::HideWindow;
2167 let mut child = match TokioCommand::new(&command)
2168 .args(&args)
2169 .current_dir(&effective_cwd)
2170 .stdout(std::process::Stdio::piped())
2171 .stderr(std::process::Stdio::piped())
2172 .hide_window()
2173 .spawn()
2174 {
2175 Ok(child) => child,
2176 Err(e) => {
2177 let _ = sender.send(crate::services::async_bridge::AsyncMessage::Plugin(
2178 fresh_core::api::PluginAsyncMessage::ProcessExit {
2179 process_id,
2180 callback_id: callback_id_u64,
2181 exit_code: -1,
2182 },
2183 ));
2184 tracing::error!("Failed to spawn background process: {}", e);
2185 return;
2186 }
2187 };
2188
2189 let stdout = child.stdout.take();
2191 let stderr = child.stderr.take();
2192 let pid = process_id;
2193
2194 if let Some(stdout) = stdout {
2196 let sender = sender_stdout;
2197 tokio::spawn(async move {
2198 let reader = BufReader::new(stdout);
2199 let mut lines = reader.lines();
2200 while let Ok(Some(line)) = lines.next_line().await {
2201 let _ =
2202 sender.send(crate::services::async_bridge::AsyncMessage::Plugin(
2203 fresh_core::api::PluginAsyncMessage::ProcessStdout {
2204 process_id: pid,
2205 data: line + "\n",
2206 },
2207 ));
2208 }
2209 });
2210 }
2211
2212 if let Some(stderr) = stderr {
2214 let sender = sender_stderr;
2215 tokio::spawn(async move {
2216 let reader = BufReader::new(stderr);
2217 let mut lines = reader.lines();
2218 while let Ok(Some(line)) = lines.next_line().await {
2219 let _ =
2220 sender.send(crate::services::async_bridge::AsyncMessage::Plugin(
2221 fresh_core::api::PluginAsyncMessage::ProcessStderr {
2222 process_id: pid,
2223 data: line + "\n",
2224 },
2225 ));
2226 }
2227 });
2228 }
2229
2230 let exit_code = match child.wait().await {
2232 Ok(status) => status.code().unwrap_or(-1),
2233 Err(_) => -1,
2234 };
2235
2236 let _ = sender.send(crate::services::async_bridge::AsyncMessage::Plugin(
2237 fresh_core::api::PluginAsyncMessage::ProcessExit {
2238 process_id,
2239 callback_id: callback_id_u64,
2240 exit_code,
2241 },
2242 ));
2243 });
2244
2245 self.background_process_handles
2247 .insert(process_id, handle.abort_handle());
2248 } else {
2249 self.plugin_manager
2251 .read()
2252 .unwrap()
2253 .reject_callback(callback_id, "Async runtime not available".to_string());
2254 }
2255 }
2256
2257 fn handle_create_virtual_buffer_with_content(
2258 &mut self,
2259 name: String,
2260 mode: String,
2261 read_only: bool,
2262 entries: Vec<fresh_core::text_property::TextPropertyEntry>,
2263 show_line_numbers: bool,
2264 show_cursors: bool,
2265 editing_disabled: bool,
2266 hidden_from_tabs: bool,
2267 request_id: Option<u64>,
2268 ) {
2269 let buffer_id =
2270 self.active_window_mut()
2271 .create_virtual_buffer(name.clone(), mode.clone(), read_only);
2272 tracing::info!(
2273 "Created virtual buffer '{}' with mode '{}' (id={:?})",
2274 name,
2275 mode,
2276 buffer_id
2277 );
2278
2279 if let Some(state) = self
2286 .windows
2287 .get_mut(&self.active_window)
2288 .map(|w| &mut w.buffers)
2289 .expect("active window present")
2290 .get_mut(&buffer_id)
2291 {
2292 state.margins.configure_for_line_numbers(show_line_numbers);
2293 state.show_cursors = show_cursors;
2294 state.editing_disabled = editing_disabled;
2295 tracing::debug!(
2296 "Set buffer {:?} view options: show_line_numbers={}, show_cursors={}, editing_disabled={}",
2297 buffer_id,
2298 show_line_numbers,
2299 show_cursors,
2300 editing_disabled
2301 );
2302 }
2303 let active_split = self
2304 .windows
2305 .get(&self.active_window)
2306 .and_then(|w| w.buffers.splits())
2307 .map(|(mgr, _)| mgr)
2308 .expect("active window must have a populated split layout")
2309 .active_split();
2310 if let Some(view_state) = self
2311 .windows
2312 .get_mut(&self.active_window)
2313 .and_then(|w| w.split_view_states_mut())
2314 .expect("active window must have a populated split layout")
2315 .get_mut(&active_split)
2316 {
2317 view_state.ensure_buffer_state(buffer_id).show_line_numbers = show_line_numbers;
2318 }
2319
2320 if hidden_from_tabs {
2322 if let Some(meta) = self.active_window_mut().buffer_metadata.get_mut(&buffer_id) {
2323 meta.hidden_from_tabs = true;
2324 }
2325 }
2326
2327 match self.set_virtual_buffer_content(buffer_id, entries) {
2329 Ok(()) => {
2330 tracing::debug!("Set virtual buffer content for {:?}", buffer_id);
2331 self.set_active_buffer(buffer_id);
2333 tracing::debug!("Switched to virtual buffer {:?}", buffer_id);
2334
2335 if let Some(req_id) = request_id {
2337 tracing::info!(
2338 "CreateVirtualBufferWithContent: resolving callback for request_id={}, buffer_id={:?}",
2339 req_id,
2340 buffer_id
2341 );
2342 let result = fresh_core::api::VirtualBufferResult {
2344 buffer_id: buffer_id.0 as u64,
2345 split_id: None,
2346 };
2347 self.plugin_manager.read().unwrap().resolve_callback(
2348 fresh_core::api::JsCallbackId::from(req_id),
2349 serde_json::to_string(&result).unwrap_or_default(),
2350 );
2351 tracing::info!(
2352 "CreateVirtualBufferWithContent: resolve_callback sent for request_id={}",
2353 req_id
2354 );
2355 }
2356 }
2357 Err(e) => {
2358 tracing::error!("Failed to set virtual buffer content: {}", e);
2359 }
2360 }
2361 }
2362
2363 fn handle_create_virtual_buffer_in_split(
2364 &mut self,
2365 name: String,
2366 mode: String,
2367 read_only: bool,
2368 entries: Vec<fresh_core::text_property::TextPropertyEntry>,
2369 ratio: f32,
2370 direction: Option<String>,
2371 panel_id: Option<String>,
2372 show_line_numbers: bool,
2373 show_cursors: bool,
2374 editing_disabled: bool,
2375 line_wrap: Option<bool>,
2376 before: bool,
2377 role: Option<String>,
2378 request_id: Option<u64>,
2379 ) {
2380 let split_role: Option<crate::view::split::SplitRole> = match role.as_deref() {
2383 Some("utility_dock") => Some(crate::view::split::SplitRole::UtilityDock),
2384 _ => None,
2385 };
2386
2387 if let Some(target_role) = split_role {
2393 if let Some(dock_leaf) = self
2394 .windows
2395 .get(&self.active_window)
2396 .and_then(|w| w.buffers.splits())
2397 .map(|(mgr, _)| mgr)
2398 .expect("active window must have a populated split layout")
2399 .find_leaf_by_role(target_role)
2400 {
2401 let source_split_before_create = self
2406 .windows
2407 .get(&self.active_window)
2408 .and_then(|w| w.buffers.splits())
2409 .map(|(mgr, _)| mgr)
2410 .expect("active window must have a populated split layout")
2411 .active_split();
2412 let buffer_id = self.active_window_mut().create_virtual_buffer(
2413 name.clone(),
2414 mode.clone(),
2415 read_only,
2416 );
2417 if let Some(state) = self
2418 .windows
2419 .get_mut(&self.active_window)
2420 .map(|w| &mut w.buffers)
2421 .expect("active window present")
2422 .get_mut(&buffer_id)
2423 {
2424 state.margins.configure_for_line_numbers(show_line_numbers);
2425 state.show_cursors = show_cursors;
2426 state.editing_disabled = editing_disabled;
2427 }
2428 if let Some(pid) = &panel_id {
2429 self.panel_ids_mut().insert(pid.clone(), buffer_id);
2430 }
2431 if let Err(e) = self.set_virtual_buffer_content(buffer_id, entries) {
2432 tracing::error!("Failed to set virtual buffer content (dock route): {}", e);
2433 return;
2434 }
2435
2436 self.windows
2440 .get_mut(&self.active_window)
2441 .and_then(|w| w.split_manager_mut())
2442 .expect("active window must have a populated split layout")
2443 .set_active_split(dock_leaf);
2444 self.active_window_mut()
2445 .set_pane_buffer(dock_leaf, buffer_id);
2446
2447 if dock_leaf != source_split_before_create {
2449 if let Some(source_view_state) = self
2450 .windows
2451 .get_mut(&self.active_window)
2452 .and_then(|w| w.split_view_states_mut())
2453 .expect("active window must have a populated split layout")
2454 .get_mut(&source_split_before_create)
2455 {
2456 source_view_state.remove_buffer(buffer_id);
2457 }
2458 }
2459
2460 if let Some(req_id) = request_id {
2461 let result = fresh_core::api::VirtualBufferResult {
2462 buffer_id: buffer_id.0 as u64,
2463 split_id: Some(dock_leaf.0 .0 as u64),
2464 };
2465 self.plugin_manager.read().unwrap().resolve_callback(
2466 fresh_core::api::JsCallbackId::from(req_id),
2467 serde_json::to_string(&result).unwrap_or_default(),
2468 );
2469 }
2470 tracing::info!(
2471 "Routed virtual buffer '{}' into existing utility dock {:?}",
2472 name,
2473 dock_leaf
2474 );
2475 return;
2476 }
2477 }
2480
2481 if let Some(pid) = &panel_id {
2483 if let Some(&existing_buffer_id) = self.panel_ids().get(pid) {
2484 if self
2486 .windows
2487 .get(&self.active_window)
2488 .map(|w| &w.buffers)
2489 .expect("active window present")
2490 .contains_key(&existing_buffer_id)
2491 {
2492 if let Err(e) = self.set_virtual_buffer_content(existing_buffer_id, entries) {
2494 tracing::error!("Failed to update panel content: {}", e);
2495 } else {
2496 tracing::info!("Updated existing panel '{}' content", pid);
2497 }
2498
2499 let splits = self
2501 .windows
2502 .get(&self.active_window)
2503 .and_then(|w| w.buffers.splits())
2504 .map(|(mgr, _)| mgr)
2505 .expect("active window must have a populated split layout")
2506 .splits_for_buffer(existing_buffer_id);
2507 if let Some(&split_id) = splits.first() {
2508 self.windows
2509 .get_mut(&self.active_window)
2510 .and_then(|w| w.split_manager_mut())
2511 .expect("active window must have a populated split layout")
2512 .set_active_split(split_id);
2513 self.active_window_mut()
2516 .set_pane_buffer(split_id, existing_buffer_id);
2517 tracing::debug!("Focused split {:?} containing panel buffer", split_id);
2518 }
2519
2520 if let Some(req_id) = request_id {
2522 let result = fresh_core::api::VirtualBufferResult {
2523 buffer_id: existing_buffer_id.0 as u64,
2524 split_id: splits.first().map(|s| s.0 .0 as u64),
2525 };
2526 self.plugin_manager.read().unwrap().resolve_callback(
2527 fresh_core::api::JsCallbackId::from(req_id),
2528 serde_json::to_string(&result).unwrap_or_default(),
2529 );
2530 }
2531 return;
2532 } else {
2533 tracing::warn!(
2535 "Removing stale panel_id '{}' pointing to non-existent buffer {:?}",
2536 pid,
2537 existing_buffer_id
2538 );
2539 self.panel_ids_mut().remove(pid);
2540 }
2542 }
2543 }
2544
2545 let source_split_before_create = self
2551 .windows
2552 .get(&self.active_window)
2553 .and_then(|w| w.buffers.splits())
2554 .map(|(mgr, _)| mgr)
2555 .expect("active window must have a populated split layout")
2556 .active_split();
2557
2558 let buffer_id =
2560 self.active_window_mut()
2561 .create_virtual_buffer(name.clone(), mode.clone(), read_only);
2562 tracing::info!(
2563 "Created virtual buffer '{}' with mode '{}' in split (id={:?})",
2564 name,
2565 mode,
2566 buffer_id
2567 );
2568
2569 if let Some(state) = self
2571 .windows
2572 .get_mut(&self.active_window)
2573 .map(|w| &mut w.buffers)
2574 .expect("active window present")
2575 .get_mut(&buffer_id)
2576 {
2577 state.margins.configure_for_line_numbers(show_line_numbers);
2578 state.show_cursors = show_cursors;
2579 state.editing_disabled = editing_disabled;
2580 tracing::debug!(
2581 "Set buffer {:?} view options: show_line_numbers={}, show_cursors={}, editing_disabled={}",
2582 buffer_id,
2583 show_line_numbers,
2584 show_cursors,
2585 editing_disabled
2586 );
2587 }
2588
2589 if let Some(pid) = panel_id {
2591 self.panel_ids_mut().insert(pid, buffer_id);
2592 }
2593
2594 if let Err(e) = self.set_virtual_buffer_content(buffer_id, entries) {
2596 tracing::error!("Failed to set virtual buffer content: {}", e);
2597 return;
2598 }
2599
2600 let split_dir = match direction.as_deref() {
2602 Some("vertical") => crate::model::event::SplitDirection::Vertical,
2603 _ => crate::model::event::SplitDirection::Horizontal,
2604 };
2605
2606 let created_split_id =
2612 match if split_role == Some(crate::view::split::SplitRole::UtilityDock) {
2613 self.windows
2614 .get_mut(&self.active_window)
2615 .and_then(|w| w.split_manager_mut())
2616 .expect("active window must have a populated split layout")
2617 .split_root_positioned(split_dir, buffer_id, ratio, before)
2618 } else {
2619 self.windows
2620 .get_mut(&self.active_window)
2621 .and_then(|w| w.split_manager_mut())
2622 .expect("active window must have a populated split layout")
2623 .split_active_positioned(split_dir, buffer_id, ratio, before)
2624 } {
2625 Ok(new_split_id) => {
2626 if new_split_id != source_split_before_create {
2632 if let Some(source_view_state) = self
2633 .windows
2634 .get_mut(&self.active_window)
2635 .and_then(|w| w.split_view_states_mut())
2636 .expect("active window must have a populated split layout")
2637 .get_mut(&source_split_before_create)
2638 {
2639 source_view_state.remove_buffer(buffer_id);
2640 }
2641 }
2642 let mut view_state = SplitViewState::with_buffer(
2644 self.terminal_width,
2645 self.terminal_height,
2646 buffer_id,
2647 );
2648 view_state.apply_config_defaults(
2649 self.config.editor.line_numbers,
2650 self.config.editor.highlight_current_line,
2651 line_wrap.unwrap_or_else(|| {
2652 self.active_window().resolve_line_wrap_for_buffer(buffer_id)
2653 }),
2654 self.config.editor.wrap_indent,
2655 self.active_window()
2656 .resolve_wrap_column_for_buffer(buffer_id),
2657 self.config.editor.rulers.clone(),
2658 );
2659 view_state.ensure_buffer_state(buffer_id).show_line_numbers = show_line_numbers;
2661 self.windows
2662 .get_mut(&self.active_window)
2663 .and_then(|w| w.split_view_states_mut())
2664 .expect("active window must have a populated split layout")
2665 .insert(new_split_id, view_state);
2666
2667 self.windows
2669 .get_mut(&self.active_window)
2670 .and_then(|w| w.split_manager_mut())
2671 .expect("active window must have a populated split layout")
2672 .set_active_split(new_split_id);
2673 if let Some(target_role) = split_role {
2681 self.windows
2682 .get_mut(&self.active_window)
2683 .and_then(|w| w.split_manager_mut())
2684 .expect("active window must have a populated split layout")
2685 .clear_role(target_role);
2686 self.windows
2687 .get_mut(&self.active_window)
2688 .and_then(|w| w.split_manager_mut())
2689 .expect("active window must have a populated split layout")
2690 .set_leaf_role(new_split_id, Some(target_role));
2691 tracing::info!(
2692 "Tagged new dock leaf {:?} with role {:?}",
2693 new_split_id,
2694 target_role
2695 );
2696 }
2697
2698 tracing::info!(
2699 "Created {:?} split with virtual buffer {:?}",
2700 split_dir,
2701 buffer_id
2702 );
2703 Some(new_split_id)
2704 }
2705 Err(e) => {
2706 tracing::error!("Failed to create split: {}", e);
2707 self.set_active_buffer(buffer_id);
2709 None
2710 }
2711 };
2712
2713 if let Some(req_id) = request_id {
2716 tracing::trace!("CreateVirtualBufferInSplit: resolving callback for request_id={}, buffer_id={:?}, split_id={:?}", req_id, buffer_id, created_split_id);
2717 let result = fresh_core::api::VirtualBufferResult {
2718 buffer_id: buffer_id.0 as u64,
2719 split_id: created_split_id.map(|s| s.0 .0 as u64),
2720 };
2721 self.plugin_manager.read().unwrap().resolve_callback(
2722 fresh_core::api::JsCallbackId::from(req_id),
2723 serde_json::to_string(&result).unwrap_or_default(),
2724 );
2725 }
2726 }
2727
2728 fn handle_create_virtual_buffer_in_existing_split(
2729 &mut self,
2730 name: String,
2731 mode: String,
2732 read_only: bool,
2733 entries: Vec<fresh_core::text_property::TextPropertyEntry>,
2734 split_id: SplitId,
2735 show_line_numbers: bool,
2736 show_cursors: bool,
2737 editing_disabled: bool,
2738 line_wrap: Option<bool>,
2739 request_id: Option<u64>,
2740 ) {
2741 let buffer_id =
2743 self.active_window_mut()
2744 .create_virtual_buffer(name.clone(), mode.clone(), read_only);
2745 tracing::info!(
2746 "Created virtual buffer '{}' with mode '{}' for existing split {:?} (id={:?})",
2747 name,
2748 mode,
2749 split_id,
2750 buffer_id
2751 );
2752
2753 if let Some(state) = self
2755 .windows
2756 .get_mut(&self.active_window)
2757 .map(|w| &mut w.buffers)
2758 .expect("active window present")
2759 .get_mut(&buffer_id)
2760 {
2761 state.margins.configure_for_line_numbers(show_line_numbers);
2762 state.show_cursors = show_cursors;
2763 state.editing_disabled = editing_disabled;
2764 }
2765
2766 if let Err(e) = self.set_virtual_buffer_content(buffer_id, entries) {
2768 tracing::error!("Failed to set virtual buffer content: {}", e);
2769 return;
2770 }
2771
2772 let leaf_id = LeafId(split_id);
2775 self.windows
2776 .get_mut(&self.active_window)
2777 .and_then(|w| w.split_manager_mut())
2778 .expect("active window must have a populated split layout")
2779 .set_active_split(leaf_id);
2780 self.active_window_mut().set_pane_buffer(leaf_id, buffer_id);
2781
2782 if let Some(view_state) = self
2788 .windows
2789 .get_mut(&self.active_window)
2790 .and_then(|w| w.split_view_states_mut())
2791 .expect("active window must have a populated split layout")
2792 .get_mut(&leaf_id)
2793 {
2794 view_state.switch_buffer(buffer_id);
2795 view_state.add_buffer(buffer_id);
2796 view_state.ensure_buffer_state(buffer_id).show_line_numbers = show_line_numbers;
2797
2798 if let Some(wrap) = line_wrap {
2800 view_state.active_state_mut().viewport.line_wrap_enabled = wrap;
2801 }
2802 }
2803
2804 tracing::info!(
2805 "Displayed virtual buffer {:?} in split {:?}",
2806 buffer_id,
2807 split_id
2808 );
2809
2810 if let Some(req_id) = request_id {
2812 let result = fresh_core::api::VirtualBufferResult {
2813 buffer_id: buffer_id.0 as u64,
2814 split_id: Some(split_id.0 as u64),
2815 };
2816 self.plugin_manager.read().unwrap().resolve_callback(
2817 fresh_core::api::JsCallbackId::from(req_id),
2818 serde_json::to_string(&result).unwrap_or_default(),
2819 );
2820 }
2821 }
2822
2823 fn handle_show_action_popup(
2824 &mut self,
2825 popup_id: String,
2826 title: String,
2827 message: String,
2828 actions: Vec<fresh_core::api::ActionPopupAction>,
2829 ) {
2830 tracing::info!(
2831 "Action popup requested: id={}, title={}, actions={}",
2832 popup_id,
2833 title,
2834 actions.len()
2835 );
2836
2837 let items: Vec<crate::model::event::PopupListItemData> = actions
2839 .iter()
2840 .map(|action| crate::model::event::PopupListItemData {
2841 text: action.label.clone(),
2842 detail: None,
2843 icon: None,
2844 data: Some(action.id.clone()),
2845 })
2846 .collect();
2847
2848 drop(actions);
2853
2854 let popup_data = crate::model::event::PopupData {
2856 kind: crate::model::event::PopupKindHint::List,
2857 title: Some(title),
2858 description: Some(message),
2859 transient: false,
2860 content: crate::model::event::PopupContentData::List { items, selected: 0 },
2861 position: crate::model::event::PopupPositionData::BottomRight,
2862 width: 60,
2863 max_height: 15,
2864 bordered: true,
2865 };
2866
2867 let mut popup_obj = crate::state::convert_popup_data_to_popup(&popup_data);
2877 popup_obj.resolver = crate::view::popup::PopupResolver::PluginAction {
2878 popup_id: popup_id.clone(),
2879 };
2880
2881 {
2888 let theme = self.theme();
2889 popup_obj.background_style = ratatui::style::Style::default().bg(theme.popup_bg);
2890 popup_obj.border_style = ratatui::style::Style::default().fg(theme.popup_border_fg);
2891 }
2892
2893 while self
2905 .active_state()
2906 .popups
2907 .top()
2908 .is_some_and(|p| matches!(p.resolver, crate::view::popup::PopupResolver::LspStatus))
2909 {
2910 self.active_state_mut().popups.hide();
2911 }
2912
2913 let existing_idx = self.global_popups.all().iter().position(|p| {
2920 matches!(
2921 &p.resolver,
2922 crate::view::popup::PopupResolver::PluginAction { popup_id: id } if id == &popup_id,
2923 )
2924 });
2925 if let Some(idx) = existing_idx {
2926 if let Some(slot) = self.global_popups.get_mut(idx) {
2927 *slot = popup_obj;
2928 }
2929 } else {
2930 self.global_popups.show(popup_obj);
2931 }
2932 tracing::info!(
2933 "Action popup shown: id={}, stack_depth={}",
2934 popup_id,
2935 self.global_popups.all().len()
2936 );
2937 }
2938
2939 fn handle_set_lsp_menu_contributions(
2949 &mut self,
2950 plugin_id: String,
2951 language: String,
2952 items: Vec<fresh_core::api::LspMenuItem>,
2953 ) {
2954 let key = (language.clone(), plugin_id.clone());
2955 if items.is_empty() {
2956 self.active_window_mut().lsp_menu_contributions.remove(&key);
2957 } else {
2958 self.active_window_mut()
2959 .lsp_menu_contributions
2960 .insert(key, items);
2961 }
2962 self.refresh_lsp_status_popup_if_open();
2967 }
2968
2969 fn handle_create_terminal(
2970 &mut self,
2971 cwd: Option<String>,
2972 direction: Option<String>,
2973 ratio: Option<f32>,
2974 focus: Option<bool>,
2975 persistent: bool,
2976 target_session_id: Option<fresh_core::WindowId>,
2977 command: Option<Vec<String>>,
2978 title: Option<String>,
2979 request_id: u64,
2980 ) {
2981 let target_id = target_session_id
2988 .filter(|id| self.windows.contains_key(id))
2989 .unwrap_or(self.active_window);
2990 let is_active_target = target_id == self.active_window;
2991
2992 let cwd_buf = cwd.map(std::path::PathBuf::from);
2993 let split_direction = direction.as_deref().map(|d| match d {
2994 "horizontal" => crate::model::event::SplitDirection::Horizontal,
2995 _ => crate::model::event::SplitDirection::Vertical,
2996 });
2997
2998 let prev_active = if is_active_target {
3006 Some(self.active_window().active_buffer())
3007 } else {
3008 None
3009 };
3010
3011 let result = {
3012 let target = self
3013 .windows
3014 .get_mut(&target_id)
3015 .expect("target window present (existence checked above)");
3016 target.create_plugin_terminal(
3017 cwd_buf,
3018 split_direction,
3019 ratio,
3020 focus.unwrap_or(true),
3021 persistent,
3022 command,
3023 title.filter(|t| !t.is_empty()),
3024 )
3025 };
3026 match result {
3027 Ok((terminal_id, buffer_id, created_split_id)) => {
3028 if is_active_target {
3029 let new_active = self.active_window().active_buffer();
3030 if prev_active != Some(new_active) {
3031 #[cfg(feature = "plugins")]
3032 self.update_plugin_state_snapshot();
3033 #[cfg(feature = "plugins")]
3034 self.plugin_manager.read().unwrap().run_hook(
3035 "buffer_activated",
3036 crate::services::plugins::hooks::HookArgs::BufferActivated {
3037 buffer_id: new_active,
3038 },
3039 );
3040 }
3041 }
3042 let api_result = fresh_core::api::TerminalResult {
3043 buffer_id: buffer_id.0 as u64,
3044 terminal_id: terminal_id.0 as u64,
3045 split_id: created_split_id.map(|s| s.0 .0 as u64),
3046 };
3047 self.plugin_manager.read().unwrap().resolve_callback(
3048 fresh_core::api::JsCallbackId::from(request_id),
3049 serde_json::to_string(&api_result).unwrap_or_default(),
3050 );
3051 tracing::info!(
3052 "Plugin created terminal {:?} with buffer {:?} in window {:?}",
3053 terminal_id,
3054 buffer_id,
3055 target_id
3056 );
3057 }
3058 Err(e) => {
3059 tracing::error!("Failed to create terminal for plugin: {e}");
3060 self.plugin_manager.read().unwrap().reject_callback(
3061 fresh_core::api::JsCallbackId::from(request_id),
3062 format!("Failed to create terminal: {e}"),
3063 );
3064 }
3065 }
3066 }
3067
3068 fn handle_get_split_by_label(&mut self, label: String, request_id: u64) {
3071 let split_id = self
3072 .windows
3073 .get(&self.active_window)
3074 .and_then(|w| w.buffers.splits())
3075 .map(|(mgr, _)| mgr)
3076 .expect("active window must have a populated split layout")
3077 .find_split_by_label(&label);
3078 let callback_id = fresh_core::api::JsCallbackId::from(request_id);
3079 let json =
3080 serde_json::to_string(&split_id.map(|s| s.0 .0)).unwrap_or_else(|_| "null".to_string());
3081 self.plugin_manager
3082 .read()
3083 .unwrap()
3084 .resolve_callback(callback_id, json);
3085 }
3086
3087 fn handle_set_buffer_show_cursors(&mut self, buffer_id: BufferId, show: bool) {
3088 if let Some(state) = self
3089 .windows
3090 .get_mut(&self.active_window)
3091 .map(|w| &mut w.buffers)
3092 .expect("active window present")
3093 .get_mut(&buffer_id)
3094 {
3095 state.show_cursors = show;
3096 } else {
3097 tracing::warn!("SetBufferShowCursors: buffer {:?} not found", buffer_id);
3098 }
3099 }
3100
3101 fn handle_override_theme_colors(
3102 &mut self,
3103 overrides: std::collections::HashMap<String, [u8; 3]>,
3104 ) {
3105 let pairs = overrides
3106 .into_iter()
3107 .map(|(k, [r, g, b])| (k, ratatui::style::Color::Rgb(r, g, b)));
3108 let applied = self.theme.write().unwrap().override_colors(pairs);
3109 if applied > 0 {
3110 self.reapply_all_overlays();
3113 }
3114 }
3115
3116 fn handle_await_next_key(&mut self, callback_id: fresh_core::api::JsCallbackId) {
3117 if let Some(payload) = self
3121 .active_window_mut()
3122 .pending_key_capture_buffer
3123 .pop_front()
3124 {
3125 let json = serde_json::to_string(&payload).unwrap_or_else(|_| "null".to_string());
3126 self.plugin_manager
3127 .read()
3128 .unwrap()
3129 .resolve_callback(callback_id, json);
3130 } else {
3131 self.active_window_mut()
3132 .pending_next_key_callbacks
3133 .push_back(callback_id);
3134 }
3135 }
3136
3137 fn handle_spawn_process(
3138 &mut self,
3139 command: String,
3140 args: Vec<String>,
3141 cwd: Option<String>,
3142 stdout_to: Option<std::path::PathBuf>,
3143 callback_id: fresh_core::api::JsCallbackId,
3144 ) {
3145 if let (Some(runtime), Some(bridge)) = (&self.tokio_runtime, &self.async_bridge) {
3146 let effective_cwd = cwd.or_else(|| {
3147 std::env::current_dir()
3148 .map(|p| p.to_string_lossy().to_string())
3149 .ok()
3150 });
3151 let sender = bridge.sender();
3152 let spawner = self.authority.process_spawner.clone();
3153
3154 let process_id = callback_id.as_u64();
3159 let (kill_tx, kill_rx) = tokio::sync::oneshot::channel::<()>();
3160 self.host_process_handles.insert(process_id, kill_tx);
3161
3162 runtime.spawn(async move {
3163 #[allow(clippy::let_underscore_must_use)]
3164 let outcome = spawner
3165 .spawn_cancellable(command, args, effective_cwd, stdout_to, kill_rx)
3166 .await;
3167 match outcome {
3168 Ok(result) => {
3169 #[allow(clippy::let_underscore_must_use)]
3170 let _ = sender.send(AsyncMessage::PluginProcessOutput {
3171 process_id,
3172 stdout: result.stdout,
3173 stderr: result.stderr,
3174 exit_code: result.exit_code,
3175 });
3176 }
3177 Err(e) => {
3178 #[allow(clippy::let_underscore_must_use)]
3179 let _ = sender.send(AsyncMessage::PluginProcessOutput {
3180 process_id,
3181 stdout: String::new(),
3182 stderr: e.to_string(),
3183 exit_code: -1,
3184 });
3185 }
3186 }
3187 });
3188 } else {
3189 self.plugin_manager
3190 .read()
3191 .unwrap()
3192 .reject_callback(callback_id, "Async runtime not available".to_string());
3193 }
3194 }
3195
3196 fn handle_kill_host_process(&mut self, process_id: u64) {
3197 if let Some(tx) = self.host_process_handles.remove(&process_id) {
3201 #[allow(clippy::let_underscore_must_use)]
3202 let _ = tx.send(());
3203 tracing::debug!("KillHostProcess: sent kill for process_id={}", process_id);
3204 } else {
3205 tracing::debug!(
3206 "KillHostProcess: unknown process_id={} (already exited?)",
3207 process_id
3208 );
3209 }
3210 }
3211
3212 fn handle_set_authority(&mut self, payload: serde_json::Value) {
3213 match serde_json::from_value::<crate::services::authority::AuthorityPayload>(payload) {
3216 Ok(parsed) => {
3217 match crate::services::authority::Authority::from_plugin_payload(parsed) {
3218 Ok(auth) => {
3219 tracing::info!("Plugin installed new authority");
3220 self.install_authority(auth);
3221 }
3222 Err(e) => {
3223 tracing::warn!("setAuthority: invalid payload: {}", e);
3224 self.set_status_message(format!("setAuthority rejected: {}", e));
3225 }
3226 }
3227 }
3228 Err(e) => {
3229 tracing::warn!("setAuthority: failed to parse payload: {}", e);
3230 self.set_status_message(format!("setAuthority rejected: {}", e));
3231 }
3232 }
3233 }
3234
3235 fn handle_set_remote_indicator_state(&mut self, state: serde_json::Value) {
3236 match serde_json::from_value::<crate::view::ui::status_bar::RemoteIndicatorOverride>(state)
3239 {
3240 Ok(over) => {
3241 self.remote_indicator_override = Some(over);
3242 }
3243 Err(e) => {
3244 tracing::warn!("setRemoteIndicatorState: invalid payload: {}", e);
3245 self.set_status_message(format!("setRemoteIndicatorState rejected: {}", e));
3246 }
3247 }
3248 }
3249
3250 fn handle_spawn_process_wait(
3251 &mut self,
3252 process_id: u64,
3253 callback_id: fresh_core::api::JsCallbackId,
3254 ) {
3255 tracing::warn!(
3256 "SpawnProcessWait not fully implemented - process_id={}",
3257 process_id
3258 );
3259 self.plugin_manager.read().unwrap().reject_callback(
3260 callback_id,
3261 format!(
3262 "SpawnProcessWait not yet fully implemented for process_id={}",
3263 process_id
3264 ),
3265 );
3266 }
3267
3268 fn handle_delay(&mut self, callback_id: fresh_core::api::JsCallbackId, duration_ms: u64) {
3269 if let (Some(runtime), Some(bridge)) = (&self.tokio_runtime, &self.async_bridge) {
3270 let sender = bridge.sender();
3271 let callback_id_u64 = callback_id.as_u64();
3272 runtime.spawn(async move {
3273 tokio::time::sleep(tokio::time::Duration::from_millis(duration_ms)).await;
3274 #[allow(clippy::let_underscore_must_use)]
3275 let _ = sender.send(crate::services::async_bridge::AsyncMessage::Plugin(
3276 fresh_core::api::PluginAsyncMessage::DelayComplete {
3277 callback_id: callback_id_u64,
3278 },
3279 ));
3280 });
3281 } else {
3282 std::thread::sleep(std::time::Duration::from_millis(duration_ms));
3283 self.plugin_manager
3284 .read()
3285 .unwrap()
3286 .resolve_callback(callback_id, "null".to_string());
3287 }
3288 }
3289
3290 fn handle_kill_background_process(&mut self, process_id: u64) {
3291 if let Some(handle) = self.background_process_handles.remove(&process_id) {
3292 handle.abort();
3293 tracing::debug!("Killed background process {}", process_id);
3294 }
3295 }
3296
3297 fn handle_create_virtual_buffer(&mut self, name: String, mode: String, read_only: bool) {
3298 let buffer_id =
3299 self.active_window_mut()
3300 .create_virtual_buffer(name.clone(), mode.clone(), read_only);
3301 tracing::info!(
3302 "Created virtual buffer '{}' with mode '{}' (id={:?})",
3303 name,
3304 mode,
3305 buffer_id
3306 );
3307 }
3309
3310 fn handle_set_virtual_buffer_content(
3311 &mut self,
3312 buffer_id: BufferId,
3313 entries: Vec<fresh_core::text_property::TextPropertyEntry>,
3314 ) {
3315 match self.set_virtual_buffer_content(buffer_id, entries) {
3316 Ok(()) => {
3317 tracing::debug!("Set virtual buffer content for {:?}", buffer_id);
3318 }
3319 Err(e) => {
3320 tracing::error!("Failed to set virtual buffer content: {}", e);
3321 }
3322 }
3323 }
3324
3325 fn handle_mount_widget_panel(
3326 &mut self,
3327 panel_id: u64,
3328 buffer_id: BufferId,
3329 spec: fresh_core::api::WidgetSpec,
3330 ) {
3331 let prev = std::collections::HashMap::new();
3336 let prev_focus = String::new();
3337 let panel_width = self.widget_panel_width(buffer_id);
3338 let out = crate::widgets::render_spec(&spec, &prev, &prev_focus, panel_width);
3339 let focus_cursor = out.focus_cursor;
3340 self.widget_registry.mount(
3341 panel_id,
3342 buffer_id,
3343 spec,
3344 out.hits,
3345 out.instance_states,
3346 out.focus_key,
3347 out.tabbable,
3348 );
3349 let entries = out.entries;
3350 if let Err(e) = self.set_virtual_buffer_content(buffer_id, entries.clone()) {
3351 tracing::error!(
3352 "Failed to render mounted widget panel {} into {:?}: {}",
3353 panel_id,
3354 buffer_id,
3355 e
3356 );
3357 } else {
3358 tracing::debug!(
3359 "Mounted widget panel {} into buffer {:?}",
3360 panel_id,
3361 buffer_id
3362 );
3363 }
3364 self.apply_widget_focus_cursor(buffer_id, &entries, focus_cursor);
3365 }
3366
3367 fn handle_update_widget_panel(&mut self, panel_id: u64, spec: fresh_core::api::WidgetSpec) {
3368 let prev = match self.widget_registry.instance_states(panel_id) {
3369 Some(s) => s.clone(),
3370 None => {
3371 tracing::debug!(
3372 "UpdateWidgetPanel for unknown panel {} ignored (not mounted)",
3373 panel_id
3374 );
3375 return;
3376 }
3377 };
3378 let prev_focus = self
3379 .widget_registry
3380 .focus_key(panel_id)
3381 .map(|s| s.to_string())
3382 .unwrap_or_default();
3383 let buffer_id_for_width = self
3384 .widget_registry
3385 .buffer_and_spec(panel_id)
3386 .map(|(b, _)| b)
3387 .unwrap_or(BufferId(0));
3388 let panel_width = self.widget_panel_width(buffer_id_for_width);
3389 let out = crate::widgets::render_spec(&spec, &prev, &prev_focus, panel_width);
3390 let focus_cursor = out.focus_cursor;
3391 let entries = out.entries;
3392 match self.widget_registry.update(
3393 panel_id,
3394 spec,
3395 out.hits,
3396 out.instance_states,
3397 out.focus_key,
3398 out.tabbable,
3399 ) {
3400 Ok(buffer_id) => {
3401 if let Err(e) = self.set_virtual_buffer_content(buffer_id, entries.clone()) {
3402 tracing::error!("Failed to render updated widget panel {}: {}", panel_id, e);
3403 }
3404 self.apply_widget_focus_cursor(buffer_id, &entries, focus_cursor);
3405 }
3406 Err(()) => {
3407 tracing::debug!(
3408 "UpdateWidgetPanel for unknown panel {} ignored (not mounted)",
3409 panel_id
3410 );
3411 }
3412 }
3413 }
3414
3415 fn apply_widget_focus_cursor(
3426 &mut self,
3427 buffer_id: BufferId,
3428 entries: &[fresh_core::text_property::TextPropertyEntry],
3429 focus_cursor: Option<crate::widgets::FocusCursor>,
3430 ) {
3431 let absolute_byte = focus_cursor.map(|fc| {
3432 let row = fc.buffer_row as usize;
3433 let prefix: usize = entries.iter().take(row).map(|e| e.text.len()).sum();
3434 prefix + fc.byte_in_row as usize
3435 });
3436
3437 if let Some(state) = self
3438 .windows
3439 .get_mut(&self.active_window)
3440 .map(|w| &mut w.buffers)
3441 .expect("active window present")
3442 .get_mut(&buffer_id)
3443 {
3444 state.show_cursors = absolute_byte.is_some();
3445 }
3446
3447 if let Some(byte) = absolute_byte {
3448 for vs in self
3449 .windows
3450 .get_mut(&self.active_window)
3451 .and_then(|w| w.split_view_states_mut())
3452 .expect("active window must have a populated split layout")
3453 .values_mut()
3454 {
3455 if vs.buffer_state(buffer_id).is_some() {
3456 let cursor = vs.cursors.primary_mut();
3457 cursor.position = byte;
3458 }
3459 }
3460 }
3461 }
3462
3463 fn widget_panel_width(&self, buffer_id: BufferId) -> u32 {
3472 let raw = self
3473 .windows
3474 .get(&self.active_window)
3475 .and_then(|w| w.buffers.splits())
3476 .map(|(_, vs)| vs)
3477 .expect("active window must have a populated split layout")
3478 .values()
3479 .find(|vs| vs.buffer_state(buffer_id).is_some() && vs.viewport.width > 0)
3480 .map(|vs| vs.viewport.width as u32)
3481 .unwrap_or_else(|| self.terminal_width.max(1) as u32);
3482 raw.saturating_sub(2).max(10)
3485 }
3486
3487 pub(super) fn rerender_widget_panel(&mut self, panel_id: u64) {
3493 let (buffer_id, is_floating, panel_width, out_pieces) = {
3502 let (buffer_id, spec) = match self.widget_registry.buffer_and_spec_ref(panel_id) {
3503 Some(s) => s,
3504 None => return,
3505 };
3506 let prev = self
3507 .widget_registry
3508 .instance_states(panel_id)
3509 .cloned()
3510 .unwrap_or_default();
3511 let prev_focus = self
3512 .widget_registry
3513 .focus_key(panel_id)
3514 .map(|s| s.to_string())
3515 .unwrap_or_default();
3516 let is_floating = buffer_id == FLOATING_PANEL_BUFFER_ID;
3517 let panel_width = if is_floating {
3518 self.floating_panel_inner_width()
3519 } else {
3520 self.widget_panel_width(buffer_id)
3521 };
3522 let out = crate::widgets::render_spec(spec, &prev, &prev_focus, panel_width);
3523 (buffer_id, is_floating, panel_width, out)
3524 };
3525 let _ = panel_width;
3526 let focus_cursor = out_pieces.focus_cursor;
3527 let entries = out_pieces.entries;
3528 let embeds = out_pieces.embeds;
3529 let overlays = out_pieces.overlays;
3530 if self
3531 .widget_registry
3532 .update_side_effects(
3533 panel_id,
3534 out_pieces.hits,
3535 out_pieces.instance_states,
3536 out_pieces.focus_key,
3537 out_pieces.tabbable,
3538 )
3539 .is_err()
3540 {
3541 tracing::warn!("rerender_widget_panel({}) lost panel mid-call", panel_id);
3542 return;
3543 }
3544 if is_floating {
3545 if let Some(fwp) = self.floating_widget_panel.as_mut() {
3546 if fwp.panel_id == panel_id {
3547 fwp.entries = entries;
3548 fwp.focus_cursor = focus_cursor;
3549 fwp.embeds = embeds;
3550 fwp.overlays = overlays;
3551 }
3552 }
3553 return;
3554 }
3555 if let Err(e) = self.set_virtual_buffer_content(buffer_id, entries.clone()) {
3556 tracing::error!("rerender_widget_panel({}) failed: {}", panel_id, e);
3557 }
3558 self.apply_widget_focus_cursor(buffer_id, &entries, focus_cursor);
3559 }
3560
3561 fn handle_widget_mutate(&mut self, panel_id: u64, mutation: fresh_core::api::WidgetMutation) {
3567 use fresh_core::api::WidgetMutation;
3568
3569 if self.widget_registry.get(panel_id).is_none() {
3571 tracing::debug!(
3572 "WidgetMutate for unknown panel {} ignored (not mounted)",
3573 panel_id
3574 );
3575 return;
3576 }
3577
3578 match mutation {
3579 WidgetMutation::SetValue {
3580 widget_key,
3581 value,
3582 cursor_byte,
3583 } => {
3584 if let Some(panel) = self.widget_registry.get_mut(panel_id) {
3591 let (scroll, multiline, completions, sel_idx, scroll_off) =
3599 match panel.instance_states.get(&widget_key) {
3600 Some(crate::widgets::WidgetInstanceState::Text {
3601 editor,
3602 scroll,
3603 completions,
3604 completion_selected_index,
3605 completion_scroll_offset,
3606 }) => (
3607 *scroll,
3608 editor.multiline,
3609 completions.clone(),
3610 *completion_selected_index,
3611 *completion_scroll_offset,
3612 ),
3613 _ => (0u32, true, Vec::new(), 0usize, 0u32),
3614 };
3615 let mut editor = if multiline {
3616 crate::primitives::text_edit::TextEdit::with_text(&value)
3617 } else {
3618 crate::primitives::text_edit::TextEdit::single_line_with_text(&value)
3619 };
3620 let target = match cursor_byte {
3621 Some(c) if c >= 0 => (c as usize).min(value.len()),
3622 _ => value.len(),
3623 };
3624 editor.set_cursor_from_flat(target);
3625 panel.instance_states.insert(
3626 widget_key,
3627 crate::widgets::WidgetInstanceState::Text {
3628 editor,
3629 scroll,
3630 completions,
3631 completion_selected_index: sel_idx,
3632 completion_scroll_offset: scroll_off,
3633 },
3634 );
3635 }
3636 }
3637 WidgetMutation::SetChecked {
3638 widget_key,
3639 checked,
3640 } => {
3641 if let Some(panel) = self.widget_registry.get_mut(panel_id) {
3645 crate::widgets::set_toggle_checked_in_spec(
3646 &mut panel.spec,
3647 &widget_key,
3648 checked,
3649 );
3650 }
3651 }
3652 WidgetMutation::SetSelectedIndex { widget_key, index } => {
3653 if let Some(panel) = self.widget_registry.get_mut(panel_id) {
3655 let prev_scroll = match panel.instance_states.get(&widget_key) {
3656 Some(crate::widgets::WidgetInstanceState::List {
3657 scroll_offset, ..
3658 }) => *scroll_offset,
3659 _ => 0,
3660 };
3661 panel.instance_states.insert(
3662 widget_key,
3663 crate::widgets::WidgetInstanceState::List {
3664 scroll_offset: prev_scroll,
3665 selected_index: index,
3666 },
3667 );
3668 }
3669 }
3670 WidgetMutation::SetCompletions { widget_key, items } => {
3671 if let Some(panel) = self.widget_registry.get_mut(panel_id) {
3680 if let Some(crate::widgets::WidgetInstanceState::Text {
3681 completions,
3682 completion_selected_index,
3683 completion_scroll_offset,
3684 ..
3685 }) = panel.instance_states.get_mut(&widget_key)
3686 {
3687 *completions = items;
3688 *completion_selected_index = 0;
3689 *completion_scroll_offset = 0;
3690 }
3691 }
3692 }
3693 WidgetMutation::SetItems {
3694 widget_key,
3695 items,
3696 item_keys,
3697 } => {
3698 if let Some(panel) = self.widget_registry.get_mut(panel_id) {
3700 crate::widgets::set_list_items_in_spec(
3701 &mut panel.spec,
3702 &widget_key,
3703 items,
3704 item_keys,
3705 );
3706 }
3707 }
3708 WidgetMutation::SetExpandedKeys { widget_key, keys } => {
3709 if let Some(panel) = self.widget_registry.get_mut(panel_id) {
3711 let (prev_scroll, prev_sel) = match panel.instance_states.get(&widget_key) {
3712 Some(crate::widgets::WidgetInstanceState::Tree {
3713 scroll_offset,
3714 selected_index,
3715 ..
3716 }) => (*scroll_offset, *selected_index),
3717 _ => (0, -1),
3718 };
3719 let expanded: std::collections::HashSet<String> = keys.into_iter().collect();
3720 panel.instance_states.insert(
3721 widget_key,
3722 crate::widgets::WidgetInstanceState::Tree {
3723 scroll_offset: prev_scroll,
3724 selected_index: prev_sel,
3725 expanded_keys: expanded,
3726 },
3727 );
3728 }
3729 }
3730 WidgetMutation::SetCheckedKeys {
3731 widget_key,
3732 checked,
3733 keys,
3734 } => {
3735 if let Some(panel) = self.widget_registry.get_mut(panel_id) {
3743 crate::widgets::set_tree_checked_keys_in_spec(
3744 &mut panel.spec,
3745 &widget_key,
3746 checked,
3747 &keys,
3748 );
3749 }
3750 }
3751 WidgetMutation::AppendTreeNodes {
3752 widget_key,
3753 new_nodes,
3754 new_item_keys,
3755 } => {
3756 if let Some(panel) = self.widget_registry.get_mut(panel_id) {
3757 crate::widgets::append_tree_nodes_in_spec(
3758 &mut panel.spec,
3759 &widget_key,
3760 new_nodes,
3761 new_item_keys,
3762 );
3763 }
3764 }
3765 WidgetMutation::SetRawEntries {
3766 widget_key,
3767 entries,
3768 } => {
3769 if let Some(panel) = self.widget_registry.get_mut(panel_id) {
3770 crate::widgets::set_raw_entries_in_spec(&mut panel.spec, &widget_key, entries);
3771 }
3772 }
3773 WidgetMutation::SetFocusKey { widget_key } => {
3774 self.widget_registry.set_focus_key(panel_id, widget_key);
3779 }
3780 }
3781
3782 self.rerender_widget_panel(panel_id);
3786 }
3787
3788 pub(super) fn handle_widget_command(
3789 &mut self,
3790 panel_id: u64,
3791 action: fresh_core::api::WidgetAction,
3792 ) {
3793 use fresh_core::api::WidgetAction;
3794 match action {
3795 WidgetAction::FocusAdvance { delta } => {
3796 self.handle_widget_focus_advance(panel_id, delta);
3797 }
3798 WidgetAction::Activate => {
3799 self.handle_widget_activate(panel_id);
3800 }
3801 WidgetAction::SelectMove { delta } => {
3802 self.handle_widget_select_move(panel_id, delta);
3803 }
3804 WidgetAction::TextInputKey { key } => {
3805 self.handle_widget_text_key(panel_id, &key);
3806 }
3807 WidgetAction::TextInputChar { text } => {
3808 self.handle_widget_text_char(panel_id, &text);
3809 }
3810 WidgetAction::Key { key } => {
3811 self.handle_widget_key(panel_id, &key);
3812 }
3813 }
3814 }
3815
3816 fn handle_widget_key(&mut self, panel_id: u64, key: &str) {
3817 let panel = match self.widget_registry.get(panel_id) {
3821 Some(p) => p,
3822 None => return,
3823 };
3824 let focus_key = panel.focus_key.clone();
3825 let widget = if focus_key.is_empty() {
3826 None
3827 } else {
3828 crate::widgets::find_widget_by_key(&panel.spec, &focus_key)
3829 };
3830 let completions_open = matches!(key, "Tab" | "Up" | "Down" | "Enter" | "Escape")
3840 && self.focused_text_completions_open(panel_id);
3841 if completions_open {
3842 match key {
3843 "Tab" => {
3844 self.fire_completion_accept(panel_id);
3845 return;
3850 }
3851 "Up" => {
3852 self.move_focused_text_completion_index(panel_id, -1);
3853 self.rerender_widget_panel(panel_id);
3858 return;
3859 }
3860 "Down" => {
3861 self.move_focused_text_completion_index(panel_id, 1);
3862 self.rerender_widget_panel(panel_id);
3863 return;
3864 }
3865 "Enter" | "Escape" => {
3866 self.dismiss_focused_text_completions(panel_id);
3867 self.rerender_widget_panel(panel_id);
3868 return;
3869 }
3870 _ => {}
3871 }
3872 }
3873 match key {
3874 "Tab" => self.handle_widget_focus_advance(panel_id, 1),
3875 "Shift+Tab" => self.handle_widget_focus_advance(panel_id, -1),
3876 "Up" | "Down" => {
3877 let delta = if key == "Up" { -1 } else { 1 };
3878 match widget {
3879 Some(fresh_core::api::WidgetSpec::List { .. }) => {
3880 self.handle_widget_select_move(panel_id, delta);
3881 }
3882 Some(fresh_core::api::WidgetSpec::Tree { .. }) => {
3883 self.handle_widget_tree_select_move(panel_id, delta);
3884 }
3885 Some(fresh_core::api::WidgetSpec::Text { rows, .. }) if *rows > 1 => {
3886 self.handle_widget_text_key(panel_id, key);
3892 }
3893 _ => {
3894 let scrollable = self
3902 .widget_registry
3903 .get(panel_id)
3904 .and_then(|p| find_scrollable_widget_key(&p.spec));
3905 if let Some(target_key) = scrollable {
3906 let target_kind = self.widget_registry.get(panel_id).and_then(|p| {
3907 crate::widgets::find_widget_by_key(&p.spec, &target_key).cloned()
3908 });
3909 match target_kind {
3910 Some(fresh_core::api::WidgetSpec::List { .. }) => {
3911 self.handle_widget_select_move_for_key(
3912 panel_id,
3913 &target_key,
3914 delta,
3915 );
3916 }
3917 Some(fresh_core::api::WidgetSpec::Tree { .. }) => {
3918 self.handle_widget_tree_select_move_for_key(
3919 panel_id,
3920 &target_key,
3921 delta,
3922 );
3923 }
3924 _ => {}
3925 }
3926 }
3927 }
3928 }
3929 }
3930 "PageUp" | "PageDown" => {
3931 let page = match widget {
3935 Some(fresh_core::api::WidgetSpec::List { visible_rows, .. })
3936 | Some(fresh_core::api::WidgetSpec::Tree { visible_rows, .. }) => {
3937 visible_rows.saturating_sub(1).max(1) as i32
3938 }
3939 _ => 0,
3940 };
3941 if page == 0 {
3942 return;
3943 }
3944 let delta = if key == "PageUp" { -page } else { page };
3945 match widget {
3946 Some(fresh_core::api::WidgetSpec::List { .. }) => {
3947 self.handle_widget_select_move(panel_id, delta);
3948 }
3949 Some(fresh_core::api::WidgetSpec::Tree { .. }) => {
3950 self.handle_widget_tree_select_move(panel_id, delta);
3951 }
3952 _ => {}
3953 }
3954 }
3955 "Left" | "Right" => match widget {
3956 Some(fresh_core::api::WidgetSpec::Text { .. }) => {
3957 self.handle_widget_text_key(panel_id, key);
3958 }
3959 Some(fresh_core::api::WidgetSpec::Tree { .. }) => {
3960 self.handle_widget_tree_lateral(panel_id, key == "Right");
3961 }
3962 _ => {}
3963 },
3964 "Backspace" | "Delete" | "Home" | "End" => match widget {
3965 Some(fresh_core::api::WidgetSpec::Text { .. }) => {
3966 self.handle_widget_text_key(panel_id, key);
3967 }
3968 _ => {}
3969 },
3970 "Enter" => match widget {
3971 Some(fresh_core::api::WidgetSpec::Button { .. })
3972 | Some(fresh_core::api::WidgetSpec::Toggle { .. }) => {
3973 self.handle_widget_activate(panel_id);
3974 }
3975 Some(fresh_core::api::WidgetSpec::List { .. }) => {
3976 self.fire_list_activate(panel_id, &focus_key);
3977 }
3978 Some(fresh_core::api::WidgetSpec::Tree { .. }) => {
3979 self.fire_tree_activate(panel_id, &focus_key);
3980 }
3981 Some(fresh_core::api::WidgetSpec::Text { rows, .. }) => {
3982 if *rows > 1 {
3983 self.handle_widget_text_key(panel_id, "Enter");
3989 } else if let Some(target_key) = self
3990 .widget_registry
3991 .get(panel_id)
3992 .and_then(|p| find_scrollable_widget_key(&p.spec))
3993 {
3994 let kind = self.widget_registry.get(panel_id).and_then(|p| {
4000 crate::widgets::find_widget_by_key(&p.spec, &target_key).cloned()
4001 });
4002 match kind {
4003 Some(fresh_core::api::WidgetSpec::List { .. }) => {
4004 self.fire_list_activate(panel_id, &target_key);
4005 }
4006 Some(fresh_core::api::WidgetSpec::Tree { .. }) => {
4007 self.fire_tree_activate(panel_id, &target_key);
4008 }
4009 _ => {}
4010 }
4011 } else {
4012 self.handle_widget_focus_advance(panel_id, 1);
4015 }
4016 }
4017 _ => {}
4018 },
4019 "Space" => match widget {
4020 Some(fresh_core::api::WidgetSpec::Button { .. })
4021 | Some(fresh_core::api::WidgetSpec::Toggle { .. }) => {
4022 self.handle_widget_activate(panel_id);
4023 }
4024 Some(fresh_core::api::WidgetSpec::Text { .. }) => {
4025 self.handle_widget_text_char(panel_id, " ");
4026 }
4027 Some(fresh_core::api::WidgetSpec::List { .. }) => {
4028 self.fire_list_activate(panel_id, &focus_key);
4029 }
4030 Some(fresh_core::api::WidgetSpec::Tree { .. }) => {
4031 if !self.fire_tree_toggle_if_checkable(panel_id, &focus_key) {
4038 self.fire_tree_activate(panel_id, &focus_key);
4039 }
4040 }
4041 _ => {}
4042 },
4043 _ => {} }
4045 }
4046
4047 fn handle_widget_focus_advance(&mut self, panel_id: u64, delta: i32) {
4048 let panel = match self.widget_registry.get(panel_id) {
4049 Some(p) => p,
4050 None => return,
4051 };
4052 if panel.tabbable.is_empty() {
4053 return;
4054 }
4055 let cur_idx = panel
4056 .tabbable
4057 .iter()
4058 .position(|k| k == &panel.focus_key)
4059 .unwrap_or(0) as i32;
4060 let n = panel.tabbable.len() as i32;
4061 let new_idx = ((cur_idx + delta) % n + n) % n;
4062 let new_key = panel.tabbable[new_idx as usize].clone();
4063 self.set_panel_focus_and_notify(panel_id, new_key);
4064 self.rerender_widget_panel(panel_id);
4065 }
4066
4067 pub(crate) fn set_panel_focus_and_notify(&mut self, panel_id: u64, new_key: String) {
4077 let old_key = self
4078 .widget_registry
4079 .focus_key(panel_id)
4080 .map(|s| s.to_string())
4081 .unwrap_or_default();
4082 if old_key == new_key {
4083 return;
4084 }
4085 self.widget_registry
4086 .set_focus_key(panel_id, new_key.clone());
4087 if self
4088 .plugin_manager
4089 .read()
4090 .unwrap()
4091 .has_hook_handlers("widget_event")
4092 {
4093 self.plugin_manager.read().unwrap().run_hook(
4094 "widget_event",
4095 fresh_core::hooks::HookArgs::WidgetEvent {
4096 panel_id,
4097 widget_key: new_key,
4098 event_type: "focus".to_string(),
4099 payload: serde_json::json!({ "previous": old_key }),
4100 },
4101 );
4102 }
4103 }
4104
4105 fn handle_widget_activate(&mut self, panel_id: u64) {
4106 let panel = match self.widget_registry.get(panel_id) {
4110 Some(p) => p,
4111 None => return,
4112 };
4113 let focus_key = panel.focus_key.clone();
4114 if focus_key.is_empty() {
4115 return;
4116 }
4117 let widget = crate::widgets::find_widget_by_key(&panel.spec, &focus_key);
4118 let (event_type, payload) = match widget {
4119 Some(fresh_core::api::WidgetSpec::Button { disabled: true, .. }) => return,
4126 Some(fresh_core::api::WidgetSpec::Button { .. }) => ("activate", serde_json::json!({})),
4127 Some(fresh_core::api::WidgetSpec::Toggle { checked, .. }) => {
4128 ("toggle", serde_json::json!({ "checked": !checked }))
4129 }
4130 _ => return,
4131 };
4132 if self
4133 .plugin_manager
4134 .read()
4135 .unwrap()
4136 .has_hook_handlers("widget_event")
4137 {
4138 self.plugin_manager.read().unwrap().run_hook(
4139 "widget_event",
4140 fresh_core::hooks::HookArgs::WidgetEvent {
4141 panel_id,
4142 widget_key: focus_key,
4143 event_type: event_type.to_string(),
4144 payload,
4145 },
4146 );
4147 }
4148 }
4149
4150 fn focused_text_completions_open(&self, panel_id: u64) -> bool {
4162 let panel = match self.widget_registry.get(panel_id) {
4163 Some(p) => p,
4164 None => return false,
4165 };
4166 if panel.focus_key.is_empty() {
4167 return false;
4168 }
4169 matches!(
4170 panel.instance_states.get(&panel.focus_key),
4171 Some(crate::widgets::WidgetInstanceState::Text { completions, .. })
4172 if !completions.is_empty()
4173 )
4174 }
4175
4176 fn move_focused_text_completion_index(&mut self, panel_id: u64, delta: i32) {
4187 let panel = match self.widget_registry.get(panel_id) {
4194 Some(p) => p,
4195 None => return,
4196 };
4197 let focus_key = panel.focus_key.clone();
4198 if focus_key.is_empty() {
4199 return;
4200 }
4201 let spec_visible_rows = match crate::widgets::find_widget_by_key(&panel.spec, &focus_key) {
4202 Some(fresh_core::api::WidgetSpec::Text {
4203 completions_visible_rows,
4204 ..
4205 }) => *completions_visible_rows,
4206 _ => 0,
4207 };
4208 let visible = if spec_visible_rows == 0 {
4209 5u32
4210 } else {
4211 spec_visible_rows
4212 };
4213 let panel = match self.widget_registry.get_mut(panel_id) {
4214 Some(p) => p,
4215 None => return,
4216 };
4217 if let Some(crate::widgets::WidgetInstanceState::Text {
4218 completions,
4219 completion_selected_index,
4220 completion_scroll_offset,
4221 ..
4222 }) = panel.instance_states.get_mut(&focus_key)
4223 {
4224 if completions.is_empty() {
4225 return;
4226 }
4227 let max = (completions.len() - 1) as i32;
4228 let cur = *completion_selected_index as i32;
4229 let next = (cur + delta).clamp(0, max);
4230 *completion_selected_index = next as usize;
4231 let next_u = next as u32;
4236 if next_u < *completion_scroll_offset {
4237 *completion_scroll_offset = next_u;
4238 } else if next_u >= *completion_scroll_offset + visible {
4239 *completion_scroll_offset = next_u + 1 - visible;
4240 }
4241 }
4242 }
4243
4244 fn dismiss_focused_text_completions(&mut self, panel_id: u64) {
4251 let focus_key = {
4252 let panel = match self.widget_registry.get_mut(panel_id) {
4253 Some(p) => p,
4254 None => return,
4255 };
4256 let focus_key = panel.focus_key.clone();
4257 if focus_key.is_empty() {
4258 return;
4259 }
4260 if let Some(crate::widgets::WidgetInstanceState::Text {
4261 completions,
4262 completion_selected_index,
4263 ..
4264 }) = panel.instance_states.get_mut(&focus_key)
4265 {
4266 if completions.is_empty() {
4267 return;
4268 }
4269 completions.clear();
4270 *completion_selected_index = 0;
4271 } else {
4272 return;
4273 }
4274 focus_key
4275 };
4276 if self
4277 .plugin_manager
4278 .read()
4279 .unwrap()
4280 .has_hook_handlers("widget_event")
4281 {
4282 self.plugin_manager.read().unwrap().run_hook(
4283 "widget_event",
4284 fresh_core::hooks::HookArgs::WidgetEvent {
4285 panel_id,
4286 widget_key: focus_key,
4287 event_type: "completion_dismiss".into(),
4288 payload: serde_json::json!({}),
4289 },
4290 );
4291 }
4292 }
4293
4294 fn fire_completion_accept(&mut self, panel_id: u64) {
4306 let (focus_key, value) = {
4307 let panel = match self.widget_registry.get(panel_id) {
4308 Some(p) => p,
4309 None => return,
4310 };
4311 let focus_key = panel.focus_key.clone();
4312 if focus_key.is_empty() {
4313 return;
4314 }
4315 match panel.instance_states.get(&focus_key) {
4316 Some(crate::widgets::WidgetInstanceState::Text {
4317 completions,
4318 completion_selected_index,
4319 ..
4320 }) if !completions.is_empty() => {
4321 let idx = (*completion_selected_index).min(completions.len() - 1);
4322 (focus_key, completions[idx].value.clone())
4323 }
4324 _ => return,
4325 }
4326 };
4327 if self
4328 .plugin_manager
4329 .read()
4330 .unwrap()
4331 .has_hook_handlers("widget_event")
4332 {
4333 self.plugin_manager.read().unwrap().run_hook(
4334 "widget_event",
4335 fresh_core::hooks::HookArgs::WidgetEvent {
4336 panel_id,
4337 widget_key: focus_key,
4338 event_type: "completion_accept".into(),
4339 payload: serde_json::json!({ "value": value }),
4340 },
4341 );
4342 }
4343 }
4344
4345 fn fire_list_activate(&mut self, panel_id: u64, focus_key: &str) {
4346 let panel = match self.widget_registry.get(panel_id) {
4347 Some(p) => p,
4348 None => return,
4349 };
4350 let widget = crate::widgets::find_widget_by_key(&panel.spec, focus_key);
4351 let (spec_sel, item_keys) = match widget {
4352 Some(fresh_core::api::WidgetSpec::List {
4353 selected_index,
4354 item_keys,
4355 ..
4356 }) => (*selected_index, item_keys.clone()),
4357 _ => return,
4358 };
4359 let sel = match panel.instance_states.get(focus_key) {
4360 Some(crate::widgets::WidgetInstanceState::List { selected_index, .. }) => {
4361 *selected_index
4362 }
4363 _ => spec_sel,
4364 };
4365 if sel < 0 {
4366 return;
4367 }
4368 let item_key = item_keys.get(sel as usize).cloned().unwrap_or_default();
4369 if self
4370 .plugin_manager
4371 .read()
4372 .unwrap()
4373 .has_hook_handlers("widget_event")
4374 {
4375 self.plugin_manager.read().unwrap().run_hook(
4376 "widget_event",
4377 fresh_core::hooks::HookArgs::WidgetEvent {
4378 panel_id,
4379 widget_key: focus_key.to_string(),
4380 event_type: "activate".into(),
4381 payload: serde_json::json!({
4382 "index": sel,
4383 "key": item_key,
4384 }),
4385 },
4386 );
4387 }
4388 }
4389
4390 fn handle_widget_select_move(&mut self, panel_id: u64, delta: i32) {
4391 let focus_key = match self.widget_registry.get(panel_id) {
4392 Some(p) => p.focus_key.clone(),
4393 None => return,
4394 };
4395 if focus_key.is_empty() {
4396 return;
4397 }
4398 self.handle_widget_select_move_for_key(panel_id, &focus_key, delta);
4399 }
4400
4401 fn handle_widget_select_move_for_key(&mut self, panel_id: u64, widget_key: &str, delta: i32) {
4407 let panel = match self.widget_registry.get(panel_id) {
4408 Some(p) => p,
4409 None => return,
4410 };
4411 let widget = crate::widgets::find_widget_by_key(&panel.spec, widget_key);
4412 let (spec_sel, total, item_keys) = match widget {
4413 Some(fresh_core::api::WidgetSpec::List {
4414 selected_index,
4415 items,
4416 item_keys,
4417 ..
4418 }) => (*selected_index, items.len() as i32, item_keys.clone()),
4419 _ => return,
4420 };
4421 if total == 0 {
4422 return;
4423 }
4424 let cur_sel = match panel.instance_states.get(widget_key) {
4425 Some(crate::widgets::WidgetInstanceState::List { selected_index, .. }) => {
4426 *selected_index
4427 }
4428 _ => spec_sel,
4429 };
4430 let raw = if cur_sel < 0 { 0 } else { cur_sel + delta };
4431 let new_sel = raw.clamp(0, total - 1);
4432 let new_key = item_keys.get(new_sel as usize).cloned().unwrap_or_default();
4433 if let Some(panel_mut) = self.widget_registry.get_mut(panel_id) {
4434 let cur_scroll = match panel_mut.instance_states.get(widget_key) {
4435 Some(crate::widgets::WidgetInstanceState::List { scroll_offset, .. }) => {
4436 *scroll_offset
4437 }
4438 _ => 0,
4439 };
4440 panel_mut.instance_states.insert(
4441 widget_key.to_string(),
4442 crate::widgets::WidgetInstanceState::List {
4443 scroll_offset: cur_scroll,
4444 selected_index: new_sel,
4445 },
4446 );
4447 }
4448 self.rerender_widget_panel(panel_id);
4449 if self
4450 .plugin_manager
4451 .read()
4452 .unwrap()
4453 .has_hook_handlers("widget_event")
4454 {
4455 self.plugin_manager.read().unwrap().run_hook(
4456 "widget_event",
4457 fresh_core::hooks::HookArgs::WidgetEvent {
4458 panel_id,
4459 widget_key: widget_key.to_string(),
4460 event_type: "select".into(),
4461 payload: serde_json::json!({ "index": new_sel, "key": new_key }),
4462 },
4463 );
4464 }
4465 }
4466
4467 fn handle_widget_tree_select_move(&mut self, panel_id: u64, delta: i32) {
4472 let focus_key = match self.widget_registry.get(panel_id) {
4473 Some(p) => p.focus_key.clone(),
4474 None => return,
4475 };
4476 if focus_key.is_empty() {
4477 return;
4478 }
4479 self.handle_widget_tree_select_move_for_key(panel_id, &focus_key, delta);
4480 }
4481
4482 fn handle_widget_tree_select_move_for_key(
4484 &mut self,
4485 panel_id: u64,
4486 widget_key: &str,
4487 delta: i32,
4488 ) {
4489 let panel = match self.widget_registry.get(panel_id) {
4490 Some(p) => p,
4491 None => return,
4492 };
4493 let widget = crate::widgets::find_widget_by_key(&panel.spec, widget_key);
4494 let (spec_sel, nodes, item_keys) = match widget {
4495 Some(fresh_core::api::WidgetSpec::Tree {
4496 selected_index,
4497 nodes,
4498 item_keys,
4499 ..
4500 }) => (*selected_index, nodes.clone(), item_keys.clone()),
4501 _ => return,
4502 };
4503 if nodes.is_empty() {
4504 return;
4505 }
4506 let (cur_sel, cur_scroll, expanded) = match panel.instance_states.get(widget_key) {
4507 Some(crate::widgets::WidgetInstanceState::Tree {
4508 selected_index,
4509 scroll_offset,
4510 expanded_keys,
4511 }) => (*selected_index, *scroll_offset, expanded_keys.clone()),
4512 _ => (spec_sel, 0u32, std::collections::HashSet::<String>::new()),
4513 };
4514 let visible_indices = collect_visible_tree_indices(&nodes, &item_keys, &expanded);
4515 if visible_indices.is_empty() {
4516 return;
4517 }
4518 let cur_pos = if cur_sel < 0 {
4519 if delta > 0 {
4520 -1
4521 } else {
4522 visible_indices.len() as i32
4523 }
4524 } else {
4525 visible_indices
4526 .iter()
4527 .position(|&v| v as i32 == cur_sel)
4528 .map(|p| p as i32)
4529 .unwrap_or(-1)
4530 };
4531 let new_pos = (cur_pos + delta).clamp(0, (visible_indices.len() as i32) - 1);
4532 let new_abs = visible_indices[new_pos as usize];
4533 let new_key = item_keys.get(new_abs).cloned().unwrap_or_default();
4534 if let Some(panel_mut) = self.widget_registry.get_mut(panel_id) {
4535 panel_mut.instance_states.insert(
4536 widget_key.to_string(),
4537 crate::widgets::WidgetInstanceState::Tree {
4538 scroll_offset: cur_scroll,
4539 selected_index: new_abs as i32,
4540 expanded_keys: expanded,
4541 },
4542 );
4543 }
4544 self.rerender_widget_panel(panel_id);
4545 if self
4546 .plugin_manager
4547 .read()
4548 .unwrap()
4549 .has_hook_handlers("widget_event")
4550 {
4551 self.plugin_manager.read().unwrap().run_hook(
4552 "widget_event",
4553 fresh_core::hooks::HookArgs::WidgetEvent {
4554 panel_id,
4555 widget_key: widget_key.to_string(),
4556 event_type: "select".into(),
4557 payload: serde_json::json!({ "index": new_abs as i64, "key": new_key }),
4558 },
4559 );
4560 }
4561 }
4562
4563 pub(super) fn handle_widget_panel_wheel(
4573 &mut self,
4574 buffer_id: crate::model::event::BufferId,
4575 delta: i32,
4576 ) -> bool {
4577 let panels = self.widget_registry.panels_for_buffer(buffer_id);
4578 let mut consumed = false;
4579 for panel_id in panels {
4580 if self.focused_text_completions_open(panel_id) {
4586 self.scroll_focused_text_completions(panel_id, delta);
4587 self.rerender_widget_panel(panel_id);
4596 consumed = true;
4597 continue;
4598 }
4599 let spec = match self.widget_registry.get(panel_id) {
4600 Some(p) => p.spec.clone(),
4601 None => continue,
4602 };
4603 let Some(widget_key) = find_scrollable_widget_key(&spec) else {
4604 continue;
4605 };
4606 let widget = crate::widgets::find_widget_by_key(&spec, &widget_key);
4607 match widget {
4608 Some(fresh_core::api::WidgetSpec::Tree { .. }) => {
4609 self.handle_widget_tree_wheel(panel_id, &widget_key, delta);
4610 consumed = true;
4611 }
4612 Some(fresh_core::api::WidgetSpec::List { .. }) => {
4613 self.handle_widget_list_wheel(panel_id, &widget_key, delta);
4614 consumed = true;
4615 }
4616 _ => {}
4617 }
4618 }
4619 consumed
4620 }
4621
4622 fn scroll_focused_text_completions(&mut self, panel_id: u64, delta: i32) {
4629 let panel = match self.widget_registry.get(panel_id) {
4630 Some(p) => p,
4631 None => return,
4632 };
4633 let focus_key = panel.focus_key.clone();
4634 if focus_key.is_empty() {
4635 return;
4636 }
4637 let spec_visible_rows = match crate::widgets::find_widget_by_key(&panel.spec, &focus_key) {
4638 Some(fresh_core::api::WidgetSpec::Text {
4639 completions_visible_rows,
4640 ..
4641 }) => *completions_visible_rows,
4642 _ => 0,
4643 };
4644 let visible = if spec_visible_rows == 0 {
4645 5u32
4646 } else {
4647 spec_visible_rows
4648 };
4649 let panel = match self.widget_registry.get_mut(panel_id) {
4650 Some(p) => p,
4651 None => return,
4652 };
4653 if let Some(crate::widgets::WidgetInstanceState::Text {
4654 completions,
4655 completion_scroll_offset,
4656 ..
4657 }) = panel.instance_states.get_mut(&focus_key)
4658 {
4659 if completions.is_empty() {
4660 return;
4661 }
4662 let total = completions.len() as u32;
4663 let max_scroll = total.saturating_sub(visible.min(total));
4664 let next = (*completion_scroll_offset as i32 + delta).clamp(0, max_scroll as i32);
4665 *completion_scroll_offset = next as u32;
4666 }
4667 }
4668
4669 fn handle_widget_tree_wheel(&mut self, panel_id: u64, widget_key: &str, delta: i32) {
4674 let panel = match self.widget_registry.get(panel_id) {
4675 Some(p) => p,
4676 None => return,
4677 };
4678 let widget = crate::widgets::find_widget_by_key(&panel.spec, widget_key);
4679 let (visible_rows, nodes, item_keys) = match widget {
4680 Some(fresh_core::api::WidgetSpec::Tree {
4681 visible_rows,
4682 nodes,
4683 item_keys,
4684 ..
4685 }) => (*visible_rows, nodes.clone(), item_keys.clone()),
4686 _ => return,
4687 };
4688 if nodes.is_empty() {
4689 return;
4690 }
4691 let (cur_sel, cur_scroll, expanded) = match panel.instance_states.get(widget_key) {
4692 Some(crate::widgets::WidgetInstanceState::Tree {
4693 selected_index,
4694 scroll_offset,
4695 expanded_keys,
4696 }) => (*selected_index, *scroll_offset, expanded_keys.clone()),
4697 _ => (-1, 0, std::collections::HashSet::<String>::new()),
4698 };
4699 let visible_indices = collect_visible_tree_indices(&nodes, &item_keys, &expanded);
4700 if visible_indices.is_empty() {
4701 return;
4702 }
4703 let visible = visible_rows.max(1);
4704 let total_visible = visible_indices.len() as u32;
4705 let max_scroll = total_visible.saturating_sub(visible);
4706 let new_scroll = (cur_scroll as i32 + delta).clamp(0, max_scroll as i32) as u32;
4707 if new_scroll == cur_scroll {
4708 return;
4709 }
4710 let cur_pos: Option<u32> = if cur_sel >= 0 {
4712 visible_indices
4713 .iter()
4714 .position(|&v| v as i32 == cur_sel)
4715 .map(|p| p as u32)
4716 } else {
4717 None
4718 };
4719 let new_sel_abs = match cur_pos {
4720 Some(pos) if pos < new_scroll => visible_indices[new_scroll as usize] as i32,
4721 Some(pos) if pos >= new_scroll + visible => {
4722 visible_indices[(new_scroll + visible - 1) as usize] as i32
4723 }
4724 _ => cur_sel,
4725 };
4726 if let Some(panel_mut) = self.widget_registry.get_mut(panel_id) {
4727 panel_mut.instance_states.insert(
4728 widget_key.to_string(),
4729 crate::widgets::WidgetInstanceState::Tree {
4730 scroll_offset: new_scroll,
4731 selected_index: new_sel_abs,
4732 expanded_keys: expanded,
4733 },
4734 );
4735 }
4736 self.rerender_widget_panel(panel_id);
4737 }
4738
4739 fn handle_widget_list_wheel(&mut self, panel_id: u64, widget_key: &str, delta: i32) {
4741 let panel = match self.widget_registry.get(panel_id) {
4742 Some(p) => p,
4743 None => return,
4744 };
4745 let widget = crate::widgets::find_widget_by_key(&panel.spec, widget_key);
4746 let (visible_rows, total) = match widget {
4747 Some(fresh_core::api::WidgetSpec::List {
4748 visible_rows,
4749 items,
4750 ..
4751 }) => (*visible_rows, items.len() as u32),
4752 _ => return,
4753 };
4754 if total == 0 {
4755 return;
4756 }
4757 let (cur_sel, cur_scroll) = match panel.instance_states.get(widget_key) {
4758 Some(crate::widgets::WidgetInstanceState::List {
4759 selected_index,
4760 scroll_offset,
4761 }) => (*selected_index, *scroll_offset),
4762 _ => (-1, 0),
4763 };
4764 let visible = visible_rows.max(1);
4765 let max_scroll = total.saturating_sub(visible);
4766 let new_scroll = (cur_scroll as i32 + delta).clamp(0, max_scroll as i32) as u32;
4767 if new_scroll == cur_scroll {
4768 return;
4769 }
4770 let new_sel = if cur_sel < 0 {
4771 cur_sel
4772 } else if (cur_sel as u32) < new_scroll {
4773 new_scroll as i32
4774 } else if (cur_sel as u32) >= new_scroll + visible {
4775 (new_scroll + visible - 1) as i32
4776 } else {
4777 cur_sel
4778 };
4779 if let Some(panel_mut) = self.widget_registry.get_mut(panel_id) {
4780 panel_mut.instance_states.insert(
4781 widget_key.to_string(),
4782 crate::widgets::WidgetInstanceState::List {
4783 scroll_offset: new_scroll,
4784 selected_index: new_sel,
4785 },
4786 );
4787 }
4788 self.rerender_widget_panel(panel_id);
4789 }
4790
4791 fn handle_widget_tree_lateral(&mut self, panel_id: u64, is_right: bool) {
4801 let panel = match self.widget_registry.get(panel_id) {
4802 Some(p) => p,
4803 None => return,
4804 };
4805 let focus_key = panel.focus_key.clone();
4806 if focus_key.is_empty() {
4807 return;
4808 }
4809 let widget = crate::widgets::find_widget_by_key(&panel.spec, &focus_key);
4810 let (spec_sel, nodes, item_keys) = match widget {
4811 Some(fresh_core::api::WidgetSpec::Tree {
4812 selected_index,
4813 nodes,
4814 item_keys,
4815 ..
4816 }) => (*selected_index, nodes.clone(), item_keys.clone()),
4817 _ => return,
4818 };
4819 if nodes.is_empty() {
4820 return;
4821 }
4822 let (cur_sel, cur_scroll, mut expanded) = match panel.instance_states.get(&focus_key) {
4823 Some(crate::widgets::WidgetInstanceState::Tree {
4824 selected_index,
4825 scroll_offset,
4826 expanded_keys,
4827 }) => (*selected_index, *scroll_offset, expanded_keys.clone()),
4828 _ => (spec_sel, 0u32, std::collections::HashSet::<String>::new()),
4829 };
4830 if cur_sel < 0 {
4831 return;
4832 }
4833 let sel_idx = cur_sel as usize;
4834 let node = match nodes.get(sel_idx) {
4835 Some(n) => n,
4836 None => return,
4837 };
4838 let key = item_keys.get(sel_idx).cloned().unwrap_or_default();
4839 let was_expanded = !key.is_empty() && expanded.contains(&key);
4840
4841 let mut new_sel = cur_sel;
4842 let mut expansion_changed: Option<bool> = None; if is_right {
4844 if node.has_children && !was_expanded && !key.is_empty() {
4845 expanded.insert(key.clone());
4846 expansion_changed = Some(true);
4847 }
4848 } else if node.has_children && was_expanded && !key.is_empty() {
4849 expanded.remove(&key);
4850 expansion_changed = Some(false);
4851 } else if let Some(parent_idx) = crate::widgets::tree_parent_index(&nodes, sel_idx) {
4852 new_sel = parent_idx as i32;
4853 }
4854 if expansion_changed.is_none() && new_sel == cur_sel {
4856 return;
4857 }
4858 let final_key = item_keys.get(new_sel as usize).cloned().unwrap_or_default();
4859 if let Some(panel_mut) = self.widget_registry.get_mut(panel_id) {
4860 panel_mut.instance_states.insert(
4861 focus_key.clone(),
4862 crate::widgets::WidgetInstanceState::Tree {
4863 scroll_offset: cur_scroll,
4864 selected_index: new_sel,
4865 expanded_keys: expanded,
4866 },
4867 );
4868 }
4869 self.rerender_widget_panel(panel_id);
4870 if self
4871 .plugin_manager
4872 .read()
4873 .unwrap()
4874 .has_hook_handlers("widget_event")
4875 {
4876 if let Some(now_expanded) = expansion_changed {
4877 self.plugin_manager.read().unwrap().run_hook(
4878 "widget_event",
4879 fresh_core::hooks::HookArgs::WidgetEvent {
4880 panel_id,
4881 widget_key: focus_key.clone(),
4882 event_type: "expand".into(),
4883 payload: serde_json::json!({
4884 "index": cur_sel as i64,
4885 "key": key,
4886 "expanded": now_expanded,
4887 }),
4888 },
4889 );
4890 } else if new_sel != cur_sel {
4891 self.plugin_manager.read().unwrap().run_hook(
4892 "widget_event",
4893 fresh_core::hooks::HookArgs::WidgetEvent {
4894 panel_id,
4895 widget_key: focus_key,
4896 event_type: "select".into(),
4897 payload: serde_json::json!({
4898 "index": new_sel as i64,
4899 "key": final_key,
4900 }),
4901 },
4902 );
4903 }
4904 }
4905 }
4906
4907 pub(crate) fn handle_widget_tree_expand_toggle(
4911 &mut self,
4912 panel_id: u64,
4913 widget_key: &str,
4914 item_key: &str,
4915 ) {
4916 if widget_key.is_empty() || item_key.is_empty() {
4917 return;
4918 }
4919 let now_expanded = {
4920 let panel = match self.widget_registry.get_mut(panel_id) {
4921 Some(p) => p,
4922 None => return,
4923 };
4924 let (cur_scroll, cur_sel, mut expanded) = match panel.instance_states.get(widget_key) {
4925 Some(crate::widgets::WidgetInstanceState::Tree {
4926 scroll_offset,
4927 selected_index,
4928 expanded_keys,
4929 }) => (*scroll_offset, *selected_index, expanded_keys.clone()),
4930 _ => (0u32, -1i32, std::collections::HashSet::<String>::new()),
4931 };
4932 let next = if expanded.contains(item_key) {
4933 expanded.remove(item_key);
4934 false
4935 } else {
4936 expanded.insert(item_key.to_string());
4937 true
4938 };
4939 panel.instance_states.insert(
4940 widget_key.to_string(),
4941 crate::widgets::WidgetInstanceState::Tree {
4942 scroll_offset: cur_scroll,
4943 selected_index: cur_sel,
4944 expanded_keys: expanded,
4945 },
4946 );
4947 next
4948 };
4949 self.rerender_widget_panel(panel_id);
4950 if self
4951 .plugin_manager
4952 .read()
4953 .unwrap()
4954 .has_hook_handlers("widget_event")
4955 {
4956 self.plugin_manager.read().unwrap().run_hook(
4957 "widget_event",
4958 fresh_core::hooks::HookArgs::WidgetEvent {
4959 panel_id,
4960 widget_key: widget_key.to_string(),
4961 event_type: "expand".into(),
4962 payload: serde_json::json!({
4963 "key": item_key,
4964 "expanded": now_expanded,
4965 }),
4966 },
4967 );
4968 }
4969 }
4970
4971 fn fire_tree_toggle_if_checkable(&mut self, panel_id: u64, focus_key: &str) -> bool {
4985 let panel = match self.widget_registry.get(panel_id) {
4986 Some(p) => p,
4987 None => return false,
4988 };
4989 let widget = crate::widgets::find_widget_by_key(&panel.spec, focus_key);
4990 let (spec_sel, nodes, item_keys, checkable) = match widget {
4991 Some(fresh_core::api::WidgetSpec::Tree {
4992 selected_index,
4993 nodes,
4994 item_keys,
4995 checkable,
4996 ..
4997 }) => (*selected_index, nodes, item_keys.clone(), *checkable),
4998 _ => return false,
4999 };
5000 if !checkable {
5001 return false;
5002 }
5003 let sel = match panel.instance_states.get(focus_key) {
5004 Some(crate::widgets::WidgetInstanceState::Tree { selected_index, .. }) => {
5005 *selected_index
5006 }
5007 _ => spec_sel,
5008 };
5009 if sel < 0 {
5010 return false;
5011 }
5012 let cur_checked = match nodes.get(sel as usize).and_then(|n| n.checked) {
5013 Some(b) => b,
5014 None => return false, };
5016 let new_checked = !cur_checked;
5017 let item_key = item_keys.get(sel as usize).cloned().unwrap_or_default();
5018 if self
5019 .plugin_manager
5020 .read()
5021 .unwrap()
5022 .has_hook_handlers("widget_event")
5023 {
5024 self.plugin_manager.read().unwrap().run_hook(
5025 "widget_event",
5026 fresh_core::hooks::HookArgs::WidgetEvent {
5027 panel_id,
5028 widget_key: focus_key.to_string(),
5029 event_type: "toggle".into(),
5030 payload: serde_json::json!({
5031 "index": sel,
5032 "key": item_key,
5033 "checked": new_checked,
5034 }),
5035 },
5036 );
5037 }
5038 true
5039 }
5040
5041 fn fire_tree_activate(&mut self, panel_id: u64, focus_key: &str) {
5042 let panel = match self.widget_registry.get(panel_id) {
5043 Some(p) => p,
5044 None => return,
5045 };
5046 let widget = crate::widgets::find_widget_by_key(&panel.spec, focus_key);
5047 let (spec_sel, item_keys) = match widget {
5048 Some(fresh_core::api::WidgetSpec::Tree {
5049 selected_index,
5050 item_keys,
5051 ..
5052 }) => (*selected_index, item_keys.clone()),
5053 _ => return,
5054 };
5055 let sel = match panel.instance_states.get(focus_key) {
5056 Some(crate::widgets::WidgetInstanceState::Tree { selected_index, .. }) => {
5057 *selected_index
5058 }
5059 _ => spec_sel,
5060 };
5061 if sel < 0 {
5062 return;
5063 }
5064 let item_key = item_keys.get(sel as usize).cloned().unwrap_or_default();
5065 if self
5066 .plugin_manager
5067 .read()
5068 .unwrap()
5069 .has_hook_handlers("widget_event")
5070 {
5071 self.plugin_manager.read().unwrap().run_hook(
5072 "widget_event",
5073 fresh_core::hooks::HookArgs::WidgetEvent {
5074 panel_id,
5075 widget_key: focus_key.to_string(),
5076 event_type: "activate".into(),
5077 payload: serde_json::json!({
5078 "index": sel,
5079 "key": item_key,
5080 }),
5081 },
5082 );
5083 }
5084 }
5085
5086 pub(super) fn focused_text_widget_panel_for_buffer(
5099 &self,
5100 buffer_id: crate::model::event::BufferId,
5101 ) -> Option<u64> {
5102 for panel_id in self.widget_registry.panels_for_buffer(buffer_id) {
5103 let panel = self.widget_registry.get(panel_id)?;
5104 if panel.focus_key.is_empty() {
5105 continue;
5106 }
5107 let widget = crate::widgets::find_widget_by_key(&panel.spec, &panel.focus_key);
5108 if matches!(widget, Some(fresh_core::api::WidgetSpec::Text { .. })) {
5109 return Some(panel_id);
5110 }
5111 }
5112 None
5113 }
5114
5115 pub(super) fn focused_widget_selected_text(&self, panel_id: u64) -> Option<String> {
5120 let panel = self.widget_registry.get(panel_id)?;
5121 if panel.focus_key.is_empty() {
5122 return None;
5123 }
5124 match panel.instance_states.get(&panel.focus_key) {
5125 Some(crate::widgets::WidgetInstanceState::Text { editor, .. }) => {
5126 editor.selected_text()
5127 }
5128 _ => None,
5129 }
5130 }
5131
5132 pub(super) fn handle_widget_select_all(&mut self, panel_id: u64) -> bool {
5137 self.with_focused_text_editor(panel_id, |editor| editor.select_all())
5141 }
5142
5143 pub(super) fn handle_widget_copy(&mut self, panel_id: u64) -> bool {
5148 if self.widget_registry.get(panel_id).is_none() {
5149 return false;
5150 }
5151 if let Some(text) = self.focused_widget_selected_text(panel_id) {
5152 self.clipboard.copy(text);
5153 }
5154 true
5155 }
5156
5157 pub(super) fn handle_widget_cut(&mut self, panel_id: u64) -> bool {
5160 if self.widget_registry.get(panel_id).is_none() {
5161 return false;
5162 }
5163 if let Some(text) = self.focused_widget_selected_text(panel_id) {
5164 self.clipboard.copy(text);
5165 self.with_focused_text_editor(panel_id, |editor| {
5166 editor.delete_selection();
5167 });
5168 }
5169 true
5170 }
5171
5172 pub(super) fn handle_widget_insert_str(&mut self, panel_id: u64, text: &str) -> bool {
5178 if self.widget_registry.get(panel_id).is_none() {
5179 return false;
5180 }
5181 let owned = text.to_string();
5182 self.with_focused_text_editor(panel_id, move |editor| {
5183 editor.insert_str(&owned);
5184 });
5185 true
5186 }
5187
5188 fn ensure_focused_text_seeded(&mut self, panel_id: u64, focus_key: &str) -> bool {
5195 let panel = match self.widget_registry.get_mut(panel_id) {
5196 Some(p) => p,
5197 None => return false,
5198 };
5199 if matches!(
5200 panel.instance_states.get(focus_key),
5201 Some(crate::widgets::WidgetInstanceState::Text { .. })
5202 ) {
5203 return true;
5204 }
5205 let widget = crate::widgets::find_widget_by_key(&panel.spec, focus_key);
5206 let (value, cursor_byte, multiline) = match widget {
5207 Some(fresh_core::api::WidgetSpec::Text {
5208 value,
5209 cursor_byte,
5210 rows,
5211 ..
5212 }) => (value.clone(), *cursor_byte, *rows > 1),
5213 _ => return false,
5214 };
5215 let mut editor = if multiline {
5216 crate::primitives::text_edit::TextEdit::with_text(&value)
5217 } else {
5218 crate::primitives::text_edit::TextEdit::single_line_with_text(&value)
5219 };
5220 let seed = if cursor_byte < 0 {
5221 value.len()
5222 } else {
5223 (cursor_byte as usize).min(value.len())
5224 };
5225 editor.set_cursor_from_flat(seed);
5226 panel.instance_states.insert(
5227 focus_key.to_string(),
5228 crate::widgets::WidgetInstanceState::Text {
5229 editor,
5230 scroll: 0,
5231 completions: Vec::new(),
5232 completion_selected_index: 0,
5233 completion_scroll_offset: 0,
5234 },
5235 );
5236 true
5237 }
5238
5239 pub(super) fn with_focused_text_editor<F>(&mut self, panel_id: u64, op: F) -> bool
5246 where
5247 F: FnOnce(&mut crate::primitives::text_edit::TextEdit),
5248 {
5249 let focus_key = match self.widget_registry.get(panel_id) {
5250 Some(p) if !p.focus_key.is_empty() => p.focus_key.clone(),
5251 _ => return false,
5252 };
5253 if !self.ensure_focused_text_seeded(panel_id, &focus_key) {
5254 return false;
5255 }
5256 let (before_value, before_cursor) = {
5257 let panel = self.widget_registry.get(panel_id).unwrap();
5258 match panel.instance_states.get(&focus_key) {
5259 Some(crate::widgets::WidgetInstanceState::Text { editor, .. }) => {
5260 (editor.value(), editor.flat_cursor_byte())
5261 }
5262 _ => return false,
5263 }
5264 };
5265 {
5266 let panel = self.widget_registry.get_mut(panel_id).unwrap();
5267 match panel.instance_states.get_mut(&focus_key) {
5268 Some(crate::widgets::WidgetInstanceState::Text { editor, .. }) => op(editor),
5269 _ => return false,
5270 }
5271 }
5272 let (after_value, after_cursor) = {
5273 let panel = self.widget_registry.get(panel_id).unwrap();
5274 match panel.instance_states.get(&focus_key) {
5275 Some(crate::widgets::WidgetInstanceState::Text { editor, .. }) => {
5276 (editor.value(), editor.flat_cursor_byte())
5277 }
5278 _ => return false,
5279 }
5280 };
5281 if after_value == before_value && after_cursor == before_cursor {
5282 return false;
5283 }
5284 self.rerender_widget_panel(panel_id);
5285 if self
5286 .plugin_manager
5287 .read()
5288 .unwrap()
5289 .has_hook_handlers("widget_event")
5290 {
5291 self.plugin_manager.read().unwrap().run_hook(
5292 "widget_event",
5293 fresh_core::hooks::HookArgs::WidgetEvent {
5294 panel_id,
5295 widget_key: focus_key.clone(),
5296 event_type: "change".into(),
5297 payload: serde_json::json!({
5298 "value": after_value,
5299 "cursorByte": after_cursor as i64,
5300 }),
5301 },
5302 );
5303 }
5304 true
5305 }
5306
5307 fn handle_widget_text_key(&mut self, panel_id: u64, key: &str) {
5313 self.with_focused_text_editor(panel_id, |editor| match key {
5314 "Backspace" => editor.backspace(),
5315 "Delete" => editor.delete(),
5316 "Left" => editor.move_left(),
5317 "Right" => editor.move_right(),
5318 "Up" => editor.move_up(),
5319 "Down" => editor.move_down(),
5320 "Home" => editor.move_home(),
5321 "End" => editor.move_end(),
5322 "Enter" => editor.insert_char('\n'),
5323 _ => { }
5324 });
5325 }
5326
5327 fn handle_widget_text_char(&mut self, panel_id: u64, text: &str) {
5334 if text.is_empty() {
5335 return;
5336 }
5337 let text = text.to_string();
5338 self.with_focused_text_editor(panel_id, move |editor| {
5339 editor.insert_str(&text);
5340 });
5341 }
5342
5343 fn handle_unmount_widget_panel(&mut self, panel_id: u64) {
5344 match self.widget_registry.unmount(panel_id) {
5345 Some(buffer_id) => {
5346 tracing::debug!(
5347 "Unmounted widget panel {} (was rendering into {:?})",
5348 panel_id,
5349 buffer_id
5350 );
5351 }
5356 None => {
5357 tracing::debug!("UnmountWidgetPanel for unknown panel {} ignored", panel_id);
5358 }
5359 }
5360 }
5361
5362 fn handle_mount_floating_widget(
5363 &mut self,
5364 panel_id: u64,
5365 spec: fresh_core::api::WidgetSpec,
5366 width_pct: u8,
5367 height_pct: u8,
5368 ) {
5369 let width_pct = width_pct.clamp(1, 100);
5370 let height_pct = height_pct.clamp(1, 100);
5371 if let Some(existing) = self.floating_widget_panel.take() {
5372 if existing.panel_id != panel_id {
5373 let _ = self.widget_registry.unmount(existing.panel_id);
5374 }
5375 }
5376 self.floating_widget_panel = Some(FloatingWidgetState {
5377 panel_id,
5378 width_pct,
5379 height_pct,
5380 entries: Vec::new(),
5381 focus_cursor: None,
5382 embeds: Vec::new(),
5383 overlays: Vec::new(),
5384 last_inner_rect: None,
5385 });
5386 let prev = std::collections::HashMap::new();
5387 let prev_focus = String::new();
5388 let panel_width = self.floating_panel_inner_width();
5389 let out = crate::widgets::render_spec(&spec, &prev, &prev_focus, panel_width);
5390 let focus_cursor = out.focus_cursor;
5391 let entries = out.entries;
5392 let embeds = out.embeds;
5393 let overlays = out.overlays;
5394 self.widget_registry.mount(
5395 panel_id,
5396 FLOATING_PANEL_BUFFER_ID,
5397 spec,
5398 out.hits,
5399 out.instance_states,
5400 out.focus_key,
5401 out.tabbable,
5402 );
5403 if let Some(fwp) = self.floating_widget_panel.as_mut() {
5404 fwp.entries = entries;
5405 fwp.focus_cursor = focus_cursor;
5406 fwp.embeds = embeds;
5407 fwp.overlays = overlays;
5408 }
5409 tracing::debug!(
5410 "Mounted floating widget panel {} ({}%x{}%)",
5411 panel_id,
5412 width_pct,
5413 height_pct
5414 );
5415 }
5416
5417 fn handle_update_floating_widget(&mut self, panel_id: u64, spec: fresh_core::api::WidgetSpec) {
5418 match self.floating_widget_panel.as_ref() {
5419 Some(fwp) if fwp.panel_id == panel_id => {}
5420 _ => {
5421 tracing::debug!(
5422 "UpdateFloatingWidget for unknown / mismatched panel {} ignored",
5423 panel_id
5424 );
5425 return;
5426 }
5427 }
5428 let prev = self
5429 .widget_registry
5430 .instance_states(panel_id)
5431 .cloned()
5432 .unwrap_or_default();
5433 let prev_focus = self
5434 .widget_registry
5435 .focus_key(panel_id)
5436 .map(|s| s.to_string())
5437 .unwrap_or_default();
5438 let panel_width = self.floating_panel_inner_width();
5439 let out = crate::widgets::render_spec(&spec, &prev, &prev_focus, panel_width);
5440 let focus_cursor = out.focus_cursor;
5441 let entries = out.entries;
5442 let embeds = out.embeds;
5443 let overlays = out.overlays;
5444 if self
5445 .widget_registry
5446 .update(
5447 panel_id,
5448 spec,
5449 out.hits,
5450 out.instance_states,
5451 out.focus_key,
5452 out.tabbable,
5453 )
5454 .is_err()
5455 {
5456 tracing::debug!(
5457 "UpdateFloatingWidget for unknown panel {} ignored (not in registry)",
5458 panel_id
5459 );
5460 return;
5461 }
5462 if let Some(fwp) = self.floating_widget_panel.as_mut() {
5463 fwp.entries = entries;
5464 fwp.focus_cursor = focus_cursor;
5465 fwp.embeds = embeds;
5466 fwp.overlays = overlays;
5467 }
5468 }
5469
5470 fn handle_unmount_floating_widget(&mut self, panel_id: u64) {
5471 match self.floating_widget_panel.as_ref() {
5472 Some(fwp) if fwp.panel_id == panel_id => {}
5473 _ => {
5474 tracing::debug!(
5475 "UnmountFloatingWidget for unknown / mismatched panel {} ignored",
5476 panel_id
5477 );
5478 return;
5479 }
5480 }
5481 self.floating_widget_panel = None;
5482 let _ = self.widget_registry.unmount(panel_id);
5483 self.active_window_mut().resize_visible_terminals();
5496 tracing::debug!("Unmounted floating widget panel {}", panel_id);
5497 }
5498
5499 pub(super) fn floating_panel_inner_width(&self) -> u32 {
5505 let term_w = self.terminal_width.max(1) as u32;
5506 let pct = self
5507 .floating_widget_panel
5508 .as_ref()
5509 .map(|f| f.width_pct.clamp(1, 100) as u32)
5510 .unwrap_or(80);
5511 let w = (term_w * pct) / 100;
5512 w.saturating_sub(2).max(10)
5513 }
5514
5515 fn handle_get_text_properties_at_cursor(&self, buffer_id: BufferId) {
5516 if let Some(state) = self
5517 .windows
5518 .get(&self.active_window)
5519 .map(|w| &w.buffers)
5520 .expect("active window present")
5521 .get(&buffer_id)
5522 {
5523 let cursor_pos = self
5524 .windows
5525 .get(&self.active_window)
5526 .and_then(|w| w.buffers.splits())
5527 .map(|(_, vs)| vs)
5528 .expect("active window must have a populated split layout")
5529 .values()
5530 .find_map(|vs| vs.buffer_state(buffer_id))
5531 .map(|bs| bs.cursors.primary().position)
5532 .unwrap_or(0);
5533 let properties = state.text_properties.get_at(cursor_pos);
5534 tracing::debug!(
5535 "Text properties at cursor in {:?}: {} properties found",
5536 buffer_id,
5537 properties.len()
5538 );
5539 }
5541 }
5542
5543 fn handle_set_context(&mut self, name: String, active: bool) {
5544 if active {
5545 self.active_window_mut()
5546 .active_custom_contexts
5547 .insert(name.clone());
5548 tracing::debug!("Set custom context: {}", name);
5549 } else {
5550 self.active_window_mut()
5551 .active_custom_contexts
5552 .remove(&name);
5553 tracing::debug!("Unset custom context: {}", name);
5554 }
5555 }
5556
5557 fn handle_disable_lsp_for_language(&mut self, language: String) {
5558 tracing::info!("Disabling LSP for language: {}", language);
5559 let __active_id = self.active_window;
5560 if let Some(lsp) = self
5561 .windows
5562 .get_mut(&__active_id)
5563 .and_then(|w| w.lsp.as_mut())
5564 {
5565 lsp.shutdown_server(&language);
5566 tracing::info!("Stopped LSP server for {}", language);
5567 }
5568 if let Some(lsp_configs) = self.config_mut().lsp.get_mut(&language) {
5569 for c in lsp_configs.as_mut_slice() {
5570 c.enabled = false;
5571 c.auto_start = false;
5572 }
5573 tracing::info!("Disabled LSP config for {}", language);
5574 }
5575 if let Err(e) = self.save_config() {
5576 tracing::error!("Failed to save config: {}", e);
5577 self.active_window_mut().status_message = Some(format!(
5578 "LSP disabled for {} (config save failed)",
5579 language
5580 ));
5581 } else {
5582 self.active_window_mut().status_message =
5583 Some(format!("LSP disabled for {}", language));
5584 }
5585 self.active_window_mut().warning_domains.lsp.clear();
5586 }
5587
5588 fn handle_restart_lsp_for_language(&mut self, language: String) {
5589 tracing::info!("Plugin restarting LSP for language: {}", language);
5590 let file_path = self
5591 .active_window()
5592 .buffer_metadata
5593 .get(&self.active_buffer())
5594 .and_then(|meta| meta.file_path().cloned());
5595 let __active_id = self.active_window;
5596 let success = if let Some(lsp) = self
5597 .windows
5598 .get_mut(&__active_id)
5599 .and_then(|w| w.lsp.as_mut())
5600 {
5601 let (ok, msg) = lsp.manual_restart(&language, file_path.as_deref());
5602 self.active_window_mut().status_message = Some(msg);
5603 ok
5604 } else {
5605 self.active_window_mut().status_message = Some("No LSP manager available".to_string());
5606 false
5607 };
5608 if success {
5609 self.reopen_buffers_for_language(&language);
5610 }
5611 }
5612
5613 fn handle_set_lsp_root_uri(&mut self, language: String, uri: String) {
5614 tracing::info!("Plugin setting LSP root URI for {}: {}", language, uri);
5615 match uri.parse::<lsp_types::Uri>() {
5616 Ok(parsed_uri) => {
5617 let __active_id = self.active_window;
5618 if let Some(lsp) = self
5619 .windows
5620 .get_mut(&__active_id)
5621 .and_then(|w| w.lsp.as_mut())
5622 {
5623 let restarted = lsp.set_language_root_uri(&language, parsed_uri);
5624 if restarted {
5625 self.active_window_mut().status_message = Some(format!(
5626 "LSP root updated for {} (restarting server)",
5627 language
5628 ));
5629 } else {
5630 self.active_window_mut().status_message =
5631 Some(format!("LSP root set for {}", language));
5632 }
5633 }
5634 }
5635 Err(e) => {
5636 tracing::error!("Invalid LSP root URI '{}': {}", uri, e);
5637 self.active_window_mut().status_message =
5638 Some(format!("Invalid LSP root URI: {}", e));
5639 }
5640 }
5641 }
5642
5643 fn handle_create_scroll_sync_group(
5644 &mut self,
5645 group_id: crate::view::scroll_sync::ScrollSyncGroupId,
5646 left_split: SplitId,
5647 right_split: SplitId,
5648 ) {
5649 let success = self
5650 .active_window_mut()
5651 .scroll_sync_manager
5652 .create_group_with_id(group_id, left_split, right_split);
5653 if success {
5654 tracing::debug!(
5655 "Created scroll sync group {} for splits {:?} and {:?}",
5656 group_id,
5657 left_split,
5658 right_split
5659 );
5660 } else {
5661 tracing::warn!(
5662 "Failed to create scroll sync group {} (ID already exists)",
5663 group_id
5664 );
5665 }
5666 }
5667
5668 fn handle_set_scroll_sync_anchors(
5669 &mut self,
5670 group_id: crate::view::scroll_sync::ScrollSyncGroupId,
5671 anchors: Vec<(usize, usize)>,
5672 ) {
5673 use crate::view::scroll_sync::SyncAnchor;
5674 let anchor_count = anchors.len();
5675 let sync_anchors: Vec<SyncAnchor> = anchors
5676 .into_iter()
5677 .map(|(left_line, right_line)| SyncAnchor {
5678 left_line,
5679 right_line,
5680 })
5681 .collect();
5682 self.active_window_mut()
5683 .scroll_sync_manager
5684 .set_anchors(group_id, sync_anchors);
5685 tracing::debug!(
5686 "Set {} anchors for scroll sync group {}",
5687 anchor_count,
5688 group_id
5689 );
5690 }
5691
5692 fn handle_remove_scroll_sync_group(
5693 &mut self,
5694 group_id: crate::view::scroll_sync::ScrollSyncGroupId,
5695 ) {
5696 if self
5697 .active_window_mut()
5698 .scroll_sync_manager
5699 .remove_group(group_id)
5700 {
5701 tracing::debug!("Removed scroll sync group {}", group_id);
5702 } else {
5703 tracing::warn!("Scroll sync group {} not found", group_id);
5704 }
5705 }
5706
5707 fn handle_create_buffer_group(
5708 &mut self,
5709 name: String,
5710 mode: String,
5711 layout_json: String,
5712 request_id: Option<u64>,
5713 ) {
5714 match self.create_buffer_group(name, mode, layout_json) {
5715 Ok(result) => {
5716 if let Some(req_id) = request_id {
5717 let json = serde_json::to_string(&result).unwrap_or_default();
5718 self.plugin_manager
5719 .read()
5720 .unwrap()
5721 .resolve_callback(fresh_core::api::JsCallbackId::from(req_id), json);
5722 }
5723 }
5724 Err(e) => {
5725 tracing::error!("Failed to create buffer group: {}", e);
5726 }
5727 }
5728 }
5729
5730 fn handle_send_terminal_input(
5731 &mut self,
5732 terminal_id: crate::services::terminal::TerminalId,
5733 data: String,
5734 ) {
5735 if let Some(handle) = self.active_window().terminal_manager.get(terminal_id) {
5736 handle.write(data.as_bytes());
5737 tracing::trace!(
5738 "Plugin sent {} bytes to terminal {:?}",
5739 data.len(),
5740 terminal_id
5741 );
5742 } else {
5743 tracing::warn!(
5744 "Plugin tried to send input to non-existent terminal {:?}",
5745 terminal_id
5746 );
5747 }
5748 }
5749
5750 fn handle_close_terminal(&mut self, terminal_id: crate::services::terminal::TerminalId) {
5751 let buffer_to_close = self
5752 .active_window()
5753 .terminal_buffers
5754 .iter()
5755 .find(|(_, &tid)| tid == terminal_id)
5756 .map(|(&bid, _)| bid);
5757 if let Some(buffer_id) = buffer_to_close {
5758 if let Err(e) = self.close_buffer(buffer_id) {
5759 tracing::warn!("Failed to close terminal buffer: {}", e);
5760 }
5761 tracing::info!("Plugin closed terminal {:?}", terminal_id);
5762 } else {
5763 self.active_window_mut().terminal_manager.close(terminal_id);
5764 tracing::info!("Plugin closed terminal {:?} (no buffer found)", terminal_id);
5765 }
5766 }
5767
5768 fn handle_signal_window(&mut self, id: fresh_core::WindowId, signal: &str) {
5776 let Some(window) = self.windows.get_mut(&id) else {
5777 tracing::warn!("Plugin SignalWindow targeted unknown window {:?}", id);
5778 return;
5779 };
5780 let results = window.process_groups.signal_all(signal);
5781 for (entry, result) in results {
5782 match result {
5783 Ok(true) => tracing::info!(
5784 "SignalWindow {:?}: {} → pid {} ({})",
5785 id,
5786 signal,
5787 entry.leader_pid,
5788 entry.label
5789 ),
5790 Ok(false) => tracing::debug!(
5791 "SignalWindow {:?}: pid {} ({}) already exited",
5792 id,
5793 entry.leader_pid,
5794 entry.label
5795 ),
5796 Err(e) => tracing::warn!(
5797 "SignalWindow {:?}: pid {} ({}): {}",
5798 id,
5799 entry.leader_pid,
5800 entry.label,
5801 e
5802 ),
5803 }
5804 }
5805 }
5806}
5807
5808#[cfg(test)]
5809mod tests {
5810 use tokio::io::{AsyncReadExt, BufReader};
5823 use tokio::process::Command as TokioCommand;
5824 use tokio::time::{timeout, Duration};
5825
5826 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
5837 async fn kill_via_oneshot_terminates_long_running_child() {
5838 let mut cmd = TokioCommand::new("sleep");
5839 cmd.args(["30"]);
5840 cmd.stdout(std::process::Stdio::piped());
5841 cmd.stderr(std::process::Stdio::piped());
5842
5843 let mut child = cmd.spawn().expect("spawn sh -c sleep 30");
5844 let pid = child.id().expect("child has a pid");
5845
5846 let (kill_tx, mut kill_rx) = tokio::sync::oneshot::channel::<()>();
5847 let stdout_pipe = child.stdout.take();
5848 let stderr_pipe = child.stderr.take();
5849
5850 let stdout_fut = async {
5851 let mut buf = String::new();
5852 if let Some(s) = stdout_pipe {
5853 #[allow(clippy::let_underscore_must_use)]
5854 let _ = BufReader::new(s).read_to_string(&mut buf).await;
5855 }
5856 buf
5857 };
5858 let stderr_fut = async {
5859 let mut buf = String::new();
5860 if let Some(s) = stderr_pipe {
5861 #[allow(clippy::let_underscore_must_use)]
5862 let _ = BufReader::new(s).read_to_string(&mut buf).await;
5863 }
5864 buf
5865 };
5866 let wait_fut = async {
5867 tokio::select! {
5868 status = child.wait() => {
5869 status.map(|s| s.code().unwrap_or(-1)).unwrap_or(-1)
5870 }
5871 _ = &mut kill_rx => {
5872 #[allow(clippy::let_underscore_must_use)]
5873 let _ = child.start_kill();
5874 child
5875 .wait()
5876 .await
5877 .map(|s| s.code().unwrap_or(-1))
5878 .unwrap_or(-1)
5879 }
5880 }
5881 };
5882
5883 tokio::time::sleep(Duration::from_millis(50)).await;
5888 kill_tx.send(()).expect("kill channel send");
5889
5890 let result = timeout(Duration::from_secs(5), async {
5891 tokio::join!(stdout_fut, stderr_fut, wait_fut)
5892 })
5893 .await;
5894
5895 let (_stdout, _stderr, exit_code) = result.expect(
5896 "kill path must resolve within 5s — if this times out the \
5897 select! arm order or kill-then-wait logic is broken",
5898 );
5899 assert_ne!(
5911 exit_code, 0,
5912 "killed child must exit non-success (got 0 — did the \
5913 kill arm fire too late, or did sleep somehow complete?)"
5914 );
5915
5916 #[cfg(unix)]
5925 {
5926 let still_alive = std::process::Command::new("kill")
5927 .args(["-0", &pid.to_string()])
5928 .status()
5929 .map(|s| s.success())
5930 .unwrap_or(false);
5931 assert!(
5932 !still_alive,
5933 "process {pid} must be reaped after wait() — a still-\
5934 alive check means the kill path leaked the child"
5935 );
5936 }
5937 #[cfg(not(unix))]
5938 {
5939 let _ = pid;
5942 }
5943 }
5944}
5945
5946impl Window {
5947 #[cfg(feature = "plugins")]
5962 pub(crate) fn populate_plugin_state_snapshot(
5963 &mut self,
5964 snapshot: &mut fresh_core::api::EditorStateSnapshot,
5965 ) {
5966 use fresh_core::api::{BufferInfo, CursorInfo, ViewportInfo};
5967
5968 let current_gen = self.resources.grammar_registry.catalog_gen();
5974 if snapshot.last_grammar_gen != current_gen {
5975 snapshot.available_grammars = self
5976 .resources
5977 .grammar_registry
5978 .available_grammar_info()
5979 .into_iter()
5980 .map(|g| fresh_core::api::GrammarInfoSnapshot {
5981 name: g.name,
5982 source: g.source.to_string(),
5983 file_extensions: g.file_extensions,
5984 short_name: g.short_name,
5985 })
5986 .collect();
5987 snapshot.last_grammar_gen = current_gen;
5988 }
5989
5990 snapshot.active_buffer_id = self.active_buffer();
5991
5992 let (mgr_ref, vs_ref) = self
5993 .buffers
5994 .splits()
5995 .expect("active window must have a populated split layout");
5996 let active_split = mgr_ref.active_split();
5997 snapshot.active_split_id = active_split.0 .0;
5998
5999 snapshot.buffers.clear();
6001 snapshot.buffer_saved_diffs.clear();
6002 snapshot.buffer_cursor_positions.clear();
6003 snapshot.buffer_text_properties.clear();
6004
6005 let active_vs_opt = vs_ref.get(&active_split);
6006 for (buffer_id, state) in &self.buffers {
6007 let is_virtual = self
6008 .buffer_metadata
6009 .get(buffer_id)
6010 .map(|m| m.is_virtual())
6011 .unwrap_or(false);
6012 let view_mode = active_vs_opt
6017 .and_then(|vs| vs.buffer_state(*buffer_id))
6018 .map(|bs| match bs.view_mode {
6019 crate::state::ViewMode::Source => "source",
6020 crate::state::ViewMode::PageView => "compose",
6021 })
6022 .unwrap_or("source");
6023 let compose_width = active_vs_opt
6024 .and_then(|vs| vs.buffer_state(*buffer_id))
6025 .and_then(|bs| bs.compose_width);
6026 let is_composing_in_any_split = vs_ref.values().any(|vs| {
6027 vs.buffer_state(*buffer_id)
6028 .map(|bs| matches!(bs.view_mode, crate::state::ViewMode::PageView))
6029 .unwrap_or(false)
6030 });
6031 let is_preview = self
6032 .buffer_metadata
6033 .get(buffer_id)
6034 .map(|m| m.is_preview)
6035 .unwrap_or(false);
6036 let splits: Vec<fresh_core::SplitId> = mgr_ref
6042 .splits_for_buffer(*buffer_id)
6043 .into_iter()
6044 .map(|leaf_id| leaf_id.0)
6045 .collect();
6046 let buffer_info = BufferInfo {
6047 id: *buffer_id,
6048 path: state.buffer.file_path().map(|p| p.to_path_buf()),
6049 modified: state.buffer.is_modified(),
6050 length: state.buffer.len(),
6051 is_virtual,
6052 view_mode: view_mode.to_string(),
6053 is_composing_in_any_split,
6054 compose_width,
6055 language: state.language.clone(),
6056 is_preview,
6057 splits,
6058 };
6059 snapshot.buffers.insert(*buffer_id, buffer_info);
6060
6061 let diff = {
6062 let diff = state.buffer.diff_since_saved();
6063 BufferSavedDiff {
6064 equal: diff.equal,
6065 byte_ranges: diff.byte_ranges.clone(),
6066 }
6067 };
6068 snapshot.buffer_saved_diffs.insert(*buffer_id, diff);
6069
6070 let is_hidden = self
6079 .buffer_metadata
6080 .get(buffer_id)
6081 .is_some_and(|m| m.hidden_from_tabs);
6082 let source_split = vs_ref.iter().find(|(split_id, vs)| {
6083 vs.keyed_states.contains_key(buffer_id)
6084 && !(is_hidden && self.grouped_subtrees.contains_key(split_id))
6085 });
6086 let cursor_pos = source_split
6087 .and_then(|(_, vs)| vs.buffer_state(*buffer_id))
6088 .map(|bs| bs.cursors.primary().position)
6089 .unwrap_or(0);
6090 tracing::trace!(
6091 "snapshot: buffer {:?} cursor_pos={} (from split {:?})",
6092 buffer_id,
6093 cursor_pos,
6094 source_split.map(|(id, _)| *id),
6095 );
6096 snapshot
6097 .buffer_cursor_positions
6098 .insert(*buffer_id, cursor_pos);
6099
6100 if !state.text_properties.is_empty() {
6102 snapshot
6103 .buffer_text_properties
6104 .insert(*buffer_id, state.text_properties.all().to_vec());
6105 }
6106 }
6107
6108 let active_buf_id = snapshot.active_buffer_id;
6119 let active_split_id = self.effective_active_pair().0;
6120 self.buffers
6121 .with_all_mut(|buffers_mut, mgr, vs_map| {
6122 let _ = mgr; if let Some(active_vs) = vs_map.get(&active_split_id) {
6124 let active_cursors = &active_vs.cursors;
6126 let primary = active_cursors.primary();
6127 let primary_position = primary.position;
6128 let primary_selection = primary.selection_range();
6129
6130 snapshot.primary_cursor = Some(CursorInfo {
6131 position: primary_position,
6132 selection: primary_selection.clone(),
6133 });
6134
6135 snapshot.all_cursors = active_cursors
6136 .iter()
6137 .map(|(_, cursor)| CursorInfo {
6138 position: cursor.position,
6139 selection: cursor.selection_range(),
6140 })
6141 .collect();
6142
6143 if let Some(range) = primary_selection {
6145 if let Some(active_state) = buffers_mut.get_mut(&active_buf_id) {
6146 snapshot.selected_text =
6147 Some(active_state.get_text_range(range.start, range.end));
6148 }
6149 }
6150
6151 let top_line = buffers_mut.get(&active_buf_id).and_then(|state| {
6153 if state.buffer.line_count().is_some() {
6154 Some(state.buffer.get_line_number(active_vs.viewport.top_byte))
6155 } else {
6156 None
6157 }
6158 });
6159 snapshot.viewport = Some(ViewportInfo {
6160 top_byte: active_vs.viewport.top_byte,
6161 top_line,
6162 left_column: active_vs.viewport.left_column,
6163 width: active_vs.viewport.width,
6164 height: active_vs.viewport.height,
6165 });
6166 } else {
6167 snapshot.primary_cursor = None;
6168 snapshot.all_cursors.clear();
6169 snapshot.viewport = None;
6170 snapshot.selected_text = None;
6171 }
6172
6173 snapshot.splits.clear();
6175 for (leaf_id, vs) in vs_map.iter() {
6176 let buf_id = vs.active_buffer;
6177 let top_line = buffers_mut.get(&buf_id).and_then(|state| {
6178 if state.buffer.line_count().is_some() {
6179 Some(state.buffer.get_line_number(vs.viewport.top_byte))
6180 } else {
6181 None
6182 }
6183 });
6184 snapshot.splits.push(fresh_core::api::SplitSnapshot {
6185 split_id: leaf_id.0 .0,
6186 buffer_id: buf_id,
6187 viewport: ViewportInfo {
6188 top_byte: vs.viewport.top_byte,
6189 top_line,
6190 left_column: vs.viewport.left_column,
6191 width: vs.viewport.width,
6192 height: vs.viewport.height,
6193 },
6194 });
6195 }
6196 })
6197 .expect("active window must have a populated split layout");
6198
6199 snapshot.active_session_plugin_states = self.plugin_state.clone();
6205 snapshot.diagnostics = Arc::clone(&self.stored_diagnostics);
6210 snapshot.folding_ranges = Arc::clone(&self.stored_folding_ranges);
6211
6212 snapshot.editor_mode = self.editor_mode.clone();
6214
6215 let active_split_id_u64 = active_split_id.0 .0;
6220 let split_changed = snapshot.plugin_view_states_split != active_split_id_u64;
6221 if split_changed {
6222 snapshot.plugin_view_states.clear();
6223 snapshot.plugin_view_states_split = active_split_id_u64;
6224 }
6225
6226 {
6228 let open_bids: Vec<_> = snapshot.buffers.keys().copied().collect();
6229 snapshot
6230 .plugin_view_states
6231 .retain(|bid, _| open_bids.contains(bid));
6232 }
6233
6234 if let Some(vs_map) = self.buffers.split_view_states() {
6236 if let Some(active_vs) = vs_map.get(&active_split_id) {
6237 for (buffer_id, buf_state) in &active_vs.keyed_states {
6238 if !buf_state.plugin_state.is_empty() {
6239 let entry = snapshot.plugin_view_states.entry(*buffer_id).or_default();
6240 for (key, value) in &buf_state.plugin_state {
6241 entry.entry(key.clone()).or_insert_with(|| value.clone());
6242 }
6243 }
6244 }
6245 }
6246 }
6247 }
6248}