use crate::model::event::{BufferId, CursorId, Event, SplitId};
use crate::services::plugins::api::{
LayoutHints, MenuPosition, PluginResponse, ViewTransformPayload,
};
use crate::view::overlay::{OverlayHandle, OverlayNamespace};
use crate::view::split::SplitViewState;
use std::io;
use super::Editor;
impl Editor {
fn find_menu_by_label_mut(&mut self, label: &str) -> Option<&mut crate::config::Menu> {
if let Some(menu) = self.config.menu.menus.iter_mut().find(|m| m.label == label) {
return Some(menu);
}
self.menu_state
.plugin_menus
.iter_mut()
.find(|m| m.label == label)
}
pub(super) fn handle_add_overlay(
&mut self,
buffer_id: BufferId,
namespace: Option<OverlayNamespace>,
range: std::ops::Range<usize>,
color: (u8, u8, u8),
underline: bool,
bold: bool,
italic: bool,
) {
if let Some(state) = self.buffers.get_mut(&buffer_id) {
let face = crate::model::event::OverlayFace::Style {
color,
bold,
italic,
underline,
};
let event = Event::AddOverlay {
namespace,
range,
face,
priority: 10,
message: None,
};
state.apply(&event);
}
}
pub(super) fn handle_remove_overlay(&mut self, buffer_id: BufferId, handle: OverlayHandle) {
if let Some(state) = self.buffers.get_mut(&buffer_id) {
let event = Event::RemoveOverlay { handle };
state.apply(&event);
}
}
pub(super) fn handle_clear_all_overlays(&mut self, buffer_id: BufferId) {
if let Some(state) = self.buffers.get_mut(&buffer_id) {
state.overlays.clear(&mut state.marker_list);
}
}
pub(super) fn handle_clear_namespace(
&mut self,
buffer_id: BufferId,
namespace: OverlayNamespace,
) {
if let Some(state) = self.buffers.get_mut(&buffer_id) {
state
.overlays
.clear_namespace(&namespace, &mut state.marker_list);
}
}
pub(super) fn handle_clear_overlays_in_range(
&mut self,
buffer_id: BufferId,
start: usize,
end: usize,
) {
if let Some(state) = self.buffers.get_mut(&buffer_id) {
state
.overlays
.remove_in_range(&(start..end), &mut state.marker_list);
}
}
pub(super) fn handle_add_virtual_text(
&mut self,
buffer_id: BufferId,
virtual_text_id: String,
position: usize,
text: String,
color: (u8, u8, u8),
use_bg: bool,
before: bool,
) {
if let Some(state) = self.buffers.get_mut(&buffer_id) {
use crate::view::virtual_text::VirtualTextPosition;
use ratatui::style::{Color, Style};
let vtext_position = if before {
VirtualTextPosition::BeforeChar
} else {
VirtualTextPosition::AfterChar
};
let style = if use_bg {
Style::default().bg(Color::Rgb(color.0, color.1, color.2))
} else {
Style::default().fg(Color::Rgb(color.0, color.1, color.2))
};
state
.virtual_texts
.remove_by_id(&mut state.marker_list, &virtual_text_id);
state.virtual_texts.add_with_id(
&mut state.marker_list,
position,
text,
style,
vtext_position,
0, virtual_text_id,
);
}
}
pub(super) fn handle_remove_virtual_text(
&mut self,
buffer_id: BufferId,
virtual_text_id: String,
) {
if let Some(state) = self.buffers.get_mut(&buffer_id) {
state
.virtual_texts
.remove_by_id(&mut state.marker_list, &virtual_text_id);
}
}
pub(super) fn handle_remove_virtual_texts_by_prefix(
&mut self,
buffer_id: BufferId,
prefix: String,
) {
if let Some(state) = self.buffers.get_mut(&buffer_id) {
state
.virtual_texts
.remove_by_prefix(&mut state.marker_list, &prefix);
}
}
pub(super) fn handle_clear_virtual_texts(&mut self, buffer_id: BufferId) {
if let Some(state) = self.buffers.get_mut(&buffer_id) {
state.virtual_texts.clear(&mut state.marker_list);
}
}
pub(super) fn handle_add_virtual_line(
&mut self,
buffer_id: BufferId,
position: usize,
text: String,
fg_color: (u8, u8, u8),
bg_color: Option<(u8, u8, u8)>,
above: bool,
namespace: String,
priority: i32,
) {
if let Some(state) = self.buffers.get_mut(&buffer_id) {
use crate::view::virtual_text::{VirtualTextNamespace, VirtualTextPosition};
use ratatui::style::{Color, Style};
let placement = if above {
VirtualTextPosition::LineAbove
} else {
VirtualTextPosition::LineBelow
};
let mut style = Style::default().fg(Color::Rgb(fg_color.0, fg_color.1, fg_color.2));
if let Some(bg) = bg_color {
style = style.bg(Color::Rgb(bg.0, bg.1, bg.2));
}
let ns = VirtualTextNamespace::from_string(namespace);
state.virtual_texts.add_line(
&mut state.marker_list,
position,
text,
style,
placement,
ns,
priority,
);
}
}
pub(super) fn handle_clear_virtual_text_namespace(
&mut self,
buffer_id: BufferId,
namespace: String,
) {
if let Some(state) = self.buffers.get_mut(&buffer_id) {
use crate::view::virtual_text::VirtualTextNamespace;
let ns = VirtualTextNamespace::from_string(namespace);
state
.virtual_texts
.clear_namespace(&mut state.marker_list, &ns);
}
}
pub(super) fn handle_add_menu_item(
&mut self,
menu_label: String,
item: crate::config::MenuItem,
position: MenuPosition,
) {
if let Some(menu) = self.find_menu_by_label_mut(&menu_label) {
let insert_idx = match position {
MenuPosition::Top => 0,
MenuPosition::Bottom => menu.items.len(),
MenuPosition::Before(label) => menu
.items
.iter()
.position(|i| match i {
crate::config::MenuItem::Action { label: l, .. }
| crate::config::MenuItem::Submenu { label: l, .. } => l == &label,
_ => false,
})
.unwrap_or(menu.items.len()),
MenuPosition::After(label) => menu
.items
.iter()
.position(|i| match i {
crate::config::MenuItem::Action { label: l, .. }
| crate::config::MenuItem::Submenu { label: l, .. } => l == &label,
_ => false,
})
.map(|i| i + 1)
.unwrap_or(menu.items.len()),
};
menu.items.insert(insert_idx, item);
tracing::info!(
"Added menu item to '{}' at position {}",
menu_label,
insert_idx
);
} else {
tracing::warn!("Menu '{}' not found for adding item", menu_label);
}
}
pub(super) fn handle_add_menu(&mut self, menu: crate::config::Menu, position: MenuPosition) {
let total_menus = self.config.menu.menus.len() + self.menu_state.plugin_menus.len();
let insert_idx = match position {
MenuPosition::Top => 0,
MenuPosition::Bottom => total_menus,
MenuPosition::Before(label) => {
self.config
.menu
.menus
.iter()
.position(|m| m.label == label)
.or_else(|| {
self.menu_state
.plugin_menus
.iter()
.position(|m| m.label == label)
.map(|i| self.config.menu.menus.len() + i)
})
.unwrap_or(total_menus)
}
MenuPosition::After(label) => {
self.config
.menu
.menus
.iter()
.position(|m| m.label == label)
.map(|i| i + 1)
.or_else(|| {
self.menu_state
.plugin_menus
.iter()
.position(|m| m.label == label)
.map(|i| self.config.menu.menus.len() + i + 1)
})
.unwrap_or(total_menus)
}
};
let plugin_idx = if insert_idx >= self.config.menu.menus.len() {
insert_idx - self.config.menu.menus.len()
} else {
0
};
self.menu_state
.plugin_menus
.insert(plugin_idx.min(self.menu_state.plugin_menus.len()), menu);
tracing::info!(
"Added plugin menu at index {} (total menus: {})",
plugin_idx,
self.config.menu.menus.len() + self.menu_state.plugin_menus.len()
);
}
pub(super) fn handle_remove_menu_item(&mut self, menu_label: String, item_label: String) {
if let Some(menu) = self.find_menu_by_label_mut(&menu_label) {
let original_len = menu.items.len();
menu.items.retain(|item| match item {
crate::config::MenuItem::Action { label, .. }
| crate::config::MenuItem::Submenu { label, .. } => label != &item_label,
_ => true, });
if menu.items.len() < original_len {
tracing::info!("Removed menu item '{}' from '{}'", item_label, menu_label);
} else {
tracing::warn!("Menu item '{}' not found in '{}'", item_label, menu_label);
}
} else {
tracing::warn!("Menu '{}' not found for removing item", menu_label);
}
}
pub(super) fn handle_remove_menu(&mut self, menu_label: String) {
let original_len = self.menu_state.plugin_menus.len();
self.menu_state
.plugin_menus
.retain(|m| m.label != menu_label);
if self.menu_state.plugin_menus.len() < original_len {
tracing::info!("Removed plugin menu '{}'", menu_label);
} else {
tracing::warn!(
"Plugin menu '{}' not found (note: cannot remove config menus)",
menu_label
);
}
}
pub(super) fn handle_focus_split(&mut self, split_id: SplitId) {
if let Some(buffer_id) = self.split_manager.buffer_for_split(split_id) {
self.focus_split(split_id, buffer_id);
tracing::info!("Focused split {:?}", split_id);
} else {
tracing::warn!("Split {:?} not found", split_id);
}
}
pub(super) fn handle_set_split_buffer(&mut self, split_id: SplitId, buffer_id: BufferId) {
if !self.buffers.contains_key(&buffer_id) {
tracing::error!("Buffer {:?} not found for SetSplitBuffer", buffer_id);
return;
}
match self.split_manager.set_split_buffer(split_id, buffer_id) {
Ok(()) => {
tracing::info!("Set split {:?} to buffer {:?}", split_id, buffer_id);
if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
view_state.view_transform = None;
view_state.compose_width = None;
}
if self.split_manager.active_split() == split_id {
self.set_active_buffer(buffer_id);
}
}
Err(e) => {
tracing::error!("Failed to set split buffer: {}", e);
}
}
}
pub(super) fn handle_close_split(&mut self, split_id: SplitId) {
match self.split_manager.close_split(split_id) {
Ok(()) => {
self.split_view_states.remove(&split_id);
self.restore_current_split_view_state();
tracing::info!("Closed split {:?}", split_id);
}
Err(e) => {
tracing::warn!("Failed to close split {:?}: {}", split_id, e);
}
}
}
pub(super) fn handle_set_split_ratio(&mut self, split_id: SplitId, ratio: f32) {
match self.split_manager.set_ratio(split_id, ratio) {
Ok(()) => {
tracing::debug!("Set split {:?} ratio to {}", split_id, ratio);
}
Err(e) => {
tracing::warn!("Failed to set split ratio {:?}: {}", split_id, e);
}
}
}
pub(super) fn handle_distribute_splits_evenly(&mut self) {
self.split_manager.distribute_splits_evenly();
tracing::debug!("Distributed splits evenly");
}
pub(super) fn handle_set_buffer_cursor(&mut self, buffer_id: BufferId, position: usize) {
let splits = self.split_manager.splits_for_buffer(buffer_id);
let active_split = self.split_manager.active_split();
tracing::debug!(
"SetBufferCursor: buffer_id={:?}, position={}, found {} splits: {:?}, active={:?}",
buffer_id,
position,
splits.len(),
splits,
active_split
);
if splits.is_empty() {
tracing::warn!("No splits found for buffer {:?}", buffer_id);
}
if let Some(state) = self.buffers.get_mut(&buffer_id) {
for split_id in &splits {
let is_active = *split_id == active_split;
if let Some(view_state) = self.split_view_states.get_mut(split_id) {
view_state.cursors.primary_mut().move_to(position, false);
let cursor = view_state.cursors.primary().clone();
view_state
.viewport
.ensure_visible(&mut state.buffer, &cursor);
tracing::debug!(
"SetBufferCursor: updated split {:?} (active={}) viewport top_byte={}",
split_id,
is_active,
view_state.viewport.top_byte
);
if is_active {
state.cursors.primary_mut().move_to(position, false);
}
} else {
tracing::warn!(
"SetBufferCursor: split {:?} not found in split_view_states",
split_id
);
}
}
} else {
tracing::warn!("Buffer {:?} not found for SetBufferCursor", buffer_id);
}
}
pub(super) fn handle_insert_text(
&mut self,
buffer_id: BufferId,
position: usize,
text: String,
) {
if let Some(state) = self.buffers.get_mut(&buffer_id) {
let event = Event::Insert {
position,
text,
cursor_id: CursorId(0),
};
state.apply(&event);
if let Some(log) = self.event_logs.get_mut(&buffer_id) {
log.append(event);
}
}
}
pub(super) fn handle_delete_range(
&mut self,
buffer_id: BufferId,
range: std::ops::Range<usize>,
) {
if let Some(state) = self.buffers.get_mut(&buffer_id) {
let deleted_text = state.get_text_range(range.start, range.end);
let event = Event::Delete {
range,
deleted_text,
cursor_id: CursorId(0),
};
state.apply(&event);
if let Some(log) = self.event_logs.get_mut(&buffer_id) {
log.append(event);
}
}
}
pub(super) fn handle_insert_at_cursor(&mut self, text: String) {
let state = self.active_state_mut();
let cursor_pos = state.cursors.primary().position;
let event = Event::Insert {
position: cursor_pos,
text,
cursor_id: CursorId(0),
};
state.apply(&event);
self.active_event_log_mut().append(event);
}
pub(super) fn handle_delete_selection(&mut self) {
let deletions: Vec<_> = {
let state = self.active_state();
state
.cursors
.iter()
.filter_map(|(_, c)| c.selection_range())
.collect()
};
if !deletions.is_empty() {
let state = self.active_state_mut();
let primary_id = state.cursors.primary_id();
let events: Vec<_> = deletions
.iter()
.rev()
.map(|range| {
let deleted_text = state.get_text_range(range.start, range.end);
Event::Delete {
range: range.clone(),
deleted_text,
cursor_id: primary_id,
}
})
.collect();
for event in events {
self.active_event_log_mut().append(event.clone());
self.apply_event_to_active_buffer(&event);
}
}
}
pub(super) fn jump_to_line_column(&mut self, line: Option<usize>, column: Option<usize>) {
let target_line = line.unwrap_or(1).saturating_sub(1); let column_offset = column.unwrap_or(1).saturating_sub(1);
let state = self.active_state_mut();
let mut iter = state.buffer.line_iterator(0, 80);
let mut target_byte = 0;
for current_line in 0..=target_line {
if let Some((line_start, _)) = iter.next() {
if current_line == target_line {
target_byte = line_start;
break;
}
} else {
break;
}
}
let final_position = target_byte + column_offset;
let buffer_len = state.buffer.len();
state.cursors.primary_mut().position = final_position.min(buffer_len);
state.cursors.primary_mut().anchor = None;
let active_split = self.split_manager.active_split();
let active_buffer = self.active_buffer();
if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
let state = self.buffers.get_mut(&active_buffer).unwrap();
view_state
.viewport
.ensure_visible(&mut state.buffer, state.cursors.primary());
}
}
pub(super) fn handle_open_file_at_location(
&mut self,
path: std::path::PathBuf,
line: Option<usize>,
column: Option<usize>,
) -> io::Result<()> {
if let Err(e) = self.open_file(&path) {
tracing::error!("Failed to open file from plugin: {}", e);
return Ok(());
}
if line.is_some() || column.is_some() {
self.jump_to_line_column(line, column);
}
Ok(())
}
pub(super) fn handle_open_file_in_split(
&mut self,
split_id: usize,
path: std::path::PathBuf,
line: Option<usize>,
column: Option<usize>,
) -> io::Result<()> {
self.save_current_split_view_state();
let target_split_id = SplitId(split_id);
if !self.split_manager.set_active_split(target_split_id) {
tracing::error!("Failed to switch to split {}", split_id);
return Ok(());
}
self.restore_current_split_view_state();
if let Err(e) = self.open_file(&path) {
tracing::error!("Failed to open file from plugin: {}", e);
return Ok(());
}
self.jump_to_line_column(line, column);
Ok(())
}
pub(super) fn handle_open_file_in_background(&mut self, path: std::path::PathBuf) {
if let Err(e) = self.open_file_no_focus(&path) {
tracing::error!("Failed to open file in background: {}", e);
} else {
tracing::info!("Opened file in background: {:?}", path);
}
}
pub(super) fn handle_show_buffer(&mut self, buffer_id: BufferId) {
if self.buffers.contains_key(&buffer_id) {
self.set_active_buffer(buffer_id);
tracing::info!("Switched to buffer {:?}", buffer_id);
} else {
tracing::warn!("Buffer {:?} not found", buffer_id);
}
}
pub(super) fn handle_close_buffer(&mut self, buffer_id: BufferId) {
match self.close_buffer(buffer_id) {
Ok(()) => {
tracing::info!("Closed buffer {:?}", buffer_id);
}
Err(e) => {
tracing::error!("Failed to close buffer {:?}: {}", buffer_id, e);
}
}
}
pub(super) fn handle_set_layout_hints(
&mut self,
buffer_id: BufferId,
split_id: Option<SplitId>,
hints: LayoutHints,
) {
let target_split = split_id.unwrap_or(self.split_manager.active_split());
let view_state = self
.split_view_states
.entry(target_split)
.or_insert_with(|| {
SplitViewState::with_buffer(self.terminal_width, self.terminal_height, buffer_id)
});
view_state.compose_width = hints.compose_width;
view_state.compose_column_guides = hints.column_guides;
}
pub(super) fn handle_set_line_numbers(&mut self, buffer_id: BufferId, enabled: bool) {
if let Some(state) = self.buffers.get_mut(&buffer_id) {
state.margins.set_line_numbers(enabled);
}
}
pub(super) fn handle_submit_view_transform(
&mut self,
buffer_id: BufferId,
split_id: Option<SplitId>,
payload: ViewTransformPayload,
) {
let target_split = split_id.unwrap_or(self.split_manager.active_split());
let view_state = self
.split_view_states
.entry(target_split)
.or_insert_with(|| {
SplitViewState::with_buffer(self.terminal_width, self.terminal_height, buffer_id)
});
view_state.view_transform = Some(payload);
}
pub(super) fn handle_clear_view_transform(&mut self, split_id: Option<SplitId>) {
let target_split = split_id.unwrap_or(self.split_manager.active_split());
if let Some(view_state) = self.split_view_states.get_mut(&target_split) {
view_state.view_transform = None;
view_state.compose_width = None;
}
}
pub(super) fn handle_refresh_lines(&mut self, buffer_id: BufferId) {
self.seen_byte_ranges.remove(&buffer_id);
#[cfg(feature = "plugins")]
{
self.plugin_render_requested = true;
}
}
pub(super) fn handle_set_line_indicator(
&mut self,
buffer_id: BufferId,
line: usize,
namespace: String,
symbol: String,
color: (u8, u8, u8),
priority: i32,
) {
if let Some(state) = self.buffers.get_mut(&buffer_id) {
let byte_offset = state.buffer.line_start_offset(line).unwrap_or(0);
let indicator = crate::view::margin::LineIndicator::new(
symbol,
ratatui::style::Color::Rgb(color.0, color.1, color.2),
priority,
);
state
.margins
.set_line_indicator(byte_offset, namespace, indicator);
}
}
pub(super) fn handle_clear_line_indicators(&mut self, buffer_id: BufferId, namespace: String) {
if let Some(state) = self.buffers.get_mut(&buffer_id) {
state
.margins
.clear_line_indicators_for_namespace(&namespace);
}
}
pub(super) fn handle_set_status(&mut self, message: String) {
if message.trim().is_empty() {
self.plugin_status_message = None;
} else {
self.plugin_status_message = Some(message);
}
}
pub(super) fn handle_start_prompt(&mut self, label: String, prompt_type: String) {
use crate::view::prompt::{Prompt, PromptType};
self.prompt = Some(Prompt::new(
label,
PromptType::Plugin {
custom_type: prompt_type.clone(),
},
));
use crate::services::plugins::hooks::HookArgs;
self.plugin_manager.run_hook(
"prompt_changed",
HookArgs::PromptChanged {
prompt_type: prompt_type.clone(),
input: String::new(),
},
);
}
pub(super) fn handle_start_prompt_with_initial(
&mut self,
label: String,
prompt_type: String,
initial_value: String,
) {
use crate::view::prompt::{Prompt, PromptType};
self.prompt = Some(Prompt::with_initial_text(
label,
PromptType::Plugin {
custom_type: prompt_type.clone(),
},
initial_value.clone(),
));
use crate::services::plugins::hooks::HookArgs;
self.plugin_manager.run_hook(
"prompt_changed",
HookArgs::PromptChanged {
prompt_type: prompt_type.clone(),
input: initial_value,
},
);
}
pub(super) fn handle_set_prompt_suggestions(
&mut self,
suggestions: Vec<crate::input::commands::Suggestion>,
) {
if let Some(prompt) = &mut self.prompt {
prompt.suggestions = suggestions;
prompt.selected_suggestion = if prompt.suggestions.is_empty() {
None
} else {
Some(0) };
}
}
pub(super) fn handle_register_command(&self, command: crate::input::commands::Command) {
self.command_registry.read().unwrap().register(command);
}
pub(super) fn handle_unregister_command(&self, name: String) {
self.command_registry.read().unwrap().unregister(&name);
}
pub(super) fn handle_define_mode(
&mut self,
name: String,
parent: Option<String>,
bindings: Vec<(String, String)>,
read_only: bool,
) {
use super::parse_key_string;
use crate::input::buffer_mode::BufferMode;
let mut mode = BufferMode::new(name.clone()).with_read_only(read_only);
if let Some(parent_name) = parent {
mode = mode.with_parent(parent_name);
}
for (key_str, command) in bindings {
if let Some((code, modifiers)) = parse_key_string(&key_str) {
mode = mode.with_binding(code, modifiers, command);
} else {
tracing::warn!("Failed to parse key binding: {}", key_str);
}
}
self.mode_registry.register(mode);
tracing::info!("Registered buffer mode '{}'", name);
}
pub(super) fn handle_send_lsp_request(
&mut self,
language: String,
method: String,
params: Option<serde_json::Value>,
request_id: u64,
) {
tracing::debug!(
"Plugin LSP request {} for language '{}': method={}",
request_id,
language,
method
);
let error = if let Some(lsp) = self.lsp.as_mut() {
if let Some(handle) = lsp.get_or_spawn(&language) {
if let Err(e) = handle.send_plugin_request(request_id, method, params) {
Some(e)
} else {
None
}
} else {
Some(format!("LSP server for '{}' is unavailable", language))
}
} else {
Some("LSP manager not initialized".to_string())
};
if let Some(err_msg) = error {
self.send_plugin_response(PluginResponse::LspRequest {
request_id,
result: Err(err_msg),
});
}
}
pub(super) fn handle_set_clipboard(&mut self, text: String) {
self.clipboard.copy(text);
}
}