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 { label, prompt_type } => {
761 self.handle_start_prompt(label, prompt_type);
762 }
763 PluginCommand::StartPromptWithInitial {
764 label,
765 prompt_type,
766 initial_value,
767 } => {
768 self.handle_start_prompt_with_initial(label, prompt_type, initial_value);
769 }
770 PluginCommand::StartPromptAsync {
771 label,
772 initial_value,
773 callback_id,
774 } => {
775 self.handle_start_prompt_async(label, initial_value, callback_id);
776 }
777 PluginCommand::AwaitNextKey { callback_id } => {
778 self.handle_await_next_key(callback_id);
779 }
780 PluginCommand::SetKeyCaptureActive { active } => {
781 self.key_capture_active = active;
782 if !active {
783 self.pending_key_capture_buffer.clear();
787 }
788 }
789 PluginCommand::SetPromptSuggestions { suggestions } => {
790 self.handle_set_prompt_suggestions(suggestions);
791 }
792 PluginCommand::SetPromptInputSync { sync } => {
793 if let Some(prompt) = &mut self.prompt {
794 prompt.sync_input_on_navigate = sync;
795 }
796 }
797
798 PluginCommand::RegisterCommand { command } => {
800 self.handle_register_command(command);
801 }
802 PluginCommand::UnregisterCommand { name } => {
803 self.handle_unregister_command(name);
804 }
805 PluginCommand::DefineMode {
806 name,
807 bindings,
808 read_only,
809 allow_text_input,
810 inherit_normal_bindings,
811 plugin_name,
812 } => {
813 self.handle_define_mode(
814 name,
815 bindings,
816 read_only,
817 allow_text_input,
818 inherit_normal_bindings,
819 plugin_name,
820 );
821 }
822
823 PluginCommand::OpenFileInBackground { path } => {
825 self.handle_open_file_in_background(path);
826 }
827 PluginCommand::OpenFileAtLocation { path, line, column } => {
828 return self.handle_open_file_at_location(path, line, column);
829 }
830 PluginCommand::OpenFileInSplit {
831 split_id,
832 path,
833 line,
834 column,
835 } => {
836 return self.handle_open_file_in_split(split_id, path, line, column);
837 }
838 PluginCommand::ShowBuffer { buffer_id } => {
839 self.handle_show_buffer(buffer_id);
840 }
841 PluginCommand::CloseBuffer { buffer_id } => {
842 self.handle_close_buffer(buffer_id);
843 }
844
845 PluginCommand::StartAnimationArea { id, rect, kind } => {
847 self.handle_start_animation_area(id, rect, kind);
848 }
849 PluginCommand::StartAnimationVirtualBuffer {
850 id,
851 buffer_id,
852 kind,
853 } => {
854 self.handle_start_animation_virtual_buffer(id, buffer_id, kind);
855 }
856 PluginCommand::CancelAnimation { id } => {
857 self.animations
858 .cancel(crate::view::animation::AnimationId::from_raw(id));
859 }
860
861 PluginCommand::SendLspRequest {
863 language,
864 method,
865 params,
866 request_id,
867 } => {
868 self.handle_send_lsp_request(language, method, params, request_id);
869 }
870
871 PluginCommand::SetClipboard { text } => {
873 self.handle_set_clipboard(text);
874 }
875
876 PluginCommand::SpawnProcess {
878 command,
879 args,
880 cwd,
881 callback_id,
882 } => {
883 self.handle_spawn_process(command, args, cwd, callback_id);
884 }
885
886 PluginCommand::SpawnHostProcess {
887 command,
888 args,
889 cwd,
890 callback_id,
891 } => {
892 self.handle_spawn_host_process(command, args, cwd, callback_id);
893 }
894
895 PluginCommand::KillHostProcess { process_id } => {
896 self.handle_kill_host_process(process_id);
897 }
898
899 PluginCommand::SetAuthority { payload } => {
900 self.handle_set_authority(payload);
901 }
902
903 PluginCommand::ClearAuthority => {
904 tracing::info!("Plugin cleared authority; restoring local");
905 self.clear_authority();
906 }
907
908 PluginCommand::SetRemoteIndicatorState { state } => {
909 self.handle_set_remote_indicator_state(state);
910 }
911
912 PluginCommand::ClearRemoteIndicatorState => {
913 self.remote_indicator_override = None;
914 }
915
916 PluginCommand::SpawnProcessWait {
917 process_id,
918 callback_id,
919 } => {
920 self.handle_spawn_process_wait(process_id, callback_id);
921 }
922
923 PluginCommand::Delay {
924 callback_id,
925 duration_ms,
926 } => {
927 self.handle_delay(callback_id, duration_ms);
928 }
929
930 PluginCommand::SpawnBackgroundProcess {
931 process_id,
932 command,
933 args,
934 cwd,
935 callback_id,
936 } => {
937 self.handle_spawn_background_process(process_id, command, args, cwd, callback_id);
938 }
939
940 PluginCommand::KillBackgroundProcess { process_id } => {
941 self.handle_kill_background_process(process_id);
942 }
943
944 PluginCommand::CreateVirtualBuffer {
946 name,
947 mode,
948 read_only,
949 } => {
950 self.handle_create_virtual_buffer(name, mode, read_only);
951 }
952 PluginCommand::CreateVirtualBufferWithContent {
953 name,
954 mode,
955 read_only,
956 entries,
957 show_line_numbers,
958 show_cursors,
959 editing_disabled,
960 hidden_from_tabs,
961 request_id,
962 } => {
963 self.handle_create_virtual_buffer_with_content(
964 name,
965 mode,
966 read_only,
967 entries,
968 show_line_numbers,
969 show_cursors,
970 editing_disabled,
971 hidden_from_tabs,
972 request_id,
973 );
974 }
975 PluginCommand::CreateVirtualBufferInSplit {
976 name,
977 mode,
978 read_only,
979 entries,
980 ratio,
981 direction,
982 panel_id,
983 show_line_numbers,
984 show_cursors,
985 editing_disabled,
986 line_wrap,
987 before,
988 request_id,
989 } => {
990 self.handle_create_virtual_buffer_in_split(
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 request_id,
1004 );
1005 }
1006 PluginCommand::SetVirtualBufferContent { buffer_id, entries } => {
1007 self.handle_set_virtual_buffer_content(buffer_id, entries);
1008 }
1009 PluginCommand::GetTextPropertiesAtCursor { buffer_id } => {
1010 self.handle_get_text_properties_at_cursor(buffer_id);
1011 }
1012 PluginCommand::CreateVirtualBufferInExistingSplit {
1013 name,
1014 mode,
1015 read_only,
1016 entries,
1017 split_id,
1018 show_line_numbers,
1019 show_cursors,
1020 editing_disabled,
1021 line_wrap,
1022 request_id,
1023 } => {
1024 self.handle_create_virtual_buffer_in_existing_split(
1025 name,
1026 mode,
1027 read_only,
1028 entries,
1029 split_id,
1030 show_line_numbers,
1031 show_cursors,
1032 editing_disabled,
1033 line_wrap,
1034 request_id,
1035 );
1036 }
1037
1038 PluginCommand::SetContext { name, active } => {
1040 self.handle_set_context(name, active);
1041 }
1042
1043 PluginCommand::SetReviewDiffHunks { hunks } => {
1045 self.review_hunks = hunks;
1046 tracing::debug!("Set {} review hunks", self.review_hunks.len());
1047 }
1048
1049 PluginCommand::ExecuteAction { action_name } => {
1051 self.handle_execute_action(action_name);
1052 }
1053 PluginCommand::ExecuteActions { actions } => {
1054 self.handle_execute_actions(actions);
1055 }
1056 PluginCommand::GetBufferText {
1057 buffer_id,
1058 start,
1059 end,
1060 request_id,
1061 } => {
1062 self.handle_get_buffer_text(buffer_id, start, end, request_id);
1063 }
1064 PluginCommand::GetLineStartPosition {
1065 buffer_id,
1066 line,
1067 request_id,
1068 } => {
1069 self.handle_get_line_start_position(buffer_id, line, request_id);
1070 }
1071 PluginCommand::GetLineEndPosition {
1072 buffer_id,
1073 line,
1074 request_id,
1075 } => {
1076 self.handle_get_line_end_position(buffer_id, line, request_id);
1077 }
1078 PluginCommand::GetBufferLineCount {
1079 buffer_id,
1080 request_id,
1081 } => {
1082 self.handle_get_buffer_line_count(buffer_id, request_id);
1083 }
1084 PluginCommand::ScrollToLineCenter {
1085 split_id,
1086 buffer_id,
1087 line,
1088 } => {
1089 self.handle_scroll_to_line_center(split_id, buffer_id, line);
1090 }
1091 PluginCommand::ScrollBufferToLine { buffer_id, line } => {
1092 self.handle_scroll_buffer_to_line(buffer_id, line);
1093 }
1094 PluginCommand::SetEditorMode { mode } => {
1095 self.handle_set_editor_mode(mode);
1096 }
1097
1098 PluginCommand::ShowActionPopup {
1100 popup_id,
1101 title,
1102 message,
1103 actions,
1104 } => {
1105 self.handle_show_action_popup(popup_id, title, message, actions);
1106 }
1107
1108 PluginCommand::DisableLspForLanguage { language } => {
1109 self.handle_disable_lsp_for_language(language);
1110 }
1111
1112 PluginCommand::RestartLspForLanguage { language } => {
1113 self.handle_restart_lsp_for_language(language);
1114 }
1115
1116 PluginCommand::SetLspRootUri { language, uri } => {
1117 self.handle_set_lsp_root_uri(language, uri);
1118 }
1119
1120 PluginCommand::CreateScrollSyncGroup {
1122 group_id,
1123 left_split,
1124 right_split,
1125 } => {
1126 self.handle_create_scroll_sync_group(group_id, left_split, right_split);
1127 }
1128 PluginCommand::SetScrollSyncAnchors { group_id, anchors } => {
1129 self.handle_set_scroll_sync_anchors(group_id, anchors);
1130 }
1131 PluginCommand::RemoveScrollSyncGroup { group_id } => {
1132 self.handle_remove_scroll_sync_group(group_id);
1133 }
1134
1135 PluginCommand::CreateCompositeBuffer {
1137 name,
1138 mode,
1139 layout,
1140 sources,
1141 hunks,
1142 initial_focus_hunk,
1143 request_id,
1144 } => {
1145 self.handle_create_composite_buffer(
1146 name,
1147 mode,
1148 layout,
1149 sources,
1150 hunks,
1151 initial_focus_hunk,
1152 request_id,
1153 );
1154 }
1155 PluginCommand::UpdateCompositeAlignment { buffer_id, hunks } => {
1156 self.handle_update_composite_alignment(buffer_id, hunks);
1157 }
1158 PluginCommand::CloseCompositeBuffer { buffer_id } => {
1159 self.close_composite_buffer(buffer_id);
1160 }
1161 PluginCommand::FlushLayout => {
1162 self.flush_layout();
1163 }
1164 PluginCommand::CompositeNextHunk { buffer_id } => {
1165 let split_id = self.split_manager.active_split();
1166 self.composite_next_hunk(split_id, buffer_id);
1167 }
1168 PluginCommand::CompositePrevHunk { buffer_id } => {
1169 let split_id = self.split_manager.active_split();
1170 self.composite_prev_hunk(split_id, buffer_id);
1171 }
1172
1173 PluginCommand::CreateBufferGroup {
1175 name,
1176 mode,
1177 layout_json,
1178 request_id,
1179 } => {
1180 self.handle_create_buffer_group(name, mode, layout_json, request_id);
1181 }
1182 PluginCommand::SetPanelContent {
1183 group_id,
1184 panel_name,
1185 entries,
1186 } => {
1187 self.set_panel_content(group_id, panel_name, entries);
1188 }
1189 PluginCommand::CloseBufferGroup { group_id } => {
1190 self.close_buffer_group(group_id);
1191 }
1192 PluginCommand::FocusPanel {
1193 group_id,
1194 panel_name,
1195 } => {
1196 self.focus_panel(group_id, panel_name);
1197 }
1198
1199 PluginCommand::SaveBufferToPath { buffer_id, path } => {
1201 self.handle_save_buffer_to_path(buffer_id, path);
1202 }
1203
1204 #[cfg(feature = "plugins")]
1206 PluginCommand::LoadPlugin { path, callback_id } => {
1207 self.handle_load_plugin(path, callback_id);
1208 }
1209 #[cfg(feature = "plugins")]
1210 PluginCommand::UnloadPlugin { name, callback_id } => {
1211 self.handle_unload_plugin(name, callback_id);
1212 }
1213 #[cfg(feature = "plugins")]
1214 PluginCommand::ReloadPlugin { name, callback_id } => {
1215 self.handle_reload_plugin(name, callback_id);
1216 }
1217 #[cfg(feature = "plugins")]
1218 PluginCommand::ListPlugins { callback_id } => {
1219 self.handle_list_plugins(callback_id);
1220 }
1221 #[cfg(not(feature = "plugins"))]
1223 PluginCommand::LoadPlugin { .. }
1224 | PluginCommand::UnloadPlugin { .. }
1225 | PluginCommand::ReloadPlugin { .. }
1226 | PluginCommand::ListPlugins { .. } => {
1227 tracing::warn!("Plugin management commands require the 'plugins' feature");
1228 }
1229
1230 PluginCommand::CreateTerminal {
1232 cwd,
1233 direction,
1234 ratio,
1235 focus,
1236 persistent,
1237 request_id,
1238 } => {
1239 self.handle_create_terminal(cwd, direction, ratio, focus, persistent, request_id);
1240 }
1241
1242 PluginCommand::SendTerminalInput { terminal_id, data } => {
1243 self.handle_send_terminal_input(terminal_id, data);
1244 }
1245
1246 PluginCommand::CloseTerminal { terminal_id } => {
1247 self.handle_close_terminal(terminal_id);
1248 }
1249
1250 PluginCommand::GrepProject {
1251 pattern,
1252 fixed_string,
1253 case_sensitive,
1254 max_results,
1255 whole_words,
1256 callback_id,
1257 } => {
1258 self.handle_grep_project(
1259 pattern,
1260 fixed_string,
1261 case_sensitive,
1262 max_results,
1263 whole_words,
1264 callback_id,
1265 );
1266 }
1267
1268 PluginCommand::GrepProjectStreaming {
1269 pattern,
1270 fixed_string,
1271 case_sensitive,
1272 max_results,
1273 whole_words,
1274 search_id,
1275 callback_id,
1276 } => {
1277 self.handle_grep_project_streaming(
1278 pattern,
1279 fixed_string,
1280 case_sensitive,
1281 max_results,
1282 whole_words,
1283 search_id,
1284 callback_id,
1285 );
1286 }
1287
1288 PluginCommand::ReplaceInBuffer {
1289 file_path,
1290 matches,
1291 replacement,
1292 callback_id,
1293 } => {
1294 self.handle_replace_in_buffer(file_path, matches, replacement, callback_id);
1295 }
1296 }
1297 Ok(())
1298 }
1299
1300 fn handle_save_buffer_to_path(&mut self, buffer_id: BufferId, path: std::path::PathBuf) {
1302 if let Some(state) = self.buffers.get_mut(&buffer_id) {
1303 match state.buffer.save_to_file(&path) {
1305 Ok(()) => {
1306 if let Err(e) = self.finalize_save(Some(path)) {
1309 tracing::warn!("Failed to finalize save: {}", e);
1310 }
1311 tracing::debug!("Saved buffer {:?} to path", buffer_id);
1312 }
1313 Err(e) => {
1314 self.handle_set_status(format!("Error saving: {}", e));
1315 tracing::error!("Failed to save buffer to path: {}", e);
1316 }
1317 }
1318 } else {
1319 self.handle_set_status(format!("Buffer {:?} not found", buffer_id));
1320 tracing::warn!("SaveBufferToPath: buffer {:?} not found", buffer_id);
1321 }
1322 }
1323
1324 #[cfg(feature = "plugins")]
1326 fn handle_load_plugin(&mut self, path: std::path::PathBuf, callback_id: JsCallbackId) {
1327 match self.plugin_manager.load_plugin(&path) {
1328 Ok(()) => {
1329 tracing::info!("Loaded plugin from {:?}", path);
1330 self.plugin_manager
1331 .resolve_callback(callback_id, "true".to_string());
1332 }
1333 Err(e) => {
1334 tracing::error!("Failed to load plugin from {:?}: {}", path, e);
1335 self.plugin_manager
1336 .reject_callback(callback_id, format!("{}", e));
1337 }
1338 }
1339 }
1340
1341 #[cfg(feature = "plugins")]
1343 fn handle_unload_plugin(&mut self, name: String, callback_id: JsCallbackId) {
1344 match self.plugin_manager.unload_plugin(&name) {
1345 Ok(()) => {
1346 tracing::info!("Unloaded plugin: {}", name);
1347 self.plugin_manager
1348 .resolve_callback(callback_id, "true".to_string());
1349 }
1350 Err(e) => {
1351 tracing::error!("Failed to unload plugin '{}': {}", name, e);
1352 self.plugin_manager
1353 .reject_callback(callback_id, format!("{}", e));
1354 }
1355 }
1356 }
1357
1358 #[cfg(feature = "plugins")]
1360 fn handle_reload_plugin(&mut self, name: String, callback_id: JsCallbackId) {
1361 match self.plugin_manager.reload_plugin(&name) {
1362 Ok(()) => {
1363 tracing::info!("Reloaded plugin: {}", name);
1364 self.plugin_manager
1365 .resolve_callback(callback_id, "true".to_string());
1366 }
1367 Err(e) => {
1368 tracing::error!("Failed to reload 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_list_plugins(&mut self, callback_id: JsCallbackId) {
1378 let plugins = self.plugin_manager.list_plugins();
1379 let json_array: Vec<serde_json::Value> = plugins
1381 .iter()
1382 .map(|p| {
1383 serde_json::json!({
1384 "name": p.name,
1385 "path": p.path.to_string_lossy(),
1386 "enabled": p.enabled
1387 })
1388 })
1389 .collect();
1390 let json_str = serde_json::to_string(&json_array).unwrap_or_else(|_| "[]".to_string());
1391 self.plugin_manager.resolve_callback(callback_id, json_str);
1392 }
1393
1394 fn handle_execute_action(&mut self, action_name: String) {
1396 use crate::input::keybindings::Action;
1397 use std::collections::HashMap;
1398
1399 if let Some(action) = Action::from_str(&action_name, &HashMap::new()) {
1401 if let Err(e) = self.handle_action(action) {
1403 tracing::warn!("Failed to execute action '{}': {}", action_name, e);
1404 } else {
1405 tracing::debug!("Executed action: {}", action_name);
1406 }
1407 } else {
1408 tracing::warn!("Unknown action: {}", action_name);
1409 }
1410 }
1411
1412 fn handle_execute_actions(&mut self, actions: Vec<fresh_core::api::ActionSpec>) {
1415 use crate::input::keybindings::Action;
1416 use std::collections::HashMap;
1417
1418 for action_spec in actions {
1419 if let Some(action) = Action::from_str(&action_spec.action, &HashMap::new()) {
1420 for _ in 0..action_spec.count {
1422 if let Err(e) = self.handle_action(action.clone()) {
1423 tracing::warn!("Failed to execute action '{}': {}", action_spec.action, e);
1424 return; }
1426 }
1427 tracing::debug!(
1428 "Executed action '{}' {} time(s)",
1429 action_spec.action,
1430 action_spec.count
1431 );
1432 } else {
1433 tracing::warn!("Unknown action: {}", action_spec.action);
1434 return; }
1436 }
1437 }
1438
1439 fn handle_get_buffer_text(
1441 &mut self,
1442 buffer_id: BufferId,
1443 start: usize,
1444 end: usize,
1445 request_id: u64,
1446 ) {
1447 let result = if let Some(state) = self.buffers.get_mut(&buffer_id) {
1448 let len = state.buffer.len();
1450 if start <= end && end <= len {
1451 Ok(state.get_text_range(start, end))
1452 } else {
1453 Err(format!(
1454 "Invalid range {}..{} for buffer of length {}",
1455 start, end, len
1456 ))
1457 }
1458 } else {
1459 Err(format!("Buffer {:?} not found", buffer_id))
1460 };
1461
1462 let callback_id = fresh_core::api::JsCallbackId::from(request_id);
1464 match result {
1465 Ok(text) => {
1466 let json = serde_json::to_string(&text).unwrap_or_else(|_| "null".to_string());
1468 self.plugin_manager.resolve_callback(callback_id, json);
1469 }
1470 Err(error) => {
1471 self.plugin_manager.reject_callback(callback_id, error);
1472 }
1473 }
1474 }
1475
1476 fn handle_set_editor_mode(&mut self, mode: Option<String>) {
1478 self.editor_mode = mode.clone();
1479 tracing::debug!("Set editor mode: {:?}", mode);
1480 }
1481
1482 fn resolve_buffer_id(&self, buffer_id: BufferId) -> BufferId {
1484 if buffer_id.0 == 0 {
1485 self.active_buffer()
1486 } else {
1487 buffer_id
1488 }
1489 }
1490
1491 fn resolve_json_callback<T: serde::Serialize>(&mut self, request_id: u64, value: T) {
1493 let callback_id = fresh_core::api::JsCallbackId::from(request_id);
1494 let json = serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string());
1495 self.plugin_manager.resolve_callback(callback_id, json);
1496 }
1497
1498 fn handle_get_line_start_position(&mut self, buffer_id: BufferId, line: u32, request_id: u64) {
1500 let actual_buffer_id = self.resolve_buffer_id(buffer_id);
1501 let result = self.buffers.get_mut(&actual_buffer_id).and_then(|state| {
1502 let len = state.buffer.len();
1503 let content = state.get_text_range(0, len);
1504 buffer_line_byte_offset(&content, len, line as usize, false)
1505 });
1506 self.resolve_json_callback(request_id, result);
1507 }
1508
1509 fn handle_get_line_end_position(&mut self, buffer_id: BufferId, line: u32, request_id: u64) {
1512 let actual_buffer_id = self.resolve_buffer_id(buffer_id);
1513 let result = self.buffers.get_mut(&actual_buffer_id).and_then(|state| {
1514 let len = state.buffer.len();
1515 let content = state.get_text_range(0, len);
1516 buffer_line_byte_offset(&content, len, line as usize, true)
1517 });
1518 self.resolve_json_callback(request_id, result);
1519 }
1520
1521 fn handle_get_buffer_line_count(&mut self, buffer_id: BufferId, request_id: u64) {
1523 let actual_buffer_id = self.resolve_buffer_id(buffer_id);
1524
1525 let result = if let Some(state) = self.buffers.get_mut(&actual_buffer_id) {
1526 let buffer_len = state.buffer.len();
1527 let content = state.get_text_range(0, buffer_len);
1528
1529 if content.is_empty() {
1531 Some(1) } else {
1533 let newline_count = content.chars().filter(|&c| c == '\n').count();
1534 let ends_with_newline = content.ends_with('\n');
1536 if ends_with_newline {
1537 Some(newline_count)
1538 } else {
1539 Some(newline_count + 1)
1540 }
1541 }
1542 } else {
1543 None
1544 };
1545
1546 self.resolve_json_callback(request_id, result);
1547 }
1548
1549 fn handle_scroll_to_line_center(
1551 &mut self,
1552 split_id: SplitId,
1553 buffer_id: BufferId,
1554 line: usize,
1555 ) {
1556 let actual_split_id = if split_id.0 == 0 {
1557 self.split_manager.active_split()
1558 } else {
1559 LeafId(split_id)
1560 };
1561 let actual_buffer_id = self.resolve_buffer_id(buffer_id);
1562
1563 let viewport_height = if let Some(view_state) = self.split_view_states.get(&actual_split_id)
1565 {
1566 view_state.viewport.height as usize
1567 } else {
1568 return;
1569 };
1570
1571 let lines_above = viewport_height / 2;
1573 let target_line = line.saturating_sub(lines_above);
1574
1575 if let Some(state) = self.buffers.get_mut(&actual_buffer_id) {
1577 let buffer = &mut state.buffer;
1578 if let Some(view_state) = self.split_view_states.get_mut(&actual_split_id) {
1579 view_state.viewport.scroll_to(buffer, target_line);
1580 view_state.viewport.set_skip_ensure_visible();
1582 }
1583 }
1584 }
1585
1586 fn handle_scroll_buffer_to_line(&mut self, buffer_id: BufferId, line: usize) {
1596 if !self.buffers.contains_key(&buffer_id) {
1597 return;
1598 }
1599
1600 let mut target_leaves: Vec<LeafId> = Vec::new();
1602
1603 for leaf_id in self.split_manager.root().leaf_split_ids() {
1605 if let Some(vs) = self.split_view_states.get(&leaf_id) {
1606 if vs.active_buffer == buffer_id {
1607 target_leaves.push(leaf_id);
1608 }
1609 }
1610 }
1611
1612 for (_group_leaf_id, node) in self.grouped_subtrees.iter() {
1614 if let crate::view::split::SplitNode::Grouped { layout, .. } = node {
1615 for inner_leaf in layout.leaf_split_ids() {
1616 if let Some(vs) = self.split_view_states.get(&inner_leaf) {
1617 if vs.active_buffer == buffer_id && !target_leaves.contains(&inner_leaf) {
1618 target_leaves.push(inner_leaf);
1619 }
1620 }
1621 }
1622 }
1623 }
1624
1625 if target_leaves.is_empty() {
1626 return;
1627 }
1628
1629 let state = match self.buffers.get_mut(&buffer_id) {
1630 Some(s) => s,
1631 None => return,
1632 };
1633
1634 for leaf_id in target_leaves {
1635 let Some(view_state) = self.split_view_states.get_mut(&leaf_id) else {
1636 continue;
1637 };
1638 let viewport_height = view_state.viewport.height as usize;
1639 let lines_above = viewport_height / 3;
1642 let target = line.saturating_sub(lines_above);
1643 view_state.viewport.scroll_to(&mut state.buffer, target);
1644 view_state.viewport.set_skip_ensure_visible();
1645 }
1646 }
1647
1648 fn handle_spawn_host_process(
1649 &mut self,
1650 command: String,
1651 args: Vec<String>,
1652 cwd: Option<String>,
1653 callback_id: JsCallbackId,
1654 ) {
1655 if let (Some(runtime), Some(bridge)) = (&self.tokio_runtime, &self.async_bridge) {
1670 use tokio::io::{AsyncReadExt, BufReader};
1671 use tokio::process::Command as TokioCommand;
1672
1673 let effective_cwd = cwd.or_else(|| {
1674 std::env::current_dir()
1675 .map(|p| p.to_string_lossy().to_string())
1676 .ok()
1677 });
1678 let sender = bridge.sender();
1679 let process_id = callback_id.as_u64();
1680
1681 let (kill_tx, mut kill_rx) = tokio::sync::oneshot::channel::<()>();
1682 self.host_process_handles.insert(process_id, kill_tx);
1683
1684 runtime.spawn(async move {
1685 use crate::services::process_hidden::HideWindow;
1686 let mut cmd = TokioCommand::new(&command);
1687 cmd.args(&args);
1688 cmd.stdout(std::process::Stdio::piped());
1689 cmd.stderr(std::process::Stdio::piped());
1690 cmd.hide_window();
1691 if let Some(ref dir) = effective_cwd {
1692 cmd.current_dir(dir);
1693 }
1694 let mut child = match cmd.spawn() {
1695 Ok(c) => c,
1696 Err(e) => {
1697 #[allow(clippy::let_underscore_must_use)]
1698 let _ = sender.send(AsyncMessage::PluginProcessOutput {
1699 process_id,
1700 stdout: String::new(),
1701 stderr: e.to_string(),
1702 exit_code: -1,
1703 });
1704 return;
1705 }
1706 };
1707
1708 let stdout_pipe = child.stdout.take();
1714 let stderr_pipe = child.stderr.take();
1715
1716 let stdout_fut = async {
1717 let mut buf = String::new();
1718 if let Some(s) = stdout_pipe {
1719 #[allow(clippy::let_underscore_must_use)]
1720 let _ = BufReader::new(s).read_to_string(&mut buf).await;
1721 }
1722 buf
1723 };
1724 let stderr_fut = async {
1725 let mut buf = String::new();
1726 if let Some(s) = stderr_pipe {
1727 #[allow(clippy::let_underscore_must_use)]
1728 let _ = BufReader::new(s).read_to_string(&mut buf).await;
1729 }
1730 buf
1731 };
1732 let wait_fut = async {
1733 tokio::select! {
1734 status = child.wait() => {
1735 status.map(|s| s.code().unwrap_or(-1)).unwrap_or(-1)
1736 }
1737 _ = &mut kill_rx => {
1738 #[allow(clippy::let_underscore_must_use)]
1742 let _ = child.start_kill();
1743 child
1744 .wait()
1745 .await
1746 .map(|s| s.code().unwrap_or(-1))
1747 .unwrap_or(-1)
1748 }
1749 }
1750 };
1751 let (stdout, stderr, exit_code) = tokio::join!(stdout_fut, stderr_fut, wait_fut);
1752
1753 #[allow(clippy::let_underscore_must_use)]
1754 let _ = sender.send(AsyncMessage::PluginProcessOutput {
1755 process_id,
1756 stdout,
1757 stderr,
1758 exit_code,
1759 });
1760 });
1761 } else {
1762 self.plugin_manager
1763 .reject_callback(callback_id, "Async runtime not available".to_string());
1764 }
1765 }
1766
1767 fn handle_spawn_background_process(
1768 &mut self,
1769 process_id: u64,
1770 command: String,
1771 args: Vec<String>,
1772 cwd: Option<String>,
1773 callback_id: JsCallbackId,
1774 ) {
1775 if let (Some(runtime), Some(bridge)) = (&self.tokio_runtime, &self.async_bridge) {
1777 use tokio::io::{AsyncBufReadExt, BufReader};
1778 use tokio::process::Command as TokioCommand;
1779
1780 let effective_cwd = cwd.unwrap_or_else(|| {
1781 std::env::current_dir()
1782 .map(|p| p.to_string_lossy().to_string())
1783 .unwrap_or_else(|_| ".".to_string())
1784 });
1785
1786 let sender = bridge.sender();
1787 let sender_stdout = sender.clone();
1788 let sender_stderr = sender.clone();
1789 let callback_id_u64 = callback_id.as_u64();
1790
1791 #[allow(clippy::let_underscore_must_use)]
1793 let handle = runtime.spawn(async move {
1794 use crate::services::process_hidden::HideWindow;
1795 let mut child = match TokioCommand::new(&command)
1796 .args(&args)
1797 .current_dir(&effective_cwd)
1798 .stdout(std::process::Stdio::piped())
1799 .stderr(std::process::Stdio::piped())
1800 .hide_window()
1801 .spawn()
1802 {
1803 Ok(child) => child,
1804 Err(e) => {
1805 let _ = sender.send(crate::services::async_bridge::AsyncMessage::Plugin(
1806 fresh_core::api::PluginAsyncMessage::ProcessExit {
1807 process_id,
1808 callback_id: callback_id_u64,
1809 exit_code: -1,
1810 },
1811 ));
1812 tracing::error!("Failed to spawn background process: {}", e);
1813 return;
1814 }
1815 };
1816
1817 let stdout = child.stdout.take();
1819 let stderr = child.stderr.take();
1820 let pid = process_id;
1821
1822 if let Some(stdout) = stdout {
1824 let sender = sender_stdout;
1825 tokio::spawn(async move {
1826 let reader = BufReader::new(stdout);
1827 let mut lines = reader.lines();
1828 while let Ok(Some(line)) = lines.next_line().await {
1829 let _ =
1830 sender.send(crate::services::async_bridge::AsyncMessage::Plugin(
1831 fresh_core::api::PluginAsyncMessage::ProcessStdout {
1832 process_id: pid,
1833 data: line + "\n",
1834 },
1835 ));
1836 }
1837 });
1838 }
1839
1840 if let Some(stderr) = stderr {
1842 let sender = sender_stderr;
1843 tokio::spawn(async move {
1844 let reader = BufReader::new(stderr);
1845 let mut lines = reader.lines();
1846 while let Ok(Some(line)) = lines.next_line().await {
1847 let _ =
1848 sender.send(crate::services::async_bridge::AsyncMessage::Plugin(
1849 fresh_core::api::PluginAsyncMessage::ProcessStderr {
1850 process_id: pid,
1851 data: line + "\n",
1852 },
1853 ));
1854 }
1855 });
1856 }
1857
1858 let exit_code = match child.wait().await {
1860 Ok(status) => status.code().unwrap_or(-1),
1861 Err(_) => -1,
1862 };
1863
1864 let _ = sender.send(crate::services::async_bridge::AsyncMessage::Plugin(
1865 fresh_core::api::PluginAsyncMessage::ProcessExit {
1866 process_id,
1867 callback_id: callback_id_u64,
1868 exit_code,
1869 },
1870 ));
1871 });
1872
1873 self.background_process_handles
1875 .insert(process_id, handle.abort_handle());
1876 } else {
1877 self.plugin_manager
1879 .reject_callback(callback_id, "Async runtime not available".to_string());
1880 }
1881 }
1882
1883 fn handle_create_virtual_buffer_with_content(
1884 &mut self,
1885 name: String,
1886 mode: String,
1887 read_only: bool,
1888 entries: Vec<fresh_core::text_property::TextPropertyEntry>,
1889 show_line_numbers: bool,
1890 show_cursors: bool,
1891 editing_disabled: bool,
1892 hidden_from_tabs: bool,
1893 request_id: Option<u64>,
1894 ) {
1895 let buffer_id = self.create_virtual_buffer(name.clone(), mode.clone(), read_only);
1896 tracing::info!(
1897 "Created virtual buffer '{}' with mode '{}' (id={:?})",
1898 name,
1899 mode,
1900 buffer_id
1901 );
1902
1903 if let Some(state) = self.buffers.get_mut(&buffer_id) {
1910 state.margins.configure_for_line_numbers(show_line_numbers);
1911 state.show_cursors = show_cursors;
1912 state.editing_disabled = editing_disabled;
1913 tracing::debug!(
1914 "Set buffer {:?} view options: show_line_numbers={}, show_cursors={}, editing_disabled={}",
1915 buffer_id,
1916 show_line_numbers,
1917 show_cursors,
1918 editing_disabled
1919 );
1920 }
1921 let active_split = self.split_manager.active_split();
1922 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
1923 view_state.ensure_buffer_state(buffer_id).show_line_numbers = show_line_numbers;
1924 }
1925
1926 if hidden_from_tabs {
1928 if let Some(meta) = self.buffer_metadata.get_mut(&buffer_id) {
1929 meta.hidden_from_tabs = true;
1930 }
1931 }
1932
1933 match self.set_virtual_buffer_content(buffer_id, entries) {
1935 Ok(()) => {
1936 tracing::debug!("Set virtual buffer content for {:?}", buffer_id);
1937 self.set_active_buffer(buffer_id);
1939 tracing::debug!("Switched to virtual buffer {:?}", buffer_id);
1940
1941 if let Some(req_id) = request_id {
1943 tracing::info!(
1944 "CreateVirtualBufferWithContent: resolving callback for request_id={}, buffer_id={:?}",
1945 req_id,
1946 buffer_id
1947 );
1948 let result = fresh_core::api::VirtualBufferResult {
1950 buffer_id: buffer_id.0 as u64,
1951 split_id: None,
1952 };
1953 self.plugin_manager.resolve_callback(
1954 fresh_core::api::JsCallbackId::from(req_id),
1955 serde_json::to_string(&result).unwrap_or_default(),
1956 );
1957 tracing::info!(
1958 "CreateVirtualBufferWithContent: resolve_callback sent for request_id={}",
1959 req_id
1960 );
1961 }
1962 }
1963 Err(e) => {
1964 tracing::error!("Failed to set virtual buffer content: {}", e);
1965 }
1966 }
1967 }
1968
1969 fn handle_create_virtual_buffer_in_split(
1970 &mut self,
1971 name: String,
1972 mode: String,
1973 read_only: bool,
1974 entries: Vec<fresh_core::text_property::TextPropertyEntry>,
1975 ratio: f32,
1976 direction: Option<String>,
1977 panel_id: Option<String>,
1978 show_line_numbers: bool,
1979 show_cursors: bool,
1980 editing_disabled: bool,
1981 line_wrap: Option<bool>,
1982 before: bool,
1983 request_id: Option<u64>,
1984 ) {
1985 if let Some(pid) = &panel_id {
1987 if let Some(&existing_buffer_id) = self.panel_ids.get(pid) {
1988 if self.buffers.contains_key(&existing_buffer_id) {
1990 if let Err(e) = self.set_virtual_buffer_content(existing_buffer_id, entries) {
1992 tracing::error!("Failed to update panel content: {}", e);
1993 } else {
1994 tracing::info!("Updated existing panel '{}' content", pid);
1995 }
1996
1997 let splits = self.split_manager.splits_for_buffer(existing_buffer_id);
1999 if let Some(&split_id) = splits.first() {
2000 self.split_manager.set_active_split(split_id);
2001 self.set_pane_buffer(split_id, existing_buffer_id);
2004 tracing::debug!("Focused split {:?} containing panel buffer", split_id);
2005 }
2006
2007 if let Some(req_id) = request_id {
2009 let result = fresh_core::api::VirtualBufferResult {
2010 buffer_id: existing_buffer_id.0 as u64,
2011 split_id: splits.first().map(|s| s.0 .0 as u64),
2012 };
2013 self.plugin_manager.resolve_callback(
2014 fresh_core::api::JsCallbackId::from(req_id),
2015 serde_json::to_string(&result).unwrap_or_default(),
2016 );
2017 }
2018 return;
2019 } else {
2020 tracing::warn!(
2022 "Removing stale panel_id '{}' pointing to non-existent buffer {:?}",
2023 pid,
2024 existing_buffer_id
2025 );
2026 self.panel_ids.remove(pid);
2027 }
2029 }
2030 }
2031
2032 let source_split_before_create = self.split_manager.active_split();
2038
2039 let buffer_id = self.create_virtual_buffer(name.clone(), mode.clone(), read_only);
2041 tracing::info!(
2042 "Created virtual buffer '{}' with mode '{}' in split (id={:?})",
2043 name,
2044 mode,
2045 buffer_id
2046 );
2047
2048 if let Some(state) = self.buffers.get_mut(&buffer_id) {
2050 state.margins.configure_for_line_numbers(show_line_numbers);
2051 state.show_cursors = show_cursors;
2052 state.editing_disabled = editing_disabled;
2053 tracing::debug!(
2054 "Set buffer {:?} view options: show_line_numbers={}, show_cursors={}, editing_disabled={}",
2055 buffer_id,
2056 show_line_numbers,
2057 show_cursors,
2058 editing_disabled
2059 );
2060 }
2061
2062 if let Some(pid) = panel_id {
2064 self.panel_ids.insert(pid, buffer_id);
2065 }
2066
2067 if let Err(e) = self.set_virtual_buffer_content(buffer_id, entries) {
2069 tracing::error!("Failed to set virtual buffer content: {}", e);
2070 return;
2071 }
2072
2073 let split_dir = match direction.as_deref() {
2075 Some("vertical") => crate::model::event::SplitDirection::Vertical,
2076 _ => crate::model::event::SplitDirection::Horizontal,
2077 };
2078
2079 let created_split_id = match self
2081 .split_manager
2082 .split_active_positioned(split_dir, buffer_id, ratio, before)
2083 {
2084 Ok(new_split_id) => {
2085 if new_split_id != source_split_before_create {
2091 if let Some(source_view_state) =
2092 self.split_view_states.get_mut(&source_split_before_create)
2093 {
2094 source_view_state.remove_buffer(buffer_id);
2095 }
2096 }
2097 let mut view_state = SplitViewState::with_buffer(
2099 self.terminal_width,
2100 self.terminal_height,
2101 buffer_id,
2102 );
2103 view_state.apply_config_defaults(
2104 self.config.editor.line_numbers,
2105 self.config.editor.highlight_current_line,
2106 line_wrap.unwrap_or_else(|| self.resolve_line_wrap_for_buffer(buffer_id)),
2107 self.config.editor.wrap_indent,
2108 self.resolve_wrap_column_for_buffer(buffer_id),
2109 self.config.editor.rulers.clone(),
2110 );
2111 view_state.ensure_buffer_state(buffer_id).show_line_numbers = show_line_numbers;
2113 self.split_view_states.insert(new_split_id, view_state);
2114
2115 self.split_manager.set_active_split(new_split_id);
2117 tracing::info!(
2120 "Created {:?} split with virtual buffer {:?}",
2121 split_dir,
2122 buffer_id
2123 );
2124 Some(new_split_id)
2125 }
2126 Err(e) => {
2127 tracing::error!("Failed to create split: {}", e);
2128 self.set_active_buffer(buffer_id);
2130 None
2131 }
2132 };
2133
2134 if let Some(req_id) = request_id {
2137 tracing::trace!("CreateVirtualBufferInSplit: resolving callback for request_id={}, buffer_id={:?}, split_id={:?}", req_id, buffer_id, created_split_id);
2138 let result = fresh_core::api::VirtualBufferResult {
2139 buffer_id: buffer_id.0 as u64,
2140 split_id: created_split_id.map(|s| s.0 .0 as u64),
2141 };
2142 self.plugin_manager.resolve_callback(
2143 fresh_core::api::JsCallbackId::from(req_id),
2144 serde_json::to_string(&result).unwrap_or_default(),
2145 );
2146 }
2147 }
2148
2149 fn handle_create_virtual_buffer_in_existing_split(
2150 &mut self,
2151 name: String,
2152 mode: String,
2153 read_only: bool,
2154 entries: Vec<fresh_core::text_property::TextPropertyEntry>,
2155 split_id: SplitId,
2156 show_line_numbers: bool,
2157 show_cursors: bool,
2158 editing_disabled: bool,
2159 line_wrap: Option<bool>,
2160 request_id: Option<u64>,
2161 ) {
2162 let buffer_id = self.create_virtual_buffer(name.clone(), mode.clone(), read_only);
2164 tracing::info!(
2165 "Created virtual buffer '{}' with mode '{}' for existing split {:?} (id={:?})",
2166 name,
2167 mode,
2168 split_id,
2169 buffer_id
2170 );
2171
2172 if let Some(state) = self.buffers.get_mut(&buffer_id) {
2174 state.margins.configure_for_line_numbers(show_line_numbers);
2175 state.show_cursors = show_cursors;
2176 state.editing_disabled = editing_disabled;
2177 }
2178
2179 if let Err(e) = self.set_virtual_buffer_content(buffer_id, entries) {
2181 tracing::error!("Failed to set virtual buffer content: {}", e);
2182 return;
2183 }
2184
2185 let leaf_id = LeafId(split_id);
2188 self.split_manager.set_active_split(leaf_id);
2189 self.set_pane_buffer(leaf_id, buffer_id);
2190
2191 if let Some(view_state) = self.split_view_states.get_mut(&leaf_id) {
2197 view_state.switch_buffer(buffer_id);
2198 view_state.add_buffer(buffer_id);
2199 view_state.ensure_buffer_state(buffer_id).show_line_numbers = show_line_numbers;
2200
2201 if let Some(wrap) = line_wrap {
2203 view_state.active_state_mut().viewport.line_wrap_enabled = wrap;
2204 }
2205 }
2206
2207 tracing::info!(
2208 "Displayed virtual buffer {:?} in split {:?}",
2209 buffer_id,
2210 split_id
2211 );
2212
2213 if let Some(req_id) = request_id {
2215 let result = fresh_core::api::VirtualBufferResult {
2216 buffer_id: buffer_id.0 as u64,
2217 split_id: Some(split_id.0 as u64),
2218 };
2219 self.plugin_manager.resolve_callback(
2220 fresh_core::api::JsCallbackId::from(req_id),
2221 serde_json::to_string(&result).unwrap_or_default(),
2222 );
2223 }
2224 }
2225
2226 fn handle_show_action_popup(
2227 &mut self,
2228 popup_id: String,
2229 title: String,
2230 message: String,
2231 actions: Vec<fresh_core::api::ActionPopupAction>,
2232 ) {
2233 tracing::info!(
2234 "Action popup requested: id={}, title={}, actions={}",
2235 popup_id,
2236 title,
2237 actions.len()
2238 );
2239
2240 let items: Vec<crate::model::event::PopupListItemData> = actions
2242 .iter()
2243 .map(|action| crate::model::event::PopupListItemData {
2244 text: action.label.clone(),
2245 detail: None,
2246 icon: None,
2247 data: Some(action.id.clone()),
2248 })
2249 .collect();
2250
2251 drop(actions);
2256
2257 let popup_data = crate::model::event::PopupData {
2259 kind: crate::model::event::PopupKindHint::List,
2260 title: Some(title),
2261 description: Some(message),
2262 transient: false,
2263 content: crate::model::event::PopupContentData::List { items, selected: 0 },
2264 position: crate::model::event::PopupPositionData::BottomRight,
2265 width: 60,
2266 max_height: 15,
2267 bordered: true,
2268 };
2269
2270 let mut popup_obj = crate::state::convert_popup_data_to_popup(&popup_data);
2280 popup_obj.resolver = crate::view::popup::PopupResolver::PluginAction {
2281 popup_id: popup_id.clone(),
2282 };
2283 self.global_popups.show(popup_obj);
2284 tracing::info!(
2285 "Action popup shown: id={}, stack_depth={}",
2286 popup_id,
2287 self.global_popups.all().len()
2288 );
2289 }
2290
2291 fn handle_create_terminal(
2292 &mut self,
2293 cwd: Option<String>,
2294 direction: Option<String>,
2295 ratio: Option<f32>,
2296 focus: Option<bool>,
2297 persistent: bool,
2298 request_id: u64,
2299 ) {
2300 let (cols, rows) = self.get_terminal_dimensions();
2301
2302 if let Some(ref bridge) = self.async_bridge {
2304 self.terminal_manager.set_async_bridge(bridge.clone());
2305 }
2306
2307 let working_dir = cwd
2309 .map(std::path::PathBuf::from)
2310 .unwrap_or_else(|| self.working_dir.clone());
2311
2312 let terminal_root = self.dir_context.terminal_dir_for(&working_dir);
2314 if let Err(e) = self.authority.filesystem.create_dir_all(&terminal_root) {
2315 tracing::warn!("Failed to create terminal directory: {}", e);
2316 }
2317 let predicted_terminal_id = self.terminal_manager.next_terminal_id();
2318 let name_stem = if persistent {
2325 format!("fresh-terminal-{}", predicted_terminal_id.0)
2326 } else {
2327 let nanos = std::time::SystemTime::now()
2328 .duration_since(std::time::UNIX_EPOCH)
2329 .map(|d| d.as_nanos())
2330 .unwrap_or(0);
2331 format!("fresh-terminal-eph-{}-{}", predicted_terminal_id.0, nanos)
2332 };
2333 let log_path = terminal_root.join(format!("{}.log", name_stem));
2334 let backing_path = terminal_root.join(format!("{}.txt", name_stem));
2335 self.terminal_backing_files
2336 .insert(predicted_terminal_id, backing_path);
2337 let backing_path_for_spawn = self
2338 .terminal_backing_files
2339 .get(&predicted_terminal_id)
2340 .cloned();
2341
2342 match self.terminal_manager.spawn(
2343 cols,
2344 rows,
2345 Some(working_dir),
2346 Some(log_path.clone()),
2347 backing_path_for_spawn,
2348 self.resolved_terminal_wrapper(),
2349 ) {
2350 Ok(terminal_id) => {
2351 self.terminal_log_files
2353 .insert(terminal_id, log_path.clone());
2354 if terminal_id != predicted_terminal_id {
2362 let existing = self.terminal_backing_files.remove(&predicted_terminal_id);
2363 let fixed_backing = if persistent {
2364 terminal_root.join(format!("fresh-terminal-{}.txt", terminal_id.0))
2365 } else {
2366 existing.unwrap_or_else(|| terminal_root.join(format!("{}.txt", name_stem)))
2367 };
2368 self.terminal_backing_files
2369 .insert(terminal_id, fixed_backing);
2370 }
2371 if !persistent {
2372 self.ephemeral_terminals.insert(terminal_id);
2373 }
2374
2375 let active_split = self.split_manager.active_split();
2389 let buffer_id = if direction.is_some() {
2390 self.create_terminal_buffer_detached(terminal_id)
2391 } else {
2392 self.create_terminal_buffer_attached(terminal_id, active_split)
2393 };
2394
2395 let created_split_id = if let Some(dir_str) = direction.as_deref() {
2396 let split_dir = match dir_str {
2397 "horizontal" => crate::model::event::SplitDirection::Horizontal,
2398 _ => crate::model::event::SplitDirection::Vertical,
2399 };
2400
2401 let split_ratio = ratio.unwrap_or(0.5);
2402 match self
2403 .split_manager
2404 .split_active(split_dir, buffer_id, split_ratio)
2405 {
2406 Ok(new_split_id) => {
2407 let mut view_state = SplitViewState::with_buffer(
2408 self.terminal_width,
2409 self.terminal_height,
2410 buffer_id,
2411 );
2412 view_state.apply_config_defaults(
2413 self.config.editor.line_numbers,
2414 self.config.editor.highlight_current_line,
2415 false,
2416 false,
2417 None,
2418 self.config.editor.rulers.clone(),
2419 );
2420 view_state.viewport.line_wrap_enabled = false;
2424 self.split_view_states.insert(new_split_id, view_state);
2425
2426 if focus.unwrap_or(true) {
2427 self.split_manager.set_active_split(new_split_id);
2428 }
2429
2430 tracing::info!(
2431 "Created {:?} split for terminal {:?} with buffer {:?}",
2432 split_dir,
2433 terminal_id,
2434 buffer_id
2435 );
2436 Some(new_split_id)
2437 }
2438 Err(e) => {
2439 tracing::error!(
2440 "Failed to create split for terminal: {}; \
2441 falling back to active split",
2442 e
2443 );
2444 if let Some(view_state) = self.split_view_states.get_mut(&active_split)
2449 {
2450 view_state.add_buffer(buffer_id);
2451 view_state.viewport.line_wrap_enabled = false;
2452 }
2453 self.set_active_buffer(buffer_id);
2454 None
2455 }
2456 }
2457 } else {
2458 self.set_active_buffer(buffer_id);
2460 None
2461 };
2462
2463 self.resize_visible_terminals();
2465
2466 let result = fresh_core::api::TerminalResult {
2468 buffer_id: buffer_id.0 as u64,
2469 terminal_id: terminal_id.0 as u64,
2470 split_id: created_split_id.map(|s| s.0 .0 as u64),
2471 };
2472 self.plugin_manager.resolve_callback(
2473 fresh_core::api::JsCallbackId::from(request_id),
2474 serde_json::to_string(&result).unwrap_or_default(),
2475 );
2476
2477 tracing::info!(
2478 "Plugin created terminal {:?} with buffer {:?}",
2479 terminal_id,
2480 buffer_id
2481 );
2482 }
2483 Err(e) => {
2484 tracing::error!("Failed to create terminal for plugin: {}", e);
2485 self.plugin_manager.reject_callback(
2486 fresh_core::api::JsCallbackId::from(request_id),
2487 format!("Failed to create terminal: {}", e),
2488 );
2489 }
2490 }
2491 }
2492 fn handle_get_split_by_label(&mut self, label: String, request_id: u64) {
2495 let split_id = self.split_manager.find_split_by_label(&label);
2496 let callback_id = fresh_core::api::JsCallbackId::from(request_id);
2497 let json =
2498 serde_json::to_string(&split_id.map(|s| s.0 .0)).unwrap_or_else(|_| "null".to_string());
2499 self.plugin_manager.resolve_callback(callback_id, json);
2500 }
2501
2502 fn handle_set_buffer_show_cursors(&mut self, buffer_id: BufferId, show: bool) {
2503 if let Some(state) = self.buffers.get_mut(&buffer_id) {
2504 state.show_cursors = show;
2505 } else {
2506 tracing::warn!("SetBufferShowCursors: buffer {:?} not found", buffer_id);
2507 }
2508 }
2509
2510 fn handle_override_theme_colors(
2511 &mut self,
2512 overrides: std::collections::HashMap<String, [u8; 3]>,
2513 ) {
2514 let pairs = overrides
2515 .into_iter()
2516 .map(|(k, [r, g, b])| (k, ratatui::style::Color::Rgb(r, g, b)));
2517 let applied = self.theme.override_colors(pairs);
2518 if applied > 0 {
2519 self.reapply_all_overlays();
2522 }
2523 }
2524
2525 fn handle_await_next_key(&mut self, callback_id: fresh_core::api::JsCallbackId) {
2526 if let Some(payload) = self.pending_key_capture_buffer.pop_front() {
2530 let json = serde_json::to_string(&payload).unwrap_or_else(|_| "null".to_string());
2531 self.plugin_manager.resolve_callback(callback_id, json);
2532 } else {
2533 self.pending_next_key_callbacks.push_back(callback_id);
2534 }
2535 }
2536
2537 fn handle_spawn_process(
2538 &mut self,
2539 command: String,
2540 args: Vec<String>,
2541 cwd: Option<String>,
2542 callback_id: fresh_core::api::JsCallbackId,
2543 ) {
2544 if let (Some(runtime), Some(bridge)) = (&self.tokio_runtime, &self.async_bridge) {
2545 let effective_cwd = cwd.or_else(|| {
2546 std::env::current_dir()
2547 .map(|p| p.to_string_lossy().to_string())
2548 .ok()
2549 });
2550 let sender = bridge.sender();
2551 let spawner = self.authority.process_spawner.clone();
2552 runtime.spawn(async move {
2553 #[allow(clippy::let_underscore_must_use)]
2554 match spawner.spawn(command, args, effective_cwd).await {
2555 Ok(result) => {
2556 let _ = sender.send(AsyncMessage::PluginProcessOutput {
2557 process_id: callback_id.as_u64(),
2558 stdout: result.stdout,
2559 stderr: result.stderr,
2560 exit_code: result.exit_code,
2561 });
2562 }
2563 Err(e) => {
2564 let _ = sender.send(AsyncMessage::PluginProcessOutput {
2565 process_id: callback_id.as_u64(),
2566 stdout: String::new(),
2567 stderr: e.to_string(),
2568 exit_code: -1,
2569 });
2570 }
2571 }
2572 });
2573 } else {
2574 self.plugin_manager
2575 .reject_callback(callback_id, "Async runtime not available".to_string());
2576 }
2577 }
2578
2579 fn handle_kill_host_process(&mut self, process_id: u64) {
2580 if let Some(tx) = self.host_process_handles.remove(&process_id) {
2584 #[allow(clippy::let_underscore_must_use)]
2585 let _ = tx.send(());
2586 tracing::debug!("KillHostProcess: sent kill for process_id={}", process_id);
2587 } else {
2588 tracing::debug!(
2589 "KillHostProcess: unknown process_id={} (already exited?)",
2590 process_id
2591 );
2592 }
2593 }
2594
2595 fn handle_set_authority(&mut self, payload: serde_json::Value) {
2596 match serde_json::from_value::<crate::services::authority::AuthorityPayload>(payload) {
2599 Ok(parsed) => {
2600 match crate::services::authority::Authority::from_plugin_payload(parsed) {
2601 Ok(auth) => {
2602 tracing::info!("Plugin installed new authority");
2603 self.install_authority(auth);
2604 }
2605 Err(e) => {
2606 tracing::warn!("setAuthority: invalid payload: {}", e);
2607 self.set_status_message(format!("setAuthority rejected: {}", e));
2608 }
2609 }
2610 }
2611 Err(e) => {
2612 tracing::warn!("setAuthority: failed to parse payload: {}", e);
2613 self.set_status_message(format!("setAuthority rejected: {}", e));
2614 }
2615 }
2616 }
2617
2618 fn handle_set_remote_indicator_state(&mut self, state: serde_json::Value) {
2619 match serde_json::from_value::<crate::view::ui::status_bar::RemoteIndicatorOverride>(state)
2622 {
2623 Ok(over) => {
2624 self.remote_indicator_override = Some(over);
2625 }
2626 Err(e) => {
2627 tracing::warn!("setRemoteIndicatorState: invalid payload: {}", e);
2628 self.set_status_message(format!("setRemoteIndicatorState rejected: {}", e));
2629 }
2630 }
2631 }
2632
2633 fn handle_spawn_process_wait(
2634 &mut self,
2635 process_id: u64,
2636 callback_id: fresh_core::api::JsCallbackId,
2637 ) {
2638 tracing::warn!(
2639 "SpawnProcessWait not fully implemented - process_id={}",
2640 process_id
2641 );
2642 self.plugin_manager.reject_callback(
2643 callback_id,
2644 format!(
2645 "SpawnProcessWait not yet fully implemented for process_id={}",
2646 process_id
2647 ),
2648 );
2649 }
2650
2651 fn handle_delay(&mut self, callback_id: fresh_core::api::JsCallbackId, duration_ms: u64) {
2652 if let (Some(runtime), Some(bridge)) = (&self.tokio_runtime, &self.async_bridge) {
2653 let sender = bridge.sender();
2654 let callback_id_u64 = callback_id.as_u64();
2655 runtime.spawn(async move {
2656 tokio::time::sleep(tokio::time::Duration::from_millis(duration_ms)).await;
2657 #[allow(clippy::let_underscore_must_use)]
2658 let _ = sender.send(crate::services::async_bridge::AsyncMessage::Plugin(
2659 fresh_core::api::PluginAsyncMessage::DelayComplete {
2660 callback_id: callback_id_u64,
2661 },
2662 ));
2663 });
2664 } else {
2665 std::thread::sleep(std::time::Duration::from_millis(duration_ms));
2666 self.plugin_manager
2667 .resolve_callback(callback_id, "null".to_string());
2668 }
2669 }
2670
2671 fn handle_kill_background_process(&mut self, process_id: u64) {
2672 if let Some(handle) = self.background_process_handles.remove(&process_id) {
2673 handle.abort();
2674 tracing::debug!("Killed background process {}", process_id);
2675 }
2676 }
2677
2678 fn handle_create_virtual_buffer(&mut self, name: String, mode: String, read_only: bool) {
2679 let buffer_id = self.create_virtual_buffer(name.clone(), mode.clone(), read_only);
2680 tracing::info!(
2681 "Created virtual buffer '{}' with mode '{}' (id={:?})",
2682 name,
2683 mode,
2684 buffer_id
2685 );
2686 }
2688
2689 fn handle_set_virtual_buffer_content(
2690 &mut self,
2691 buffer_id: BufferId,
2692 entries: Vec<fresh_core::text_property::TextPropertyEntry>,
2693 ) {
2694 match self.set_virtual_buffer_content(buffer_id, entries) {
2695 Ok(()) => {
2696 tracing::debug!("Set virtual buffer content for {:?}", buffer_id);
2697 }
2698 Err(e) => {
2699 tracing::error!("Failed to set virtual buffer content: {}", e);
2700 }
2701 }
2702 }
2703
2704 fn handle_get_text_properties_at_cursor(&self, buffer_id: BufferId) {
2705 if let Some(state) = self.buffers.get(&buffer_id) {
2706 let cursor_pos = self
2707 .split_view_states
2708 .values()
2709 .find_map(|vs| vs.buffer_state(buffer_id))
2710 .map(|bs| bs.cursors.primary().position)
2711 .unwrap_or(0);
2712 let properties = state.text_properties.get_at(cursor_pos);
2713 tracing::debug!(
2714 "Text properties at cursor in {:?}: {} properties found",
2715 buffer_id,
2716 properties.len()
2717 );
2718 }
2720 }
2721
2722 fn handle_set_context(&mut self, name: String, active: bool) {
2723 if active {
2724 self.active_custom_contexts.insert(name.clone());
2725 tracing::debug!("Set custom context: {}", name);
2726 } else {
2727 self.active_custom_contexts.remove(&name);
2728 tracing::debug!("Unset custom context: {}", name);
2729 }
2730 }
2731
2732 fn handle_disable_lsp_for_language(&mut self, language: String) {
2733 tracing::info!("Disabling LSP for language: {}", language);
2734 if let Some(ref mut lsp) = self.lsp {
2735 lsp.shutdown_server(&language);
2736 tracing::info!("Stopped LSP server for {}", language);
2737 }
2738 if let Some(lsp_configs) = self.config_mut().lsp.get_mut(&language) {
2739 for c in lsp_configs.as_mut_slice() {
2740 c.enabled = false;
2741 c.auto_start = false;
2742 }
2743 tracing::info!("Disabled LSP config for {}", language);
2744 }
2745 if let Err(e) = self.save_config() {
2746 tracing::error!("Failed to save config: {}", e);
2747 self.status_message = Some(format!(
2748 "LSP disabled for {} (config save failed)",
2749 language
2750 ));
2751 } else {
2752 self.status_message = Some(format!("LSP disabled for {}", language));
2753 }
2754 self.warning_domains.lsp.clear();
2755 }
2756
2757 fn handle_restart_lsp_for_language(&mut self, language: String) {
2758 tracing::info!("Plugin restarting LSP for language: {}", language);
2759 let file_path = self
2760 .buffer_metadata
2761 .get(&self.active_buffer())
2762 .and_then(|meta| meta.file_path().cloned());
2763 let success = if let Some(ref mut lsp) = self.lsp {
2764 let (ok, msg) = lsp.manual_restart(&language, file_path.as_deref());
2765 self.status_message = Some(msg);
2766 ok
2767 } else {
2768 self.status_message = Some("No LSP manager available".to_string());
2769 false
2770 };
2771 if success {
2772 self.reopen_buffers_for_language(&language);
2773 }
2774 }
2775
2776 fn handle_set_lsp_root_uri(&mut self, language: String, uri: String) {
2777 tracing::info!("Plugin setting LSP root URI for {}: {}", language, uri);
2778 match uri.parse::<lsp_types::Uri>() {
2779 Ok(parsed_uri) => {
2780 if let Some(ref mut lsp) = self.lsp {
2781 let restarted = lsp.set_language_root_uri(&language, parsed_uri);
2782 if restarted {
2783 self.status_message = Some(format!(
2784 "LSP root updated for {} (restarting server)",
2785 language
2786 ));
2787 } else {
2788 self.status_message = Some(format!("LSP root set for {}", language));
2789 }
2790 }
2791 }
2792 Err(e) => {
2793 tracing::error!("Invalid LSP root URI '{}': {}", uri, e);
2794 self.status_message = Some(format!("Invalid LSP root URI: {}", e));
2795 }
2796 }
2797 }
2798
2799 fn handle_create_scroll_sync_group(
2800 &mut self,
2801 group_id: crate::view::scroll_sync::ScrollSyncGroupId,
2802 left_split: SplitId,
2803 right_split: SplitId,
2804 ) {
2805 let success =
2806 self.scroll_sync_manager
2807 .create_group_with_id(group_id, left_split, right_split);
2808 if success {
2809 tracing::debug!(
2810 "Created scroll sync group {} for splits {:?} and {:?}",
2811 group_id,
2812 left_split,
2813 right_split
2814 );
2815 } else {
2816 tracing::warn!(
2817 "Failed to create scroll sync group {} (ID already exists)",
2818 group_id
2819 );
2820 }
2821 }
2822
2823 fn handle_set_scroll_sync_anchors(
2824 &mut self,
2825 group_id: crate::view::scroll_sync::ScrollSyncGroupId,
2826 anchors: Vec<(usize, usize)>,
2827 ) {
2828 use crate::view::scroll_sync::SyncAnchor;
2829 let anchor_count = anchors.len();
2830 let sync_anchors: Vec<SyncAnchor> = anchors
2831 .into_iter()
2832 .map(|(left_line, right_line)| SyncAnchor {
2833 left_line,
2834 right_line,
2835 })
2836 .collect();
2837 self.scroll_sync_manager.set_anchors(group_id, sync_anchors);
2838 tracing::debug!(
2839 "Set {} anchors for scroll sync group {}",
2840 anchor_count,
2841 group_id
2842 );
2843 }
2844
2845 fn handle_remove_scroll_sync_group(
2846 &mut self,
2847 group_id: crate::view::scroll_sync::ScrollSyncGroupId,
2848 ) {
2849 if self.scroll_sync_manager.remove_group(group_id) {
2850 tracing::debug!("Removed scroll sync group {}", group_id);
2851 } else {
2852 tracing::warn!("Scroll sync group {} not found", group_id);
2853 }
2854 }
2855
2856 fn handle_create_buffer_group(
2857 &mut self,
2858 name: String,
2859 mode: String,
2860 layout_json: String,
2861 request_id: Option<u64>,
2862 ) {
2863 match self.create_buffer_group(name, mode, layout_json) {
2864 Ok(result) => {
2865 if let Some(req_id) = request_id {
2866 let json = serde_json::to_string(&result).unwrap_or_default();
2867 self.plugin_manager
2868 .resolve_callback(fresh_core::api::JsCallbackId::from(req_id), json);
2869 }
2870 }
2871 Err(e) => {
2872 tracing::error!("Failed to create buffer group: {}", e);
2873 }
2874 }
2875 }
2876
2877 fn handle_send_terminal_input(
2878 &mut self,
2879 terminal_id: crate::services::terminal::TerminalId,
2880 data: String,
2881 ) {
2882 if let Some(handle) = self.terminal_manager.get(terminal_id) {
2883 handle.write(data.as_bytes());
2884 tracing::trace!(
2885 "Plugin sent {} bytes to terminal {:?}",
2886 data.len(),
2887 terminal_id
2888 );
2889 } else {
2890 tracing::warn!(
2891 "Plugin tried to send input to non-existent terminal {:?}",
2892 terminal_id
2893 );
2894 }
2895 }
2896
2897 fn handle_close_terminal(&mut self, terminal_id: crate::services::terminal::TerminalId) {
2898 let buffer_to_close = self
2899 .terminal_buffers
2900 .iter()
2901 .find(|(_, &tid)| tid == terminal_id)
2902 .map(|(&bid, _)| bid);
2903 if let Some(buffer_id) = buffer_to_close {
2904 if let Err(e) = self.close_buffer(buffer_id) {
2905 tracing::warn!("Failed to close terminal buffer: {}", e);
2906 }
2907 tracing::info!("Plugin closed terminal {:?}", terminal_id);
2908 } else {
2909 self.terminal_manager.close(terminal_id);
2910 tracing::info!("Plugin closed terminal {:?} (no buffer found)", terminal_id);
2911 }
2912 }
2913}
2914
2915#[cfg(test)]
2916mod tests {
2917 use tokio::io::{AsyncReadExt, BufReader};
2930 use tokio::process::Command as TokioCommand;
2931 use tokio::time::{timeout, Duration};
2932
2933 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2944 async fn kill_via_oneshot_terminates_long_running_child() {
2945 let mut cmd = TokioCommand::new("sleep");
2946 cmd.args(["30"]);
2947 cmd.stdout(std::process::Stdio::piped());
2948 cmd.stderr(std::process::Stdio::piped());
2949
2950 let mut child = cmd.spawn().expect("spawn sh -c sleep 30");
2951 let pid = child.id().expect("child has a pid");
2952
2953 let (kill_tx, mut kill_rx) = tokio::sync::oneshot::channel::<()>();
2954 let stdout_pipe = child.stdout.take();
2955 let stderr_pipe = child.stderr.take();
2956
2957 let stdout_fut = async {
2958 let mut buf = String::new();
2959 if let Some(s) = stdout_pipe {
2960 #[allow(clippy::let_underscore_must_use)]
2961 let _ = BufReader::new(s).read_to_string(&mut buf).await;
2962 }
2963 buf
2964 };
2965 let stderr_fut = async {
2966 let mut buf = String::new();
2967 if let Some(s) = stderr_pipe {
2968 #[allow(clippy::let_underscore_must_use)]
2969 let _ = BufReader::new(s).read_to_string(&mut buf).await;
2970 }
2971 buf
2972 };
2973 let wait_fut = async {
2974 tokio::select! {
2975 status = child.wait() => {
2976 status.map(|s| s.code().unwrap_or(-1)).unwrap_or(-1)
2977 }
2978 _ = &mut kill_rx => {
2979 #[allow(clippy::let_underscore_must_use)]
2980 let _ = child.start_kill();
2981 child
2982 .wait()
2983 .await
2984 .map(|s| s.code().unwrap_or(-1))
2985 .unwrap_or(-1)
2986 }
2987 }
2988 };
2989
2990 tokio::time::sleep(Duration::from_millis(50)).await;
2995 kill_tx.send(()).expect("kill channel send");
2996
2997 let result = timeout(Duration::from_secs(5), async {
2998 tokio::join!(stdout_fut, stderr_fut, wait_fut)
2999 })
3000 .await;
3001
3002 let (_stdout, _stderr, exit_code) = result.expect(
3003 "kill path must resolve within 5s — if this times out the \
3004 select! arm order or kill-then-wait logic is broken",
3005 );
3006 assert_ne!(
3018 exit_code, 0,
3019 "killed child must exit non-success (got 0 — did the \
3020 kill arm fire too late, or did sleep somehow complete?)"
3021 );
3022
3023 #[cfg(unix)]
3032 {
3033 let still_alive = std::process::Command::new("kill")
3034 .args(["-0", &pid.to_string()])
3035 .status()
3036 .map(|s| s.success())
3037 .unwrap_or(false);
3038 assert!(
3039 !still_alive,
3040 "process {pid} must be reaped after wait() — a still-\
3041 alive check means the kill path leaked the child"
3042 );
3043 }
3044 #[cfg(not(unix))]
3045 {
3046 let _ = pid;
3049 }
3050 }
3051}