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::Editor;
25
26fn buffer_line_byte_offset(
31 content: &str,
32 buffer_len: usize,
33 line: usize,
34 want_end: bool,
35) -> Option<usize> {
36 if !want_end && line == 0 {
37 return Some(0);
38 }
39 let mut current_line = 0usize;
40 for (byte_idx, c) in content.char_indices() {
41 if c == '\n' {
42 if want_end && current_line == line {
43 return Some(byte_idx);
44 }
45 current_line += 1;
46 if !want_end && current_line == line {
47 return Some(byte_idx + 1);
48 }
49 }
50 }
51 if want_end && current_line == line {
52 Some(buffer_len)
53 } else {
54 None
55 }
56}
57
58impl Editor {
59 #[cfg(feature = "plugins")]
61 pub(super) fn update_plugin_state_snapshot(&mut self) {
62 if let Some(snapshot_handle) = self.plugin_manager.state_snapshot_handle() {
64 use fresh_core::api::{BufferInfo, CursorInfo, ViewportInfo};
65 let mut snapshot = snapshot_handle.write().unwrap();
66
67 let current_gen = self.grammar_registry.catalog_gen();
73 if snapshot.last_grammar_gen != current_gen {
74 snapshot.available_grammars = self
75 .grammar_registry
76 .available_grammar_info()
77 .into_iter()
78 .map(|g| fresh_core::api::GrammarInfoSnapshot {
79 name: g.name,
80 source: g.source.to_string(),
81 file_extensions: g.file_extensions,
82 short_name: g.short_name,
83 })
84 .collect();
85 snapshot.last_grammar_gen = current_gen;
86 }
87
88 snapshot.active_buffer_id = self.active_buffer();
90
91 snapshot.active_split_id = self.split_manager.active_split().0 .0;
93
94 snapshot.buffers.clear();
96 snapshot.buffer_saved_diffs.clear();
97 snapshot.buffer_cursor_positions.clear();
98 snapshot.buffer_text_properties.clear();
99
100 for (buffer_id, state) in &self.buffers {
101 let is_virtual = self
102 .buffer_metadata
103 .get(buffer_id)
104 .map(|m| m.is_virtual())
105 .unwrap_or(false);
106 let active_split = self.split_manager.active_split();
111 let active_vs = self.split_view_states.get(&active_split);
112 let view_mode = active_vs
113 .and_then(|vs| vs.buffer_state(*buffer_id))
114 .map(|bs| match bs.view_mode {
115 crate::state::ViewMode::Source => "source",
116 crate::state::ViewMode::PageView => "compose",
117 })
118 .unwrap_or("source");
119 let compose_width = active_vs
120 .and_then(|vs| vs.buffer_state(*buffer_id))
121 .and_then(|bs| bs.compose_width);
122 let is_composing_in_any_split = self.split_view_states.values().any(|vs| {
123 vs.buffer_state(*buffer_id)
124 .map(|bs| matches!(bs.view_mode, crate::state::ViewMode::PageView))
125 .unwrap_or(false)
126 });
127 let is_preview = self
128 .buffer_metadata
129 .get(buffer_id)
130 .map(|m| m.is_preview)
131 .unwrap_or(false);
132 let splits: Vec<fresh_core::SplitId> = self
138 .split_manager
139 .splits_for_buffer(*buffer_id)
140 .into_iter()
141 .map(|leaf_id| leaf_id.0)
142 .collect();
143 let buffer_info = BufferInfo {
144 id: *buffer_id,
145 path: state.buffer.file_path().map(|p| p.to_path_buf()),
146 modified: state.buffer.is_modified(),
147 length: state.buffer.len(),
148 is_virtual,
149 view_mode: view_mode.to_string(),
150 is_composing_in_any_split,
151 compose_width,
152 language: state.language.clone(),
153 is_preview,
154 splits,
155 };
156 snapshot.buffers.insert(*buffer_id, buffer_info);
157
158 let diff = {
159 let diff = state.buffer.diff_since_saved();
160 BufferSavedDiff {
161 equal: diff.equal,
162 byte_ranges: diff.byte_ranges.clone(),
163 }
164 };
165 snapshot.buffer_saved_diffs.insert(*buffer_id, diff);
166
167 let is_hidden = self
176 .buffer_metadata
177 .get(buffer_id)
178 .is_some_and(|m| m.hidden_from_tabs);
179 let source_split = self.split_view_states.iter().find(|(split_id, vs)| {
180 vs.keyed_states.contains_key(buffer_id)
181 && !(is_hidden && self.grouped_subtrees.contains_key(split_id))
182 });
183 let cursor_pos = source_split
184 .and_then(|(_, vs)| vs.buffer_state(*buffer_id))
185 .map(|bs| bs.cursors.primary().position)
186 .unwrap_or(0);
187 tracing::trace!(
188 "snapshot: buffer {:?} cursor_pos={} (from split {:?})",
189 buffer_id,
190 cursor_pos,
191 source_split.map(|(id, _)| *id),
192 );
193 snapshot
194 .buffer_cursor_positions
195 .insert(*buffer_id, cursor_pos);
196
197 if !state.text_properties.is_empty() {
199 snapshot
200 .buffer_text_properties
201 .insert(*buffer_id, state.text_properties.all().to_vec());
202 }
203 }
204
205 if let Some(active_vs) = self
207 .split_view_states
208 .get(&self.split_manager.active_split())
209 {
210 let active_cursors = &active_vs.cursors;
212 let primary = active_cursors.primary();
213 let primary_position = primary.position;
214 let primary_selection = primary.selection_range();
215
216 snapshot.primary_cursor = Some(CursorInfo {
217 position: primary_position,
218 selection: primary_selection.clone(),
219 });
220
221 snapshot.all_cursors = active_cursors
223 .iter()
224 .map(|(_, cursor)| CursorInfo {
225 position: cursor.position,
226 selection: cursor.selection_range(),
227 })
228 .collect();
229
230 if let Some(range) = primary_selection {
232 if let Some(active_state) = self.buffers.get_mut(&self.active_buffer()) {
233 snapshot.selected_text =
234 Some(active_state.get_text_range(range.start, range.end));
235 }
236 }
237
238 let top_line = self.buffers.get(&self.active_buffer()).and_then(|state| {
240 if state.buffer.line_count().is_some() {
241 Some(state.buffer.get_line_number(active_vs.viewport.top_byte))
242 } else {
243 None
244 }
245 });
246 snapshot.viewport = Some(ViewportInfo {
247 top_byte: active_vs.viewport.top_byte,
248 top_line,
249 left_column: active_vs.viewport.left_column,
250 width: active_vs.viewport.width,
251 height: active_vs.viewport.height,
252 });
253 } else {
254 snapshot.primary_cursor = None;
255 snapshot.all_cursors.clear();
256 snapshot.viewport = None;
257 snapshot.selected_text = None;
258 }
259
260 snapshot.splits.clear();
265 for (leaf_id, vs) in &self.split_view_states {
266 let buf_id = vs.active_buffer;
267 let top_line = self.buffers.get(&buf_id).and_then(|state| {
268 if state.buffer.line_count().is_some() {
269 Some(state.buffer.get_line_number(vs.viewport.top_byte))
270 } else {
271 None
272 }
273 });
274 snapshot.splits.push(fresh_core::api::SplitSnapshot {
275 split_id: leaf_id.0 .0,
276 buffer_id: buf_id,
277 viewport: ViewportInfo {
278 top_byte: vs.viewport.top_byte,
279 top_line,
280 left_column: vs.viewport.left_column,
281 width: vs.viewport.width,
282 height: vs.viewport.height,
283 },
284 });
285 }
286
287 snapshot.clipboard = self.clipboard.get_internal().to_string();
289
290 snapshot.working_dir = self.working_dir.clone();
292 snapshot.authority_label = self.authority.display_label.clone();
293
294 snapshot.diagnostics = Arc::clone(&self.stored_diagnostics);
296
297 snapshot.folding_ranges = Arc::clone(&self.stored_folding_ranges);
299
300 if !Arc::ptr_eq(&self.config, &self.config_snapshot_anchor) {
309 let json = serde_json::to_value(&*self.config).unwrap_or(serde_json::Value::Null);
310 self.config_cached_json = Arc::new(json);
311 self.config_snapshot_anchor = Arc::clone(&self.config);
312 }
313 snapshot.config = Arc::clone(&self.config_cached_json);
314
315 snapshot.user_config = Arc::clone(&self.user_config_raw);
319
320 snapshot.editor_mode = self.editor_mode.clone();
322
323 for (plugin_name, state_map) in &self.plugin_global_state {
326 let entry = snapshot
327 .plugin_global_states
328 .entry(plugin_name.clone())
329 .or_default();
330 for (key, value) in state_map {
331 entry.entry(key.clone()).or_insert_with(|| value.clone());
332 }
333 }
334
335 let active_split_id = self.split_manager.active_split().0 .0;
340 let split_changed = snapshot.plugin_view_states_split != active_split_id;
341 if split_changed {
342 snapshot.plugin_view_states.clear();
343 snapshot.plugin_view_states_split = active_split_id;
344 }
345
346 {
348 let open_bids: Vec<_> = snapshot.buffers.keys().copied().collect();
349 snapshot
350 .plugin_view_states
351 .retain(|bid, _| open_bids.contains(bid));
352 }
353
354 if let Some(active_vs) = self
356 .split_view_states
357 .get(&self.split_manager.active_split())
358 {
359 for (buffer_id, buf_state) in &active_vs.keyed_states {
360 if !buf_state.plugin_state.is_empty() {
361 let entry = snapshot.plugin_view_states.entry(*buffer_id).or_default();
362 for (key, value) in &buf_state.plugin_state {
363 entry.entry(key.clone()).or_insert_with(|| value.clone());
365 }
366 }
367 }
368 }
369 }
370 }
371
372 pub fn handle_plugin_command(&mut self, command: PluginCommand) -> AnyhowResult<()> {
374 match command {
375 PluginCommand::InsertText {
377 buffer_id,
378 position,
379 text,
380 } => {
381 self.handle_insert_text(buffer_id, position, text);
382 }
383 PluginCommand::DeleteRange { buffer_id, range } => {
384 self.handle_delete_range(buffer_id, range);
385 }
386 PluginCommand::InsertAtCursor { text } => {
387 self.handle_insert_at_cursor(text);
388 }
389 PluginCommand::DeleteSelection => {
390 self.handle_delete_selection();
391 }
392
393 PluginCommand::AddOverlay {
395 buffer_id,
396 namespace,
397 range,
398 options,
399 } => {
400 self.handle_add_overlay(buffer_id, namespace, range, options);
401 }
402 PluginCommand::RemoveOverlay { buffer_id, handle } => {
403 self.handle_remove_overlay(buffer_id, handle);
404 }
405 PluginCommand::ClearAllOverlays { buffer_id } => {
406 self.handle_clear_all_overlays(buffer_id);
407 }
408 PluginCommand::ClearNamespace {
409 buffer_id,
410 namespace,
411 } => {
412 self.handle_clear_namespace(buffer_id, namespace);
413 }
414 PluginCommand::ClearOverlaysInRange {
415 buffer_id,
416 start,
417 end,
418 } => {
419 self.handle_clear_overlays_in_range(buffer_id, start, end);
420 }
421
422 PluginCommand::AddVirtualText {
424 buffer_id,
425 virtual_text_id,
426 position,
427 text,
428 color,
429 use_bg,
430 before,
431 } => {
432 self.handle_add_virtual_text(
433 buffer_id,
434 virtual_text_id,
435 position,
436 text,
437 color,
438 use_bg,
439 before,
440 );
441 }
442 PluginCommand::AddVirtualTextStyled {
443 buffer_id,
444 virtual_text_id,
445 position,
446 text,
447 fg,
448 bg,
449 bold,
450 italic,
451 before,
452 } => {
453 self.handle_add_virtual_text_styled(
454 buffer_id,
455 virtual_text_id,
456 position,
457 text,
458 fg,
459 bg,
460 bold,
461 italic,
462 before,
463 );
464 }
465 PluginCommand::RemoveVirtualText {
466 buffer_id,
467 virtual_text_id,
468 } => {
469 self.handle_remove_virtual_text(buffer_id, virtual_text_id);
470 }
471 PluginCommand::RemoveVirtualTextsByPrefix { buffer_id, prefix } => {
472 self.handle_remove_virtual_texts_by_prefix(buffer_id, prefix);
473 }
474 PluginCommand::ClearVirtualTexts { buffer_id } => {
475 self.handle_clear_virtual_texts(buffer_id);
476 }
477 PluginCommand::AddVirtualLine {
478 buffer_id,
479 position,
480 text,
481 fg_color,
482 bg_color,
483 above,
484 namespace,
485 priority,
486 } => {
487 self.handle_add_virtual_line(
488 buffer_id, position, text, fg_color, bg_color, above, namespace, priority,
489 );
490 }
491 PluginCommand::ClearVirtualTextNamespace {
492 buffer_id,
493 namespace,
494 } => {
495 self.handle_clear_virtual_text_namespace(buffer_id, namespace);
496 }
497
498 PluginCommand::AddConceal {
500 buffer_id,
501 namespace,
502 start,
503 end,
504 replacement,
505 } => {
506 self.handle_add_conceal(buffer_id, namespace, start, end, replacement);
507 }
508 PluginCommand::ClearConcealNamespace {
509 buffer_id,
510 namespace,
511 } => {
512 self.handle_clear_conceal_namespace(buffer_id, namespace);
513 }
514 PluginCommand::ClearConcealsInRange {
515 buffer_id,
516 start,
517 end,
518 } => {
519 self.handle_clear_conceals_in_range(buffer_id, start, end);
520 }
521
522 PluginCommand::AddFold {
523 buffer_id,
524 start,
525 end,
526 placeholder,
527 } => {
528 self.handle_add_fold(buffer_id, start, end, placeholder);
529 }
530 PluginCommand::ClearFolds { buffer_id } => {
531 self.handle_clear_folds(buffer_id);
532 }
533
534 PluginCommand::AddSoftBreak {
536 buffer_id,
537 namespace,
538 position,
539 indent,
540 } => {
541 self.handle_add_soft_break(buffer_id, namespace, position, indent);
542 }
543 PluginCommand::ClearSoftBreakNamespace {
544 buffer_id,
545 namespace,
546 } => {
547 self.handle_clear_soft_break_namespace(buffer_id, namespace);
548 }
549 PluginCommand::ClearSoftBreaksInRange {
550 buffer_id,
551 start,
552 end,
553 } => {
554 self.handle_clear_soft_breaks_in_range(buffer_id, start, end);
555 }
556
557 PluginCommand::AddMenuItem {
559 menu_label,
560 item,
561 position,
562 } => {
563 self.handle_add_menu_item(menu_label, item, position);
564 }
565 PluginCommand::AddMenu { menu, position } => {
566 self.handle_add_menu(menu, position);
567 }
568 PluginCommand::RemoveMenuItem {
569 menu_label,
570 item_label,
571 } => {
572 self.handle_remove_menu_item(menu_label, item_label);
573 }
574 PluginCommand::RemoveMenu { menu_label } => {
575 self.handle_remove_menu(menu_label);
576 }
577
578 PluginCommand::FocusSplit { split_id } => {
580 self.handle_focus_split(split_id);
581 }
582 PluginCommand::SetSplitBuffer {
583 split_id,
584 buffer_id,
585 } => {
586 self.handle_set_split_buffer(split_id, buffer_id);
587 }
588 PluginCommand::SetSplitScroll { split_id, top_byte } => {
589 self.handle_set_split_scroll(split_id, top_byte);
590 }
591 PluginCommand::RequestHighlights {
592 buffer_id,
593 range,
594 request_id,
595 } => {
596 self.handle_request_highlights(buffer_id, range, request_id);
597 }
598 PluginCommand::CloseSplit { split_id } => {
599 self.handle_close_split(split_id);
600 }
601 PluginCommand::SetSplitRatio { split_id, ratio } => {
602 self.handle_set_split_ratio(split_id, ratio);
603 }
604 PluginCommand::SetSplitLabel { split_id, label } => {
605 self.split_manager.set_label(LeafId(split_id), label);
606 }
607 PluginCommand::ClearSplitLabel { split_id } => {
608 self.split_manager.clear_label(split_id);
609 }
610 PluginCommand::GetSplitByLabel { label, request_id } => {
611 self.handle_get_split_by_label(label, request_id);
612 }
613 PluginCommand::DistributeSplitsEvenly { split_ids: _ } => {
614 self.handle_distribute_splits_evenly();
615 }
616 PluginCommand::SetBufferCursor {
617 buffer_id,
618 position,
619 } => {
620 self.handle_set_buffer_cursor(buffer_id, position);
621 }
622 PluginCommand::SetBufferShowCursors { buffer_id, show } => {
623 self.handle_set_buffer_show_cursors(buffer_id, show);
624 }
625
626 PluginCommand::SetLayoutHints {
628 buffer_id,
629 split_id,
630 range: _,
631 hints,
632 } => {
633 self.handle_set_layout_hints(buffer_id, split_id, hints);
634 }
635 PluginCommand::SetLineNumbers { buffer_id, enabled } => {
636 self.handle_set_line_numbers(buffer_id, enabled);
637 }
638 PluginCommand::SetViewMode { buffer_id, mode } => {
639 self.handle_set_view_mode(buffer_id, &mode);
640 }
641 PluginCommand::SetLineWrap {
642 buffer_id,
643 split_id,
644 enabled,
645 } => {
646 self.handle_set_line_wrap(buffer_id, split_id, enabled);
647 }
648 PluginCommand::SubmitViewTransform {
649 buffer_id,
650 split_id,
651 payload,
652 } => {
653 self.handle_submit_view_transform(buffer_id, split_id, payload);
654 }
655 PluginCommand::ClearViewTransform {
656 buffer_id: _,
657 split_id,
658 } => {
659 self.handle_clear_view_transform(split_id);
660 }
661 PluginCommand::SetViewState {
662 buffer_id,
663 key,
664 value,
665 } => {
666 self.handle_set_view_state(buffer_id, key, value);
667 }
668 PluginCommand::SetGlobalState {
669 plugin_name,
670 key,
671 value,
672 } => {
673 self.handle_set_global_state(plugin_name, key, value);
674 }
675 PluginCommand::RefreshLines { buffer_id } => {
676 self.handle_refresh_lines(buffer_id);
677 }
678 PluginCommand::RefreshAllLines => {
679 self.handle_refresh_all_lines();
680 }
681 PluginCommand::HookCompleted { .. } => {
682 }
684 PluginCommand::SetLineIndicator {
685 buffer_id,
686 line,
687 namespace,
688 symbol,
689 color,
690 priority,
691 } => {
692 self.handle_set_line_indicator(buffer_id, line, namespace, symbol, color, priority);
693 }
694 PluginCommand::SetLineIndicators {
695 buffer_id,
696 lines,
697 namespace,
698 symbol,
699 color,
700 priority,
701 } => {
702 self.handle_set_line_indicators(
703 buffer_id, lines, namespace, symbol, color, priority,
704 );
705 }
706 PluginCommand::ClearLineIndicators {
707 buffer_id,
708 namespace,
709 } => {
710 self.handle_clear_line_indicators(buffer_id, namespace);
711 }
712 PluginCommand::SetFileExplorerDecorations {
713 namespace,
714 decorations,
715 } => {
716 self.handle_set_file_explorer_decorations(namespace, decorations);
717 }
718 PluginCommand::ClearFileExplorerDecorations { namespace } => {
719 self.handle_clear_file_explorer_decorations(&namespace);
720 }
721
722 PluginCommand::SetStatus { message } => {
724 self.handle_set_status(message);
725 }
726 PluginCommand::ApplyTheme { theme_name } => {
727 self.apply_theme(&theme_name);
728 }
729 PluginCommand::OverrideThemeColors { overrides } => {
730 self.handle_override_theme_colors(overrides);
731 }
732 PluginCommand::ReloadConfig => {
733 self.reload_config();
734 }
735 PluginCommand::SetSetting { path, value, .. } => {
736 self.handle_set_setting(path, value);
737 }
738 PluginCommand::ReloadThemes { apply_theme } => {
739 self.reload_themes();
740 if let Some(theme_name) = apply_theme {
741 self.apply_theme(&theme_name);
742 }
743 }
744 PluginCommand::RegisterGrammar {
745 language,
746 grammar_path,
747 extensions,
748 } => {
749 self.handle_register_grammar(language, grammar_path, extensions);
750 }
751 PluginCommand::RegisterLanguageConfig { language, config } => {
752 self.handle_register_language_config(language, config);
753 }
754 PluginCommand::RegisterLspServer { language, config } => {
755 self.handle_register_lsp_server(language, config);
756 }
757 PluginCommand::ReloadGrammars { callback_id } => {
758 self.handle_reload_grammars(callback_id);
759 }
760 PluginCommand::StartPrompt {
761 label,
762 prompt_type,
763 floating_overlay,
764 } => {
765 self.handle_start_prompt(label, prompt_type, floating_overlay);
766 }
767 PluginCommand::StartPromptWithInitial {
768 label,
769 prompt_type,
770 initial_value,
771 floating_overlay,
772 } => {
773 self.handle_start_prompt_with_initial(
774 label,
775 prompt_type,
776 initial_value,
777 floating_overlay,
778 );
779 }
780 PluginCommand::StartPromptAsync {
781 label,
782 initial_value,
783 callback_id,
784 } => {
785 self.handle_start_prompt_async(label, initial_value, callback_id);
786 }
787 PluginCommand::AwaitNextKey { callback_id } => {
788 self.handle_await_next_key(callback_id);
789 }
790 PluginCommand::SetKeyCaptureActive { active } => {
791 self.key_capture_active = active;
792 if !active {
793 self.pending_key_capture_buffer.clear();
797 }
798 }
799 PluginCommand::SetPromptSuggestions { suggestions } => {
800 self.handle_set_prompt_suggestions(suggestions);
801 }
802 PluginCommand::SetPromptInputSync { sync } => {
803 if let Some(prompt) = &mut self.prompt {
804 prompt.sync_input_on_navigate = sync;
805 }
806 }
807 PluginCommand::SetPromptTitle { title } => {
808 if let Some(prompt) = &mut self.prompt {
809 prompt.title = title;
810 }
811 }
812
813 PluginCommand::RegisterCommand { command } => {
815 self.handle_register_command(command);
816 }
817 PluginCommand::UnregisterCommand { name } => {
818 self.handle_unregister_command(name);
819 }
820 PluginCommand::DefineMode {
821 name,
822 bindings,
823 read_only,
824 allow_text_input,
825 inherit_normal_bindings,
826 plugin_name,
827 } => {
828 self.handle_define_mode(
829 name,
830 bindings,
831 read_only,
832 allow_text_input,
833 inherit_normal_bindings,
834 plugin_name,
835 );
836 }
837
838 PluginCommand::OpenFileInBackground { path } => {
840 self.handle_open_file_in_background(path);
841 }
842 PluginCommand::OpenFileAtLocation { path, line, column } => {
843 return self.handle_open_file_at_location(path, line, column);
844 }
845 PluginCommand::OpenFileInSplit {
846 split_id,
847 path,
848 line,
849 column,
850 } => {
851 return self.handle_open_file_in_split(split_id, path, line, column);
852 }
853 PluginCommand::ShowBuffer { buffer_id } => {
854 self.handle_show_buffer(buffer_id);
855 }
856 PluginCommand::CloseBuffer { buffer_id } => {
857 self.handle_close_buffer(buffer_id);
858 }
859
860 PluginCommand::StartAnimationArea { id, rect, kind } => {
862 self.handle_start_animation_area(id, rect, kind);
863 }
864 PluginCommand::StartAnimationVirtualBuffer {
865 id,
866 buffer_id,
867 kind,
868 } => {
869 self.handle_start_animation_virtual_buffer(id, buffer_id, kind);
870 }
871 PluginCommand::CancelAnimation { id } => {
872 self.animations
873 .cancel(crate::view::animation::AnimationId::from_raw(id));
874 }
875
876 PluginCommand::SendLspRequest {
878 language,
879 method,
880 params,
881 request_id,
882 } => {
883 self.handle_send_lsp_request(language, method, params, request_id);
884 }
885
886 PluginCommand::SetClipboard { text } => {
888 self.handle_set_clipboard(text);
889 }
890
891 PluginCommand::SpawnProcess {
893 command,
894 args,
895 cwd,
896 callback_id,
897 } => {
898 self.handle_spawn_process(command, args, cwd, callback_id);
899 }
900
901 PluginCommand::SpawnHostProcess {
902 command,
903 args,
904 cwd,
905 callback_id,
906 } => {
907 self.handle_spawn_host_process(command, args, cwd, callback_id);
908 }
909
910 PluginCommand::KillHostProcess { process_id } => {
911 self.handle_kill_host_process(process_id);
912 }
913
914 PluginCommand::SetAuthority { payload } => {
915 self.handle_set_authority(payload);
916 }
917
918 PluginCommand::ClearAuthority => {
919 tracing::info!("Plugin cleared authority; restoring local");
920 self.clear_authority();
921 }
922
923 PluginCommand::SetRemoteIndicatorState { state } => {
924 self.handle_set_remote_indicator_state(state);
925 }
926
927 PluginCommand::ClearRemoteIndicatorState => {
928 self.remote_indicator_override = None;
929 }
930
931 PluginCommand::SpawnProcessWait {
932 process_id,
933 callback_id,
934 } => {
935 self.handle_spawn_process_wait(process_id, callback_id);
936 }
937
938 PluginCommand::Delay {
939 callback_id,
940 duration_ms,
941 } => {
942 self.handle_delay(callback_id, duration_ms);
943 }
944
945 PluginCommand::SpawnBackgroundProcess {
946 process_id,
947 command,
948 args,
949 cwd,
950 callback_id,
951 } => {
952 self.handle_spawn_background_process(process_id, command, args, cwd, callback_id);
953 }
954
955 PluginCommand::KillBackgroundProcess { process_id } => {
956 self.handle_kill_background_process(process_id);
957 }
958
959 PluginCommand::CreateVirtualBuffer {
961 name,
962 mode,
963 read_only,
964 } => {
965 self.handle_create_virtual_buffer(name, mode, read_only);
966 }
967 PluginCommand::CreateVirtualBufferWithContent {
968 name,
969 mode,
970 read_only,
971 entries,
972 show_line_numbers,
973 show_cursors,
974 editing_disabled,
975 hidden_from_tabs,
976 request_id,
977 } => {
978 self.handle_create_virtual_buffer_with_content(
979 name,
980 mode,
981 read_only,
982 entries,
983 show_line_numbers,
984 show_cursors,
985 editing_disabled,
986 hidden_from_tabs,
987 request_id,
988 );
989 }
990 PluginCommand::CreateVirtualBufferInSplit {
991 name,
992 mode,
993 read_only,
994 entries,
995 ratio,
996 direction,
997 panel_id,
998 show_line_numbers,
999 show_cursors,
1000 editing_disabled,
1001 line_wrap,
1002 before,
1003 role,
1004 request_id,
1005 } => {
1006 self.handle_create_virtual_buffer_in_split(
1007 name,
1008 mode,
1009 read_only,
1010 entries,
1011 ratio,
1012 direction,
1013 panel_id,
1014 show_line_numbers,
1015 show_cursors,
1016 editing_disabled,
1017 line_wrap,
1018 before,
1019 role,
1020 request_id,
1021 );
1022 }
1023 PluginCommand::SetVirtualBufferContent { buffer_id, entries } => {
1024 self.handle_set_virtual_buffer_content(buffer_id, entries);
1025 }
1026 PluginCommand::GetTextPropertiesAtCursor { buffer_id } => {
1027 self.handle_get_text_properties_at_cursor(buffer_id);
1028 }
1029 PluginCommand::CreateVirtualBufferInExistingSplit {
1030 name,
1031 mode,
1032 read_only,
1033 entries,
1034 split_id,
1035 show_line_numbers,
1036 show_cursors,
1037 editing_disabled,
1038 line_wrap,
1039 request_id,
1040 } => {
1041 self.handle_create_virtual_buffer_in_existing_split(
1042 name,
1043 mode,
1044 read_only,
1045 entries,
1046 split_id,
1047 show_line_numbers,
1048 show_cursors,
1049 editing_disabled,
1050 line_wrap,
1051 request_id,
1052 );
1053 }
1054
1055 PluginCommand::SetContext { name, active } => {
1057 self.handle_set_context(name, active);
1058 }
1059
1060 PluginCommand::SetReviewDiffHunks { hunks } => {
1062 self.review_hunks = hunks;
1063 tracing::debug!("Set {} review hunks", self.review_hunks.len());
1064 }
1065
1066 PluginCommand::ExecuteAction { action_name } => {
1068 self.handle_execute_action(action_name);
1069 }
1070 PluginCommand::ExecuteActions { actions } => {
1071 self.handle_execute_actions(actions);
1072 }
1073 PluginCommand::GetBufferText {
1074 buffer_id,
1075 start,
1076 end,
1077 request_id,
1078 } => {
1079 self.handle_get_buffer_text(buffer_id, start, end, request_id);
1080 }
1081 PluginCommand::GetLineStartPosition {
1082 buffer_id,
1083 line,
1084 request_id,
1085 } => {
1086 self.handle_get_line_start_position(buffer_id, line, request_id);
1087 }
1088 PluginCommand::GetLineEndPosition {
1089 buffer_id,
1090 line,
1091 request_id,
1092 } => {
1093 self.handle_get_line_end_position(buffer_id, line, request_id);
1094 }
1095 PluginCommand::GetBufferLineCount {
1096 buffer_id,
1097 request_id,
1098 } => {
1099 self.handle_get_buffer_line_count(buffer_id, request_id);
1100 }
1101 PluginCommand::ScrollToLineCenter {
1102 split_id,
1103 buffer_id,
1104 line,
1105 } => {
1106 self.handle_scroll_to_line_center(split_id, buffer_id, line);
1107 }
1108 PluginCommand::ScrollBufferToLine { buffer_id, line } => {
1109 self.handle_scroll_buffer_to_line(buffer_id, line);
1110 }
1111 PluginCommand::SetEditorMode { mode } => {
1112 self.handle_set_editor_mode(mode);
1113 }
1114
1115 PluginCommand::ShowActionPopup {
1117 popup_id,
1118 title,
1119 message,
1120 actions,
1121 } => {
1122 self.handle_show_action_popup(popup_id, title, message, actions);
1123 }
1124
1125 PluginCommand::DisableLspForLanguage { language } => {
1126 self.handle_disable_lsp_for_language(language);
1127 }
1128
1129 PluginCommand::RestartLspForLanguage { language } => {
1130 self.handle_restart_lsp_for_language(language);
1131 }
1132
1133 PluginCommand::SetLspRootUri { language, uri } => {
1134 self.handle_set_lsp_root_uri(language, uri);
1135 }
1136
1137 PluginCommand::CreateScrollSyncGroup {
1139 group_id,
1140 left_split,
1141 right_split,
1142 } => {
1143 self.handle_create_scroll_sync_group(group_id, left_split, right_split);
1144 }
1145 PluginCommand::SetScrollSyncAnchors { group_id, anchors } => {
1146 self.handle_set_scroll_sync_anchors(group_id, anchors);
1147 }
1148 PluginCommand::RemoveScrollSyncGroup { group_id } => {
1149 self.handle_remove_scroll_sync_group(group_id);
1150 }
1151
1152 PluginCommand::CreateCompositeBuffer {
1154 name,
1155 mode,
1156 layout,
1157 sources,
1158 hunks,
1159 initial_focus_hunk,
1160 request_id,
1161 } => {
1162 self.handle_create_composite_buffer(
1163 name,
1164 mode,
1165 layout,
1166 sources,
1167 hunks,
1168 initial_focus_hunk,
1169 request_id,
1170 );
1171 }
1172 PluginCommand::UpdateCompositeAlignment { buffer_id, hunks } => {
1173 self.handle_update_composite_alignment(buffer_id, hunks);
1174 }
1175 PluginCommand::CloseCompositeBuffer { buffer_id } => {
1176 self.close_composite_buffer(buffer_id);
1177 }
1178 PluginCommand::FlushLayout => {
1179 self.flush_layout();
1180 }
1181 PluginCommand::CompositeNextHunk { buffer_id } => {
1182 let split_id = self.split_manager.active_split();
1183 self.composite_next_hunk(split_id, buffer_id);
1184 }
1185 PluginCommand::CompositePrevHunk { buffer_id } => {
1186 let split_id = self.split_manager.active_split();
1187 self.composite_prev_hunk(split_id, buffer_id);
1188 }
1189
1190 PluginCommand::CreateBufferGroup {
1192 name,
1193 mode,
1194 layout_json,
1195 request_id,
1196 } => {
1197 self.handle_create_buffer_group(name, mode, layout_json, request_id);
1198 }
1199 PluginCommand::SetPanelContent {
1200 group_id,
1201 panel_name,
1202 entries,
1203 } => {
1204 self.set_panel_content(group_id, panel_name, entries);
1205 }
1206 PluginCommand::CloseBufferGroup { group_id } => {
1207 self.close_buffer_group(group_id);
1208 }
1209 PluginCommand::FocusPanel {
1210 group_id,
1211 panel_name,
1212 } => {
1213 self.focus_panel(group_id, panel_name);
1214 }
1215
1216 PluginCommand::SaveBufferToPath { buffer_id, path } => {
1218 self.handle_save_buffer_to_path(buffer_id, path);
1219 }
1220
1221 #[cfg(feature = "plugins")]
1223 PluginCommand::LoadPlugin { path, callback_id } => {
1224 self.handle_load_plugin(path, callback_id);
1225 }
1226 #[cfg(feature = "plugins")]
1227 PluginCommand::UnloadPlugin { name, callback_id } => {
1228 self.handle_unload_plugin(name, callback_id);
1229 }
1230 #[cfg(feature = "plugins")]
1231 PluginCommand::ReloadPlugin { name, callback_id } => {
1232 self.handle_reload_plugin(name, callback_id);
1233 }
1234 #[cfg(feature = "plugins")]
1235 PluginCommand::ListPlugins { callback_id } => {
1236 self.handle_list_plugins(callback_id);
1237 }
1238 #[cfg(not(feature = "plugins"))]
1240 PluginCommand::LoadPlugin { .. }
1241 | PluginCommand::UnloadPlugin { .. }
1242 | PluginCommand::ReloadPlugin { .. }
1243 | PluginCommand::ListPlugins { .. } => {
1244 tracing::warn!("Plugin management commands require the 'plugins' feature");
1245 }
1246
1247 PluginCommand::CreateTerminal {
1249 cwd,
1250 direction,
1251 ratio,
1252 focus,
1253 persistent,
1254 request_id,
1255 } => {
1256 self.handle_create_terminal(cwd, direction, ratio, focus, persistent, request_id);
1257 }
1258
1259 PluginCommand::SendTerminalInput { terminal_id, data } => {
1260 self.handle_send_terminal_input(terminal_id, data);
1261 }
1262
1263 PluginCommand::CloseTerminal { terminal_id } => {
1264 self.handle_close_terminal(terminal_id);
1265 }
1266
1267 PluginCommand::GrepProject {
1268 pattern,
1269 fixed_string,
1270 case_sensitive,
1271 max_results,
1272 whole_words,
1273 callback_id,
1274 } => {
1275 self.handle_grep_project(
1276 pattern,
1277 fixed_string,
1278 case_sensitive,
1279 max_results,
1280 whole_words,
1281 callback_id,
1282 );
1283 }
1284
1285 PluginCommand::GrepProjectStreaming {
1286 pattern,
1287 fixed_string,
1288 case_sensitive,
1289 max_results,
1290 whole_words,
1291 search_id,
1292 callback_id,
1293 } => {
1294 self.handle_grep_project_streaming(
1295 pattern,
1296 fixed_string,
1297 case_sensitive,
1298 max_results,
1299 whole_words,
1300 search_id,
1301 callback_id,
1302 );
1303 }
1304
1305 PluginCommand::ReplaceInBuffer {
1306 file_path,
1307 matches,
1308 replacement,
1309 callback_id,
1310 } => {
1311 self.handle_replace_in_buffer(file_path, matches, replacement, callback_id);
1312 }
1313 }
1314 Ok(())
1315 }
1316
1317 fn handle_save_buffer_to_path(&mut self, buffer_id: BufferId, path: std::path::PathBuf) {
1319 if let Some(state) = self.buffers.get_mut(&buffer_id) {
1320 match state.buffer.save_to_file(&path) {
1322 Ok(()) => {
1323 if let Err(e) = self.finalize_save(Some(path)) {
1326 tracing::warn!("Failed to finalize save: {}", e);
1327 }
1328 tracing::debug!("Saved buffer {:?} to path", buffer_id);
1329 }
1330 Err(e) => {
1331 self.handle_set_status(format!("Error saving: {}", e));
1332 tracing::error!("Failed to save buffer to path: {}", e);
1333 }
1334 }
1335 } else {
1336 self.handle_set_status(format!("Buffer {:?} not found", buffer_id));
1337 tracing::warn!("SaveBufferToPath: buffer {:?} not found", buffer_id);
1338 }
1339 }
1340
1341 #[cfg(feature = "plugins")]
1343 fn handle_load_plugin(&mut self, path: std::path::PathBuf, callback_id: JsCallbackId) {
1344 match self.plugin_manager.load_plugin(&path) {
1345 Ok(()) => {
1346 tracing::info!("Loaded plugin from {:?}", path);
1347 self.plugin_manager
1348 .resolve_callback(callback_id, "true".to_string());
1349 }
1350 Err(e) => {
1351 tracing::error!("Failed to load plugin from {:?}: {}", path, e);
1352 self.plugin_manager
1353 .reject_callback(callback_id, format!("{}", e));
1354 }
1355 }
1356 }
1357
1358 #[cfg(feature = "plugins")]
1360 fn handle_unload_plugin(&mut self, name: String, callback_id: JsCallbackId) {
1361 match self.plugin_manager.unload_plugin(&name) {
1362 Ok(()) => {
1363 tracing::info!("Unloaded plugin: {}", name);
1364 self.plugin_manager
1365 .resolve_callback(callback_id, "true".to_string());
1366 }
1367 Err(e) => {
1368 tracing::error!("Failed to unload plugin '{}': {}", name, e);
1369 self.plugin_manager
1370 .reject_callback(callback_id, format!("{}", e));
1371 }
1372 }
1373 }
1374
1375 #[cfg(feature = "plugins")]
1377 fn handle_reload_plugin(&mut self, name: String, callback_id: JsCallbackId) {
1378 match self.plugin_manager.reload_plugin(&name) {
1379 Ok(()) => {
1380 tracing::info!("Reloaded plugin: {}", name);
1381 self.plugin_manager
1382 .resolve_callback(callback_id, "true".to_string());
1383 }
1384 Err(e) => {
1385 tracing::error!("Failed to reload plugin '{}': {}", name, e);
1386 self.plugin_manager
1387 .reject_callback(callback_id, format!("{}", e));
1388 }
1389 }
1390 }
1391
1392 #[cfg(feature = "plugins")]
1394 fn handle_list_plugins(&mut self, callback_id: JsCallbackId) {
1395 let plugins = self.plugin_manager.list_plugins();
1396 let json_array: Vec<serde_json::Value> = plugins
1398 .iter()
1399 .map(|p| {
1400 serde_json::json!({
1401 "name": p.name,
1402 "path": p.path.to_string_lossy(),
1403 "enabled": p.enabled
1404 })
1405 })
1406 .collect();
1407 let json_str = serde_json::to_string(&json_array).unwrap_or_else(|_| "[]".to_string());
1408 self.plugin_manager.resolve_callback(callback_id, json_str);
1409 }
1410
1411 fn handle_execute_action(&mut self, action_name: String) {
1413 use crate::input::keybindings::Action;
1414 use std::collections::HashMap;
1415
1416 if let Some(action) = Action::from_str(&action_name, &HashMap::new()) {
1418 if let Err(e) = self.handle_action(action) {
1420 tracing::warn!("Failed to execute action '{}': {}", action_name, e);
1421 } else {
1422 tracing::debug!("Executed action: {}", action_name);
1423 }
1424 } else {
1425 tracing::warn!("Unknown action: {}", action_name);
1426 }
1427 }
1428
1429 fn handle_execute_actions(&mut self, actions: Vec<fresh_core::api::ActionSpec>) {
1432 use crate::input::keybindings::Action;
1433 use std::collections::HashMap;
1434
1435 for action_spec in actions {
1436 if let Some(action) = Action::from_str(&action_spec.action, &HashMap::new()) {
1437 for _ in 0..action_spec.count {
1439 if let Err(e) = self.handle_action(action.clone()) {
1440 tracing::warn!("Failed to execute action '{}': {}", action_spec.action, e);
1441 return; }
1443 }
1444 tracing::debug!(
1445 "Executed action '{}' {} time(s)",
1446 action_spec.action,
1447 action_spec.count
1448 );
1449 } else {
1450 tracing::warn!("Unknown action: {}", action_spec.action);
1451 return; }
1453 }
1454 }
1455
1456 fn handle_get_buffer_text(
1458 &mut self,
1459 buffer_id: BufferId,
1460 start: usize,
1461 end: usize,
1462 request_id: u64,
1463 ) {
1464 let result = if let Some(state) = self.buffers.get_mut(&buffer_id) {
1465 let len = state.buffer.len();
1467 if start <= end && end <= len {
1468 Ok(state.get_text_range(start, end))
1469 } else {
1470 Err(format!(
1471 "Invalid range {}..{} for buffer of length {}",
1472 start, end, len
1473 ))
1474 }
1475 } else {
1476 Err(format!("Buffer {:?} not found", buffer_id))
1477 };
1478
1479 let callback_id = fresh_core::api::JsCallbackId::from(request_id);
1481 match result {
1482 Ok(text) => {
1483 let json = serde_json::to_string(&text).unwrap_or_else(|_| "null".to_string());
1485 self.plugin_manager.resolve_callback(callback_id, json);
1486 }
1487 Err(error) => {
1488 self.plugin_manager.reject_callback(callback_id, error);
1489 }
1490 }
1491 }
1492
1493 fn handle_set_editor_mode(&mut self, mode: Option<String>) {
1495 self.editor_mode = mode.clone();
1496 tracing::debug!("Set editor mode: {:?}", mode);
1497 }
1498
1499 fn resolve_buffer_id(&self, buffer_id: BufferId) -> BufferId {
1501 if buffer_id.0 == 0 {
1502 self.active_buffer()
1503 } else {
1504 buffer_id
1505 }
1506 }
1507
1508 fn resolve_json_callback<T: serde::Serialize>(&mut self, request_id: u64, value: T) {
1510 let callback_id = fresh_core::api::JsCallbackId::from(request_id);
1511 let json = serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string());
1512 self.plugin_manager.resolve_callback(callback_id, json);
1513 }
1514
1515 fn handle_get_line_start_position(&mut self, buffer_id: BufferId, line: u32, request_id: u64) {
1517 let actual_buffer_id = self.resolve_buffer_id(buffer_id);
1518 let result = self.buffers.get_mut(&actual_buffer_id).and_then(|state| {
1519 let len = state.buffer.len();
1520 let content = state.get_text_range(0, len);
1521 buffer_line_byte_offset(&content, len, line as usize, false)
1522 });
1523 self.resolve_json_callback(request_id, result);
1524 }
1525
1526 fn handle_get_line_end_position(&mut self, buffer_id: BufferId, line: u32, request_id: u64) {
1529 let actual_buffer_id = self.resolve_buffer_id(buffer_id);
1530 let result = self.buffers.get_mut(&actual_buffer_id).and_then(|state| {
1531 let len = state.buffer.len();
1532 let content = state.get_text_range(0, len);
1533 buffer_line_byte_offset(&content, len, line as usize, true)
1534 });
1535 self.resolve_json_callback(request_id, result);
1536 }
1537
1538 fn handle_get_buffer_line_count(&mut self, buffer_id: BufferId, request_id: u64) {
1540 let actual_buffer_id = self.resolve_buffer_id(buffer_id);
1541
1542 let result = if let Some(state) = self.buffers.get_mut(&actual_buffer_id) {
1543 let buffer_len = state.buffer.len();
1544 let content = state.get_text_range(0, buffer_len);
1545
1546 if content.is_empty() {
1548 Some(1) } else {
1550 let newline_count = content.chars().filter(|&c| c == '\n').count();
1551 let ends_with_newline = content.ends_with('\n');
1553 if ends_with_newline {
1554 Some(newline_count)
1555 } else {
1556 Some(newline_count + 1)
1557 }
1558 }
1559 } else {
1560 None
1561 };
1562
1563 self.resolve_json_callback(request_id, result);
1564 }
1565
1566 fn handle_scroll_to_line_center(
1568 &mut self,
1569 split_id: SplitId,
1570 buffer_id: BufferId,
1571 line: usize,
1572 ) {
1573 let actual_split_id = if split_id.0 == 0 {
1574 self.split_manager.active_split()
1575 } else {
1576 LeafId(split_id)
1577 };
1578 let actual_buffer_id = self.resolve_buffer_id(buffer_id);
1579
1580 let viewport_height = if let Some(view_state) = self.split_view_states.get(&actual_split_id)
1582 {
1583 view_state.viewport.height as usize
1584 } else {
1585 return;
1586 };
1587
1588 let lines_above = viewport_height / 2;
1590 let target_line = line.saturating_sub(lines_above);
1591
1592 if let Some(state) = self.buffers.get_mut(&actual_buffer_id) {
1594 let buffer = &mut state.buffer;
1595 if let Some(view_state) = self.split_view_states.get_mut(&actual_split_id) {
1596 view_state.viewport.scroll_to(buffer, target_line);
1597 view_state.viewport.set_skip_ensure_visible();
1599 }
1600 }
1601 }
1602
1603 fn handle_scroll_buffer_to_line(&mut self, buffer_id: BufferId, line: usize) {
1613 if !self.buffers.contains_key(&buffer_id) {
1614 return;
1615 }
1616
1617 let mut target_leaves: Vec<LeafId> = Vec::new();
1619
1620 for leaf_id in self.split_manager.root().leaf_split_ids() {
1622 if let Some(vs) = self.split_view_states.get(&leaf_id) {
1623 if vs.active_buffer == buffer_id {
1624 target_leaves.push(leaf_id);
1625 }
1626 }
1627 }
1628
1629 for (_group_leaf_id, node) in self.grouped_subtrees.iter() {
1631 if let crate::view::split::SplitNode::Grouped { layout, .. } = node {
1632 for inner_leaf in layout.leaf_split_ids() {
1633 if let Some(vs) = self.split_view_states.get(&inner_leaf) {
1634 if vs.active_buffer == buffer_id && !target_leaves.contains(&inner_leaf) {
1635 target_leaves.push(inner_leaf);
1636 }
1637 }
1638 }
1639 }
1640 }
1641
1642 if target_leaves.is_empty() {
1643 return;
1644 }
1645
1646 let state = match self.buffers.get_mut(&buffer_id) {
1647 Some(s) => s,
1648 None => return,
1649 };
1650
1651 for leaf_id in target_leaves {
1652 let Some(view_state) = self.split_view_states.get_mut(&leaf_id) else {
1653 continue;
1654 };
1655 let viewport_height = view_state.viewport.height as usize;
1656 let lines_above = viewport_height / 3;
1659 let target = line.saturating_sub(lines_above);
1660 view_state.viewport.scroll_to(&mut state.buffer, target);
1661 view_state.viewport.set_skip_ensure_visible();
1662 }
1663 }
1664
1665 fn handle_spawn_host_process(
1666 &mut self,
1667 command: String,
1668 args: Vec<String>,
1669 cwd: Option<String>,
1670 callback_id: JsCallbackId,
1671 ) {
1672 if let (Some(runtime), Some(bridge)) = (&self.tokio_runtime, &self.async_bridge) {
1687 use tokio::io::{AsyncReadExt, BufReader};
1688 use tokio::process::Command as TokioCommand;
1689
1690 let effective_cwd = cwd.or_else(|| {
1691 std::env::current_dir()
1692 .map(|p| p.to_string_lossy().to_string())
1693 .ok()
1694 });
1695 let sender = bridge.sender();
1696 let process_id = callback_id.as_u64();
1697
1698 let (kill_tx, mut kill_rx) = tokio::sync::oneshot::channel::<()>();
1699 self.host_process_handles.insert(process_id, kill_tx);
1700
1701 runtime.spawn(async move {
1702 use crate::services::process_hidden::HideWindow;
1703 let mut cmd = TokioCommand::new(&command);
1704 cmd.args(&args);
1705 cmd.stdout(std::process::Stdio::piped());
1706 cmd.stderr(std::process::Stdio::piped());
1707 cmd.hide_window();
1708 if let Some(ref dir) = effective_cwd {
1709 cmd.current_dir(dir);
1710 }
1711 let mut child = match cmd.spawn() {
1712 Ok(c) => c,
1713 Err(e) => {
1714 #[allow(clippy::let_underscore_must_use)]
1715 let _ = sender.send(AsyncMessage::PluginProcessOutput {
1716 process_id,
1717 stdout: String::new(),
1718 stderr: e.to_string(),
1719 exit_code: -1,
1720 });
1721 return;
1722 }
1723 };
1724
1725 let stdout_pipe = child.stdout.take();
1731 let stderr_pipe = child.stderr.take();
1732
1733 let stdout_fut = async {
1734 let mut buf = String::new();
1735 if let Some(s) = stdout_pipe {
1736 #[allow(clippy::let_underscore_must_use)]
1737 let _ = BufReader::new(s).read_to_string(&mut buf).await;
1738 }
1739 buf
1740 };
1741 let stderr_fut = async {
1742 let mut buf = String::new();
1743 if let Some(s) = stderr_pipe {
1744 #[allow(clippy::let_underscore_must_use)]
1745 let _ = BufReader::new(s).read_to_string(&mut buf).await;
1746 }
1747 buf
1748 };
1749 let wait_fut = async {
1750 tokio::select! {
1751 status = child.wait() => {
1752 status.map(|s| s.code().unwrap_or(-1)).unwrap_or(-1)
1753 }
1754 _ = &mut kill_rx => {
1755 #[allow(clippy::let_underscore_must_use)]
1759 let _ = child.start_kill();
1760 child
1761 .wait()
1762 .await
1763 .map(|s| s.code().unwrap_or(-1))
1764 .unwrap_or(-1)
1765 }
1766 }
1767 };
1768 let (stdout, stderr, exit_code) = tokio::join!(stdout_fut, stderr_fut, wait_fut);
1769
1770 #[allow(clippy::let_underscore_must_use)]
1771 let _ = sender.send(AsyncMessage::PluginProcessOutput {
1772 process_id,
1773 stdout,
1774 stderr,
1775 exit_code,
1776 });
1777 });
1778 } else {
1779 self.plugin_manager
1780 .reject_callback(callback_id, "Async runtime not available".to_string());
1781 }
1782 }
1783
1784 fn handle_spawn_background_process(
1785 &mut self,
1786 process_id: u64,
1787 command: String,
1788 args: Vec<String>,
1789 cwd: Option<String>,
1790 callback_id: JsCallbackId,
1791 ) {
1792 if let (Some(runtime), Some(bridge)) = (&self.tokio_runtime, &self.async_bridge) {
1794 use tokio::io::{AsyncBufReadExt, BufReader};
1795 use tokio::process::Command as TokioCommand;
1796
1797 let effective_cwd = cwd.unwrap_or_else(|| {
1798 std::env::current_dir()
1799 .map(|p| p.to_string_lossy().to_string())
1800 .unwrap_or_else(|_| ".".to_string())
1801 });
1802
1803 let sender = bridge.sender();
1804 let sender_stdout = sender.clone();
1805 let sender_stderr = sender.clone();
1806 let callback_id_u64 = callback_id.as_u64();
1807
1808 #[allow(clippy::let_underscore_must_use)]
1810 let handle = runtime.spawn(async move {
1811 use crate::services::process_hidden::HideWindow;
1812 let mut child = match TokioCommand::new(&command)
1813 .args(&args)
1814 .current_dir(&effective_cwd)
1815 .stdout(std::process::Stdio::piped())
1816 .stderr(std::process::Stdio::piped())
1817 .hide_window()
1818 .spawn()
1819 {
1820 Ok(child) => child,
1821 Err(e) => {
1822 let _ = sender.send(crate::services::async_bridge::AsyncMessage::Plugin(
1823 fresh_core::api::PluginAsyncMessage::ProcessExit {
1824 process_id,
1825 callback_id: callback_id_u64,
1826 exit_code: -1,
1827 },
1828 ));
1829 tracing::error!("Failed to spawn background process: {}", e);
1830 return;
1831 }
1832 };
1833
1834 let stdout = child.stdout.take();
1836 let stderr = child.stderr.take();
1837 let pid = process_id;
1838
1839 if let Some(stdout) = stdout {
1841 let sender = sender_stdout;
1842 tokio::spawn(async move {
1843 let reader = BufReader::new(stdout);
1844 let mut lines = reader.lines();
1845 while let Ok(Some(line)) = lines.next_line().await {
1846 let _ =
1847 sender.send(crate::services::async_bridge::AsyncMessage::Plugin(
1848 fresh_core::api::PluginAsyncMessage::ProcessStdout {
1849 process_id: pid,
1850 data: line + "\n",
1851 },
1852 ));
1853 }
1854 });
1855 }
1856
1857 if let Some(stderr) = stderr {
1859 let sender = sender_stderr;
1860 tokio::spawn(async move {
1861 let reader = BufReader::new(stderr);
1862 let mut lines = reader.lines();
1863 while let Ok(Some(line)) = lines.next_line().await {
1864 let _ =
1865 sender.send(crate::services::async_bridge::AsyncMessage::Plugin(
1866 fresh_core::api::PluginAsyncMessage::ProcessStderr {
1867 process_id: pid,
1868 data: line + "\n",
1869 },
1870 ));
1871 }
1872 });
1873 }
1874
1875 let exit_code = match child.wait().await {
1877 Ok(status) => status.code().unwrap_or(-1),
1878 Err(_) => -1,
1879 };
1880
1881 let _ = sender.send(crate::services::async_bridge::AsyncMessage::Plugin(
1882 fresh_core::api::PluginAsyncMessage::ProcessExit {
1883 process_id,
1884 callback_id: callback_id_u64,
1885 exit_code,
1886 },
1887 ));
1888 });
1889
1890 self.background_process_handles
1892 .insert(process_id, handle.abort_handle());
1893 } else {
1894 self.plugin_manager
1896 .reject_callback(callback_id, "Async runtime not available".to_string());
1897 }
1898 }
1899
1900 fn handle_create_virtual_buffer_with_content(
1901 &mut self,
1902 name: String,
1903 mode: String,
1904 read_only: bool,
1905 entries: Vec<fresh_core::text_property::TextPropertyEntry>,
1906 show_line_numbers: bool,
1907 show_cursors: bool,
1908 editing_disabled: bool,
1909 hidden_from_tabs: bool,
1910 request_id: Option<u64>,
1911 ) {
1912 let buffer_id = self.create_virtual_buffer(name.clone(), mode.clone(), read_only);
1913 tracing::info!(
1914 "Created virtual buffer '{}' with mode '{}' (id={:?})",
1915 name,
1916 mode,
1917 buffer_id
1918 );
1919
1920 if let Some(state) = self.buffers.get_mut(&buffer_id) {
1927 state.margins.configure_for_line_numbers(show_line_numbers);
1928 state.show_cursors = show_cursors;
1929 state.editing_disabled = editing_disabled;
1930 tracing::debug!(
1931 "Set buffer {:?} view options: show_line_numbers={}, show_cursors={}, editing_disabled={}",
1932 buffer_id,
1933 show_line_numbers,
1934 show_cursors,
1935 editing_disabled
1936 );
1937 }
1938 let active_split = self.split_manager.active_split();
1939 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
1940 view_state.ensure_buffer_state(buffer_id).show_line_numbers = show_line_numbers;
1941 }
1942
1943 if hidden_from_tabs {
1945 if let Some(meta) = self.buffer_metadata.get_mut(&buffer_id) {
1946 meta.hidden_from_tabs = true;
1947 }
1948 }
1949
1950 match self.set_virtual_buffer_content(buffer_id, entries) {
1952 Ok(()) => {
1953 tracing::debug!("Set virtual buffer content for {:?}", buffer_id);
1954 self.set_active_buffer(buffer_id);
1956 tracing::debug!("Switched to virtual buffer {:?}", buffer_id);
1957
1958 if let Some(req_id) = request_id {
1960 tracing::info!(
1961 "CreateVirtualBufferWithContent: resolving callback for request_id={}, buffer_id={:?}",
1962 req_id,
1963 buffer_id
1964 );
1965 let result = fresh_core::api::VirtualBufferResult {
1967 buffer_id: buffer_id.0 as u64,
1968 split_id: None,
1969 };
1970 self.plugin_manager.resolve_callback(
1971 fresh_core::api::JsCallbackId::from(req_id),
1972 serde_json::to_string(&result).unwrap_or_default(),
1973 );
1974 tracing::info!(
1975 "CreateVirtualBufferWithContent: resolve_callback sent for request_id={}",
1976 req_id
1977 );
1978 }
1979 }
1980 Err(e) => {
1981 tracing::error!("Failed to set virtual buffer content: {}", e);
1982 }
1983 }
1984 }
1985
1986 fn handle_create_virtual_buffer_in_split(
1987 &mut self,
1988 name: String,
1989 mode: String,
1990 read_only: bool,
1991 entries: Vec<fresh_core::text_property::TextPropertyEntry>,
1992 ratio: f32,
1993 direction: Option<String>,
1994 panel_id: Option<String>,
1995 show_line_numbers: bool,
1996 show_cursors: bool,
1997 editing_disabled: bool,
1998 line_wrap: Option<bool>,
1999 before: bool,
2000 role: Option<String>,
2001 request_id: Option<u64>,
2002 ) {
2003 let split_role: Option<crate::view::split::SplitRole> = match role.as_deref() {
2006 Some("utility_dock") => Some(crate::view::split::SplitRole::UtilityDock),
2007 _ => None,
2008 };
2009
2010 if let Some(target_role) = split_role {
2016 if let Some(dock_leaf) = self.split_manager.find_leaf_by_role(target_role) {
2017 let source_split_before_create = self.split_manager.active_split();
2022 let buffer_id = self.create_virtual_buffer(name.clone(), mode.clone(), read_only);
2023 if let Some(state) = self.buffers.get_mut(&buffer_id) {
2024 state.margins.configure_for_line_numbers(show_line_numbers);
2025 state.show_cursors = show_cursors;
2026 state.editing_disabled = editing_disabled;
2027 }
2028 if let Some(pid) = &panel_id {
2029 self.panel_ids.insert(pid.clone(), buffer_id);
2030 }
2031 if let Err(e) = self.set_virtual_buffer_content(buffer_id, entries) {
2032 tracing::error!("Failed to set virtual buffer content (dock route): {}", e);
2033 return;
2034 }
2035
2036 self.split_manager.set_active_split(dock_leaf);
2040 self.set_pane_buffer(dock_leaf, buffer_id);
2041
2042 if dock_leaf != source_split_before_create {
2044 if let Some(source_view_state) =
2045 self.split_view_states.get_mut(&source_split_before_create)
2046 {
2047 source_view_state.remove_buffer(buffer_id);
2048 }
2049 }
2050
2051 if let Some(req_id) = request_id {
2052 let result = fresh_core::api::VirtualBufferResult {
2053 buffer_id: buffer_id.0 as u64,
2054 split_id: Some(dock_leaf.0 .0 as u64),
2055 };
2056 self.plugin_manager.resolve_callback(
2057 fresh_core::api::JsCallbackId::from(req_id),
2058 serde_json::to_string(&result).unwrap_or_default(),
2059 );
2060 }
2061 tracing::info!(
2062 "Routed virtual buffer '{}' into existing utility dock {:?}",
2063 name,
2064 dock_leaf
2065 );
2066 return;
2067 }
2068 }
2071
2072 if let Some(pid) = &panel_id {
2074 if let Some(&existing_buffer_id) = self.panel_ids.get(pid) {
2075 if self.buffers.contains_key(&existing_buffer_id) {
2077 if let Err(e) = self.set_virtual_buffer_content(existing_buffer_id, entries) {
2079 tracing::error!("Failed to update panel content: {}", e);
2080 } else {
2081 tracing::info!("Updated existing panel '{}' content", pid);
2082 }
2083
2084 let splits = self.split_manager.splits_for_buffer(existing_buffer_id);
2086 if let Some(&split_id) = splits.first() {
2087 self.split_manager.set_active_split(split_id);
2088 self.set_pane_buffer(split_id, existing_buffer_id);
2091 tracing::debug!("Focused split {:?} containing panel buffer", split_id);
2092 }
2093
2094 if let Some(req_id) = request_id {
2096 let result = fresh_core::api::VirtualBufferResult {
2097 buffer_id: existing_buffer_id.0 as u64,
2098 split_id: splits.first().map(|s| s.0 .0 as u64),
2099 };
2100 self.plugin_manager.resolve_callback(
2101 fresh_core::api::JsCallbackId::from(req_id),
2102 serde_json::to_string(&result).unwrap_or_default(),
2103 );
2104 }
2105 return;
2106 } else {
2107 tracing::warn!(
2109 "Removing stale panel_id '{}' pointing to non-existent buffer {:?}",
2110 pid,
2111 existing_buffer_id
2112 );
2113 self.panel_ids.remove(pid);
2114 }
2116 }
2117 }
2118
2119 let source_split_before_create = self.split_manager.active_split();
2125
2126 let buffer_id = self.create_virtual_buffer(name.clone(), mode.clone(), read_only);
2128 tracing::info!(
2129 "Created virtual buffer '{}' with mode '{}' in split (id={:?})",
2130 name,
2131 mode,
2132 buffer_id
2133 );
2134
2135 if let Some(state) = self.buffers.get_mut(&buffer_id) {
2137 state.margins.configure_for_line_numbers(show_line_numbers);
2138 state.show_cursors = show_cursors;
2139 state.editing_disabled = editing_disabled;
2140 tracing::debug!(
2141 "Set buffer {:?} view options: show_line_numbers={}, show_cursors={}, editing_disabled={}",
2142 buffer_id,
2143 show_line_numbers,
2144 show_cursors,
2145 editing_disabled
2146 );
2147 }
2148
2149 if let Some(pid) = panel_id {
2151 self.panel_ids.insert(pid, buffer_id);
2152 }
2153
2154 if let Err(e) = self.set_virtual_buffer_content(buffer_id, entries) {
2156 tracing::error!("Failed to set virtual buffer content: {}", e);
2157 return;
2158 }
2159
2160 let split_dir = match direction.as_deref() {
2162 Some("vertical") => crate::model::event::SplitDirection::Vertical,
2163 _ => crate::model::event::SplitDirection::Horizontal,
2164 };
2165
2166 let created_split_id =
2172 match if split_role == Some(crate::view::split::SplitRole::UtilityDock) {
2173 self.split_manager
2174 .split_root_positioned(split_dir, buffer_id, ratio, before)
2175 } else {
2176 self.split_manager
2177 .split_active_positioned(split_dir, buffer_id, ratio, before)
2178 } {
2179 Ok(new_split_id) => {
2180 if new_split_id != source_split_before_create {
2186 if let Some(source_view_state) =
2187 self.split_view_states.get_mut(&source_split_before_create)
2188 {
2189 source_view_state.remove_buffer(buffer_id);
2190 }
2191 }
2192 let mut view_state = SplitViewState::with_buffer(
2194 self.terminal_width,
2195 self.terminal_height,
2196 buffer_id,
2197 );
2198 view_state.apply_config_defaults(
2199 self.config.editor.line_numbers,
2200 self.config.editor.highlight_current_line,
2201 line_wrap.unwrap_or_else(|| self.resolve_line_wrap_for_buffer(buffer_id)),
2202 self.config.editor.wrap_indent,
2203 self.resolve_wrap_column_for_buffer(buffer_id),
2204 self.config.editor.rulers.clone(),
2205 );
2206 view_state.ensure_buffer_state(buffer_id).show_line_numbers = show_line_numbers;
2208 self.split_view_states.insert(new_split_id, view_state);
2209
2210 self.split_manager.set_active_split(new_split_id);
2212 if let Some(target_role) = split_role {
2220 self.split_manager.clear_role(target_role);
2221 self.split_manager
2222 .set_leaf_role(new_split_id, Some(target_role));
2223 tracing::info!(
2224 "Tagged new dock leaf {:?} with role {:?}",
2225 new_split_id,
2226 target_role
2227 );
2228 }
2229
2230 tracing::info!(
2231 "Created {:?} split with virtual buffer {:?}",
2232 split_dir,
2233 buffer_id
2234 );
2235 Some(new_split_id)
2236 }
2237 Err(e) => {
2238 tracing::error!("Failed to create split: {}", e);
2239 self.set_active_buffer(buffer_id);
2241 None
2242 }
2243 };
2244
2245 if let Some(req_id) = request_id {
2248 tracing::trace!("CreateVirtualBufferInSplit: resolving callback for request_id={}, buffer_id={:?}, split_id={:?}", req_id, buffer_id, created_split_id);
2249 let result = fresh_core::api::VirtualBufferResult {
2250 buffer_id: buffer_id.0 as u64,
2251 split_id: created_split_id.map(|s| s.0 .0 as u64),
2252 };
2253 self.plugin_manager.resolve_callback(
2254 fresh_core::api::JsCallbackId::from(req_id),
2255 serde_json::to_string(&result).unwrap_or_default(),
2256 );
2257 }
2258 }
2259
2260 fn handle_create_virtual_buffer_in_existing_split(
2261 &mut self,
2262 name: String,
2263 mode: String,
2264 read_only: bool,
2265 entries: Vec<fresh_core::text_property::TextPropertyEntry>,
2266 split_id: SplitId,
2267 show_line_numbers: bool,
2268 show_cursors: bool,
2269 editing_disabled: bool,
2270 line_wrap: Option<bool>,
2271 request_id: Option<u64>,
2272 ) {
2273 let buffer_id = self.create_virtual_buffer(name.clone(), mode.clone(), read_only);
2275 tracing::info!(
2276 "Created virtual buffer '{}' with mode '{}' for existing split {:?} (id={:?})",
2277 name,
2278 mode,
2279 split_id,
2280 buffer_id
2281 );
2282
2283 if let Some(state) = self.buffers.get_mut(&buffer_id) {
2285 state.margins.configure_for_line_numbers(show_line_numbers);
2286 state.show_cursors = show_cursors;
2287 state.editing_disabled = editing_disabled;
2288 }
2289
2290 if let Err(e) = self.set_virtual_buffer_content(buffer_id, entries) {
2292 tracing::error!("Failed to set virtual buffer content: {}", e);
2293 return;
2294 }
2295
2296 let leaf_id = LeafId(split_id);
2299 self.split_manager.set_active_split(leaf_id);
2300 self.set_pane_buffer(leaf_id, buffer_id);
2301
2302 if let Some(view_state) = self.split_view_states.get_mut(&leaf_id) {
2308 view_state.switch_buffer(buffer_id);
2309 view_state.add_buffer(buffer_id);
2310 view_state.ensure_buffer_state(buffer_id).show_line_numbers = show_line_numbers;
2311
2312 if let Some(wrap) = line_wrap {
2314 view_state.active_state_mut().viewport.line_wrap_enabled = wrap;
2315 }
2316 }
2317
2318 tracing::info!(
2319 "Displayed virtual buffer {:?} in split {:?}",
2320 buffer_id,
2321 split_id
2322 );
2323
2324 if let Some(req_id) = request_id {
2326 let result = fresh_core::api::VirtualBufferResult {
2327 buffer_id: buffer_id.0 as u64,
2328 split_id: Some(split_id.0 as u64),
2329 };
2330 self.plugin_manager.resolve_callback(
2331 fresh_core::api::JsCallbackId::from(req_id),
2332 serde_json::to_string(&result).unwrap_or_default(),
2333 );
2334 }
2335 }
2336
2337 fn handle_show_action_popup(
2338 &mut self,
2339 popup_id: String,
2340 title: String,
2341 message: String,
2342 actions: Vec<fresh_core::api::ActionPopupAction>,
2343 ) {
2344 tracing::info!(
2345 "Action popup requested: id={}, title={}, actions={}",
2346 popup_id,
2347 title,
2348 actions.len()
2349 );
2350
2351 let items: Vec<crate::model::event::PopupListItemData> = actions
2353 .iter()
2354 .map(|action| crate::model::event::PopupListItemData {
2355 text: action.label.clone(),
2356 detail: None,
2357 icon: None,
2358 data: Some(action.id.clone()),
2359 })
2360 .collect();
2361
2362 drop(actions);
2367
2368 let popup_data = crate::model::event::PopupData {
2370 kind: crate::model::event::PopupKindHint::List,
2371 title: Some(title),
2372 description: Some(message),
2373 transient: false,
2374 content: crate::model::event::PopupContentData::List { items, selected: 0 },
2375 position: crate::model::event::PopupPositionData::BottomRight,
2376 width: 60,
2377 max_height: 15,
2378 bordered: true,
2379 };
2380
2381 let mut popup_obj = crate::state::convert_popup_data_to_popup(&popup_data);
2391 popup_obj.resolver = crate::view::popup::PopupResolver::PluginAction {
2392 popup_id: popup_id.clone(),
2393 };
2394 self.global_popups.show(popup_obj);
2395 tracing::info!(
2396 "Action popup shown: id={}, stack_depth={}",
2397 popup_id,
2398 self.global_popups.all().len()
2399 );
2400 }
2401
2402 fn handle_create_terminal(
2403 &mut self,
2404 cwd: Option<String>,
2405 direction: Option<String>,
2406 ratio: Option<f32>,
2407 focus: Option<bool>,
2408 persistent: bool,
2409 request_id: u64,
2410 ) {
2411 let (cols, rows) = self.get_terminal_dimensions();
2412
2413 if let Some(ref bridge) = self.async_bridge {
2415 self.terminal_manager.set_async_bridge(bridge.clone());
2416 }
2417
2418 let working_dir = cwd
2420 .map(std::path::PathBuf::from)
2421 .unwrap_or_else(|| self.working_dir.clone());
2422
2423 let terminal_root = self.dir_context.terminal_dir_for(&working_dir);
2425 if let Err(e) = self.authority.filesystem.create_dir_all(&terminal_root) {
2426 tracing::warn!("Failed to create terminal directory: {}", e);
2427 }
2428 let predicted_terminal_id = self.terminal_manager.next_terminal_id();
2429 let name_stem = if persistent {
2436 format!("fresh-terminal-{}", predicted_terminal_id.0)
2437 } else {
2438 let nanos = std::time::SystemTime::now()
2439 .duration_since(std::time::UNIX_EPOCH)
2440 .map(|d| d.as_nanos())
2441 .unwrap_or(0);
2442 format!("fresh-terminal-eph-{}-{}", predicted_terminal_id.0, nanos)
2443 };
2444 let log_path = terminal_root.join(format!("{}.log", name_stem));
2445 let backing_path = terminal_root.join(format!("{}.txt", name_stem));
2446 self.terminal_backing_files
2447 .insert(predicted_terminal_id, backing_path);
2448 let backing_path_for_spawn = self
2449 .terminal_backing_files
2450 .get(&predicted_terminal_id)
2451 .cloned();
2452
2453 match self.terminal_manager.spawn(
2454 cols,
2455 rows,
2456 Some(working_dir),
2457 Some(log_path.clone()),
2458 backing_path_for_spawn,
2459 self.resolved_terminal_wrapper(),
2460 ) {
2461 Ok(terminal_id) => {
2462 self.terminal_log_files
2464 .insert(terminal_id, log_path.clone());
2465 if terminal_id != predicted_terminal_id {
2473 let existing = self.terminal_backing_files.remove(&predicted_terminal_id);
2474 let fixed_backing = if persistent {
2475 terminal_root.join(format!("fresh-terminal-{}.txt", terminal_id.0))
2476 } else {
2477 existing.unwrap_or_else(|| terminal_root.join(format!("{}.txt", name_stem)))
2478 };
2479 self.terminal_backing_files
2480 .insert(terminal_id, fixed_backing);
2481 }
2482 if !persistent {
2483 self.ephemeral_terminals.insert(terminal_id);
2484 }
2485
2486 let active_split = self.split_manager.active_split();
2500 let buffer_id = if direction.is_some() {
2501 self.create_terminal_buffer_detached(terminal_id)
2502 } else {
2503 self.create_terminal_buffer_attached(terminal_id, active_split)
2504 };
2505
2506 let created_split_id = if let Some(dir_str) = direction.as_deref() {
2507 let split_dir = match dir_str {
2508 "horizontal" => crate::model::event::SplitDirection::Horizontal,
2509 _ => crate::model::event::SplitDirection::Vertical,
2510 };
2511
2512 let split_ratio = ratio.unwrap_or(0.5);
2513 match self
2514 .split_manager
2515 .split_active(split_dir, buffer_id, split_ratio)
2516 {
2517 Ok(new_split_id) => {
2518 let mut view_state = SplitViewState::with_buffer(
2519 self.terminal_width,
2520 self.terminal_height,
2521 buffer_id,
2522 );
2523 view_state.apply_config_defaults(
2524 self.config.editor.line_numbers,
2525 self.config.editor.highlight_current_line,
2526 false,
2527 false,
2528 None,
2529 self.config.editor.rulers.clone(),
2530 );
2531 view_state.viewport.line_wrap_enabled = false;
2535 self.split_view_states.insert(new_split_id, view_state);
2536
2537 if focus.unwrap_or(true) {
2538 self.split_manager.set_active_split(new_split_id);
2539 }
2540
2541 tracing::info!(
2542 "Created {:?} split for terminal {:?} with buffer {:?}",
2543 split_dir,
2544 terminal_id,
2545 buffer_id
2546 );
2547 Some(new_split_id)
2548 }
2549 Err(e) => {
2550 tracing::error!(
2551 "Failed to create split for terminal: {}; \
2552 falling back to active split",
2553 e
2554 );
2555 if let Some(view_state) = self.split_view_states.get_mut(&active_split)
2560 {
2561 view_state.add_buffer(buffer_id);
2562 view_state.viewport.line_wrap_enabled = false;
2563 }
2564 self.set_active_buffer(buffer_id);
2565 None
2566 }
2567 }
2568 } else {
2569 self.set_active_buffer(buffer_id);
2571 None
2572 };
2573
2574 self.resize_visible_terminals();
2576
2577 let result = fresh_core::api::TerminalResult {
2579 buffer_id: buffer_id.0 as u64,
2580 terminal_id: terminal_id.0 as u64,
2581 split_id: created_split_id.map(|s| s.0 .0 as u64),
2582 };
2583 self.plugin_manager.resolve_callback(
2584 fresh_core::api::JsCallbackId::from(request_id),
2585 serde_json::to_string(&result).unwrap_or_default(),
2586 );
2587
2588 tracing::info!(
2589 "Plugin created terminal {:?} with buffer {:?}",
2590 terminal_id,
2591 buffer_id
2592 );
2593 }
2594 Err(e) => {
2595 tracing::error!("Failed to create terminal for plugin: {}", e);
2596 self.plugin_manager.reject_callback(
2597 fresh_core::api::JsCallbackId::from(request_id),
2598 format!("Failed to create terminal: {}", e),
2599 );
2600 }
2601 }
2602 }
2603 fn handle_get_split_by_label(&mut self, label: String, request_id: u64) {
2606 let split_id = self.split_manager.find_split_by_label(&label);
2607 let callback_id = fresh_core::api::JsCallbackId::from(request_id);
2608 let json =
2609 serde_json::to_string(&split_id.map(|s| s.0 .0)).unwrap_or_else(|_| "null".to_string());
2610 self.plugin_manager.resolve_callback(callback_id, json);
2611 }
2612
2613 fn handle_set_buffer_show_cursors(&mut self, buffer_id: BufferId, show: bool) {
2614 if let Some(state) = self.buffers.get_mut(&buffer_id) {
2615 state.show_cursors = show;
2616 } else {
2617 tracing::warn!("SetBufferShowCursors: buffer {:?} not found", buffer_id);
2618 }
2619 }
2620
2621 fn handle_override_theme_colors(
2622 &mut self,
2623 overrides: std::collections::HashMap<String, [u8; 3]>,
2624 ) {
2625 let pairs = overrides
2626 .into_iter()
2627 .map(|(k, [r, g, b])| (k, ratatui::style::Color::Rgb(r, g, b)));
2628 let applied = self.theme.override_colors(pairs);
2629 if applied > 0 {
2630 self.reapply_all_overlays();
2633 }
2634 }
2635
2636 fn handle_await_next_key(&mut self, callback_id: fresh_core::api::JsCallbackId) {
2637 if let Some(payload) = self.pending_key_capture_buffer.pop_front() {
2641 let json = serde_json::to_string(&payload).unwrap_or_else(|_| "null".to_string());
2642 self.plugin_manager.resolve_callback(callback_id, json);
2643 } else {
2644 self.pending_next_key_callbacks.push_back(callback_id);
2645 }
2646 }
2647
2648 fn handle_spawn_process(
2649 &mut self,
2650 command: String,
2651 args: Vec<String>,
2652 cwd: Option<String>,
2653 callback_id: fresh_core::api::JsCallbackId,
2654 ) {
2655 if let (Some(runtime), Some(bridge)) = (&self.tokio_runtime, &self.async_bridge) {
2656 let effective_cwd = cwd.or_else(|| {
2657 std::env::current_dir()
2658 .map(|p| p.to_string_lossy().to_string())
2659 .ok()
2660 });
2661 let sender = bridge.sender();
2662 let spawner = self.authority.process_spawner.clone();
2663 runtime.spawn(async move {
2664 #[allow(clippy::let_underscore_must_use)]
2665 match spawner.spawn(command, args, effective_cwd).await {
2666 Ok(result) => {
2667 let _ = sender.send(AsyncMessage::PluginProcessOutput {
2668 process_id: callback_id.as_u64(),
2669 stdout: result.stdout,
2670 stderr: result.stderr,
2671 exit_code: result.exit_code,
2672 });
2673 }
2674 Err(e) => {
2675 let _ = sender.send(AsyncMessage::PluginProcessOutput {
2676 process_id: callback_id.as_u64(),
2677 stdout: String::new(),
2678 stderr: e.to_string(),
2679 exit_code: -1,
2680 });
2681 }
2682 }
2683 });
2684 } else {
2685 self.plugin_manager
2686 .reject_callback(callback_id, "Async runtime not available".to_string());
2687 }
2688 }
2689
2690 fn handle_kill_host_process(&mut self, process_id: u64) {
2691 if let Some(tx) = self.host_process_handles.remove(&process_id) {
2695 #[allow(clippy::let_underscore_must_use)]
2696 let _ = tx.send(());
2697 tracing::debug!("KillHostProcess: sent kill for process_id={}", process_id);
2698 } else {
2699 tracing::debug!(
2700 "KillHostProcess: unknown process_id={} (already exited?)",
2701 process_id
2702 );
2703 }
2704 }
2705
2706 fn handle_set_authority(&mut self, payload: serde_json::Value) {
2707 match serde_json::from_value::<crate::services::authority::AuthorityPayload>(payload) {
2710 Ok(parsed) => {
2711 match crate::services::authority::Authority::from_plugin_payload(parsed) {
2712 Ok(auth) => {
2713 tracing::info!("Plugin installed new authority");
2714 self.install_authority(auth);
2715 }
2716 Err(e) => {
2717 tracing::warn!("setAuthority: invalid payload: {}", e);
2718 self.set_status_message(format!("setAuthority rejected: {}", e));
2719 }
2720 }
2721 }
2722 Err(e) => {
2723 tracing::warn!("setAuthority: failed to parse payload: {}", e);
2724 self.set_status_message(format!("setAuthority rejected: {}", e));
2725 }
2726 }
2727 }
2728
2729 fn handle_set_remote_indicator_state(&mut self, state: serde_json::Value) {
2730 match serde_json::from_value::<crate::view::ui::status_bar::RemoteIndicatorOverride>(state)
2733 {
2734 Ok(over) => {
2735 self.remote_indicator_override = Some(over);
2736 }
2737 Err(e) => {
2738 tracing::warn!("setRemoteIndicatorState: invalid payload: {}", e);
2739 self.set_status_message(format!("setRemoteIndicatorState rejected: {}", e));
2740 }
2741 }
2742 }
2743
2744 fn handle_spawn_process_wait(
2745 &mut self,
2746 process_id: u64,
2747 callback_id: fresh_core::api::JsCallbackId,
2748 ) {
2749 tracing::warn!(
2750 "SpawnProcessWait not fully implemented - process_id={}",
2751 process_id
2752 );
2753 self.plugin_manager.reject_callback(
2754 callback_id,
2755 format!(
2756 "SpawnProcessWait not yet fully implemented for process_id={}",
2757 process_id
2758 ),
2759 );
2760 }
2761
2762 fn handle_delay(&mut self, callback_id: fresh_core::api::JsCallbackId, duration_ms: u64) {
2763 if let (Some(runtime), Some(bridge)) = (&self.tokio_runtime, &self.async_bridge) {
2764 let sender = bridge.sender();
2765 let callback_id_u64 = callback_id.as_u64();
2766 runtime.spawn(async move {
2767 tokio::time::sleep(tokio::time::Duration::from_millis(duration_ms)).await;
2768 #[allow(clippy::let_underscore_must_use)]
2769 let _ = sender.send(crate::services::async_bridge::AsyncMessage::Plugin(
2770 fresh_core::api::PluginAsyncMessage::DelayComplete {
2771 callback_id: callback_id_u64,
2772 },
2773 ));
2774 });
2775 } else {
2776 std::thread::sleep(std::time::Duration::from_millis(duration_ms));
2777 self.plugin_manager
2778 .resolve_callback(callback_id, "null".to_string());
2779 }
2780 }
2781
2782 fn handle_kill_background_process(&mut self, process_id: u64) {
2783 if let Some(handle) = self.background_process_handles.remove(&process_id) {
2784 handle.abort();
2785 tracing::debug!("Killed background process {}", process_id);
2786 }
2787 }
2788
2789 fn handle_create_virtual_buffer(&mut self, name: String, mode: String, read_only: bool) {
2790 let buffer_id = self.create_virtual_buffer(name.clone(), mode.clone(), read_only);
2791 tracing::info!(
2792 "Created virtual buffer '{}' with mode '{}' (id={:?})",
2793 name,
2794 mode,
2795 buffer_id
2796 );
2797 }
2799
2800 fn handle_set_virtual_buffer_content(
2801 &mut self,
2802 buffer_id: BufferId,
2803 entries: Vec<fresh_core::text_property::TextPropertyEntry>,
2804 ) {
2805 match self.set_virtual_buffer_content(buffer_id, entries) {
2806 Ok(()) => {
2807 tracing::debug!("Set virtual buffer content for {:?}", buffer_id);
2808 }
2809 Err(e) => {
2810 tracing::error!("Failed to set virtual buffer content: {}", e);
2811 }
2812 }
2813 }
2814
2815 fn handle_get_text_properties_at_cursor(&self, buffer_id: BufferId) {
2816 if let Some(state) = self.buffers.get(&buffer_id) {
2817 let cursor_pos = self
2818 .split_view_states
2819 .values()
2820 .find_map(|vs| vs.buffer_state(buffer_id))
2821 .map(|bs| bs.cursors.primary().position)
2822 .unwrap_or(0);
2823 let properties = state.text_properties.get_at(cursor_pos);
2824 tracing::debug!(
2825 "Text properties at cursor in {:?}: {} properties found",
2826 buffer_id,
2827 properties.len()
2828 );
2829 }
2831 }
2832
2833 fn handle_set_context(&mut self, name: String, active: bool) {
2834 if active {
2835 self.active_custom_contexts.insert(name.clone());
2836 tracing::debug!("Set custom context: {}", name);
2837 } else {
2838 self.active_custom_contexts.remove(&name);
2839 tracing::debug!("Unset custom context: {}", name);
2840 }
2841 }
2842
2843 fn handle_disable_lsp_for_language(&mut self, language: String) {
2844 tracing::info!("Disabling LSP for language: {}", language);
2845 if let Some(ref mut lsp) = self.lsp {
2846 lsp.shutdown_server(&language);
2847 tracing::info!("Stopped LSP server for {}", language);
2848 }
2849 if let Some(lsp_configs) = self.config_mut().lsp.get_mut(&language) {
2850 for c in lsp_configs.as_mut_slice() {
2851 c.enabled = false;
2852 c.auto_start = false;
2853 }
2854 tracing::info!("Disabled LSP config for {}", language);
2855 }
2856 if let Err(e) = self.save_config() {
2857 tracing::error!("Failed to save config: {}", e);
2858 self.status_message = Some(format!(
2859 "LSP disabled for {} (config save failed)",
2860 language
2861 ));
2862 } else {
2863 self.status_message = Some(format!("LSP disabled for {}", language));
2864 }
2865 self.warning_domains.lsp.clear();
2866 }
2867
2868 fn handle_restart_lsp_for_language(&mut self, language: String) {
2869 tracing::info!("Plugin restarting LSP for language: {}", language);
2870 let file_path = self
2871 .buffer_metadata
2872 .get(&self.active_buffer())
2873 .and_then(|meta| meta.file_path().cloned());
2874 let success = if let Some(ref mut lsp) = self.lsp {
2875 let (ok, msg) = lsp.manual_restart(&language, file_path.as_deref());
2876 self.status_message = Some(msg);
2877 ok
2878 } else {
2879 self.status_message = Some("No LSP manager available".to_string());
2880 false
2881 };
2882 if success {
2883 self.reopen_buffers_for_language(&language);
2884 }
2885 }
2886
2887 fn handle_set_lsp_root_uri(&mut self, language: String, uri: String) {
2888 tracing::info!("Plugin setting LSP root URI for {}: {}", language, uri);
2889 match uri.parse::<lsp_types::Uri>() {
2890 Ok(parsed_uri) => {
2891 if let Some(ref mut lsp) = self.lsp {
2892 let restarted = lsp.set_language_root_uri(&language, parsed_uri);
2893 if restarted {
2894 self.status_message = Some(format!(
2895 "LSP root updated for {} (restarting server)",
2896 language
2897 ));
2898 } else {
2899 self.status_message = Some(format!("LSP root set for {}", language));
2900 }
2901 }
2902 }
2903 Err(e) => {
2904 tracing::error!("Invalid LSP root URI '{}': {}", uri, e);
2905 self.status_message = Some(format!("Invalid LSP root URI: {}", e));
2906 }
2907 }
2908 }
2909
2910 fn handle_create_scroll_sync_group(
2911 &mut self,
2912 group_id: crate::view::scroll_sync::ScrollSyncGroupId,
2913 left_split: SplitId,
2914 right_split: SplitId,
2915 ) {
2916 let success =
2917 self.scroll_sync_manager
2918 .create_group_with_id(group_id, left_split, right_split);
2919 if success {
2920 tracing::debug!(
2921 "Created scroll sync group {} for splits {:?} and {:?}",
2922 group_id,
2923 left_split,
2924 right_split
2925 );
2926 } else {
2927 tracing::warn!(
2928 "Failed to create scroll sync group {} (ID already exists)",
2929 group_id
2930 );
2931 }
2932 }
2933
2934 fn handle_set_scroll_sync_anchors(
2935 &mut self,
2936 group_id: crate::view::scroll_sync::ScrollSyncGroupId,
2937 anchors: Vec<(usize, usize)>,
2938 ) {
2939 use crate::view::scroll_sync::SyncAnchor;
2940 let anchor_count = anchors.len();
2941 let sync_anchors: Vec<SyncAnchor> = anchors
2942 .into_iter()
2943 .map(|(left_line, right_line)| SyncAnchor {
2944 left_line,
2945 right_line,
2946 })
2947 .collect();
2948 self.scroll_sync_manager.set_anchors(group_id, sync_anchors);
2949 tracing::debug!(
2950 "Set {} anchors for scroll sync group {}",
2951 anchor_count,
2952 group_id
2953 );
2954 }
2955
2956 fn handle_remove_scroll_sync_group(
2957 &mut self,
2958 group_id: crate::view::scroll_sync::ScrollSyncGroupId,
2959 ) {
2960 if self.scroll_sync_manager.remove_group(group_id) {
2961 tracing::debug!("Removed scroll sync group {}", group_id);
2962 } else {
2963 tracing::warn!("Scroll sync group {} not found", group_id);
2964 }
2965 }
2966
2967 fn handle_create_buffer_group(
2968 &mut self,
2969 name: String,
2970 mode: String,
2971 layout_json: String,
2972 request_id: Option<u64>,
2973 ) {
2974 match self.create_buffer_group(name, mode, layout_json) {
2975 Ok(result) => {
2976 if let Some(req_id) = request_id {
2977 let json = serde_json::to_string(&result).unwrap_or_default();
2978 self.plugin_manager
2979 .resolve_callback(fresh_core::api::JsCallbackId::from(req_id), json);
2980 }
2981 }
2982 Err(e) => {
2983 tracing::error!("Failed to create buffer group: {}", e);
2984 }
2985 }
2986 }
2987
2988 fn handle_send_terminal_input(
2989 &mut self,
2990 terminal_id: crate::services::terminal::TerminalId,
2991 data: String,
2992 ) {
2993 if let Some(handle) = self.terminal_manager.get(terminal_id) {
2994 handle.write(data.as_bytes());
2995 tracing::trace!(
2996 "Plugin sent {} bytes to terminal {:?}",
2997 data.len(),
2998 terminal_id
2999 );
3000 } else {
3001 tracing::warn!(
3002 "Plugin tried to send input to non-existent terminal {:?}",
3003 terminal_id
3004 );
3005 }
3006 }
3007
3008 fn handle_close_terminal(&mut self, terminal_id: crate::services::terminal::TerminalId) {
3009 let buffer_to_close = self
3010 .terminal_buffers
3011 .iter()
3012 .find(|(_, &tid)| tid == terminal_id)
3013 .map(|(&bid, _)| bid);
3014 if let Some(buffer_id) = buffer_to_close {
3015 if let Err(e) = self.close_buffer(buffer_id) {
3016 tracing::warn!("Failed to close terminal buffer: {}", e);
3017 }
3018 tracing::info!("Plugin closed terminal {:?}", terminal_id);
3019 } else {
3020 self.terminal_manager.close(terminal_id);
3021 tracing::info!("Plugin closed terminal {:?} (no buffer found)", terminal_id);
3022 }
3023 }
3024}
3025
3026#[cfg(test)]
3027mod tests {
3028 use tokio::io::{AsyncReadExt, BufReader};
3041 use tokio::process::Command as TokioCommand;
3042 use tokio::time::{timeout, Duration};
3043
3044 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3055 async fn kill_via_oneshot_terminates_long_running_child() {
3056 let mut cmd = TokioCommand::new("sleep");
3057 cmd.args(["30"]);
3058 cmd.stdout(std::process::Stdio::piped());
3059 cmd.stderr(std::process::Stdio::piped());
3060
3061 let mut child = cmd.spawn().expect("spawn sh -c sleep 30");
3062 let pid = child.id().expect("child has a pid");
3063
3064 let (kill_tx, mut kill_rx) = tokio::sync::oneshot::channel::<()>();
3065 let stdout_pipe = child.stdout.take();
3066 let stderr_pipe = child.stderr.take();
3067
3068 let stdout_fut = async {
3069 let mut buf = String::new();
3070 if let Some(s) = stdout_pipe {
3071 #[allow(clippy::let_underscore_must_use)]
3072 let _ = BufReader::new(s).read_to_string(&mut buf).await;
3073 }
3074 buf
3075 };
3076 let stderr_fut = async {
3077 let mut buf = String::new();
3078 if let Some(s) = stderr_pipe {
3079 #[allow(clippy::let_underscore_must_use)]
3080 let _ = BufReader::new(s).read_to_string(&mut buf).await;
3081 }
3082 buf
3083 };
3084 let wait_fut = async {
3085 tokio::select! {
3086 status = child.wait() => {
3087 status.map(|s| s.code().unwrap_or(-1)).unwrap_or(-1)
3088 }
3089 _ = &mut kill_rx => {
3090 #[allow(clippy::let_underscore_must_use)]
3091 let _ = child.start_kill();
3092 child
3093 .wait()
3094 .await
3095 .map(|s| s.code().unwrap_or(-1))
3096 .unwrap_or(-1)
3097 }
3098 }
3099 };
3100
3101 tokio::time::sleep(Duration::from_millis(50)).await;
3106 kill_tx.send(()).expect("kill channel send");
3107
3108 let result = timeout(Duration::from_secs(5), async {
3109 tokio::join!(stdout_fut, stderr_fut, wait_fut)
3110 })
3111 .await;
3112
3113 let (_stdout, _stderr, exit_code) = result.expect(
3114 "kill path must resolve within 5s — if this times out the \
3115 select! arm order or kill-then-wait logic is broken",
3116 );
3117 assert_ne!(
3129 exit_code, 0,
3130 "killed child must exit non-success (got 0 — did the \
3131 kill arm fire too late, or did sleep somehow complete?)"
3132 );
3133
3134 #[cfg(unix)]
3143 {
3144 let still_alive = std::process::Command::new("kill")
3145 .args(["-0", &pid.to_string()])
3146 .status()
3147 .map(|s| s.success())
3148 .unwrap_or(false);
3149 assert!(
3150 !still_alive,
3151 "process {pid} must be reaped after wait() — a still-\
3152 alive check means the kill path leaked the child"
3153 );
3154 }
3155 #[cfg(not(unix))]
3156 {
3157 let _ = pid;
3160 }
3161 }
3162}