use crate::input::command_registry::CommandRegistry;
use crate::input::commands::Command;
use crate::model::event::{BufferId, SplitId};
use crate::services::plugins::hooks::{HookCallback, HookRegistry};
use crate::view::overlay::{OverlayHandle, OverlayNamespace};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::ops::Range;
use std::path::PathBuf;
use std::sync::{Arc, RwLock};
#[derive(Debug, Clone)]
pub enum PluginResponse {
VirtualBufferCreated {
request_id: u64,
buffer_id: BufferId,
split_id: Option<SplitId>,
},
LspRequest {
request_id: u64,
result: Result<Value, String>,
},
HighlightsComputed {
request_id: u64,
spans: Vec<TsHighlightSpan>,
},
BufferText {
request_id: u64,
text: Result<String, String>,
},
CompositeBufferCreated {
request_id: u64,
buffer_id: BufferId,
},
}
#[derive(Debug, Clone)]
pub struct CursorInfo {
pub position: usize,
pub selection: Option<Range<usize>>,
}
#[derive(Debug, Clone)]
pub struct ActionSpec {
pub action: String,
pub count: u32,
}
#[derive(Debug, Clone)]
pub struct BufferInfo {
pub id: BufferId,
pub path: Option<PathBuf>,
pub modified: bool,
pub length: usize,
}
#[derive(Debug, Clone)]
pub struct BufferSavedDiff {
pub equal: bool,
pub byte_ranges: Vec<Range<usize>>,
pub line_ranges: Option<Vec<Range<usize>>>,
}
#[derive(Debug, Clone)]
pub struct ViewportInfo {
pub top_byte: usize,
pub left_column: usize,
pub width: u16,
pub height: u16,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LayoutHints {
pub compose_width: Option<u16>,
pub column_guides: Option<Vec<u16>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompositeLayoutConfig {
#[serde(rename = "type")]
pub layout_type: String,
#[serde(default)]
pub ratios: Option<Vec<f32>>,
#[serde(default = "default_true")]
pub show_separator: bool,
#[serde(default)]
pub spacing: Option<u16>,
}
fn default_true() -> bool {
true
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompositeSourceConfig {
pub buffer_id: usize,
pub label: String,
#[serde(default)]
pub editable: bool,
#[serde(default)]
pub style: Option<CompositePaneStyle>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct CompositePaneStyle {
#[serde(default)]
pub add_bg: Option<(u8, u8, u8)>,
#[serde(default)]
pub remove_bg: Option<(u8, u8, u8)>,
#[serde(default)]
pub modify_bg: Option<(u8, u8, u8)>,
#[serde(default)]
pub gutter_style: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompositeHunk {
pub old_start: usize,
pub old_count: usize,
pub new_start: usize,
pub new_count: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ViewTokenWireKind {
Text(String),
Newline,
Space,
Break,
BinaryByte(u8),
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ViewTokenStyle {
#[serde(default)]
pub fg: Option<(u8, u8, u8)>,
#[serde(default)]
pub bg: Option<(u8, u8, u8)>,
#[serde(default)]
pub bold: bool,
#[serde(default)]
pub italic: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ViewTokenWire {
pub source_offset: Option<usize>,
pub kind: ViewTokenWireKind,
#[serde(default)]
pub style: Option<ViewTokenStyle>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ViewTransformPayload {
pub range: Range<usize>,
pub tokens: Vec<ViewTokenWire>,
pub layout_hints: Option<LayoutHints>,
}
#[derive(Debug, Clone)]
pub struct EditorStateSnapshot {
pub active_buffer_id: BufferId,
pub active_split_id: usize,
pub buffers: HashMap<BufferId, BufferInfo>,
pub buffer_saved_diffs: HashMap<BufferId, BufferSavedDiff>,
pub primary_cursor: Option<CursorInfo>,
pub all_cursors: Vec<CursorInfo>,
pub viewport: Option<ViewportInfo>,
pub buffer_cursor_positions: HashMap<BufferId, usize>,
pub buffer_text_properties:
HashMap<BufferId, Vec<crate::primitives::text_property::TextProperty>>,
pub selected_text: Option<String>,
pub clipboard: String,
pub working_dir: PathBuf,
pub diagnostics: HashMap<String, Vec<lsp_types::Diagnostic>>,
pub config: serde_json::Value,
pub user_config: serde_json::Value,
pub editor_mode: Option<String>,
}
impl EditorStateSnapshot {
pub fn new() -> Self {
Self {
active_buffer_id: BufferId(0),
active_split_id: 0,
buffers: HashMap::new(),
buffer_saved_diffs: HashMap::new(),
primary_cursor: None,
all_cursors: Vec::new(),
viewport: None,
buffer_cursor_positions: HashMap::new(),
buffer_text_properties: HashMap::new(),
selected_text: None,
clipboard: String::new(),
working_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
diagnostics: HashMap::new(),
config: serde_json::Value::Null,
user_config: serde_json::Value::Null,
editor_mode: None,
}
}
}
#[derive(Debug, Clone)]
pub enum MenuPosition {
Top,
Bottom,
Before(String),
After(String),
}
#[derive(Debug, Clone)]
pub enum PluginCommand {
InsertText {
buffer_id: BufferId,
position: usize,
text: String,
},
DeleteRange {
buffer_id: BufferId,
range: Range<usize>,
},
AddOverlay {
buffer_id: BufferId,
namespace: Option<OverlayNamespace>,
range: Range<usize>,
color: (u8, u8, u8),
bg_color: Option<(u8, u8, u8)>,
underline: bool,
bold: bool,
italic: bool,
extend_to_line_end: bool,
},
RemoveOverlay {
buffer_id: BufferId,
handle: OverlayHandle,
},
SetStatus { message: String },
ApplyTheme { theme_name: String },
ReloadConfig,
RegisterCommand { command: Command },
UnregisterCommand { name: String },
OpenFileInBackground { path: PathBuf },
InsertAtCursor { text: String },
SpawnProcess {
command: String,
args: Vec<String>,
cwd: Option<String>,
callback_id: u64, },
SetLayoutHints {
buffer_id: BufferId,
split_id: Option<SplitId>,
range: Range<usize>,
hints: LayoutHints,
},
SetLineNumbers { buffer_id: BufferId, enabled: bool },
SubmitViewTransform {
buffer_id: BufferId,
split_id: Option<SplitId>,
payload: ViewTransformPayload,
},
ClearViewTransform {
buffer_id: BufferId,
split_id: Option<SplitId>,
},
ClearAllOverlays { buffer_id: BufferId },
ClearNamespace {
buffer_id: BufferId,
namespace: OverlayNamespace,
},
ClearOverlaysInRange {
buffer_id: BufferId,
start: usize,
end: usize,
},
AddVirtualText {
buffer_id: BufferId,
virtual_text_id: String,
position: usize,
text: String,
color: (u8, u8, u8),
use_bg: bool, before: bool, },
RemoveVirtualText {
buffer_id: BufferId,
virtual_text_id: String,
},
RemoveVirtualTextsByPrefix { buffer_id: BufferId, prefix: String },
ClearVirtualTexts { buffer_id: BufferId },
AddVirtualLine {
buffer_id: BufferId,
position: usize,
text: String,
fg_color: (u8, u8, u8),
bg_color: Option<(u8, u8, u8)>,
above: bool,
namespace: String,
priority: i32,
},
ClearVirtualTextNamespace {
buffer_id: BufferId,
namespace: String,
},
RefreshLines { buffer_id: BufferId },
SetLineIndicator {
buffer_id: BufferId,
line: usize,
namespace: String,
symbol: String,
color: (u8, u8, u8),
priority: i32,
},
ClearLineIndicators {
buffer_id: BufferId,
namespace: String,
},
OpenFileAtLocation {
path: PathBuf,
line: Option<usize>, column: Option<usize>, },
OpenFileInSplit {
split_id: usize,
path: PathBuf,
line: Option<usize>, column: Option<usize>, },
StartPrompt {
label: String,
prompt_type: String, },
StartPromptWithInitial {
label: String,
prompt_type: String,
initial_value: String,
},
SetPromptSuggestions {
suggestions: Vec<crate::input::commands::Suggestion>,
},
AddMenuItem {
menu_label: String,
item: crate::config::MenuItem,
position: MenuPosition,
},
AddMenu {
menu: crate::config::Menu,
position: MenuPosition,
},
RemoveMenuItem {
menu_label: String,
item_label: String,
},
RemoveMenu { menu_label: String },
CreateVirtualBuffer {
name: String,
mode: String,
read_only: bool,
},
CreateVirtualBufferWithContent {
name: String,
mode: String,
read_only: bool,
entries: Vec<crate::primitives::text_property::TextPropertyEntry>,
show_line_numbers: bool,
show_cursors: bool,
editing_disabled: bool,
hidden_from_tabs: bool,
request_id: Option<u64>,
},
CreateVirtualBufferInSplit {
name: String,
mode: String,
read_only: bool,
entries: Vec<crate::primitives::text_property::TextPropertyEntry>,
ratio: f32,
direction: Option<String>,
panel_id: Option<String>,
show_line_numbers: bool,
show_cursors: bool,
editing_disabled: bool,
line_wrap: Option<bool>,
request_id: Option<u64>,
},
SetVirtualBufferContent {
buffer_id: BufferId,
entries: Vec<crate::primitives::text_property::TextPropertyEntry>,
},
GetTextPropertiesAtCursor { buffer_id: BufferId },
DefineMode {
name: String,
parent: Option<String>,
bindings: Vec<(String, String)>, read_only: bool,
},
ShowBuffer { buffer_id: BufferId },
CreateVirtualBufferInExistingSplit {
name: String,
mode: String,
read_only: bool,
entries: Vec<crate::primitives::text_property::TextPropertyEntry>,
split_id: SplitId,
show_line_numbers: bool,
show_cursors: bool,
editing_disabled: bool,
line_wrap: Option<bool>,
request_id: Option<u64>,
},
CloseBuffer { buffer_id: BufferId },
CreateCompositeBuffer {
name: String,
mode: String,
layout: CompositeLayoutConfig,
sources: Vec<CompositeSourceConfig>,
hunks: Option<Vec<CompositeHunk>>,
request_id: Option<u64>,
},
UpdateCompositeAlignment {
buffer_id: BufferId,
hunks: Vec<CompositeHunk>,
},
CloseCompositeBuffer { buffer_id: BufferId },
FocusSplit { split_id: SplitId },
SetSplitBuffer {
split_id: SplitId,
buffer_id: BufferId,
},
SetSplitScroll { split_id: SplitId, top_byte: usize },
RequestHighlights {
buffer_id: BufferId,
range: Range<usize>,
request_id: u64,
},
CloseSplit { split_id: SplitId },
SetSplitRatio {
split_id: SplitId,
ratio: f32,
},
DistributeSplitsEvenly {
split_ids: Vec<SplitId>,
},
SetBufferCursor {
buffer_id: BufferId,
position: usize,
},
SendLspRequest {
language: String,
method: String,
params: Option<Value>,
request_id: u64,
},
SetClipboard { text: String },
DeleteSelection,
SetContext {
name: String,
active: bool,
},
SetReviewDiffHunks { hunks: Vec<ReviewHunk> },
ExecuteAction {
action_name: String,
},
ExecuteActions {
actions: Vec<ActionSpec>,
},
GetBufferText {
buffer_id: BufferId,
start: usize,
end: usize,
request_id: u64,
},
SetEditorMode {
mode: Option<String>,
},
ShowActionPopup {
popup_id: String,
title: String,
message: String,
actions: Vec<ActionPopupAction>,
},
DisableLspForLanguage {
language: String,
},
CreateScrollSyncGroup {
group_id: u32,
left_split: SplitId,
right_split: SplitId,
},
SetScrollSyncAnchors {
group_id: u32,
anchors: Vec<(usize, usize)>,
},
RemoveScrollSyncGroup {
group_id: u32,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum HunkStatus {
Pending,
Staged,
Discarded,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReviewHunk {
pub id: String,
pub file: String,
pub context_header: String,
pub status: HunkStatus,
pub base_range: Option<(usize, usize)>,
pub modified_range: Option<(usize, usize)>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ActionPopupAction {
pub id: String,
pub label: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TsHighlightSpan {
pub start: u32,
pub end: u32,
pub color: (u8, u8, u8),
pub bold: bool,
pub italic: bool,
}
pub struct PluginApi {
hooks: Arc<RwLock<HookRegistry>>,
commands: Arc<RwLock<CommandRegistry>>,
command_sender: std::sync::mpsc::Sender<PluginCommand>,
state_snapshot: Arc<RwLock<EditorStateSnapshot>>,
}
impl PluginApi {
pub fn new(
hooks: Arc<RwLock<HookRegistry>>,
commands: Arc<RwLock<CommandRegistry>>,
command_sender: std::sync::mpsc::Sender<PluginCommand>,
state_snapshot: Arc<RwLock<EditorStateSnapshot>>,
) -> Self {
Self {
hooks,
commands,
command_sender,
state_snapshot,
}
}
pub fn register_hook(&self, hook_name: &str, callback: HookCallback) {
let mut hooks = self.hooks.write().unwrap();
hooks.add_hook(hook_name, callback);
}
pub fn unregister_hooks(&self, hook_name: &str) {
let mut hooks = self.hooks.write().unwrap();
hooks.remove_hooks(hook_name);
}
pub fn register_command(&self, command: Command) {
let commands = self.commands.read().unwrap();
commands.register(command);
}
pub fn unregister_command(&self, name: &str) {
let commands = self.commands.read().unwrap();
commands.unregister(name);
}
pub fn send_command(&self, command: PluginCommand) -> Result<(), String> {
self.command_sender
.send(command)
.map_err(|e| format!("Failed to send command: {}", e))
}
pub fn insert_text(
&self,
buffer_id: BufferId,
position: usize,
text: String,
) -> Result<(), String> {
self.send_command(PluginCommand::InsertText {
buffer_id,
position,
text,
})
}
pub fn delete_range(&self, buffer_id: BufferId, range: Range<usize>) -> Result<(), String> {
self.send_command(PluginCommand::DeleteRange { buffer_id, range })
}
pub fn add_overlay(
&self,
buffer_id: BufferId,
namespace: Option<String>,
range: Range<usize>,
color: (u8, u8, u8),
bg_color: Option<(u8, u8, u8)>,
underline: bool,
bold: bool,
italic: bool,
extend_to_line_end: bool,
) -> Result<(), String> {
self.send_command(PluginCommand::AddOverlay {
buffer_id,
namespace: namespace.map(crate::view::overlay::OverlayNamespace::from_string),
range,
color,
bg_color,
underline,
bold,
italic,
extend_to_line_end,
})
}
pub fn remove_overlay(&self, buffer_id: BufferId, handle: String) -> Result<(), String> {
self.send_command(PluginCommand::RemoveOverlay {
buffer_id,
handle: crate::view::overlay::OverlayHandle::from_string(handle),
})
}
pub fn clear_namespace(&self, buffer_id: BufferId, namespace: String) -> Result<(), String> {
self.send_command(PluginCommand::ClearNamespace {
buffer_id,
namespace: crate::view::overlay::OverlayNamespace::from_string(namespace),
})
}
pub fn clear_overlays_in_range(
&self,
buffer_id: BufferId,
start: usize,
end: usize,
) -> Result<(), String> {
self.send_command(PluginCommand::ClearOverlaysInRange {
buffer_id,
start,
end,
})
}
pub fn set_status(&self, message: String) -> Result<(), String> {
self.send_command(PluginCommand::SetStatus { message })
}
pub fn open_file_at_location(
&self,
path: PathBuf,
line: Option<usize>,
column: Option<usize>,
) -> Result<(), String> {
self.send_command(PluginCommand::OpenFileAtLocation { path, line, column })
}
pub fn open_file_in_split(
&self,
split_id: usize,
path: PathBuf,
line: Option<usize>,
column: Option<usize>,
) -> Result<(), String> {
self.send_command(PluginCommand::OpenFileInSplit {
split_id,
path,
line,
column,
})
}
pub fn start_prompt(&self, label: String, prompt_type: String) -> Result<(), String> {
self.send_command(PluginCommand::StartPrompt { label, prompt_type })
}
pub fn set_prompt_suggestions(
&self,
suggestions: Vec<crate::input::commands::Suggestion>,
) -> Result<(), String> {
self.send_command(PluginCommand::SetPromptSuggestions { suggestions })
}
pub fn add_menu_item(
&self,
menu_label: String,
item: crate::config::MenuItem,
position: MenuPosition,
) -> Result<(), String> {
self.send_command(PluginCommand::AddMenuItem {
menu_label,
item,
position,
})
}
pub fn add_menu(
&self,
menu: crate::config::Menu,
position: MenuPosition,
) -> Result<(), String> {
self.send_command(PluginCommand::AddMenu { menu, position })
}
pub fn remove_menu_item(&self, menu_label: String, item_label: String) -> Result<(), String> {
self.send_command(PluginCommand::RemoveMenuItem {
menu_label,
item_label,
})
}
pub fn remove_menu(&self, menu_label: String) -> Result<(), String> {
self.send_command(PluginCommand::RemoveMenu { menu_label })
}
pub fn create_virtual_buffer(
&self,
name: String,
mode: String,
read_only: bool,
) -> Result<(), String> {
self.send_command(PluginCommand::CreateVirtualBuffer {
name,
mode,
read_only,
})
}
pub fn create_virtual_buffer_with_content(
&self,
name: String,
mode: String,
read_only: bool,
entries: Vec<crate::primitives::text_property::TextPropertyEntry>,
) -> Result<(), String> {
self.send_command(PluginCommand::CreateVirtualBufferWithContent {
name,
mode,
read_only,
entries,
show_line_numbers: true,
show_cursors: true,
editing_disabled: false,
hidden_from_tabs: false,
request_id: None,
})
}
pub fn set_virtual_buffer_content(
&self,
buffer_id: BufferId,
entries: Vec<crate::primitives::text_property::TextPropertyEntry>,
) -> Result<(), String> {
self.send_command(PluginCommand::SetVirtualBufferContent { buffer_id, entries })
}
pub fn get_text_properties_at_cursor(&self, buffer_id: BufferId) -> Result<(), String> {
self.send_command(PluginCommand::GetTextPropertiesAtCursor { buffer_id })
}
pub fn define_mode(
&self,
name: String,
parent: Option<String>,
bindings: Vec<(String, String)>,
read_only: bool,
) -> Result<(), String> {
self.send_command(PluginCommand::DefineMode {
name,
parent,
bindings,
read_only,
})
}
pub fn show_buffer(&self, buffer_id: BufferId) -> Result<(), String> {
self.send_command(PluginCommand::ShowBuffer { buffer_id })
}
pub fn set_split_scroll(&self, split_id: usize, top_byte: usize) -> Result<(), String> {
self.send_command(PluginCommand::SetSplitScroll {
split_id: SplitId(split_id),
top_byte,
})
}
pub fn get_highlights(
&self,
buffer_id: BufferId,
range: Range<usize>,
request_id: u64,
) -> Result<(), String> {
self.send_command(PluginCommand::RequestHighlights {
buffer_id,
range,
request_id,
})
}
pub fn get_active_buffer_id(&self) -> BufferId {
let snapshot = self.state_snapshot.read().unwrap();
snapshot.active_buffer_id
}
pub fn get_active_split_id(&self) -> usize {
let snapshot = self.state_snapshot.read().unwrap();
snapshot.active_split_id
}
pub fn get_buffer_info(&self, buffer_id: BufferId) -> Option<BufferInfo> {
let snapshot = self.state_snapshot.read().unwrap();
snapshot.buffers.get(&buffer_id).cloned()
}
pub fn list_buffers(&self) -> Vec<BufferInfo> {
let snapshot = self.state_snapshot.read().unwrap();
snapshot.buffers.values().cloned().collect()
}
pub fn get_primary_cursor(&self) -> Option<CursorInfo> {
let snapshot = self.state_snapshot.read().unwrap();
snapshot.primary_cursor.clone()
}
pub fn get_all_cursors(&self) -> Vec<CursorInfo> {
let snapshot = self.state_snapshot.read().unwrap();
snapshot.all_cursors.clone()
}
pub fn get_viewport(&self) -> Option<ViewportInfo> {
let snapshot = self.state_snapshot.read().unwrap();
snapshot.viewport.clone()
}
pub fn state_snapshot_handle(&self) -> Arc<RwLock<EditorStateSnapshot>> {
Arc::clone(&self.state_snapshot)
}
}
impl Clone for PluginApi {
fn clone(&self) -> Self {
Self {
hooks: Arc::clone(&self.hooks),
commands: Arc::clone(&self.commands),
command_sender: self.command_sender.clone(),
state_snapshot: Arc::clone(&self.state_snapshot),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_plugin_api_creation() {
let hooks = Arc::new(RwLock::new(HookRegistry::new()));
let commands = Arc::new(RwLock::new(CommandRegistry::new()));
let (tx, _rx) = std::sync::mpsc::channel();
let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
let api = PluginApi::new(hooks, commands, tx, state_snapshot);
let _clone = api.clone();
}
#[test]
fn test_register_hook() {
let hooks = Arc::new(RwLock::new(HookRegistry::new()));
let commands = Arc::new(RwLock::new(CommandRegistry::new()));
let (tx, _rx) = std::sync::mpsc::channel();
let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
let api = PluginApi::new(hooks.clone(), commands, tx, state_snapshot);
api.register_hook("test-hook", Box::new(|_| true));
let hook_registry = hooks.read().unwrap();
assert_eq!(hook_registry.hook_count("test-hook"), 1);
}
#[test]
fn test_send_command() {
let hooks = Arc::new(RwLock::new(HookRegistry::new()));
let commands = Arc::new(RwLock::new(CommandRegistry::new()));
let (tx, rx) = std::sync::mpsc::channel();
let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
let api = PluginApi::new(hooks, commands, tx, state_snapshot);
let result = api.insert_text(BufferId(1), 0, "test".to_string());
assert!(result.is_ok());
let received = rx.try_recv();
assert!(received.is_ok());
match received.unwrap() {
PluginCommand::InsertText {
buffer_id,
position,
text,
} => {
assert_eq!(buffer_id.0, 1);
assert_eq!(position, 0);
assert_eq!(text, "test");
}
_ => panic!("Wrong command type"),
}
}
#[test]
fn test_add_overlay_command() {
let hooks = Arc::new(RwLock::new(HookRegistry::new()));
let commands = Arc::new(RwLock::new(CommandRegistry::new()));
let (tx, rx) = std::sync::mpsc::channel();
let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
let api = PluginApi::new(hooks, commands, tx, state_snapshot);
let result = api.add_overlay(
BufferId(1),
Some("test-overlay".to_string()),
0..10,
(255, 0, 0),
None,
true,
false,
false,
false,
);
assert!(result.is_ok());
let received = rx.try_recv().unwrap();
match received {
PluginCommand::AddOverlay {
buffer_id,
namespace,
range,
color,
bg_color,
underline,
bold,
italic,
extend_to_line_end,
} => {
assert_eq!(buffer_id.0, 1);
assert_eq!(namespace.as_ref().map(|n| n.as_str()), Some("test-overlay"));
assert_eq!(range, 0..10);
assert_eq!(color, (255, 0, 0));
assert_eq!(bg_color, None);
assert!(underline);
assert!(!bold);
assert!(!italic);
assert!(!extend_to_line_end);
}
_ => panic!("Wrong command type"),
}
}
#[test]
fn test_set_status_command() {
let hooks = Arc::new(RwLock::new(HookRegistry::new()));
let commands = Arc::new(RwLock::new(CommandRegistry::new()));
let (tx, rx) = std::sync::mpsc::channel();
let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
let api = PluginApi::new(hooks, commands, tx, state_snapshot);
let result = api.set_status("Test status".to_string());
assert!(result.is_ok());
let received = rx.try_recv().unwrap();
match received {
PluginCommand::SetStatus { message } => {
assert_eq!(message, "Test status");
}
_ => panic!("Wrong command type"),
}
}
#[test]
fn test_get_active_buffer_id() {
let hooks = Arc::new(RwLock::new(HookRegistry::new()));
let commands = Arc::new(RwLock::new(CommandRegistry::new()));
let (tx, _rx) = std::sync::mpsc::channel();
let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
{
let mut snapshot = state_snapshot.write().unwrap();
snapshot.active_buffer_id = BufferId(5);
}
let api = PluginApi::new(hooks, commands, tx, state_snapshot);
let active_id = api.get_active_buffer_id();
assert_eq!(active_id.0, 5);
}
#[test]
fn test_get_buffer_info() {
let hooks = Arc::new(RwLock::new(HookRegistry::new()));
let commands = Arc::new(RwLock::new(CommandRegistry::new()));
let (tx, _rx) = std::sync::mpsc::channel();
let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
{
let mut snapshot = state_snapshot.write().unwrap();
let buffer_info = BufferInfo {
id: BufferId(1),
path: Some(std::path::PathBuf::from("/test/file.txt")),
modified: true,
length: 100,
};
snapshot.buffers.insert(BufferId(1), buffer_info);
}
let api = PluginApi::new(hooks, commands, tx, state_snapshot);
let info = api.get_buffer_info(BufferId(1));
assert!(info.is_some());
let info = info.unwrap();
assert_eq!(info.id.0, 1);
assert_eq!(
info.path.as_ref().unwrap().to_str().unwrap(),
"/test/file.txt"
);
assert!(info.modified);
assert_eq!(info.length, 100);
let no_info = api.get_buffer_info(BufferId(999));
assert!(no_info.is_none());
}
#[test]
fn test_list_buffers() {
let hooks = Arc::new(RwLock::new(HookRegistry::new()));
let commands = Arc::new(RwLock::new(CommandRegistry::new()));
let (tx, _rx) = std::sync::mpsc::channel();
let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
{
let mut snapshot = state_snapshot.write().unwrap();
snapshot.buffers.insert(
BufferId(1),
BufferInfo {
id: BufferId(1),
path: Some(std::path::PathBuf::from("/file1.txt")),
modified: false,
length: 50,
},
);
snapshot.buffers.insert(
BufferId(2),
BufferInfo {
id: BufferId(2),
path: Some(std::path::PathBuf::from("/file2.txt")),
modified: true,
length: 100,
},
);
snapshot.buffers.insert(
BufferId(3),
BufferInfo {
id: BufferId(3),
path: None,
modified: false,
length: 0,
},
);
}
let api = PluginApi::new(hooks, commands, tx, state_snapshot);
let buffers = api.list_buffers();
assert_eq!(buffers.len(), 3);
assert!(buffers.iter().any(|b| b.id.0 == 1));
assert!(buffers.iter().any(|b| b.id.0 == 2));
assert!(buffers.iter().any(|b| b.id.0 == 3));
}
#[test]
fn test_get_primary_cursor() {
let hooks = Arc::new(RwLock::new(HookRegistry::new()));
let commands = Arc::new(RwLock::new(CommandRegistry::new()));
let (tx, _rx) = std::sync::mpsc::channel();
let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
{
let mut snapshot = state_snapshot.write().unwrap();
snapshot.primary_cursor = Some(CursorInfo {
position: 42,
selection: Some(10..42),
});
}
let api = PluginApi::new(hooks, commands, tx, state_snapshot);
let cursor = api.get_primary_cursor();
assert!(cursor.is_some());
let cursor = cursor.unwrap();
assert_eq!(cursor.position, 42);
assert_eq!(cursor.selection, Some(10..42));
}
#[test]
fn test_get_all_cursors() {
let hooks = Arc::new(RwLock::new(HookRegistry::new()));
let commands = Arc::new(RwLock::new(CommandRegistry::new()));
let (tx, _rx) = std::sync::mpsc::channel();
let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
{
let mut snapshot = state_snapshot.write().unwrap();
snapshot.all_cursors = vec![
CursorInfo {
position: 10,
selection: None,
},
CursorInfo {
position: 20,
selection: Some(15..20),
},
CursorInfo {
position: 30,
selection: Some(25..30),
},
];
}
let api = PluginApi::new(hooks, commands, tx, state_snapshot);
let cursors = api.get_all_cursors();
assert_eq!(cursors.len(), 3);
assert_eq!(cursors[0].position, 10);
assert_eq!(cursors[0].selection, None);
assert_eq!(cursors[1].position, 20);
assert_eq!(cursors[1].selection, Some(15..20));
assert_eq!(cursors[2].position, 30);
assert_eq!(cursors[2].selection, Some(25..30));
}
#[test]
fn test_get_viewport() {
let hooks = Arc::new(RwLock::new(HookRegistry::new()));
let commands = Arc::new(RwLock::new(CommandRegistry::new()));
let (tx, _rx) = std::sync::mpsc::channel();
let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
{
let mut snapshot = state_snapshot.write().unwrap();
snapshot.viewport = Some(ViewportInfo {
top_byte: 100,
left_column: 5,
width: 80,
height: 24,
});
}
let api = PluginApi::new(hooks, commands, tx, state_snapshot);
let viewport = api.get_viewport();
assert!(viewport.is_some());
let viewport = viewport.unwrap();
assert_eq!(viewport.top_byte, 100);
assert_eq!(viewport.left_column, 5);
assert_eq!(viewport.width, 80);
assert_eq!(viewport.height, 24);
}
}