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