1use std::path::PathBuf;
15use std::sync::Arc;
16
17use anyhow::Result as AnyhowResult;
18use rust_i18n::t;
19
20use fresh_core::api::{BufferSavedDiff, JsCallbackId, PluginCommand};
21
22use crate::input::keybindings::Action;
23use crate::model::event::{BufferId, Event, LeafId, SplitId};
24use crate::services::async_bridge::AsyncMessage;
25use crate::view::split::SplitViewState;
26
27use super::Editor;
28
29impl Editor {
30 #[cfg(feature = "plugins")]
32 pub(super) fn update_plugin_state_snapshot(&mut self) {
33 if let Some(snapshot_handle) = self.plugin_manager.state_snapshot_handle() {
35 use fresh_core::api::{BufferInfo, CursorInfo, ViewportInfo};
36 let mut snapshot = snapshot_handle.write().unwrap();
37
38 let grammar_count = self.grammar_registry.available_syntaxes().len();
40 if snapshot.available_grammars.len() != grammar_count {
41 snapshot.available_grammars = self
42 .grammar_registry
43 .available_grammar_info()
44 .into_iter()
45 .map(|g| fresh_core::api::GrammarInfoSnapshot {
46 name: g.name,
47 source: g.source.to_string(),
48 file_extensions: g.file_extensions,
49 short_name: g.short_name,
50 })
51 .collect();
52 }
53
54 snapshot.active_buffer_id = self.active_buffer();
56
57 snapshot.active_split_id = self.split_manager.active_split().0 .0;
59
60 snapshot.buffers.clear();
62 snapshot.buffer_saved_diffs.clear();
63 snapshot.buffer_cursor_positions.clear();
64 snapshot.buffer_text_properties.clear();
65
66 for (buffer_id, state) in &self.buffers {
67 let is_virtual = self
68 .buffer_metadata
69 .get(buffer_id)
70 .map(|m| m.is_virtual())
71 .unwrap_or(false);
72 let active_split = self.split_manager.active_split();
77 let active_vs = self.split_view_states.get(&active_split);
78 let view_mode = active_vs
79 .and_then(|vs| vs.buffer_state(*buffer_id))
80 .map(|bs| match bs.view_mode {
81 crate::state::ViewMode::Source => "source",
82 crate::state::ViewMode::PageView => "compose",
83 })
84 .unwrap_or("source");
85 let compose_width = active_vs
86 .and_then(|vs| vs.buffer_state(*buffer_id))
87 .and_then(|bs| bs.compose_width);
88 let is_composing_in_any_split = self.split_view_states.values().any(|vs| {
89 vs.buffer_state(*buffer_id)
90 .map(|bs| matches!(bs.view_mode, crate::state::ViewMode::PageView))
91 .unwrap_or(false)
92 });
93 let is_preview = self
94 .buffer_metadata
95 .get(buffer_id)
96 .map(|m| m.is_preview)
97 .unwrap_or(false);
98 let buffer_info = BufferInfo {
99 id: *buffer_id,
100 path: state.buffer.file_path().map(|p| p.to_path_buf()),
101 modified: state.buffer.is_modified(),
102 length: state.buffer.len(),
103 is_virtual,
104 view_mode: view_mode.to_string(),
105 is_composing_in_any_split,
106 compose_width,
107 language: state.language.clone(),
108 is_preview,
109 };
110 snapshot.buffers.insert(*buffer_id, buffer_info);
111
112 let diff = {
113 let diff = state.buffer.diff_since_saved();
114 BufferSavedDiff {
115 equal: diff.equal,
116 byte_ranges: diff.byte_ranges.clone(),
117 }
118 };
119 snapshot.buffer_saved_diffs.insert(*buffer_id, diff);
120
121 let is_hidden = self
130 .buffer_metadata
131 .get(buffer_id)
132 .is_some_and(|m| m.hidden_from_tabs);
133 let source_split = self.split_view_states.iter().find(|(split_id, vs)| {
134 vs.keyed_states.contains_key(buffer_id)
135 && !(is_hidden && self.grouped_subtrees.contains_key(split_id))
136 });
137 let cursor_pos = source_split
138 .and_then(|(_, vs)| vs.buffer_state(*buffer_id))
139 .map(|bs| bs.cursors.primary().position)
140 .unwrap_or(0);
141 tracing::trace!(
142 "snapshot: buffer {:?} cursor_pos={} (from split {:?})",
143 buffer_id,
144 cursor_pos,
145 source_split.map(|(id, _)| *id),
146 );
147 snapshot
148 .buffer_cursor_positions
149 .insert(*buffer_id, cursor_pos);
150
151 if !state.text_properties.is_empty() {
153 snapshot
154 .buffer_text_properties
155 .insert(*buffer_id, state.text_properties.all().to_vec());
156 }
157 }
158
159 if let Some(active_vs) = self
161 .split_view_states
162 .get(&self.split_manager.active_split())
163 {
164 let active_cursors = &active_vs.cursors;
166 let primary = active_cursors.primary();
167 let primary_position = primary.position;
168 let primary_selection = primary.selection_range();
169
170 snapshot.primary_cursor = Some(CursorInfo {
171 position: primary_position,
172 selection: primary_selection.clone(),
173 });
174
175 snapshot.all_cursors = active_cursors
177 .iter()
178 .map(|(_, cursor)| CursorInfo {
179 position: cursor.position,
180 selection: cursor.selection_range(),
181 })
182 .collect();
183
184 if let Some(range) = primary_selection {
186 if let Some(active_state) = self.buffers.get_mut(&self.active_buffer()) {
187 snapshot.selected_text =
188 Some(active_state.get_text_range(range.start, range.end));
189 }
190 }
191
192 let top_line = self.buffers.get(&self.active_buffer()).and_then(|state| {
194 if state.buffer.line_count().is_some() {
195 Some(state.buffer.get_line_number(active_vs.viewport.top_byte))
196 } else {
197 None
198 }
199 });
200 snapshot.viewport = Some(ViewportInfo {
201 top_byte: active_vs.viewport.top_byte,
202 top_line,
203 left_column: active_vs.viewport.left_column,
204 width: active_vs.viewport.width,
205 height: active_vs.viewport.height,
206 });
207 } else {
208 snapshot.primary_cursor = None;
209 snapshot.all_cursors.clear();
210 snapshot.viewport = None;
211 snapshot.selected_text = None;
212 }
213
214 snapshot.clipboard = self.clipboard.get_internal().to_string();
216
217 snapshot.working_dir = self.working_dir.clone();
219
220 snapshot.diagnostics = self.stored_diagnostics.clone();
222
223 snapshot.folding_ranges = self.stored_folding_ranges.clone();
225
226 snapshot.config = serde_json::to_value(&self.config).unwrap_or(serde_json::Value::Null);
228
229 snapshot.user_config = self.user_config_raw.clone();
232
233 snapshot.editor_mode = self.editor_mode.clone();
235
236 for (plugin_name, state_map) in &self.plugin_global_state {
239 let entry = snapshot
240 .plugin_global_states
241 .entry(plugin_name.clone())
242 .or_default();
243 for (key, value) in state_map {
244 entry.entry(key.clone()).or_insert_with(|| value.clone());
245 }
246 }
247
248 let active_split_id = self.split_manager.active_split().0 .0;
253 let split_changed = snapshot.plugin_view_states_split != active_split_id;
254 if split_changed {
255 snapshot.plugin_view_states.clear();
256 snapshot.plugin_view_states_split = active_split_id;
257 }
258
259 {
261 let open_bids: Vec<_> = snapshot.buffers.keys().copied().collect();
262 snapshot
263 .plugin_view_states
264 .retain(|bid, _| open_bids.contains(bid));
265 }
266
267 if let Some(active_vs) = self
269 .split_view_states
270 .get(&self.split_manager.active_split())
271 {
272 for (buffer_id, buf_state) in &active_vs.keyed_states {
273 if !buf_state.plugin_state.is_empty() {
274 let entry = snapshot.plugin_view_states.entry(*buffer_id).or_default();
275 for (key, value) in &buf_state.plugin_state {
276 entry.entry(key.clone()).or_insert_with(|| value.clone());
278 }
279 }
280 }
281 }
282 }
283 }
284
285 pub fn handle_plugin_command(&mut self, command: PluginCommand) -> AnyhowResult<()> {
287 match command {
288 PluginCommand::InsertText {
290 buffer_id,
291 position,
292 text,
293 } => {
294 self.handle_insert_text(buffer_id, position, text);
295 }
296 PluginCommand::DeleteRange { buffer_id, range } => {
297 self.handle_delete_range(buffer_id, range);
298 }
299 PluginCommand::InsertAtCursor { text } => {
300 self.handle_insert_at_cursor(text);
301 }
302 PluginCommand::DeleteSelection => {
303 self.handle_delete_selection();
304 }
305
306 PluginCommand::AddOverlay {
308 buffer_id,
309 namespace,
310 range,
311 options,
312 } => {
313 self.handle_add_overlay(buffer_id, namespace, range, options);
314 }
315 PluginCommand::RemoveOverlay { buffer_id, handle } => {
316 self.handle_remove_overlay(buffer_id, handle);
317 }
318 PluginCommand::ClearAllOverlays { buffer_id } => {
319 self.handle_clear_all_overlays(buffer_id);
320 }
321 PluginCommand::ClearNamespace {
322 buffer_id,
323 namespace,
324 } => {
325 self.handle_clear_namespace(buffer_id, namespace);
326 }
327 PluginCommand::ClearOverlaysInRange {
328 buffer_id,
329 start,
330 end,
331 } => {
332 self.handle_clear_overlays_in_range(buffer_id, start, end);
333 }
334
335 PluginCommand::AddVirtualText {
337 buffer_id,
338 virtual_text_id,
339 position,
340 text,
341 color,
342 use_bg,
343 before,
344 } => {
345 self.handle_add_virtual_text(
346 buffer_id,
347 virtual_text_id,
348 position,
349 text,
350 color,
351 use_bg,
352 before,
353 );
354 }
355 PluginCommand::RemoveVirtualText {
356 buffer_id,
357 virtual_text_id,
358 } => {
359 self.handle_remove_virtual_text(buffer_id, virtual_text_id);
360 }
361 PluginCommand::RemoveVirtualTextsByPrefix { buffer_id, prefix } => {
362 self.handle_remove_virtual_texts_by_prefix(buffer_id, prefix);
363 }
364 PluginCommand::ClearVirtualTexts { buffer_id } => {
365 self.handle_clear_virtual_texts(buffer_id);
366 }
367 PluginCommand::AddVirtualLine {
368 buffer_id,
369 position,
370 text,
371 fg_color,
372 bg_color,
373 above,
374 namespace,
375 priority,
376 } => {
377 self.handle_add_virtual_line(
378 buffer_id, position, text, fg_color, bg_color, above, namespace, priority,
379 );
380 }
381 PluginCommand::ClearVirtualTextNamespace {
382 buffer_id,
383 namespace,
384 } => {
385 self.handle_clear_virtual_text_namespace(buffer_id, namespace);
386 }
387
388 PluginCommand::AddConceal {
390 buffer_id,
391 namespace,
392 start,
393 end,
394 replacement,
395 } => {
396 self.handle_add_conceal(buffer_id, namespace, start, end, replacement);
397 }
398 PluginCommand::ClearConcealNamespace {
399 buffer_id,
400 namespace,
401 } => {
402 self.handle_clear_conceal_namespace(buffer_id, namespace);
403 }
404 PluginCommand::ClearConcealsInRange {
405 buffer_id,
406 start,
407 end,
408 } => {
409 self.handle_clear_conceals_in_range(buffer_id, start, end);
410 }
411
412 PluginCommand::AddFold {
413 buffer_id,
414 start,
415 end,
416 placeholder,
417 } => {
418 self.handle_add_fold(buffer_id, start, end, placeholder);
419 }
420 PluginCommand::ClearFolds { buffer_id } => {
421 self.handle_clear_folds(buffer_id);
422 }
423
424 PluginCommand::AddSoftBreak {
426 buffer_id,
427 namespace,
428 position,
429 indent,
430 } => {
431 self.handle_add_soft_break(buffer_id, namespace, position, indent);
432 }
433 PluginCommand::ClearSoftBreakNamespace {
434 buffer_id,
435 namespace,
436 } => {
437 self.handle_clear_soft_break_namespace(buffer_id, namespace);
438 }
439 PluginCommand::ClearSoftBreaksInRange {
440 buffer_id,
441 start,
442 end,
443 } => {
444 self.handle_clear_soft_breaks_in_range(buffer_id, start, end);
445 }
446
447 PluginCommand::AddMenuItem {
449 menu_label,
450 item,
451 position,
452 } => {
453 self.handle_add_menu_item(menu_label, item, position);
454 }
455 PluginCommand::AddMenu { menu, position } => {
456 self.handle_add_menu(menu, position);
457 }
458 PluginCommand::RemoveMenuItem {
459 menu_label,
460 item_label,
461 } => {
462 self.handle_remove_menu_item(menu_label, item_label);
463 }
464 PluginCommand::RemoveMenu { menu_label } => {
465 self.handle_remove_menu(menu_label);
466 }
467
468 PluginCommand::FocusSplit { split_id } => {
470 self.handle_focus_split(split_id);
471 }
472 PluginCommand::SetSplitBuffer {
473 split_id,
474 buffer_id,
475 } => {
476 self.handle_set_split_buffer(split_id, buffer_id);
477 }
478 PluginCommand::SetSplitScroll { split_id, top_byte } => {
479 self.handle_set_split_scroll(split_id, top_byte);
480 }
481 PluginCommand::RequestHighlights {
482 buffer_id,
483 range,
484 request_id,
485 } => {
486 self.handle_request_highlights(buffer_id, range, request_id);
487 }
488 PluginCommand::CloseSplit { split_id } => {
489 self.handle_close_split(split_id);
490 }
491 PluginCommand::SetSplitRatio { split_id, ratio } => {
492 self.handle_set_split_ratio(split_id, ratio);
493 }
494 PluginCommand::SetSplitLabel { split_id, label } => {
495 self.split_manager.set_label(LeafId(split_id), label);
496 }
497 PluginCommand::ClearSplitLabel { split_id } => {
498 self.split_manager.clear_label(split_id);
499 }
500 PluginCommand::GetSplitByLabel { label, request_id } => {
501 let split_id = self.split_manager.find_split_by_label(&label);
502 let callback_id = fresh_core::api::JsCallbackId::from(request_id);
503 let json = serde_json::to_string(&split_id.map(|s| s.0 .0))
504 .unwrap_or_else(|_| "null".to_string());
505 self.plugin_manager.resolve_callback(callback_id, json);
506 }
507 PluginCommand::DistributeSplitsEvenly { split_ids: _ } => {
508 self.handle_distribute_splits_evenly();
509 }
510 PluginCommand::SetBufferCursor {
511 buffer_id,
512 position,
513 } => {
514 self.handle_set_buffer_cursor(buffer_id, position);
515 }
516 PluginCommand::SetBufferShowCursors { buffer_id, show } => {
517 if let Some(state) = self.buffers.get_mut(&buffer_id) {
518 state.show_cursors = show;
519 } else {
520 tracing::warn!("SetBufferShowCursors: buffer {:?} not found", buffer_id);
521 }
522 }
523
524 PluginCommand::SetLayoutHints {
526 buffer_id,
527 split_id,
528 range: _,
529 hints,
530 } => {
531 self.handle_set_layout_hints(buffer_id, split_id, hints);
532 }
533 PluginCommand::SetLineNumbers { buffer_id, enabled } => {
534 self.handle_set_line_numbers(buffer_id, enabled);
535 }
536 PluginCommand::SetViewMode { buffer_id, mode } => {
537 self.handle_set_view_mode(buffer_id, &mode);
538 }
539 PluginCommand::SetLineWrap {
540 buffer_id,
541 split_id,
542 enabled,
543 } => {
544 self.handle_set_line_wrap(buffer_id, split_id, enabled);
545 }
546 PluginCommand::SubmitViewTransform {
547 buffer_id,
548 split_id,
549 payload,
550 } => {
551 self.handle_submit_view_transform(buffer_id, split_id, payload);
552 }
553 PluginCommand::ClearViewTransform {
554 buffer_id: _,
555 split_id,
556 } => {
557 self.handle_clear_view_transform(split_id);
558 }
559 PluginCommand::SetViewState {
560 buffer_id,
561 key,
562 value,
563 } => {
564 self.handle_set_view_state(buffer_id, key, value);
565 }
566 PluginCommand::SetGlobalState {
567 plugin_name,
568 key,
569 value,
570 } => {
571 self.handle_set_global_state(plugin_name, key, value);
572 }
573 PluginCommand::RefreshLines { buffer_id } => {
574 self.handle_refresh_lines(buffer_id);
575 }
576 PluginCommand::RefreshAllLines => {
577 self.handle_refresh_all_lines();
578 }
579 PluginCommand::HookCompleted { .. } => {
580 }
582 PluginCommand::SetLineIndicator {
583 buffer_id,
584 line,
585 namespace,
586 symbol,
587 color,
588 priority,
589 } => {
590 self.handle_set_line_indicator(buffer_id, line, namespace, symbol, color, priority);
591 }
592 PluginCommand::SetLineIndicators {
593 buffer_id,
594 lines,
595 namespace,
596 symbol,
597 color,
598 priority,
599 } => {
600 self.handle_set_line_indicators(
601 buffer_id, lines, namespace, symbol, color, priority,
602 );
603 }
604 PluginCommand::ClearLineIndicators {
605 buffer_id,
606 namespace,
607 } => {
608 self.handle_clear_line_indicators(buffer_id, namespace);
609 }
610 PluginCommand::SetFileExplorerDecorations {
611 namespace,
612 decorations,
613 } => {
614 self.handle_set_file_explorer_decorations(namespace, decorations);
615 }
616 PluginCommand::ClearFileExplorerDecorations { namespace } => {
617 self.handle_clear_file_explorer_decorations(&namespace);
618 }
619
620 PluginCommand::SetStatus { message } => {
622 self.handle_set_status(message);
623 }
624 PluginCommand::ApplyTheme { theme_name } => {
625 self.apply_theme(&theme_name);
626 }
627 PluginCommand::ReloadConfig => {
628 self.reload_config();
629 }
630 PluginCommand::ReloadThemes { apply_theme } => {
631 self.reload_themes();
632 if let Some(theme_name) = apply_theme {
633 self.apply_theme(&theme_name);
634 }
635 }
636 PluginCommand::RegisterGrammar {
637 language,
638 grammar_path,
639 extensions,
640 } => {
641 self.handle_register_grammar(language, grammar_path, extensions);
642 }
643 PluginCommand::RegisterLanguageConfig { language, config } => {
644 self.handle_register_language_config(language, config);
645 }
646 PluginCommand::RegisterLspServer { language, config } => {
647 self.handle_register_lsp_server(language, config);
648 }
649 PluginCommand::ReloadGrammars { callback_id } => {
650 self.handle_reload_grammars(callback_id);
651 }
652 PluginCommand::StartPrompt { label, prompt_type } => {
653 self.handle_start_prompt(label, prompt_type);
654 }
655 PluginCommand::StartPromptWithInitial {
656 label,
657 prompt_type,
658 initial_value,
659 } => {
660 self.handle_start_prompt_with_initial(label, prompt_type, initial_value);
661 }
662 PluginCommand::StartPromptAsync {
663 label,
664 initial_value,
665 callback_id,
666 } => {
667 self.handle_start_prompt_async(label, initial_value, callback_id);
668 }
669 PluginCommand::SetPromptSuggestions { suggestions } => {
670 self.handle_set_prompt_suggestions(suggestions);
671 }
672 PluginCommand::SetPromptInputSync { sync } => {
673 if let Some(prompt) = &mut self.prompt {
674 prompt.sync_input_on_navigate = sync;
675 }
676 }
677
678 PluginCommand::RegisterCommand { command } => {
680 self.handle_register_command(command);
681 }
682 PluginCommand::UnregisterCommand { name } => {
683 self.handle_unregister_command(name);
684 }
685 PluginCommand::DefineMode {
686 name,
687 bindings,
688 read_only,
689 allow_text_input,
690 inherit_normal_bindings,
691 plugin_name,
692 } => {
693 self.handle_define_mode(
694 name,
695 bindings,
696 read_only,
697 allow_text_input,
698 inherit_normal_bindings,
699 plugin_name,
700 );
701 }
702
703 PluginCommand::OpenFileInBackground { path } => {
705 self.handle_open_file_in_background(path);
706 }
707 PluginCommand::OpenFileAtLocation { path, line, column } => {
708 return self.handle_open_file_at_location(path, line, column);
709 }
710 PluginCommand::OpenFileInSplit {
711 split_id,
712 path,
713 line,
714 column,
715 } => {
716 return self.handle_open_file_in_split(split_id, path, line, column);
717 }
718 PluginCommand::ShowBuffer { buffer_id } => {
719 self.handle_show_buffer(buffer_id);
720 }
721 PluginCommand::CloseBuffer { buffer_id } => {
722 self.handle_close_buffer(buffer_id);
723 }
724
725 PluginCommand::SendLspRequest {
727 language,
728 method,
729 params,
730 request_id,
731 } => {
732 self.handle_send_lsp_request(language, method, params, request_id);
733 }
734
735 PluginCommand::SetClipboard { text } => {
737 self.handle_set_clipboard(text);
738 }
739
740 PluginCommand::SpawnProcess {
742 command,
743 args,
744 cwd,
745 callback_id,
746 } => {
747 if let (Some(runtime), Some(bridge)) = (&self.tokio_runtime, &self.async_bridge) {
750 let effective_cwd = cwd.or_else(|| {
751 std::env::current_dir()
752 .map(|p| p.to_string_lossy().to_string())
753 .ok()
754 });
755 let sender = bridge.sender();
756 let spawner = self.process_spawner.clone();
757
758 runtime.spawn(async move {
759 #[allow(clippy::let_underscore_must_use)]
761 match spawner.spawn(command, args, effective_cwd).await {
762 Ok(result) => {
763 let _ = sender.send(AsyncMessage::PluginProcessOutput {
764 process_id: callback_id.as_u64(),
765 stdout: result.stdout,
766 stderr: result.stderr,
767 exit_code: result.exit_code,
768 });
769 }
770 Err(e) => {
771 let _ = sender.send(AsyncMessage::PluginProcessOutput {
772 process_id: callback_id.as_u64(),
773 stdout: String::new(),
774 stderr: e.to_string(),
775 exit_code: -1,
776 });
777 }
778 }
779 });
780 } else {
781 self.plugin_manager
783 .reject_callback(callback_id, "Async runtime not available".to_string());
784 }
785 }
786
787 PluginCommand::SpawnProcessWait {
788 process_id,
789 callback_id,
790 } => {
791 tracing::warn!(
794 "SpawnProcessWait not fully implemented - process_id={}",
795 process_id
796 );
797 self.plugin_manager.reject_callback(
798 callback_id,
799 format!(
800 "SpawnProcessWait not yet fully implemented for process_id={}",
801 process_id
802 ),
803 );
804 }
805
806 PluginCommand::Delay {
807 callback_id,
808 duration_ms,
809 } => {
810 if let (Some(runtime), Some(bridge)) = (&self.tokio_runtime, &self.async_bridge) {
812 let sender = bridge.sender();
813 let callback_id_u64 = callback_id.as_u64();
814 runtime.spawn(async move {
815 tokio::time::sleep(tokio::time::Duration::from_millis(duration_ms)).await;
816 #[allow(clippy::let_underscore_must_use)]
818 let _ = sender.send(crate::services::async_bridge::AsyncMessage::Plugin(
819 fresh_core::api::PluginAsyncMessage::DelayComplete {
820 callback_id: callback_id_u64,
821 },
822 ));
823 });
824 } else {
825 std::thread::sleep(std::time::Duration::from_millis(duration_ms));
827 self.plugin_manager
828 .resolve_callback(callback_id, "null".to_string());
829 }
830 }
831
832 PluginCommand::SpawnBackgroundProcess {
833 process_id,
834 command,
835 args,
836 cwd,
837 callback_id,
838 } => {
839 if let (Some(runtime), Some(bridge)) = (&self.tokio_runtime, &self.async_bridge) {
841 use tokio::io::{AsyncBufReadExt, BufReader};
842 use tokio::process::Command as TokioCommand;
843
844 let effective_cwd = cwd.unwrap_or_else(|| {
845 std::env::current_dir()
846 .map(|p| p.to_string_lossy().to_string())
847 .unwrap_or_else(|_| ".".to_string())
848 });
849
850 let sender = bridge.sender();
851 let sender_stdout = sender.clone();
852 let sender_stderr = sender.clone();
853 let callback_id_u64 = callback_id.as_u64();
854
855 #[allow(clippy::let_underscore_must_use)]
857 let handle = runtime.spawn(async move {
858 let mut child = match TokioCommand::new(&command)
859 .args(&args)
860 .current_dir(&effective_cwd)
861 .stdout(std::process::Stdio::piped())
862 .stderr(std::process::Stdio::piped())
863 .spawn()
864 {
865 Ok(child) => child,
866 Err(e) => {
867 let _ = sender.send(
868 crate::services::async_bridge::AsyncMessage::Plugin(
869 fresh_core::api::PluginAsyncMessage::ProcessExit {
870 process_id,
871 callback_id: callback_id_u64,
872 exit_code: -1,
873 },
874 ),
875 );
876 tracing::error!("Failed to spawn background process: {}", e);
877 return;
878 }
879 };
880
881 let stdout = child.stdout.take();
883 let stderr = child.stderr.take();
884 let pid = process_id;
885
886 if let Some(stdout) = stdout {
888 let sender = sender_stdout;
889 tokio::spawn(async move {
890 let reader = BufReader::new(stdout);
891 let mut lines = reader.lines();
892 while let Ok(Some(line)) = lines.next_line().await {
893 let _ = sender.send(
894 crate::services::async_bridge::AsyncMessage::Plugin(
895 fresh_core::api::PluginAsyncMessage::ProcessStdout {
896 process_id: pid,
897 data: line + "\n",
898 },
899 ),
900 );
901 }
902 });
903 }
904
905 if let Some(stderr) = stderr {
907 let sender = sender_stderr;
908 tokio::spawn(async move {
909 let reader = BufReader::new(stderr);
910 let mut lines = reader.lines();
911 while let Ok(Some(line)) = lines.next_line().await {
912 let _ = sender.send(
913 crate::services::async_bridge::AsyncMessage::Plugin(
914 fresh_core::api::PluginAsyncMessage::ProcessStderr {
915 process_id: pid,
916 data: line + "\n",
917 },
918 ),
919 );
920 }
921 });
922 }
923
924 let exit_code = match child.wait().await {
926 Ok(status) => status.code().unwrap_or(-1),
927 Err(_) => -1,
928 };
929
930 let _ = sender.send(crate::services::async_bridge::AsyncMessage::Plugin(
931 fresh_core::api::PluginAsyncMessage::ProcessExit {
932 process_id,
933 callback_id: callback_id_u64,
934 exit_code,
935 },
936 ));
937 });
938
939 self.background_process_handles
941 .insert(process_id, handle.abort_handle());
942 } else {
943 self.plugin_manager
945 .reject_callback(callback_id, "Async runtime not available".to_string());
946 }
947 }
948
949 PluginCommand::KillBackgroundProcess { process_id } => {
950 if let Some(handle) = self.background_process_handles.remove(&process_id) {
951 handle.abort();
952 tracing::debug!("Killed background process {}", process_id);
953 }
954 }
955
956 PluginCommand::CreateVirtualBuffer {
958 name,
959 mode,
960 read_only,
961 } => {
962 let buffer_id = self.create_virtual_buffer(name.clone(), mode.clone(), read_only);
963 tracing::info!(
964 "Created virtual buffer '{}' with mode '{}' (id={:?})",
965 name,
966 mode,
967 buffer_id
968 );
969 }
971 PluginCommand::CreateVirtualBufferWithContent {
972 name,
973 mode,
974 read_only,
975 entries,
976 show_line_numbers,
977 show_cursors,
978 editing_disabled,
979 hidden_from_tabs,
980 request_id,
981 } => {
982 let buffer_id = self.create_virtual_buffer(name.clone(), mode.clone(), read_only);
983 tracing::info!(
984 "Created virtual buffer '{}' with mode '{}' (id={:?})",
985 name,
986 mode,
987 buffer_id
988 );
989
990 if let Some(state) = self.buffers.get_mut(&buffer_id) {
997 state.margins.configure_for_line_numbers(show_line_numbers);
998 state.show_cursors = show_cursors;
999 state.editing_disabled = editing_disabled;
1000 tracing::debug!(
1001 "Set buffer {:?} view options: show_line_numbers={}, show_cursors={}, editing_disabled={}",
1002 buffer_id,
1003 show_line_numbers,
1004 show_cursors,
1005 editing_disabled
1006 );
1007 }
1008 let active_split = self.split_manager.active_split();
1009 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
1010 view_state.ensure_buffer_state(buffer_id).show_line_numbers = show_line_numbers;
1011 }
1012
1013 if hidden_from_tabs {
1015 if let Some(meta) = self.buffer_metadata.get_mut(&buffer_id) {
1016 meta.hidden_from_tabs = true;
1017 }
1018 }
1019
1020 match self.set_virtual_buffer_content(buffer_id, entries) {
1022 Ok(()) => {
1023 tracing::debug!("Set virtual buffer content for {:?}", buffer_id);
1024 self.set_active_buffer(buffer_id);
1026 tracing::debug!("Switched to virtual buffer {:?}", buffer_id);
1027
1028 if let Some(req_id) = request_id {
1030 tracing::info!(
1031 "CreateVirtualBufferWithContent: resolving callback for request_id={}, buffer_id={:?}",
1032 req_id,
1033 buffer_id
1034 );
1035 let result = fresh_core::api::VirtualBufferResult {
1037 buffer_id: buffer_id.0 as u64,
1038 split_id: None,
1039 };
1040 self.plugin_manager.resolve_callback(
1041 fresh_core::api::JsCallbackId::from(req_id),
1042 serde_json::to_string(&result).unwrap_or_default(),
1043 );
1044 tracing::info!("CreateVirtualBufferWithContent: resolve_callback sent for request_id={}", req_id);
1045 }
1046 }
1047 Err(e) => {
1048 tracing::error!("Failed to set virtual buffer content: {}", e);
1049 }
1050 }
1051 }
1052 PluginCommand::CreateVirtualBufferInSplit {
1053 name,
1054 mode,
1055 read_only,
1056 entries,
1057 ratio,
1058 direction,
1059 panel_id,
1060 show_line_numbers,
1061 show_cursors,
1062 editing_disabled,
1063 line_wrap,
1064 before,
1065 request_id,
1066 } => {
1067 if let Some(pid) = &panel_id {
1069 if let Some(&existing_buffer_id) = self.panel_ids.get(pid) {
1070 if self.buffers.contains_key(&existing_buffer_id) {
1072 if let Err(e) =
1074 self.set_virtual_buffer_content(existing_buffer_id, entries)
1075 {
1076 tracing::error!("Failed to update panel content: {}", e);
1077 } else {
1078 tracing::info!("Updated existing panel '{}' content", pid);
1079 }
1080
1081 let splits = self.split_manager.splits_for_buffer(existing_buffer_id);
1083 if let Some(&split_id) = splits.first() {
1084 self.split_manager.set_active_split(split_id);
1085 self.split_manager.set_active_buffer_id(existing_buffer_id);
1088 tracing::debug!(
1089 "Focused split {:?} containing panel buffer",
1090 split_id
1091 );
1092 }
1093
1094 if let Some(req_id) = request_id {
1096 let result = fresh_core::api::VirtualBufferResult {
1097 buffer_id: existing_buffer_id.0 as u64,
1098 split_id: splits.first().map(|s| s.0 .0 as u64),
1099 };
1100 self.plugin_manager.resolve_callback(
1101 fresh_core::api::JsCallbackId::from(req_id),
1102 serde_json::to_string(&result).unwrap_or_default(),
1103 );
1104 }
1105 return Ok(());
1106 } else {
1107 tracing::warn!(
1109 "Removing stale panel_id '{}' pointing to non-existent buffer {:?}",
1110 pid,
1111 existing_buffer_id
1112 );
1113 self.panel_ids.remove(pid);
1114 }
1116 }
1117 }
1118
1119 let source_split_before_create = self.split_manager.active_split();
1125
1126 let buffer_id = self.create_virtual_buffer(name.clone(), mode.clone(), read_only);
1128 tracing::info!(
1129 "Created virtual buffer '{}' with mode '{}' in split (id={:?})",
1130 name,
1131 mode,
1132 buffer_id
1133 );
1134
1135 if let Some(state) = self.buffers.get_mut(&buffer_id) {
1137 state.margins.configure_for_line_numbers(show_line_numbers);
1138 state.show_cursors = show_cursors;
1139 state.editing_disabled = editing_disabled;
1140 tracing::debug!(
1141 "Set buffer {:?} view options: show_line_numbers={}, show_cursors={}, editing_disabled={}",
1142 buffer_id,
1143 show_line_numbers,
1144 show_cursors,
1145 editing_disabled
1146 );
1147 }
1148
1149 if let Some(pid) = panel_id {
1151 self.panel_ids.insert(pid, buffer_id);
1152 }
1153
1154 if let Err(e) = self.set_virtual_buffer_content(buffer_id, entries) {
1156 tracing::error!("Failed to set virtual buffer content: {}", e);
1157 return Ok(());
1158 }
1159
1160 let split_dir = match direction.as_deref() {
1162 Some("vertical") => crate::model::event::SplitDirection::Vertical,
1163 _ => crate::model::event::SplitDirection::Horizontal,
1164 };
1165
1166 let created_split_id = match self
1168 .split_manager
1169 .split_active_positioned(split_dir, buffer_id, ratio, before)
1170 {
1171 Ok(new_split_id) => {
1172 if new_split_id != source_split_before_create {
1178 if let Some(source_view_state) =
1179 self.split_view_states.get_mut(&source_split_before_create)
1180 {
1181 source_view_state.remove_buffer(buffer_id);
1182 }
1183 }
1184 let mut view_state = SplitViewState::with_buffer(
1186 self.terminal_width,
1187 self.terminal_height,
1188 buffer_id,
1189 );
1190 view_state.apply_config_defaults(
1191 self.config.editor.line_numbers,
1192 self.config.editor.highlight_current_line,
1193 line_wrap
1194 .unwrap_or_else(|| self.resolve_line_wrap_for_buffer(buffer_id)),
1195 self.config.editor.wrap_indent,
1196 self.resolve_wrap_column_for_buffer(buffer_id),
1197 self.config.editor.rulers.clone(),
1198 );
1199 view_state.ensure_buffer_state(buffer_id).show_line_numbers =
1201 show_line_numbers;
1202 self.split_view_states.insert(new_split_id, view_state);
1203
1204 self.split_manager.set_active_split(new_split_id);
1206 tracing::info!(
1209 "Created {:?} split with virtual buffer {:?}",
1210 split_dir,
1211 buffer_id
1212 );
1213 Some(new_split_id)
1214 }
1215 Err(e) => {
1216 tracing::error!("Failed to create split: {}", e);
1217 self.set_active_buffer(buffer_id);
1219 None
1220 }
1221 };
1222
1223 if let Some(req_id) = request_id {
1226 tracing::trace!("CreateVirtualBufferInSplit: resolving callback for request_id={}, buffer_id={:?}, split_id={:?}", req_id, buffer_id, created_split_id);
1227 let result = fresh_core::api::VirtualBufferResult {
1228 buffer_id: buffer_id.0 as u64,
1229 split_id: created_split_id.map(|s| s.0 .0 as u64),
1230 };
1231 self.plugin_manager.resolve_callback(
1232 fresh_core::api::JsCallbackId::from(req_id),
1233 serde_json::to_string(&result).unwrap_or_default(),
1234 );
1235 }
1236 }
1237 PluginCommand::SetVirtualBufferContent { buffer_id, entries } => {
1238 match self.set_virtual_buffer_content(buffer_id, entries) {
1239 Ok(()) => {
1240 tracing::debug!("Set virtual buffer content for {:?}", buffer_id);
1241 }
1242 Err(e) => {
1243 tracing::error!("Failed to set virtual buffer content: {}", e);
1244 }
1245 }
1246 }
1247 PluginCommand::GetTextPropertiesAtCursor { buffer_id } => {
1248 if let Some(state) = self.buffers.get(&buffer_id) {
1250 let cursor_pos = self
1251 .split_view_states
1252 .values()
1253 .find_map(|vs| vs.buffer_state(buffer_id))
1254 .map(|bs| bs.cursors.primary().position)
1255 .unwrap_or(0);
1256 let properties = state.text_properties.get_at(cursor_pos);
1257 tracing::debug!(
1258 "Text properties at cursor in {:?}: {} properties found",
1259 buffer_id,
1260 properties.len()
1261 );
1262 }
1264 }
1265 PluginCommand::CreateVirtualBufferInExistingSplit {
1266 name,
1267 mode,
1268 read_only,
1269 entries,
1270 split_id,
1271 show_line_numbers,
1272 show_cursors,
1273 editing_disabled,
1274 line_wrap,
1275 request_id,
1276 } => {
1277 let buffer_id = self.create_virtual_buffer(name.clone(), mode.clone(), read_only);
1279 tracing::info!(
1280 "Created virtual buffer '{}' with mode '{}' for existing split {:?} (id={:?})",
1281 name,
1282 mode,
1283 split_id,
1284 buffer_id
1285 );
1286
1287 if let Some(state) = self.buffers.get_mut(&buffer_id) {
1289 state.margins.configure_for_line_numbers(show_line_numbers);
1290 state.show_cursors = show_cursors;
1291 state.editing_disabled = editing_disabled;
1292 }
1293
1294 if let Err(e) = self.set_virtual_buffer_content(buffer_id, entries) {
1296 tracing::error!("Failed to set virtual buffer content: {}", e);
1297 return Ok(());
1298 }
1299
1300 let leaf_id = LeafId(split_id);
1302 self.split_manager.set_split_buffer(leaf_id, buffer_id);
1303
1304 self.split_manager.set_active_split(leaf_id);
1306 self.split_manager.set_active_buffer_id(buffer_id);
1307
1308 if let Some(view_state) = self.split_view_states.get_mut(&leaf_id) {
1310 view_state.switch_buffer(buffer_id);
1311 view_state.add_buffer(buffer_id);
1312 view_state.ensure_buffer_state(buffer_id).show_line_numbers = show_line_numbers;
1313
1314 if let Some(wrap) = line_wrap {
1316 view_state.active_state_mut().viewport.line_wrap_enabled = wrap;
1317 }
1318 }
1319
1320 tracing::info!(
1321 "Displayed virtual buffer {:?} in split {:?}",
1322 buffer_id,
1323 split_id
1324 );
1325
1326 if let Some(req_id) = request_id {
1328 let result = fresh_core::api::VirtualBufferResult {
1329 buffer_id: buffer_id.0 as u64,
1330 split_id: Some(split_id.0 as u64),
1331 };
1332 self.plugin_manager.resolve_callback(
1333 fresh_core::api::JsCallbackId::from(req_id),
1334 serde_json::to_string(&result).unwrap_or_default(),
1335 );
1336 }
1337 }
1338
1339 PluginCommand::SetContext { name, active } => {
1341 if active {
1342 self.active_custom_contexts.insert(name.clone());
1343 tracing::debug!("Set custom context: {}", name);
1344 } else {
1345 self.active_custom_contexts.remove(&name);
1346 tracing::debug!("Unset custom context: {}", name);
1347 }
1348 }
1349
1350 PluginCommand::SetReviewDiffHunks { hunks } => {
1352 self.review_hunks = hunks;
1353 tracing::debug!("Set {} review hunks", self.review_hunks.len());
1354 }
1355
1356 PluginCommand::ExecuteAction { action_name } => {
1358 self.handle_execute_action(action_name);
1359 }
1360 PluginCommand::ExecuteActions { actions } => {
1361 self.handle_execute_actions(actions);
1362 }
1363 PluginCommand::GetBufferText {
1364 buffer_id,
1365 start,
1366 end,
1367 request_id,
1368 } => {
1369 self.handle_get_buffer_text(buffer_id, start, end, request_id);
1370 }
1371 PluginCommand::GetLineStartPosition {
1372 buffer_id,
1373 line,
1374 request_id,
1375 } => {
1376 self.handle_get_line_start_position(buffer_id, line, request_id);
1377 }
1378 PluginCommand::GetLineEndPosition {
1379 buffer_id,
1380 line,
1381 request_id,
1382 } => {
1383 self.handle_get_line_end_position(buffer_id, line, request_id);
1384 }
1385 PluginCommand::GetBufferLineCount {
1386 buffer_id,
1387 request_id,
1388 } => {
1389 self.handle_get_buffer_line_count(buffer_id, request_id);
1390 }
1391 PluginCommand::ScrollToLineCenter {
1392 split_id,
1393 buffer_id,
1394 line,
1395 } => {
1396 self.handle_scroll_to_line_center(split_id, buffer_id, line);
1397 }
1398 PluginCommand::ScrollBufferToLine { buffer_id, line } => {
1399 self.handle_scroll_buffer_to_line(buffer_id, line);
1400 }
1401 PluginCommand::SetEditorMode { mode } => {
1402 self.handle_set_editor_mode(mode);
1403 }
1404
1405 PluginCommand::ShowActionPopup {
1407 popup_id,
1408 title,
1409 message,
1410 actions,
1411 } => {
1412 tracing::info!(
1413 "Action popup requested: id={}, title={}, actions={}",
1414 popup_id,
1415 title,
1416 actions.len()
1417 );
1418
1419 let items: Vec<crate::model::event::PopupListItemData> = actions
1421 .iter()
1422 .map(|action| crate::model::event::PopupListItemData {
1423 text: action.label.clone(),
1424 detail: None,
1425 icon: None,
1426 data: Some(action.id.clone()),
1427 })
1428 .collect();
1429
1430 let action_ids: Vec<(String, String)> =
1432 actions.into_iter().map(|a| (a.id, a.label)).collect();
1433 self.active_action_popup = Some((popup_id.clone(), action_ids));
1434
1435 let popup = crate::model::event::PopupData {
1437 kind: crate::model::event::PopupKindHint::List,
1438 title: Some(title),
1439 description: Some(message),
1440 transient: false,
1441 content: crate::model::event::PopupContentData::List { items, selected: 0 },
1442 position: crate::model::event::PopupPositionData::BottomRight,
1443 width: 60,
1444 max_height: 15,
1445 bordered: true,
1446 };
1447
1448 self.show_popup(popup);
1449 tracing::info!(
1450 "Action popup shown: id={}, active_action_popup={:?}",
1451 popup_id,
1452 self.active_action_popup.as_ref().map(|(id, _)| id)
1453 );
1454 }
1455
1456 PluginCommand::DisableLspForLanguage { language } => {
1457 tracing::info!("Disabling LSP for language: {}", language);
1458
1459 if let Some(ref mut lsp) = self.lsp {
1461 lsp.shutdown_server(&language);
1462 tracing::info!("Stopped LSP server for {}", language);
1463 }
1464
1465 if let Some(lsp_configs) = self.config.lsp.get_mut(&language) {
1467 for c in lsp_configs.as_mut_slice() {
1468 c.enabled = false;
1469 c.auto_start = false;
1470 }
1471 tracing::info!("Disabled LSP config for {}", language);
1472 }
1473
1474 if let Err(e) = self.save_config() {
1476 tracing::error!("Failed to save config: {}", e);
1477 self.status_message = Some(format!(
1478 "LSP disabled for {} (config save failed)",
1479 language
1480 ));
1481 } else {
1482 self.status_message = Some(format!("LSP disabled for {}", language));
1483 }
1484
1485 self.warning_domains.lsp.clear();
1487 }
1488
1489 PluginCommand::RestartLspForLanguage { language } => {
1490 tracing::info!("Plugin restarting LSP for language: {}", language);
1491
1492 let file_path = self
1493 .buffer_metadata
1494 .get(&self.active_buffer())
1495 .and_then(|meta| meta.file_path().cloned());
1496 let success = if let Some(ref mut lsp) = self.lsp {
1497 let (ok, msg) = lsp.manual_restart(&language, file_path.as_deref());
1498 self.status_message = Some(msg);
1499 ok
1500 } else {
1501 self.status_message = Some("No LSP manager available".to_string());
1502 false
1503 };
1504
1505 if success {
1506 self.reopen_buffers_for_language(&language);
1507 }
1508 }
1509
1510 PluginCommand::SetLspRootUri { language, uri } => {
1511 tracing::info!("Plugin setting LSP root URI for {}: {}", language, uri);
1512
1513 match uri.parse::<lsp_types::Uri>() {
1515 Ok(parsed_uri) => {
1516 if let Some(ref mut lsp) = self.lsp {
1517 let restarted = lsp.set_language_root_uri(&language, parsed_uri);
1518 if restarted {
1519 self.status_message = Some(format!(
1520 "LSP root updated for {} (restarting server)",
1521 language
1522 ));
1523 } else {
1524 self.status_message =
1525 Some(format!("LSP root set for {}", language));
1526 }
1527 }
1528 }
1529 Err(e) => {
1530 tracing::error!("Invalid LSP root URI '{}': {}", uri, e);
1531 self.status_message = Some(format!("Invalid LSP root URI: {}", e));
1532 }
1533 }
1534 }
1535
1536 PluginCommand::CreateScrollSyncGroup {
1538 group_id,
1539 left_split,
1540 right_split,
1541 } => {
1542 let success = self.scroll_sync_manager.create_group_with_id(
1543 group_id,
1544 left_split,
1545 right_split,
1546 );
1547 if success {
1548 tracing::debug!(
1549 "Created scroll sync group {} for splits {:?} and {:?}",
1550 group_id,
1551 left_split,
1552 right_split
1553 );
1554 } else {
1555 tracing::warn!(
1556 "Failed to create scroll sync group {} (ID already exists)",
1557 group_id
1558 );
1559 }
1560 }
1561 PluginCommand::SetScrollSyncAnchors { group_id, anchors } => {
1562 use crate::view::scroll_sync::SyncAnchor;
1563 let anchor_count = anchors.len();
1564 let sync_anchors: Vec<SyncAnchor> = anchors
1565 .into_iter()
1566 .map(|(left_line, right_line)| SyncAnchor {
1567 left_line,
1568 right_line,
1569 })
1570 .collect();
1571 self.scroll_sync_manager.set_anchors(group_id, sync_anchors);
1572 tracing::debug!(
1573 "Set {} anchors for scroll sync group {}",
1574 anchor_count,
1575 group_id
1576 );
1577 }
1578 PluginCommand::RemoveScrollSyncGroup { group_id } => {
1579 if self.scroll_sync_manager.remove_group(group_id) {
1580 tracing::debug!("Removed scroll sync group {}", group_id);
1581 } else {
1582 tracing::warn!("Scroll sync group {} not found", group_id);
1583 }
1584 }
1585
1586 PluginCommand::CreateCompositeBuffer {
1588 name,
1589 mode,
1590 layout,
1591 sources,
1592 hunks,
1593 initial_focus_hunk,
1594 request_id,
1595 } => {
1596 self.handle_create_composite_buffer(
1597 name,
1598 mode,
1599 layout,
1600 sources,
1601 hunks,
1602 initial_focus_hunk,
1603 request_id,
1604 );
1605 }
1606 PluginCommand::UpdateCompositeAlignment { buffer_id, hunks } => {
1607 self.handle_update_composite_alignment(buffer_id, hunks);
1608 }
1609 PluginCommand::CloseCompositeBuffer { buffer_id } => {
1610 self.close_composite_buffer(buffer_id);
1611 }
1612 PluginCommand::FlushLayout => {
1613 self.flush_layout();
1614 }
1615 PluginCommand::CompositeNextHunk { buffer_id } => {
1616 let split_id = self.split_manager.active_split();
1617 self.composite_next_hunk(split_id, buffer_id);
1618 }
1619 PluginCommand::CompositePrevHunk { buffer_id } => {
1620 let split_id = self.split_manager.active_split();
1621 self.composite_prev_hunk(split_id, buffer_id);
1622 }
1623
1624 PluginCommand::CreateBufferGroup {
1626 name,
1627 mode,
1628 layout_json,
1629 request_id,
1630 } => match self.create_buffer_group(name, mode, layout_json) {
1631 Ok(result) => {
1632 if let Some(req_id) = request_id {
1633 let json = serde_json::to_string(&result).unwrap_or_default();
1634 self.plugin_manager
1635 .resolve_callback(fresh_core::api::JsCallbackId::from(req_id), json);
1636 }
1637 }
1638 Err(e) => {
1639 tracing::error!("Failed to create buffer group: {}", e);
1640 }
1641 },
1642 PluginCommand::SetPanelContent {
1643 group_id,
1644 panel_name,
1645 entries,
1646 } => {
1647 self.set_panel_content(group_id, panel_name, entries);
1648 }
1649 PluginCommand::CloseBufferGroup { group_id } => {
1650 self.close_buffer_group(group_id);
1651 }
1652 PluginCommand::FocusPanel {
1653 group_id,
1654 panel_name,
1655 } => {
1656 self.focus_panel(group_id, panel_name);
1657 }
1658
1659 PluginCommand::SaveBufferToPath { buffer_id, path } => {
1661 self.handle_save_buffer_to_path(buffer_id, path);
1662 }
1663
1664 #[cfg(feature = "plugins")]
1666 PluginCommand::LoadPlugin { path, callback_id } => {
1667 self.handle_load_plugin(path, callback_id);
1668 }
1669 #[cfg(feature = "plugins")]
1670 PluginCommand::UnloadPlugin { name, callback_id } => {
1671 self.handle_unload_plugin(name, callback_id);
1672 }
1673 #[cfg(feature = "plugins")]
1674 PluginCommand::ReloadPlugin { name, callback_id } => {
1675 self.handle_reload_plugin(name, callback_id);
1676 }
1677 #[cfg(feature = "plugins")]
1678 PluginCommand::ListPlugins { callback_id } => {
1679 self.handle_list_plugins(callback_id);
1680 }
1681 #[cfg(not(feature = "plugins"))]
1683 PluginCommand::LoadPlugin { .. }
1684 | PluginCommand::UnloadPlugin { .. }
1685 | PluginCommand::ReloadPlugin { .. }
1686 | PluginCommand::ListPlugins { .. } => {
1687 tracing::warn!("Plugin management commands require the 'plugins' feature");
1688 }
1689
1690 PluginCommand::CreateTerminal {
1692 cwd,
1693 direction,
1694 ratio,
1695 focus,
1696 request_id,
1697 } => {
1698 let (cols, rows) = self.get_terminal_dimensions();
1699
1700 if let Some(ref bridge) = self.async_bridge {
1702 self.terminal_manager.set_async_bridge(bridge.clone());
1703 }
1704
1705 let working_dir = cwd
1707 .map(std::path::PathBuf::from)
1708 .unwrap_or_else(|| self.working_dir.clone());
1709
1710 let terminal_root = self.dir_context.terminal_dir_for(&working_dir);
1712 if let Err(e) = self.filesystem.create_dir_all(&terminal_root) {
1713 tracing::warn!("Failed to create terminal directory: {}", e);
1714 }
1715 let predicted_terminal_id = self.terminal_manager.next_terminal_id();
1716 let log_path =
1717 terminal_root.join(format!("fresh-terminal-{}.log", predicted_terminal_id.0));
1718 let backing_path =
1719 terminal_root.join(format!("fresh-terminal-{}.txt", predicted_terminal_id.0));
1720 self.terminal_backing_files
1721 .insert(predicted_terminal_id, backing_path);
1722 let backing_path_for_spawn = self
1723 .terminal_backing_files
1724 .get(&predicted_terminal_id)
1725 .cloned();
1726
1727 match self.terminal_manager.spawn(
1728 cols,
1729 rows,
1730 Some(working_dir),
1731 Some(log_path.clone()),
1732 backing_path_for_spawn,
1733 ) {
1734 Ok(terminal_id) => {
1735 self.terminal_log_files
1737 .insert(terminal_id, log_path.clone());
1738 if terminal_id != predicted_terminal_id {
1740 self.terminal_backing_files.remove(&predicted_terminal_id);
1741 let backing_path =
1742 terminal_root.join(format!("fresh-terminal-{}.txt", terminal_id.0));
1743 self.terminal_backing_files
1744 .insert(terminal_id, backing_path);
1745 }
1746
1747 let active_split = self.split_manager.active_split();
1749 let buffer_id =
1750 self.create_terminal_buffer_attached(terminal_id, active_split);
1751
1752 let created_split_id = if let Some(dir_str) = direction.as_deref() {
1756 let split_dir = match dir_str {
1757 "horizontal" => crate::model::event::SplitDirection::Horizontal,
1758 _ => crate::model::event::SplitDirection::Vertical,
1759 };
1760
1761 let split_ratio = ratio.unwrap_or(0.5);
1762 match self
1763 .split_manager
1764 .split_active(split_dir, buffer_id, split_ratio)
1765 {
1766 Ok(new_split_id) => {
1767 let mut view_state = SplitViewState::with_buffer(
1768 self.terminal_width,
1769 self.terminal_height,
1770 buffer_id,
1771 );
1772 view_state.apply_config_defaults(
1773 self.config.editor.line_numbers,
1774 self.config.editor.highlight_current_line,
1775 false,
1776 false,
1777 None,
1778 self.config.editor.rulers.clone(),
1779 );
1780 self.split_view_states.insert(new_split_id, view_state);
1781
1782 if focus.unwrap_or(true) {
1783 self.split_manager.set_active_split(new_split_id);
1784 }
1785
1786 tracing::info!(
1787 "Created {:?} split for terminal {:?} with buffer {:?}",
1788 split_dir,
1789 terminal_id,
1790 buffer_id
1791 );
1792 Some(new_split_id)
1793 }
1794 Err(e) => {
1795 tracing::error!("Failed to create split for terminal: {}", e);
1796 self.set_active_buffer(buffer_id);
1797 None
1798 }
1799 }
1800 } else {
1801 self.set_active_buffer(buffer_id);
1803 None
1804 };
1805
1806 self.resize_visible_terminals();
1808
1809 let result = fresh_core::api::TerminalResult {
1811 buffer_id: buffer_id.0 as u64,
1812 terminal_id: terminal_id.0 as u64,
1813 split_id: created_split_id.map(|s| s.0 .0 as u64),
1814 };
1815 self.plugin_manager.resolve_callback(
1816 fresh_core::api::JsCallbackId::from(request_id),
1817 serde_json::to_string(&result).unwrap_or_default(),
1818 );
1819
1820 tracing::info!(
1821 "Plugin created terminal {:?} with buffer {:?}",
1822 terminal_id,
1823 buffer_id
1824 );
1825 }
1826 Err(e) => {
1827 tracing::error!("Failed to create terminal for plugin: {}", e);
1828 self.plugin_manager.reject_callback(
1829 fresh_core::api::JsCallbackId::from(request_id),
1830 format!("Failed to create terminal: {}", e),
1831 );
1832 }
1833 }
1834 }
1835
1836 PluginCommand::SendTerminalInput { terminal_id, data } => {
1837 if let Some(handle) = self.terminal_manager.get(terminal_id) {
1838 handle.write(data.as_bytes());
1839 tracing::trace!(
1840 "Plugin sent {} bytes to terminal {:?}",
1841 data.len(),
1842 terminal_id
1843 );
1844 } else {
1845 tracing::warn!(
1846 "Plugin tried to send input to non-existent terminal {:?}",
1847 terminal_id
1848 );
1849 }
1850 }
1851
1852 PluginCommand::CloseTerminal { terminal_id } => {
1853 let buffer_to_close = self
1855 .terminal_buffers
1856 .iter()
1857 .find(|(_, &tid)| tid == terminal_id)
1858 .map(|(&bid, _)| bid);
1859
1860 if let Some(buffer_id) = buffer_to_close {
1861 if let Err(e) = self.close_buffer(buffer_id) {
1862 tracing::warn!("Failed to close terminal buffer: {}", e);
1863 }
1864 tracing::info!("Plugin closed terminal {:?}", terminal_id);
1865 } else {
1866 self.terminal_manager.close(terminal_id);
1868 tracing::info!("Plugin closed terminal {:?} (no buffer found)", terminal_id);
1869 }
1870 }
1871
1872 PluginCommand::GrepProject {
1873 pattern,
1874 fixed_string,
1875 case_sensitive,
1876 max_results,
1877 whole_words,
1878 callback_id,
1879 } => {
1880 self.handle_grep_project(
1881 pattern,
1882 fixed_string,
1883 case_sensitive,
1884 max_results,
1885 whole_words,
1886 callback_id,
1887 );
1888 }
1889
1890 PluginCommand::GrepProjectStreaming {
1891 pattern,
1892 fixed_string,
1893 case_sensitive,
1894 max_results,
1895 whole_words,
1896 search_id,
1897 callback_id,
1898 } => {
1899 self.handle_grep_project_streaming(
1900 pattern,
1901 fixed_string,
1902 case_sensitive,
1903 max_results,
1904 whole_words,
1905 search_id,
1906 callback_id,
1907 );
1908 }
1909
1910 PluginCommand::ReplaceInBuffer {
1911 file_path,
1912 matches,
1913 replacement,
1914 callback_id,
1915 } => {
1916 self.handle_replace_in_buffer(file_path, matches, replacement, callback_id);
1917 }
1918 }
1919 Ok(())
1920 }
1921
1922 fn handle_save_buffer_to_path(&mut self, buffer_id: BufferId, path: std::path::PathBuf) {
1924 if let Some(state) = self.buffers.get_mut(&buffer_id) {
1925 match state.buffer.save_to_file(&path) {
1927 Ok(()) => {
1928 if let Err(e) = self.finalize_save(Some(path)) {
1931 tracing::warn!("Failed to finalize save: {}", e);
1932 }
1933 tracing::debug!("Saved buffer {:?} to path", buffer_id);
1934 }
1935 Err(e) => {
1936 self.handle_set_status(format!("Error saving: {}", e));
1937 tracing::error!("Failed to save buffer to path: {}", e);
1938 }
1939 }
1940 } else {
1941 self.handle_set_status(format!("Buffer {:?} not found", buffer_id));
1942 tracing::warn!("SaveBufferToPath: buffer {:?} not found", buffer_id);
1943 }
1944 }
1945
1946 #[cfg(feature = "plugins")]
1948 fn handle_load_plugin(&mut self, path: std::path::PathBuf, callback_id: JsCallbackId) {
1949 match self.plugin_manager.load_plugin(&path) {
1950 Ok(()) => {
1951 tracing::info!("Loaded plugin from {:?}", path);
1952 self.plugin_manager
1953 .resolve_callback(callback_id, "true".to_string());
1954 }
1955 Err(e) => {
1956 tracing::error!("Failed to load plugin from {:?}: {}", path, e);
1957 self.plugin_manager
1958 .reject_callback(callback_id, format!("{}", e));
1959 }
1960 }
1961 }
1962
1963 #[cfg(feature = "plugins")]
1965 fn handle_unload_plugin(&mut self, name: String, callback_id: JsCallbackId) {
1966 match self.plugin_manager.unload_plugin(&name) {
1967 Ok(()) => {
1968 tracing::info!("Unloaded plugin: {}", name);
1969 self.plugin_manager
1970 .resolve_callback(callback_id, "true".to_string());
1971 }
1972 Err(e) => {
1973 tracing::error!("Failed to unload plugin '{}': {}", name, e);
1974 self.plugin_manager
1975 .reject_callback(callback_id, format!("{}", e));
1976 }
1977 }
1978 }
1979
1980 #[cfg(feature = "plugins")]
1982 fn handle_reload_plugin(&mut self, name: String, callback_id: JsCallbackId) {
1983 match self.plugin_manager.reload_plugin(&name) {
1984 Ok(()) => {
1985 tracing::info!("Reloaded plugin: {}", name);
1986 self.plugin_manager
1987 .resolve_callback(callback_id, "true".to_string());
1988 }
1989 Err(e) => {
1990 tracing::error!("Failed to reload plugin '{}': {}", name, e);
1991 self.plugin_manager
1992 .reject_callback(callback_id, format!("{}", e));
1993 }
1994 }
1995 }
1996
1997 #[cfg(feature = "plugins")]
1999 fn handle_list_plugins(&mut self, callback_id: JsCallbackId) {
2000 let plugins = self.plugin_manager.list_plugins();
2001 let json_array: Vec<serde_json::Value> = plugins
2003 .iter()
2004 .map(|p| {
2005 serde_json::json!({
2006 "name": p.name,
2007 "path": p.path.to_string_lossy(),
2008 "enabled": p.enabled
2009 })
2010 })
2011 .collect();
2012 let json_str = serde_json::to_string(&json_array).unwrap_or_else(|_| "[]".to_string());
2013 self.plugin_manager.resolve_callback(callback_id, json_str);
2014 }
2015
2016 fn handle_execute_action(&mut self, action_name: String) {
2018 use crate::input::keybindings::Action;
2019 use std::collections::HashMap;
2020
2021 if let Some(action) = Action::from_str(&action_name, &HashMap::new()) {
2023 if let Err(e) = self.handle_action(action) {
2025 tracing::warn!("Failed to execute action '{}': {}", action_name, e);
2026 } else {
2027 tracing::debug!("Executed action: {}", action_name);
2028 }
2029 } else {
2030 tracing::warn!("Unknown action: {}", action_name);
2031 }
2032 }
2033
2034 fn handle_execute_actions(&mut self, actions: Vec<fresh_core::api::ActionSpec>) {
2037 use crate::input::keybindings::Action;
2038 use std::collections::HashMap;
2039
2040 for action_spec in actions {
2041 if let Some(action) = Action::from_str(&action_spec.action, &HashMap::new()) {
2042 for _ in 0..action_spec.count {
2044 if let Err(e) = self.handle_action(action.clone()) {
2045 tracing::warn!("Failed to execute action '{}': {}", action_spec.action, e);
2046 return; }
2048 }
2049 tracing::debug!(
2050 "Executed action '{}' {} time(s)",
2051 action_spec.action,
2052 action_spec.count
2053 );
2054 } else {
2055 tracing::warn!("Unknown action: {}", action_spec.action);
2056 return; }
2058 }
2059 }
2060
2061 fn handle_get_buffer_text(
2063 &mut self,
2064 buffer_id: BufferId,
2065 start: usize,
2066 end: usize,
2067 request_id: u64,
2068 ) {
2069 let result = if let Some(state) = self.buffers.get_mut(&buffer_id) {
2070 let len = state.buffer.len();
2072 if start <= end && end <= len {
2073 Ok(state.get_text_range(start, end))
2074 } else {
2075 Err(format!(
2076 "Invalid range {}..{} for buffer of length {}",
2077 start, end, len
2078 ))
2079 }
2080 } else {
2081 Err(format!("Buffer {:?} not found", buffer_id))
2082 };
2083
2084 let callback_id = fresh_core::api::JsCallbackId::from(request_id);
2086 match result {
2087 Ok(text) => {
2088 let json = serde_json::to_string(&text).unwrap_or_else(|_| "null".to_string());
2090 self.plugin_manager.resolve_callback(callback_id, json);
2091 }
2092 Err(error) => {
2093 self.plugin_manager.reject_callback(callback_id, error);
2094 }
2095 }
2096 }
2097
2098 fn handle_set_editor_mode(&mut self, mode: Option<String>) {
2100 self.editor_mode = mode.clone();
2101 tracing::debug!("Set editor mode: {:?}", mode);
2102 }
2103
2104 fn handle_get_line_start_position(&mut self, buffer_id: BufferId, line: u32, request_id: u64) {
2106 let actual_buffer_id = if buffer_id.0 == 0 {
2108 self.active_buffer_id()
2109 } else {
2110 buffer_id
2111 };
2112
2113 let result = if let Some(state) = self.buffers.get_mut(&actual_buffer_id) {
2114 let line_number = line as usize;
2116 let buffer_len = state.buffer.len();
2117
2118 if line_number == 0 {
2119 Some(0)
2121 } else {
2122 let mut current_line = 0;
2124 let mut line_start = None;
2125
2126 let content = state.get_text_range(0, buffer_len);
2128 for (byte_idx, c) in content.char_indices() {
2129 if c == '\n' {
2130 current_line += 1;
2131 if current_line == line_number {
2132 line_start = Some(byte_idx + 1);
2134 break;
2135 }
2136 }
2137 }
2138 line_start
2139 }
2140 } else {
2141 None
2142 };
2143
2144 let callback_id = fresh_core::api::JsCallbackId::from(request_id);
2146 let json = serde_json::to_string(&result).unwrap_or_else(|_| "null".to_string());
2148 self.plugin_manager.resolve_callback(callback_id, json);
2149 }
2150
2151 fn handle_get_line_end_position(&mut self, buffer_id: BufferId, line: u32, request_id: u64) {
2154 let actual_buffer_id = if buffer_id.0 == 0 {
2156 self.active_buffer_id()
2157 } else {
2158 buffer_id
2159 };
2160
2161 let result = if let Some(state) = self.buffers.get_mut(&actual_buffer_id) {
2162 let line_number = line as usize;
2163 let buffer_len = state.buffer.len();
2164
2165 let content = state.get_text_range(0, buffer_len);
2167 let mut current_line = 0;
2168 let mut line_end = None;
2169
2170 for (byte_idx, c) in content.char_indices() {
2171 if c == '\n' {
2172 if current_line == line_number {
2173 line_end = Some(byte_idx);
2175 break;
2176 }
2177 current_line += 1;
2178 }
2179 }
2180
2181 if line_end.is_none() && current_line == line_number {
2183 line_end = Some(buffer_len);
2184 }
2185
2186 line_end
2187 } else {
2188 None
2189 };
2190
2191 let callback_id = fresh_core::api::JsCallbackId::from(request_id);
2192 let json = serde_json::to_string(&result).unwrap_or_else(|_| "null".to_string());
2193 self.plugin_manager.resolve_callback(callback_id, json);
2194 }
2195
2196 fn handle_get_buffer_line_count(&mut self, buffer_id: BufferId, request_id: u64) {
2198 let actual_buffer_id = if buffer_id.0 == 0 {
2200 self.active_buffer_id()
2201 } else {
2202 buffer_id
2203 };
2204
2205 let result = if let Some(state) = self.buffers.get_mut(&actual_buffer_id) {
2206 let buffer_len = state.buffer.len();
2207 let content = state.get_text_range(0, buffer_len);
2208
2209 if content.is_empty() {
2211 Some(1) } else {
2213 let newline_count = content.chars().filter(|&c| c == '\n').count();
2214 let ends_with_newline = content.ends_with('\n');
2216 if ends_with_newline {
2217 Some(newline_count)
2218 } else {
2219 Some(newline_count + 1)
2220 }
2221 }
2222 } else {
2223 None
2224 };
2225
2226 let callback_id = fresh_core::api::JsCallbackId::from(request_id);
2227 let json = serde_json::to_string(&result).unwrap_or_else(|_| "null".to_string());
2228 self.plugin_manager.resolve_callback(callback_id, json);
2229 }
2230
2231 fn handle_scroll_to_line_center(
2233 &mut self,
2234 split_id: SplitId,
2235 buffer_id: BufferId,
2236 line: usize,
2237 ) {
2238 let actual_split_id = if split_id.0 == 0 {
2240 self.split_manager.active_split()
2241 } else {
2242 LeafId(split_id)
2243 };
2244
2245 let actual_buffer_id = if buffer_id.0 == 0 {
2247 self.active_buffer()
2248 } else {
2249 buffer_id
2250 };
2251
2252 let viewport_height = if let Some(view_state) = self.split_view_states.get(&actual_split_id)
2254 {
2255 view_state.viewport.height as usize
2256 } else {
2257 return;
2258 };
2259
2260 let lines_above = viewport_height / 2;
2262 let target_line = line.saturating_sub(lines_above);
2263
2264 if let Some(state) = self.buffers.get_mut(&actual_buffer_id) {
2266 let buffer = &mut state.buffer;
2267 if let Some(view_state) = self.split_view_states.get_mut(&actual_split_id) {
2268 view_state.viewport.scroll_to(buffer, target_line);
2269 view_state.viewport.set_skip_ensure_visible();
2271 }
2272 }
2273 }
2274
2275 fn handle_scroll_buffer_to_line(&mut self, buffer_id: BufferId, line: usize) {
2285 if !self.buffers.contains_key(&buffer_id) {
2286 return;
2287 }
2288
2289 let mut target_leaves: Vec<LeafId> = Vec::new();
2291
2292 for leaf_id in self.split_manager.root().leaf_split_ids() {
2294 if let Some(vs) = self.split_view_states.get(&leaf_id) {
2295 if vs.active_buffer == buffer_id {
2296 target_leaves.push(leaf_id);
2297 }
2298 }
2299 }
2300
2301 for (_group_leaf_id, node) in self.grouped_subtrees.iter() {
2303 if let crate::view::split::SplitNode::Grouped { layout, .. } = node {
2304 for inner_leaf in layout.leaf_split_ids() {
2305 if let Some(vs) = self.split_view_states.get(&inner_leaf) {
2306 if vs.active_buffer == buffer_id && !target_leaves.contains(&inner_leaf) {
2307 target_leaves.push(inner_leaf);
2308 }
2309 }
2310 }
2311 }
2312 }
2313
2314 if target_leaves.is_empty() {
2315 return;
2316 }
2317
2318 let state = match self.buffers.get_mut(&buffer_id) {
2319 Some(s) => s,
2320 None => return,
2321 };
2322
2323 for leaf_id in target_leaves {
2324 let Some(view_state) = self.split_view_states.get_mut(&leaf_id) else {
2325 continue;
2326 };
2327 let viewport_height = view_state.viewport.height as usize;
2328 let lines_above = viewport_height / 3;
2331 let target = line.saturating_sub(lines_above);
2332 view_state.viewport.scroll_to(&mut state.buffer, target);
2333 view_state.viewport.set_skip_ensure_visible();
2334 }
2335 }
2336}