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().to_path_buf();
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 snapshot.workspace_trust_level =
146 self.authority.workspace_trust.level().as_str().to_string();
147 snapshot.env_active = self.authority.env_provider.is_active();
148
149 let mut session_infos: Vec<fresh_core::api::WindowInfo> = self
155 .windows
156 .values()
157 .map(|s| {
158 let slot = s.plugin_state.get("orchestrator");
159 let project_path = slot
160 .and_then(|m| m.get("project_path"))
161 .and_then(|v| v.as_str())
162 .map(std::path::PathBuf::from);
163 let shared_worktree = slot
164 .and_then(|m| m.get("shared_worktree"))
165 .and_then(|v| v.as_bool())
166 .unwrap_or(false);
167 fresh_core::api::WindowInfo {
168 id: s.id,
169 label: s.label.clone(),
170 root: s.root.clone(),
171 project_path,
172 shared_worktree,
173 }
174 })
175 .collect();
176 session_infos.sort_by_key(|s| s.id.0);
177 snapshot.windows = session_infos;
178 snapshot.active_window_id = self.active_window;
179
180 if !Arc::ptr_eq(&self.config, &self.config_snapshot_anchor) {
189 let json = serde_json::to_value(&*self.config).unwrap_or(serde_json::Value::Null);
190 self.config_cached_json = Arc::new(json);
191 self.config_snapshot_anchor = Arc::clone(&self.config);
192 }
193 snapshot.config = Arc::clone(&self.config_cached_json);
194
195 snapshot.user_config = Arc::clone(&self.user_config_raw);
198
199 for (plugin_name, state_map) in &self.plugin_global_state {
202 let entry = snapshot
203 .plugin_global_states
204 .entry(plugin_name.clone())
205 .or_default();
206 for (key, value) in state_map {
207 entry.entry(key.clone()).or_insert_with(|| value.clone());
208 }
209 }
210 }
211
212 pub fn handle_plugin_command(&mut self, command: PluginCommand) -> AnyhowResult<()> {
214 match command {
215 PluginCommand::InsertText {
217 buffer_id,
218 position,
219 text,
220 } => {
221 self.handle_insert_text(buffer_id, position, text);
222 }
223 PluginCommand::DeleteRange { buffer_id, range } => {
224 self.handle_delete_range(buffer_id, range);
225 }
226 PluginCommand::InsertAtCursor { text } => {
227 self.handle_insert_at_cursor(text);
228 }
229 PluginCommand::DeleteSelection => {
230 self.handle_delete_selection();
231 }
232
233 PluginCommand::AddOverlay {
235 buffer_id,
236 namespace,
237 range,
238 options,
239 } => {
240 self.handle_add_overlay(buffer_id, namespace, range, options);
241 }
242 PluginCommand::RemoveOverlay { buffer_id, handle } => {
243 self.handle_remove_overlay(buffer_id, handle);
244 }
245 PluginCommand::ClearAllOverlays { buffer_id } => {
246 self.handle_clear_all_overlays(buffer_id);
247 }
248 PluginCommand::ClearNamespace {
249 buffer_id,
250 namespace,
251 } => {
252 self.handle_clear_namespace(buffer_id, namespace);
253 }
254 PluginCommand::ClearOverlaysInRange {
255 buffer_id,
256 start,
257 end,
258 } => {
259 self.handle_clear_overlays_in_range(buffer_id, start, end);
260 }
261
262 PluginCommand::AddVirtualText {
264 buffer_id,
265 virtual_text_id,
266 position,
267 text,
268 color,
269 use_bg,
270 before,
271 } => {
272 self.handle_add_virtual_text(
273 buffer_id,
274 virtual_text_id,
275 position,
276 text,
277 color,
278 use_bg,
279 before,
280 );
281 }
282 PluginCommand::AddVirtualTextStyled {
283 buffer_id,
284 virtual_text_id,
285 position,
286 text,
287 fg,
288 bg,
289 bold,
290 italic,
291 before,
292 } => {
293 self.handle_add_virtual_text_styled(
294 buffer_id,
295 virtual_text_id,
296 position,
297 text,
298 fg,
299 bg,
300 bold,
301 italic,
302 before,
303 );
304 }
305 PluginCommand::RemoveVirtualText {
306 buffer_id,
307 virtual_text_id,
308 } => {
309 self.handle_remove_virtual_text(buffer_id, virtual_text_id);
310 }
311 PluginCommand::RemoveVirtualTextsByPrefix { buffer_id, prefix } => {
312 self.handle_remove_virtual_texts_by_prefix(buffer_id, prefix);
313 }
314 PluginCommand::ClearVirtualTexts { buffer_id } => {
315 self.handle_clear_virtual_texts(buffer_id);
316 }
317 PluginCommand::AddVirtualLine {
318 buffer_id,
319 position,
320 text,
321 fg_color,
322 bg_color,
323 above,
324 namespace,
325 priority,
326 gutter_glyph,
327 gutter_color,
328 text_overlays,
329 } => {
330 self.handle_add_virtual_line(
331 buffer_id,
332 position,
333 text,
334 fg_color,
335 bg_color,
336 above,
337 namespace,
338 priority,
339 gutter_glyph,
340 gutter_color,
341 text_overlays,
342 );
343 }
344 PluginCommand::ClearVirtualTextNamespace {
345 buffer_id,
346 namespace,
347 } => {
348 self.handle_clear_virtual_text_namespace(buffer_id, namespace);
349 }
350
351 PluginCommand::AddConceal {
353 buffer_id,
354 namespace,
355 start,
356 end,
357 replacement,
358 } => {
359 self.handle_add_conceal(buffer_id, namespace, start, end, replacement);
360 }
361 PluginCommand::ClearConcealNamespace {
362 buffer_id,
363 namespace,
364 } => {
365 self.handle_clear_conceal_namespace(buffer_id, namespace);
366 }
367 PluginCommand::ClearConcealsInRange {
368 buffer_id,
369 start,
370 end,
371 } => {
372 self.handle_clear_conceals_in_range(buffer_id, start, end);
373 }
374
375 PluginCommand::AddFold {
376 buffer_id,
377 start,
378 end,
379 placeholder,
380 } => {
381 self.handle_add_fold(buffer_id, start, end, placeholder);
382 }
383 PluginCommand::ClearFolds { buffer_id } => {
384 self.handle_clear_folds(buffer_id);
385 }
386 PluginCommand::SetFoldingRanges { buffer_id, ranges } => {
387 self.handle_set_folding_ranges(buffer_id, ranges);
388 }
389
390 PluginCommand::AddSoftBreak {
392 buffer_id,
393 namespace,
394 position,
395 indent,
396 } => {
397 self.handle_add_soft_break(buffer_id, namespace, position, indent);
398 }
399 PluginCommand::ClearSoftBreakNamespace {
400 buffer_id,
401 namespace,
402 } => {
403 self.handle_clear_soft_break_namespace(buffer_id, namespace);
404 }
405 PluginCommand::ClearSoftBreaksInRange {
406 buffer_id,
407 start,
408 end,
409 } => {
410 self.handle_clear_soft_breaks_in_range(buffer_id, start, end);
411 }
412
413 PluginCommand::AddMenuItem {
415 menu_label,
416 item,
417 position,
418 } => {
419 self.handle_add_menu_item(menu_label, item, position);
420 }
421 PluginCommand::AddMenu { menu, position } => {
422 self.handle_add_menu(menu, position);
423 }
424 PluginCommand::RemoveMenuItem {
425 menu_label,
426 item_label,
427 } => {
428 self.handle_remove_menu_item(menu_label, item_label);
429 }
430 PluginCommand::RemoveMenu { menu_label } => {
431 self.handle_remove_menu(menu_label);
432 }
433
434 PluginCommand::FocusSplit { split_id } => {
436 self.handle_focus_split(split_id);
437 }
438 PluginCommand::SetSplitBuffer {
439 split_id,
440 buffer_id,
441 } => {
442 self.handle_set_split_buffer(split_id, buffer_id);
443 }
444 PluginCommand::SetSplitScroll { split_id, top_byte } => {
445 self.handle_set_split_scroll(split_id, top_byte);
446 }
447 PluginCommand::RequestHighlights {
448 buffer_id,
449 range,
450 request_id,
451 } => {
452 self.handle_request_highlights(buffer_id, range, request_id);
453 }
454 PluginCommand::CloseSplit { split_id } => {
455 self.handle_close_split(split_id);
456 }
457 PluginCommand::SetSplitRatio { split_id, ratio } => {
458 self.handle_set_split_ratio(split_id, ratio);
459 }
460 PluginCommand::SetSplitLabel { split_id, label } => {
461 self.windows
462 .get_mut(&self.active_window)
463 .and_then(|w| w.split_manager_mut())
464 .expect("active window must have a populated split layout")
465 .set_label(LeafId(split_id), label);
466 }
467 PluginCommand::ClearSplitLabel { split_id } => {
468 self.windows
469 .get_mut(&self.active_window)
470 .and_then(|w| w.split_manager_mut())
471 .expect("active window must have a populated split layout")
472 .clear_label(split_id);
473 }
474 PluginCommand::GetSplitByLabel { label, request_id } => {
475 self.handle_get_split_by_label(label, request_id);
476 }
477 PluginCommand::DistributeSplitsEvenly { split_ids: _ } => {
478 self.handle_distribute_splits_evenly();
479 }
480 PluginCommand::SetBufferCursor {
481 buffer_id,
482 position,
483 } => {
484 self.handle_set_buffer_cursor(buffer_id, position);
485 }
486 PluginCommand::SetBufferShowCursors { buffer_id, show } => {
487 self.handle_set_buffer_show_cursors(buffer_id, show);
488 }
489
490 PluginCommand::SetLayoutHints {
492 buffer_id,
493 split_id,
494 range: _,
495 hints,
496 } => {
497 self.handle_set_layout_hints(buffer_id, split_id, hints);
498 }
499 PluginCommand::SetLineNumbers { buffer_id, enabled } => {
500 self.handle_set_line_numbers(buffer_id, enabled);
501 }
502 PluginCommand::SetViewMode { buffer_id, mode } => {
503 self.handle_set_view_mode(buffer_id, &mode);
504 }
505 PluginCommand::SetLineWrap {
506 buffer_id,
507 split_id,
508 enabled,
509 } => {
510 self.handle_set_line_wrap(buffer_id, split_id, enabled);
511 }
512 PluginCommand::SubmitViewTransform {
513 buffer_id,
514 split_id,
515 payload,
516 } => {
517 self.handle_submit_view_transform(buffer_id, split_id, payload);
518 }
519 PluginCommand::ClearViewTransform {
520 buffer_id: _,
521 split_id,
522 } => {
523 self.handle_clear_view_transform(split_id);
524 }
525 PluginCommand::SetViewState {
526 buffer_id,
527 key,
528 value,
529 } => {
530 self.handle_set_view_state(buffer_id, key, value);
531 }
532 PluginCommand::SetGlobalState {
533 plugin_name,
534 key,
535 value,
536 } => {
537 self.handle_set_global_state(plugin_name, key, value);
538 }
539 PluginCommand::SetWindowState {
540 plugin_name,
541 key,
542 value,
543 } => {
544 self.handle_set_session_state(plugin_name, key, value);
545 }
546 PluginCommand::RefreshLines { buffer_id } => {
547 self.handle_refresh_lines(buffer_id);
548 }
549 PluginCommand::RefreshAllLines => {
550 self.handle_refresh_all_lines();
551 }
552 PluginCommand::HookCompleted { .. } => {
553 }
555 PluginCommand::SetLineIndicator {
556 buffer_id,
557 line,
558 namespace,
559 symbol,
560 color,
561 priority,
562 } => {
563 self.handle_set_line_indicator(buffer_id, line, namespace, symbol, color, priority);
564 }
565 PluginCommand::SetLineIndicators {
566 buffer_id,
567 lines,
568 namespace,
569 symbol,
570 color,
571 priority,
572 } => {
573 self.handle_set_line_indicators(
574 buffer_id, lines, namespace, symbol, color, priority,
575 );
576 }
577 PluginCommand::ClearLineIndicators {
578 buffer_id,
579 namespace,
580 } => {
581 self.handle_clear_line_indicators(buffer_id, namespace);
582 }
583 PluginCommand::SetFileExplorerDecorations {
584 namespace,
585 decorations,
586 } => {
587 self.active_window_mut()
588 .handle_set_file_explorer_decorations(namespace, decorations);
589 }
590 PluginCommand::ClearFileExplorerDecorations { namespace } => {
591 self.active_window_mut()
592 .handle_clear_file_explorer_decorations(&namespace);
593 }
594
595 PluginCommand::SetStatus { message } => {
597 self.handle_set_status(message);
598 }
599 PluginCommand::ApplyTheme { theme_name } => {
600 self.apply_theme(&theme_name);
601 }
602 PluginCommand::OverrideThemeColors { overrides } => {
603 self.handle_override_theme_colors(overrides);
604 }
605 PluginCommand::ReloadConfig => {
606 self.reload_config();
607 }
608 PluginCommand::SetSetting { path, value, .. } => {
609 self.handle_set_setting(path, value);
610 }
611 PluginCommand::AddPluginConfigField {
612 plugin_name,
613 field_name,
614 field_schema,
615 } => {
616 self.handle_add_plugin_config_field(plugin_name, field_name, field_schema);
617 }
618 PluginCommand::ReloadThemes { apply_theme } => {
619 self.reload_themes();
620 if let Some(theme_name) = apply_theme {
621 self.apply_theme(&theme_name);
622 }
623 }
624 PluginCommand::RegisterGrammar {
625 language,
626 grammar_path,
627 extensions,
628 } => {
629 self.handle_register_grammar(language, grammar_path, extensions);
630 }
631 PluginCommand::RegisterLanguageConfig { language, config } => {
632 self.handle_register_language_config(language, config);
633 }
634 PluginCommand::RegisterLspServer { language, config } => {
635 self.handle_register_lsp_server(language, config);
636 }
637 PluginCommand::ReloadGrammars { callback_id } => {
638 self.handle_reload_grammars(callback_id);
639 }
640 PluginCommand::StartPrompt {
641 label,
642 prompt_type,
643 floating_overlay,
644 } => {
645 self.handle_start_prompt(label, prompt_type, floating_overlay);
646 }
647 PluginCommand::StartPromptWithInitial {
648 label,
649 prompt_type,
650 initial_value,
651 floating_overlay,
652 } => {
653 self.handle_start_prompt_with_initial(
654 label,
655 prompt_type,
656 initial_value,
657 floating_overlay,
658 );
659 }
660 PluginCommand::StartPromptAsync {
661 label,
662 initial_value,
663 callback_id,
664 } => {
665 self.handle_start_prompt_async(label, initial_value, callback_id);
666 }
667 PluginCommand::AwaitNextKey { callback_id } => {
668 self.handle_await_next_key(callback_id);
669 }
670 PluginCommand::SetKeyCaptureActive { active } => {
671 self.active_window_mut().key_capture_active = active;
672 if !active {
673 self.active_window_mut().pending_key_capture_buffer.clear();
677 }
678 }
679 PluginCommand::SetPromptSuggestions { suggestions } => {
680 self.handle_set_prompt_suggestions(suggestions);
681 }
682 PluginCommand::SetPromptInputSync { sync } => {
683 if let Some(prompt) = &mut self.active_window_mut().prompt {
684 prompt.sync_input_on_navigate = sync;
685 }
686 }
687 PluginCommand::SetPromptTitle { title } => {
688 if let Some(prompt) = &mut self.active_window_mut().prompt {
689 prompt.title = title;
690 }
691 }
692 PluginCommand::SetPromptFooter { footer } => {
693 if let Some(prompt) = &mut self.active_window_mut().prompt {
694 prompt.footer = footer;
695 }
696 }
697 PluginCommand::SetPromptToolbar { spec } => {
698 if let Some(prompt) = &mut self.active_window_mut().prompt {
699 prompt.toolbar_widget = spec;
700 }
701 }
702 PluginCommand::ToggleOverlayToolbarWidget { key } => {
703 self.toggle_overlay_toolbar_widget(&key);
704 }
705 PluginCommand::SetPromptStatus { status } => {
706 if let Some(prompt) = &mut self.active_window_mut().prompt {
707 prompt.status = status;
708 }
709 }
710 PluginCommand::SetPromptSelectedIndex { index } => {
711 if let Some(prompt) = &mut self.active_window_mut().prompt {
712 let len = prompt.suggestions.len();
713 if len > 0 {
714 let clamped = (index as usize).min(len - 1);
715 prompt.selected_suggestion = Some(clamped);
716 }
717 }
718 }
719
720 PluginCommand::CreateWindow { root, label } => {
723 if !root.is_absolute() {
724 tracing::warn!(
725 "CreateWindow rejected: root must be absolute, got {:?}",
726 root
727 );
728 } else {
729 let _ = self.create_window_at(root, label);
730 }
731 }
732 PluginCommand::CreateWindowWithTerminal {
733 root,
734 label,
735 cwd,
736 command,
737 title,
738 request_id,
739 } => {
740 self.handle_create_window_with_terminal(
741 root, label, cwd, command, title, request_id,
742 );
743 }
744 PluginCommand::SetActiveWindow { id } => {
745 self.set_active_window(id);
746 }
747 PluginCommand::CloseWindow { id } => {
748 let _ = self.close_window(id);
749 }
750 PluginCommand::PrewarmWindow { id } => {
751 self.prewarm_window(id);
752 }
753
754 PluginCommand::WatchPath {
756 path,
757 recursive,
758 request_id,
759 } => {
760 let result = if let Some(ref bridge) = self.async_bridge {
761 self.file_watcher_manager.watch(bridge, &path, recursive)
762 } else {
763 Err(
764 "watchPath: no async bridge — file watching is unavailable in this build"
765 .to_string(),
766 )
767 };
768 self.last_watch_response_for_test = Some((request_id, result.clone()));
769 self.send_plugin_response(fresh_core::api::PluginResponse::WatchPathRegistered {
770 request_id,
771 result,
772 });
773 }
774 PluginCommand::UnwatchPath { handle } => {
775 self.file_watcher_manager.unwatch(handle);
776 }
777
778 PluginCommand::PreviewWindowInRect { id } => {
779 self.preview_window_id = match id {
783 Some(sid) if sid != self.active_window && self.windows.contains_key(&sid) => {
784 Some(sid)
785 }
786 _ => None,
787 };
788 }
789
790 PluginCommand::RegisterCommand { command } => {
792 self.handle_register_command(command);
793 }
794 PluginCommand::RegisterStatusBarElement {
795 plugin_name,
796 token_name,
797 title,
798 } => {
799 if let Err(e) = self.register_status_bar_element(&plugin_name, &token_name, &title)
800 {
801 tracing::warn!("Failed to register statusbar element: {}", e);
802 }
803 }
804 PluginCommand::SetStatusBarValue {
805 buffer_id,
806 key,
807 value,
808 } => {
809 if let Err(e) =
810 self.set_status_bar_value(fresh_core::BufferId(buffer_id as usize), &key, value)
811 {
812 tracing::warn!("Failed to set statusbar value: {}", e);
813 }
814 }
815 PluginCommand::UnregisterCommand { name } => {
816 self.handle_unregister_command(name);
817 }
818 PluginCommand::DefineMode {
819 name,
820 bindings,
821 read_only,
822 allow_text_input,
823 inherit_normal_bindings,
824 plugin_name,
825 } => {
826 self.handle_define_mode(
827 name,
828 bindings,
829 read_only,
830 allow_text_input,
831 inherit_normal_bindings,
832 plugin_name,
833 );
834 }
835
836 PluginCommand::OpenFileInBackground { path, window_id } => {
838 let route_to_inactive = match window_id {
839 Some(id) if id != self.active_window && self.windows.contains_key(&id) => {
840 Some(id)
841 }
842 _ => None,
843 };
844 if let Some(target) = route_to_inactive {
845 self.handle_open_file_in_inactive_session(target, path);
846 } else {
847 self.handle_open_file_in_background(path);
848 }
849 }
850 PluginCommand::OpenFileAtLocation { path, line, column } => {
851 return self.handle_open_file_at_location(path, line, column);
852 }
853 PluginCommand::OpenFileInSplit {
854 split_id,
855 path,
856 line,
857 column,
858 } => {
859 return self.handle_open_file_in_split(split_id, path, line, column);
860 }
861 PluginCommand::ShowBuffer { buffer_id } => {
862 self.handle_show_buffer(buffer_id);
863 }
864 PluginCommand::CloseBuffer { buffer_id } => {
865 self.handle_close_buffer(buffer_id);
866 }
867 PluginCommand::CloseOtherBuffersInSplit {
868 buffer_id,
869 split_id,
870 } => {
871 self.handle_close_other_buffers_in_split(buffer_id, split_id);
872 }
873 PluginCommand::CloseAllBuffersInSplit { split_id } => {
874 self.handle_close_all_buffers_in_split(split_id);
875 }
876 PluginCommand::CloseBuffersToRightInSplit {
877 buffer_id,
878 split_id,
879 } => {
880 self.handle_close_buffers_to_right_in_split(buffer_id, split_id);
881 }
882 PluginCommand::CloseBuffersToLeftInSplit {
883 buffer_id,
884 split_id,
885 } => {
886 self.handle_close_buffers_to_left_in_split(buffer_id, split_id);
887 }
888
889 PluginCommand::MoveTabLeft => {
890 self.handle_move_tab_left();
891 }
892 PluginCommand::MoveTabRight => {
893 self.handle_move_tab_right();
894 }
895
896 PluginCommand::StartAnimationArea { id, rect, kind } => {
898 self.handle_start_animation_area(id, rect, kind);
899 }
900 PluginCommand::StartAnimationVirtualBuffer {
901 id,
902 buffer_id,
903 kind,
904 } => {
905 self.handle_start_animation_virtual_buffer(id, buffer_id, kind);
906 }
907 PluginCommand::CancelAnimation { id } => {
908 self.active_window_mut()
909 .animations
910 .cancel(crate::view::animation::AnimationId::from_raw(id));
911 }
912
913 PluginCommand::SendLspRequest {
915 language,
916 method,
917 params,
918 request_id,
919 } => {
920 self.handle_send_lsp_request(language, method, params, request_id);
921 }
922
923 PluginCommand::SetClipboard { text } => {
925 self.handle_set_clipboard(text);
926 }
927
928 PluginCommand::SpawnProcess {
930 command,
931 args,
932 cwd,
933 stdout_to,
934 callback_id,
935 } => {
936 self.handle_spawn_process(command, args, cwd, stdout_to, callback_id);
937 }
938
939 PluginCommand::SpawnHostProcess {
940 command,
941 args,
942 cwd,
943 callback_id,
944 } => {
945 self.handle_spawn_host_process(command, args, cwd, callback_id);
946 }
947
948 PluginCommand::KillHostProcess { process_id } => {
949 self.handle_kill_host_process(process_id);
950 }
951
952 PluginCommand::SetAuthority { payload } => {
953 self.handle_set_authority(payload);
954 }
955
956 PluginCommand::ClearAuthority => {
957 tracing::info!("Plugin cleared authority; restoring local");
958 self.clear_authority();
959 }
960
961 PluginCommand::SetEnv { snippet, dir } => {
962 use crate::services::workspace_trust::TrustLevel;
966 if self.authority.workspace_trust.level() == TrustLevel::Trusted {
967 self.authority
968 .env_provider
969 .set(snippet, dir.map(std::path::PathBuf::from));
970 self.request_restart(self.working_dir().to_path_buf());
972 } else {
973 self.active_window_mut().status_message =
974 Some("Workspace not trusted — cannot activate environment".to_string());
975 }
976 }
977
978 PluginCommand::ClearEnv => {
979 let was_active = self.authority.env_provider.is_active();
980 self.authority.env_provider.clear();
981 if was_active {
982 self.request_restart(self.working_dir().to_path_buf());
983 }
984 }
985
986 PluginCommand::SetRemoteIndicatorState { state } => {
987 self.handle_set_remote_indicator_state(state);
988 }
989
990 PluginCommand::ClearRemoteIndicatorState => {
991 self.remote_indicator_override = None;
992 }
993
994 PluginCommand::SpawnProcessWait {
995 process_id,
996 callback_id,
997 } => {
998 self.handle_spawn_process_wait(process_id, callback_id);
999 }
1000
1001 PluginCommand::Delay {
1002 callback_id,
1003 duration_ms,
1004 } => {
1005 self.handle_delay(callback_id, duration_ms);
1006 }
1007
1008 PluginCommand::HttpFetch {
1009 url,
1010 target_path,
1011 callback_id,
1012 } => {
1013 self.handle_http_fetch(url, target_path, callback_id);
1014 }
1015
1016 PluginCommand::SpawnBackgroundProcess {
1017 process_id,
1018 command,
1019 args,
1020 cwd,
1021 callback_id,
1022 } => {
1023 self.handle_spawn_background_process(process_id, command, args, cwd, callback_id);
1024 }
1025
1026 PluginCommand::KillBackgroundProcess { process_id } => {
1027 self.handle_kill_background_process(process_id);
1028 }
1029
1030 PluginCommand::CreateVirtualBuffer {
1032 name,
1033 mode,
1034 read_only,
1035 } => {
1036 self.handle_create_virtual_buffer(name, mode, read_only);
1037 }
1038 PluginCommand::CreateVirtualBufferWithContent {
1039 name,
1040 mode,
1041 read_only,
1042 entries,
1043 show_line_numbers,
1044 show_cursors,
1045 editing_disabled,
1046 hidden_from_tabs,
1047 request_id,
1048 } => {
1049 self.handle_create_virtual_buffer_with_content(
1050 name,
1051 mode,
1052 read_only,
1053 entries,
1054 show_line_numbers,
1055 show_cursors,
1056 editing_disabled,
1057 hidden_from_tabs,
1058 request_id,
1059 );
1060 }
1061 PluginCommand::CreateVirtualBufferInSplit {
1062 name,
1063 mode,
1064 read_only,
1065 entries,
1066 ratio,
1067 direction,
1068 panel_id,
1069 show_line_numbers,
1070 show_cursors,
1071 editing_disabled,
1072 line_wrap,
1073 before,
1074 role,
1075 request_id,
1076 } => {
1077 self.handle_create_virtual_buffer_in_split(
1078 name,
1079 mode,
1080 read_only,
1081 entries,
1082 ratio,
1083 direction,
1084 panel_id,
1085 show_line_numbers,
1086 show_cursors,
1087 editing_disabled,
1088 line_wrap,
1089 before,
1090 role,
1091 request_id,
1092 );
1093 }
1094 PluginCommand::SetVirtualBufferContent { buffer_id, entries } => {
1095 self.handle_set_virtual_buffer_content(buffer_id, entries);
1096 }
1097 PluginCommand::GetTextPropertiesAtCursor { buffer_id } => {
1098 self.handle_get_text_properties_at_cursor(buffer_id);
1099 }
1100 PluginCommand::CreateVirtualBufferInExistingSplit {
1101 name,
1102 mode,
1103 read_only,
1104 entries,
1105 split_id,
1106 show_line_numbers,
1107 show_cursors,
1108 editing_disabled,
1109 line_wrap,
1110 request_id,
1111 } => {
1112 self.handle_create_virtual_buffer_in_existing_split(
1113 name,
1114 mode,
1115 read_only,
1116 entries,
1117 split_id,
1118 show_line_numbers,
1119 show_cursors,
1120 editing_disabled,
1121 line_wrap,
1122 request_id,
1123 );
1124 }
1125
1126 PluginCommand::SetContext { name, active } => {
1128 self.handle_set_context(name, active);
1129 }
1130
1131 PluginCommand::SetReviewDiffHunks { hunks } => {
1133 self.active_window_mut().review_hunks = hunks;
1134 tracing::debug!(
1135 "Set {} review hunks",
1136 self.active_window_mut().review_hunks.len()
1137 );
1138 }
1139
1140 PluginCommand::ExecuteAction { action_name } => {
1142 self.handle_execute_action(action_name);
1143 }
1144 PluginCommand::ExecuteActions { actions } => {
1145 self.handle_execute_actions(actions);
1146 }
1147 PluginCommand::GetBufferText {
1148 buffer_id,
1149 start,
1150 end,
1151 request_id,
1152 } => {
1153 self.handle_get_buffer_text(buffer_id, start, end, request_id);
1154 }
1155 PluginCommand::GetLineStartPosition {
1156 buffer_id,
1157 line,
1158 request_id,
1159 } => {
1160 self.handle_get_line_start_position(buffer_id, line, request_id);
1161 }
1162 PluginCommand::GetLineEndPosition {
1163 buffer_id,
1164 line,
1165 request_id,
1166 } => {
1167 self.handle_get_line_end_position(buffer_id, line, request_id);
1168 }
1169 PluginCommand::GetBufferLineCount {
1170 buffer_id,
1171 request_id,
1172 } => {
1173 self.handle_get_buffer_line_count(buffer_id, request_id);
1174 }
1175 PluginCommand::OpenFileStreaming { path, request_id } => {
1176 self.handle_open_file_streaming(path, request_id);
1177 }
1178 PluginCommand::RefreshBufferFromDisk {
1179 buffer_id,
1180 request_id,
1181 } => {
1182 self.handle_refresh_buffer_from_disk(buffer_id, request_id);
1183 }
1184 PluginCommand::SetBufferGroupPanelBuffer {
1185 group_id,
1186 panel_name,
1187 buffer_id,
1188 request_id,
1189 } => {
1190 self.handle_set_buffer_group_panel_buffer(
1191 group_id, panel_name, buffer_id, request_id,
1192 );
1193 }
1194 PluginCommand::ScrollToLineCenter {
1195 split_id,
1196 buffer_id,
1197 line,
1198 } => {
1199 self.handle_scroll_to_line_center(split_id, buffer_id, line);
1200 }
1201 PluginCommand::ScrollBufferToLine { buffer_id, line } => {
1202 self.handle_scroll_buffer_to_line(buffer_id, line);
1203 }
1204 PluginCommand::SetEditorMode { mode } => {
1205 self.handle_set_editor_mode(mode);
1206 }
1207
1208 PluginCommand::ShowActionPopup {
1210 popup_id,
1211 title,
1212 message,
1213 actions,
1214 } => {
1215 self.handle_show_action_popup(popup_id, title, message, actions);
1216 }
1217
1218 PluginCommand::SetLspMenuContributions {
1219 plugin_id,
1220 language,
1221 items,
1222 } => {
1223 self.handle_set_lsp_menu_contributions(plugin_id, language, items);
1224 }
1225
1226 PluginCommand::DisableLspForLanguage { language } => {
1227 self.handle_disable_lsp_for_language(language);
1228 }
1229
1230 PluginCommand::RestartLspForLanguage { language } => {
1231 self.handle_restart_lsp_for_language(language);
1232 }
1233
1234 PluginCommand::SetLspRootUri { language, uri } => {
1235 self.handle_set_lsp_root_uri(language, uri);
1236 }
1237
1238 PluginCommand::CreateScrollSyncGroup {
1240 group_id,
1241 left_split,
1242 right_split,
1243 } => {
1244 self.handle_create_scroll_sync_group(group_id, left_split, right_split);
1245 }
1246 PluginCommand::SetScrollSyncAnchors { group_id, anchors } => {
1247 self.handle_set_scroll_sync_anchors(group_id, anchors);
1248 }
1249 PluginCommand::RemoveScrollSyncGroup { group_id } => {
1250 self.handle_remove_scroll_sync_group(group_id);
1251 }
1252
1253 PluginCommand::CreateCompositeBuffer {
1255 name,
1256 mode,
1257 layout,
1258 sources,
1259 hunks,
1260 initial_focus_hunk,
1261 request_id,
1262 } => {
1263 self.handle_create_composite_buffer(
1264 name,
1265 mode,
1266 layout,
1267 sources,
1268 hunks,
1269 initial_focus_hunk,
1270 request_id,
1271 );
1272 }
1273 PluginCommand::UpdateCompositeAlignment { buffer_id, hunks } => {
1274 self.handle_update_composite_alignment(buffer_id, hunks);
1275 }
1276 PluginCommand::CloseCompositeBuffer { buffer_id } => {
1277 self.active_window_mut().close_composite_buffer(buffer_id);
1278 }
1279 PluginCommand::FlushLayout => {
1280 self.flush_layout();
1281 }
1282 PluginCommand::CompositeNextHunk { buffer_id } => {
1283 let split_id = self
1284 .windows
1285 .get(&self.active_window)
1286 .and_then(|w| w.buffers.splits())
1287 .map(|(mgr, _)| mgr)
1288 .expect("active window must have a populated split layout")
1289 .active_split();
1290 self.active_window_mut()
1291 .composite_next_hunk(split_id, buffer_id);
1292 }
1293 PluginCommand::CompositePrevHunk { buffer_id } => {
1294 let split_id = self
1295 .windows
1296 .get(&self.active_window)
1297 .and_then(|w| w.buffers.splits())
1298 .map(|(mgr, _)| mgr)
1299 .expect("active window must have a populated split layout")
1300 .active_split();
1301 self.active_window_mut()
1302 .composite_prev_hunk(split_id, buffer_id);
1303 }
1304
1305 PluginCommand::CreateBufferGroup {
1307 name,
1308 mode,
1309 layout_json,
1310 request_id,
1311 } => {
1312 self.handle_create_buffer_group(name, mode, layout_json, request_id);
1313 }
1314 PluginCommand::SetPanelContent {
1315 group_id,
1316 panel_name,
1317 entries,
1318 } => {
1319 self.set_panel_content(group_id, panel_name, entries);
1320 }
1321 PluginCommand::CloseBufferGroup { group_id } => {
1322 self.close_buffer_group(group_id);
1323 }
1324 PluginCommand::FocusPanel {
1325 group_id,
1326 panel_name,
1327 } => {
1328 self.focus_panel(group_id, panel_name);
1329 }
1330
1331 PluginCommand::SaveBufferToPath { buffer_id, path } => {
1333 self.handle_save_buffer_to_path(buffer_id, path);
1334 }
1335
1336 #[cfg(feature = "plugins")]
1338 PluginCommand::LoadPlugin { path, callback_id } => {
1339 self.handle_load_plugin(path, callback_id);
1340 }
1341 #[cfg(feature = "plugins")]
1342 PluginCommand::UnloadPlugin { name, callback_id } => {
1343 self.handle_unload_plugin(name, callback_id);
1344 }
1345 #[cfg(feature = "plugins")]
1346 PluginCommand::ReloadPlugin { name, callback_id } => {
1347 self.handle_reload_plugin(name, callback_id);
1348 }
1349 #[cfg(feature = "plugins")]
1350 PluginCommand::ListPlugins { callback_id } => {
1351 self.handle_list_plugins(callback_id);
1352 }
1353 #[cfg(not(feature = "plugins"))]
1355 PluginCommand::LoadPlugin { .. }
1356 | PluginCommand::UnloadPlugin { .. }
1357 | PluginCommand::ReloadPlugin { .. }
1358 | PluginCommand::ListPlugins { .. } => {
1359 tracing::warn!("Plugin management commands require the 'plugins' feature");
1360 }
1361
1362 PluginCommand::CreateTerminal {
1364 cwd,
1365 direction,
1366 ratio,
1367 focus,
1368 persistent,
1369 window_id,
1370 command,
1371 title,
1372 request_id,
1373 } => {
1374 self.handle_create_terminal(
1375 cwd, direction, ratio, focus, persistent, window_id, command, title, request_id,
1376 );
1377 }
1378
1379 PluginCommand::SendTerminalInput { terminal_id, data } => {
1380 self.handle_send_terminal_input(terminal_id, data);
1381 }
1382
1383 PluginCommand::CloseTerminal { terminal_id } => {
1384 self.handle_close_terminal(terminal_id);
1385 }
1386
1387 PluginCommand::SignalWindow { id, signal } => {
1388 self.handle_signal_window(id, &signal);
1389 }
1390
1391 PluginCommand::GrepProject {
1392 pattern,
1393 fixed_string,
1394 case_sensitive,
1395 max_results,
1396 whole_words,
1397 callback_id,
1398 } => {
1399 self.handle_grep_project(
1400 pattern,
1401 fixed_string,
1402 case_sensitive,
1403 max_results,
1404 whole_words,
1405 callback_id,
1406 );
1407 }
1408
1409 PluginCommand::BeginSearch {
1410 pattern,
1411 fixed_string,
1412 case_sensitive,
1413 max_results,
1414 whole_words,
1415 handle_id,
1416 } => {
1417 self.handle_begin_search(
1418 pattern,
1419 fixed_string,
1420 case_sensitive,
1421 max_results,
1422 whole_words,
1423 handle_id,
1424 );
1425 }
1426
1427 PluginCommand::ReplaceInBuffer {
1428 file_path,
1429 matches,
1430 replacement,
1431 callback_id,
1432 } => {
1433 self.handle_replace_in_buffer(file_path, matches, replacement, callback_id);
1434 }
1435
1436 PluginCommand::MountWidgetPanel {
1437 panel_id,
1438 buffer_id,
1439 spec,
1440 } => {
1441 self.handle_mount_widget_panel(panel_id, buffer_id, spec);
1442 }
1443
1444 PluginCommand::UpdateWidgetPanel { panel_id, spec } => {
1445 self.handle_update_widget_panel(panel_id, spec);
1446 }
1447
1448 PluginCommand::UnmountWidgetPanel { panel_id } => {
1449 self.handle_unmount_widget_panel(panel_id);
1450 }
1451
1452 PluginCommand::WidgetCommand { panel_id, action } => {
1453 self.handle_widget_command(panel_id, action);
1454 }
1455
1456 PluginCommand::WidgetMutate { panel_id, mutation } => {
1457 self.handle_widget_mutate(panel_id, mutation);
1458 }
1459
1460 PluginCommand::MountFloatingWidget {
1461 panel_id,
1462 spec,
1463 width_pct,
1464 height_pct,
1465 } => {
1466 self.handle_mount_floating_widget(panel_id, spec, width_pct, height_pct);
1467 }
1468
1469 PluginCommand::UpdateFloatingWidget { panel_id, spec } => {
1470 self.handle_update_floating_widget(panel_id, spec);
1471 }
1472
1473 PluginCommand::UnmountFloatingWidget { panel_id } => {
1474 self.handle_unmount_floating_widget(panel_id);
1475 }
1476 }
1477 Ok(())
1478 }
1479
1480 fn handle_save_buffer_to_path(&mut self, buffer_id: BufferId, path: std::path::PathBuf) {
1482 if let Some(state) = self
1483 .windows
1484 .get_mut(&self.active_window)
1485 .map(|w| &mut w.buffers)
1486 .expect("active window present")
1487 .get_mut(&buffer_id)
1488 {
1489 match state.buffer.save_to_file(&path) {
1491 Ok(()) => {
1492 if let Err(e) = self.finalize_save(Some(path)) {
1495 tracing::warn!("Failed to finalize save: {}", e);
1496 }
1497 tracing::debug!("Saved buffer {:?} to path", buffer_id);
1498 }
1499 Err(e) => {
1500 self.handle_set_status(format!("Error saving: {}", e));
1501 tracing::error!("Failed to save buffer to path: {}", e);
1502 }
1503 }
1504 } else {
1505 self.handle_set_status(format!("Buffer {:?} not found", buffer_id));
1506 tracing::warn!("SaveBufferToPath: buffer {:?} not found", buffer_id);
1507 }
1508 }
1509
1510 #[cfg(feature = "plugins")]
1512 fn handle_load_plugin(&mut self, path: std::path::PathBuf, callback_id: JsCallbackId) {
1513 let load_result = self.plugin_manager.read().unwrap().load_plugin(&path);
1514 match load_result {
1515 Ok(()) => {
1516 tracing::info!("Loaded plugin from {:?}", path);
1517 self.plugin_manager
1518 .read()
1519 .unwrap()
1520 .resolve_callback(callback_id, "true".to_string());
1521 }
1522 Err(e) => {
1523 tracing::error!("Failed to load plugin from {:?}: {}", path, e);
1524 self.plugin_manager
1525 .read()
1526 .unwrap()
1527 .reject_callback(callback_id, format!("{}", e));
1528 }
1529 }
1530 }
1531
1532 #[cfg(feature = "plugins")]
1534 fn handle_unload_plugin(&mut self, name: String, callback_id: JsCallbackId) {
1535 let result = self.plugin_manager.write().unwrap().unload_plugin(&name);
1538 match result {
1539 Ok(()) => {
1540 tracing::info!("Unloaded plugin: {}", name);
1541 if let Ok(mut schemas) = self.plugin_schemas.write() {
1542 schemas.remove(&name);
1543 }
1544 self.plugin_manager
1545 .read()
1546 .unwrap()
1547 .resolve_callback(callback_id, "true".to_string());
1548 }
1549 Err(e) => {
1550 tracing::error!("Failed to unload plugin '{}': {}", name, e);
1551 self.plugin_manager
1552 .read()
1553 .unwrap()
1554 .reject_callback(callback_id, format!("{}", e));
1555 }
1556 }
1557 }
1558
1559 #[cfg(feature = "plugins")]
1561 fn handle_reload_plugin(&mut self, name: String, callback_id: JsCallbackId) {
1562 let path = self
1566 .plugin_manager
1567 .read()
1568 .unwrap()
1569 .list_plugins()
1570 .into_iter()
1571 .find(|p| p.name == name)
1572 .map(|p| p.path);
1573 let _ = path; let reload_result = self.plugin_manager.read().unwrap().reload_plugin(&name);
1575 match reload_result {
1576 Ok(()) => {
1577 tracing::info!("Reloaded plugin: {}", name);
1578 self.plugin_manager
1579 .read()
1580 .unwrap()
1581 .resolve_callback(callback_id, "true".to_string());
1582 }
1583 Err(e) => {
1584 tracing::error!("Failed to reload plugin '{}': {}", name, e);
1585 self.plugin_manager
1586 .read()
1587 .unwrap()
1588 .reject_callback(callback_id, format!("{}", e));
1589 }
1590 }
1591 }
1592
1593 #[cfg(feature = "plugins")]
1595 fn handle_list_plugins(&mut self, callback_id: JsCallbackId) {
1596 let plugins = self.plugin_manager.read().unwrap().list_plugins();
1597 let json_array: Vec<serde_json::Value> = plugins
1599 .iter()
1600 .map(|p| {
1601 serde_json::json!({
1602 "name": p.name,
1603 "path": p.path.to_string_lossy(),
1604 "enabled": p.enabled
1605 })
1606 })
1607 .collect();
1608 let json_str = serde_json::to_string(&json_array).unwrap_or_else(|_| "[]".to_string());
1609 self.plugin_manager
1610 .read()
1611 .unwrap()
1612 .resolve_callback(callback_id, json_str);
1613 }
1614
1615 fn handle_execute_action(&mut self, action_name: String) {
1617 use crate::input::keybindings::Action;
1618 use std::collections::HashMap;
1619
1620 if let Some(action) = Action::from_str(&action_name, &HashMap::new()) {
1622 if let Err(e) = self.handle_action(action) {
1624 tracing::warn!("Failed to execute action '{}': {}", action_name, e);
1625 } else {
1626 tracing::debug!("Executed action: {}", action_name);
1627 }
1628 } else {
1629 tracing::warn!("Unknown action: {}", action_name);
1630 }
1631 }
1632
1633 fn handle_execute_actions(&mut self, actions: Vec<fresh_core::api::ActionSpec>) {
1636 use crate::input::keybindings::Action;
1637 use std::collections::HashMap;
1638
1639 for action_spec in actions {
1640 if let Some(action) = Action::from_str(&action_spec.action, &HashMap::new()) {
1641 for _ in 0..action_spec.count {
1643 if let Err(e) = self.handle_action(action.clone()) {
1644 tracing::warn!("Failed to execute action '{}': {}", action_spec.action, e);
1645 return; }
1647 }
1648 tracing::debug!(
1649 "Executed action '{}' {} time(s)",
1650 action_spec.action,
1651 action_spec.count
1652 );
1653 } else {
1654 tracing::warn!("Unknown action: {}", action_spec.action);
1655 return; }
1657 }
1658 }
1659
1660 fn handle_get_buffer_text(
1662 &mut self,
1663 buffer_id: BufferId,
1664 start: usize,
1665 end: usize,
1666 request_id: u64,
1667 ) {
1668 let result = if let Some(state) = self
1669 .windows
1670 .get_mut(&self.active_window)
1671 .map(|w| &mut w.buffers)
1672 .expect("active window present")
1673 .get_mut(&buffer_id)
1674 {
1675 let len = state.buffer.len();
1677 if start <= end && end <= len {
1678 Ok(state.get_text_range(start, end))
1679 } else {
1680 Err(format!(
1681 "Invalid range {}..{} for buffer of length {}",
1682 start, end, len
1683 ))
1684 }
1685 } else {
1686 Err(format!("Buffer {:?} not found", buffer_id))
1687 };
1688
1689 let callback_id = fresh_core::api::JsCallbackId::from(request_id);
1691 match result {
1692 Ok(text) => {
1693 let json = serde_json::to_string(&text).unwrap_or_else(|_| "null".to_string());
1695 self.plugin_manager
1696 .read()
1697 .unwrap()
1698 .resolve_callback(callback_id, json);
1699 }
1700 Err(error) => {
1701 self.plugin_manager
1702 .read()
1703 .unwrap()
1704 .reject_callback(callback_id, error);
1705 }
1706 }
1707 }
1708
1709 fn handle_set_editor_mode(&mut self, mode: Option<String>) {
1711 self.active_window_mut().editor_mode = mode.clone();
1712 tracing::debug!("Set editor mode: {:?}", mode);
1713 }
1714
1715 fn resolve_buffer_id(&self, buffer_id: BufferId) -> BufferId {
1717 if buffer_id.0 == 0 {
1718 self.active_buffer()
1719 } else {
1720 buffer_id
1721 }
1722 }
1723
1724 fn resolve_json_callback<T: serde::Serialize>(&mut self, request_id: u64, value: T) {
1726 let callback_id = fresh_core::api::JsCallbackId::from(request_id);
1727 let json = serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string());
1728 self.plugin_manager
1729 .read()
1730 .unwrap()
1731 .resolve_callback(callback_id, json);
1732 }
1733
1734 fn handle_get_line_start_position(&mut self, buffer_id: BufferId, line: u32, request_id: u64) {
1736 let actual_buffer_id = self.resolve_buffer_id(buffer_id);
1737 let result = self
1738 .windows
1739 .get_mut(&self.active_window)
1740 .map(|w| &mut w.buffers)
1741 .expect("active window present")
1742 .get_mut(&actual_buffer_id)
1743 .and_then(|state| {
1744 let len = state.buffer.len();
1745 let content = state.get_text_range(0, len);
1746 buffer_line_byte_offset(&content, len, line as usize, false)
1747 });
1748 self.resolve_json_callback(request_id, result);
1749 }
1750
1751 fn handle_get_line_end_position(&mut self, buffer_id: BufferId, line: u32, request_id: u64) {
1754 let actual_buffer_id = self.resolve_buffer_id(buffer_id);
1755 let result = self
1756 .windows
1757 .get_mut(&self.active_window)
1758 .map(|w| &mut w.buffers)
1759 .expect("active window present")
1760 .get_mut(&actual_buffer_id)
1761 .and_then(|state| {
1762 let len = state.buffer.len();
1763 let content = state.get_text_range(0, len);
1764 buffer_line_byte_offset(&content, len, line as usize, true)
1765 });
1766 self.resolve_json_callback(request_id, result);
1767 }
1768
1769 fn handle_get_buffer_line_count(&mut self, buffer_id: BufferId, request_id: u64) {
1771 let actual_buffer_id = self.resolve_buffer_id(buffer_id);
1772
1773 let result = if let Some(state) = self
1774 .windows
1775 .get_mut(&self.active_window)
1776 .map(|w| &mut w.buffers)
1777 .expect("active window present")
1778 .get_mut(&actual_buffer_id)
1779 {
1780 let buffer_len = state.buffer.len();
1781 let content = state.get_text_range(0, buffer_len);
1782
1783 if content.is_empty() {
1785 Some(1) } else {
1787 let newline_count = content.chars().filter(|&c| c == '\n').count();
1788 let ends_with_newline = content.ends_with('\n');
1790 if ends_with_newline {
1791 Some(newline_count)
1792 } else {
1793 Some(newline_count + 1)
1794 }
1795 }
1796 } else {
1797 None
1798 };
1799
1800 self.resolve_json_callback(request_id, result);
1801 }
1802
1803 fn handle_open_file_streaming(&mut self, path: std::path::PathBuf, request_id: u64) {
1820 if !self.authority.filesystem.exists(&path) {
1823 if let Some(parent) = path.parent() {
1824 if !parent.as_os_str().is_empty() {
1825 if let Err(e) = std::fs::create_dir_all(parent) {
1826 tracing::warn!(
1827 "openFileStreaming: failed to create parent dir {:?}: {}",
1828 parent,
1829 e
1830 );
1831 self.resolve_json_callback::<Option<u64>>(request_id, None);
1832 return;
1833 }
1834 }
1835 }
1836 if let Err(e) = std::fs::write(&path, b"") {
1837 tracing::warn!(
1838 "openFileStreaming: failed to create empty file at {:?}: {}",
1839 path,
1840 e
1841 );
1842 self.resolve_json_callback::<Option<u64>>(request_id, None);
1843 return;
1844 }
1845 }
1846
1847 let buffer_id = match self.open_file_no_focus(&path) {
1851 Ok(id) => id,
1852 Err(e) => {
1853 tracing::warn!(
1854 "openFileStreaming: open_file_no_focus failed for {:?}: {}",
1855 path,
1856 e
1857 );
1858 self.resolve_json_callback::<Option<u64>>(request_id, None);
1859 return;
1860 }
1861 };
1862
1863 if let Some(meta) = self.active_window_mut().buffer_metadata.get_mut(&buffer_id) {
1868 meta.hidden_from_tabs = true;
1869 meta.auto_revert_enabled = false;
1870 }
1871 let active_split = self
1872 .windows
1873 .get(&self.active_window)
1874 .and_then(|w| w.buffers.splits())
1875 .map(|(mgr, _)| mgr)
1876 .expect("active window must have a populated split layout")
1877 .active_split();
1878 if let Some(vs) = self
1879 .windows
1880 .get_mut(&self.active_window)
1881 .and_then(|w| w.split_view_states_mut())
1882 .expect("active window must have a populated split layout")
1883 .get_mut(&active_split)
1884 {
1885 use crate::view::split::TabTarget;
1886 vs.open_buffers
1887 .retain(|t| !matches!(t, TabTarget::Buffer(b) if *b == buffer_id));
1888 }
1889
1890 self.resolve_json_callback(request_id, Some(buffer_id.0));
1891 }
1892
1893 fn handle_set_buffer_group_panel_buffer(
1896 &mut self,
1897 group_id: usize,
1898 panel_name: String,
1899 buffer_id: BufferId,
1900 request_id: u64,
1901 ) {
1902 let actual_buffer_id = self.resolve_buffer_id(buffer_id);
1903 let ok = self.set_buffer_group_panel_buffer(group_id, panel_name, actual_buffer_id);
1904 self.resolve_json_callback(request_id, ok);
1905 }
1906
1907 fn handle_refresh_buffer_from_disk(&mut self, buffer_id: BufferId, request_id: u64) {
1911 let actual_buffer_id = self.resolve_buffer_id(buffer_id);
1912
1913 let path = self
1914 .windows
1915 .get(&self.active_window)
1916 .and_then(|w| w.buffers.splits())
1917 .map(|(_, _)| ())
1918 .and_then(|_| {
1919 self.windows
1920 .get(&self.active_window)?
1921 .buffers
1922 .get(&actual_buffer_id)?
1923 .buffer
1924 .file_path()
1925 .map(|p| p.to_path_buf())
1926 });
1927
1928 let Some(path) = path else {
1929 self.resolve_json_callback::<Option<usize>>(request_id, None);
1931 return;
1932 };
1933
1934 let new_size = match self.authority.filesystem.metadata(&path) {
1935 Ok(m) => m.size as usize,
1936 Err(_) => {
1937 self.resolve_json_callback::<Option<usize>>(request_id, None);
1938 return;
1939 }
1940 };
1941
1942 let new_total = if let Some(state) = self
1943 .windows
1944 .get_mut(&self.active_window)
1945 .map(|w| &mut w.buffers)
1946 .expect("active window present")
1947 .get_mut(&actual_buffer_id)
1948 {
1949 let old = state.buffer.total_bytes();
1950 if new_size > old {
1951 state.buffer.extend_streaming(&path, new_size);
1952 }
1953 state.buffer.total_bytes()
1954 } else {
1955 self.resolve_json_callback::<Option<usize>>(request_id, None);
1956 return;
1957 };
1958
1959 self.resolve_json_callback(request_id, Some(new_total));
1960 }
1961
1962 fn handle_scroll_to_line_center(
1964 &mut self,
1965 split_id: SplitId,
1966 buffer_id: BufferId,
1967 line: usize,
1968 ) {
1969 let actual_split_id = if split_id.0 == 0 {
1970 self.windows
1971 .get(&self.active_window)
1972 .and_then(|w| w.buffers.splits())
1973 .map(|(mgr, _)| mgr)
1974 .expect("active window must have a populated split layout")
1975 .active_split()
1976 } else {
1977 LeafId(split_id)
1978 };
1979 let actual_buffer_id = self.resolve_buffer_id(buffer_id);
1980
1981 let viewport_height = if let Some(view_state) = self
1983 .windows
1984 .get(&self.active_window)
1985 .and_then(|w| w.buffers.splits())
1986 .map(|(_, vs)| vs)
1987 .expect("active window must have a populated split layout")
1988 .get(&actual_split_id)
1989 {
1990 view_state.viewport.height as usize
1991 } else {
1992 return;
1993 };
1994
1995 let lines_above = viewport_height / 2;
1997 let target_line = line.saturating_sub(lines_above);
1998
1999 self.active_window_mut().scroll_split_viewport_to(
2000 actual_buffer_id,
2001 actual_split_id,
2002 target_line,
2003 true,
2004 );
2005 }
2006
2007 fn handle_scroll_buffer_to_line(&mut self, buffer_id: BufferId, line: usize) {
2017 if !self
2018 .windows
2019 .get(&self.active_window)
2020 .map(|w| &w.buffers)
2021 .expect("active window present")
2022 .contains_key(&buffer_id)
2023 {
2024 return;
2025 }
2026
2027 let mut target_leaves: Vec<LeafId> = Vec::new();
2029
2030 for leaf_id in self
2032 .windows
2033 .get(&self.active_window)
2034 .and_then(|w| w.buffers.splits())
2035 .map(|(mgr, _)| mgr)
2036 .expect("active window must have a populated split layout")
2037 .root()
2038 .leaf_split_ids()
2039 {
2040 if let Some(vs) = self
2041 .windows
2042 .get(&self.active_window)
2043 .and_then(|w| w.buffers.splits())
2044 .map(|(_, vs)| vs)
2045 .expect("active window must have a populated split layout")
2046 .get(&leaf_id)
2047 {
2048 if vs.active_buffer == buffer_id {
2049 target_leaves.push(leaf_id);
2050 }
2051 }
2052 }
2053
2054 for (_group_leaf_id, node) in self.active_window().grouped_subtrees.iter() {
2056 if let crate::view::split::SplitNode::Grouped { layout, .. } = node {
2057 for inner_leaf in layout.leaf_split_ids() {
2058 if let Some(vs) = self
2059 .windows
2060 .get(&self.active_window)
2061 .and_then(|w| w.buffers.splits())
2062 .map(|(_, vs)| vs)
2063 .expect("active window must have a populated split layout")
2064 .get(&inner_leaf)
2065 {
2066 if vs.active_buffer == buffer_id && !target_leaves.contains(&inner_leaf) {
2067 target_leaves.push(inner_leaf);
2068 }
2069 }
2070 }
2071 }
2072 }
2073
2074 if target_leaves.is_empty() {
2075 return;
2076 }
2077
2078 self.active_window_mut()
2079 .scroll_buffer_to_line_in_splits(buffer_id, &target_leaves, line);
2080 }
2081
2082 fn handle_spawn_host_process(
2083 &mut self,
2084 command: String,
2085 args: Vec<String>,
2086 cwd: Option<String>,
2087 callback_id: JsCallbackId,
2088 ) {
2089 if let (Some(runtime), Some(bridge)) = (&self.tokio_runtime, &self.async_bridge) {
2104 use tokio::io::{AsyncReadExt, BufReader};
2105 use tokio::process::Command as TokioCommand;
2106
2107 let effective_cwd = cwd.or_else(|| {
2108 std::env::current_dir()
2109 .map(|p| p.to_string_lossy().to_string())
2110 .ok()
2111 });
2112 let sender = bridge.sender();
2113 let process_id = callback_id.as_u64();
2114
2115 if let crate::services::workspace_trust::SpawnDecision::Deny(reason) = self
2122 .authority
2123 .workspace_trust
2124 .decide(&command, effective_cwd.as_deref())
2125 {
2126 #[allow(clippy::let_underscore_must_use)]
2127 let _ = sender.send(AsyncMessage::PluginProcessOutput {
2128 process_id,
2129 stdout: String::new(),
2130 stderr: reason,
2131 exit_code: -1,
2132 });
2133 return;
2134 }
2135
2136 let (kill_tx, mut kill_rx) = tokio::sync::oneshot::channel::<()>();
2137 self.host_process_handles.insert(process_id, kill_tx);
2138
2139 runtime.spawn(async move {
2140 use crate::services::process_hidden::HideWindow;
2141 let mut cmd = TokioCommand::new(&command);
2142 cmd.args(&args);
2143 cmd.stdout(std::process::Stdio::piped());
2144 cmd.stderr(std::process::Stdio::piped());
2145 cmd.hide_window();
2146 if let Some(ref dir) = effective_cwd {
2147 cmd.current_dir(dir);
2148 }
2149 let mut child = match cmd.spawn() {
2150 Ok(c) => c,
2151 Err(e) => {
2152 #[allow(clippy::let_underscore_must_use)]
2153 let _ = sender.send(AsyncMessage::PluginProcessOutput {
2154 process_id,
2155 stdout: String::new(),
2156 stderr: e.to_string(),
2157 exit_code: -1,
2158 });
2159 return;
2160 }
2161 };
2162
2163 let stdout_pipe = child.stdout.take();
2169 let stderr_pipe = child.stderr.take();
2170
2171 let stdout_fut = async {
2172 let mut buf = String::new();
2173 if let Some(s) = stdout_pipe {
2174 #[allow(clippy::let_underscore_must_use)]
2175 let _ = BufReader::new(s).read_to_string(&mut buf).await;
2176 }
2177 buf
2178 };
2179 let stderr_fut = async {
2180 let mut buf = String::new();
2181 if let Some(s) = stderr_pipe {
2182 #[allow(clippy::let_underscore_must_use)]
2183 let _ = BufReader::new(s).read_to_string(&mut buf).await;
2184 }
2185 buf
2186 };
2187 let wait_fut = async {
2188 tokio::select! {
2189 status = child.wait() => {
2190 status.map(|s| s.code().unwrap_or(-1)).unwrap_or(-1)
2191 }
2192 _ = &mut kill_rx => {
2193 #[allow(clippy::let_underscore_must_use)]
2197 let _ = child.start_kill();
2198 child
2199 .wait()
2200 .await
2201 .map(|s| s.code().unwrap_or(-1))
2202 .unwrap_or(-1)
2203 }
2204 }
2205 };
2206 let (stdout, stderr, exit_code) = tokio::join!(stdout_fut, stderr_fut, wait_fut);
2207
2208 #[allow(clippy::let_underscore_must_use)]
2209 let _ = sender.send(AsyncMessage::PluginProcessOutput {
2210 process_id,
2211 stdout,
2212 stderr,
2213 exit_code,
2214 });
2215 });
2216 } else {
2217 self.plugin_manager
2218 .read()
2219 .unwrap()
2220 .reject_callback(callback_id, "Async runtime not available".to_string());
2221 }
2222 }
2223
2224 fn handle_spawn_background_process(
2225 &mut self,
2226 process_id: u64,
2227 command: String,
2228 args: Vec<String>,
2229 cwd: Option<String>,
2230 callback_id: JsCallbackId,
2231 ) {
2232 if let (Some(runtime), Some(bridge)) = (&self.tokio_runtime, &self.async_bridge) {
2234 use tokio::io::{AsyncBufReadExt, BufReader};
2235 use tokio::process::Command as TokioCommand;
2236
2237 let effective_cwd = cwd.unwrap_or_else(|| {
2238 std::env::current_dir()
2239 .map(|p| p.to_string_lossy().to_string())
2240 .unwrap_or_else(|_| ".".to_string())
2241 });
2242
2243 let sender = bridge.sender();
2244 let sender_stdout = sender.clone();
2245 let sender_stderr = sender.clone();
2246 let callback_id_u64 = callback_id.as_u64();
2247
2248 #[allow(clippy::let_underscore_must_use)]
2250 let handle = runtime.spawn(async move {
2251 use crate::services::process_hidden::HideWindow;
2252 let mut child = match TokioCommand::new(&command)
2253 .args(&args)
2254 .current_dir(&effective_cwd)
2255 .stdout(std::process::Stdio::piped())
2256 .stderr(std::process::Stdio::piped())
2257 .hide_window()
2258 .spawn()
2259 {
2260 Ok(child) => child,
2261 Err(e) => {
2262 let _ = sender.send(crate::services::async_bridge::AsyncMessage::Plugin(
2263 fresh_core::api::PluginAsyncMessage::ProcessExit {
2264 process_id,
2265 callback_id: callback_id_u64,
2266 exit_code: -1,
2267 },
2268 ));
2269 tracing::error!("Failed to spawn background process: {}", e);
2270 return;
2271 }
2272 };
2273
2274 let stdout = child.stdout.take();
2276 let stderr = child.stderr.take();
2277 let pid = process_id;
2278
2279 if let Some(stdout) = stdout {
2281 let sender = sender_stdout;
2282 tokio::spawn(async move {
2283 let reader = BufReader::new(stdout);
2284 let mut lines = reader.lines();
2285 while let Ok(Some(line)) = lines.next_line().await {
2286 let _ =
2287 sender.send(crate::services::async_bridge::AsyncMessage::Plugin(
2288 fresh_core::api::PluginAsyncMessage::ProcessStdout {
2289 process_id: pid,
2290 data: line + "\n",
2291 },
2292 ));
2293 }
2294 });
2295 }
2296
2297 if let Some(stderr) = stderr {
2299 let sender = sender_stderr;
2300 tokio::spawn(async move {
2301 let reader = BufReader::new(stderr);
2302 let mut lines = reader.lines();
2303 while let Ok(Some(line)) = lines.next_line().await {
2304 let _ =
2305 sender.send(crate::services::async_bridge::AsyncMessage::Plugin(
2306 fresh_core::api::PluginAsyncMessage::ProcessStderr {
2307 process_id: pid,
2308 data: line + "\n",
2309 },
2310 ));
2311 }
2312 });
2313 }
2314
2315 let exit_code = match child.wait().await {
2317 Ok(status) => status.code().unwrap_or(-1),
2318 Err(_) => -1,
2319 };
2320
2321 let _ = sender.send(crate::services::async_bridge::AsyncMessage::Plugin(
2322 fresh_core::api::PluginAsyncMessage::ProcessExit {
2323 process_id,
2324 callback_id: callback_id_u64,
2325 exit_code,
2326 },
2327 ));
2328 });
2329
2330 self.background_process_handles
2332 .insert(process_id, handle.abort_handle());
2333 } else {
2334 self.plugin_manager
2336 .read()
2337 .unwrap()
2338 .reject_callback(callback_id, "Async runtime not available".to_string());
2339 }
2340 }
2341
2342 fn handle_create_virtual_buffer_with_content(
2343 &mut self,
2344 name: String,
2345 mode: String,
2346 read_only: bool,
2347 entries: Vec<fresh_core::text_property::TextPropertyEntry>,
2348 show_line_numbers: bool,
2349 show_cursors: bool,
2350 editing_disabled: bool,
2351 hidden_from_tabs: bool,
2352 request_id: Option<u64>,
2353 ) {
2354 let buffer_id =
2355 self.active_window_mut()
2356 .create_virtual_buffer(name.clone(), mode.clone(), read_only);
2357 tracing::info!(
2358 "Created virtual buffer '{}' with mode '{}' (id={:?})",
2359 name,
2360 mode,
2361 buffer_id
2362 );
2363
2364 if let Some(state) = self
2371 .windows
2372 .get_mut(&self.active_window)
2373 .map(|w| &mut w.buffers)
2374 .expect("active window present")
2375 .get_mut(&buffer_id)
2376 {
2377 state.margins.configure_for_line_numbers(show_line_numbers);
2378 state.show_cursors = show_cursors;
2379 state.editing_disabled = editing_disabled;
2380 tracing::debug!(
2381 "Set buffer {:?} view options: show_line_numbers={}, show_cursors={}, editing_disabled={}",
2382 buffer_id,
2383 show_line_numbers,
2384 show_cursors,
2385 editing_disabled
2386 );
2387 }
2388 let active_split = self
2389 .windows
2390 .get(&self.active_window)
2391 .and_then(|w| w.buffers.splits())
2392 .map(|(mgr, _)| mgr)
2393 .expect("active window must have a populated split layout")
2394 .active_split();
2395 if let Some(view_state) = self
2396 .windows
2397 .get_mut(&self.active_window)
2398 .and_then(|w| w.split_view_states_mut())
2399 .expect("active window must have a populated split layout")
2400 .get_mut(&active_split)
2401 {
2402 view_state.ensure_buffer_state(buffer_id).show_line_numbers = show_line_numbers;
2403 }
2404
2405 if hidden_from_tabs {
2407 if let Some(meta) = self.active_window_mut().buffer_metadata.get_mut(&buffer_id) {
2408 meta.hidden_from_tabs = true;
2409 }
2410 }
2411
2412 match self.set_virtual_buffer_content(buffer_id, entries) {
2414 Ok(()) => {
2415 tracing::debug!("Set virtual buffer content for {:?}", buffer_id);
2416 self.set_active_buffer(buffer_id);
2418 tracing::debug!("Switched to virtual buffer {:?}", buffer_id);
2419
2420 if let Some(req_id) = request_id {
2422 tracing::info!(
2423 "CreateVirtualBufferWithContent: resolving callback for request_id={}, buffer_id={:?}",
2424 req_id,
2425 buffer_id
2426 );
2427 let result = fresh_core::api::VirtualBufferResult {
2429 buffer_id: buffer_id.0 as u64,
2430 split_id: None,
2431 };
2432 self.plugin_manager.read().unwrap().resolve_callback(
2433 fresh_core::api::JsCallbackId::from(req_id),
2434 serde_json::to_string(&result).unwrap_or_default(),
2435 );
2436 tracing::info!(
2437 "CreateVirtualBufferWithContent: resolve_callback sent for request_id={}",
2438 req_id
2439 );
2440 }
2441 }
2442 Err(e) => {
2443 tracing::error!("Failed to set virtual buffer content: {}", e);
2444 }
2445 }
2446 }
2447
2448 fn handle_create_virtual_buffer_in_split(
2449 &mut self,
2450 name: String,
2451 mode: String,
2452 read_only: bool,
2453 entries: Vec<fresh_core::text_property::TextPropertyEntry>,
2454 ratio: f32,
2455 direction: Option<String>,
2456 panel_id: Option<String>,
2457 show_line_numbers: bool,
2458 show_cursors: bool,
2459 editing_disabled: bool,
2460 line_wrap: Option<bool>,
2461 before: bool,
2462 role: Option<String>,
2463 request_id: Option<u64>,
2464 ) {
2465 let split_role: Option<crate::view::split::SplitRole> = match role.as_deref() {
2468 Some("utility_dock") => Some(crate::view::split::SplitRole::UtilityDock),
2469 _ => None,
2470 };
2471
2472 if let Some(target_role) = split_role {
2478 if let Some(dock_leaf) = self
2479 .windows
2480 .get(&self.active_window)
2481 .and_then(|w| w.buffers.splits())
2482 .map(|(mgr, _)| mgr)
2483 .expect("active window must have a populated split layout")
2484 .find_leaf_by_role(target_role)
2485 {
2486 let source_split_before_create = self
2491 .windows
2492 .get(&self.active_window)
2493 .and_then(|w| w.buffers.splits())
2494 .map(|(mgr, _)| mgr)
2495 .expect("active window must have a populated split layout")
2496 .active_split();
2497 let buffer_id = self.active_window_mut().create_virtual_buffer(
2498 name.clone(),
2499 mode.clone(),
2500 read_only,
2501 );
2502 if let Some(state) = self
2503 .windows
2504 .get_mut(&self.active_window)
2505 .map(|w| &mut w.buffers)
2506 .expect("active window present")
2507 .get_mut(&buffer_id)
2508 {
2509 state.margins.configure_for_line_numbers(show_line_numbers);
2510 state.show_cursors = show_cursors;
2511 state.editing_disabled = editing_disabled;
2512 }
2513 if let Some(pid) = &panel_id {
2514 self.panel_ids_mut().insert(pid.clone(), buffer_id);
2515 }
2516 if let Err(e) = self.set_virtual_buffer_content(buffer_id, entries) {
2517 tracing::error!("Failed to set virtual buffer content (dock route): {}", e);
2518 return;
2519 }
2520
2521 self.windows
2525 .get_mut(&self.active_window)
2526 .and_then(|w| w.split_manager_mut())
2527 .expect("active window must have a populated split layout")
2528 .set_active_split(dock_leaf);
2529 self.active_window_mut()
2530 .set_pane_buffer(dock_leaf, buffer_id);
2531
2532 if dock_leaf != source_split_before_create {
2534 if let Some(source_view_state) = self
2535 .windows
2536 .get_mut(&self.active_window)
2537 .and_then(|w| w.split_view_states_mut())
2538 .expect("active window must have a populated split layout")
2539 .get_mut(&source_split_before_create)
2540 {
2541 source_view_state.remove_buffer(buffer_id);
2542 }
2543 }
2544
2545 if let Some(req_id) = request_id {
2546 let result = fresh_core::api::VirtualBufferResult {
2547 buffer_id: buffer_id.0 as u64,
2548 split_id: Some(dock_leaf.0 .0 as u64),
2549 };
2550 self.plugin_manager.read().unwrap().resolve_callback(
2551 fresh_core::api::JsCallbackId::from(req_id),
2552 serde_json::to_string(&result).unwrap_or_default(),
2553 );
2554 }
2555 tracing::info!(
2556 "Routed virtual buffer '{}' into existing utility dock {:?}",
2557 name,
2558 dock_leaf
2559 );
2560 return;
2561 }
2562 }
2565
2566 if let Some(pid) = &panel_id {
2568 if let Some(&existing_buffer_id) = self.panel_ids().get(pid) {
2569 if self
2571 .windows
2572 .get(&self.active_window)
2573 .map(|w| &w.buffers)
2574 .expect("active window present")
2575 .contains_key(&existing_buffer_id)
2576 {
2577 if let Err(e) = self.set_virtual_buffer_content(existing_buffer_id, entries) {
2579 tracing::error!("Failed to update panel content: {}", e);
2580 } else {
2581 tracing::info!("Updated existing panel '{}' content", pid);
2582 }
2583
2584 let splits = self
2586 .windows
2587 .get(&self.active_window)
2588 .and_then(|w| w.buffers.splits())
2589 .map(|(mgr, _)| mgr)
2590 .expect("active window must have a populated split layout")
2591 .splits_for_buffer(existing_buffer_id);
2592 if let Some(&split_id) = splits.first() {
2593 self.windows
2594 .get_mut(&self.active_window)
2595 .and_then(|w| w.split_manager_mut())
2596 .expect("active window must have a populated split layout")
2597 .set_active_split(split_id);
2598 self.active_window_mut()
2601 .set_pane_buffer(split_id, existing_buffer_id);
2602 tracing::debug!("Focused split {:?} containing panel buffer", split_id);
2603 }
2604
2605 if let Some(req_id) = request_id {
2607 let result = fresh_core::api::VirtualBufferResult {
2608 buffer_id: existing_buffer_id.0 as u64,
2609 split_id: splits.first().map(|s| s.0 .0 as u64),
2610 };
2611 self.plugin_manager.read().unwrap().resolve_callback(
2612 fresh_core::api::JsCallbackId::from(req_id),
2613 serde_json::to_string(&result).unwrap_or_default(),
2614 );
2615 }
2616 return;
2617 } else {
2618 tracing::warn!(
2620 "Removing stale panel_id '{}' pointing to non-existent buffer {:?}",
2621 pid,
2622 existing_buffer_id
2623 );
2624 self.panel_ids_mut().remove(pid);
2625 }
2627 }
2628 }
2629
2630 let source_split_before_create = self
2636 .windows
2637 .get(&self.active_window)
2638 .and_then(|w| w.buffers.splits())
2639 .map(|(mgr, _)| mgr)
2640 .expect("active window must have a populated split layout")
2641 .active_split();
2642
2643 let buffer_id =
2645 self.active_window_mut()
2646 .create_virtual_buffer(name.clone(), mode.clone(), read_only);
2647 tracing::info!(
2648 "Created virtual buffer '{}' with mode '{}' in split (id={:?})",
2649 name,
2650 mode,
2651 buffer_id
2652 );
2653
2654 if let Some(state) = self
2656 .windows
2657 .get_mut(&self.active_window)
2658 .map(|w| &mut w.buffers)
2659 .expect("active window present")
2660 .get_mut(&buffer_id)
2661 {
2662 state.margins.configure_for_line_numbers(show_line_numbers);
2663 state.show_cursors = show_cursors;
2664 state.editing_disabled = editing_disabled;
2665 tracing::debug!(
2666 "Set buffer {:?} view options: show_line_numbers={}, show_cursors={}, editing_disabled={}",
2667 buffer_id,
2668 show_line_numbers,
2669 show_cursors,
2670 editing_disabled
2671 );
2672 }
2673
2674 if let Some(pid) = panel_id {
2676 self.panel_ids_mut().insert(pid, buffer_id);
2677 }
2678
2679 if let Err(e) = self.set_virtual_buffer_content(buffer_id, entries) {
2681 tracing::error!("Failed to set virtual buffer content: {}", e);
2682 return;
2683 }
2684
2685 let split_dir = match direction.as_deref() {
2687 Some("vertical") => crate::model::event::SplitDirection::Vertical,
2688 _ => crate::model::event::SplitDirection::Horizontal,
2689 };
2690
2691 let created_split_id =
2697 match if split_role == Some(crate::view::split::SplitRole::UtilityDock) {
2698 self.windows
2699 .get_mut(&self.active_window)
2700 .and_then(|w| w.split_manager_mut())
2701 .expect("active window must have a populated split layout")
2702 .split_root_positioned(split_dir, buffer_id, ratio, before)
2703 } else {
2704 self.windows
2705 .get_mut(&self.active_window)
2706 .and_then(|w| w.split_manager_mut())
2707 .expect("active window must have a populated split layout")
2708 .split_active_positioned(split_dir, buffer_id, ratio, before)
2709 } {
2710 Ok(new_split_id) => {
2711 if new_split_id != source_split_before_create {
2717 if let Some(source_view_state) = self
2718 .windows
2719 .get_mut(&self.active_window)
2720 .and_then(|w| w.split_view_states_mut())
2721 .expect("active window must have a populated split layout")
2722 .get_mut(&source_split_before_create)
2723 {
2724 source_view_state.remove_buffer(buffer_id);
2725 }
2726 }
2727 let mut view_state = SplitViewState::with_buffer(
2729 self.terminal_width,
2730 self.terminal_height,
2731 buffer_id,
2732 );
2733 view_state.apply_config_defaults(
2734 self.config.editor.line_numbers,
2735 self.config.editor.highlight_current_line,
2736 line_wrap.unwrap_or_else(|| {
2737 self.active_window().resolve_line_wrap_for_buffer(buffer_id)
2738 }),
2739 self.config.editor.wrap_indent,
2740 self.active_window()
2741 .resolve_wrap_column_for_buffer(buffer_id),
2742 self.config.editor.rulers.clone(),
2743 );
2744 view_state.ensure_buffer_state(buffer_id).show_line_numbers = show_line_numbers;
2746 self.windows
2747 .get_mut(&self.active_window)
2748 .and_then(|w| w.split_view_states_mut())
2749 .expect("active window must have a populated split layout")
2750 .insert(new_split_id, view_state);
2751
2752 self.windows
2754 .get_mut(&self.active_window)
2755 .and_then(|w| w.split_manager_mut())
2756 .expect("active window must have a populated split layout")
2757 .set_active_split(new_split_id);
2758 if let Some(target_role) = split_role {
2766 self.windows
2767 .get_mut(&self.active_window)
2768 .and_then(|w| w.split_manager_mut())
2769 .expect("active window must have a populated split layout")
2770 .clear_role(target_role);
2771 self.windows
2772 .get_mut(&self.active_window)
2773 .and_then(|w| w.split_manager_mut())
2774 .expect("active window must have a populated split layout")
2775 .set_leaf_role(new_split_id, Some(target_role));
2776 tracing::info!(
2777 "Tagged new dock leaf {:?} with role {:?}",
2778 new_split_id,
2779 target_role
2780 );
2781 }
2782
2783 tracing::info!(
2784 "Created {:?} split with virtual buffer {:?}",
2785 split_dir,
2786 buffer_id
2787 );
2788 Some(new_split_id)
2789 }
2790 Err(e) => {
2791 tracing::error!("Failed to create split: {}", e);
2792 self.set_active_buffer(buffer_id);
2794 None
2795 }
2796 };
2797
2798 if let Some(req_id) = request_id {
2801 tracing::trace!("CreateVirtualBufferInSplit: resolving callback for request_id={}, buffer_id={:?}, split_id={:?}", req_id, buffer_id, created_split_id);
2802 let result = fresh_core::api::VirtualBufferResult {
2803 buffer_id: buffer_id.0 as u64,
2804 split_id: created_split_id.map(|s| s.0 .0 as u64),
2805 };
2806 self.plugin_manager.read().unwrap().resolve_callback(
2807 fresh_core::api::JsCallbackId::from(req_id),
2808 serde_json::to_string(&result).unwrap_or_default(),
2809 );
2810 }
2811 }
2812
2813 fn handle_create_virtual_buffer_in_existing_split(
2814 &mut self,
2815 name: String,
2816 mode: String,
2817 read_only: bool,
2818 entries: Vec<fresh_core::text_property::TextPropertyEntry>,
2819 split_id: SplitId,
2820 show_line_numbers: bool,
2821 show_cursors: bool,
2822 editing_disabled: bool,
2823 line_wrap: Option<bool>,
2824 request_id: Option<u64>,
2825 ) {
2826 let buffer_id =
2828 self.active_window_mut()
2829 .create_virtual_buffer(name.clone(), mode.clone(), read_only);
2830 tracing::info!(
2831 "Created virtual buffer '{}' with mode '{}' for existing split {:?} (id={:?})",
2832 name,
2833 mode,
2834 split_id,
2835 buffer_id
2836 );
2837
2838 if let Some(state) = self
2840 .windows
2841 .get_mut(&self.active_window)
2842 .map(|w| &mut w.buffers)
2843 .expect("active window present")
2844 .get_mut(&buffer_id)
2845 {
2846 state.margins.configure_for_line_numbers(show_line_numbers);
2847 state.show_cursors = show_cursors;
2848 state.editing_disabled = editing_disabled;
2849 }
2850
2851 if let Err(e) = self.set_virtual_buffer_content(buffer_id, entries) {
2853 tracing::error!("Failed to set virtual buffer content: {}", e);
2854 return;
2855 }
2856
2857 let leaf_id = LeafId(split_id);
2860 self.windows
2861 .get_mut(&self.active_window)
2862 .and_then(|w| w.split_manager_mut())
2863 .expect("active window must have a populated split layout")
2864 .set_active_split(leaf_id);
2865 self.active_window_mut().set_pane_buffer(leaf_id, buffer_id);
2866
2867 if let Some(view_state) = self
2873 .windows
2874 .get_mut(&self.active_window)
2875 .and_then(|w| w.split_view_states_mut())
2876 .expect("active window must have a populated split layout")
2877 .get_mut(&leaf_id)
2878 {
2879 view_state.switch_buffer(buffer_id);
2880 view_state.add_buffer(buffer_id);
2881 view_state.ensure_buffer_state(buffer_id).show_line_numbers = show_line_numbers;
2882
2883 if let Some(wrap) = line_wrap {
2885 view_state.active_state_mut().viewport.line_wrap_enabled = wrap;
2886 }
2887 }
2888
2889 tracing::info!(
2890 "Displayed virtual buffer {:?} in split {:?}",
2891 buffer_id,
2892 split_id
2893 );
2894
2895 if let Some(req_id) = request_id {
2897 let result = fresh_core::api::VirtualBufferResult {
2898 buffer_id: buffer_id.0 as u64,
2899 split_id: Some(split_id.0 as u64),
2900 };
2901 self.plugin_manager.read().unwrap().resolve_callback(
2902 fresh_core::api::JsCallbackId::from(req_id),
2903 serde_json::to_string(&result).unwrap_or_default(),
2904 );
2905 }
2906 }
2907
2908 fn handle_show_action_popup(
2909 &mut self,
2910 popup_id: String,
2911 title: String,
2912 message: String,
2913 actions: Vec<fresh_core::api::ActionPopupAction>,
2914 ) {
2915 tracing::info!(
2916 "Action popup requested: id={}, title={}, actions={}",
2917 popup_id,
2918 title,
2919 actions.len()
2920 );
2921
2922 let items: Vec<crate::model::event::PopupListItemData> = actions
2924 .iter()
2925 .map(|action| crate::model::event::PopupListItemData {
2926 text: action.label.clone(),
2927 detail: None,
2928 icon: None,
2929 data: Some(action.id.clone()),
2930 })
2931 .collect();
2932
2933 drop(actions);
2938
2939 let popup_data = crate::model::event::PopupData {
2941 kind: crate::model::event::PopupKindHint::List,
2942 title: Some(title),
2943 description: Some(message),
2944 transient: false,
2945 content: crate::model::event::PopupContentData::List { items, selected: 0 },
2946 position: crate::model::event::PopupPositionData::BottomRight,
2947 width: 60,
2948 max_height: 15,
2949 bordered: true,
2950 };
2951
2952 let mut popup_obj = crate::state::convert_popup_data_to_popup(&popup_data);
2962 popup_obj.resolver = crate::view::popup::PopupResolver::PluginAction {
2963 popup_id: popup_id.clone(),
2964 };
2965
2966 {
2973 let theme = self.theme();
2974 popup_obj.background_style = ratatui::style::Style::default().bg(theme.popup_bg);
2975 popup_obj.border_style = ratatui::style::Style::default().fg(theme.popup_border_fg);
2976 }
2977
2978 while self
2990 .active_state()
2991 .popups
2992 .top()
2993 .is_some_and(|p| matches!(p.resolver, crate::view::popup::PopupResolver::LspStatus))
2994 {
2995 self.active_state_mut().popups.hide();
2996 }
2997
2998 let existing_idx = self.global_popups.all().iter().position(|p| {
3005 matches!(
3006 &p.resolver,
3007 crate::view::popup::PopupResolver::PluginAction { popup_id: id } if id == &popup_id,
3008 )
3009 });
3010 if let Some(idx) = existing_idx {
3011 if let Some(slot) = self.global_popups.get_mut(idx) {
3012 *slot = popup_obj;
3013 }
3014 } else {
3015 self.global_popups.show(popup_obj);
3016 }
3017 tracing::info!(
3018 "Action popup shown: id={}, stack_depth={}",
3019 popup_id,
3020 self.global_popups.all().len()
3021 );
3022 }
3023
3024 fn handle_set_lsp_menu_contributions(
3034 &mut self,
3035 plugin_id: String,
3036 language: String,
3037 items: Vec<fresh_core::api::LspMenuItem>,
3038 ) {
3039 let key = (language.clone(), plugin_id.clone());
3040 if items.is_empty() {
3041 self.active_window_mut().lsp_menu_contributions.remove(&key);
3042 } else {
3043 self.active_window_mut()
3044 .lsp_menu_contributions
3045 .insert(key, items);
3046 }
3047 self.refresh_lsp_status_popup_if_open();
3052 }
3053
3054 fn handle_create_window_with_terminal(
3055 &mut self,
3056 root: std::path::PathBuf,
3057 label: String,
3058 cwd: Option<String>,
3059 command: Option<Vec<String>>,
3060 title: Option<String>,
3061 request_id: u64,
3062 ) {
3063 let callback_id = JsCallbackId::from(request_id);
3064 if !root.is_absolute() {
3065 let msg = format!(
3066 "createWindowWithTerminal: root must be absolute, got {:?}",
3067 root
3068 );
3069 tracing::warn!("{}", msg);
3070 self.plugin_manager
3071 .read()
3072 .unwrap()
3073 .reject_callback(callback_id, msg);
3074 return;
3075 }
3076 let cwd_buf = cwd.map(std::path::PathBuf::from);
3077 match self.create_window_with_terminal(root, label, cwd_buf, command, title) {
3078 Ok((window_id, terminal_id, buffer_id)) => {
3079 let api_result = fresh_core::api::SessionWithTerminalResult {
3080 window_id: window_id.0,
3081 terminal_id: terminal_id.0 as u64,
3082 buffer_id: buffer_id.0 as u64,
3083 };
3084 self.plugin_manager.read().unwrap().resolve_callback(
3085 callback_id,
3086 serde_json::to_string(&api_result).unwrap_or_default(),
3087 );
3088 }
3089 Err(e) => {
3090 tracing::error!("createWindowWithTerminal failed: {e}");
3091 self.plugin_manager
3092 .read()
3093 .unwrap()
3094 .reject_callback(callback_id, format!("createWindowWithTerminal: {e}"));
3095 }
3096 }
3097 }
3098
3099 fn handle_create_terminal(
3100 &mut self,
3101 cwd: Option<String>,
3102 direction: Option<String>,
3103 ratio: Option<f32>,
3104 focus: Option<bool>,
3105 persistent: bool,
3106 target_session_id: Option<fresh_core::WindowId>,
3107 command: Option<Vec<String>>,
3108 title: Option<String>,
3109 request_id: u64,
3110 ) {
3111 let target_id = target_session_id
3118 .filter(|id| self.windows.contains_key(id))
3119 .unwrap_or(self.active_window);
3120 let is_active_target = target_id == self.active_window;
3121
3122 let cwd_buf = cwd.map(std::path::PathBuf::from);
3123 let split_direction = direction.as_deref().map(|d| match d {
3124 "horizontal" => crate::model::event::SplitDirection::Horizontal,
3125 _ => crate::model::event::SplitDirection::Vertical,
3126 });
3127
3128 let prev_active = if is_active_target {
3136 Some(self.active_window().active_buffer())
3137 } else {
3138 None
3139 };
3140
3141 let result = {
3142 let target = self
3143 .windows
3144 .get_mut(&target_id)
3145 .expect("target window present (existence checked above)");
3146 target.create_plugin_terminal(
3147 cwd_buf,
3148 split_direction,
3149 ratio,
3150 focus.unwrap_or(true),
3151 persistent,
3152 command,
3153 title.filter(|t| !t.is_empty()),
3154 )
3155 };
3156 match result {
3157 Ok((terminal_id, buffer_id, created_split_id)) => {
3158 if is_active_target {
3159 let new_active = self.active_window().active_buffer();
3160 if prev_active != Some(new_active) {
3161 #[cfg(feature = "plugins")]
3162 self.update_plugin_state_snapshot();
3163 #[cfg(feature = "plugins")]
3164 self.plugin_manager.read().unwrap().run_hook(
3165 "buffer_activated",
3166 crate::services::plugins::hooks::HookArgs::BufferActivated {
3167 buffer_id: new_active,
3168 },
3169 );
3170 }
3171 }
3172 let api_result = fresh_core::api::TerminalResult {
3173 buffer_id: buffer_id.0 as u64,
3174 terminal_id: terminal_id.0 as u64,
3175 split_id: created_split_id.map(|s| s.0 .0 as u64),
3176 };
3177 self.plugin_manager.read().unwrap().resolve_callback(
3178 fresh_core::api::JsCallbackId::from(request_id),
3179 serde_json::to_string(&api_result).unwrap_or_default(),
3180 );
3181 tracing::info!(
3182 "Plugin created terminal {:?} with buffer {:?} in window {:?}",
3183 terminal_id,
3184 buffer_id,
3185 target_id
3186 );
3187 }
3188 Err(e) => {
3189 tracing::error!("Failed to create terminal for plugin: {e}");
3190 self.plugin_manager.read().unwrap().reject_callback(
3191 fresh_core::api::JsCallbackId::from(request_id),
3192 format!("Failed to create terminal: {e}"),
3193 );
3194 }
3195 }
3196 }
3197
3198 fn handle_get_split_by_label(&mut self, label: String, request_id: u64) {
3201 let split_id = self
3202 .windows
3203 .get(&self.active_window)
3204 .and_then(|w| w.buffers.splits())
3205 .map(|(mgr, _)| mgr)
3206 .expect("active window must have a populated split layout")
3207 .find_split_by_label(&label);
3208 let callback_id = fresh_core::api::JsCallbackId::from(request_id);
3209 let json =
3210 serde_json::to_string(&split_id.map(|s| s.0 .0)).unwrap_or_else(|_| "null".to_string());
3211 self.plugin_manager
3212 .read()
3213 .unwrap()
3214 .resolve_callback(callback_id, json);
3215 }
3216
3217 fn handle_set_buffer_show_cursors(&mut self, buffer_id: BufferId, show: bool) {
3218 if let Some(state) = self
3219 .windows
3220 .get_mut(&self.active_window)
3221 .map(|w| &mut w.buffers)
3222 .expect("active window present")
3223 .get_mut(&buffer_id)
3224 {
3225 state.show_cursors = show;
3226 state.cursor_visibility_locked = true;
3229 } else {
3230 tracing::warn!("SetBufferShowCursors: buffer {:?} not found", buffer_id);
3231 }
3232 }
3233
3234 fn handle_override_theme_colors(
3235 &mut self,
3236 overrides: std::collections::HashMap<String, [u8; 3]>,
3237 ) {
3238 let pairs = overrides
3239 .into_iter()
3240 .map(|(k, [r, g, b])| (k, ratatui::style::Color::Rgb(r, g, b)));
3241 let applied = self.theme.write().unwrap().override_colors(pairs);
3242 if applied > 0 {
3243 self.reapply_all_overlays();
3246 }
3247 }
3248
3249 fn handle_await_next_key(&mut self, callback_id: fresh_core::api::JsCallbackId) {
3250 if let Some(payload) = self
3254 .active_window_mut()
3255 .pending_key_capture_buffer
3256 .pop_front()
3257 {
3258 let json = serde_json::to_string(&payload).unwrap_or_else(|_| "null".to_string());
3259 self.plugin_manager
3260 .read()
3261 .unwrap()
3262 .resolve_callback(callback_id, json);
3263 } else {
3264 self.active_window_mut()
3265 .pending_next_key_callbacks
3266 .push_back(callback_id);
3267 }
3268 }
3269
3270 fn handle_spawn_process(
3271 &mut self,
3272 command: String,
3273 args: Vec<String>,
3274 cwd: Option<String>,
3275 stdout_to: Option<std::path::PathBuf>,
3276 callback_id: fresh_core::api::JsCallbackId,
3277 ) {
3278 if let (Some(runtime), Some(bridge)) = (&self.tokio_runtime, &self.async_bridge) {
3279 let effective_cwd = cwd.or_else(|| {
3280 std::env::current_dir()
3281 .map(|p| p.to_string_lossy().to_string())
3282 .ok()
3283 });
3284 let sender = bridge.sender();
3285 let spawner = self.authority.process_spawner.clone();
3286
3287 let process_id = callback_id.as_u64();
3292 let (kill_tx, kill_rx) = tokio::sync::oneshot::channel::<()>();
3293 self.host_process_handles.insert(process_id, kill_tx);
3294
3295 runtime.spawn(async move {
3296 #[allow(clippy::let_underscore_must_use)]
3297 let outcome = spawner
3298 .spawn_cancellable(command, args, effective_cwd, stdout_to, kill_rx)
3299 .await;
3300 match outcome {
3301 Ok(result) => {
3302 #[allow(clippy::let_underscore_must_use)]
3303 let _ = sender.send(AsyncMessage::PluginProcessOutput {
3304 process_id,
3305 stdout: result.stdout,
3306 stderr: result.stderr,
3307 exit_code: result.exit_code,
3308 });
3309 }
3310 Err(e) => {
3311 #[allow(clippy::let_underscore_must_use)]
3312 let _ = sender.send(AsyncMessage::PluginProcessOutput {
3313 process_id,
3314 stdout: String::new(),
3315 stderr: e.to_string(),
3316 exit_code: -1,
3317 });
3318 }
3319 }
3320 });
3321 } else {
3322 self.plugin_manager
3323 .read()
3324 .unwrap()
3325 .reject_callback(callback_id, "Async runtime not available".to_string());
3326 }
3327 }
3328
3329 fn handle_kill_host_process(&mut self, process_id: u64) {
3330 if let Some(tx) = self.host_process_handles.remove(&process_id) {
3334 #[allow(clippy::let_underscore_must_use)]
3335 let _ = tx.send(());
3336 tracing::debug!("KillHostProcess: sent kill for process_id={}", process_id);
3337 } else {
3338 tracing::debug!(
3339 "KillHostProcess: unknown process_id={} (already exited?)",
3340 process_id
3341 );
3342 }
3343 }
3344
3345 fn handle_set_authority(&mut self, payload: serde_json::Value) {
3346 match serde_json::from_value::<crate::services::authority::AuthorityPayload>(payload) {
3349 Ok(parsed) => {
3350 let trust = std::sync::Arc::clone(&self.authority.workspace_trust);
3353 let env = std::sync::Arc::clone(&self.authority.env_provider);
3354 match crate::services::authority::Authority::from_plugin_payload(parsed, trust, env)
3355 {
3356 Ok(auth) => {
3357 tracing::info!("Plugin installed new authority");
3358 self.install_authority(auth);
3359 }
3360 Err(e) => {
3361 tracing::warn!("setAuthority: invalid payload: {}", e);
3362 self.set_status_message(format!("setAuthority rejected: {}", e));
3363 }
3364 }
3365 }
3366 Err(e) => {
3367 tracing::warn!("setAuthority: failed to parse payload: {}", e);
3368 self.set_status_message(format!("setAuthority rejected: {}", e));
3369 }
3370 }
3371 }
3372
3373 fn handle_set_remote_indicator_state(&mut self, state: serde_json::Value) {
3374 match serde_json::from_value::<crate::view::ui::status_bar::RemoteIndicatorOverride>(state)
3377 {
3378 Ok(over) => {
3379 self.remote_indicator_override = Some(over);
3380 }
3381 Err(e) => {
3382 tracing::warn!("setRemoteIndicatorState: invalid payload: {}", e);
3383 self.set_status_message(format!("setRemoteIndicatorState rejected: {}", e));
3384 }
3385 }
3386 }
3387
3388 fn handle_spawn_process_wait(
3389 &mut self,
3390 process_id: u64,
3391 callback_id: fresh_core::api::JsCallbackId,
3392 ) {
3393 tracing::warn!(
3394 "SpawnProcessWait not fully implemented - process_id={}",
3395 process_id
3396 );
3397 self.plugin_manager.read().unwrap().reject_callback(
3398 callback_id,
3399 format!(
3400 "SpawnProcessWait not yet fully implemented for process_id={}",
3401 process_id
3402 ),
3403 );
3404 }
3405
3406 fn handle_delay(&mut self, callback_id: fresh_core::api::JsCallbackId, duration_ms: u64) {
3407 if let (Some(runtime), Some(bridge)) = (&self.tokio_runtime, &self.async_bridge) {
3408 let sender = bridge.sender();
3409 let callback_id_u64 = callback_id.as_u64();
3410 runtime.spawn(async move {
3411 tokio::time::sleep(tokio::time::Duration::from_millis(duration_ms)).await;
3412 #[allow(clippy::let_underscore_must_use)]
3413 let _ = sender.send(crate::services::async_bridge::AsyncMessage::Plugin(
3414 fresh_core::api::PluginAsyncMessage::DelayComplete {
3415 callback_id: callback_id_u64,
3416 },
3417 ));
3418 });
3419 } else {
3420 std::thread::sleep(std::time::Duration::from_millis(duration_ms));
3421 self.plugin_manager
3422 .read()
3423 .unwrap()
3424 .resolve_callback(callback_id, "null".to_string());
3425 }
3426 }
3427
3428 fn handle_http_fetch(
3429 &mut self,
3430 url: String,
3431 target_path: std::path::PathBuf,
3432 callback_id: fresh_core::api::JsCallbackId,
3433 ) {
3434 if let (Some(runtime), Some(bridge)) = (&self.tokio_runtime, &self.async_bridge) {
3435 let sender = bridge.sender();
3436 let process_id = callback_id.as_u64();
3437
3438 runtime.spawn(async move {
3439 let fetch =
3440 tokio::task::spawn_blocking(move || fetch_url_to_file(&url, &target_path))
3441 .await;
3442
3443 let (stdout, stderr, exit_code) = match fetch {
3444 Ok(Ok(status)) => {
3445 if (200..300).contains(&status) {
3446 (String::new(), String::new(), 0)
3447 } else {
3448 (String::new(), format!("HTTP {}", status), i32::from(status))
3449 }
3450 }
3451 Ok(Err(e)) => (String::new(), e, -1),
3452 Err(e) => (String::new(), format!("fetch task failed: {}", e), -1),
3453 };
3454
3455 #[allow(clippy::let_underscore_must_use)]
3456 let _ = sender.send(AsyncMessage::PluginProcessOutput {
3457 process_id,
3458 stdout,
3459 stderr,
3460 exit_code,
3461 });
3462 });
3463 } else {
3464 self.plugin_manager
3465 .read()
3466 .unwrap()
3467 .reject_callback(callback_id, "Async runtime not available".to_string());
3468 }
3469 }
3470
3471 fn handle_kill_background_process(&mut self, process_id: u64) {
3472 if let Some(handle) = self.background_process_handles.remove(&process_id) {
3473 handle.abort();
3474 tracing::debug!("Killed background process {}", process_id);
3475 }
3476 }
3477
3478 fn handle_create_virtual_buffer(&mut self, name: String, mode: String, read_only: bool) {
3479 let buffer_id =
3480 self.active_window_mut()
3481 .create_virtual_buffer(name.clone(), mode.clone(), read_only);
3482 tracing::info!(
3483 "Created virtual buffer '{}' with mode '{}' (id={:?})",
3484 name,
3485 mode,
3486 buffer_id
3487 );
3488 }
3490
3491 fn handle_set_virtual_buffer_content(
3492 &mut self,
3493 buffer_id: BufferId,
3494 entries: Vec<fresh_core::text_property::TextPropertyEntry>,
3495 ) {
3496 match self.set_virtual_buffer_content(buffer_id, entries) {
3497 Ok(()) => {
3498 tracing::debug!("Set virtual buffer content for {:?}", buffer_id);
3499 }
3500 Err(e) => {
3501 tracing::error!("Failed to set virtual buffer content: {}", e);
3502 }
3503 }
3504 }
3505
3506 fn handle_mount_widget_panel(
3507 &mut self,
3508 panel_id: u64,
3509 buffer_id: BufferId,
3510 spec: fresh_core::api::WidgetSpec,
3511 ) {
3512 let prev = std::collections::HashMap::new();
3517 let prev_focus = String::new();
3518 let panel_width = self.widget_panel_width(buffer_id);
3519 let out = crate::widgets::render_spec(&spec, &prev, &prev_focus, panel_width);
3520 let focus_cursor = out.focus_cursor;
3521 self.widget_registry.mount(
3522 panel_id,
3523 buffer_id,
3524 spec,
3525 out.hits,
3526 out.instance_states,
3527 out.focus_key,
3528 out.tabbable,
3529 );
3530 let entries = out.entries;
3531 if let Err(e) = self.set_virtual_buffer_content(buffer_id, entries.clone()) {
3532 tracing::error!(
3533 "Failed to render mounted widget panel {} into {:?}: {}",
3534 panel_id,
3535 buffer_id,
3536 e
3537 );
3538 } else {
3539 tracing::debug!(
3540 "Mounted widget panel {} into buffer {:?}",
3541 panel_id,
3542 buffer_id
3543 );
3544 }
3545 self.apply_widget_focus_cursor(buffer_id, &entries, focus_cursor);
3546 }
3547
3548 fn handle_update_widget_panel(&mut self, panel_id: u64, spec: fresh_core::api::WidgetSpec) {
3549 let prev = match self.widget_registry.instance_states(panel_id) {
3550 Some(s) => s.clone(),
3551 None => {
3552 tracing::debug!(
3553 "UpdateWidgetPanel for unknown panel {} ignored (not mounted)",
3554 panel_id
3555 );
3556 return;
3557 }
3558 };
3559 let prev_focus = self
3560 .widget_registry
3561 .focus_key(panel_id)
3562 .map(|s| s.to_string())
3563 .unwrap_or_default();
3564 let buffer_id_for_width = self
3565 .widget_registry
3566 .buffer_and_spec(panel_id)
3567 .map(|(b, _)| b)
3568 .unwrap_or(BufferId(0));
3569 let panel_width = self.widget_panel_width(buffer_id_for_width);
3570 let out = crate::widgets::render_spec(&spec, &prev, &prev_focus, panel_width);
3571 let focus_cursor = out.focus_cursor;
3572 let entries = out.entries;
3573 match self.widget_registry.update(
3574 panel_id,
3575 spec,
3576 out.hits,
3577 out.instance_states,
3578 out.focus_key,
3579 out.tabbable,
3580 ) {
3581 Ok(buffer_id) => {
3582 if let Err(e) = self.set_virtual_buffer_content(buffer_id, entries.clone()) {
3583 tracing::error!("Failed to render updated widget panel {}: {}", panel_id, e);
3584 }
3585 self.apply_widget_focus_cursor(buffer_id, &entries, focus_cursor);
3586 }
3587 Err(()) => {
3588 tracing::debug!(
3589 "UpdateWidgetPanel for unknown panel {} ignored (not mounted)",
3590 panel_id
3591 );
3592 }
3593 }
3594 }
3595
3596 fn apply_widget_focus_cursor(
3607 &mut self,
3608 buffer_id: BufferId,
3609 entries: &[fresh_core::text_property::TextPropertyEntry],
3610 focus_cursor: Option<crate::widgets::FocusCursor>,
3611 ) {
3612 let locked = self
3618 .windows
3619 .get(&self.active_window)
3620 .and_then(|w| w.buffers.get(&buffer_id))
3621 .map(|s| s.cursor_visibility_locked)
3622 .unwrap_or(false);
3623 if locked {
3624 return;
3625 }
3626
3627 let absolute_byte = focus_cursor.map(|fc| {
3628 let row = fc.buffer_row as usize;
3629 let prefix: usize = entries.iter().take(row).map(|e| e.text.len()).sum();
3630 prefix + fc.byte_in_row as usize
3631 });
3632
3633 if let Some(state) = self
3634 .windows
3635 .get_mut(&self.active_window)
3636 .map(|w| &mut w.buffers)
3637 .expect("active window present")
3638 .get_mut(&buffer_id)
3639 {
3640 state.show_cursors = absolute_byte.is_some();
3641 }
3642
3643 if let Some(byte) = absolute_byte {
3644 for vs in self
3645 .windows
3646 .get_mut(&self.active_window)
3647 .and_then(|w| w.split_view_states_mut())
3648 .expect("active window must have a populated split layout")
3649 .values_mut()
3650 {
3651 if vs.buffer_state(buffer_id).is_some() {
3652 let cursor = vs.cursors.primary_mut();
3653 cursor.position = byte;
3654 }
3655 }
3656 }
3657 }
3658
3659 fn widget_panel_width(&self, buffer_id: BufferId) -> u32 {
3668 let raw = self
3669 .windows
3670 .get(&self.active_window)
3671 .and_then(|w| w.buffers.splits())
3672 .map(|(_, vs)| vs)
3673 .expect("active window must have a populated split layout")
3674 .values()
3675 .find(|vs| vs.buffer_state(buffer_id).is_some() && vs.viewport.width > 0)
3676 .map(|vs| vs.viewport.width as u32)
3677 .unwrap_or_else(|| self.terminal_width.max(1) as u32);
3678 raw.saturating_sub(2).max(10)
3681 }
3682
3683 pub(super) fn rerender_widget_panel(&mut self, panel_id: u64) {
3689 let (buffer_id, is_floating, panel_width, out_pieces) = {
3698 let (buffer_id, spec) = match self.widget_registry.buffer_and_spec_ref(panel_id) {
3699 Some(s) => s,
3700 None => return,
3701 };
3702 let prev = self
3703 .widget_registry
3704 .instance_states(panel_id)
3705 .cloned()
3706 .unwrap_or_default();
3707 let prev_focus = self
3708 .widget_registry
3709 .focus_key(panel_id)
3710 .map(|s| s.to_string())
3711 .unwrap_or_default();
3712 let is_floating = buffer_id == FLOATING_PANEL_BUFFER_ID;
3713 let panel_width = if is_floating {
3714 self.floating_panel_inner_width()
3715 } else {
3716 self.widget_panel_width(buffer_id)
3717 };
3718 let out = crate::widgets::render_spec(spec, &prev, &prev_focus, panel_width);
3719 (buffer_id, is_floating, panel_width, out)
3720 };
3721 let _ = panel_width;
3722 let focus_cursor = out_pieces.focus_cursor;
3723 let entries = out_pieces.entries;
3724 let embeds = out_pieces.embeds;
3725 let overlays = out_pieces.overlays;
3726 let scroll_regions = out_pieces.scroll_regions;
3727 if self
3728 .widget_registry
3729 .update_side_effects(
3730 panel_id,
3731 out_pieces.hits,
3732 out_pieces.instance_states,
3733 out_pieces.focus_key,
3734 out_pieces.tabbable,
3735 )
3736 .is_err()
3737 {
3738 tracing::warn!("rerender_widget_panel({}) lost panel mid-call", panel_id);
3739 return;
3740 }
3741 if is_floating {
3742 if let Some(fwp) = self.floating_widget_panel.as_mut() {
3743 if fwp.panel_id == panel_id {
3744 fwp.entries = entries;
3745 fwp.focus_cursor = focus_cursor;
3746 fwp.embeds = embeds;
3747 fwp.overlays = overlays;
3748 fwp.scroll_regions = scroll_regions;
3749 }
3750 }
3751 return;
3752 }
3753 if let Err(e) = self.set_virtual_buffer_content(buffer_id, entries.clone()) {
3754 tracing::error!("rerender_widget_panel({}) failed: {}", panel_id, e);
3755 }
3756 self.apply_widget_focus_cursor(buffer_id, &entries, focus_cursor);
3757 }
3758
3759 fn handle_widget_mutate(&mut self, panel_id: u64, mutation: fresh_core::api::WidgetMutation) {
3765 use fresh_core::api::WidgetMutation;
3766
3767 if self.widget_registry.get(panel_id).is_none() {
3769 tracing::debug!(
3770 "WidgetMutate for unknown panel {} ignored (not mounted)",
3771 panel_id
3772 );
3773 return;
3774 }
3775
3776 match mutation {
3777 WidgetMutation::SetValue {
3778 widget_key,
3779 value,
3780 cursor_byte,
3781 } => {
3782 if let Some(panel) = self.widget_registry.get_mut(panel_id) {
3789 let (scroll, multiline, completions, sel_idx, scroll_off) =
3797 match panel.instance_states.get(&widget_key) {
3798 Some(crate::widgets::WidgetInstanceState::Text {
3799 editor,
3800 scroll,
3801 completions,
3802 completion_selected_index,
3803 completion_scroll_offset,
3804 }) => (
3805 *scroll,
3806 editor.multiline,
3807 completions.clone(),
3808 *completion_selected_index,
3809 *completion_scroll_offset,
3810 ),
3811 _ => (0u32, true, Vec::new(), 0usize, 0u32),
3812 };
3813 let mut editor = if multiline {
3814 crate::primitives::text_edit::TextEdit::with_text(&value)
3815 } else {
3816 crate::primitives::text_edit::TextEdit::single_line_with_text(&value)
3817 };
3818 let target = match cursor_byte {
3819 Some(c) if c >= 0 => (c as usize).min(value.len()),
3820 _ => value.len(),
3821 };
3822 editor.set_cursor_from_flat(target);
3823 panel.instance_states.insert(
3824 widget_key,
3825 crate::widgets::WidgetInstanceState::Text {
3826 editor,
3827 scroll,
3828 completions,
3829 completion_selected_index: sel_idx,
3830 completion_scroll_offset: scroll_off,
3831 },
3832 );
3833 }
3834 }
3835 WidgetMutation::SetChecked {
3836 widget_key,
3837 checked,
3838 } => {
3839 if let Some(panel) = self.widget_registry.get_mut(panel_id) {
3843 crate::widgets::set_toggle_checked_in_spec(
3844 &mut panel.spec,
3845 &widget_key,
3846 checked,
3847 );
3848 }
3849 }
3850 WidgetMutation::SetSelectedIndex { widget_key, index } => {
3851 if let Some(panel) = self.widget_registry.get_mut(panel_id) {
3853 let prev_scroll = match panel.instance_states.get(&widget_key) {
3854 Some(crate::widgets::WidgetInstanceState::List {
3855 scroll_offset, ..
3856 }) => *scroll_offset,
3857 _ => 0,
3858 };
3859 panel.instance_states.insert(
3860 widget_key,
3861 crate::widgets::WidgetInstanceState::List {
3862 scroll_offset: prev_scroll,
3863 selected_index: index,
3864 },
3865 );
3866 }
3867 }
3868 WidgetMutation::SetCompletions { widget_key, items } => {
3869 if let Some(panel) = self.widget_registry.get_mut(panel_id) {
3878 if let Some(crate::widgets::WidgetInstanceState::Text {
3879 completions,
3880 completion_selected_index,
3881 completion_scroll_offset,
3882 ..
3883 }) = panel.instance_states.get_mut(&widget_key)
3884 {
3885 *completions = items;
3886 *completion_selected_index = 0;
3887 *completion_scroll_offset = 0;
3888 }
3889 }
3890 }
3891 WidgetMutation::SetItems {
3892 widget_key,
3893 items,
3894 item_keys,
3895 } => {
3896 if let Some(panel) = self.widget_registry.get_mut(panel_id) {
3898 crate::widgets::set_list_items_in_spec(
3899 &mut panel.spec,
3900 &widget_key,
3901 items,
3902 item_keys,
3903 );
3904 }
3905 }
3906 WidgetMutation::SetExpandedKeys { widget_key, keys } => {
3907 if let Some(panel) = self.widget_registry.get_mut(panel_id) {
3909 let (prev_scroll, prev_sel) = match panel.instance_states.get(&widget_key) {
3910 Some(crate::widgets::WidgetInstanceState::Tree {
3911 scroll_offset,
3912 selected_index,
3913 ..
3914 }) => (*scroll_offset, *selected_index),
3915 _ => (0, -1),
3916 };
3917 let expanded: std::collections::HashSet<String> = keys.into_iter().collect();
3918 panel.instance_states.insert(
3919 widget_key,
3920 crate::widgets::WidgetInstanceState::Tree {
3921 scroll_offset: prev_scroll,
3922 selected_index: prev_sel,
3923 expanded_keys: expanded,
3924 },
3925 );
3926 }
3927 }
3928 WidgetMutation::SetCheckedKeys {
3929 widget_key,
3930 checked,
3931 keys,
3932 } => {
3933 if let Some(panel) = self.widget_registry.get_mut(panel_id) {
3941 crate::widgets::set_tree_checked_keys_in_spec(
3942 &mut panel.spec,
3943 &widget_key,
3944 checked,
3945 &keys,
3946 );
3947 }
3948 }
3949 WidgetMutation::AppendTreeNodes {
3950 widget_key,
3951 new_nodes,
3952 new_item_keys,
3953 } => {
3954 if let Some(panel) = self.widget_registry.get_mut(panel_id) {
3955 crate::widgets::append_tree_nodes_in_spec(
3956 &mut panel.spec,
3957 &widget_key,
3958 new_nodes,
3959 new_item_keys,
3960 );
3961 }
3962 }
3963 WidgetMutation::SetRawEntries {
3964 widget_key,
3965 entries,
3966 } => {
3967 if let Some(panel) = self.widget_registry.get_mut(panel_id) {
3968 crate::widgets::set_raw_entries_in_spec(&mut panel.spec, &widget_key, entries);
3969 }
3970 }
3971 WidgetMutation::SetFocusKey { widget_key } => {
3972 self.widget_registry.set_focus_key(panel_id, widget_key);
3977 }
3978 }
3979
3980 self.rerender_widget_panel(panel_id);
3984 }
3985
3986 pub(super) fn handle_widget_command(
3987 &mut self,
3988 panel_id: u64,
3989 action: fresh_core::api::WidgetAction,
3990 ) {
3991 use fresh_core::api::WidgetAction;
3992 match action {
3993 WidgetAction::FocusAdvance { delta } => {
3994 self.handle_widget_focus_advance(panel_id, delta);
3995 }
3996 WidgetAction::Activate => {
3997 self.handle_widget_activate(panel_id);
3998 }
3999 WidgetAction::SelectMove { delta } => {
4000 self.handle_widget_select_move(panel_id, delta);
4001 }
4002 WidgetAction::TextInputKey { key } => {
4003 self.handle_widget_text_key(panel_id, &key);
4004 }
4005 WidgetAction::TextInputChar { text } => {
4006 self.handle_widget_text_char(panel_id, &text);
4007 }
4008 WidgetAction::Key { key } => {
4009 self.handle_widget_key(panel_id, &key);
4010 }
4011 }
4012 }
4013
4014 fn handle_widget_key(&mut self, panel_id: u64, key: &str) {
4015 let panel = match self.widget_registry.get(panel_id) {
4019 Some(p) => p,
4020 None => return,
4021 };
4022 let focus_key = panel.focus_key.clone();
4023 let widget = if focus_key.is_empty() {
4024 None
4025 } else {
4026 crate::widgets::find_widget_by_key(&panel.spec, &focus_key)
4027 };
4028 let completions_open = matches!(key, "Tab" | "Up" | "Down" | "Enter" | "Escape")
4038 && self.focused_text_completions_open(panel_id);
4039 if completions_open {
4040 match key {
4041 "Tab" => {
4042 self.fire_completion_accept(panel_id);
4043 return;
4048 }
4049 "Up" => {
4050 self.move_focused_text_completion_index(panel_id, -1);
4051 self.rerender_widget_panel(panel_id);
4056 return;
4057 }
4058 "Down" => {
4059 self.move_focused_text_completion_index(panel_id, 1);
4060 self.rerender_widget_panel(panel_id);
4061 return;
4062 }
4063 "Enter" | "Escape" => {
4064 self.dismiss_focused_text_completions(panel_id);
4065 self.rerender_widget_panel(panel_id);
4066 return;
4067 }
4068 _ => {}
4069 }
4070 }
4071 match key {
4072 "Tab" => self.handle_widget_focus_advance(panel_id, 1),
4073 "Shift+Tab" => self.handle_widget_focus_advance(panel_id, -1),
4074 "Up" | "Down" => {
4075 let delta = if key == "Up" { -1 } else { 1 };
4076 match widget {
4077 Some(fresh_core::api::WidgetSpec::List { .. }) => {
4078 self.handle_widget_select_move(panel_id, delta);
4079 }
4080 Some(fresh_core::api::WidgetSpec::Tree { .. }) => {
4081 self.handle_widget_tree_select_move(panel_id, delta);
4082 }
4083 Some(fresh_core::api::WidgetSpec::Text { rows, .. }) if *rows > 1 => {
4084 self.handle_widget_text_key(panel_id, key);
4090 }
4091 _ => {
4092 let scrollable = self
4100 .widget_registry
4101 .get(panel_id)
4102 .and_then(|p| find_scrollable_widget_key(&p.spec));
4103 if let Some(target_key) = scrollable {
4104 let target_kind = self.widget_registry.get(panel_id).and_then(|p| {
4105 crate::widgets::find_widget_by_key(&p.spec, &target_key).cloned()
4106 });
4107 match target_kind {
4108 Some(fresh_core::api::WidgetSpec::List { .. }) => {
4109 self.handle_widget_select_move_for_key(
4110 panel_id,
4111 &target_key,
4112 delta,
4113 );
4114 }
4115 Some(fresh_core::api::WidgetSpec::Tree { .. }) => {
4116 self.handle_widget_tree_select_move_for_key(
4117 panel_id,
4118 &target_key,
4119 delta,
4120 );
4121 }
4122 _ => {}
4123 }
4124 }
4125 }
4126 }
4127 }
4128 "PageUp" | "PageDown" => {
4129 let page = match widget {
4133 Some(fresh_core::api::WidgetSpec::List { visible_rows, .. })
4134 | Some(fresh_core::api::WidgetSpec::Tree { visible_rows, .. }) => {
4135 visible_rows.saturating_sub(1).max(1) as i32
4136 }
4137 _ => 0,
4138 };
4139 if page == 0 {
4140 return;
4141 }
4142 let delta = if key == "PageUp" { -page } else { page };
4143 match widget {
4144 Some(fresh_core::api::WidgetSpec::List { .. }) => {
4145 self.handle_widget_select_move(panel_id, delta);
4146 }
4147 Some(fresh_core::api::WidgetSpec::Tree { .. }) => {
4148 self.handle_widget_tree_select_move(panel_id, delta);
4149 }
4150 _ => {}
4151 }
4152 }
4153 "Left" | "Right" => match widget {
4154 Some(fresh_core::api::WidgetSpec::Text { .. }) => {
4155 self.handle_widget_text_key(panel_id, key);
4156 }
4157 Some(fresh_core::api::WidgetSpec::Tree { .. }) => {
4158 self.handle_widget_tree_lateral(panel_id, key == "Right");
4159 }
4160 _ => {}
4161 },
4162 "Backspace" | "Delete" | "Home" | "End" => match widget {
4163 Some(fresh_core::api::WidgetSpec::Text { .. }) => {
4164 self.handle_widget_text_key(panel_id, key);
4165 }
4166 _ => {}
4167 },
4168 "Enter" => match widget {
4169 Some(fresh_core::api::WidgetSpec::Button { .. })
4170 | Some(fresh_core::api::WidgetSpec::Toggle { .. }) => {
4171 self.handle_widget_activate(panel_id);
4172 }
4173 Some(fresh_core::api::WidgetSpec::List { .. }) => {
4174 self.fire_list_activate(panel_id, &focus_key);
4175 }
4176 Some(fresh_core::api::WidgetSpec::Tree { .. }) => {
4177 self.fire_tree_activate(panel_id, &focus_key);
4178 }
4179 Some(fresh_core::api::WidgetSpec::Text { rows, .. }) => {
4180 if *rows > 1 {
4181 self.handle_widget_text_key(panel_id, "Enter");
4187 } else if let Some(target_key) = self
4188 .widget_registry
4189 .get(panel_id)
4190 .and_then(|p| find_scrollable_widget_key(&p.spec))
4191 {
4192 let kind = self.widget_registry.get(panel_id).and_then(|p| {
4198 crate::widgets::find_widget_by_key(&p.spec, &target_key).cloned()
4199 });
4200 match kind {
4201 Some(fresh_core::api::WidgetSpec::List { .. }) => {
4202 self.fire_list_activate(panel_id, &target_key);
4203 }
4204 Some(fresh_core::api::WidgetSpec::Tree { .. }) => {
4205 self.fire_tree_activate(panel_id, &target_key);
4206 }
4207 _ => {}
4208 }
4209 } else {
4210 self.handle_widget_focus_advance(panel_id, 1);
4213 }
4214 }
4215 _ => {}
4216 },
4217 "Space" => match widget {
4218 Some(fresh_core::api::WidgetSpec::Button { .. })
4219 | Some(fresh_core::api::WidgetSpec::Toggle { .. }) => {
4220 self.handle_widget_activate(panel_id);
4221 }
4222 Some(fresh_core::api::WidgetSpec::Text { .. }) => {
4223 self.handle_widget_text_char(panel_id, " ");
4224 }
4225 Some(fresh_core::api::WidgetSpec::List { .. }) => {
4226 self.fire_list_activate(panel_id, &focus_key);
4227 }
4228 Some(fresh_core::api::WidgetSpec::Tree { .. }) => {
4229 if !self.fire_tree_toggle_if_checkable(panel_id, &focus_key) {
4236 self.fire_tree_activate(panel_id, &focus_key);
4237 }
4238 }
4239 _ => {}
4240 },
4241 _ => {} }
4243 }
4244
4245 fn handle_widget_focus_advance(&mut self, panel_id: u64, delta: i32) {
4246 let panel = match self.widget_registry.get(panel_id) {
4247 Some(p) => p,
4248 None => return,
4249 };
4250 if panel.tabbable.is_empty() {
4251 return;
4252 }
4253 let cur_idx = panel
4254 .tabbable
4255 .iter()
4256 .position(|k| k == &panel.focus_key)
4257 .unwrap_or(0) as i32;
4258 let n = panel.tabbable.len() as i32;
4259 let new_idx = ((cur_idx + delta) % n + n) % n;
4260 let new_key = panel.tabbable[new_idx as usize].clone();
4261 self.set_panel_focus_and_notify(panel_id, new_key);
4262 self.rerender_widget_panel(panel_id);
4263 }
4264
4265 pub(crate) fn set_panel_focus_and_notify(&mut self, panel_id: u64, new_key: String) {
4275 let old_key = self
4276 .widget_registry
4277 .focus_key(panel_id)
4278 .map(|s| s.to_string())
4279 .unwrap_or_default();
4280 if old_key == new_key {
4281 return;
4282 }
4283 self.widget_registry
4284 .set_focus_key(panel_id, new_key.clone());
4285 if self
4286 .plugin_manager
4287 .read()
4288 .unwrap()
4289 .has_hook_handlers("widget_event")
4290 {
4291 self.plugin_manager.read().unwrap().run_hook(
4292 "widget_event",
4293 fresh_core::hooks::HookArgs::WidgetEvent {
4294 panel_id,
4295 widget_key: new_key,
4296 event_type: "focus".to_string(),
4297 payload: serde_json::json!({ "previous": old_key }),
4298 },
4299 );
4300 }
4301 }
4302
4303 fn handle_widget_activate(&mut self, panel_id: u64) {
4304 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 let widget = crate::widgets::find_widget_by_key(&panel.spec, &focus_key);
4316 let (event_type, payload) = match widget {
4317 Some(fresh_core::api::WidgetSpec::Button { disabled: true, .. }) => return,
4324 Some(fresh_core::api::WidgetSpec::Button { .. }) => ("activate", serde_json::json!({})),
4325 Some(fresh_core::api::WidgetSpec::Toggle { checked, .. }) => {
4326 ("toggle", serde_json::json!({ "checked": !checked }))
4327 }
4328 _ => return,
4329 };
4330 if self
4331 .plugin_manager
4332 .read()
4333 .unwrap()
4334 .has_hook_handlers("widget_event")
4335 {
4336 self.plugin_manager.read().unwrap().run_hook(
4337 "widget_event",
4338 fresh_core::hooks::HookArgs::WidgetEvent {
4339 panel_id,
4340 widget_key: focus_key,
4341 event_type: event_type.to_string(),
4342 payload,
4343 },
4344 );
4345 }
4346 }
4347
4348 fn focused_text_completions_open(&self, panel_id: u64) -> bool {
4360 let panel = match self.widget_registry.get(panel_id) {
4361 Some(p) => p,
4362 None => return false,
4363 };
4364 if panel.focus_key.is_empty() {
4365 return false;
4366 }
4367 matches!(
4368 panel.instance_states.get(&panel.focus_key),
4369 Some(crate::widgets::WidgetInstanceState::Text { completions, .. })
4370 if !completions.is_empty()
4371 )
4372 }
4373
4374 fn move_focused_text_completion_index(&mut self, panel_id: u64, delta: i32) {
4385 let panel = match self.widget_registry.get(panel_id) {
4392 Some(p) => p,
4393 None => return,
4394 };
4395 let focus_key = panel.focus_key.clone();
4396 if focus_key.is_empty() {
4397 return;
4398 }
4399 let spec_visible_rows = match crate::widgets::find_widget_by_key(&panel.spec, &focus_key) {
4400 Some(fresh_core::api::WidgetSpec::Text {
4401 completions_visible_rows,
4402 ..
4403 }) => *completions_visible_rows,
4404 _ => 0,
4405 };
4406 let visible = if spec_visible_rows == 0 {
4407 5u32
4408 } else {
4409 spec_visible_rows
4410 };
4411 let panel = match self.widget_registry.get_mut(panel_id) {
4412 Some(p) => p,
4413 None => return,
4414 };
4415 if let Some(crate::widgets::WidgetInstanceState::Text {
4416 completions,
4417 completion_selected_index,
4418 completion_scroll_offset,
4419 ..
4420 }) = panel.instance_states.get_mut(&focus_key)
4421 {
4422 if completions.is_empty() {
4423 return;
4424 }
4425 let max = (completions.len() - 1) as i32;
4426 let cur = *completion_selected_index as i32;
4427 let next = (cur + delta).clamp(0, max);
4428 *completion_selected_index = next as usize;
4429 let next_u = next as u32;
4434 if next_u < *completion_scroll_offset {
4435 *completion_scroll_offset = next_u;
4436 } else if next_u >= *completion_scroll_offset + visible {
4437 *completion_scroll_offset = next_u + 1 - visible;
4438 }
4439 }
4440 }
4441
4442 fn dismiss_focused_text_completions(&mut self, panel_id: u64) {
4449 let focus_key = {
4450 let panel = match self.widget_registry.get_mut(panel_id) {
4451 Some(p) => p,
4452 None => return,
4453 };
4454 let focus_key = panel.focus_key.clone();
4455 if focus_key.is_empty() {
4456 return;
4457 }
4458 if let Some(crate::widgets::WidgetInstanceState::Text {
4459 completions,
4460 completion_selected_index,
4461 ..
4462 }) = panel.instance_states.get_mut(&focus_key)
4463 {
4464 if completions.is_empty() {
4465 return;
4466 }
4467 completions.clear();
4468 *completion_selected_index = 0;
4469 } else {
4470 return;
4471 }
4472 focus_key
4473 };
4474 if self
4475 .plugin_manager
4476 .read()
4477 .unwrap()
4478 .has_hook_handlers("widget_event")
4479 {
4480 self.plugin_manager.read().unwrap().run_hook(
4481 "widget_event",
4482 fresh_core::hooks::HookArgs::WidgetEvent {
4483 panel_id,
4484 widget_key: focus_key,
4485 event_type: "completion_dismiss".into(),
4486 payload: serde_json::json!({}),
4487 },
4488 );
4489 }
4490 }
4491
4492 fn fire_completion_accept(&mut self, panel_id: u64) {
4504 let (focus_key, value) = {
4505 let panel = match self.widget_registry.get(panel_id) {
4506 Some(p) => p,
4507 None => return,
4508 };
4509 let focus_key = panel.focus_key.clone();
4510 if focus_key.is_empty() {
4511 return;
4512 }
4513 match panel.instance_states.get(&focus_key) {
4514 Some(crate::widgets::WidgetInstanceState::Text {
4515 completions,
4516 completion_selected_index,
4517 ..
4518 }) if !completions.is_empty() => {
4519 let idx = (*completion_selected_index).min(completions.len() - 1);
4520 (focus_key, completions[idx].value.clone())
4521 }
4522 _ => return,
4523 }
4524 };
4525 if self
4526 .plugin_manager
4527 .read()
4528 .unwrap()
4529 .has_hook_handlers("widget_event")
4530 {
4531 self.plugin_manager.read().unwrap().run_hook(
4532 "widget_event",
4533 fresh_core::hooks::HookArgs::WidgetEvent {
4534 panel_id,
4535 widget_key: focus_key,
4536 event_type: "completion_accept".into(),
4537 payload: serde_json::json!({ "value": value }),
4538 },
4539 );
4540 }
4541 }
4542
4543 fn fire_list_activate(&mut self, panel_id: u64, focus_key: &str) {
4544 let panel = match self.widget_registry.get(panel_id) {
4545 Some(p) => p,
4546 None => return,
4547 };
4548 let widget = crate::widgets::find_widget_by_key(&panel.spec, focus_key);
4549 let (spec_sel, item_keys) = match widget {
4550 Some(fresh_core::api::WidgetSpec::List {
4551 selected_index,
4552 item_keys,
4553 ..
4554 }) => (*selected_index, item_keys.clone()),
4555 _ => return,
4556 };
4557 let sel = match panel.instance_states.get(focus_key) {
4558 Some(crate::widgets::WidgetInstanceState::List { selected_index, .. }) => {
4559 *selected_index
4560 }
4561 _ => spec_sel,
4562 };
4563 if sel < 0 {
4564 return;
4565 }
4566 let item_key = item_keys.get(sel as usize).cloned().unwrap_or_default();
4567 if self
4568 .plugin_manager
4569 .read()
4570 .unwrap()
4571 .has_hook_handlers("widget_event")
4572 {
4573 self.plugin_manager.read().unwrap().run_hook(
4574 "widget_event",
4575 fresh_core::hooks::HookArgs::WidgetEvent {
4576 panel_id,
4577 widget_key: focus_key.to_string(),
4578 event_type: "activate".into(),
4579 payload: serde_json::json!({
4580 "index": sel,
4581 "key": item_key,
4582 }),
4583 },
4584 );
4585 }
4586 }
4587
4588 fn handle_widget_select_move(&mut self, panel_id: u64, delta: i32) {
4589 let focus_key = match self.widget_registry.get(panel_id) {
4590 Some(p) => p.focus_key.clone(),
4591 None => return,
4592 };
4593 if focus_key.is_empty() {
4594 return;
4595 }
4596 self.handle_widget_select_move_for_key(panel_id, &focus_key, delta);
4597 }
4598
4599 pub(super) fn set_widget_list_selected_index(
4607 &mut self,
4608 panel_id: u64,
4609 widget_key: &str,
4610 index: i32,
4611 ) {
4612 if let Some(panel) = self.widget_registry.get_mut(panel_id) {
4613 let prev_scroll = match panel.instance_states.get(widget_key) {
4614 Some(crate::widgets::WidgetInstanceState::List { scroll_offset, .. }) => {
4615 *scroll_offset
4616 }
4617 _ => 0,
4618 };
4619 panel.instance_states.insert(
4620 widget_key.to_string(),
4621 crate::widgets::WidgetInstanceState::List {
4622 scroll_offset: prev_scroll,
4623 selected_index: index,
4624 },
4625 );
4626 }
4627 self.rerender_widget_panel(panel_id);
4628 }
4629
4630 fn handle_widget_select_move_for_key(&mut self, panel_id: u64, widget_key: &str, delta: i32) {
4636 let panel = match self.widget_registry.get(panel_id) {
4637 Some(p) => p,
4638 None => return,
4639 };
4640 let widget = crate::widgets::find_widget_by_key(&panel.spec, widget_key);
4641 let (spec_sel, total, item_keys) = match widget {
4642 Some(fresh_core::api::WidgetSpec::List {
4643 selected_index,
4644 items,
4645 item_keys,
4646 ..
4647 }) => (*selected_index, items.len() as i32, item_keys.clone()),
4648 _ => return,
4649 };
4650 if total == 0 {
4651 return;
4652 }
4653 let cur_sel = match panel.instance_states.get(widget_key) {
4654 Some(crate::widgets::WidgetInstanceState::List { selected_index, .. }) => {
4655 *selected_index
4656 }
4657 _ => spec_sel,
4658 };
4659 let raw = if cur_sel < 0 { 0 } else { cur_sel + delta };
4660 let new_sel = raw.clamp(0, total - 1);
4661 let new_key = item_keys.get(new_sel as usize).cloned().unwrap_or_default();
4662 if let Some(panel_mut) = self.widget_registry.get_mut(panel_id) {
4663 let cur_scroll = match panel_mut.instance_states.get(widget_key) {
4664 Some(crate::widgets::WidgetInstanceState::List { scroll_offset, .. }) => {
4665 *scroll_offset
4666 }
4667 _ => 0,
4668 };
4669 panel_mut.instance_states.insert(
4670 widget_key.to_string(),
4671 crate::widgets::WidgetInstanceState::List {
4672 scroll_offset: cur_scroll,
4673 selected_index: new_sel,
4674 },
4675 );
4676 }
4677 self.rerender_widget_panel(panel_id);
4678 if self
4679 .plugin_manager
4680 .read()
4681 .unwrap()
4682 .has_hook_handlers("widget_event")
4683 {
4684 self.plugin_manager.read().unwrap().run_hook(
4685 "widget_event",
4686 fresh_core::hooks::HookArgs::WidgetEvent {
4687 panel_id,
4688 widget_key: widget_key.to_string(),
4689 event_type: "select".into(),
4690 payload: serde_json::json!({ "index": new_sel, "key": new_key }),
4691 },
4692 );
4693 }
4694 }
4695
4696 fn handle_widget_tree_select_move(&mut self, panel_id: u64, delta: i32) {
4701 let focus_key = match self.widget_registry.get(panel_id) {
4702 Some(p) => p.focus_key.clone(),
4703 None => return,
4704 };
4705 if focus_key.is_empty() {
4706 return;
4707 }
4708 self.handle_widget_tree_select_move_for_key(panel_id, &focus_key, delta);
4709 }
4710
4711 fn handle_widget_tree_select_move_for_key(
4713 &mut self,
4714 panel_id: u64,
4715 widget_key: &str,
4716 delta: i32,
4717 ) {
4718 let panel = match self.widget_registry.get(panel_id) {
4719 Some(p) => p,
4720 None => return,
4721 };
4722 let widget = crate::widgets::find_widget_by_key(&panel.spec, widget_key);
4723 let (spec_sel, nodes, item_keys) = match widget {
4724 Some(fresh_core::api::WidgetSpec::Tree {
4725 selected_index,
4726 nodes,
4727 item_keys,
4728 ..
4729 }) => (*selected_index, nodes.clone(), item_keys.clone()),
4730 _ => return,
4731 };
4732 if nodes.is_empty() {
4733 return;
4734 }
4735 let (cur_sel, cur_scroll, expanded) = match panel.instance_states.get(widget_key) {
4736 Some(crate::widgets::WidgetInstanceState::Tree {
4737 selected_index,
4738 scroll_offset,
4739 expanded_keys,
4740 }) => (*selected_index, *scroll_offset, expanded_keys.clone()),
4741 _ => (spec_sel, 0u32, std::collections::HashSet::<String>::new()),
4742 };
4743 let visible_indices = collect_visible_tree_indices(&nodes, &item_keys, &expanded);
4744 if visible_indices.is_empty() {
4745 return;
4746 }
4747 let cur_pos = if cur_sel < 0 {
4748 if delta > 0 {
4749 -1
4750 } else {
4751 visible_indices.len() as i32
4752 }
4753 } else {
4754 visible_indices
4755 .iter()
4756 .position(|&v| v as i32 == cur_sel)
4757 .map(|p| p as i32)
4758 .unwrap_or(-1)
4759 };
4760 let new_pos = (cur_pos + delta).clamp(0, (visible_indices.len() as i32) - 1);
4761 let new_abs = visible_indices[new_pos as usize];
4762 let new_key = item_keys.get(new_abs).cloned().unwrap_or_default();
4763 if let Some(panel_mut) = self.widget_registry.get_mut(panel_id) {
4764 panel_mut.instance_states.insert(
4765 widget_key.to_string(),
4766 crate::widgets::WidgetInstanceState::Tree {
4767 scroll_offset: cur_scroll,
4768 selected_index: new_abs as i32,
4769 expanded_keys: expanded,
4770 },
4771 );
4772 }
4773 self.rerender_widget_panel(panel_id);
4774 if self
4775 .plugin_manager
4776 .read()
4777 .unwrap()
4778 .has_hook_handlers("widget_event")
4779 {
4780 self.plugin_manager.read().unwrap().run_hook(
4781 "widget_event",
4782 fresh_core::hooks::HookArgs::WidgetEvent {
4783 panel_id,
4784 widget_key: widget_key.to_string(),
4785 event_type: "select".into(),
4786 payload: serde_json::json!({ "index": new_abs as i64, "key": new_key }),
4787 },
4788 );
4789 }
4790 }
4791
4792 pub(super) fn handle_widget_panel_wheel(
4802 &mut self,
4803 buffer_id: crate::model::event::BufferId,
4804 delta: i32,
4805 ) -> bool {
4806 let panels = self.widget_registry.panels_for_buffer(buffer_id);
4807 let mut consumed = false;
4808 for panel_id in panels {
4809 if self.focused_text_completions_open(panel_id) {
4815 self.scroll_focused_text_completions(panel_id, delta);
4816 self.rerender_widget_panel(panel_id);
4825 consumed = true;
4826 continue;
4827 }
4828 let spec = match self.widget_registry.get(panel_id) {
4829 Some(p) => p.spec.clone(),
4830 None => continue,
4831 };
4832 let Some(widget_key) = find_scrollable_widget_key(&spec) else {
4833 continue;
4834 };
4835 let widget = crate::widgets::find_widget_by_key(&spec, &widget_key);
4836 match widget {
4837 Some(fresh_core::api::WidgetSpec::Tree { .. }) => {
4838 self.handle_widget_tree_wheel(panel_id, &widget_key, delta);
4839 consumed = true;
4840 }
4841 Some(fresh_core::api::WidgetSpec::List { .. }) => {
4842 self.handle_widget_list_wheel(panel_id, &widget_key, delta);
4843 consumed = true;
4844 }
4845 _ => {}
4846 }
4847 }
4848 consumed
4849 }
4850
4851 fn scroll_focused_text_completions(&mut self, panel_id: u64, delta: i32) {
4858 let panel = match self.widget_registry.get(panel_id) {
4859 Some(p) => p,
4860 None => return,
4861 };
4862 let focus_key = panel.focus_key.clone();
4863 if focus_key.is_empty() {
4864 return;
4865 }
4866 let spec_visible_rows = match crate::widgets::find_widget_by_key(&panel.spec, &focus_key) {
4867 Some(fresh_core::api::WidgetSpec::Text {
4868 completions_visible_rows,
4869 ..
4870 }) => *completions_visible_rows,
4871 _ => 0,
4872 };
4873 let visible = if spec_visible_rows == 0 {
4874 5u32
4875 } else {
4876 spec_visible_rows
4877 };
4878 let panel = match self.widget_registry.get_mut(panel_id) {
4879 Some(p) => p,
4880 None => return,
4881 };
4882 if let Some(crate::widgets::WidgetInstanceState::Text {
4883 completions,
4884 completion_scroll_offset,
4885 ..
4886 }) = panel.instance_states.get_mut(&focus_key)
4887 {
4888 if completions.is_empty() {
4889 return;
4890 }
4891 let total = completions.len() as u32;
4892 let max_scroll = total.saturating_sub(visible.min(total));
4893 let next = (*completion_scroll_offset as i32 + delta).clamp(0, max_scroll as i32);
4894 *completion_scroll_offset = next as u32;
4895 }
4896 }
4897
4898 fn handle_widget_tree_wheel(&mut self, panel_id: u64, widget_key: &str, delta: i32) {
4903 let panel = match self.widget_registry.get(panel_id) {
4904 Some(p) => p,
4905 None => return,
4906 };
4907 let widget = crate::widgets::find_widget_by_key(&panel.spec, widget_key);
4908 let (visible_rows, nodes, item_keys) = match widget {
4909 Some(fresh_core::api::WidgetSpec::Tree {
4910 visible_rows,
4911 nodes,
4912 item_keys,
4913 ..
4914 }) => (*visible_rows, nodes.clone(), item_keys.clone()),
4915 _ => return,
4916 };
4917 if nodes.is_empty() {
4918 return;
4919 }
4920 let (cur_sel, cur_scroll, expanded) = match panel.instance_states.get(widget_key) {
4921 Some(crate::widgets::WidgetInstanceState::Tree {
4922 selected_index,
4923 scroll_offset,
4924 expanded_keys,
4925 }) => (*selected_index, *scroll_offset, expanded_keys.clone()),
4926 _ => (-1, 0, std::collections::HashSet::<String>::new()),
4927 };
4928 let visible_indices = collect_visible_tree_indices(&nodes, &item_keys, &expanded);
4929 if visible_indices.is_empty() {
4930 return;
4931 }
4932 let visible = visible_rows.max(1);
4933 let total_visible = visible_indices.len() as u32;
4934 let max_scroll = total_visible.saturating_sub(visible);
4935 let new_scroll = (cur_scroll as i32 + delta).clamp(0, max_scroll as i32) as u32;
4936 if new_scroll == cur_scroll {
4937 return;
4938 }
4939 let cur_pos: Option<u32> = if cur_sel >= 0 {
4941 visible_indices
4942 .iter()
4943 .position(|&v| v as i32 == cur_sel)
4944 .map(|p| p as u32)
4945 } else {
4946 None
4947 };
4948 let new_sel_abs = match cur_pos {
4949 Some(pos) if pos < new_scroll => visible_indices[new_scroll as usize] as i32,
4950 Some(pos) if pos >= new_scroll + visible => {
4951 visible_indices[(new_scroll + visible - 1) as usize] as i32
4952 }
4953 _ => cur_sel,
4954 };
4955 if let Some(panel_mut) = self.widget_registry.get_mut(panel_id) {
4956 panel_mut.instance_states.insert(
4957 widget_key.to_string(),
4958 crate::widgets::WidgetInstanceState::Tree {
4959 scroll_offset: new_scroll,
4960 selected_index: new_sel_abs,
4961 expanded_keys: expanded,
4962 },
4963 );
4964 }
4965 self.rerender_widget_panel(panel_id);
4966 }
4967
4968 fn handle_widget_list_wheel(&mut self, panel_id: u64, widget_key: &str, delta: i32) {
4970 let panel = match self.widget_registry.get(panel_id) {
4971 Some(p) => p,
4972 None => return,
4973 };
4974 let widget = crate::widgets::find_widget_by_key(&panel.spec, widget_key);
4975 let (visible_rows, total) = match widget {
4976 Some(fresh_core::api::WidgetSpec::List {
4977 visible_rows,
4978 items,
4979 ..
4980 }) => (*visible_rows, items.len() as u32),
4981 _ => return,
4982 };
4983 if total == 0 {
4984 return;
4985 }
4986 let (cur_sel, cur_scroll) = match panel.instance_states.get(widget_key) {
4987 Some(crate::widgets::WidgetInstanceState::List {
4988 selected_index,
4989 scroll_offset,
4990 }) => (*selected_index, *scroll_offset),
4991 _ => (-1, 0),
4992 };
4993 let visible = visible_rows.max(1);
4994 let max_scroll = total.saturating_sub(visible);
4995 let new_scroll = (cur_scroll as i32 + delta).clamp(0, max_scroll as i32) as u32;
4996 if new_scroll == cur_scroll {
4997 return;
4998 }
4999 let new_sel = if cur_sel < 0 {
5000 cur_sel
5001 } else if (cur_sel as u32) < new_scroll {
5002 new_scroll as i32
5003 } else if (cur_sel as u32) >= new_scroll + visible {
5004 (new_scroll + visible - 1) as i32
5005 } else {
5006 cur_sel
5007 };
5008 if let Some(panel_mut) = self.widget_registry.get_mut(panel_id) {
5009 panel_mut.instance_states.insert(
5010 widget_key.to_string(),
5011 crate::widgets::WidgetInstanceState::List {
5012 scroll_offset: new_scroll,
5013 selected_index: new_sel,
5014 },
5015 );
5016 }
5017 self.rerender_widget_panel(panel_id);
5018 }
5019
5020 fn handle_widget_tree_lateral(&mut self, panel_id: u64, is_right: bool) {
5030 let panel = match self.widget_registry.get(panel_id) {
5031 Some(p) => p,
5032 None => return,
5033 };
5034 let focus_key = panel.focus_key.clone();
5035 if focus_key.is_empty() {
5036 return;
5037 }
5038 let widget = crate::widgets::find_widget_by_key(&panel.spec, &focus_key);
5039 let (spec_sel, nodes, item_keys) = match widget {
5040 Some(fresh_core::api::WidgetSpec::Tree {
5041 selected_index,
5042 nodes,
5043 item_keys,
5044 ..
5045 }) => (*selected_index, nodes.clone(), item_keys.clone()),
5046 _ => return,
5047 };
5048 if nodes.is_empty() {
5049 return;
5050 }
5051 let (cur_sel, cur_scroll, mut expanded) = match panel.instance_states.get(&focus_key) {
5052 Some(crate::widgets::WidgetInstanceState::Tree {
5053 selected_index,
5054 scroll_offset,
5055 expanded_keys,
5056 }) => (*selected_index, *scroll_offset, expanded_keys.clone()),
5057 _ => (spec_sel, 0u32, std::collections::HashSet::<String>::new()),
5058 };
5059 if cur_sel < 0 {
5060 return;
5061 }
5062 let sel_idx = cur_sel as usize;
5063 let node = match nodes.get(sel_idx) {
5064 Some(n) => n,
5065 None => return,
5066 };
5067 let key = item_keys.get(sel_idx).cloned().unwrap_or_default();
5068 let was_expanded = !key.is_empty() && expanded.contains(&key);
5069
5070 let mut new_sel = cur_sel;
5071 let mut expansion_changed: Option<bool> = None; if is_right {
5073 if node.has_children && !was_expanded && !key.is_empty() {
5074 expanded.insert(key.clone());
5075 expansion_changed = Some(true);
5076 }
5077 } else if node.has_children && was_expanded && !key.is_empty() {
5078 expanded.remove(&key);
5079 expansion_changed = Some(false);
5080 } else if let Some(parent_idx) = crate::widgets::tree_parent_index(&nodes, sel_idx) {
5081 new_sel = parent_idx as i32;
5082 }
5083 if expansion_changed.is_none() && new_sel == cur_sel {
5085 return;
5086 }
5087 let final_key = item_keys.get(new_sel as usize).cloned().unwrap_or_default();
5088 if let Some(panel_mut) = self.widget_registry.get_mut(panel_id) {
5089 panel_mut.instance_states.insert(
5090 focus_key.clone(),
5091 crate::widgets::WidgetInstanceState::Tree {
5092 scroll_offset: cur_scroll,
5093 selected_index: new_sel,
5094 expanded_keys: expanded,
5095 },
5096 );
5097 }
5098 self.rerender_widget_panel(panel_id);
5099 if self
5100 .plugin_manager
5101 .read()
5102 .unwrap()
5103 .has_hook_handlers("widget_event")
5104 {
5105 if let Some(now_expanded) = expansion_changed {
5106 self.plugin_manager.read().unwrap().run_hook(
5107 "widget_event",
5108 fresh_core::hooks::HookArgs::WidgetEvent {
5109 panel_id,
5110 widget_key: focus_key.clone(),
5111 event_type: "expand".into(),
5112 payload: serde_json::json!({
5113 "index": cur_sel as i64,
5114 "key": key,
5115 "expanded": now_expanded,
5116 }),
5117 },
5118 );
5119 } else if new_sel != cur_sel {
5120 self.plugin_manager.read().unwrap().run_hook(
5121 "widget_event",
5122 fresh_core::hooks::HookArgs::WidgetEvent {
5123 panel_id,
5124 widget_key: focus_key,
5125 event_type: "select".into(),
5126 payload: serde_json::json!({
5127 "index": new_sel as i64,
5128 "key": final_key,
5129 }),
5130 },
5131 );
5132 }
5133 }
5134 }
5135
5136 pub(crate) fn handle_widget_tree_expand_toggle(
5140 &mut self,
5141 panel_id: u64,
5142 widget_key: &str,
5143 item_key: &str,
5144 ) {
5145 if widget_key.is_empty() || item_key.is_empty() {
5146 return;
5147 }
5148 let now_expanded = {
5149 let panel = match self.widget_registry.get_mut(panel_id) {
5150 Some(p) => p,
5151 None => return,
5152 };
5153 let (cur_scroll, cur_sel, mut expanded) = match panel.instance_states.get(widget_key) {
5154 Some(crate::widgets::WidgetInstanceState::Tree {
5155 scroll_offset,
5156 selected_index,
5157 expanded_keys,
5158 }) => (*scroll_offset, *selected_index, expanded_keys.clone()),
5159 _ => (0u32, -1i32, std::collections::HashSet::<String>::new()),
5160 };
5161 let next = if expanded.contains(item_key) {
5162 expanded.remove(item_key);
5163 false
5164 } else {
5165 expanded.insert(item_key.to_string());
5166 true
5167 };
5168 panel.instance_states.insert(
5169 widget_key.to_string(),
5170 crate::widgets::WidgetInstanceState::Tree {
5171 scroll_offset: cur_scroll,
5172 selected_index: cur_sel,
5173 expanded_keys: expanded,
5174 },
5175 );
5176 next
5177 };
5178 self.rerender_widget_panel(panel_id);
5179 if self
5180 .plugin_manager
5181 .read()
5182 .unwrap()
5183 .has_hook_handlers("widget_event")
5184 {
5185 self.plugin_manager.read().unwrap().run_hook(
5186 "widget_event",
5187 fresh_core::hooks::HookArgs::WidgetEvent {
5188 panel_id,
5189 widget_key: widget_key.to_string(),
5190 event_type: "expand".into(),
5191 payload: serde_json::json!({
5192 "key": item_key,
5193 "expanded": now_expanded,
5194 }),
5195 },
5196 );
5197 }
5198 }
5199
5200 fn fire_tree_toggle_if_checkable(&mut self, panel_id: u64, focus_key: &str) -> bool {
5214 let panel = match self.widget_registry.get(panel_id) {
5215 Some(p) => p,
5216 None => return false,
5217 };
5218 let widget = crate::widgets::find_widget_by_key(&panel.spec, focus_key);
5219 let (spec_sel, nodes, item_keys, checkable) = match widget {
5220 Some(fresh_core::api::WidgetSpec::Tree {
5221 selected_index,
5222 nodes,
5223 item_keys,
5224 checkable,
5225 ..
5226 }) => (*selected_index, nodes, item_keys.clone(), *checkable),
5227 _ => return false,
5228 };
5229 if !checkable {
5230 return false;
5231 }
5232 let sel = match panel.instance_states.get(focus_key) {
5233 Some(crate::widgets::WidgetInstanceState::Tree { selected_index, .. }) => {
5234 *selected_index
5235 }
5236 _ => spec_sel,
5237 };
5238 if sel < 0 {
5239 return false;
5240 }
5241 let cur_checked = match nodes.get(sel as usize).and_then(|n| n.checked) {
5242 Some(b) => b,
5243 None => return false, };
5245 let new_checked = !cur_checked;
5246 let item_key = item_keys.get(sel as usize).cloned().unwrap_or_default();
5247 if self
5248 .plugin_manager
5249 .read()
5250 .unwrap()
5251 .has_hook_handlers("widget_event")
5252 {
5253 self.plugin_manager.read().unwrap().run_hook(
5254 "widget_event",
5255 fresh_core::hooks::HookArgs::WidgetEvent {
5256 panel_id,
5257 widget_key: focus_key.to_string(),
5258 event_type: "toggle".into(),
5259 payload: serde_json::json!({
5260 "index": sel,
5261 "key": item_key,
5262 "checked": new_checked,
5263 }),
5264 },
5265 );
5266 }
5267 true
5268 }
5269
5270 fn fire_tree_activate(&mut self, panel_id: u64, focus_key: &str) {
5271 let panel = match self.widget_registry.get(panel_id) {
5272 Some(p) => p,
5273 None => return,
5274 };
5275 let widget = crate::widgets::find_widget_by_key(&panel.spec, focus_key);
5276 let (spec_sel, item_keys) = match widget {
5277 Some(fresh_core::api::WidgetSpec::Tree {
5278 selected_index,
5279 item_keys,
5280 ..
5281 }) => (*selected_index, item_keys.clone()),
5282 _ => return,
5283 };
5284 let sel = match panel.instance_states.get(focus_key) {
5285 Some(crate::widgets::WidgetInstanceState::Tree { selected_index, .. }) => {
5286 *selected_index
5287 }
5288 _ => spec_sel,
5289 };
5290 if sel < 0 {
5291 return;
5292 }
5293 let item_key = item_keys.get(sel as usize).cloned().unwrap_or_default();
5294 if self
5295 .plugin_manager
5296 .read()
5297 .unwrap()
5298 .has_hook_handlers("widget_event")
5299 {
5300 self.plugin_manager.read().unwrap().run_hook(
5301 "widget_event",
5302 fresh_core::hooks::HookArgs::WidgetEvent {
5303 panel_id,
5304 widget_key: focus_key.to_string(),
5305 event_type: "activate".into(),
5306 payload: serde_json::json!({
5307 "index": sel,
5308 "key": item_key,
5309 }),
5310 },
5311 );
5312 }
5313 }
5314
5315 pub(super) fn focused_text_widget_panel_for_buffer(
5328 &self,
5329 buffer_id: crate::model::event::BufferId,
5330 ) -> Option<u64> {
5331 for panel_id in self.widget_registry.panels_for_buffer(buffer_id) {
5332 let panel = self.widget_registry.get(panel_id)?;
5333 if panel.focus_key.is_empty() {
5334 continue;
5335 }
5336 let widget = crate::widgets::find_widget_by_key(&panel.spec, &panel.focus_key);
5337 if matches!(widget, Some(fresh_core::api::WidgetSpec::Text { .. })) {
5338 return Some(panel_id);
5339 }
5340 }
5341 None
5342 }
5343
5344 pub(super) fn focused_widget_selected_text(&self, panel_id: u64) -> Option<String> {
5349 let panel = self.widget_registry.get(panel_id)?;
5350 if panel.focus_key.is_empty() {
5351 return None;
5352 }
5353 match panel.instance_states.get(&panel.focus_key) {
5354 Some(crate::widgets::WidgetInstanceState::Text { editor, .. }) => {
5355 editor.selected_text()
5356 }
5357 _ => None,
5358 }
5359 }
5360
5361 pub(super) fn handle_widget_select_all(&mut self, panel_id: u64) -> bool {
5366 self.with_focused_text_editor(panel_id, |editor| editor.select_all())
5370 }
5371
5372 pub(super) fn handle_widget_copy(&mut self, panel_id: u64) -> bool {
5377 if self.widget_registry.get(panel_id).is_none() {
5378 return false;
5379 }
5380 if let Some(text) = self.focused_widget_selected_text(panel_id) {
5381 self.clipboard.copy(text);
5382 }
5383 true
5384 }
5385
5386 pub(super) fn handle_widget_cut(&mut self, panel_id: u64) -> bool {
5389 if self.widget_registry.get(panel_id).is_none() {
5390 return false;
5391 }
5392 if let Some(text) = self.focused_widget_selected_text(panel_id) {
5393 self.clipboard.copy(text);
5394 self.with_focused_text_editor(panel_id, |editor| {
5395 editor.delete_selection();
5396 });
5397 }
5398 true
5399 }
5400
5401 pub(super) fn handle_widget_insert_str(&mut self, panel_id: u64, text: &str) -> bool {
5407 if self.widget_registry.get(panel_id).is_none() {
5408 return false;
5409 }
5410 let owned = text.to_string();
5411 self.with_focused_text_editor(panel_id, move |editor| {
5412 editor.insert_str(&owned);
5413 });
5414 true
5415 }
5416
5417 fn ensure_focused_text_seeded(&mut self, panel_id: u64, focus_key: &str) -> bool {
5424 let panel = match self.widget_registry.get_mut(panel_id) {
5425 Some(p) => p,
5426 None => return false,
5427 };
5428 if matches!(
5429 panel.instance_states.get(focus_key),
5430 Some(crate::widgets::WidgetInstanceState::Text { .. })
5431 ) {
5432 return true;
5433 }
5434 let widget = crate::widgets::find_widget_by_key(&panel.spec, focus_key);
5435 let (value, cursor_byte, multiline) = match widget {
5436 Some(fresh_core::api::WidgetSpec::Text {
5437 value,
5438 cursor_byte,
5439 rows,
5440 ..
5441 }) => (value.clone(), *cursor_byte, *rows > 1),
5442 _ => return false,
5443 };
5444 let mut editor = if multiline {
5445 crate::primitives::text_edit::TextEdit::with_text(&value)
5446 } else {
5447 crate::primitives::text_edit::TextEdit::single_line_with_text(&value)
5448 };
5449 let seed = if cursor_byte < 0 {
5450 value.len()
5451 } else {
5452 (cursor_byte as usize).min(value.len())
5453 };
5454 editor.set_cursor_from_flat(seed);
5455 panel.instance_states.insert(
5456 focus_key.to_string(),
5457 crate::widgets::WidgetInstanceState::Text {
5458 editor,
5459 scroll: 0,
5460 completions: Vec::new(),
5461 completion_selected_index: 0,
5462 completion_scroll_offset: 0,
5463 },
5464 );
5465 true
5466 }
5467
5468 pub(super) fn with_focused_text_editor<F>(&mut self, panel_id: u64, op: F) -> bool
5475 where
5476 F: FnOnce(&mut crate::primitives::text_edit::TextEdit),
5477 {
5478 let focus_key = match self.widget_registry.get(panel_id) {
5479 Some(p) if !p.focus_key.is_empty() => p.focus_key.clone(),
5480 _ => return false,
5481 };
5482 if !self.ensure_focused_text_seeded(panel_id, &focus_key) {
5483 return false;
5484 }
5485 let (before_value, before_cursor) = {
5486 let panel = self.widget_registry.get(panel_id).unwrap();
5487 match panel.instance_states.get(&focus_key) {
5488 Some(crate::widgets::WidgetInstanceState::Text { editor, .. }) => {
5489 (editor.value(), editor.flat_cursor_byte())
5490 }
5491 _ => return false,
5492 }
5493 };
5494 {
5495 let panel = self.widget_registry.get_mut(panel_id).unwrap();
5496 match panel.instance_states.get_mut(&focus_key) {
5497 Some(crate::widgets::WidgetInstanceState::Text { editor, .. }) => op(editor),
5498 _ => return false,
5499 }
5500 }
5501 let (after_value, after_cursor) = {
5502 let panel = self.widget_registry.get(panel_id).unwrap();
5503 match panel.instance_states.get(&focus_key) {
5504 Some(crate::widgets::WidgetInstanceState::Text { editor, .. }) => {
5505 (editor.value(), editor.flat_cursor_byte())
5506 }
5507 _ => return false,
5508 }
5509 };
5510 if after_value == before_value && after_cursor == before_cursor {
5511 return false;
5512 }
5513 self.rerender_widget_panel(panel_id);
5514 if self
5515 .plugin_manager
5516 .read()
5517 .unwrap()
5518 .has_hook_handlers("widget_event")
5519 {
5520 self.plugin_manager.read().unwrap().run_hook(
5521 "widget_event",
5522 fresh_core::hooks::HookArgs::WidgetEvent {
5523 panel_id,
5524 widget_key: focus_key.clone(),
5525 event_type: "change".into(),
5526 payload: serde_json::json!({
5527 "value": after_value,
5528 "cursorByte": after_cursor as i64,
5529 }),
5530 },
5531 );
5532 }
5533 true
5534 }
5535
5536 fn handle_widget_text_key(&mut self, panel_id: u64, key: &str) {
5542 self.with_focused_text_editor(panel_id, |editor| match key {
5543 "Backspace" => editor.backspace(),
5544 "Delete" => editor.delete(),
5545 "Left" => editor.move_left(),
5546 "Right" => editor.move_right(),
5547 "Up" => editor.move_up(),
5548 "Down" => editor.move_down(),
5549 "Home" => editor.move_home(),
5550 "End" => editor.move_end(),
5551 "Enter" => editor.insert_char('\n'),
5552 _ => { }
5553 });
5554 }
5555
5556 fn handle_widget_text_char(&mut self, panel_id: u64, text: &str) {
5563 if text.is_empty() {
5564 return;
5565 }
5566 let text = text.to_string();
5567 self.with_focused_text_editor(panel_id, move |editor| {
5568 editor.insert_str(&text);
5569 });
5570 }
5571
5572 fn handle_unmount_widget_panel(&mut self, panel_id: u64) {
5573 match self.widget_registry.unmount(panel_id) {
5574 Some(buffer_id) => {
5575 tracing::debug!(
5576 "Unmounted widget panel {} (was rendering into {:?})",
5577 panel_id,
5578 buffer_id
5579 );
5580 }
5585 None => {
5586 tracing::debug!("UnmountWidgetPanel for unknown panel {} ignored", panel_id);
5587 }
5588 }
5589 }
5590
5591 fn handle_mount_floating_widget(
5592 &mut self,
5593 panel_id: u64,
5594 spec: fresh_core::api::WidgetSpec,
5595 width_pct: u8,
5596 height_pct: u8,
5597 ) {
5598 let width_pct = width_pct.clamp(1, 100);
5599 let height_pct = height_pct.clamp(1, 100);
5600 if let Some(existing) = self.floating_widget_panel.take() {
5601 if existing.panel_id != panel_id {
5602 let _ = self.widget_registry.unmount(existing.panel_id);
5603 }
5604 }
5605 self.floating_widget_panel = Some(FloatingWidgetState {
5606 panel_id,
5607 width_pct,
5608 height_pct,
5609 entries: Vec::new(),
5610 focus_cursor: None,
5611 embeds: Vec::new(),
5612 overlays: Vec::new(),
5613 scroll_regions: Vec::new(),
5614 scrollbar_tracks: Vec::new(),
5615 scrollbar_mouse: Default::default(),
5616 scrollbar_drag_key: None,
5617 last_inner_rect: None,
5618 });
5619 let prev = std::collections::HashMap::new();
5620 let prev_focus = String::new();
5621 let panel_width = self.floating_panel_inner_width();
5622 let out = crate::widgets::render_spec(&spec, &prev, &prev_focus, panel_width);
5623 let focus_cursor = out.focus_cursor;
5624 let entries = out.entries;
5625 let embeds = out.embeds;
5626 let overlays = out.overlays;
5627 let scroll_regions = out.scroll_regions;
5628 self.widget_registry.mount(
5629 panel_id,
5630 FLOATING_PANEL_BUFFER_ID,
5631 spec,
5632 out.hits,
5633 out.instance_states,
5634 out.focus_key,
5635 out.tabbable,
5636 );
5637 if let Some(fwp) = self.floating_widget_panel.as_mut() {
5638 fwp.entries = entries;
5639 fwp.focus_cursor = focus_cursor;
5640 fwp.embeds = embeds;
5641 fwp.overlays = overlays;
5642 fwp.scroll_regions = scroll_regions;
5643 }
5644 tracing::debug!(
5645 "Mounted floating widget panel {} ({}%x{}%)",
5646 panel_id,
5647 width_pct,
5648 height_pct
5649 );
5650 }
5651
5652 fn handle_update_floating_widget(&mut self, panel_id: u64, spec: fresh_core::api::WidgetSpec) {
5653 match self.floating_widget_panel.as_ref() {
5654 Some(fwp) if fwp.panel_id == panel_id => {}
5655 _ => {
5656 tracing::debug!(
5657 "UpdateFloatingWidget for unknown / mismatched panel {} ignored",
5658 panel_id
5659 );
5660 return;
5661 }
5662 }
5663 let prev = self
5664 .widget_registry
5665 .instance_states(panel_id)
5666 .cloned()
5667 .unwrap_or_default();
5668 let prev_focus = self
5669 .widget_registry
5670 .focus_key(panel_id)
5671 .map(|s| s.to_string())
5672 .unwrap_or_default();
5673 let panel_width = self.floating_panel_inner_width();
5674 let out = crate::widgets::render_spec(&spec, &prev, &prev_focus, panel_width);
5675 let focus_cursor = out.focus_cursor;
5676 let entries = out.entries;
5677 let embeds = out.embeds;
5678 let overlays = out.overlays;
5679 let scroll_regions = out.scroll_regions;
5680 if self
5681 .widget_registry
5682 .update(
5683 panel_id,
5684 spec,
5685 out.hits,
5686 out.instance_states,
5687 out.focus_key,
5688 out.tabbable,
5689 )
5690 .is_err()
5691 {
5692 tracing::debug!(
5693 "UpdateFloatingWidget for unknown panel {} ignored (not in registry)",
5694 panel_id
5695 );
5696 return;
5697 }
5698 if let Some(fwp) = self.floating_widget_panel.as_mut() {
5699 fwp.entries = entries;
5700 fwp.focus_cursor = focus_cursor;
5701 fwp.embeds = embeds;
5702 fwp.overlays = overlays;
5703 fwp.scroll_regions = scroll_regions;
5704 }
5705 }
5706
5707 fn handle_unmount_floating_widget(&mut self, panel_id: u64) {
5708 match self.floating_widget_panel.as_ref() {
5709 Some(fwp) if fwp.panel_id == panel_id => {}
5710 _ => {
5711 tracing::debug!(
5712 "UnmountFloatingWidget for unknown / mismatched panel {} ignored",
5713 panel_id
5714 );
5715 return;
5716 }
5717 }
5718 self.floating_widget_panel = None;
5719 let _ = self.widget_registry.unmount(panel_id);
5720 self.active_window_mut().resize_visible_terminals();
5733 tracing::debug!("Unmounted floating widget panel {}", panel_id);
5734 }
5735
5736 pub(super) fn floating_panel_inner_width(&self) -> u32 {
5742 let term_w = self.terminal_width.max(1) as u32;
5743 let pct = self
5744 .floating_widget_panel
5745 .as_ref()
5746 .map(|f| f.width_pct.clamp(1, 100) as u32)
5747 .unwrap_or(80);
5748 let w = (term_w * pct) / 100;
5749 w.saturating_sub(2).max(10)
5750 }
5751
5752 fn handle_get_text_properties_at_cursor(&self, buffer_id: BufferId) {
5753 if let Some(state) = self
5754 .windows
5755 .get(&self.active_window)
5756 .map(|w| &w.buffers)
5757 .expect("active window present")
5758 .get(&buffer_id)
5759 {
5760 let cursor_pos = self
5761 .windows
5762 .get(&self.active_window)
5763 .and_then(|w| w.buffers.splits())
5764 .map(|(_, vs)| vs)
5765 .expect("active window must have a populated split layout")
5766 .values()
5767 .find_map(|vs| vs.buffer_state(buffer_id))
5768 .map(|bs| bs.cursors.primary().position)
5769 .unwrap_or(0);
5770 let properties = state.text_properties.get_at(cursor_pos);
5771 tracing::debug!(
5772 "Text properties at cursor in {:?}: {} properties found",
5773 buffer_id,
5774 properties.len()
5775 );
5776 }
5778 }
5779
5780 fn handle_set_context(&mut self, name: String, active: bool) {
5781 if active {
5782 self.active_window_mut()
5783 .active_custom_contexts
5784 .insert(name.clone());
5785 tracing::debug!("Set custom context: {}", name);
5786 } else {
5787 self.active_window_mut()
5788 .active_custom_contexts
5789 .remove(&name);
5790 tracing::debug!("Unset custom context: {}", name);
5791 }
5792 }
5793
5794 fn handle_disable_lsp_for_language(&mut self, language: String) {
5795 tracing::info!("Disabling LSP for language: {}", language);
5796 let __active_id = self.active_window;
5797 if let Some(lsp) = self
5798 .windows
5799 .get_mut(&__active_id)
5800 .and_then(|w| w.lsp.as_mut())
5801 {
5802 lsp.shutdown_server(&language);
5803 tracing::info!("Stopped LSP server for {}", language);
5804 }
5805 if let Some(lsp_configs) = self.config_mut().lsp.get_mut(&language) {
5806 for c in lsp_configs.as_mut_slice() {
5807 c.enabled = false;
5808 c.auto_start = false;
5809 }
5810 tracing::info!("Disabled LSP config for {}", language);
5811 }
5812 if let Err(e) = self.save_config() {
5813 tracing::error!("Failed to save config: {}", e);
5814 self.active_window_mut().status_message = Some(format!(
5815 "LSP disabled for {} (config save failed)",
5816 language
5817 ));
5818 } else {
5819 self.active_window_mut().status_message =
5820 Some(format!("LSP disabled for {}", language));
5821 }
5822 self.active_window_mut().warning_domains.lsp.clear();
5823 }
5824
5825 fn handle_restart_lsp_for_language(&mut self, language: String) {
5826 tracing::info!("Plugin restarting LSP for language: {}", language);
5827 let file_path = self
5828 .active_window()
5829 .buffer_metadata
5830 .get(&self.active_buffer())
5831 .and_then(|meta| meta.file_path().cloned());
5832 let __active_id = self.active_window;
5833 let success = if let Some(lsp) = self
5834 .windows
5835 .get_mut(&__active_id)
5836 .and_then(|w| w.lsp.as_mut())
5837 {
5838 let (ok, msg) = lsp.manual_restart(&language, file_path.as_deref());
5839 self.active_window_mut().status_message = Some(msg);
5840 ok
5841 } else {
5842 self.active_window_mut().status_message = Some("No LSP manager available".to_string());
5843 false
5844 };
5845 if success {
5846 self.reopen_buffers_for_language(&language);
5847 }
5848 }
5849
5850 fn handle_set_lsp_root_uri(&mut self, language: String, uri: String) {
5851 tracing::info!("Plugin setting LSP root URI for {}: {}", language, uri);
5852 match uri.parse::<lsp_types::Uri>() {
5853 Ok(parsed_uri) => {
5854 let __active_id = self.active_window;
5855 if let Some(lsp) = self
5856 .windows
5857 .get_mut(&__active_id)
5858 .and_then(|w| w.lsp.as_mut())
5859 {
5860 let restarted = lsp.set_language_root_uri(&language, parsed_uri);
5861 if restarted {
5862 self.active_window_mut().status_message = Some(format!(
5863 "LSP root updated for {} (restarting server)",
5864 language
5865 ));
5866 } else {
5867 self.active_window_mut().status_message =
5868 Some(format!("LSP root set for {}", language));
5869 }
5870 }
5871 }
5872 Err(e) => {
5873 tracing::error!("Invalid LSP root URI '{}': {}", uri, e);
5874 self.active_window_mut().status_message =
5875 Some(format!("Invalid LSP root URI: {}", e));
5876 }
5877 }
5878 }
5879
5880 fn handle_create_scroll_sync_group(
5881 &mut self,
5882 group_id: crate::view::scroll_sync::ScrollSyncGroupId,
5883 left_split: SplitId,
5884 right_split: SplitId,
5885 ) {
5886 let success = self
5887 .active_window_mut()
5888 .scroll_sync_manager
5889 .create_group_with_id(group_id, left_split, right_split);
5890 if success {
5891 tracing::debug!(
5892 "Created scroll sync group {} for splits {:?} and {:?}",
5893 group_id,
5894 left_split,
5895 right_split
5896 );
5897 } else {
5898 tracing::warn!(
5899 "Failed to create scroll sync group {} (ID already exists)",
5900 group_id
5901 );
5902 }
5903 }
5904
5905 fn handle_set_scroll_sync_anchors(
5906 &mut self,
5907 group_id: crate::view::scroll_sync::ScrollSyncGroupId,
5908 anchors: Vec<(usize, usize)>,
5909 ) {
5910 use crate::view::scroll_sync::SyncAnchor;
5911 let anchor_count = anchors.len();
5912 let sync_anchors: Vec<SyncAnchor> = anchors
5913 .into_iter()
5914 .map(|(left_line, right_line)| SyncAnchor {
5915 left_line,
5916 right_line,
5917 })
5918 .collect();
5919 self.active_window_mut()
5920 .scroll_sync_manager
5921 .set_anchors(group_id, sync_anchors);
5922 tracing::debug!(
5923 "Set {} anchors for scroll sync group {}",
5924 anchor_count,
5925 group_id
5926 );
5927 }
5928
5929 fn handle_remove_scroll_sync_group(
5930 &mut self,
5931 group_id: crate::view::scroll_sync::ScrollSyncGroupId,
5932 ) {
5933 if self
5934 .active_window_mut()
5935 .scroll_sync_manager
5936 .remove_group(group_id)
5937 {
5938 tracing::debug!("Removed scroll sync group {}", group_id);
5939 } else {
5940 tracing::warn!("Scroll sync group {} not found", group_id);
5941 }
5942 }
5943
5944 fn handle_create_buffer_group(
5945 &mut self,
5946 name: String,
5947 mode: String,
5948 layout_json: String,
5949 request_id: Option<u64>,
5950 ) {
5951 match self.create_buffer_group(name, mode, layout_json) {
5952 Ok(result) => {
5953 if let Some(req_id) = request_id {
5954 let json = serde_json::to_string(&result).unwrap_or_default();
5955 self.plugin_manager
5956 .read()
5957 .unwrap()
5958 .resolve_callback(fresh_core::api::JsCallbackId::from(req_id), json);
5959 }
5960 }
5961 Err(e) => {
5962 tracing::error!("Failed to create buffer group: {}", e);
5963 }
5964 }
5965 }
5966
5967 fn handle_send_terminal_input(
5968 &mut self,
5969 terminal_id: crate::services::terminal::TerminalId,
5970 data: String,
5971 ) {
5972 if let Some(handle) = self.active_window().terminal_manager.get(terminal_id) {
5973 handle.write(data.as_bytes());
5974 tracing::trace!(
5975 "Plugin sent {} bytes to terminal {:?}",
5976 data.len(),
5977 terminal_id
5978 );
5979 } else {
5980 tracing::warn!(
5981 "Plugin tried to send input to non-existent terminal {:?}",
5982 terminal_id
5983 );
5984 }
5985 }
5986
5987 fn handle_close_terminal(&mut self, terminal_id: crate::services::terminal::TerminalId) {
5988 let buffer_to_close = self
5989 .active_window()
5990 .terminal_buffers
5991 .iter()
5992 .find(|(_, &tid)| tid == terminal_id)
5993 .map(|(&bid, _)| bid);
5994 if let Some(buffer_id) = buffer_to_close {
5995 if let Err(e) = self.close_buffer(buffer_id) {
5996 tracing::warn!("Failed to close terminal buffer: {}", e);
5997 }
5998 tracing::info!("Plugin closed terminal {:?}", terminal_id);
5999 } else {
6000 self.active_window_mut().terminal_manager.close(terminal_id);
6001 tracing::info!("Plugin closed terminal {:?} (no buffer found)", terminal_id);
6002 }
6003 }
6004
6005 fn handle_signal_window(&mut self, id: fresh_core::WindowId, signal: &str) {
6013 let Some(window) = self.windows.get_mut(&id) else {
6014 tracing::warn!("Plugin SignalWindow targeted unknown window {:?}", id);
6015 return;
6016 };
6017 let results = window.process_groups.signal_all(signal);
6018 for (entry, result) in results {
6019 match result {
6020 Ok(true) => tracing::info!(
6021 "SignalWindow {:?}: {} → pid {} ({})",
6022 id,
6023 signal,
6024 entry.leader_pid,
6025 entry.label
6026 ),
6027 Ok(false) => tracing::debug!(
6028 "SignalWindow {:?}: pid {} ({}) already exited",
6029 id,
6030 entry.leader_pid,
6031 entry.label
6032 ),
6033 Err(e) => tracing::warn!(
6034 "SignalWindow {:?}: pid {} ({}): {}",
6035 id,
6036 entry.leader_pid,
6037 entry.label,
6038 e
6039 ),
6040 }
6041 }
6042 }
6043}
6044
6045#[cfg(test)]
6046mod tests {
6047 use tokio::io::{AsyncReadExt, BufReader};
6060 use tokio::process::Command as TokioCommand;
6061 use tokio::time::{timeout, Duration};
6062
6063 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
6074 async fn kill_via_oneshot_terminates_long_running_child() {
6075 let mut cmd = TokioCommand::new("sleep");
6076 cmd.args(["30"]);
6077 cmd.stdout(std::process::Stdio::piped());
6078 cmd.stderr(std::process::Stdio::piped());
6079
6080 let mut child = cmd.spawn().expect("spawn sh -c sleep 30");
6081 let pid = child.id().expect("child has a pid");
6082
6083 let (kill_tx, mut kill_rx) = tokio::sync::oneshot::channel::<()>();
6084 let stdout_pipe = child.stdout.take();
6085 let stderr_pipe = child.stderr.take();
6086
6087 let stdout_fut = async {
6088 let mut buf = String::new();
6089 if let Some(s) = stdout_pipe {
6090 #[allow(clippy::let_underscore_must_use)]
6091 let _ = BufReader::new(s).read_to_string(&mut buf).await;
6092 }
6093 buf
6094 };
6095 let stderr_fut = async {
6096 let mut buf = String::new();
6097 if let Some(s) = stderr_pipe {
6098 #[allow(clippy::let_underscore_must_use)]
6099 let _ = BufReader::new(s).read_to_string(&mut buf).await;
6100 }
6101 buf
6102 };
6103 let wait_fut = async {
6104 tokio::select! {
6105 status = child.wait() => {
6106 status.map(|s| s.code().unwrap_or(-1)).unwrap_or(-1)
6107 }
6108 _ = &mut kill_rx => {
6109 #[allow(clippy::let_underscore_must_use)]
6110 let _ = child.start_kill();
6111 child
6112 .wait()
6113 .await
6114 .map(|s| s.code().unwrap_or(-1))
6115 .unwrap_or(-1)
6116 }
6117 }
6118 };
6119
6120 tokio::time::sleep(Duration::from_millis(50)).await;
6125 kill_tx.send(()).expect("kill channel send");
6126
6127 let result = timeout(Duration::from_secs(5), async {
6128 tokio::join!(stdout_fut, stderr_fut, wait_fut)
6129 })
6130 .await;
6131
6132 let (_stdout, _stderr, exit_code) = result.expect(
6133 "kill path must resolve within 5s — if this times out the \
6134 select! arm order or kill-then-wait logic is broken",
6135 );
6136 assert_ne!(
6148 exit_code, 0,
6149 "killed child must exit non-success (got 0 — did the \
6150 kill arm fire too late, or did sleep somehow complete?)"
6151 );
6152
6153 #[cfg(unix)]
6162 {
6163 let still_alive = std::process::Command::new("kill")
6164 .args(["-0", &pid.to_string()])
6165 .status()
6166 .map(|s| s.success())
6167 .unwrap_or(false);
6168 assert!(
6169 !still_alive,
6170 "process {pid} must be reaped after wait() — a still-\
6171 alive check means the kill path leaked the child"
6172 );
6173 }
6174 #[cfg(not(unix))]
6175 {
6176 let _ = pid;
6179 }
6180 }
6181}
6182
6183impl Window {
6184 #[cfg(feature = "plugins")]
6199 pub(crate) fn populate_plugin_state_snapshot(
6200 &mut self,
6201 snapshot: &mut fresh_core::api::EditorStateSnapshot,
6202 ) {
6203 use fresh_core::api::{BufferInfo, CursorInfo, ViewportInfo};
6204
6205 let current_gen = self.resources.grammar_registry.catalog_gen();
6211 if snapshot.last_grammar_gen != current_gen {
6212 snapshot.available_grammars = self
6213 .resources
6214 .grammar_registry
6215 .available_grammar_info()
6216 .into_iter()
6217 .map(|g| fresh_core::api::GrammarInfoSnapshot {
6218 name: g.name,
6219 source: g.source.to_string(),
6220 file_extensions: g.file_extensions,
6221 short_name: g.short_name,
6222 })
6223 .collect();
6224 snapshot.last_grammar_gen = current_gen;
6225 }
6226
6227 snapshot.active_buffer_id = self.active_buffer();
6228
6229 let (mgr_ref, vs_ref) = self
6230 .buffers
6231 .splits()
6232 .expect("active window must have a populated split layout");
6233 let active_split = mgr_ref.active_split();
6234 snapshot.active_split_id = active_split.0 .0;
6235
6236 snapshot.buffers.clear();
6238 snapshot.buffer_saved_diffs.clear();
6239 snapshot.buffer_cursor_positions.clear();
6240 snapshot.buffer_text_properties.clear();
6241
6242 let active_vs_opt = vs_ref.get(&active_split);
6243 for (buffer_id, state) in &self.buffers {
6244 let is_virtual = self
6245 .buffer_metadata
6246 .get(buffer_id)
6247 .map(|m| m.is_virtual())
6248 .unwrap_or(false);
6249 let view_mode = active_vs_opt
6254 .and_then(|vs| vs.buffer_state(*buffer_id))
6255 .map(|bs| match bs.view_mode {
6256 crate::state::ViewMode::Source => "source",
6257 crate::state::ViewMode::PageView => "compose",
6258 })
6259 .unwrap_or("source");
6260 let compose_width = active_vs_opt
6261 .and_then(|vs| vs.buffer_state(*buffer_id))
6262 .and_then(|bs| bs.compose_width);
6263 let is_composing_in_any_split = vs_ref.values().any(|vs| {
6264 vs.buffer_state(*buffer_id)
6265 .map(|bs| matches!(bs.view_mode, crate::state::ViewMode::PageView))
6266 .unwrap_or(false)
6267 });
6268 let is_preview = self
6269 .buffer_metadata
6270 .get(buffer_id)
6271 .map(|m| m.is_preview)
6272 .unwrap_or(false);
6273 let splits: Vec<fresh_core::SplitId> = mgr_ref
6279 .splits_for_buffer(*buffer_id)
6280 .into_iter()
6281 .map(|leaf_id| leaf_id.0)
6282 .collect();
6283 let buffer_info = BufferInfo {
6284 id: *buffer_id,
6285 path: state.buffer.file_path().map(|p| p.to_path_buf()),
6286 modified: state.buffer.is_modified(),
6287 length: state.buffer.len(),
6288 is_virtual,
6289 view_mode: view_mode.to_string(),
6290 is_composing_in_any_split,
6291 compose_width,
6292 language: state.language.clone(),
6293 is_preview,
6294 splits,
6295 };
6296 snapshot.buffers.insert(*buffer_id, buffer_info);
6297
6298 let diff = {
6299 let diff = state.buffer.diff_since_saved();
6300 BufferSavedDiff {
6301 equal: diff.equal,
6302 byte_ranges: diff.byte_ranges.clone(),
6303 }
6304 };
6305 snapshot.buffer_saved_diffs.insert(*buffer_id, diff);
6306
6307 let is_hidden = self
6316 .buffer_metadata
6317 .get(buffer_id)
6318 .is_some_and(|m| m.hidden_from_tabs);
6319 let source_split = vs_ref.iter().find(|(split_id, vs)| {
6320 vs.keyed_states.contains_key(buffer_id)
6321 && !(is_hidden && self.grouped_subtrees.contains_key(split_id))
6322 });
6323 let cursor_pos = source_split
6324 .and_then(|(_, vs)| vs.buffer_state(*buffer_id))
6325 .map(|bs| bs.cursors.primary().position)
6326 .unwrap_or(0);
6327 tracing::trace!(
6328 "snapshot: buffer {:?} cursor_pos={} (from split {:?})",
6329 buffer_id,
6330 cursor_pos,
6331 source_split.map(|(id, _)| *id),
6332 );
6333 snapshot
6334 .buffer_cursor_positions
6335 .insert(*buffer_id, cursor_pos);
6336
6337 if !state.text_properties.is_empty() {
6339 snapshot
6340 .buffer_text_properties
6341 .insert(*buffer_id, state.text_properties.all().to_vec());
6342 }
6343 }
6344
6345 let active_buf_id = snapshot.active_buffer_id;
6356 let active_split_id = self.effective_active_pair().0;
6357 self.buffers
6358 .with_all_mut(|buffers_mut, mgr, vs_map| {
6359 let _ = mgr; if let Some(active_vs) = vs_map.get(&active_split_id) {
6361 let active_cursors = &active_vs.cursors;
6363 let primary = active_cursors.primary();
6364 let primary_position = primary.position;
6365 let primary_selection = primary.selection_range();
6366
6367 let line_of = |offset: usize| -> Option<usize> {
6373 buffers_mut.get(&active_buf_id).and_then(|state| {
6374 if state.buffer.line_count().is_some() {
6375 Some(state.buffer.get_line_number(offset))
6376 } else {
6377 None
6378 }
6379 })
6380 };
6381
6382 snapshot.primary_cursor = Some(CursorInfo {
6383 position: primary_position,
6384 selection: primary_selection.clone(),
6385 line: line_of(primary_position),
6386 });
6387
6388 snapshot.all_cursors = active_cursors
6389 .iter()
6390 .map(|(_, cursor)| CursorInfo {
6391 position: cursor.position,
6392 selection: cursor.selection_range(),
6393 line: line_of(cursor.position),
6394 })
6395 .collect();
6396
6397 if let Some(range) = primary_selection {
6399 if let Some(active_state) = buffers_mut.get_mut(&active_buf_id) {
6400 snapshot.selected_text =
6401 Some(active_state.get_text_range(range.start, range.end));
6402 }
6403 }
6404
6405 let top_line = buffers_mut.get(&active_buf_id).and_then(|state| {
6407 if state.buffer.line_count().is_some() {
6408 Some(state.buffer.get_line_number(active_vs.viewport.top_byte))
6409 } else {
6410 None
6411 }
6412 });
6413 snapshot.viewport = Some(ViewportInfo {
6414 top_byte: active_vs.viewport.top_byte,
6415 top_line,
6416 left_column: active_vs.viewport.left_column,
6417 width: active_vs.viewport.width,
6418 height: active_vs.viewport.height,
6419 });
6420 } else {
6421 snapshot.primary_cursor = None;
6422 snapshot.all_cursors.clear();
6423 snapshot.viewport = None;
6424 snapshot.selected_text = None;
6425 }
6426
6427 snapshot.splits.clear();
6429 for (leaf_id, vs) in vs_map.iter() {
6430 let buf_id = vs.active_buffer;
6431 let top_line = buffers_mut.get(&buf_id).and_then(|state| {
6432 if state.buffer.line_count().is_some() {
6433 Some(state.buffer.get_line_number(vs.viewport.top_byte))
6434 } else {
6435 None
6436 }
6437 });
6438 snapshot.splits.push(fresh_core::api::SplitSnapshot {
6439 split_id: leaf_id.0 .0,
6440 buffer_id: buf_id,
6441 viewport: ViewportInfo {
6442 top_byte: vs.viewport.top_byte,
6443 top_line,
6444 left_column: vs.viewport.left_column,
6445 width: vs.viewport.width,
6446 height: vs.viewport.height,
6447 },
6448 });
6449 }
6450 })
6451 .expect("active window must have a populated split layout");
6452
6453 snapshot.active_session_plugin_states = self.plugin_state.clone();
6459 snapshot.diagnostics = Arc::clone(&self.stored_diagnostics);
6464 snapshot.folding_ranges = Arc::clone(&self.stored_folding_ranges);
6465
6466 snapshot.editor_mode = self.editor_mode.clone();
6468
6469 let active_split_id_u64 = active_split_id.0 .0;
6474 let split_changed = snapshot.plugin_view_states_split != active_split_id_u64;
6475 if split_changed {
6476 snapshot.plugin_view_states.clear();
6477 snapshot.plugin_view_states_split = active_split_id_u64;
6478 }
6479
6480 {
6482 let open_bids: Vec<_> = snapshot.buffers.keys().copied().collect();
6483 snapshot
6484 .plugin_view_states
6485 .retain(|bid, _| open_bids.contains(bid));
6486 }
6487
6488 if let Some(vs_map) = self.buffers.split_view_states() {
6490 if let Some(active_vs) = vs_map.get(&active_split_id) {
6491 for (buffer_id, buf_state) in &active_vs.keyed_states {
6492 if !buf_state.plugin_state.is_empty() {
6493 let entry = snapshot.plugin_view_states.entry(*buffer_id).or_default();
6494 for (key, value) in &buf_state.plugin_state {
6495 entry.entry(key.clone()).or_insert_with(|| value.clone());
6496 }
6497 }
6498 }
6499 }
6500 }
6501 }
6502}
6503
6504const HTTP_FETCH_MAX_BYTES: u64 = 64 * 1024 * 1024;
6508
6509fn fetch_url_to_file(url: &str, target: &std::path::Path) -> Result<u16, String> {
6515 let tls_config = ureq::tls::TlsConfig::builder()
6519 .root_certs(ureq::tls::RootCerts::PlatformVerifier)
6520 .build();
6521
6522 let agent = ureq::Agent::config_builder()
6523 .timeout_global(Some(std::time::Duration::from_secs(30)))
6524 .http_status_as_error(false)
6525 .tls_config(tls_config)
6526 .build()
6527 .new_agent();
6528
6529 let response = agent
6530 .get(url)
6531 .header("User-Agent", "fresh-editor")
6532 .call()
6533 .map_err(|e| format!("HTTP request failed: {}", e))?;
6534
6535 let status = response.status().as_u16();
6536 if !(200..300).contains(&status) {
6537 return Ok(status);
6538 }
6539
6540 let mut file = std::fs::File::create(target)
6541 .map_err(|e| format!("failed to create {}: {}", target.display(), e))?;
6542
6543 let mut reader = response
6544 .into_body()
6545 .into_with_config()
6546 .limit(HTTP_FETCH_MAX_BYTES)
6547 .reader();
6548
6549 std::io::copy(&mut reader, &mut file)
6550 .map_err(|e| format!("failed to write response body: {}", e))?;
6551
6552 Ok(status)
6553}