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