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 PluginCommand::ClearOverlaysInRangeForNamespace {
262 buffer_id,
263 namespace,
264 start,
265 end,
266 } => {
267 self.handle_clear_overlays_in_range_for_namespace(buffer_id, namespace, start, end);
268 }
269
270 PluginCommand::AddVirtualText {
272 buffer_id,
273 virtual_text_id,
274 position,
275 text,
276 color,
277 use_bg,
278 before,
279 } => {
280 self.handle_add_virtual_text(
281 buffer_id,
282 virtual_text_id,
283 position,
284 text,
285 color,
286 use_bg,
287 before,
288 );
289 }
290 PluginCommand::AddVirtualTextStyled {
291 buffer_id,
292 virtual_text_id,
293 position,
294 text,
295 fg,
296 bg,
297 bold,
298 italic,
299 before,
300 } => {
301 self.handle_add_virtual_text_styled(
302 buffer_id,
303 virtual_text_id,
304 position,
305 text,
306 fg,
307 bg,
308 bold,
309 italic,
310 before,
311 );
312 }
313 PluginCommand::RemoveVirtualText {
314 buffer_id,
315 virtual_text_id,
316 } => {
317 self.handle_remove_virtual_text(buffer_id, virtual_text_id);
318 }
319 PluginCommand::RemoveVirtualTextsByPrefix { buffer_id, prefix } => {
320 self.handle_remove_virtual_texts_by_prefix(buffer_id, prefix);
321 }
322 PluginCommand::ClearVirtualTexts { buffer_id } => {
323 self.handle_clear_virtual_texts(buffer_id);
324 }
325 PluginCommand::AddVirtualLine {
326 buffer_id,
327 position,
328 text,
329 fg_color,
330 bg_color,
331 above,
332 namespace,
333 priority,
334 gutter_glyph,
335 gutter_color,
336 text_overlays,
337 } => {
338 self.handle_add_virtual_line(
339 buffer_id,
340 position,
341 text,
342 fg_color,
343 bg_color,
344 above,
345 namespace,
346 priority,
347 gutter_glyph,
348 gutter_color,
349 text_overlays,
350 );
351 }
352 PluginCommand::ClearVirtualTextNamespace {
353 buffer_id,
354 namespace,
355 } => {
356 self.handle_clear_virtual_text_namespace(buffer_id, namespace);
357 }
358
359 PluginCommand::AddConceal {
361 buffer_id,
362 namespace,
363 start,
364 end,
365 replacement,
366 } => {
367 self.handle_add_conceal(buffer_id, namespace, start, end, replacement);
368 }
369 PluginCommand::ClearConcealNamespace {
370 buffer_id,
371 namespace,
372 } => {
373 self.handle_clear_conceal_namespace(buffer_id, namespace);
374 }
375 PluginCommand::ClearConcealsInRange {
376 buffer_id,
377 start,
378 end,
379 } => {
380 self.handle_clear_conceals_in_range(buffer_id, start, end);
381 }
382
383 PluginCommand::AddFold {
384 buffer_id,
385 start,
386 end,
387 placeholder,
388 } => {
389 self.handle_add_fold(buffer_id, start, end, placeholder);
390 }
391 PluginCommand::ClearFolds { buffer_id } => {
392 self.handle_clear_folds(buffer_id);
393 }
394 PluginCommand::SetFoldingRanges { buffer_id, ranges } => {
395 self.handle_set_folding_ranges(buffer_id, ranges);
396 }
397
398 PluginCommand::AddSoftBreak {
400 buffer_id,
401 namespace,
402 position,
403 indent,
404 } => {
405 self.handle_add_soft_break(buffer_id, namespace, position, indent);
406 }
407 PluginCommand::ClearSoftBreakNamespace {
408 buffer_id,
409 namespace,
410 } => {
411 self.handle_clear_soft_break_namespace(buffer_id, namespace);
412 }
413 PluginCommand::ClearSoftBreaksInRange {
414 buffer_id,
415 start,
416 end,
417 } => {
418 self.handle_clear_soft_breaks_in_range(buffer_id, start, end);
419 }
420
421 PluginCommand::AddMenuItem {
423 menu_label,
424 item,
425 position,
426 } => {
427 self.handle_add_menu_item(menu_label, item, position);
428 }
429 PluginCommand::AddMenu { menu, position } => {
430 self.handle_add_menu(menu, position);
431 }
432 PluginCommand::RemoveMenuItem {
433 menu_label,
434 item_label,
435 } => {
436 self.handle_remove_menu_item(menu_label, item_label);
437 }
438 PluginCommand::RemoveMenu { menu_label } => {
439 self.handle_remove_menu(menu_label);
440 }
441
442 PluginCommand::FocusSplit { split_id } => {
444 self.handle_focus_split(split_id);
445 }
446 PluginCommand::SetSplitBuffer {
447 split_id,
448 buffer_id,
449 } => {
450 self.handle_set_split_buffer(split_id, buffer_id);
451 }
452 PluginCommand::SetSplitScroll { split_id, top_byte } => {
453 self.handle_set_split_scroll(split_id, top_byte);
454 }
455 PluginCommand::RequestHighlights {
456 buffer_id,
457 range,
458 request_id,
459 } => {
460 self.handle_request_highlights(buffer_id, range, request_id);
461 }
462 PluginCommand::CloseSplit { split_id } => {
463 self.handle_close_split(split_id);
464 }
465 PluginCommand::SetSplitRatio { split_id, ratio } => {
466 self.handle_set_split_ratio(split_id, ratio);
467 }
468 PluginCommand::SetSplitLabel { split_id, label } => {
469 self.windows
470 .get_mut(&self.active_window)
471 .and_then(|w| w.split_manager_mut())
472 .expect("active window must have a populated split layout")
473 .set_label(LeafId(split_id), label);
474 }
475 PluginCommand::ClearSplitLabel { split_id } => {
476 self.windows
477 .get_mut(&self.active_window)
478 .and_then(|w| w.split_manager_mut())
479 .expect("active window must have a populated split layout")
480 .clear_label(split_id);
481 }
482 PluginCommand::GetSplitByLabel { label, request_id } => {
483 self.handle_get_split_by_label(label, request_id);
484 }
485 PluginCommand::DistributeSplitsEvenly { split_ids: _ } => {
486 self.handle_distribute_splits_evenly();
487 }
488 PluginCommand::SetBufferCursor {
489 buffer_id,
490 position,
491 } => {
492 self.handle_set_buffer_cursor(buffer_id, position);
493 }
494 PluginCommand::SetBufferShowCursors { buffer_id, show } => {
495 self.handle_set_buffer_show_cursors(buffer_id, show);
496 }
497
498 PluginCommand::SetLayoutHints {
500 buffer_id,
501 split_id,
502 range: _,
503 hints,
504 } => {
505 self.handle_set_layout_hints(buffer_id, split_id, hints);
506 }
507 PluginCommand::SetLineNumbers { buffer_id, enabled } => {
508 self.handle_set_line_numbers(buffer_id, enabled);
509 }
510 PluginCommand::SetViewMode { buffer_id, mode } => {
511 self.handle_set_view_mode(buffer_id, &mode);
512 }
513 PluginCommand::SetLineWrap {
514 buffer_id,
515 split_id,
516 enabled,
517 } => {
518 self.handle_set_line_wrap(buffer_id, split_id, enabled);
519 }
520 PluginCommand::SubmitViewTransform {
521 buffer_id,
522 split_id,
523 payload,
524 } => {
525 self.handle_submit_view_transform(buffer_id, split_id, payload);
526 }
527 PluginCommand::ClearViewTransform {
528 buffer_id: _,
529 split_id,
530 } => {
531 self.handle_clear_view_transform(split_id);
532 }
533 PluginCommand::SetViewState {
534 buffer_id,
535 key,
536 value,
537 } => {
538 self.handle_set_view_state(buffer_id, key, value);
539 }
540 PluginCommand::SetGlobalState {
541 plugin_name,
542 key,
543 value,
544 } => {
545 self.handle_set_global_state(plugin_name, key, value);
546 }
547 PluginCommand::SetWindowState {
548 plugin_name,
549 key,
550 value,
551 } => {
552 self.handle_set_session_state(plugin_name, key, value);
553 }
554 PluginCommand::RefreshLines { buffer_id } => {
555 self.handle_refresh_lines(buffer_id);
556 }
557 PluginCommand::RefreshAllLines => {
558 self.handle_refresh_all_lines();
559 }
560 PluginCommand::HookCompleted { .. } => {
561 }
563 PluginCommand::SetLineIndicator {
564 buffer_id,
565 line,
566 namespace,
567 symbol,
568 color,
569 priority,
570 } => {
571 self.handle_set_line_indicator(buffer_id, line, namespace, symbol, color, priority);
572 }
573 PluginCommand::SetLineIndicators {
574 buffer_id,
575 lines,
576 namespace,
577 symbol,
578 color,
579 priority,
580 } => {
581 self.handle_set_line_indicators(
582 buffer_id, lines, namespace, symbol, color, priority,
583 );
584 }
585 PluginCommand::ClearLineIndicators {
586 buffer_id,
587 namespace,
588 } => {
589 self.handle_clear_line_indicators(buffer_id, namespace);
590 }
591 PluginCommand::SetFileExplorerDecorations {
592 namespace,
593 decorations,
594 } => {
595 self.active_window_mut()
596 .handle_set_file_explorer_decorations(namespace, decorations);
597 }
598 PluginCommand::ClearFileExplorerDecorations { namespace } => {
599 self.active_window_mut()
600 .handle_clear_file_explorer_decorations(&namespace);
601 }
602
603 PluginCommand::SetStatus { message } => {
605 self.handle_set_status(message);
606 }
607 PluginCommand::ApplyTheme { theme_name } => {
608 self.apply_theme(&theme_name);
609 }
610 PluginCommand::OverrideThemeColors { overrides } => {
611 self.handle_override_theme_colors(overrides);
612 }
613 PluginCommand::ReloadConfig => {
614 self.reload_config();
615 }
616 PluginCommand::SetSetting { path, value, .. } => {
617 self.handle_set_setting(path, value);
618 }
619 PluginCommand::AddPluginConfigField {
620 plugin_name,
621 field_name,
622 field_schema,
623 } => {
624 self.handle_add_plugin_config_field(plugin_name, field_name, field_schema);
625 }
626 PluginCommand::ReloadThemes { apply_theme } => {
627 self.reload_themes();
628 if let Some(theme_name) = apply_theme {
629 self.apply_theme(&theme_name);
630 }
631 }
632 PluginCommand::RegisterGrammar {
633 language,
634 grammar_path,
635 extensions,
636 } => {
637 self.handle_register_grammar(language, grammar_path, extensions);
638 }
639 PluginCommand::RegisterLanguageConfig { language, config } => {
640 self.handle_register_language_config(language, config);
641 }
642 PluginCommand::RegisterLspServer { language, config } => {
643 self.handle_register_lsp_server(language, config);
644 }
645 PluginCommand::ReloadGrammars { callback_id } => {
646 self.handle_reload_grammars(callback_id);
647 }
648 PluginCommand::CancelPrompt => {
649 self.cancel_prompt();
650 }
651 PluginCommand::StartPrompt {
652 label,
653 prompt_type,
654 floating_overlay,
655 } => {
656 self.handle_start_prompt(label, prompt_type, floating_overlay);
657 }
658 PluginCommand::StartPromptWithInitial {
659 label,
660 prompt_type,
661 initial_value,
662 floating_overlay,
663 } => {
664 self.handle_start_prompt_with_initial(
665 label,
666 prompt_type,
667 initial_value,
668 floating_overlay,
669 );
670 }
671 PluginCommand::StartPromptAsync {
672 label,
673 initial_value,
674 callback_id,
675 } => {
676 self.handle_start_prompt_async(label, initial_value, callback_id);
677 }
678 PluginCommand::AwaitNextKey { callback_id } => {
679 self.handle_await_next_key(callback_id);
680 }
681 PluginCommand::SetKeyCaptureActive { active } => {
682 self.active_window_mut().key_capture_active = active;
683 if !active {
684 self.active_window_mut().pending_key_capture_buffer.clear();
688 }
689 }
690 PluginCommand::SetPromptSuggestions { suggestions } => {
691 self.handle_set_prompt_suggestions(suggestions);
692 }
693 PluginCommand::SetPromptInputSync { sync } => {
694 if let Some(prompt) = &mut self.active_window_mut().prompt {
695 prompt.sync_input_on_navigate = sync;
696 }
697 }
698 PluginCommand::SetPromptTitle { title } => {
699 if let Some(prompt) = &mut self.active_window_mut().prompt {
700 prompt.title = title;
701 }
702 }
703 PluginCommand::SetPromptFooter { footer } => {
704 if let Some(prompt) = &mut self.active_window_mut().prompt {
705 prompt.footer = footer;
706 }
707 }
708 PluginCommand::SetPromptToolbar { spec } => {
709 if let Some(prompt) = &mut self.active_window_mut().prompt {
710 prompt.toolbar_widget = spec;
711 }
712 }
713 PluginCommand::ToggleOverlayToolbarWidget { key } => {
714 self.toggle_overlay_toolbar_widget(&key);
715 }
716 PluginCommand::SetPromptStatus { status } => {
717 if let Some(prompt) = &mut self.active_window_mut().prompt {
718 prompt.status = status;
719 }
720 }
721 PluginCommand::SetPromptSelectedIndex { index } => {
722 if let Some(prompt) = &mut self.active_window_mut().prompt {
723 let len = prompt.suggestions.len();
724 if len > 0 {
725 let clamped = (index as usize).min(len - 1);
726 prompt.selected_suggestion = Some(clamped);
727 }
728 }
729 }
730
731 PluginCommand::CreateWindow { root, label } => {
734 if !root.is_absolute() {
735 tracing::warn!(
736 "CreateWindow rejected: root must be absolute, got {:?}",
737 root
738 );
739 } else {
740 let _ = self.create_window_at(root, label);
741 }
742 }
743 PluginCommand::CreateWindowWithTerminal {
744 root,
745 label,
746 cwd,
747 command,
748 title,
749 request_id,
750 } => {
751 self.handle_create_window_with_terminal(
752 root, label, cwd, command, title, request_id,
753 );
754 }
755 PluginCommand::SetActiveWindow { id } => {
756 self.set_active_window(id);
757 }
758 PluginCommand::CloseWindow { id } => {
759 let _ = self.close_window(id);
760 }
761 PluginCommand::PrewarmWindow { id } => {
762 self.prewarm_window(id);
763 }
764
765 PluginCommand::WatchPath {
767 path,
768 recursive,
769 request_id,
770 } => {
771 let result = if let Some(ref bridge) = self.async_bridge {
772 self.file_watcher_manager.watch(bridge, &path, recursive)
773 } else {
774 Err(
775 "watchPath: no async bridge — file watching is unavailable in this build"
776 .to_string(),
777 )
778 };
779 self.last_watch_response_for_test = Some((request_id, result.clone()));
780 self.send_plugin_response(fresh_core::api::PluginResponse::WatchPathRegistered {
781 request_id,
782 result,
783 });
784 }
785 PluginCommand::UnwatchPath { handle } => {
786 self.file_watcher_manager.unwatch(handle);
787 }
788
789 PluginCommand::PreviewWindowInRect { id } => {
790 self.preview_window_id = match id {
794 Some(sid) if sid != self.active_window && self.windows.contains_key(&sid) => {
795 Some(sid)
796 }
797 _ => None,
798 };
799 }
800
801 PluginCommand::RegisterCommand { command } => {
803 self.handle_register_command(command);
804 }
805 PluginCommand::RegisterStatusBarElement {
806 plugin_name,
807 token_name,
808 title,
809 } => {
810 if let Err(e) = self.register_status_bar_element(&plugin_name, &token_name, &title)
811 {
812 tracing::warn!("Failed to register statusbar element: {}", e);
813 }
814 }
815 PluginCommand::SetStatusBarValue {
816 buffer_id,
817 key,
818 value,
819 } => {
820 if let Err(e) =
821 self.set_status_bar_value(fresh_core::BufferId(buffer_id as usize), &key, value)
822 {
823 tracing::debug!("Skipped statusbar value for stale buffer: {}", e);
829 }
830 }
831 PluginCommand::UnregisterCommand { name } => {
832 self.handle_unregister_command(name);
833 }
834 PluginCommand::DefineMode {
835 name,
836 bindings,
837 read_only,
838 allow_text_input,
839 inherit_normal_bindings,
840 plugin_name,
841 } => {
842 self.handle_define_mode(
843 name,
844 bindings,
845 read_only,
846 allow_text_input,
847 inherit_normal_bindings,
848 plugin_name,
849 );
850 }
851
852 PluginCommand::OpenFileInBackground { path, window_id } => {
854 let route_to_inactive = match window_id {
855 Some(id) if id != self.active_window && self.windows.contains_key(&id) => {
856 Some(id)
857 }
858 _ => None,
859 };
860 if let Some(target) = route_to_inactive {
861 self.handle_open_file_in_inactive_session(target, path);
862 } else {
863 self.handle_open_file_in_background(path);
864 }
865 }
866 PluginCommand::OpenFileAtLocation { path, line, column } => {
867 return self.handle_open_file_at_location(path, line, column);
868 }
869 PluginCommand::OpenFileInSplit {
870 split_id,
871 path,
872 line,
873 column,
874 } => {
875 return self.handle_open_file_in_split(split_id, path, line, column);
876 }
877 PluginCommand::ShowBuffer { buffer_id } => {
878 self.handle_show_buffer(buffer_id);
879 }
880 PluginCommand::CloseBuffer { buffer_id } => {
881 self.handle_close_buffer(buffer_id);
882 }
883 PluginCommand::CloseOtherBuffersInSplit {
884 buffer_id,
885 split_id,
886 } => {
887 self.handle_close_other_buffers_in_split(buffer_id, split_id);
888 }
889 PluginCommand::CloseAllBuffersInSplit { split_id } => {
890 self.handle_close_all_buffers_in_split(split_id);
891 }
892 PluginCommand::CloseBuffersToRightInSplit {
893 buffer_id,
894 split_id,
895 } => {
896 self.handle_close_buffers_to_right_in_split(buffer_id, split_id);
897 }
898 PluginCommand::CloseBuffersToLeftInSplit {
899 buffer_id,
900 split_id,
901 } => {
902 self.handle_close_buffers_to_left_in_split(buffer_id, split_id);
903 }
904
905 PluginCommand::MoveTabLeft => {
906 self.handle_move_tab_left();
907 }
908 PluginCommand::MoveTabRight => {
909 self.handle_move_tab_right();
910 }
911
912 PluginCommand::StartAnimationArea { id, rect, kind } => {
914 self.handle_start_animation_area(id, rect, kind);
915 }
916 PluginCommand::StartAnimationVirtualBuffer {
917 id,
918 buffer_id,
919 kind,
920 } => {
921 self.handle_start_animation_virtual_buffer(id, buffer_id, kind);
922 }
923 PluginCommand::CancelAnimation { id } => {
924 self.active_window_mut()
925 .animations
926 .cancel(crate::view::animation::AnimationId::from_raw(id));
927 }
928
929 PluginCommand::SendLspRequest {
931 language,
932 method,
933 params,
934 request_id,
935 } => {
936 self.handle_send_lsp_request(language, method, params, request_id);
937 }
938
939 PluginCommand::SetClipboard { text } => {
941 self.handle_set_clipboard(text);
942 }
943
944 PluginCommand::SpawnProcess {
946 command,
947 args,
948 cwd,
949 stdout_to,
950 callback_id,
951 } => {
952 self.handle_spawn_process(command, args, cwd, stdout_to, callback_id);
953 }
954
955 PluginCommand::SpawnHostProcess {
956 command,
957 args,
958 cwd,
959 callback_id,
960 } => {
961 self.handle_spawn_host_process(command, args, cwd, callback_id);
962 }
963
964 PluginCommand::KillHostProcess { process_id } => {
965 self.handle_kill_host_process(process_id);
966 }
967
968 PluginCommand::SetAuthority { payload } => {
969 self.handle_set_authority(payload);
970 }
971
972 PluginCommand::ClearAuthority => {
973 tracing::info!("Plugin cleared authority; restoring local");
974 self.clear_authority();
975 }
976
977 PluginCommand::SetEnv { snippet, dir } => {
978 use crate::services::workspace_trust::TrustLevel;
982 if self.authority.workspace_trust.level() == TrustLevel::Trusted {
983 self.authority
984 .env_provider
985 .set(snippet, dir.map(std::path::PathBuf::from));
986 self.request_restart(self.working_dir().to_path_buf());
988 } else {
989 self.active_window_mut().status_message =
990 Some("Workspace not trusted — cannot activate environment".to_string());
991 }
992 }
993
994 PluginCommand::ClearEnv => {
995 let was_active = self.authority.env_provider.is_active();
996 self.authority.env_provider.clear();
997 if was_active {
998 self.request_restart(self.working_dir().to_path_buf());
999 }
1000 }
1001
1002 PluginCommand::SetRemoteIndicatorState { state } => {
1003 self.handle_set_remote_indicator_state(state);
1004 }
1005
1006 PluginCommand::ClearRemoteIndicatorState => {
1007 self.remote_indicator_override = None;
1008 }
1009
1010 PluginCommand::SpawnProcessWait {
1011 process_id,
1012 callback_id,
1013 } => {
1014 self.handle_spawn_process_wait(process_id, callback_id);
1015 }
1016
1017 PluginCommand::Delay {
1018 callback_id,
1019 duration_ms,
1020 } => {
1021 self.handle_delay(callback_id, duration_ms);
1022 }
1023
1024 PluginCommand::HttpFetch {
1025 url,
1026 target_path,
1027 callback_id,
1028 } => {
1029 self.handle_http_fetch(url, target_path, callback_id);
1030 }
1031
1032 PluginCommand::SpawnBackgroundProcess {
1033 process_id,
1034 command,
1035 args,
1036 cwd,
1037 callback_id,
1038 } => {
1039 self.handle_spawn_background_process(process_id, command, args, cwd, callback_id);
1040 }
1041
1042 PluginCommand::KillBackgroundProcess { process_id } => {
1043 self.handle_kill_background_process(process_id);
1044 }
1045
1046 PluginCommand::CreateVirtualBuffer {
1048 name,
1049 mode,
1050 read_only,
1051 } => {
1052 self.handle_create_virtual_buffer(name, mode, read_only);
1053 }
1054 PluginCommand::CreateVirtualBufferWithContent {
1055 name,
1056 mode,
1057 read_only,
1058 entries,
1059 show_line_numbers,
1060 show_cursors,
1061 editing_disabled,
1062 hidden_from_tabs,
1063 request_id,
1064 } => {
1065 self.handle_create_virtual_buffer_with_content(
1066 name,
1067 mode,
1068 read_only,
1069 entries,
1070 show_line_numbers,
1071 show_cursors,
1072 editing_disabled,
1073 hidden_from_tabs,
1074 request_id,
1075 );
1076 }
1077 PluginCommand::CreateVirtualBufferInSplit {
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 self.handle_create_virtual_buffer_in_split(
1094 name,
1095 mode,
1096 read_only,
1097 entries,
1098 ratio,
1099 direction,
1100 panel_id,
1101 show_line_numbers,
1102 show_cursors,
1103 editing_disabled,
1104 line_wrap,
1105 before,
1106 role,
1107 request_id,
1108 );
1109 }
1110 PluginCommand::SetVirtualBufferContent { buffer_id, entries } => {
1111 self.handle_set_virtual_buffer_content(buffer_id, entries);
1112 }
1113 PluginCommand::GetTextPropertiesAtCursor { buffer_id } => {
1114 self.handle_get_text_properties_at_cursor(buffer_id);
1115 }
1116 PluginCommand::CreateVirtualBufferInExistingSplit {
1117 name,
1118 mode,
1119 read_only,
1120 entries,
1121 split_id,
1122 show_line_numbers,
1123 show_cursors,
1124 editing_disabled,
1125 line_wrap,
1126 request_id,
1127 } => {
1128 self.handle_create_virtual_buffer_in_existing_split(
1129 name,
1130 mode,
1131 read_only,
1132 entries,
1133 split_id,
1134 show_line_numbers,
1135 show_cursors,
1136 editing_disabled,
1137 line_wrap,
1138 request_id,
1139 );
1140 }
1141
1142 PluginCommand::SetContext { name, active } => {
1144 self.handle_set_context(name, active);
1145 }
1146
1147 PluginCommand::SetReviewDiffHunks { hunks } => {
1149 self.active_window_mut().review_hunks = hunks;
1150 tracing::debug!(
1151 "Set {} review hunks",
1152 self.active_window_mut().review_hunks.len()
1153 );
1154 }
1155
1156 PluginCommand::ExecuteAction { action_name } => {
1158 self.handle_execute_action(action_name);
1159 }
1160 PluginCommand::ExecuteActions { actions } => {
1161 self.handle_execute_actions(actions);
1162 }
1163 PluginCommand::GetBufferText {
1164 buffer_id,
1165 start,
1166 end,
1167 request_id,
1168 } => {
1169 self.handle_get_buffer_text(buffer_id, start, end, request_id);
1170 }
1171 PluginCommand::GetLineStartPosition {
1172 buffer_id,
1173 line,
1174 request_id,
1175 } => {
1176 self.handle_get_line_start_position(buffer_id, line, request_id);
1177 }
1178 PluginCommand::GetLineEndPosition {
1179 buffer_id,
1180 line,
1181 request_id,
1182 } => {
1183 self.handle_get_line_end_position(buffer_id, line, request_id);
1184 }
1185 PluginCommand::GetBufferLineCount {
1186 buffer_id,
1187 request_id,
1188 } => {
1189 self.handle_get_buffer_line_count(buffer_id, request_id);
1190 }
1191 PluginCommand::OpenFileStreaming { path, request_id } => {
1192 self.handle_open_file_streaming(path, request_id);
1193 }
1194 PluginCommand::RefreshBufferFromDisk {
1195 buffer_id,
1196 request_id,
1197 } => {
1198 self.handle_refresh_buffer_from_disk(buffer_id, request_id);
1199 }
1200 PluginCommand::SetBufferGroupPanelBuffer {
1201 group_id,
1202 panel_name,
1203 buffer_id,
1204 request_id,
1205 } => {
1206 self.handle_set_buffer_group_panel_buffer(
1207 group_id, panel_name, buffer_id, request_id,
1208 );
1209 }
1210 PluginCommand::ScrollToLineCenter {
1211 split_id,
1212 buffer_id,
1213 line,
1214 } => {
1215 self.handle_scroll_to_line_center(split_id, buffer_id, line);
1216 }
1217 PluginCommand::ScrollBufferToLine { buffer_id, line } => {
1218 self.handle_scroll_buffer_to_line(buffer_id, line);
1219 }
1220 PluginCommand::SetEditorMode { mode } => {
1221 self.handle_set_editor_mode(mode);
1222 }
1223
1224 PluginCommand::ShowActionPopup {
1226 popup_id,
1227 title,
1228 message,
1229 actions,
1230 } => {
1231 self.handle_show_action_popup(popup_id, title, message, actions);
1232 }
1233
1234 PluginCommand::SetLspMenuContributions {
1235 plugin_id,
1236 language,
1237 items,
1238 } => {
1239 self.handle_set_lsp_menu_contributions(plugin_id, language, items);
1240 }
1241
1242 PluginCommand::DisableLspForLanguage { language } => {
1243 self.handle_disable_lsp_for_language(language);
1244 }
1245
1246 PluginCommand::RestartLspForLanguage { language } => {
1247 self.handle_restart_lsp_for_language(language);
1248 }
1249
1250 PluginCommand::SetLspRootUri { language, uri } => {
1251 self.handle_set_lsp_root_uri(language, uri);
1252 }
1253
1254 PluginCommand::CreateScrollSyncGroup {
1256 group_id,
1257 left_split,
1258 right_split,
1259 } => {
1260 self.handle_create_scroll_sync_group(group_id, left_split, right_split);
1261 }
1262 PluginCommand::SetScrollSyncAnchors { group_id, anchors } => {
1263 self.handle_set_scroll_sync_anchors(group_id, anchors);
1264 }
1265 PluginCommand::RemoveScrollSyncGroup { group_id } => {
1266 self.handle_remove_scroll_sync_group(group_id);
1267 }
1268
1269 PluginCommand::CreateCompositeBuffer {
1271 name,
1272 mode,
1273 layout,
1274 sources,
1275 hunks,
1276 initial_focus_hunk,
1277 request_id,
1278 } => {
1279 self.handle_create_composite_buffer(
1280 name,
1281 mode,
1282 layout,
1283 sources,
1284 hunks,
1285 initial_focus_hunk,
1286 request_id,
1287 );
1288 }
1289 PluginCommand::UpdateCompositeAlignment { buffer_id, hunks } => {
1290 self.handle_update_composite_alignment(buffer_id, hunks);
1291 }
1292 PluginCommand::CloseCompositeBuffer { buffer_id } => {
1293 self.active_window_mut().close_composite_buffer(buffer_id);
1294 }
1295 PluginCommand::FlushLayout => {
1296 self.flush_layout();
1297 }
1298 PluginCommand::CompositeNextHunk { buffer_id } => {
1299 let split_id = self
1300 .windows
1301 .get(&self.active_window)
1302 .and_then(|w| w.buffers.splits())
1303 .map(|(mgr, _)| mgr)
1304 .expect("active window must have a populated split layout")
1305 .active_split();
1306 self.active_window_mut()
1307 .composite_next_hunk(split_id, buffer_id);
1308 }
1309 PluginCommand::CompositePrevHunk { buffer_id } => {
1310 let split_id = self
1311 .windows
1312 .get(&self.active_window)
1313 .and_then(|w| w.buffers.splits())
1314 .map(|(mgr, _)| mgr)
1315 .expect("active window must have a populated split layout")
1316 .active_split();
1317 self.active_window_mut()
1318 .composite_prev_hunk(split_id, buffer_id);
1319 }
1320
1321 PluginCommand::CreateBufferGroup {
1323 name,
1324 mode,
1325 layout_json,
1326 request_id,
1327 } => {
1328 self.handle_create_buffer_group(name, mode, layout_json, request_id);
1329 }
1330 PluginCommand::SetPanelContent {
1331 group_id,
1332 panel_name,
1333 entries,
1334 } => {
1335 self.set_panel_content(group_id, panel_name, entries);
1336 }
1337 PluginCommand::CloseBufferGroup { group_id } => {
1338 self.close_buffer_group(group_id);
1339 }
1340 PluginCommand::FocusPanel {
1341 group_id,
1342 panel_name,
1343 } => {
1344 self.focus_panel(group_id, panel_name);
1345 }
1346
1347 PluginCommand::SaveBufferToPath { buffer_id, path } => {
1349 self.handle_save_buffer_to_path(buffer_id, path);
1350 }
1351
1352 #[cfg(feature = "plugins")]
1354 PluginCommand::LoadPlugin { path, callback_id } => {
1355 self.handle_load_plugin(path, callback_id);
1356 }
1357 #[cfg(feature = "plugins")]
1358 PluginCommand::UnloadPlugin { name, callback_id } => {
1359 self.handle_unload_plugin(name, callback_id);
1360 }
1361 #[cfg(feature = "plugins")]
1362 PluginCommand::ReloadPlugin { name, callback_id } => {
1363 self.handle_reload_plugin(name, callback_id);
1364 }
1365 #[cfg(feature = "plugins")]
1366 PluginCommand::ListPlugins { callback_id } => {
1367 self.handle_list_plugins(callback_id);
1368 }
1369 #[cfg(not(feature = "plugins"))]
1371 PluginCommand::LoadPlugin { .. }
1372 | PluginCommand::UnloadPlugin { .. }
1373 | PluginCommand::ReloadPlugin { .. }
1374 | PluginCommand::ListPlugins { .. } => {
1375 tracing::warn!("Plugin management commands require the 'plugins' feature");
1376 }
1377
1378 PluginCommand::CreateTerminal {
1380 cwd,
1381 direction,
1382 ratio,
1383 focus,
1384 persistent,
1385 window_id,
1386 command,
1387 title,
1388 request_id,
1389 } => {
1390 self.handle_create_terminal(
1391 cwd, direction, ratio, focus, persistent, window_id, command, title, request_id,
1392 );
1393 }
1394
1395 PluginCommand::SendTerminalInput { terminal_id, data } => {
1396 self.handle_send_terminal_input(terminal_id, data);
1397 }
1398
1399 PluginCommand::CloseTerminal { terminal_id } => {
1400 self.handle_close_terminal(terminal_id);
1401 }
1402
1403 PluginCommand::SignalWindow { id, signal } => {
1404 self.handle_signal_window(id, &signal);
1405 }
1406
1407 PluginCommand::GrepProject {
1408 pattern,
1409 fixed_string,
1410 case_sensitive,
1411 max_results,
1412 whole_words,
1413 callback_id,
1414 } => {
1415 self.handle_grep_project(
1416 pattern,
1417 fixed_string,
1418 case_sensitive,
1419 max_results,
1420 whole_words,
1421 callback_id,
1422 );
1423 }
1424
1425 PluginCommand::BeginSearch {
1426 pattern,
1427 fixed_string,
1428 case_sensitive,
1429 max_results,
1430 whole_words,
1431 source_buffer_id,
1432 handle_id,
1433 } => {
1434 self.handle_begin_search(
1435 pattern,
1436 fixed_string,
1437 case_sensitive,
1438 max_results,
1439 whole_words,
1440 source_buffer_id,
1441 handle_id,
1442 );
1443 }
1444
1445 PluginCommand::ReplaceInBuffer {
1446 file_path,
1447 buffer_id,
1448 matches,
1449 replacement,
1450 callback_id,
1451 } => {
1452 self.handle_replace_in_buffer(
1453 file_path,
1454 buffer_id,
1455 matches,
1456 replacement,
1457 callback_id,
1458 );
1459 }
1460
1461 PluginCommand::MountWidgetPanel {
1462 panel_id,
1463 buffer_id,
1464 spec,
1465 } => {
1466 self.handle_mount_widget_panel(panel_id, buffer_id, spec);
1467 }
1468
1469 PluginCommand::UpdateWidgetPanel { panel_id, spec } => {
1470 self.handle_update_widget_panel(panel_id, spec);
1471 }
1472
1473 PluginCommand::UnmountWidgetPanel { panel_id } => {
1474 self.handle_unmount_widget_panel(panel_id);
1475 }
1476
1477 PluginCommand::WidgetCommand { panel_id, action } => {
1478 self.handle_widget_command(panel_id, action);
1479 }
1480
1481 PluginCommand::WidgetMutate { panel_id, mutation } => {
1482 self.handle_widget_mutate(panel_id, mutation);
1483 }
1484
1485 PluginCommand::MountFloatingWidget {
1486 panel_id,
1487 spec,
1488 width_pct,
1489 height_pct,
1490 } => {
1491 self.handle_mount_floating_widget(panel_id, spec, width_pct, height_pct);
1492 }
1493
1494 PluginCommand::UpdateFloatingWidget { panel_id, spec } => {
1495 self.handle_update_floating_widget(panel_id, spec);
1496 }
1497
1498 PluginCommand::UnmountFloatingWidget { panel_id } => {
1499 self.handle_unmount_floating_widget(panel_id);
1500 }
1501 }
1502 Ok(())
1503 }
1504
1505 fn handle_save_buffer_to_path(&mut self, buffer_id: BufferId, path: std::path::PathBuf) {
1507 if let Some(state) = self
1508 .windows
1509 .get_mut(&self.active_window)
1510 .map(|w| &mut w.buffers)
1511 .expect("active window present")
1512 .get_mut(&buffer_id)
1513 {
1514 match state.buffer.save_to_file(&path) {
1516 Ok(()) => {
1517 if let Err(e) = self.finalize_save(Some(path)) {
1520 tracing::warn!("Failed to finalize save: {}", e);
1521 }
1522 tracing::debug!("Saved buffer {:?} to path", buffer_id);
1523 }
1524 Err(e) => {
1525 self.handle_set_status(format!("Error saving: {}", e));
1526 tracing::error!("Failed to save buffer to path: {}", e);
1527 }
1528 }
1529 } else {
1530 self.handle_set_status(format!("Buffer {:?} not found", buffer_id));
1531 tracing::warn!("SaveBufferToPath: buffer {:?} not found", buffer_id);
1532 }
1533 }
1534
1535 #[cfg(feature = "plugins")]
1537 fn handle_load_plugin(&mut self, path: std::path::PathBuf, callback_id: JsCallbackId) {
1538 let load_result = self.plugin_manager.read().unwrap().load_plugin(&path);
1539 match load_result {
1540 Ok(()) => {
1541 tracing::info!("Loaded plugin from {:?}", path);
1542 self.plugin_manager
1543 .read()
1544 .unwrap()
1545 .resolve_callback(callback_id, "true".to_string());
1546 }
1547 Err(e) => {
1548 tracing::error!("Failed to load plugin from {:?}: {}", path, e);
1549 self.plugin_manager
1550 .read()
1551 .unwrap()
1552 .reject_callback(callback_id, format!("{}", e));
1553 }
1554 }
1555 }
1556
1557 #[cfg(feature = "plugins")]
1559 fn handle_unload_plugin(&mut self, name: String, callback_id: JsCallbackId) {
1560 let result = self.plugin_manager.write().unwrap().unload_plugin(&name);
1563 match result {
1564 Ok(()) => {
1565 tracing::info!("Unloaded plugin: {}", name);
1566 if let Ok(mut schemas) = self.plugin_schemas.write() {
1567 schemas.remove(&name);
1568 }
1569 self.plugin_manager
1570 .read()
1571 .unwrap()
1572 .resolve_callback(callback_id, "true".to_string());
1573 }
1574 Err(e) => {
1575 tracing::error!("Failed to unload plugin '{}': {}", name, e);
1576 self.plugin_manager
1577 .read()
1578 .unwrap()
1579 .reject_callback(callback_id, format!("{}", e));
1580 }
1581 }
1582 }
1583
1584 #[cfg(feature = "plugins")]
1586 fn handle_reload_plugin(&mut self, name: String, callback_id: JsCallbackId) {
1587 let path = self
1591 .plugin_manager
1592 .read()
1593 .unwrap()
1594 .list_plugins()
1595 .into_iter()
1596 .find(|p| p.name == name)
1597 .map(|p| p.path);
1598 let _ = path; let reload_result = self.plugin_manager.read().unwrap().reload_plugin(&name);
1600 match reload_result {
1601 Ok(()) => {
1602 tracing::info!("Reloaded plugin: {}", name);
1603 self.plugin_manager
1604 .read()
1605 .unwrap()
1606 .resolve_callback(callback_id, "true".to_string());
1607 }
1608 Err(e) => {
1609 tracing::error!("Failed to reload plugin '{}': {}", name, e);
1610 self.plugin_manager
1611 .read()
1612 .unwrap()
1613 .reject_callback(callback_id, format!("{}", e));
1614 }
1615 }
1616 }
1617
1618 #[cfg(feature = "plugins")]
1620 fn handle_list_plugins(&mut self, callback_id: JsCallbackId) {
1621 let plugins = self.plugin_manager.read().unwrap().list_plugins();
1622 let json_array: Vec<serde_json::Value> = plugins
1624 .iter()
1625 .map(|p| {
1626 serde_json::json!({
1627 "name": p.name,
1628 "path": p.path.to_string_lossy(),
1629 "enabled": p.enabled
1630 })
1631 })
1632 .collect();
1633 let json_str = serde_json::to_string(&json_array).unwrap_or_else(|_| "[]".to_string());
1634 self.plugin_manager
1635 .read()
1636 .unwrap()
1637 .resolve_callback(callback_id, json_str);
1638 }
1639
1640 fn handle_execute_action(&mut self, action_name: String) {
1642 use crate::input::keybindings::Action;
1643 use std::collections::HashMap;
1644
1645 if let Some(action) = Action::from_str(&action_name, &HashMap::new()) {
1647 if let Err(e) = self.handle_action(action) {
1649 tracing::warn!("Failed to execute action '{}': {}", action_name, e);
1650 } else {
1651 tracing::debug!("Executed action: {}", action_name);
1652 }
1653 } else {
1654 tracing::warn!("Unknown action: {}", action_name);
1655 }
1656 }
1657
1658 fn handle_execute_actions(&mut self, actions: Vec<fresh_core::api::ActionSpec>) {
1661 use crate::input::keybindings::Action;
1662 use std::collections::HashMap;
1663
1664 for action_spec in actions {
1665 if let Some(action) = Action::from_str(&action_spec.action, &HashMap::new()) {
1666 for _ in 0..action_spec.count {
1668 if let Err(e) = self.handle_action(action.clone()) {
1669 tracing::warn!("Failed to execute action '{}': {}", action_spec.action, e);
1670 return; }
1672 }
1673 tracing::debug!(
1674 "Executed action '{}' {} time(s)",
1675 action_spec.action,
1676 action_spec.count
1677 );
1678 } else {
1679 tracing::warn!("Unknown action: {}", action_spec.action);
1680 return; }
1682 }
1683 }
1684
1685 fn handle_get_buffer_text(
1690 &mut self,
1691 buffer_id: BufferId,
1692 start: usize,
1693 end: usize,
1694 request_id: u64,
1695 ) {
1696 let result = if let Some(state) = self
1697 .windows
1698 .get_mut(&self.active_window)
1699 .map(|w| &mut w.buffers)
1700 .expect("active window present")
1701 .get_mut(&buffer_id)
1702 {
1703 let (start, end) = clamp_buffer_text_range(start, end, state.buffer.len());
1711 Ok(state.get_text_range(start, end))
1712 } else {
1713 Err(format!("Buffer {:?} not found", buffer_id))
1714 };
1715
1716 let callback_id = fresh_core::api::JsCallbackId::from(request_id);
1718 match result {
1719 Ok(text) => {
1720 let json = serde_json::to_string(&text).unwrap_or_else(|_| "null".to_string());
1722 self.plugin_manager
1723 .read()
1724 .unwrap()
1725 .resolve_callback(callback_id, json);
1726 }
1727 Err(error) => {
1728 self.plugin_manager
1729 .read()
1730 .unwrap()
1731 .reject_callback(callback_id, error);
1732 }
1733 }
1734 }
1735
1736 fn handle_set_editor_mode(&mut self, mode: Option<String>) {
1738 self.active_window_mut().editor_mode = mode.clone();
1739 tracing::debug!("Set editor mode: {:?}", mode);
1740 }
1741
1742 fn resolve_buffer_id(&self, buffer_id: BufferId) -> BufferId {
1744 if buffer_id.0 == 0 {
1745 self.active_buffer()
1746 } else {
1747 buffer_id
1748 }
1749 }
1750
1751 fn resolve_json_callback<T: serde::Serialize>(&mut self, request_id: u64, value: T) {
1753 let callback_id = fresh_core::api::JsCallbackId::from(request_id);
1754 let json = serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string());
1755 self.plugin_manager
1756 .read()
1757 .unwrap()
1758 .resolve_callback(callback_id, json);
1759 }
1760
1761 fn handle_get_line_start_position(&mut self, buffer_id: BufferId, line: u32, request_id: u64) {
1763 let actual_buffer_id = self.resolve_buffer_id(buffer_id);
1764 let result = self
1765 .windows
1766 .get_mut(&self.active_window)
1767 .map(|w| &mut w.buffers)
1768 .expect("active window present")
1769 .get_mut(&actual_buffer_id)
1770 .and_then(|state| {
1771 let len = state.buffer.len();
1772 let content = state.get_text_range(0, len);
1773 buffer_line_byte_offset(&content, len, line as usize, false)
1774 });
1775 self.resolve_json_callback(request_id, result);
1776 }
1777
1778 fn handle_get_line_end_position(&mut self, buffer_id: BufferId, line: u32, request_id: u64) {
1781 let actual_buffer_id = self.resolve_buffer_id(buffer_id);
1782 let result = self
1783 .windows
1784 .get_mut(&self.active_window)
1785 .map(|w| &mut w.buffers)
1786 .expect("active window present")
1787 .get_mut(&actual_buffer_id)
1788 .and_then(|state| {
1789 let len = state.buffer.len();
1790 let content = state.get_text_range(0, len);
1791 buffer_line_byte_offset(&content, len, line as usize, true)
1792 });
1793 self.resolve_json_callback(request_id, result);
1794 }
1795
1796 fn handle_get_buffer_line_count(&mut self, buffer_id: BufferId, request_id: u64) {
1798 let actual_buffer_id = self.resolve_buffer_id(buffer_id);
1799
1800 let result = if let Some(state) = self
1801 .windows
1802 .get_mut(&self.active_window)
1803 .map(|w| &mut w.buffers)
1804 .expect("active window present")
1805 .get_mut(&actual_buffer_id)
1806 {
1807 let buffer_len = state.buffer.len();
1808 let content = state.get_text_range(0, buffer_len);
1809
1810 if content.is_empty() {
1812 Some(1) } else {
1814 let newline_count = content.chars().filter(|&c| c == '\n').count();
1815 let ends_with_newline = content.ends_with('\n');
1817 if ends_with_newline {
1818 Some(newline_count)
1819 } else {
1820 Some(newline_count + 1)
1821 }
1822 }
1823 } else {
1824 None
1825 };
1826
1827 self.resolve_json_callback(request_id, result);
1828 }
1829
1830 fn handle_open_file_streaming(&mut self, path: std::path::PathBuf, request_id: u64) {
1847 if !self.authority.filesystem.exists(&path) {
1850 if let Some(parent) = path.parent() {
1851 if !parent.as_os_str().is_empty() {
1852 if let Err(e) = std::fs::create_dir_all(parent) {
1853 tracing::warn!(
1854 "openFileStreaming: failed to create parent dir {:?}: {}",
1855 parent,
1856 e
1857 );
1858 self.resolve_json_callback::<Option<u64>>(request_id, None);
1859 return;
1860 }
1861 }
1862 }
1863 if let Err(e) = std::fs::write(&path, b"") {
1864 tracing::warn!(
1865 "openFileStreaming: failed to create empty file at {:?}: {}",
1866 path,
1867 e
1868 );
1869 self.resolve_json_callback::<Option<u64>>(request_id, None);
1870 return;
1871 }
1872 }
1873
1874 let buffer_id = match self.open_file_no_focus(&path) {
1878 Ok(id) => id,
1879 Err(e) => {
1880 tracing::warn!(
1881 "openFileStreaming: open_file_no_focus failed for {:?}: {}",
1882 path,
1883 e
1884 );
1885 self.resolve_json_callback::<Option<u64>>(request_id, None);
1886 return;
1887 }
1888 };
1889
1890 if let Some(meta) = self.active_window_mut().buffer_metadata.get_mut(&buffer_id) {
1895 meta.hidden_from_tabs = true;
1896 meta.auto_revert_enabled = false;
1897 }
1898 let active_split = self
1899 .windows
1900 .get(&self.active_window)
1901 .and_then(|w| w.buffers.splits())
1902 .map(|(mgr, _)| mgr)
1903 .expect("active window must have a populated split layout")
1904 .active_split();
1905 if let Some(vs) = self
1906 .windows
1907 .get_mut(&self.active_window)
1908 .and_then(|w| w.split_view_states_mut())
1909 .expect("active window must have a populated split layout")
1910 .get_mut(&active_split)
1911 {
1912 use crate::view::split::TabTarget;
1913 vs.open_buffers
1914 .retain(|t| !matches!(t, TabTarget::Buffer(b) if *b == buffer_id));
1915 }
1916
1917 self.resolve_json_callback(request_id, Some(buffer_id.0));
1918 }
1919
1920 fn handle_set_buffer_group_panel_buffer(
1923 &mut self,
1924 group_id: usize,
1925 panel_name: String,
1926 buffer_id: BufferId,
1927 request_id: u64,
1928 ) {
1929 let actual_buffer_id = self.resolve_buffer_id(buffer_id);
1930 let ok = self.set_buffer_group_panel_buffer(group_id, panel_name, actual_buffer_id);
1931 self.resolve_json_callback(request_id, ok);
1932 }
1933
1934 fn handle_refresh_buffer_from_disk(&mut self, buffer_id: BufferId, request_id: u64) {
1938 let actual_buffer_id = self.resolve_buffer_id(buffer_id);
1939
1940 let path = self
1941 .windows
1942 .get(&self.active_window)
1943 .and_then(|w| w.buffers.splits())
1944 .map(|(_, _)| ())
1945 .and_then(|_| {
1946 self.windows
1947 .get(&self.active_window)?
1948 .buffers
1949 .get(&actual_buffer_id)?
1950 .buffer
1951 .file_path()
1952 .map(|p| p.to_path_buf())
1953 });
1954
1955 let Some(path) = path else {
1956 self.resolve_json_callback::<Option<usize>>(request_id, None);
1958 return;
1959 };
1960
1961 let new_size = match self.authority.filesystem.metadata(&path) {
1962 Ok(m) => m.size as usize,
1963 Err(_) => {
1964 self.resolve_json_callback::<Option<usize>>(request_id, None);
1965 return;
1966 }
1967 };
1968
1969 let new_total = if let Some(state) = self
1970 .windows
1971 .get_mut(&self.active_window)
1972 .map(|w| &mut w.buffers)
1973 .expect("active window present")
1974 .get_mut(&actual_buffer_id)
1975 {
1976 let old = state.buffer.total_bytes();
1977 if new_size > old {
1978 state.buffer.extend_streaming(&path, new_size);
1979 }
1980 state.buffer.total_bytes()
1981 } else {
1982 self.resolve_json_callback::<Option<usize>>(request_id, None);
1983 return;
1984 };
1985
1986 self.resolve_json_callback(request_id, Some(new_total));
1987 }
1988
1989 fn handle_scroll_to_line_center(
1991 &mut self,
1992 split_id: SplitId,
1993 buffer_id: BufferId,
1994 line: usize,
1995 ) {
1996 let actual_split_id = if split_id.0 == 0 {
1997 self.windows
1998 .get(&self.active_window)
1999 .and_then(|w| w.buffers.splits())
2000 .map(|(mgr, _)| mgr)
2001 .expect("active window must have a populated split layout")
2002 .active_split()
2003 } else {
2004 LeafId(split_id)
2005 };
2006 let actual_buffer_id = self.resolve_buffer_id(buffer_id);
2007
2008 let viewport_height = if let Some(view_state) = self
2010 .windows
2011 .get(&self.active_window)
2012 .and_then(|w| w.buffers.splits())
2013 .map(|(_, vs)| vs)
2014 .expect("active window must have a populated split layout")
2015 .get(&actual_split_id)
2016 {
2017 view_state.viewport.height as usize
2018 } else {
2019 return;
2020 };
2021
2022 let lines_above = viewport_height / 2;
2024 let target_line = line.saturating_sub(lines_above);
2025
2026 self.active_window_mut().scroll_split_viewport_to(
2027 actual_buffer_id,
2028 actual_split_id,
2029 target_line,
2030 true,
2031 );
2032 }
2033
2034 fn handle_scroll_buffer_to_line(&mut self, buffer_id: BufferId, line: usize) {
2044 if !self
2045 .windows
2046 .get(&self.active_window)
2047 .map(|w| &w.buffers)
2048 .expect("active window present")
2049 .contains_key(&buffer_id)
2050 {
2051 return;
2052 }
2053
2054 let mut target_leaves: Vec<LeafId> = Vec::new();
2056
2057 for leaf_id in self
2059 .windows
2060 .get(&self.active_window)
2061 .and_then(|w| w.buffers.splits())
2062 .map(|(mgr, _)| mgr)
2063 .expect("active window must have a populated split layout")
2064 .root()
2065 .leaf_split_ids()
2066 {
2067 if let Some(vs) = self
2068 .windows
2069 .get(&self.active_window)
2070 .and_then(|w| w.buffers.splits())
2071 .map(|(_, vs)| vs)
2072 .expect("active window must have a populated split layout")
2073 .get(&leaf_id)
2074 {
2075 if vs.active_buffer == buffer_id {
2076 target_leaves.push(leaf_id);
2077 }
2078 }
2079 }
2080
2081 for (_group_leaf_id, node) in self.active_window().grouped_subtrees.iter() {
2083 if let crate::view::split::SplitNode::Grouped { layout, .. } = node {
2084 for inner_leaf in layout.leaf_split_ids() {
2085 if let Some(vs) = self
2086 .windows
2087 .get(&self.active_window)
2088 .and_then(|w| w.buffers.splits())
2089 .map(|(_, vs)| vs)
2090 .expect("active window must have a populated split layout")
2091 .get(&inner_leaf)
2092 {
2093 if vs.active_buffer == buffer_id && !target_leaves.contains(&inner_leaf) {
2094 target_leaves.push(inner_leaf);
2095 }
2096 }
2097 }
2098 }
2099 }
2100
2101 if target_leaves.is_empty() {
2102 return;
2103 }
2104
2105 self.active_window_mut()
2106 .scroll_buffer_to_line_in_splits(buffer_id, &target_leaves, line);
2107 }
2108
2109 fn handle_spawn_host_process(
2110 &mut self,
2111 command: String,
2112 args: Vec<String>,
2113 cwd: Option<String>,
2114 callback_id: JsCallbackId,
2115 ) {
2116 if let (Some(runtime), Some(bridge)) = (&self.tokio_runtime, &self.async_bridge) {
2131 use tokio::io::{AsyncReadExt, BufReader};
2132 use tokio::process::Command as TokioCommand;
2133
2134 let effective_cwd = cwd.or_else(|| {
2135 std::env::current_dir()
2136 .map(|p| p.to_string_lossy().to_string())
2137 .ok()
2138 });
2139 let sender = bridge.sender();
2140 let process_id = callback_id.as_u64();
2141
2142 if let crate::services::workspace_trust::SpawnDecision::Deny(reason) = self
2149 .authority
2150 .workspace_trust
2151 .decide(&command, effective_cwd.as_deref())
2152 {
2153 #[allow(clippy::let_underscore_must_use)]
2154 let _ = sender.send(AsyncMessage::PluginProcessOutput {
2155 process_id,
2156 stdout: String::new(),
2157 stderr: reason,
2158 exit_code: -1,
2159 });
2160 return;
2161 }
2162
2163 let (kill_tx, mut kill_rx) = tokio::sync::oneshot::channel::<()>();
2164 self.host_process_handles.insert(process_id, kill_tx);
2165
2166 runtime.spawn(async move {
2167 use crate::services::process_hidden::HideWindow;
2168 let mut cmd = TokioCommand::new(&command);
2169 cmd.args(&args);
2170 cmd.stdout(std::process::Stdio::piped());
2171 cmd.stderr(std::process::Stdio::piped());
2172 cmd.hide_window();
2173 if let Some(ref dir) = effective_cwd {
2174 cmd.current_dir(dir);
2175 }
2176 let mut child = match cmd.spawn() {
2177 Ok(c) => c,
2178 Err(e) => {
2179 #[allow(clippy::let_underscore_must_use)]
2180 let _ = sender.send(AsyncMessage::PluginProcessOutput {
2181 process_id,
2182 stdout: String::new(),
2183 stderr: e.to_string(),
2184 exit_code: -1,
2185 });
2186 return;
2187 }
2188 };
2189
2190 let stdout_pipe = child.stdout.take();
2196 let stderr_pipe = child.stderr.take();
2197
2198 let stdout_fut = async {
2199 let mut buf = String::new();
2200 if let Some(s) = stdout_pipe {
2201 #[allow(clippy::let_underscore_must_use)]
2202 let _ = BufReader::new(s).read_to_string(&mut buf).await;
2203 }
2204 buf
2205 };
2206 let stderr_fut = async {
2207 let mut buf = String::new();
2208 if let Some(s) = stderr_pipe {
2209 #[allow(clippy::let_underscore_must_use)]
2210 let _ = BufReader::new(s).read_to_string(&mut buf).await;
2211 }
2212 buf
2213 };
2214 let wait_fut = async {
2215 tokio::select! {
2216 status = child.wait() => {
2217 status.map(|s| s.code().unwrap_or(-1)).unwrap_or(-1)
2218 }
2219 _ = &mut kill_rx => {
2220 #[allow(clippy::let_underscore_must_use)]
2224 let _ = child.start_kill();
2225 child
2226 .wait()
2227 .await
2228 .map(|s| s.code().unwrap_or(-1))
2229 .unwrap_or(-1)
2230 }
2231 }
2232 };
2233 let (stdout, stderr, exit_code) = tokio::join!(stdout_fut, stderr_fut, wait_fut);
2234
2235 #[allow(clippy::let_underscore_must_use)]
2236 let _ = sender.send(AsyncMessage::PluginProcessOutput {
2237 process_id,
2238 stdout,
2239 stderr,
2240 exit_code,
2241 });
2242 });
2243 } else {
2244 self.plugin_manager
2245 .read()
2246 .unwrap()
2247 .reject_callback(callback_id, "Async runtime not available".to_string());
2248 }
2249 }
2250
2251 fn handle_spawn_background_process(
2252 &mut self,
2253 process_id: u64,
2254 command: String,
2255 args: Vec<String>,
2256 cwd: Option<String>,
2257 callback_id: JsCallbackId,
2258 ) {
2259 if let (Some(runtime), Some(bridge)) = (&self.tokio_runtime, &self.async_bridge) {
2261 use tokio::io::{AsyncBufReadExt, BufReader};
2262 use tokio::process::Command as TokioCommand;
2263
2264 let effective_cwd = cwd.unwrap_or_else(|| {
2265 std::env::current_dir()
2266 .map(|p| p.to_string_lossy().to_string())
2267 .unwrap_or_else(|_| ".".to_string())
2268 });
2269
2270 let sender = bridge.sender();
2271 let sender_stdout = sender.clone();
2272 let sender_stderr = sender.clone();
2273 let callback_id_u64 = callback_id.as_u64();
2274
2275 #[allow(clippy::let_underscore_must_use)]
2277 let handle = runtime.spawn(async move {
2278 use crate::services::process_hidden::HideWindow;
2279 let mut child = match TokioCommand::new(&command)
2280 .args(&args)
2281 .current_dir(&effective_cwd)
2282 .stdout(std::process::Stdio::piped())
2283 .stderr(std::process::Stdio::piped())
2284 .hide_window()
2285 .spawn()
2286 {
2287 Ok(child) => child,
2288 Err(e) => {
2289 let _ = sender.send(crate::services::async_bridge::AsyncMessage::Plugin(
2290 fresh_core::api::PluginAsyncMessage::ProcessExit {
2291 process_id,
2292 callback_id: callback_id_u64,
2293 exit_code: -1,
2294 },
2295 ));
2296 tracing::error!("Failed to spawn background process: {}", e);
2297 return;
2298 }
2299 };
2300
2301 let stdout = child.stdout.take();
2303 let stderr = child.stderr.take();
2304 let pid = process_id;
2305
2306 if let Some(stdout) = stdout {
2308 let sender = sender_stdout;
2309 tokio::spawn(async move {
2310 let reader = BufReader::new(stdout);
2311 let mut lines = reader.lines();
2312 while let Ok(Some(line)) = lines.next_line().await {
2313 let _ =
2314 sender.send(crate::services::async_bridge::AsyncMessage::Plugin(
2315 fresh_core::api::PluginAsyncMessage::ProcessStdout {
2316 process_id: pid,
2317 data: line + "\n",
2318 },
2319 ));
2320 }
2321 });
2322 }
2323
2324 if let Some(stderr) = stderr {
2326 let sender = sender_stderr;
2327 tokio::spawn(async move {
2328 let reader = BufReader::new(stderr);
2329 let mut lines = reader.lines();
2330 while let Ok(Some(line)) = lines.next_line().await {
2331 let _ =
2332 sender.send(crate::services::async_bridge::AsyncMessage::Plugin(
2333 fresh_core::api::PluginAsyncMessage::ProcessStderr {
2334 process_id: pid,
2335 data: line + "\n",
2336 },
2337 ));
2338 }
2339 });
2340 }
2341
2342 let exit_code = match child.wait().await {
2344 Ok(status) => status.code().unwrap_or(-1),
2345 Err(_) => -1,
2346 };
2347
2348 let _ = sender.send(crate::services::async_bridge::AsyncMessage::Plugin(
2349 fresh_core::api::PluginAsyncMessage::ProcessExit {
2350 process_id,
2351 callback_id: callback_id_u64,
2352 exit_code,
2353 },
2354 ));
2355 });
2356
2357 self.background_process_handles
2359 .insert(process_id, handle.abort_handle());
2360 } else {
2361 self.plugin_manager
2363 .read()
2364 .unwrap()
2365 .reject_callback(callback_id, "Async runtime not available".to_string());
2366 }
2367 }
2368
2369 fn handle_create_virtual_buffer_with_content(
2370 &mut self,
2371 name: String,
2372 mode: String,
2373 read_only: bool,
2374 entries: Vec<fresh_core::text_property::TextPropertyEntry>,
2375 show_line_numbers: bool,
2376 show_cursors: bool,
2377 editing_disabled: bool,
2378 hidden_from_tabs: bool,
2379 request_id: Option<u64>,
2380 ) {
2381 let buffer_id =
2382 self.active_window_mut()
2383 .create_virtual_buffer(name.clone(), mode.clone(), read_only);
2384 tracing::info!(
2385 "Created virtual buffer '{}' with mode '{}' (id={:?})",
2386 name,
2387 mode,
2388 buffer_id
2389 );
2390
2391 if let Some(state) = self
2398 .windows
2399 .get_mut(&self.active_window)
2400 .map(|w| &mut w.buffers)
2401 .expect("active window present")
2402 .get_mut(&buffer_id)
2403 {
2404 state.margins.configure_for_line_numbers(show_line_numbers);
2405 state.show_cursors = show_cursors;
2406 state.editing_disabled = editing_disabled;
2407 tracing::debug!(
2408 "Set buffer {:?} view options: show_line_numbers={}, show_cursors={}, editing_disabled={}",
2409 buffer_id,
2410 show_line_numbers,
2411 show_cursors,
2412 editing_disabled
2413 );
2414 }
2415 let active_split = self
2416 .windows
2417 .get(&self.active_window)
2418 .and_then(|w| w.buffers.splits())
2419 .map(|(mgr, _)| mgr)
2420 .expect("active window must have a populated split layout")
2421 .active_split();
2422 if let Some(view_state) = self
2423 .windows
2424 .get_mut(&self.active_window)
2425 .and_then(|w| w.split_view_states_mut())
2426 .expect("active window must have a populated split layout")
2427 .get_mut(&active_split)
2428 {
2429 view_state.ensure_buffer_state(buffer_id).show_line_numbers = show_line_numbers;
2430 }
2431
2432 if hidden_from_tabs {
2434 if let Some(meta) = self.active_window_mut().buffer_metadata.get_mut(&buffer_id) {
2435 meta.hidden_from_tabs = true;
2436 }
2437 }
2438
2439 match self.set_virtual_buffer_content(buffer_id, entries) {
2441 Ok(()) => {
2442 tracing::debug!("Set virtual buffer content for {:?}", buffer_id);
2443 self.set_active_buffer(buffer_id);
2445 tracing::debug!("Switched to virtual buffer {:?}", buffer_id);
2446
2447 if let Some(req_id) = request_id {
2449 tracing::info!(
2450 "CreateVirtualBufferWithContent: resolving callback for request_id={}, buffer_id={:?}",
2451 req_id,
2452 buffer_id
2453 );
2454 let result = fresh_core::api::VirtualBufferResult {
2456 buffer_id: buffer_id.0 as u64,
2457 split_id: None,
2458 };
2459 self.plugin_manager.read().unwrap().resolve_callback(
2460 fresh_core::api::JsCallbackId::from(req_id),
2461 serde_json::to_string(&result).unwrap_or_default(),
2462 );
2463 tracing::info!(
2464 "CreateVirtualBufferWithContent: resolve_callback sent for request_id={}",
2465 req_id
2466 );
2467 }
2468 }
2469 Err(e) => {
2470 tracing::error!("Failed to set virtual buffer content: {}", e);
2471 }
2472 }
2473 }
2474
2475 fn handle_create_virtual_buffer_in_split(
2476 &mut self,
2477 name: String,
2478 mode: String,
2479 read_only: bool,
2480 entries: Vec<fresh_core::text_property::TextPropertyEntry>,
2481 ratio: f32,
2482 direction: Option<String>,
2483 panel_id: Option<String>,
2484 show_line_numbers: bool,
2485 show_cursors: bool,
2486 editing_disabled: bool,
2487 line_wrap: Option<bool>,
2488 before: bool,
2489 role: Option<String>,
2490 request_id: Option<u64>,
2491 ) {
2492 let split_role: Option<crate::view::split::SplitRole> = match role.as_deref() {
2495 Some("utility_dock") => Some(crate::view::split::SplitRole::UtilityDock),
2496 _ => None,
2497 };
2498
2499 if let Some(target_role) = split_role {
2505 if let Some(dock_leaf) = self
2506 .windows
2507 .get(&self.active_window)
2508 .and_then(|w| w.buffers.splits())
2509 .map(|(mgr, _)| mgr)
2510 .expect("active window must have a populated split layout")
2511 .find_leaf_by_role(target_role)
2512 {
2513 let source_split_before_create = self
2518 .windows
2519 .get(&self.active_window)
2520 .and_then(|w| w.buffers.splits())
2521 .map(|(mgr, _)| mgr)
2522 .expect("active window must have a populated split layout")
2523 .active_split();
2524 let buffer_id = self.active_window_mut().create_virtual_buffer(
2525 name.clone(),
2526 mode.clone(),
2527 read_only,
2528 );
2529 if let Some(state) = self
2530 .windows
2531 .get_mut(&self.active_window)
2532 .map(|w| &mut w.buffers)
2533 .expect("active window present")
2534 .get_mut(&buffer_id)
2535 {
2536 state.margins.configure_for_line_numbers(show_line_numbers);
2537 state.show_cursors = show_cursors;
2538 state.editing_disabled = editing_disabled;
2539 }
2540 if let Some(pid) = &panel_id {
2541 self.panel_ids_mut().insert(pid.clone(), buffer_id);
2542 }
2543 if let Err(e) = self.set_virtual_buffer_content(buffer_id, entries) {
2544 tracing::error!("Failed to set virtual buffer content (dock route): {}", e);
2545 return;
2546 }
2547
2548 self.windows
2552 .get_mut(&self.active_window)
2553 .and_then(|w| w.split_manager_mut())
2554 .expect("active window must have a populated split layout")
2555 .set_active_split(dock_leaf);
2556 self.active_window_mut()
2557 .set_pane_buffer(dock_leaf, buffer_id);
2558
2559 if dock_leaf != source_split_before_create {
2561 if let Some(source_view_state) = self
2562 .windows
2563 .get_mut(&self.active_window)
2564 .and_then(|w| w.split_view_states_mut())
2565 .expect("active window must have a populated split layout")
2566 .get_mut(&source_split_before_create)
2567 {
2568 source_view_state.remove_buffer(buffer_id);
2569 }
2570 }
2571
2572 if let Some(req_id) = request_id {
2573 let result = fresh_core::api::VirtualBufferResult {
2574 buffer_id: buffer_id.0 as u64,
2575 split_id: Some(dock_leaf.0 .0 as u64),
2576 };
2577 self.plugin_manager.read().unwrap().resolve_callback(
2578 fresh_core::api::JsCallbackId::from(req_id),
2579 serde_json::to_string(&result).unwrap_or_default(),
2580 );
2581 }
2582 tracing::info!(
2583 "Routed virtual buffer '{}' into existing utility dock {:?}",
2584 name,
2585 dock_leaf
2586 );
2587 return;
2588 }
2589 }
2592
2593 if let Some(pid) = &panel_id {
2595 if let Some(&existing_buffer_id) = self.panel_ids().get(pid) {
2596 if self
2598 .windows
2599 .get(&self.active_window)
2600 .map(|w| &w.buffers)
2601 .expect("active window present")
2602 .contains_key(&existing_buffer_id)
2603 {
2604 if let Err(e) = self.set_virtual_buffer_content(existing_buffer_id, entries) {
2606 tracing::error!("Failed to update panel content: {}", e);
2607 } else {
2608 tracing::info!("Updated existing panel '{}' content", pid);
2609 }
2610
2611 let splits = self
2613 .windows
2614 .get(&self.active_window)
2615 .and_then(|w| w.buffers.splits())
2616 .map(|(mgr, _)| mgr)
2617 .expect("active window must have a populated split layout")
2618 .splits_for_buffer(existing_buffer_id);
2619 if let Some(&split_id) = splits.first() {
2620 self.windows
2621 .get_mut(&self.active_window)
2622 .and_then(|w| w.split_manager_mut())
2623 .expect("active window must have a populated split layout")
2624 .set_active_split(split_id);
2625 self.active_window_mut()
2628 .set_pane_buffer(split_id, existing_buffer_id);
2629 tracing::debug!("Focused split {:?} containing panel buffer", split_id);
2630 }
2631
2632 if let Some(req_id) = request_id {
2634 let result = fresh_core::api::VirtualBufferResult {
2635 buffer_id: existing_buffer_id.0 as u64,
2636 split_id: splits.first().map(|s| s.0 .0 as u64),
2637 };
2638 self.plugin_manager.read().unwrap().resolve_callback(
2639 fresh_core::api::JsCallbackId::from(req_id),
2640 serde_json::to_string(&result).unwrap_or_default(),
2641 );
2642 }
2643 return;
2644 } else {
2645 tracing::warn!(
2647 "Removing stale panel_id '{}' pointing to non-existent buffer {:?}",
2648 pid,
2649 existing_buffer_id
2650 );
2651 self.panel_ids_mut().remove(pid);
2652 }
2654 }
2655 }
2656
2657 let source_split_before_create = self
2663 .windows
2664 .get(&self.active_window)
2665 .and_then(|w| w.buffers.splits())
2666 .map(|(mgr, _)| mgr)
2667 .expect("active window must have a populated split layout")
2668 .active_split();
2669
2670 let buffer_id =
2672 self.active_window_mut()
2673 .create_virtual_buffer(name.clone(), mode.clone(), read_only);
2674 tracing::info!(
2675 "Created virtual buffer '{}' with mode '{}' in split (id={:?})",
2676 name,
2677 mode,
2678 buffer_id
2679 );
2680
2681 if let Some(state) = self
2683 .windows
2684 .get_mut(&self.active_window)
2685 .map(|w| &mut w.buffers)
2686 .expect("active window present")
2687 .get_mut(&buffer_id)
2688 {
2689 state.margins.configure_for_line_numbers(show_line_numbers);
2690 state.show_cursors = show_cursors;
2691 state.editing_disabled = editing_disabled;
2692 tracing::debug!(
2693 "Set buffer {:?} view options: show_line_numbers={}, show_cursors={}, editing_disabled={}",
2694 buffer_id,
2695 show_line_numbers,
2696 show_cursors,
2697 editing_disabled
2698 );
2699 }
2700
2701 if let Some(pid) = panel_id {
2703 self.panel_ids_mut().insert(pid, buffer_id);
2704 }
2705
2706 if let Err(e) = self.set_virtual_buffer_content(buffer_id, entries) {
2708 tracing::error!("Failed to set virtual buffer content: {}", e);
2709 return;
2710 }
2711
2712 let split_dir = match direction.as_deref() {
2714 Some("vertical") => crate::model::event::SplitDirection::Vertical,
2715 _ => crate::model::event::SplitDirection::Horizontal,
2716 };
2717
2718 let created_split_id =
2724 match if split_role == Some(crate::view::split::SplitRole::UtilityDock) {
2725 self.windows
2726 .get_mut(&self.active_window)
2727 .and_then(|w| w.split_manager_mut())
2728 .expect("active window must have a populated split layout")
2729 .split_root_positioned(split_dir, buffer_id, ratio, before)
2730 } else {
2731 self.windows
2732 .get_mut(&self.active_window)
2733 .and_then(|w| w.split_manager_mut())
2734 .expect("active window must have a populated split layout")
2735 .split_active_positioned(split_dir, buffer_id, ratio, before)
2736 } {
2737 Ok(new_split_id) => {
2738 if new_split_id != source_split_before_create {
2744 if let Some(source_view_state) = self
2745 .windows
2746 .get_mut(&self.active_window)
2747 .and_then(|w| w.split_view_states_mut())
2748 .expect("active window must have a populated split layout")
2749 .get_mut(&source_split_before_create)
2750 {
2751 source_view_state.remove_buffer(buffer_id);
2752 }
2753 }
2754 let mut view_state = SplitViewState::with_buffer(
2756 self.terminal_width,
2757 self.terminal_height,
2758 buffer_id,
2759 );
2760 view_state.apply_config_defaults(
2761 self.config.editor.line_numbers,
2762 self.config.editor.highlight_current_line,
2763 line_wrap.unwrap_or_else(|| {
2764 self.active_window().resolve_line_wrap_for_buffer(buffer_id)
2765 }),
2766 self.config.editor.wrap_indent,
2767 self.active_window()
2768 .resolve_wrap_column_for_buffer(buffer_id),
2769 self.config.editor.rulers.clone(),
2770 );
2771 view_state.ensure_buffer_state(buffer_id).show_line_numbers = show_line_numbers;
2773 self.windows
2774 .get_mut(&self.active_window)
2775 .and_then(|w| w.split_view_states_mut())
2776 .expect("active window must have a populated split layout")
2777 .insert(new_split_id, view_state);
2778
2779 self.windows
2781 .get_mut(&self.active_window)
2782 .and_then(|w| w.split_manager_mut())
2783 .expect("active window must have a populated split layout")
2784 .set_active_split(new_split_id);
2785 if let Some(target_role) = split_role {
2793 self.windows
2794 .get_mut(&self.active_window)
2795 .and_then(|w| w.split_manager_mut())
2796 .expect("active window must have a populated split layout")
2797 .clear_role(target_role);
2798 self.windows
2799 .get_mut(&self.active_window)
2800 .and_then(|w| w.split_manager_mut())
2801 .expect("active window must have a populated split layout")
2802 .set_leaf_role(new_split_id, Some(target_role));
2803 tracing::info!(
2804 "Tagged new dock leaf {:?} with role {:?}",
2805 new_split_id,
2806 target_role
2807 );
2808 }
2809
2810 tracing::info!(
2811 "Created {:?} split with virtual buffer {:?}",
2812 split_dir,
2813 buffer_id
2814 );
2815 Some(new_split_id)
2816 }
2817 Err(e) => {
2818 tracing::error!("Failed to create split: {}", e);
2819 self.set_active_buffer(buffer_id);
2821 None
2822 }
2823 };
2824
2825 if let Some(req_id) = request_id {
2828 tracing::trace!("CreateVirtualBufferInSplit: resolving callback for request_id={}, buffer_id={:?}, split_id={:?}", req_id, buffer_id, created_split_id);
2829 let result = fresh_core::api::VirtualBufferResult {
2830 buffer_id: buffer_id.0 as u64,
2831 split_id: created_split_id.map(|s| s.0 .0 as u64),
2832 };
2833 self.plugin_manager.read().unwrap().resolve_callback(
2834 fresh_core::api::JsCallbackId::from(req_id),
2835 serde_json::to_string(&result).unwrap_or_default(),
2836 );
2837 }
2838 }
2839
2840 fn handle_create_virtual_buffer_in_existing_split(
2841 &mut self,
2842 name: String,
2843 mode: String,
2844 read_only: bool,
2845 entries: Vec<fresh_core::text_property::TextPropertyEntry>,
2846 split_id: SplitId,
2847 show_line_numbers: bool,
2848 show_cursors: bool,
2849 editing_disabled: bool,
2850 line_wrap: Option<bool>,
2851 request_id: Option<u64>,
2852 ) {
2853 let buffer_id =
2855 self.active_window_mut()
2856 .create_virtual_buffer(name.clone(), mode.clone(), read_only);
2857 tracing::info!(
2858 "Created virtual buffer '{}' with mode '{}' for existing split {:?} (id={:?})",
2859 name,
2860 mode,
2861 split_id,
2862 buffer_id
2863 );
2864
2865 if let Some(state) = self
2867 .windows
2868 .get_mut(&self.active_window)
2869 .map(|w| &mut w.buffers)
2870 .expect("active window present")
2871 .get_mut(&buffer_id)
2872 {
2873 state.margins.configure_for_line_numbers(show_line_numbers);
2874 state.show_cursors = show_cursors;
2875 state.editing_disabled = editing_disabled;
2876 }
2877
2878 if let Err(e) = self.set_virtual_buffer_content(buffer_id, entries) {
2880 tracing::error!("Failed to set virtual buffer content: {}", e);
2881 return;
2882 }
2883
2884 let leaf_id = LeafId(split_id);
2887 self.windows
2888 .get_mut(&self.active_window)
2889 .and_then(|w| w.split_manager_mut())
2890 .expect("active window must have a populated split layout")
2891 .set_active_split(leaf_id);
2892 self.active_window_mut().set_pane_buffer(leaf_id, buffer_id);
2893
2894 if let Some(view_state) = self
2900 .windows
2901 .get_mut(&self.active_window)
2902 .and_then(|w| w.split_view_states_mut())
2903 .expect("active window must have a populated split layout")
2904 .get_mut(&leaf_id)
2905 {
2906 view_state.switch_buffer(buffer_id);
2907 view_state.add_buffer(buffer_id);
2908 view_state.ensure_buffer_state(buffer_id).show_line_numbers = show_line_numbers;
2909
2910 if let Some(wrap) = line_wrap {
2912 view_state.active_state_mut().viewport.line_wrap_enabled = wrap;
2913 }
2914 }
2915
2916 tracing::info!(
2917 "Displayed virtual buffer {:?} in split {:?}",
2918 buffer_id,
2919 split_id
2920 );
2921
2922 if let Some(req_id) = request_id {
2924 let result = fresh_core::api::VirtualBufferResult {
2925 buffer_id: buffer_id.0 as u64,
2926 split_id: Some(split_id.0 as u64),
2927 };
2928 self.plugin_manager.read().unwrap().resolve_callback(
2929 fresh_core::api::JsCallbackId::from(req_id),
2930 serde_json::to_string(&result).unwrap_or_default(),
2931 );
2932 }
2933 }
2934
2935 fn handle_show_action_popup(
2936 &mut self,
2937 popup_id: String,
2938 title: String,
2939 message: String,
2940 actions: Vec<fresh_core::api::ActionPopupAction>,
2941 ) {
2942 tracing::info!(
2943 "Action popup requested: id={}, title={}, actions={}",
2944 popup_id,
2945 title,
2946 actions.len()
2947 );
2948
2949 let items: Vec<crate::model::event::PopupListItemData> = actions
2951 .iter()
2952 .map(|action| crate::model::event::PopupListItemData {
2953 text: action.label.clone(),
2954 detail: None,
2955 icon: None,
2956 data: Some(action.id.clone()),
2957 })
2958 .collect();
2959
2960 drop(actions);
2965
2966 let popup_data = crate::model::event::PopupData {
2968 kind: crate::model::event::PopupKindHint::List,
2969 title: Some(title),
2970 description: Some(message),
2971 transient: false,
2972 content: crate::model::event::PopupContentData::List { items, selected: 0 },
2973 position: crate::model::event::PopupPositionData::BottomRight,
2974 width: 60,
2975 max_height: 15,
2976 bordered: true,
2977 };
2978
2979 let mut popup_obj = crate::state::convert_popup_data_to_popup(&popup_data);
2989 popup_obj.resolver = crate::view::popup::PopupResolver::PluginAction {
2990 popup_id: popup_id.clone(),
2991 };
2992
2993 {
3000 let theme = self.theme();
3001 popup_obj.background_style = ratatui::style::Style::default().bg(theme.popup_bg);
3002 popup_obj.border_style = ratatui::style::Style::default().fg(theme.popup_border_fg);
3003 }
3004
3005 while self
3017 .active_state()
3018 .popups
3019 .top()
3020 .is_some_and(|p| matches!(p.resolver, crate::view::popup::PopupResolver::LspStatus))
3021 {
3022 self.active_state_mut().popups.hide();
3023 }
3024
3025 let existing_idx = self.global_popups.all().iter().position(|p| {
3032 matches!(
3033 &p.resolver,
3034 crate::view::popup::PopupResolver::PluginAction { popup_id: id } if id == &popup_id,
3035 )
3036 });
3037 if let Some(idx) = existing_idx {
3038 if let Some(slot) = self.global_popups.get_mut(idx) {
3039 *slot = popup_obj;
3040 }
3041 } else {
3042 self.global_popups.show(popup_obj);
3043 }
3044 tracing::info!(
3045 "Action popup shown: id={}, stack_depth={}",
3046 popup_id,
3047 self.global_popups.all().len()
3048 );
3049 }
3050
3051 fn handle_set_lsp_menu_contributions(
3061 &mut self,
3062 plugin_id: String,
3063 language: String,
3064 items: Vec<fresh_core::api::LspMenuItem>,
3065 ) {
3066 let key = (language.clone(), plugin_id.clone());
3067 if items.is_empty() {
3068 self.active_window_mut().lsp_menu_contributions.remove(&key);
3069 } else {
3070 self.active_window_mut()
3071 .lsp_menu_contributions
3072 .insert(key, items);
3073 }
3074 self.refresh_lsp_status_popup_if_open();
3079 }
3080
3081 fn handle_create_window_with_terminal(
3082 &mut self,
3083 root: std::path::PathBuf,
3084 label: String,
3085 cwd: Option<String>,
3086 command: Option<Vec<String>>,
3087 title: Option<String>,
3088 request_id: u64,
3089 ) {
3090 let callback_id = JsCallbackId::from(request_id);
3091 if !root.is_absolute() {
3092 let msg = format!(
3093 "createWindowWithTerminal: root must be absolute, got {:?}",
3094 root
3095 );
3096 tracing::warn!("{}", msg);
3097 self.plugin_manager
3098 .read()
3099 .unwrap()
3100 .reject_callback(callback_id, msg);
3101 return;
3102 }
3103 let cwd_buf = cwd.map(std::path::PathBuf::from);
3104 match self.create_window_with_terminal(root, label, cwd_buf, command, title) {
3105 Ok((window_id, terminal_id, buffer_id)) => {
3106 let api_result = fresh_core::api::SessionWithTerminalResult {
3107 window_id: window_id.0,
3108 terminal_id: terminal_id.0 as u64,
3109 buffer_id: buffer_id.0 as u64,
3110 };
3111 self.plugin_manager.read().unwrap().resolve_callback(
3112 callback_id,
3113 serde_json::to_string(&api_result).unwrap_or_default(),
3114 );
3115 }
3116 Err(e) => {
3117 tracing::error!("createWindowWithTerminal failed: {e}");
3118 self.plugin_manager
3119 .read()
3120 .unwrap()
3121 .reject_callback(callback_id, format!("createWindowWithTerminal: {e}"));
3122 }
3123 }
3124 }
3125
3126 fn handle_create_terminal(
3127 &mut self,
3128 cwd: Option<String>,
3129 direction: Option<String>,
3130 ratio: Option<f32>,
3131 focus: Option<bool>,
3132 persistent: bool,
3133 target_session_id: Option<fresh_core::WindowId>,
3134 command: Option<Vec<String>>,
3135 title: Option<String>,
3136 request_id: u64,
3137 ) {
3138 let target_id = target_session_id
3145 .filter(|id| self.windows.contains_key(id))
3146 .unwrap_or(self.active_window);
3147 let is_active_target = target_id == self.active_window;
3148
3149 let cwd_buf = cwd.map(std::path::PathBuf::from);
3150 let split_direction = direction.as_deref().map(|d| match d {
3151 "horizontal" => crate::model::event::SplitDirection::Horizontal,
3152 _ => crate::model::event::SplitDirection::Vertical,
3153 });
3154
3155 let prev_active = if is_active_target {
3163 Some(self.active_window().active_buffer())
3164 } else {
3165 None
3166 };
3167
3168 let result = {
3169 let target = self
3170 .windows
3171 .get_mut(&target_id)
3172 .expect("target window present (existence checked above)");
3173 target.create_plugin_terminal(
3174 cwd_buf,
3175 split_direction,
3176 ratio,
3177 focus.unwrap_or(true),
3178 persistent,
3179 command,
3180 title.filter(|t| !t.is_empty()),
3181 )
3182 };
3183 match result {
3184 Ok((terminal_id, buffer_id, created_split_id)) => {
3185 if is_active_target {
3186 let new_active = self.active_window().active_buffer();
3187 if prev_active != Some(new_active) {
3188 #[cfg(feature = "plugins")]
3189 self.update_plugin_state_snapshot();
3190 #[cfg(feature = "plugins")]
3191 self.plugin_manager.read().unwrap().run_hook(
3192 "buffer_activated",
3193 crate::services::plugins::hooks::HookArgs::BufferActivated {
3194 buffer_id: new_active,
3195 },
3196 );
3197 }
3198 }
3199 let api_result = fresh_core::api::TerminalResult {
3200 buffer_id: buffer_id.0 as u64,
3201 terminal_id: terminal_id.0 as u64,
3202 split_id: created_split_id.map(|s| s.0 .0 as u64),
3203 };
3204 self.plugin_manager.read().unwrap().resolve_callback(
3205 fresh_core::api::JsCallbackId::from(request_id),
3206 serde_json::to_string(&api_result).unwrap_or_default(),
3207 );
3208 tracing::info!(
3209 "Plugin created terminal {:?} with buffer {:?} in window {:?}",
3210 terminal_id,
3211 buffer_id,
3212 target_id
3213 );
3214 }
3215 Err(e) => {
3216 tracing::error!("Failed to create terminal for plugin: {e}");
3217 self.plugin_manager.read().unwrap().reject_callback(
3218 fresh_core::api::JsCallbackId::from(request_id),
3219 format!("Failed to create terminal: {e}"),
3220 );
3221 }
3222 }
3223 }
3224
3225 fn handle_get_split_by_label(&mut self, label: String, request_id: u64) {
3228 let split_id = self
3229 .windows
3230 .get(&self.active_window)
3231 .and_then(|w| w.buffers.splits())
3232 .map(|(mgr, _)| mgr)
3233 .expect("active window must have a populated split layout")
3234 .find_split_by_label(&label);
3235 let callback_id = fresh_core::api::JsCallbackId::from(request_id);
3236 let json =
3237 serde_json::to_string(&split_id.map(|s| s.0 .0)).unwrap_or_else(|_| "null".to_string());
3238 self.plugin_manager
3239 .read()
3240 .unwrap()
3241 .resolve_callback(callback_id, json);
3242 }
3243
3244 fn handle_set_buffer_show_cursors(&mut self, buffer_id: BufferId, show: bool) {
3245 if let Some(state) = self
3246 .windows
3247 .get_mut(&self.active_window)
3248 .map(|w| &mut w.buffers)
3249 .expect("active window present")
3250 .get_mut(&buffer_id)
3251 {
3252 state.show_cursors = show;
3253 state.cursor_visibility_locked = true;
3256 } else {
3257 tracing::warn!("SetBufferShowCursors: buffer {:?} not found", buffer_id);
3258 }
3259 }
3260
3261 fn handle_override_theme_colors(
3262 &mut self,
3263 overrides: std::collections::HashMap<String, [u8; 3]>,
3264 ) {
3265 let pairs = overrides
3266 .into_iter()
3267 .map(|(k, [r, g, b])| (k, ratatui::style::Color::Rgb(r, g, b)));
3268 let applied = self.theme.write().unwrap().override_colors(pairs);
3269 if applied > 0 {
3270 self.reapply_all_overlays();
3273 }
3274 }
3275
3276 fn handle_await_next_key(&mut self, callback_id: fresh_core::api::JsCallbackId) {
3277 if let Some(payload) = self
3281 .active_window_mut()
3282 .pending_key_capture_buffer
3283 .pop_front()
3284 {
3285 let json = serde_json::to_string(&payload).unwrap_or_else(|_| "null".to_string());
3286 self.plugin_manager
3287 .read()
3288 .unwrap()
3289 .resolve_callback(callback_id, json);
3290 } else {
3291 self.active_window_mut()
3292 .pending_next_key_callbacks
3293 .push_back(callback_id);
3294 }
3295 }
3296
3297 fn handle_spawn_process(
3298 &mut self,
3299 command: String,
3300 args: Vec<String>,
3301 cwd: Option<String>,
3302 stdout_to: Option<std::path::PathBuf>,
3303 callback_id: fresh_core::api::JsCallbackId,
3304 ) {
3305 if let (Some(runtime), Some(bridge)) = (&self.tokio_runtime, &self.async_bridge) {
3306 let effective_cwd = cwd.or_else(|| {
3307 std::env::current_dir()
3308 .map(|p| p.to_string_lossy().to_string())
3309 .ok()
3310 });
3311 let sender = bridge.sender();
3312 let spawner = self.authority.process_spawner.clone();
3313
3314 let process_id = callback_id.as_u64();
3319 let (kill_tx, kill_rx) = tokio::sync::oneshot::channel::<()>();
3320 self.host_process_handles.insert(process_id, kill_tx);
3321
3322 runtime.spawn(async move {
3323 #[allow(clippy::let_underscore_must_use)]
3324 let outcome = spawner
3325 .spawn_cancellable(command, args, effective_cwd, stdout_to, kill_rx)
3326 .await;
3327 match outcome {
3328 Ok(result) => {
3329 #[allow(clippy::let_underscore_must_use)]
3330 let _ = sender.send(AsyncMessage::PluginProcessOutput {
3331 process_id,
3332 stdout: result.stdout,
3333 stderr: result.stderr,
3334 exit_code: result.exit_code,
3335 });
3336 }
3337 Err(e) => {
3338 #[allow(clippy::let_underscore_must_use)]
3339 let _ = sender.send(AsyncMessage::PluginProcessOutput {
3340 process_id,
3341 stdout: String::new(),
3342 stderr: e.to_string(),
3343 exit_code: -1,
3344 });
3345 }
3346 }
3347 });
3348 } else {
3349 self.plugin_manager
3350 .read()
3351 .unwrap()
3352 .reject_callback(callback_id, "Async runtime not available".to_string());
3353 }
3354 }
3355
3356 fn handle_kill_host_process(&mut self, process_id: u64) {
3357 if let Some(tx) = self.host_process_handles.remove(&process_id) {
3361 #[allow(clippy::let_underscore_must_use)]
3362 let _ = tx.send(());
3363 tracing::debug!("KillHostProcess: sent kill for process_id={}", process_id);
3364 } else {
3365 tracing::debug!(
3366 "KillHostProcess: unknown process_id={} (already exited?)",
3367 process_id
3368 );
3369 }
3370 }
3371
3372 fn handle_set_authority(&mut self, payload: serde_json::Value) {
3373 match serde_json::from_value::<crate::services::authority::AuthorityPayload>(payload) {
3376 Ok(parsed) => {
3377 let trust = std::sync::Arc::clone(&self.authority.workspace_trust);
3380 let env = std::sync::Arc::clone(&self.authority.env_provider);
3381 match crate::services::authority::Authority::from_plugin_payload(parsed, trust, env)
3382 {
3383 Ok(auth) => {
3384 tracing::info!("Plugin installed new authority");
3385 self.install_authority(auth);
3386 }
3387 Err(e) => {
3388 tracing::warn!("setAuthority: invalid payload: {}", e);
3389 self.set_status_message(format!("setAuthority rejected: {}", e));
3390 }
3391 }
3392 }
3393 Err(e) => {
3394 tracing::warn!("setAuthority: failed to parse payload: {}", e);
3395 self.set_status_message(format!("setAuthority rejected: {}", e));
3396 }
3397 }
3398 }
3399
3400 fn handle_set_remote_indicator_state(&mut self, state: serde_json::Value) {
3401 match serde_json::from_value::<crate::view::ui::status_bar::RemoteIndicatorOverride>(state)
3404 {
3405 Ok(over) => {
3406 self.remote_indicator_override = Some(over);
3407 }
3408 Err(e) => {
3409 tracing::warn!("setRemoteIndicatorState: invalid payload: {}", e);
3410 self.set_status_message(format!("setRemoteIndicatorState rejected: {}", e));
3411 }
3412 }
3413 }
3414
3415 fn handle_spawn_process_wait(
3416 &mut self,
3417 process_id: u64,
3418 callback_id: fresh_core::api::JsCallbackId,
3419 ) {
3420 tracing::warn!(
3421 "SpawnProcessWait not fully implemented - process_id={}",
3422 process_id
3423 );
3424 self.plugin_manager.read().unwrap().reject_callback(
3425 callback_id,
3426 format!(
3427 "SpawnProcessWait not yet fully implemented for process_id={}",
3428 process_id
3429 ),
3430 );
3431 }
3432
3433 fn handle_delay(&mut self, callback_id: fresh_core::api::JsCallbackId, duration_ms: u64) {
3434 if let (Some(runtime), Some(bridge)) = (&self.tokio_runtime, &self.async_bridge) {
3435 let sender = bridge.sender();
3436 let callback_id_u64 = callback_id.as_u64();
3437 runtime.spawn(async move {
3438 tokio::time::sleep(tokio::time::Duration::from_millis(duration_ms)).await;
3439 #[allow(clippy::let_underscore_must_use)]
3440 let _ = sender.send(crate::services::async_bridge::AsyncMessage::Plugin(
3441 fresh_core::api::PluginAsyncMessage::DelayComplete {
3442 callback_id: callback_id_u64,
3443 },
3444 ));
3445 });
3446 } else {
3447 std::thread::sleep(std::time::Duration::from_millis(duration_ms));
3448 self.plugin_manager
3449 .read()
3450 .unwrap()
3451 .resolve_callback(callback_id, "null".to_string());
3452 }
3453 }
3454
3455 fn handle_http_fetch(
3456 &mut self,
3457 url: String,
3458 target_path: std::path::PathBuf,
3459 callback_id: fresh_core::api::JsCallbackId,
3460 ) {
3461 if let (Some(runtime), Some(bridge)) = (&self.tokio_runtime, &self.async_bridge) {
3462 let sender = bridge.sender();
3463 let process_id = callback_id.as_u64();
3464
3465 runtime.spawn(async move {
3466 let fetch =
3467 tokio::task::spawn_blocking(move || fetch_url_to_file(&url, &target_path))
3468 .await;
3469
3470 let (stdout, stderr, exit_code) = match fetch {
3471 Ok(Ok(status)) => {
3472 if (200..300).contains(&status) {
3473 (String::new(), String::new(), 0)
3474 } else {
3475 (String::new(), format!("HTTP {}", status), i32::from(status))
3476 }
3477 }
3478 Ok(Err(e)) => (String::new(), e, -1),
3479 Err(e) => (String::new(), format!("fetch task failed: {}", e), -1),
3480 };
3481
3482 #[allow(clippy::let_underscore_must_use)]
3483 let _ = sender.send(AsyncMessage::PluginProcessOutput {
3484 process_id,
3485 stdout,
3486 stderr,
3487 exit_code,
3488 });
3489 });
3490 } else {
3491 self.plugin_manager
3492 .read()
3493 .unwrap()
3494 .reject_callback(callback_id, "Async runtime not available".to_string());
3495 }
3496 }
3497
3498 fn handle_kill_background_process(&mut self, process_id: u64) {
3499 if let Some(handle) = self.background_process_handles.remove(&process_id) {
3500 handle.abort();
3501 tracing::debug!("Killed background process {}", process_id);
3502 }
3503 }
3504
3505 fn handle_create_virtual_buffer(&mut self, name: String, mode: String, read_only: bool) {
3506 let buffer_id =
3507 self.active_window_mut()
3508 .create_virtual_buffer(name.clone(), mode.clone(), read_only);
3509 tracing::info!(
3510 "Created virtual buffer '{}' with mode '{}' (id={:?})",
3511 name,
3512 mode,
3513 buffer_id
3514 );
3515 }
3517
3518 fn handle_set_virtual_buffer_content(
3519 &mut self,
3520 buffer_id: BufferId,
3521 entries: Vec<fresh_core::text_property::TextPropertyEntry>,
3522 ) {
3523 match self.set_virtual_buffer_content(buffer_id, entries) {
3524 Ok(()) => {
3525 tracing::debug!("Set virtual buffer content for {:?}", buffer_id);
3526 }
3527 Err(e) => {
3528 tracing::error!("Failed to set virtual buffer content: {}", e);
3529 }
3530 }
3531 }
3532
3533 fn handle_mount_widget_panel(
3534 &mut self,
3535 panel_id: u64,
3536 buffer_id: BufferId,
3537 spec: fresh_core::api::WidgetSpec,
3538 ) {
3539 let prev = std::collections::HashMap::new();
3544 let prev_focus = String::new();
3545 let panel_width = self.widget_panel_width(buffer_id);
3546 let out = crate::widgets::render_spec(&spec, &prev, &prev_focus, panel_width);
3547 let focus_cursor = out.focus_cursor;
3548 self.widget_registry.mount(
3549 panel_id,
3550 buffer_id,
3551 spec,
3552 out.hits,
3553 out.instance_states,
3554 out.focus_key,
3555 out.tabbable,
3556 );
3557 let entries = out.entries;
3558 if let Err(e) = self.set_virtual_buffer_content(buffer_id, entries.clone()) {
3559 tracing::error!(
3560 "Failed to render mounted widget panel {} into {:?}: {}",
3561 panel_id,
3562 buffer_id,
3563 e
3564 );
3565 } else {
3566 tracing::debug!(
3567 "Mounted widget panel {} into buffer {:?}",
3568 panel_id,
3569 buffer_id
3570 );
3571 }
3572 self.apply_widget_focus_cursor(buffer_id, &entries, focus_cursor);
3573 }
3574
3575 fn handle_update_widget_panel(&mut self, panel_id: u64, spec: fresh_core::api::WidgetSpec) {
3576 let prev = match self.widget_registry.instance_states(panel_id) {
3577 Some(s) => s.clone(),
3578 None => {
3579 tracing::debug!(
3580 "UpdateWidgetPanel for unknown panel {} ignored (not mounted)",
3581 panel_id
3582 );
3583 return;
3584 }
3585 };
3586 let prev_focus = self
3587 .widget_registry
3588 .focus_key(panel_id)
3589 .map(|s| s.to_string())
3590 .unwrap_or_default();
3591 let buffer_id_for_width = self
3592 .widget_registry
3593 .buffer_and_spec(panel_id)
3594 .map(|(b, _)| b)
3595 .unwrap_or(BufferId(0));
3596 let panel_width = self.widget_panel_width(buffer_id_for_width);
3597 let out = crate::widgets::render_spec(&spec, &prev, &prev_focus, panel_width);
3598 let focus_cursor = out.focus_cursor;
3599 let entries = out.entries;
3600 match self.widget_registry.update(
3601 panel_id,
3602 spec,
3603 out.hits,
3604 out.instance_states,
3605 out.focus_key,
3606 out.tabbable,
3607 ) {
3608 Ok(buffer_id) => {
3609 if let Err(e) = self.set_virtual_buffer_content(buffer_id, entries.clone()) {
3610 tracing::error!("Failed to render updated widget panel {}: {}", panel_id, e);
3611 }
3612 self.apply_widget_focus_cursor(buffer_id, &entries, focus_cursor);
3613 }
3614 Err(()) => {
3615 tracing::debug!(
3616 "UpdateWidgetPanel for unknown panel {} ignored (not mounted)",
3617 panel_id
3618 );
3619 }
3620 }
3621 }
3622
3623 fn apply_widget_focus_cursor(
3634 &mut self,
3635 buffer_id: BufferId,
3636 entries: &[fresh_core::text_property::TextPropertyEntry],
3637 focus_cursor: Option<crate::widgets::FocusCursor>,
3638 ) {
3639 let locked = self
3645 .windows
3646 .get(&self.active_window)
3647 .and_then(|w| w.buffers.get(&buffer_id))
3648 .map(|s| s.cursor_visibility_locked)
3649 .unwrap_or(false);
3650 if locked {
3651 return;
3652 }
3653
3654 let absolute_byte = focus_cursor.map(|fc| {
3655 let row = fc.buffer_row as usize;
3656 let prefix: usize = entries.iter().take(row).map(|e| e.text.len()).sum();
3657 prefix + fc.byte_in_row as usize
3658 });
3659
3660 if let Some(state) = self
3661 .windows
3662 .get_mut(&self.active_window)
3663 .map(|w| &mut w.buffers)
3664 .expect("active window present")
3665 .get_mut(&buffer_id)
3666 {
3667 state.show_cursors = absolute_byte.is_some();
3668 }
3669
3670 if let Some(byte) = absolute_byte {
3671 for vs in self
3672 .windows
3673 .get_mut(&self.active_window)
3674 .and_then(|w| w.split_view_states_mut())
3675 .expect("active window must have a populated split layout")
3676 .values_mut()
3677 {
3678 if vs.buffer_state(buffer_id).is_some() {
3679 let cursor = vs.cursors.primary_mut();
3680 cursor.position = byte;
3681 }
3682 }
3683 }
3684 }
3685
3686 fn widget_panel_width(&self, buffer_id: BufferId) -> u32 {
3695 let raw = self
3696 .windows
3697 .get(&self.active_window)
3698 .and_then(|w| w.buffers.splits())
3699 .map(|(_, vs)| vs)
3700 .expect("active window must have a populated split layout")
3701 .values()
3702 .find(|vs| vs.buffer_state(buffer_id).is_some() && vs.viewport.width > 0)
3703 .map(|vs| vs.viewport.width as u32)
3704 .unwrap_or_else(|| self.terminal_width.max(1) as u32);
3705 raw.saturating_sub(2).max(10)
3708 }
3709
3710 pub(super) fn rerender_widget_panel(&mut self, panel_id: u64) {
3716 let (buffer_id, is_floating, panel_width, out_pieces) = {
3725 let (buffer_id, spec) = match self.widget_registry.buffer_and_spec_ref(panel_id) {
3726 Some(s) => s,
3727 None => return,
3728 };
3729 let prev = self
3730 .widget_registry
3731 .instance_states(panel_id)
3732 .cloned()
3733 .unwrap_or_default();
3734 let prev_focus = self
3735 .widget_registry
3736 .focus_key(panel_id)
3737 .map(|s| s.to_string())
3738 .unwrap_or_default();
3739 let is_floating = buffer_id == FLOATING_PANEL_BUFFER_ID;
3740 let panel_width = if is_floating {
3741 self.floating_panel_inner_width()
3742 } else {
3743 self.widget_panel_width(buffer_id)
3744 };
3745 let out = crate::widgets::render_spec(spec, &prev, &prev_focus, panel_width);
3746 (buffer_id, is_floating, panel_width, out)
3747 };
3748 let _ = panel_width;
3749 let focus_cursor = out_pieces.focus_cursor;
3750 let entries = out_pieces.entries;
3751 let embeds = out_pieces.embeds;
3752 let overlays = out_pieces.overlays;
3753 let scroll_regions = out_pieces.scroll_regions;
3754 if self
3755 .widget_registry
3756 .update_side_effects(
3757 panel_id,
3758 out_pieces.hits,
3759 out_pieces.instance_states,
3760 out_pieces.focus_key,
3761 out_pieces.tabbable,
3762 )
3763 .is_err()
3764 {
3765 tracing::warn!("rerender_widget_panel({}) lost panel mid-call", panel_id);
3766 return;
3767 }
3768 if is_floating {
3769 if let Some(fwp) = self.floating_widget_panel.as_mut() {
3770 if fwp.panel_id == panel_id {
3771 fwp.entries = entries;
3772 fwp.focus_cursor = focus_cursor;
3773 fwp.embeds = embeds;
3774 fwp.overlays = overlays;
3775 fwp.scroll_regions = scroll_regions;
3776 }
3777 }
3778 return;
3779 }
3780 if let Err(e) = self.set_virtual_buffer_content(buffer_id, entries.clone()) {
3781 tracing::error!("rerender_widget_panel({}) failed: {}", panel_id, e);
3782 }
3783 self.apply_widget_focus_cursor(buffer_id, &entries, focus_cursor);
3784 }
3785
3786 fn handle_widget_mutate(&mut self, panel_id: u64, mutation: fresh_core::api::WidgetMutation) {
3792 use fresh_core::api::WidgetMutation;
3793
3794 if self.widget_registry.get(panel_id).is_none() {
3796 tracing::debug!(
3797 "WidgetMutate for unknown panel {} ignored (not mounted)",
3798 panel_id
3799 );
3800 return;
3801 }
3802
3803 match mutation {
3804 WidgetMutation::SetValue {
3805 widget_key,
3806 value,
3807 cursor_byte,
3808 } => {
3809 if let Some(panel) = self.widget_registry.get_mut(panel_id) {
3816 let (scroll, multiline, completions, sel_idx, scroll_off) =
3824 match panel.instance_states.get(&widget_key) {
3825 Some(crate::widgets::WidgetInstanceState::Text {
3826 editor,
3827 scroll,
3828 completions,
3829 completion_selected_index,
3830 completion_scroll_offset,
3831 }) => (
3832 *scroll,
3833 editor.multiline,
3834 completions.clone(),
3835 *completion_selected_index,
3836 *completion_scroll_offset,
3837 ),
3838 _ => (0u32, true, Vec::new(), 0usize, 0u32),
3839 };
3840 let mut editor = if multiline {
3841 crate::primitives::text_edit::TextEdit::with_text(&value)
3842 } else {
3843 crate::primitives::text_edit::TextEdit::single_line_with_text(&value)
3844 };
3845 let target = match cursor_byte {
3846 Some(c) if c >= 0 => (c as usize).min(value.len()),
3847 _ => value.len(),
3848 };
3849 editor.set_cursor_from_flat(target);
3850 panel.instance_states.insert(
3851 widget_key,
3852 crate::widgets::WidgetInstanceState::Text {
3853 editor,
3854 scroll,
3855 completions,
3856 completion_selected_index: sel_idx,
3857 completion_scroll_offset: scroll_off,
3858 },
3859 );
3860 }
3861 }
3862 WidgetMutation::SetChecked {
3863 widget_key,
3864 checked,
3865 } => {
3866 if let Some(panel) = self.widget_registry.get_mut(panel_id) {
3870 crate::widgets::set_toggle_checked_in_spec(
3871 &mut panel.spec,
3872 &widget_key,
3873 checked,
3874 );
3875 }
3876 }
3877 WidgetMutation::SetSelectedIndex { widget_key, index } => {
3878 if let Some(panel) = self.widget_registry.get_mut(panel_id) {
3880 let prev_scroll = match panel.instance_states.get(&widget_key) {
3881 Some(crate::widgets::WidgetInstanceState::List {
3882 scroll_offset, ..
3883 }) => *scroll_offset,
3884 _ => 0,
3885 };
3886 panel.instance_states.insert(
3887 widget_key,
3888 crate::widgets::WidgetInstanceState::List {
3889 scroll_offset: prev_scroll,
3890 selected_index: index,
3891 },
3892 );
3893 }
3894 }
3895 WidgetMutation::SetCompletions { widget_key, items } => {
3896 if let Some(panel) = self.widget_registry.get_mut(panel_id) {
3905 if let Some(crate::widgets::WidgetInstanceState::Text {
3906 completions,
3907 completion_selected_index,
3908 completion_scroll_offset,
3909 ..
3910 }) = panel.instance_states.get_mut(&widget_key)
3911 {
3912 *completions = items;
3913 *completion_selected_index = 0;
3914 *completion_scroll_offset = 0;
3915 }
3916 }
3917 }
3918 WidgetMutation::SetItems {
3919 widget_key,
3920 items,
3921 item_keys,
3922 } => {
3923 if let Some(panel) = self.widget_registry.get_mut(panel_id) {
3925 crate::widgets::set_list_items_in_spec(
3926 &mut panel.spec,
3927 &widget_key,
3928 items,
3929 item_keys,
3930 );
3931 }
3932 }
3933 WidgetMutation::SetExpandedKeys { widget_key, keys } => {
3934 if let Some(panel) = self.widget_registry.get_mut(panel_id) {
3936 let (prev_scroll, prev_sel) = match panel.instance_states.get(&widget_key) {
3937 Some(crate::widgets::WidgetInstanceState::Tree {
3938 scroll_offset,
3939 selected_index,
3940 ..
3941 }) => (*scroll_offset, *selected_index),
3942 _ => (0, -1),
3943 };
3944 let expanded: std::collections::HashSet<String> = keys.into_iter().collect();
3945 panel.instance_states.insert(
3946 widget_key,
3947 crate::widgets::WidgetInstanceState::Tree {
3948 scroll_offset: prev_scroll,
3949 selected_index: prev_sel,
3950 expanded_keys: expanded,
3951 },
3952 );
3953 }
3954 }
3955 WidgetMutation::SetCheckedKeys {
3956 widget_key,
3957 checked,
3958 keys,
3959 } => {
3960 if let Some(panel) = self.widget_registry.get_mut(panel_id) {
3968 crate::widgets::set_tree_checked_keys_in_spec(
3969 &mut panel.spec,
3970 &widget_key,
3971 checked,
3972 &keys,
3973 );
3974 }
3975 }
3976 WidgetMutation::AppendTreeNodes {
3977 widget_key,
3978 new_nodes,
3979 new_item_keys,
3980 } => {
3981 if let Some(panel) = self.widget_registry.get_mut(panel_id) {
3982 crate::widgets::append_tree_nodes_in_spec(
3983 &mut panel.spec,
3984 &widget_key,
3985 new_nodes,
3986 new_item_keys,
3987 );
3988 }
3989 }
3990 WidgetMutation::SetRawEntries {
3991 widget_key,
3992 entries,
3993 } => {
3994 if let Some(panel) = self.widget_registry.get_mut(panel_id) {
3995 crate::widgets::set_raw_entries_in_spec(&mut panel.spec, &widget_key, entries);
3996 }
3997 }
3998 WidgetMutation::SetFocusKey { widget_key } => {
3999 self.widget_registry.set_focus_key(panel_id, widget_key);
4004 }
4005 }
4006
4007 self.rerender_widget_panel(panel_id);
4011 }
4012
4013 pub(super) fn handle_widget_command(
4014 &mut self,
4015 panel_id: u64,
4016 action: fresh_core::api::WidgetAction,
4017 ) {
4018 use fresh_core::api::WidgetAction;
4019 match action {
4020 WidgetAction::FocusAdvance { delta } => {
4021 self.handle_widget_focus_advance(panel_id, delta);
4022 }
4023 WidgetAction::Activate => {
4024 self.handle_widget_activate(panel_id);
4025 }
4026 WidgetAction::SelectMove { delta } => {
4027 self.handle_widget_select_move(panel_id, delta);
4028 }
4029 WidgetAction::TextInputKey { key } => {
4030 self.handle_widget_text_key(panel_id, &key);
4031 }
4032 WidgetAction::TextInputChar { text } => {
4033 self.handle_widget_text_char(panel_id, &text);
4034 }
4035 WidgetAction::Key { key } => {
4036 self.handle_widget_key(panel_id, &key);
4037 }
4038 }
4039 }
4040
4041 fn handle_widget_key(&mut self, panel_id: u64, key: &str) {
4042 let panel = match self.widget_registry.get(panel_id) {
4046 Some(p) => p,
4047 None => return,
4048 };
4049 let focus_key = panel.focus_key.clone();
4050 let widget = if focus_key.is_empty() {
4051 None
4052 } else {
4053 crate::widgets::find_widget_by_key(&panel.spec, &focus_key)
4054 };
4055 let completions_open = matches!(key, "Tab" | "Up" | "Down" | "Enter" | "Escape")
4065 && self.focused_text_completions_open(panel_id);
4066 if completions_open {
4067 match key {
4068 "Tab" => {
4069 self.fire_completion_accept(panel_id);
4070 return;
4075 }
4076 "Up" => {
4077 self.move_focused_text_completion_index(panel_id, -1);
4078 self.rerender_widget_panel(panel_id);
4083 return;
4084 }
4085 "Down" => {
4086 self.move_focused_text_completion_index(panel_id, 1);
4087 self.rerender_widget_panel(panel_id);
4088 return;
4089 }
4090 "Enter" | "Escape" => {
4091 self.dismiss_focused_text_completions(panel_id);
4092 self.rerender_widget_panel(panel_id);
4093 return;
4094 }
4095 _ => {}
4096 }
4097 }
4098 match key {
4099 "Tab" => self.handle_widget_focus_advance(panel_id, 1),
4100 "Shift+Tab" => self.handle_widget_focus_advance(panel_id, -1),
4101 "Up" | "Down" => {
4102 let delta = if key == "Up" { -1 } else { 1 };
4103 match widget {
4104 Some(fresh_core::api::WidgetSpec::List { .. }) => {
4105 self.handle_widget_select_move(panel_id, delta);
4106 }
4107 Some(fresh_core::api::WidgetSpec::Tree { .. }) => {
4108 self.handle_widget_tree_select_move(panel_id, delta);
4109 }
4110 Some(fresh_core::api::WidgetSpec::Text { rows, .. }) if *rows > 1 => {
4111 self.handle_widget_text_key(panel_id, key);
4117 }
4118 _ => {
4119 let scrollable = self
4127 .widget_registry
4128 .get(panel_id)
4129 .and_then(|p| find_scrollable_widget_key(&p.spec));
4130 if let Some(target_key) = scrollable {
4131 let target_kind = self.widget_registry.get(panel_id).and_then(|p| {
4132 crate::widgets::find_widget_by_key(&p.spec, &target_key).cloned()
4133 });
4134 match target_kind {
4135 Some(fresh_core::api::WidgetSpec::List { .. }) => {
4136 self.handle_widget_select_move_for_key(
4137 panel_id,
4138 &target_key,
4139 delta,
4140 );
4141 }
4142 Some(fresh_core::api::WidgetSpec::Tree { .. }) => {
4143 self.handle_widget_tree_select_move_for_key(
4144 panel_id,
4145 &target_key,
4146 delta,
4147 );
4148 }
4149 _ => {}
4150 }
4151 }
4152 }
4153 }
4154 }
4155 "PageUp" | "PageDown" => {
4156 let page = match widget {
4160 Some(fresh_core::api::WidgetSpec::List { visible_rows, .. })
4161 | Some(fresh_core::api::WidgetSpec::Tree { visible_rows, .. }) => {
4162 visible_rows.saturating_sub(1).max(1) as i32
4163 }
4164 _ => 0,
4165 };
4166 if page == 0 {
4167 return;
4168 }
4169 let delta = if key == "PageUp" { -page } else { page };
4170 match widget {
4171 Some(fresh_core::api::WidgetSpec::List { .. }) => {
4172 self.handle_widget_select_move(panel_id, delta);
4173 }
4174 Some(fresh_core::api::WidgetSpec::Tree { .. }) => {
4175 self.handle_widget_tree_select_move(panel_id, delta);
4176 }
4177 _ => {}
4178 }
4179 }
4180 "Left" | "Right" => match widget {
4181 Some(fresh_core::api::WidgetSpec::Text { .. }) => {
4182 self.handle_widget_text_key(panel_id, key);
4183 }
4184 Some(fresh_core::api::WidgetSpec::Tree { .. }) => {
4185 self.handle_widget_tree_lateral(panel_id, key == "Right");
4186 }
4187 _ => {}
4188 },
4189 "Backspace" | "Delete" | "Home" | "End" => match widget {
4190 Some(fresh_core::api::WidgetSpec::Text { .. }) => {
4191 self.handle_widget_text_key(panel_id, key);
4192 }
4193 _ => {}
4194 },
4195 "Enter" => match widget {
4196 Some(fresh_core::api::WidgetSpec::Button { .. })
4197 | Some(fresh_core::api::WidgetSpec::Toggle { .. }) => {
4198 self.handle_widget_activate(panel_id);
4199 }
4200 Some(fresh_core::api::WidgetSpec::List { .. }) => {
4201 self.fire_list_activate(panel_id, &focus_key);
4202 }
4203 Some(fresh_core::api::WidgetSpec::Tree { .. }) => {
4204 self.fire_tree_activate(panel_id, &focus_key);
4205 }
4206 Some(fresh_core::api::WidgetSpec::Text { rows, .. }) => {
4207 if *rows > 1 {
4208 self.handle_widget_text_key(panel_id, "Enter");
4214 } else if let Some(target_key) = self
4215 .widget_registry
4216 .get(panel_id)
4217 .and_then(|p| find_scrollable_widget_key(&p.spec))
4218 {
4219 let kind = self.widget_registry.get(panel_id).and_then(|p| {
4225 crate::widgets::find_widget_by_key(&p.spec, &target_key).cloned()
4226 });
4227 match kind {
4228 Some(fresh_core::api::WidgetSpec::List { .. }) => {
4229 self.fire_list_activate(panel_id, &target_key);
4230 }
4231 Some(fresh_core::api::WidgetSpec::Tree { .. }) => {
4232 self.fire_tree_activate(panel_id, &target_key);
4233 }
4234 _ => {}
4235 }
4236 } else {
4237 self.handle_widget_focus_advance(panel_id, 1);
4240 }
4241 }
4242 _ => {}
4243 },
4244 "Space" => match widget {
4245 Some(fresh_core::api::WidgetSpec::Button { .. })
4246 | Some(fresh_core::api::WidgetSpec::Toggle { .. }) => {
4247 self.handle_widget_activate(panel_id);
4248 }
4249 Some(fresh_core::api::WidgetSpec::Text { .. }) => {
4250 self.handle_widget_text_char(panel_id, " ");
4251 }
4252 Some(fresh_core::api::WidgetSpec::List { .. }) => {
4253 self.fire_list_activate(panel_id, &focus_key);
4254 }
4255 Some(fresh_core::api::WidgetSpec::Tree { .. }) => {
4256 if !self.fire_tree_toggle_if_checkable(panel_id, &focus_key) {
4263 self.fire_tree_activate(panel_id, &focus_key);
4264 }
4265 }
4266 _ => {}
4267 },
4268 _ => {} }
4270 }
4271
4272 fn handle_widget_focus_advance(&mut self, panel_id: u64, delta: i32) {
4273 let panel = match self.widget_registry.get(panel_id) {
4274 Some(p) => p,
4275 None => return,
4276 };
4277 if panel.tabbable.is_empty() {
4278 return;
4279 }
4280 let cur_idx = panel
4281 .tabbable
4282 .iter()
4283 .position(|k| k == &panel.focus_key)
4284 .unwrap_or(0) as i32;
4285 let n = panel.tabbable.len() as i32;
4286 let new_idx = ((cur_idx + delta) % n + n) % n;
4287 let new_key = panel.tabbable[new_idx as usize].clone();
4288 self.set_panel_focus_and_notify(panel_id, new_key);
4289 self.rerender_widget_panel(panel_id);
4290 }
4291
4292 pub(crate) fn set_panel_focus_and_notify(&mut self, panel_id: u64, new_key: String) {
4302 let old_key = self
4303 .widget_registry
4304 .focus_key(panel_id)
4305 .map(|s| s.to_string())
4306 .unwrap_or_default();
4307 if old_key == new_key {
4308 return;
4309 }
4310 self.widget_registry
4311 .set_focus_key(panel_id, new_key.clone());
4312 if self
4313 .plugin_manager
4314 .read()
4315 .unwrap()
4316 .has_hook_handlers("widget_event")
4317 {
4318 self.plugin_manager.read().unwrap().run_hook(
4319 "widget_event",
4320 fresh_core::hooks::HookArgs::WidgetEvent {
4321 panel_id,
4322 widget_key: new_key,
4323 event_type: "focus".to_string(),
4324 payload: serde_json::json!({ "previous": old_key }),
4325 },
4326 );
4327 }
4328 }
4329
4330 fn handle_widget_activate(&mut self, panel_id: u64) {
4331 let panel = match self.widget_registry.get(panel_id) {
4335 Some(p) => p,
4336 None => return,
4337 };
4338 let focus_key = panel.focus_key.clone();
4339 if focus_key.is_empty() {
4340 return;
4341 }
4342 let widget = crate::widgets::find_widget_by_key(&panel.spec, &focus_key);
4343 let (event_type, payload) = match widget {
4344 Some(fresh_core::api::WidgetSpec::Button { disabled: true, .. }) => return,
4351 Some(fresh_core::api::WidgetSpec::Button { .. }) => ("activate", serde_json::json!({})),
4352 Some(fresh_core::api::WidgetSpec::Toggle { checked, .. }) => {
4353 ("toggle", serde_json::json!({ "checked": !checked }))
4354 }
4355 _ => return,
4356 };
4357 if self
4358 .plugin_manager
4359 .read()
4360 .unwrap()
4361 .has_hook_handlers("widget_event")
4362 {
4363 self.plugin_manager.read().unwrap().run_hook(
4364 "widget_event",
4365 fresh_core::hooks::HookArgs::WidgetEvent {
4366 panel_id,
4367 widget_key: focus_key,
4368 event_type: event_type.to_string(),
4369 payload,
4370 },
4371 );
4372 }
4373 }
4374
4375 fn focused_text_completions_open(&self, panel_id: u64) -> bool {
4387 let panel = match self.widget_registry.get(panel_id) {
4388 Some(p) => p,
4389 None => return false,
4390 };
4391 if panel.focus_key.is_empty() {
4392 return false;
4393 }
4394 matches!(
4395 panel.instance_states.get(&panel.focus_key),
4396 Some(crate::widgets::WidgetInstanceState::Text { completions, .. })
4397 if !completions.is_empty()
4398 )
4399 }
4400
4401 fn move_focused_text_completion_index(&mut self, panel_id: u64, delta: i32) {
4412 let panel = match self.widget_registry.get(panel_id) {
4419 Some(p) => p,
4420 None => return,
4421 };
4422 let focus_key = panel.focus_key.clone();
4423 if focus_key.is_empty() {
4424 return;
4425 }
4426 let spec_visible_rows = match crate::widgets::find_widget_by_key(&panel.spec, &focus_key) {
4427 Some(fresh_core::api::WidgetSpec::Text {
4428 completions_visible_rows,
4429 ..
4430 }) => *completions_visible_rows,
4431 _ => 0,
4432 };
4433 let visible = if spec_visible_rows == 0 {
4434 5u32
4435 } else {
4436 spec_visible_rows
4437 };
4438 let panel = match self.widget_registry.get_mut(panel_id) {
4439 Some(p) => p,
4440 None => return,
4441 };
4442 if let Some(crate::widgets::WidgetInstanceState::Text {
4443 completions,
4444 completion_selected_index,
4445 completion_scroll_offset,
4446 ..
4447 }) = panel.instance_states.get_mut(&focus_key)
4448 {
4449 if completions.is_empty() {
4450 return;
4451 }
4452 let max = (completions.len() - 1) as i32;
4453 let cur = *completion_selected_index as i32;
4454 let next = (cur + delta).clamp(0, max);
4455 *completion_selected_index = next as usize;
4456 let next_u = next as u32;
4461 if next_u < *completion_scroll_offset {
4462 *completion_scroll_offset = next_u;
4463 } else if next_u >= *completion_scroll_offset + visible {
4464 *completion_scroll_offset = next_u + 1 - visible;
4465 }
4466 }
4467 }
4468
4469 fn dismiss_focused_text_completions(&mut self, panel_id: u64) {
4476 let focus_key = {
4477 let panel = match self.widget_registry.get_mut(panel_id) {
4478 Some(p) => p,
4479 None => return,
4480 };
4481 let focus_key = panel.focus_key.clone();
4482 if focus_key.is_empty() {
4483 return;
4484 }
4485 if let Some(crate::widgets::WidgetInstanceState::Text {
4486 completions,
4487 completion_selected_index,
4488 ..
4489 }) = panel.instance_states.get_mut(&focus_key)
4490 {
4491 if completions.is_empty() {
4492 return;
4493 }
4494 completions.clear();
4495 *completion_selected_index = 0;
4496 } else {
4497 return;
4498 }
4499 focus_key
4500 };
4501 if self
4502 .plugin_manager
4503 .read()
4504 .unwrap()
4505 .has_hook_handlers("widget_event")
4506 {
4507 self.plugin_manager.read().unwrap().run_hook(
4508 "widget_event",
4509 fresh_core::hooks::HookArgs::WidgetEvent {
4510 panel_id,
4511 widget_key: focus_key,
4512 event_type: "completion_dismiss".into(),
4513 payload: serde_json::json!({}),
4514 },
4515 );
4516 }
4517 }
4518
4519 fn fire_completion_accept(&mut self, panel_id: u64) {
4531 let (focus_key, value) = {
4532 let panel = match self.widget_registry.get(panel_id) {
4533 Some(p) => p,
4534 None => return,
4535 };
4536 let focus_key = panel.focus_key.clone();
4537 if focus_key.is_empty() {
4538 return;
4539 }
4540 match panel.instance_states.get(&focus_key) {
4541 Some(crate::widgets::WidgetInstanceState::Text {
4542 completions,
4543 completion_selected_index,
4544 ..
4545 }) if !completions.is_empty() => {
4546 let idx = (*completion_selected_index).min(completions.len() - 1);
4547 (focus_key, completions[idx].value.clone())
4548 }
4549 _ => return,
4550 }
4551 };
4552 if self
4553 .plugin_manager
4554 .read()
4555 .unwrap()
4556 .has_hook_handlers("widget_event")
4557 {
4558 self.plugin_manager.read().unwrap().run_hook(
4559 "widget_event",
4560 fresh_core::hooks::HookArgs::WidgetEvent {
4561 panel_id,
4562 widget_key: focus_key,
4563 event_type: "completion_accept".into(),
4564 payload: serde_json::json!({ "value": value }),
4565 },
4566 );
4567 }
4568 }
4569
4570 fn fire_list_activate(&mut self, panel_id: u64, focus_key: &str) {
4571 let panel = match self.widget_registry.get(panel_id) {
4572 Some(p) => p,
4573 None => return,
4574 };
4575 let widget = crate::widgets::find_widget_by_key(&panel.spec, focus_key);
4576 let (spec_sel, item_keys) = match widget {
4577 Some(fresh_core::api::WidgetSpec::List {
4578 selected_index,
4579 item_keys,
4580 ..
4581 }) => (*selected_index, item_keys.clone()),
4582 _ => return,
4583 };
4584 let sel = match panel.instance_states.get(focus_key) {
4585 Some(crate::widgets::WidgetInstanceState::List { selected_index, .. }) => {
4586 *selected_index
4587 }
4588 _ => spec_sel,
4589 };
4590 if sel < 0 {
4591 return;
4592 }
4593 let item_key = item_keys.get(sel as usize).cloned().unwrap_or_default();
4594 if self
4595 .plugin_manager
4596 .read()
4597 .unwrap()
4598 .has_hook_handlers("widget_event")
4599 {
4600 self.plugin_manager.read().unwrap().run_hook(
4601 "widget_event",
4602 fresh_core::hooks::HookArgs::WidgetEvent {
4603 panel_id,
4604 widget_key: focus_key.to_string(),
4605 event_type: "activate".into(),
4606 payload: serde_json::json!({
4607 "index": sel,
4608 "key": item_key,
4609 }),
4610 },
4611 );
4612 }
4613 }
4614
4615 fn handle_widget_select_move(&mut self, panel_id: u64, delta: i32) {
4616 let focus_key = match self.widget_registry.get(panel_id) {
4617 Some(p) => p.focus_key.clone(),
4618 None => return,
4619 };
4620 if focus_key.is_empty() {
4621 return;
4622 }
4623 self.handle_widget_select_move_for_key(panel_id, &focus_key, delta);
4624 }
4625
4626 pub(super) fn set_widget_list_selected_index(
4634 &mut self,
4635 panel_id: u64,
4636 widget_key: &str,
4637 index: i32,
4638 ) {
4639 if let Some(panel) = self.widget_registry.get_mut(panel_id) {
4640 let prev_scroll = match panel.instance_states.get(widget_key) {
4641 Some(crate::widgets::WidgetInstanceState::List { scroll_offset, .. }) => {
4642 *scroll_offset
4643 }
4644 _ => 0,
4645 };
4646 panel.instance_states.insert(
4647 widget_key.to_string(),
4648 crate::widgets::WidgetInstanceState::List {
4649 scroll_offset: prev_scroll,
4650 selected_index: index,
4651 },
4652 );
4653 }
4654 self.rerender_widget_panel(panel_id);
4655 }
4656
4657 fn handle_widget_select_move_for_key(&mut self, panel_id: u64, widget_key: &str, delta: i32) {
4663 let panel = match self.widget_registry.get(panel_id) {
4664 Some(p) => p,
4665 None => return,
4666 };
4667 let widget = crate::widgets::find_widget_by_key(&panel.spec, widget_key);
4668 let (spec_sel, total, item_keys) = match widget {
4669 Some(fresh_core::api::WidgetSpec::List {
4670 selected_index,
4671 items,
4672 item_keys,
4673 ..
4674 }) => (*selected_index, items.len() as i32, item_keys.clone()),
4675 _ => return,
4676 };
4677 if total == 0 {
4678 return;
4679 }
4680 let cur_sel = match panel.instance_states.get(widget_key) {
4681 Some(crate::widgets::WidgetInstanceState::List { selected_index, .. }) => {
4682 *selected_index
4683 }
4684 _ => spec_sel,
4685 };
4686 let raw = if cur_sel < 0 { 0 } else { cur_sel + delta };
4687 let new_sel = raw.clamp(0, total - 1);
4688 let new_key = item_keys.get(new_sel as usize).cloned().unwrap_or_default();
4689 if let Some(panel_mut) = self.widget_registry.get_mut(panel_id) {
4690 let cur_scroll = match panel_mut.instance_states.get(widget_key) {
4691 Some(crate::widgets::WidgetInstanceState::List { scroll_offset, .. }) => {
4692 *scroll_offset
4693 }
4694 _ => 0,
4695 };
4696 panel_mut.instance_states.insert(
4697 widget_key.to_string(),
4698 crate::widgets::WidgetInstanceState::List {
4699 scroll_offset: cur_scroll,
4700 selected_index: new_sel,
4701 },
4702 );
4703 }
4704 self.rerender_widget_panel(panel_id);
4705 if self
4706 .plugin_manager
4707 .read()
4708 .unwrap()
4709 .has_hook_handlers("widget_event")
4710 {
4711 self.plugin_manager.read().unwrap().run_hook(
4712 "widget_event",
4713 fresh_core::hooks::HookArgs::WidgetEvent {
4714 panel_id,
4715 widget_key: widget_key.to_string(),
4716 event_type: "select".into(),
4717 payload: serde_json::json!({ "index": new_sel, "key": new_key }),
4718 },
4719 );
4720 }
4721 }
4722
4723 fn handle_widget_tree_select_move(&mut self, panel_id: u64, delta: i32) {
4728 let focus_key = match self.widget_registry.get(panel_id) {
4729 Some(p) => p.focus_key.clone(),
4730 None => return,
4731 };
4732 if focus_key.is_empty() {
4733 return;
4734 }
4735 self.handle_widget_tree_select_move_for_key(panel_id, &focus_key, delta);
4736 }
4737
4738 fn handle_widget_tree_select_move_for_key(
4740 &mut self,
4741 panel_id: u64,
4742 widget_key: &str,
4743 delta: i32,
4744 ) {
4745 let panel = match self.widget_registry.get(panel_id) {
4746 Some(p) => p,
4747 None => return,
4748 };
4749 let widget = crate::widgets::find_widget_by_key(&panel.spec, widget_key);
4750 let (spec_sel, nodes, item_keys) = match widget {
4751 Some(fresh_core::api::WidgetSpec::Tree {
4752 selected_index,
4753 nodes,
4754 item_keys,
4755 ..
4756 }) => (*selected_index, nodes.clone(), item_keys.clone()),
4757 _ => return,
4758 };
4759 if nodes.is_empty() {
4760 return;
4761 }
4762 let (cur_sel, cur_scroll, expanded) = match panel.instance_states.get(widget_key) {
4763 Some(crate::widgets::WidgetInstanceState::Tree {
4764 selected_index,
4765 scroll_offset,
4766 expanded_keys,
4767 }) => (*selected_index, *scroll_offset, expanded_keys.clone()),
4768 _ => (spec_sel, 0u32, std::collections::HashSet::<String>::new()),
4769 };
4770 let visible_indices = collect_visible_tree_indices(&nodes, &item_keys, &expanded);
4771 if visible_indices.is_empty() {
4772 return;
4773 }
4774 let cur_pos = if cur_sel < 0 {
4775 if delta > 0 {
4776 -1
4777 } else {
4778 visible_indices.len() as i32
4779 }
4780 } else {
4781 visible_indices
4782 .iter()
4783 .position(|&v| v as i32 == cur_sel)
4784 .map(|p| p as i32)
4785 .unwrap_or(-1)
4786 };
4787 let new_pos = (cur_pos + delta).clamp(0, (visible_indices.len() as i32) - 1);
4788 let new_abs = visible_indices[new_pos as usize];
4789 let new_key = item_keys.get(new_abs).cloned().unwrap_or_default();
4790 if let Some(panel_mut) = self.widget_registry.get_mut(panel_id) {
4791 panel_mut.instance_states.insert(
4792 widget_key.to_string(),
4793 crate::widgets::WidgetInstanceState::Tree {
4794 scroll_offset: cur_scroll,
4795 selected_index: new_abs as i32,
4796 expanded_keys: expanded,
4797 },
4798 );
4799 }
4800 self.rerender_widget_panel(panel_id);
4801 if self
4802 .plugin_manager
4803 .read()
4804 .unwrap()
4805 .has_hook_handlers("widget_event")
4806 {
4807 self.plugin_manager.read().unwrap().run_hook(
4808 "widget_event",
4809 fresh_core::hooks::HookArgs::WidgetEvent {
4810 panel_id,
4811 widget_key: widget_key.to_string(),
4812 event_type: "select".into(),
4813 payload: serde_json::json!({ "index": new_abs as i64, "key": new_key }),
4814 },
4815 );
4816 }
4817 }
4818
4819 pub(super) fn handle_widget_panel_wheel(
4829 &mut self,
4830 buffer_id: crate::model::event::BufferId,
4831 delta: i32,
4832 ) -> bool {
4833 let panels = self.widget_registry.panels_for_buffer(buffer_id);
4834 let mut consumed = false;
4835 for panel_id in panels {
4836 if self.focused_text_completions_open(panel_id) {
4842 self.scroll_focused_text_completions(panel_id, delta);
4843 self.rerender_widget_panel(panel_id);
4852 consumed = true;
4853 continue;
4854 }
4855 let spec = match self.widget_registry.get(panel_id) {
4856 Some(p) => p.spec.clone(),
4857 None => continue,
4858 };
4859 let Some(widget_key) = find_scrollable_widget_key(&spec) else {
4860 continue;
4861 };
4862 let widget = crate::widgets::find_widget_by_key(&spec, &widget_key);
4863 match widget {
4864 Some(fresh_core::api::WidgetSpec::Tree { .. }) => {
4865 consumed |= self.handle_widget_tree_wheel(panel_id, &widget_key, delta);
4873 }
4874 Some(fresh_core::api::WidgetSpec::List { .. }) => {
4875 consumed |= self.handle_widget_list_wheel(panel_id, &widget_key, delta);
4876 }
4877 _ => {}
4878 }
4879 }
4880 consumed
4881 }
4882
4883 fn scroll_focused_text_completions(&mut self, panel_id: u64, delta: i32) {
4890 let panel = match self.widget_registry.get(panel_id) {
4891 Some(p) => p,
4892 None => return,
4893 };
4894 let focus_key = panel.focus_key.clone();
4895 if focus_key.is_empty() {
4896 return;
4897 }
4898 let spec_visible_rows = match crate::widgets::find_widget_by_key(&panel.spec, &focus_key) {
4899 Some(fresh_core::api::WidgetSpec::Text {
4900 completions_visible_rows,
4901 ..
4902 }) => *completions_visible_rows,
4903 _ => 0,
4904 };
4905 let visible = if spec_visible_rows == 0 {
4906 5u32
4907 } else {
4908 spec_visible_rows
4909 };
4910 let panel = match self.widget_registry.get_mut(panel_id) {
4911 Some(p) => p,
4912 None => return,
4913 };
4914 if let Some(crate::widgets::WidgetInstanceState::Text {
4915 completions,
4916 completion_scroll_offset,
4917 ..
4918 }) = panel.instance_states.get_mut(&focus_key)
4919 {
4920 if completions.is_empty() {
4921 return;
4922 }
4923 let total = completions.len() as u32;
4924 let max_scroll = total.saturating_sub(visible.min(total));
4925 let next = (*completion_scroll_offset as i32 + delta).clamp(0, max_scroll as i32);
4926 *completion_scroll_offset = next as u32;
4927 }
4928 }
4929
4930 fn handle_widget_tree_wheel(&mut self, panel_id: u64, widget_key: &str, delta: i32) -> bool {
4935 let panel = match self.widget_registry.get(panel_id) {
4936 Some(p) => p,
4937 None => return false,
4938 };
4939 let widget = crate::widgets::find_widget_by_key(&panel.spec, widget_key);
4940 let (visible_rows, nodes, item_keys) = match widget {
4941 Some(fresh_core::api::WidgetSpec::Tree {
4942 visible_rows,
4943 nodes,
4944 item_keys,
4945 ..
4946 }) => (*visible_rows, nodes.clone(), item_keys.clone()),
4947 _ => return false,
4948 };
4949 if nodes.is_empty() {
4950 return false;
4951 }
4952 let (cur_sel, cur_scroll, expanded) = match panel.instance_states.get(widget_key) {
4953 Some(crate::widgets::WidgetInstanceState::Tree {
4954 selected_index,
4955 scroll_offset,
4956 expanded_keys,
4957 }) => (*selected_index, *scroll_offset, expanded_keys.clone()),
4958 _ => (-1, 0, std::collections::HashSet::<String>::new()),
4959 };
4960 let visible_indices = collect_visible_tree_indices(&nodes, &item_keys, &expanded);
4961 if visible_indices.is_empty() {
4962 return false;
4963 }
4964 let visible = visible_rows.max(1);
4965 let total_visible = visible_indices.len() as u32;
4966 let max_scroll = total_visible.saturating_sub(visible);
4967 let new_scroll = (cur_scroll as i32 + delta).clamp(0, max_scroll as i32) as u32;
4968 if new_scroll == cur_scroll {
4969 return false;
4970 }
4971 let cur_pos: Option<u32> = if cur_sel >= 0 {
4973 visible_indices
4974 .iter()
4975 .position(|&v| v as i32 == cur_sel)
4976 .map(|p| p as u32)
4977 } else {
4978 None
4979 };
4980 let new_sel_abs = match cur_pos {
4981 Some(pos) if pos < new_scroll => visible_indices[new_scroll as usize] as i32,
4982 Some(pos) if pos >= new_scroll + visible => {
4983 visible_indices[(new_scroll + visible - 1) as usize] as i32
4984 }
4985 _ => cur_sel,
4986 };
4987 if let Some(panel_mut) = self.widget_registry.get_mut(panel_id) {
4988 panel_mut.instance_states.insert(
4989 widget_key.to_string(),
4990 crate::widgets::WidgetInstanceState::Tree {
4991 scroll_offset: new_scroll,
4992 selected_index: new_sel_abs,
4993 expanded_keys: expanded,
4994 },
4995 );
4996 }
4997 self.rerender_widget_panel(panel_id);
4998 true
4999 }
5000
5001 fn handle_widget_list_wheel(&mut self, panel_id: u64, widget_key: &str, delta: i32) -> bool {
5004 let panel = match self.widget_registry.get(panel_id) {
5005 Some(p) => p,
5006 None => return false,
5007 };
5008 let widget = crate::widgets::find_widget_by_key(&panel.spec, widget_key);
5009 let (visible_rows, total) = match widget {
5010 Some(fresh_core::api::WidgetSpec::List {
5011 visible_rows,
5012 items,
5013 ..
5014 }) => (*visible_rows, items.len() as u32),
5015 _ => return false,
5016 };
5017 if total == 0 {
5018 return false;
5019 }
5020 let (cur_sel, cur_scroll) = match panel.instance_states.get(widget_key) {
5021 Some(crate::widgets::WidgetInstanceState::List {
5022 selected_index,
5023 scroll_offset,
5024 }) => (*selected_index, *scroll_offset),
5025 _ => (-1, 0),
5026 };
5027 let visible = visible_rows.max(1);
5028 let max_scroll = total.saturating_sub(visible);
5029 let new_scroll = (cur_scroll as i32 + delta).clamp(0, max_scroll as i32) as u32;
5030 if new_scroll == cur_scroll {
5031 return false;
5032 }
5033 let new_sel = if cur_sel < 0 {
5034 cur_sel
5035 } else if (cur_sel as u32) < new_scroll {
5036 new_scroll as i32
5037 } else if (cur_sel as u32) >= new_scroll + visible {
5038 (new_scroll + visible - 1) as i32
5039 } else {
5040 cur_sel
5041 };
5042 if let Some(panel_mut) = self.widget_registry.get_mut(panel_id) {
5043 panel_mut.instance_states.insert(
5044 widget_key.to_string(),
5045 crate::widgets::WidgetInstanceState::List {
5046 scroll_offset: new_scroll,
5047 selected_index: new_sel,
5048 },
5049 );
5050 }
5051 self.rerender_widget_panel(panel_id);
5052 true
5053 }
5054
5055 fn handle_widget_tree_lateral(&mut self, panel_id: u64, is_right: bool) {
5065 let panel = match self.widget_registry.get(panel_id) {
5066 Some(p) => p,
5067 None => return,
5068 };
5069 let focus_key = panel.focus_key.clone();
5070 if focus_key.is_empty() {
5071 return;
5072 }
5073 let widget = crate::widgets::find_widget_by_key(&panel.spec, &focus_key);
5074 let (spec_sel, nodes, item_keys) = match widget {
5075 Some(fresh_core::api::WidgetSpec::Tree {
5076 selected_index,
5077 nodes,
5078 item_keys,
5079 ..
5080 }) => (*selected_index, nodes.clone(), item_keys.clone()),
5081 _ => return,
5082 };
5083 if nodes.is_empty() {
5084 return;
5085 }
5086 let (cur_sel, cur_scroll, mut expanded) = match panel.instance_states.get(&focus_key) {
5087 Some(crate::widgets::WidgetInstanceState::Tree {
5088 selected_index,
5089 scroll_offset,
5090 expanded_keys,
5091 }) => (*selected_index, *scroll_offset, expanded_keys.clone()),
5092 _ => (spec_sel, 0u32, std::collections::HashSet::<String>::new()),
5093 };
5094 if cur_sel < 0 {
5095 return;
5096 }
5097 let sel_idx = cur_sel as usize;
5098 let node = match nodes.get(sel_idx) {
5099 Some(n) => n,
5100 None => return,
5101 };
5102 let key = item_keys.get(sel_idx).cloned().unwrap_or_default();
5103 let was_expanded = !key.is_empty() && expanded.contains(&key);
5104
5105 let mut new_sel = cur_sel;
5106 let mut expansion_changed: Option<bool> = None; if is_right {
5108 if node.has_children && !was_expanded && !key.is_empty() {
5109 expanded.insert(key.clone());
5110 expansion_changed = Some(true);
5111 }
5112 } else if node.has_children && was_expanded && !key.is_empty() {
5113 expanded.remove(&key);
5114 expansion_changed = Some(false);
5115 } else if let Some(parent_idx) = crate::widgets::tree_parent_index(&nodes, sel_idx) {
5116 new_sel = parent_idx as i32;
5117 }
5118 if expansion_changed.is_none() && new_sel == cur_sel {
5120 return;
5121 }
5122 let final_key = item_keys.get(new_sel as usize).cloned().unwrap_or_default();
5123 if let Some(panel_mut) = self.widget_registry.get_mut(panel_id) {
5124 panel_mut.instance_states.insert(
5125 focus_key.clone(),
5126 crate::widgets::WidgetInstanceState::Tree {
5127 scroll_offset: cur_scroll,
5128 selected_index: new_sel,
5129 expanded_keys: expanded,
5130 },
5131 );
5132 }
5133 self.rerender_widget_panel(panel_id);
5134 if self
5135 .plugin_manager
5136 .read()
5137 .unwrap()
5138 .has_hook_handlers("widget_event")
5139 {
5140 if let Some(now_expanded) = expansion_changed {
5141 self.plugin_manager.read().unwrap().run_hook(
5142 "widget_event",
5143 fresh_core::hooks::HookArgs::WidgetEvent {
5144 panel_id,
5145 widget_key: focus_key.clone(),
5146 event_type: "expand".into(),
5147 payload: serde_json::json!({
5148 "index": cur_sel as i64,
5149 "key": key,
5150 "expanded": now_expanded,
5151 }),
5152 },
5153 );
5154 } else if new_sel != cur_sel {
5155 self.plugin_manager.read().unwrap().run_hook(
5156 "widget_event",
5157 fresh_core::hooks::HookArgs::WidgetEvent {
5158 panel_id,
5159 widget_key: focus_key,
5160 event_type: "select".into(),
5161 payload: serde_json::json!({
5162 "index": new_sel as i64,
5163 "key": final_key,
5164 }),
5165 },
5166 );
5167 }
5168 }
5169 }
5170
5171 pub(crate) fn handle_widget_tree_expand_toggle(
5175 &mut self,
5176 panel_id: u64,
5177 widget_key: &str,
5178 item_key: &str,
5179 ) {
5180 if widget_key.is_empty() || item_key.is_empty() {
5181 return;
5182 }
5183 let now_expanded = {
5184 let panel = match self.widget_registry.get_mut(panel_id) {
5185 Some(p) => p,
5186 None => return,
5187 };
5188 let (cur_scroll, cur_sel, mut expanded) = match panel.instance_states.get(widget_key) {
5189 Some(crate::widgets::WidgetInstanceState::Tree {
5190 scroll_offset,
5191 selected_index,
5192 expanded_keys,
5193 }) => (*scroll_offset, *selected_index, expanded_keys.clone()),
5194 _ => (0u32, -1i32, std::collections::HashSet::<String>::new()),
5195 };
5196 let next = if expanded.contains(item_key) {
5197 expanded.remove(item_key);
5198 false
5199 } else {
5200 expanded.insert(item_key.to_string());
5201 true
5202 };
5203 panel.instance_states.insert(
5204 widget_key.to_string(),
5205 crate::widgets::WidgetInstanceState::Tree {
5206 scroll_offset: cur_scroll,
5207 selected_index: cur_sel,
5208 expanded_keys: expanded,
5209 },
5210 );
5211 next
5212 };
5213 self.rerender_widget_panel(panel_id);
5214 if self
5215 .plugin_manager
5216 .read()
5217 .unwrap()
5218 .has_hook_handlers("widget_event")
5219 {
5220 self.plugin_manager.read().unwrap().run_hook(
5221 "widget_event",
5222 fresh_core::hooks::HookArgs::WidgetEvent {
5223 panel_id,
5224 widget_key: widget_key.to_string(),
5225 event_type: "expand".into(),
5226 payload: serde_json::json!({
5227 "key": item_key,
5228 "expanded": now_expanded,
5229 }),
5230 },
5231 );
5232 }
5233 }
5234
5235 fn fire_tree_toggle_if_checkable(&mut self, panel_id: u64, focus_key: &str) -> bool {
5249 let panel = match self.widget_registry.get(panel_id) {
5250 Some(p) => p,
5251 None => return false,
5252 };
5253 let widget = crate::widgets::find_widget_by_key(&panel.spec, focus_key);
5254 let (spec_sel, nodes, item_keys, checkable) = match widget {
5255 Some(fresh_core::api::WidgetSpec::Tree {
5256 selected_index,
5257 nodes,
5258 item_keys,
5259 checkable,
5260 ..
5261 }) => (*selected_index, nodes, item_keys.clone(), *checkable),
5262 _ => return false,
5263 };
5264 if !checkable {
5265 return false;
5266 }
5267 let sel = match panel.instance_states.get(focus_key) {
5268 Some(crate::widgets::WidgetInstanceState::Tree { selected_index, .. }) => {
5269 *selected_index
5270 }
5271 _ => spec_sel,
5272 };
5273 if sel < 0 {
5274 return false;
5275 }
5276 let cur_checked = match nodes.get(sel as usize).and_then(|n| n.checked) {
5277 Some(b) => b,
5278 None => return false, };
5280 let new_checked = !cur_checked;
5281 let item_key = item_keys.get(sel as usize).cloned().unwrap_or_default();
5282 if self
5283 .plugin_manager
5284 .read()
5285 .unwrap()
5286 .has_hook_handlers("widget_event")
5287 {
5288 self.plugin_manager.read().unwrap().run_hook(
5289 "widget_event",
5290 fresh_core::hooks::HookArgs::WidgetEvent {
5291 panel_id,
5292 widget_key: focus_key.to_string(),
5293 event_type: "toggle".into(),
5294 payload: serde_json::json!({
5295 "index": sel,
5296 "key": item_key,
5297 "checked": new_checked,
5298 }),
5299 },
5300 );
5301 }
5302 true
5303 }
5304
5305 fn fire_tree_activate(&mut self, panel_id: u64, focus_key: &str) {
5306 let panel = match self.widget_registry.get(panel_id) {
5307 Some(p) => p,
5308 None => return,
5309 };
5310 let widget = crate::widgets::find_widget_by_key(&panel.spec, focus_key);
5311 let (spec_sel, item_keys) = match widget {
5312 Some(fresh_core::api::WidgetSpec::Tree {
5313 selected_index,
5314 item_keys,
5315 ..
5316 }) => (*selected_index, item_keys.clone()),
5317 _ => return,
5318 };
5319 let sel = match panel.instance_states.get(focus_key) {
5320 Some(crate::widgets::WidgetInstanceState::Tree { selected_index, .. }) => {
5321 *selected_index
5322 }
5323 _ => spec_sel,
5324 };
5325 if sel < 0 {
5326 return;
5327 }
5328 let item_key = item_keys.get(sel as usize).cloned().unwrap_or_default();
5329 if self
5330 .plugin_manager
5331 .read()
5332 .unwrap()
5333 .has_hook_handlers("widget_event")
5334 {
5335 self.plugin_manager.read().unwrap().run_hook(
5336 "widget_event",
5337 fresh_core::hooks::HookArgs::WidgetEvent {
5338 panel_id,
5339 widget_key: focus_key.to_string(),
5340 event_type: "activate".into(),
5341 payload: serde_json::json!({
5342 "index": sel,
5343 "key": item_key,
5344 }),
5345 },
5346 );
5347 }
5348 }
5349
5350 pub(super) fn focused_text_widget_panel_for_buffer(
5363 &self,
5364 buffer_id: crate::model::event::BufferId,
5365 ) -> Option<u64> {
5366 for panel_id in self.widget_registry.panels_for_buffer(buffer_id) {
5367 let panel = self.widget_registry.get(panel_id)?;
5368 if panel.focus_key.is_empty() {
5369 continue;
5370 }
5371 let widget = crate::widgets::find_widget_by_key(&panel.spec, &panel.focus_key);
5372 if matches!(widget, Some(fresh_core::api::WidgetSpec::Text { .. })) {
5373 return Some(panel_id);
5374 }
5375 }
5376 None
5377 }
5378
5379 pub(super) fn focused_widget_selected_text(&self, panel_id: u64) -> Option<String> {
5384 let panel = self.widget_registry.get(panel_id)?;
5385 if panel.focus_key.is_empty() {
5386 return None;
5387 }
5388 match panel.instance_states.get(&panel.focus_key) {
5389 Some(crate::widgets::WidgetInstanceState::Text { editor, .. }) => {
5390 editor.selected_text()
5391 }
5392 _ => None,
5393 }
5394 }
5395
5396 pub(super) fn handle_widget_select_all(&mut self, panel_id: u64) -> bool {
5401 self.with_focused_text_editor(panel_id, |editor| editor.select_all())
5405 }
5406
5407 pub(super) fn handle_widget_copy(&mut self, panel_id: u64) -> bool {
5412 if self.widget_registry.get(panel_id).is_none() {
5413 return false;
5414 }
5415 if let Some(text) = self.focused_widget_selected_text(panel_id) {
5416 self.clipboard.copy(text);
5417 }
5418 true
5419 }
5420
5421 pub(super) fn handle_widget_cut(&mut self, panel_id: u64) -> bool {
5424 if self.widget_registry.get(panel_id).is_none() {
5425 return false;
5426 }
5427 if let Some(text) = self.focused_widget_selected_text(panel_id) {
5428 self.clipboard.copy(text);
5429 self.with_focused_text_editor(panel_id, |editor| {
5430 editor.delete_selection();
5431 });
5432 }
5433 true
5434 }
5435
5436 pub(super) fn handle_widget_insert_str(&mut self, panel_id: u64, text: &str) -> bool {
5442 if self.widget_registry.get(panel_id).is_none() {
5443 return false;
5444 }
5445 let owned = text.to_string();
5446 self.with_focused_text_editor(panel_id, move |editor| {
5447 editor.insert_str(&owned);
5448 });
5449 true
5450 }
5451
5452 fn ensure_focused_text_seeded(&mut self, panel_id: u64, focus_key: &str) -> bool {
5459 let panel = match self.widget_registry.get_mut(panel_id) {
5460 Some(p) => p,
5461 None => return false,
5462 };
5463 if matches!(
5464 panel.instance_states.get(focus_key),
5465 Some(crate::widgets::WidgetInstanceState::Text { .. })
5466 ) {
5467 return true;
5468 }
5469 let widget = crate::widgets::find_widget_by_key(&panel.spec, focus_key);
5470 let (value, cursor_byte, multiline) = match widget {
5471 Some(fresh_core::api::WidgetSpec::Text {
5472 value,
5473 cursor_byte,
5474 rows,
5475 ..
5476 }) => (value.clone(), *cursor_byte, *rows > 1),
5477 _ => return false,
5478 };
5479 let mut editor = if multiline {
5480 crate::primitives::text_edit::TextEdit::with_text(&value)
5481 } else {
5482 crate::primitives::text_edit::TextEdit::single_line_with_text(&value)
5483 };
5484 let seed = if cursor_byte < 0 {
5485 value.len()
5486 } else {
5487 (cursor_byte as usize).min(value.len())
5488 };
5489 editor.set_cursor_from_flat(seed);
5490 panel.instance_states.insert(
5491 focus_key.to_string(),
5492 crate::widgets::WidgetInstanceState::Text {
5493 editor,
5494 scroll: 0,
5495 completions: Vec::new(),
5496 completion_selected_index: 0,
5497 completion_scroll_offset: 0,
5498 },
5499 );
5500 true
5501 }
5502
5503 pub(super) fn with_focused_text_editor<F>(&mut self, panel_id: u64, op: F) -> bool
5510 where
5511 F: FnOnce(&mut crate::primitives::text_edit::TextEdit),
5512 {
5513 let focus_key = match self.widget_registry.get(panel_id) {
5514 Some(p) if !p.focus_key.is_empty() => p.focus_key.clone(),
5515 _ => return false,
5516 };
5517 if !self.ensure_focused_text_seeded(panel_id, &focus_key) {
5518 return false;
5519 }
5520 let (before_value, before_cursor) = {
5521 let panel = self.widget_registry.get(panel_id).unwrap();
5522 match panel.instance_states.get(&focus_key) {
5523 Some(crate::widgets::WidgetInstanceState::Text { editor, .. }) => {
5524 (editor.value(), editor.flat_cursor_byte())
5525 }
5526 _ => return false,
5527 }
5528 };
5529 {
5530 let panel = self.widget_registry.get_mut(panel_id).unwrap();
5531 match panel.instance_states.get_mut(&focus_key) {
5532 Some(crate::widgets::WidgetInstanceState::Text { editor, .. }) => op(editor),
5533 _ => return false,
5534 }
5535 }
5536 let (after_value, after_cursor) = {
5537 let panel = self.widget_registry.get(panel_id).unwrap();
5538 match panel.instance_states.get(&focus_key) {
5539 Some(crate::widgets::WidgetInstanceState::Text { editor, .. }) => {
5540 (editor.value(), editor.flat_cursor_byte())
5541 }
5542 _ => return false,
5543 }
5544 };
5545 if after_value == before_value && after_cursor == before_cursor {
5546 return false;
5547 }
5548 self.rerender_widget_panel(panel_id);
5549 if self
5550 .plugin_manager
5551 .read()
5552 .unwrap()
5553 .has_hook_handlers("widget_event")
5554 {
5555 self.plugin_manager.read().unwrap().run_hook(
5556 "widget_event",
5557 fresh_core::hooks::HookArgs::WidgetEvent {
5558 panel_id,
5559 widget_key: focus_key.clone(),
5560 event_type: "change".into(),
5561 payload: serde_json::json!({
5562 "value": after_value,
5563 "cursorByte": after_cursor as i64,
5564 }),
5565 },
5566 );
5567 }
5568 true
5569 }
5570
5571 fn handle_widget_text_key(&mut self, panel_id: u64, key: &str) {
5577 self.with_focused_text_editor(panel_id, |editor| match key {
5578 "Backspace" => editor.backspace(),
5579 "Delete" => editor.delete(),
5580 "Left" => editor.move_left(),
5581 "Right" => editor.move_right(),
5582 "Up" => editor.move_up(),
5583 "Down" => editor.move_down(),
5584 "Home" => editor.move_home(),
5585 "End" => editor.move_end(),
5586 "Enter" => editor.insert_char('\n'),
5587 _ => { }
5588 });
5589 }
5590
5591 fn handle_widget_text_char(&mut self, panel_id: u64, text: &str) {
5598 if text.is_empty() {
5599 return;
5600 }
5601 let text = text.to_string();
5602 self.with_focused_text_editor(panel_id, move |editor| {
5603 editor.insert_str(&text);
5604 });
5605 }
5606
5607 fn handle_unmount_widget_panel(&mut self, panel_id: u64) {
5608 match self.widget_registry.unmount(panel_id) {
5609 Some(buffer_id) => {
5610 tracing::debug!(
5611 "Unmounted widget panel {} (was rendering into {:?})",
5612 panel_id,
5613 buffer_id
5614 );
5615 }
5620 None => {
5621 tracing::debug!("UnmountWidgetPanel for unknown panel {} ignored", panel_id);
5622 }
5623 }
5624 }
5625
5626 fn handle_mount_floating_widget(
5627 &mut self,
5628 panel_id: u64,
5629 spec: fresh_core::api::WidgetSpec,
5630 width_pct: u8,
5631 height_pct: u8,
5632 ) {
5633 let width_pct = width_pct.clamp(1, 100);
5634 let height_pct = height_pct.clamp(1, 100);
5635 if let Some(existing) = self.floating_widget_panel.take() {
5636 if existing.panel_id != panel_id {
5637 let _ = self.widget_registry.unmount(existing.panel_id);
5638 }
5639 }
5640 self.floating_widget_panel = Some(FloatingWidgetState {
5641 panel_id,
5642 width_pct,
5643 height_pct,
5644 entries: Vec::new(),
5645 focus_cursor: None,
5646 embeds: Vec::new(),
5647 overlays: Vec::new(),
5648 scroll_regions: Vec::new(),
5649 scrollbar_tracks: Vec::new(),
5650 scrollbar_mouse: Default::default(),
5651 scrollbar_drag_key: None,
5652 last_inner_rect: None,
5653 });
5654 let prev = std::collections::HashMap::new();
5655 let prev_focus = String::new();
5656 let panel_width = self.floating_panel_inner_width();
5657 let out = crate::widgets::render_spec(&spec, &prev, &prev_focus, panel_width);
5658 let focus_cursor = out.focus_cursor;
5659 let entries = out.entries;
5660 let embeds = out.embeds;
5661 let overlays = out.overlays;
5662 let scroll_regions = out.scroll_regions;
5663 self.widget_registry.mount(
5664 panel_id,
5665 FLOATING_PANEL_BUFFER_ID,
5666 spec,
5667 out.hits,
5668 out.instance_states,
5669 out.focus_key,
5670 out.tabbable,
5671 );
5672 if let Some(fwp) = self.floating_widget_panel.as_mut() {
5673 fwp.entries = entries;
5674 fwp.focus_cursor = focus_cursor;
5675 fwp.embeds = embeds;
5676 fwp.overlays = overlays;
5677 fwp.scroll_regions = scroll_regions;
5678 }
5679 tracing::debug!(
5680 "Mounted floating widget panel {} ({}%x{}%)",
5681 panel_id,
5682 width_pct,
5683 height_pct
5684 );
5685 }
5686
5687 fn handle_update_floating_widget(&mut self, panel_id: u64, spec: fresh_core::api::WidgetSpec) {
5688 match self.floating_widget_panel.as_ref() {
5689 Some(fwp) if fwp.panel_id == panel_id => {}
5690 _ => {
5691 tracing::debug!(
5692 "UpdateFloatingWidget for unknown / mismatched panel {} ignored",
5693 panel_id
5694 );
5695 return;
5696 }
5697 }
5698 let prev = self
5699 .widget_registry
5700 .instance_states(panel_id)
5701 .cloned()
5702 .unwrap_or_default();
5703 let prev_focus = self
5704 .widget_registry
5705 .focus_key(panel_id)
5706 .map(|s| s.to_string())
5707 .unwrap_or_default();
5708 let panel_width = self.floating_panel_inner_width();
5709 let out = crate::widgets::render_spec(&spec, &prev, &prev_focus, panel_width);
5710 let focus_cursor = out.focus_cursor;
5711 let entries = out.entries;
5712 let embeds = out.embeds;
5713 let overlays = out.overlays;
5714 let scroll_regions = out.scroll_regions;
5715 if self
5716 .widget_registry
5717 .update(
5718 panel_id,
5719 spec,
5720 out.hits,
5721 out.instance_states,
5722 out.focus_key,
5723 out.tabbable,
5724 )
5725 .is_err()
5726 {
5727 tracing::debug!(
5728 "UpdateFloatingWidget for unknown panel {} ignored (not in registry)",
5729 panel_id
5730 );
5731 return;
5732 }
5733 if let Some(fwp) = self.floating_widget_panel.as_mut() {
5734 fwp.entries = entries;
5735 fwp.focus_cursor = focus_cursor;
5736 fwp.embeds = embeds;
5737 fwp.overlays = overlays;
5738 fwp.scroll_regions = scroll_regions;
5739 }
5740 }
5741
5742 fn handle_unmount_floating_widget(&mut self, panel_id: u64) {
5743 match self.floating_widget_panel.as_ref() {
5744 Some(fwp) if fwp.panel_id == panel_id => {}
5745 _ => {
5746 tracing::debug!(
5747 "UnmountFloatingWidget for unknown / mismatched panel {} ignored",
5748 panel_id
5749 );
5750 return;
5751 }
5752 }
5753 self.floating_widget_panel = None;
5754 let _ = self.widget_registry.unmount(panel_id);
5755 self.active_window_mut().resize_visible_terminals();
5768 tracing::debug!("Unmounted floating widget panel {}", panel_id);
5769 }
5770
5771 pub(super) fn floating_panel_inner_width(&self) -> u32 {
5777 let term_w = self.terminal_width.max(1) as u32;
5778 let pct = self
5779 .floating_widget_panel
5780 .as_ref()
5781 .map(|f| f.width_pct.clamp(1, 100) as u32)
5782 .unwrap_or(80);
5783 let w = (term_w * pct) / 100;
5784 w.saturating_sub(2).max(10)
5785 }
5786
5787 fn handle_get_text_properties_at_cursor(&self, buffer_id: BufferId) {
5788 if let Some(state) = self
5789 .windows
5790 .get(&self.active_window)
5791 .map(|w| &w.buffers)
5792 .expect("active window present")
5793 .get(&buffer_id)
5794 {
5795 let cursor_pos = self
5796 .windows
5797 .get(&self.active_window)
5798 .and_then(|w| w.buffers.splits())
5799 .map(|(_, vs)| vs)
5800 .expect("active window must have a populated split layout")
5801 .values()
5802 .find_map(|vs| vs.buffer_state(buffer_id))
5803 .map(|bs| bs.cursors.primary().position)
5804 .unwrap_or(0);
5805 let properties = state.text_properties.get_at(cursor_pos);
5806 tracing::debug!(
5807 "Text properties at cursor in {:?}: {} properties found",
5808 buffer_id,
5809 properties.len()
5810 );
5811 }
5813 }
5814
5815 fn handle_set_context(&mut self, name: String, active: bool) {
5816 if active {
5817 self.active_window_mut()
5818 .active_custom_contexts
5819 .insert(name.clone());
5820 tracing::debug!("Set custom context: {}", name);
5821 } else {
5822 self.active_window_mut()
5823 .active_custom_contexts
5824 .remove(&name);
5825 tracing::debug!("Unset custom context: {}", name);
5826 }
5827 }
5828
5829 fn handle_disable_lsp_for_language(&mut self, language: String) {
5830 tracing::info!("Disabling LSP for language: {}", language);
5831 let __active_id = self.active_window;
5832 if let Some(lsp) = self
5833 .windows
5834 .get_mut(&__active_id)
5835 .and_then(|w| w.lsp.as_mut())
5836 {
5837 lsp.shutdown_server(&language);
5838 tracing::info!("Stopped LSP server for {}", language);
5839 }
5840 if let Some(lsp_configs) = self.config_mut().lsp.get_mut(&language) {
5841 for c in lsp_configs.as_mut_slice() {
5842 c.enabled = false;
5843 c.auto_start = false;
5844 }
5845 tracing::info!("Disabled LSP config for {}", language);
5846 }
5847 if let Err(e) = self.save_config() {
5848 tracing::error!("Failed to save config: {}", e);
5849 self.active_window_mut().status_message = Some(format!(
5850 "LSP disabled for {} (config save failed)",
5851 language
5852 ));
5853 } else {
5854 self.active_window_mut().status_message =
5855 Some(format!("LSP disabled for {}", language));
5856 }
5857 self.active_window_mut().warning_domains.lsp.clear();
5858 }
5859
5860 fn handle_restart_lsp_for_language(&mut self, language: String) {
5861 tracing::info!("Plugin restarting LSP for language: {}", language);
5862 let file_path = self
5863 .active_window()
5864 .buffer_metadata
5865 .get(&self.active_buffer())
5866 .and_then(|meta| meta.file_path().cloned());
5867 let __active_id = self.active_window;
5868 let success = if let Some(lsp) = self
5869 .windows
5870 .get_mut(&__active_id)
5871 .and_then(|w| w.lsp.as_mut())
5872 {
5873 let (ok, msg) = lsp.manual_restart(&language, file_path.as_deref());
5874 self.active_window_mut().status_message = Some(msg);
5875 ok
5876 } else {
5877 self.active_window_mut().status_message = Some("No LSP manager available".to_string());
5878 false
5879 };
5880 if success {
5881 self.reopen_buffers_for_language(&language);
5882 }
5883 }
5884
5885 fn handle_set_lsp_root_uri(&mut self, language: String, uri: String) {
5886 tracing::info!("Plugin setting LSP root URI for {}: {}", language, uri);
5887 match uri.parse::<lsp_types::Uri>() {
5888 Ok(parsed_uri) => {
5889 let __active_id = self.active_window;
5890 if let Some(lsp) = self
5891 .windows
5892 .get_mut(&__active_id)
5893 .and_then(|w| w.lsp.as_mut())
5894 {
5895 let restarted = lsp.set_language_root_uri(&language, parsed_uri);
5896 if restarted {
5897 self.active_window_mut().status_message = Some(format!(
5898 "LSP root updated for {} (restarting server)",
5899 language
5900 ));
5901 } else {
5902 self.active_window_mut().status_message =
5903 Some(format!("LSP root set for {}", language));
5904 }
5905 }
5906 }
5907 Err(e) => {
5908 tracing::error!("Invalid LSP root URI '{}': {}", uri, e);
5909 self.active_window_mut().status_message =
5910 Some(format!("Invalid LSP root URI: {}", e));
5911 }
5912 }
5913 }
5914
5915 fn handle_create_scroll_sync_group(
5916 &mut self,
5917 group_id: crate::view::scroll_sync::ScrollSyncGroupId,
5918 left_split: SplitId,
5919 right_split: SplitId,
5920 ) {
5921 let success = self
5922 .active_window_mut()
5923 .scroll_sync_manager
5924 .create_group_with_id(group_id, left_split, right_split);
5925 if success {
5926 tracing::debug!(
5927 "Created scroll sync group {} for splits {:?} and {:?}",
5928 group_id,
5929 left_split,
5930 right_split
5931 );
5932 } else {
5933 tracing::warn!(
5934 "Failed to create scroll sync group {} (ID already exists)",
5935 group_id
5936 );
5937 }
5938 }
5939
5940 fn handle_set_scroll_sync_anchors(
5941 &mut self,
5942 group_id: crate::view::scroll_sync::ScrollSyncGroupId,
5943 anchors: Vec<(usize, usize)>,
5944 ) {
5945 use crate::view::scroll_sync::SyncAnchor;
5946 let anchor_count = anchors.len();
5947 let sync_anchors: Vec<SyncAnchor> = anchors
5948 .into_iter()
5949 .map(|(left_line, right_line)| SyncAnchor {
5950 left_line,
5951 right_line,
5952 })
5953 .collect();
5954 self.active_window_mut()
5955 .scroll_sync_manager
5956 .set_anchors(group_id, sync_anchors);
5957 tracing::debug!(
5958 "Set {} anchors for scroll sync group {}",
5959 anchor_count,
5960 group_id
5961 );
5962 }
5963
5964 fn handle_remove_scroll_sync_group(
5965 &mut self,
5966 group_id: crate::view::scroll_sync::ScrollSyncGroupId,
5967 ) {
5968 if self
5969 .active_window_mut()
5970 .scroll_sync_manager
5971 .remove_group(group_id)
5972 {
5973 tracing::debug!("Removed scroll sync group {}", group_id);
5974 } else {
5975 tracing::warn!("Scroll sync group {} not found", group_id);
5976 }
5977 }
5978
5979 fn handle_create_buffer_group(
5980 &mut self,
5981 name: String,
5982 mode: String,
5983 layout_json: String,
5984 request_id: Option<u64>,
5985 ) {
5986 match self.create_buffer_group(name, mode, layout_json) {
5987 Ok(result) => {
5988 if let Some(req_id) = request_id {
5989 let json = serde_json::to_string(&result).unwrap_or_default();
5990 self.plugin_manager
5991 .read()
5992 .unwrap()
5993 .resolve_callback(fresh_core::api::JsCallbackId::from(req_id), json);
5994 }
5995 }
5996 Err(e) => {
5997 tracing::error!("Failed to create buffer group: {}", e);
5998 }
5999 }
6000 }
6001
6002 fn handle_send_terminal_input(
6003 &mut self,
6004 terminal_id: crate::services::terminal::TerminalId,
6005 data: String,
6006 ) {
6007 if let Some(handle) = self.active_window().terminal_manager.get(terminal_id) {
6008 handle.write(data.as_bytes());
6009 tracing::trace!(
6010 "Plugin sent {} bytes to terminal {:?}",
6011 data.len(),
6012 terminal_id
6013 );
6014 } else {
6015 tracing::warn!(
6016 "Plugin tried to send input to non-existent terminal {:?}",
6017 terminal_id
6018 );
6019 }
6020 }
6021
6022 fn handle_close_terminal(&mut self, terminal_id: crate::services::terminal::TerminalId) {
6023 let buffer_to_close = self
6024 .active_window()
6025 .terminal_buffers
6026 .iter()
6027 .find(|(_, &tid)| tid == terminal_id)
6028 .map(|(&bid, _)| bid);
6029 if let Some(buffer_id) = buffer_to_close {
6030 if let Err(e) = self.close_buffer(buffer_id) {
6031 tracing::warn!("Failed to close terminal buffer: {}", e);
6032 }
6033 tracing::info!("Plugin closed terminal {:?}", terminal_id);
6034 } else {
6035 self.active_window_mut().terminal_manager.close(terminal_id);
6036 tracing::info!("Plugin closed terminal {:?} (no buffer found)", terminal_id);
6037 }
6038 }
6039
6040 fn handle_signal_window(&mut self, id: fresh_core::WindowId, signal: &str) {
6048 let Some(window) = self.windows.get_mut(&id) else {
6049 tracing::warn!("Plugin SignalWindow targeted unknown window {:?}", id);
6050 return;
6051 };
6052 let results = window.process_groups.signal_all(signal);
6053 for (entry, result) in results {
6054 match result {
6055 Ok(true) => tracing::info!(
6056 "SignalWindow {:?}: {} → pid {} ({})",
6057 id,
6058 signal,
6059 entry.leader_pid,
6060 entry.label
6061 ),
6062 Ok(false) => tracing::debug!(
6063 "SignalWindow {:?}: pid {} ({}) already exited",
6064 id,
6065 entry.leader_pid,
6066 entry.label
6067 ),
6068 Err(e) => tracing::warn!(
6069 "SignalWindow {:?}: pid {} ({}): {}",
6070 id,
6071 entry.leader_pid,
6072 entry.label,
6073 e
6074 ),
6075 }
6076 }
6077 }
6078}
6079
6080fn clamp_buffer_text_range(start: usize, end: usize, len: usize) -> (usize, usize) {
6091 let end = end.min(len);
6092 let start = start.min(end);
6093 (start, end)
6094}
6095
6096#[cfg(test)]
6097mod tests {
6098 use tokio::io::{AsyncReadExt, BufReader};
6111 use tokio::process::Command as TokioCommand;
6112 use tokio::time::{timeout, Duration};
6113
6114 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
6125 async fn kill_via_oneshot_terminates_long_running_child() {
6126 let mut cmd = TokioCommand::new("sleep");
6127 cmd.args(["30"]);
6128 cmd.stdout(std::process::Stdio::piped());
6129 cmd.stderr(std::process::Stdio::piped());
6130
6131 let mut child = cmd.spawn().expect("spawn sh -c sleep 30");
6132 let pid = child.id().expect("child has a pid");
6133
6134 let (kill_tx, mut kill_rx) = tokio::sync::oneshot::channel::<()>();
6135 let stdout_pipe = child.stdout.take();
6136 let stderr_pipe = child.stderr.take();
6137
6138 let stdout_fut = async {
6139 let mut buf = String::new();
6140 if let Some(s) = stdout_pipe {
6141 #[allow(clippy::let_underscore_must_use)]
6142 let _ = BufReader::new(s).read_to_string(&mut buf).await;
6143 }
6144 buf
6145 };
6146 let stderr_fut = async {
6147 let mut buf = String::new();
6148 if let Some(s) = stderr_pipe {
6149 #[allow(clippy::let_underscore_must_use)]
6150 let _ = BufReader::new(s).read_to_string(&mut buf).await;
6151 }
6152 buf
6153 };
6154 let wait_fut = async {
6155 tokio::select! {
6156 status = child.wait() => {
6157 status.map(|s| s.code().unwrap_or(-1)).unwrap_or(-1)
6158 }
6159 _ = &mut kill_rx => {
6160 #[allow(clippy::let_underscore_must_use)]
6161 let _ = child.start_kill();
6162 child
6163 .wait()
6164 .await
6165 .map(|s| s.code().unwrap_or(-1))
6166 .unwrap_or(-1)
6167 }
6168 }
6169 };
6170
6171 tokio::time::sleep(Duration::from_millis(50)).await;
6176 kill_tx.send(()).expect("kill channel send");
6177
6178 let result = timeout(Duration::from_secs(5), async {
6179 tokio::join!(stdout_fut, stderr_fut, wait_fut)
6180 })
6181 .await;
6182
6183 let (_stdout, _stderr, exit_code) = result.expect(
6184 "kill path must resolve within 5s — if this times out the \
6185 select! arm order or kill-then-wait logic is broken",
6186 );
6187 assert_ne!(
6199 exit_code, 0,
6200 "killed child must exit non-success (got 0 — did the \
6201 kill arm fire too late, or did sleep somehow complete?)"
6202 );
6203
6204 #[cfg(unix)]
6213 {
6214 let still_alive = std::process::Command::new("kill")
6215 .args(["-0", &pid.to_string()])
6216 .status()
6217 .map(|s| s.success())
6218 .unwrap_or(false);
6219 assert!(
6220 !still_alive,
6221 "process {pid} must be reaped after wait() — a still-\
6222 alive check means the kill path leaked the child"
6223 );
6224 }
6225 #[cfg(not(unix))]
6226 {
6227 let _ = pid;
6230 }
6231 }
6232
6233 use super::clamp_buffer_text_range;
6234
6235 #[test]
6236 fn clamp_text_range_passes_through_in_bounds() {
6237 assert_eq!(clamp_buffer_text_range(0, 165, 165), (0, 165));
6238 assert_eq!(clamp_buffer_text_range(10, 50, 165), (10, 50));
6239 }
6240
6241 #[test]
6247 fn clamp_text_range_clamps_stale_end_past_buffer() {
6248 assert_eq!(clamp_buffer_text_range(0, 165_003, 165_002), (0, 165_002));
6249 }
6250
6251 #[test]
6252 fn clamp_text_range_pins_overlarge_start_to_empty() {
6253 assert_eq!(clamp_buffer_text_range(200, 250, 165), (165, 165));
6255 }
6256}
6257
6258impl Window {
6259 #[cfg(feature = "plugins")]
6274 pub(crate) fn populate_plugin_state_snapshot(
6275 &mut self,
6276 snapshot: &mut fresh_core::api::EditorStateSnapshot,
6277 ) {
6278 use fresh_core::api::{BufferInfo, CursorInfo, ViewportInfo};
6279
6280 let current_gen = self.resources.grammar_registry.catalog_gen();
6286 if snapshot.last_grammar_gen != current_gen {
6287 snapshot.available_grammars = self
6288 .resources
6289 .grammar_registry
6290 .available_grammar_info()
6291 .into_iter()
6292 .map(|g| fresh_core::api::GrammarInfoSnapshot {
6293 name: g.name,
6294 source: g.source.to_string(),
6295 file_extensions: g.file_extensions,
6296 short_name: g.short_name,
6297 })
6298 .collect();
6299 snapshot.last_grammar_gen = current_gen;
6300 }
6301
6302 snapshot.active_buffer_id = self.active_buffer();
6303
6304 let (mgr_ref, vs_ref) = self
6305 .buffers
6306 .splits()
6307 .expect("active window must have a populated split layout");
6308 let active_split = mgr_ref.active_split();
6309 snapshot.active_split_id = active_split.0 .0;
6310
6311 snapshot.buffers.clear();
6313 snapshot.buffer_saved_diffs.clear();
6314 snapshot.buffer_cursor_positions.clear();
6315 snapshot.buffer_text_properties.clear();
6316
6317 let active_vs_opt = vs_ref.get(&active_split);
6318 for (buffer_id, state) in &self.buffers {
6319 let is_virtual = self
6320 .buffer_metadata
6321 .get(buffer_id)
6322 .map(|m| m.is_virtual())
6323 .unwrap_or(false);
6324 let view_mode = active_vs_opt
6329 .and_then(|vs| vs.buffer_state(*buffer_id))
6330 .map(|bs| match bs.view_mode {
6331 crate::state::ViewMode::Source => "source",
6332 crate::state::ViewMode::PageView => "compose",
6333 })
6334 .unwrap_or("source");
6335 let compose_width = active_vs_opt
6336 .and_then(|vs| vs.buffer_state(*buffer_id))
6337 .and_then(|bs| bs.compose_width);
6338 let is_composing_in_any_split = vs_ref.values().any(|vs| {
6339 vs.buffer_state(*buffer_id)
6340 .map(|bs| matches!(bs.view_mode, crate::state::ViewMode::PageView))
6341 .unwrap_or(false)
6342 });
6343 let is_preview = self
6344 .buffer_metadata
6345 .get(buffer_id)
6346 .map(|m| m.is_preview)
6347 .unwrap_or(false);
6348 let splits: Vec<fresh_core::SplitId> = mgr_ref
6354 .splits_for_buffer(*buffer_id)
6355 .into_iter()
6356 .map(|leaf_id| leaf_id.0)
6357 .collect();
6358 let buffer_info = BufferInfo {
6359 id: *buffer_id,
6360 path: state.buffer.file_path().map(|p| p.to_path_buf()),
6361 modified: state.buffer.is_modified(),
6362 length: state.buffer.len(),
6363 is_virtual,
6364 view_mode: view_mode.to_string(),
6365 is_composing_in_any_split,
6366 compose_width,
6367 language: state.language.clone(),
6368 is_preview,
6369 splits,
6370 };
6371 snapshot.buffers.insert(*buffer_id, buffer_info);
6372
6373 let diff = {
6374 let diff = state.buffer.diff_since_saved();
6375 BufferSavedDiff {
6376 equal: diff.equal,
6377 byte_ranges: diff.byte_ranges.clone(),
6378 }
6379 };
6380 snapshot.buffer_saved_diffs.insert(*buffer_id, diff);
6381
6382 let is_hidden = self
6391 .buffer_metadata
6392 .get(buffer_id)
6393 .is_some_and(|m| m.hidden_from_tabs);
6394 let source_split = vs_ref.iter().find(|(split_id, vs)| {
6395 vs.keyed_states.contains_key(buffer_id)
6396 && !(is_hidden && self.grouped_subtrees.contains_key(split_id))
6397 });
6398 let cursor_pos = source_split
6399 .and_then(|(_, vs)| vs.buffer_state(*buffer_id))
6400 .map(|bs| bs.cursors.primary().position)
6401 .unwrap_or(0);
6402 tracing::trace!(
6403 "snapshot: buffer {:?} cursor_pos={} (from split {:?})",
6404 buffer_id,
6405 cursor_pos,
6406 source_split.map(|(id, _)| *id),
6407 );
6408 snapshot
6409 .buffer_cursor_positions
6410 .insert(*buffer_id, cursor_pos);
6411
6412 if !state.text_properties.is_empty() {
6414 snapshot
6415 .buffer_text_properties
6416 .insert(*buffer_id, state.text_properties.all().to_vec());
6417 }
6418 }
6419
6420 let active_buf_id = snapshot.active_buffer_id;
6431 let active_split_id = self.effective_active_pair().0;
6432 self.buffers
6433 .with_all_mut(|buffers_mut, mgr, vs_map| {
6434 let _ = mgr; if let Some(active_vs) = vs_map.get(&active_split_id) {
6436 let active_cursors = &active_vs.cursors;
6438 let primary = active_cursors.primary();
6439 let primary_position = primary.position;
6440 let primary_selection = primary.selection_range();
6441
6442 let line_of = |offset: usize| -> Option<usize> {
6448 buffers_mut.get(&active_buf_id).and_then(|state| {
6449 if state.buffer.line_count().is_some() {
6450 Some(state.buffer.get_line_number(offset))
6451 } else {
6452 None
6453 }
6454 })
6455 };
6456
6457 snapshot.primary_cursor = Some(CursorInfo {
6458 position: primary_position,
6459 selection: primary_selection.clone(),
6460 line: line_of(primary_position),
6461 });
6462
6463 snapshot.all_cursors = active_cursors
6464 .iter()
6465 .map(|(_, cursor)| CursorInfo {
6466 position: cursor.position,
6467 selection: cursor.selection_range(),
6468 line: line_of(cursor.position),
6469 })
6470 .collect();
6471
6472 if let Some(range) = primary_selection {
6474 if let Some(active_state) = buffers_mut.get_mut(&active_buf_id) {
6475 snapshot.selected_text =
6476 Some(active_state.get_text_range(range.start, range.end));
6477 }
6478 }
6479
6480 let top_line = buffers_mut.get(&active_buf_id).and_then(|state| {
6482 if state.buffer.line_count().is_some() {
6483 Some(state.buffer.get_line_number(active_vs.viewport.top_byte))
6484 } else {
6485 None
6486 }
6487 });
6488 snapshot.viewport = Some(ViewportInfo {
6489 top_byte: active_vs.viewport.top_byte,
6490 top_line,
6491 left_column: active_vs.viewport.left_column,
6492 width: active_vs.viewport.width,
6493 height: active_vs.viewport.height,
6494 });
6495 } else {
6496 snapshot.primary_cursor = None;
6497 snapshot.all_cursors.clear();
6498 snapshot.viewport = None;
6499 snapshot.selected_text = None;
6500 }
6501
6502 snapshot.splits.clear();
6504 for (leaf_id, vs) in vs_map.iter() {
6505 let buf_id = vs.active_buffer;
6506 let top_line = buffers_mut.get(&buf_id).and_then(|state| {
6507 if state.buffer.line_count().is_some() {
6508 Some(state.buffer.get_line_number(vs.viewport.top_byte))
6509 } else {
6510 None
6511 }
6512 });
6513 snapshot.splits.push(fresh_core::api::SplitSnapshot {
6514 split_id: leaf_id.0 .0,
6515 buffer_id: buf_id,
6516 viewport: ViewportInfo {
6517 top_byte: vs.viewport.top_byte,
6518 top_line,
6519 left_column: vs.viewport.left_column,
6520 width: vs.viewport.width,
6521 height: vs.viewport.height,
6522 },
6523 });
6524 }
6525 })
6526 .expect("active window must have a populated split layout");
6527
6528 snapshot.active_session_plugin_states = self.plugin_state.clone();
6534 snapshot.diagnostics = Arc::clone(&self.stored_diagnostics);
6539 snapshot.folding_ranges = Arc::clone(&self.stored_folding_ranges);
6540
6541 snapshot.editor_mode = self.editor_mode.clone();
6543
6544 let active_split_id_u64 = active_split_id.0 .0;
6549 let split_changed = snapshot.plugin_view_states_split != active_split_id_u64;
6550 if split_changed {
6551 snapshot.plugin_view_states.clear();
6552 snapshot.plugin_view_states_split = active_split_id_u64;
6553 }
6554
6555 {
6557 let open_bids: Vec<_> = snapshot.buffers.keys().copied().collect();
6558 snapshot
6559 .plugin_view_states
6560 .retain(|bid, _| open_bids.contains(bid));
6561 }
6562
6563 if let Some(vs_map) = self.buffers.split_view_states() {
6565 if let Some(active_vs) = vs_map.get(&active_split_id) {
6566 for (buffer_id, buf_state) in &active_vs.keyed_states {
6567 if !buf_state.plugin_state.is_empty() {
6568 let entry = snapshot.plugin_view_states.entry(*buffer_id).or_default();
6569 for (key, value) in &buf_state.plugin_state {
6570 entry.entry(key.clone()).or_insert_with(|| value.clone());
6571 }
6572 }
6573 }
6574 }
6575 }
6576 }
6577}
6578
6579const HTTP_FETCH_MAX_BYTES: u64 = 64 * 1024 * 1024;
6583
6584fn fetch_url_to_file(url: &str, target: &std::path::Path) -> Result<u16, String> {
6590 let tls_config = ureq::tls::TlsConfig::builder()
6594 .root_certs(ureq::tls::RootCerts::PlatformVerifier)
6595 .build();
6596
6597 let agent = ureq::Agent::config_builder()
6598 .timeout_global(Some(std::time::Duration::from_secs(30)))
6599 .http_status_as_error(false)
6600 .tls_config(tls_config)
6601 .build()
6602 .new_agent();
6603
6604 let response = agent
6605 .get(url)
6606 .header("User-Agent", "fresh-editor")
6607 .call()
6608 .map_err(|e| format!("HTTP request failed: {}", e))?;
6609
6610 let status = response.status().as_u16();
6611 if !(200..300).contains(&status) {
6612 return Ok(status);
6613 }
6614
6615 let mut file = std::fs::File::create(target)
6616 .map_err(|e| format!("failed to create {}: {}", target.display(), e))?;
6617
6618 let mut reader = response
6619 .into_body()
6620 .into_with_config()
6621 .limit(HTTP_FETCH_MAX_BYTES)
6622 .reader();
6623
6624 std::io::copy(&mut reader, &mut file)
6625 .map_err(|e| format!("failed to write response body: {}", e))?;
6626
6627 Ok(status)
6628}