use async_trait::async_trait;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::{mpsc, RwLock};
use tracing::{debug, info, warn};
use crate::core::CoderLibError;
use crate::integration::{HostEvent, HostCommand, MessageLevel, EditState, EditConfig};
pub struct EditEventHandler {
state: Arc<RwLock<EditState>>,
config: EditConfig,
listeners: Arc<RwLock<HashMap<String, Box<dyn EventListener>>>>,
command_sender: Option<mpsc::UnboundedSender<HostCommand>>,
hotkey_tracker: Arc<RwLock<HotkeyTracker>>,
}
#[async_trait]
pub trait EventListener: Send + Sync {
async fn handle_event(&self, event: &HostEvent, state: &EditState) -> Result<Option<HostCommand>, CoderLibError>;
fn event_types(&self) -> Vec<String>;
fn priority(&self) -> i32 { 0 }
}
#[derive(Debug, Clone)]
pub struct HotkeyTracker {
current_sequence: Vec<String>,
last_key_time: std::time::Instant,
sequence_timeout: u64,
}
pub struct AIAssistantListener {
hotkey_sequence: Vec<String>,
}
pub struct FileOperationListener;
pub struct CursorTrackingListener;
pub struct AutoSaveListener {
interval: u64,
last_saves: Arc<RwLock<HashMap<PathBuf, std::time::Instant>>>,
}
pub struct ContextGatheringListener {
auto_gather: bool,
max_context_size: usize,
}
impl EditEventHandler {
pub fn new(initial_state: EditState, config: EditConfig) -> Self {
let hotkey_tracker = HotkeyTracker {
current_sequence: Vec::new(),
last_key_time: std::time::Instant::now(),
sequence_timeout: 1000, };
Self {
state: Arc::new(RwLock::new(initial_state)),
config,
listeners: Arc::new(RwLock::new(HashMap::new())),
command_sender: None,
hotkey_tracker: Arc::new(RwLock::new(hotkey_tracker)),
}
}
pub fn set_command_sender(&mut self, sender: mpsc::UnboundedSender<HostCommand>) {
self.command_sender = Some(sender);
}
pub async fn register_default_listeners(&self) -> Result<(), CoderLibError> {
let ai_listener = AIAssistantListener::new(&self.config.ai_hotkey);
self.register_listener("ai_assistant".to_string(), Box::new(ai_listener)).await;
self.register_listener("file_operations".to_string(), Box::new(FileOperationListener)).await;
self.register_listener("cursor_tracking".to_string(), Box::new(CursorTrackingListener)).await;
if self.config.auto_apply_simple {
let auto_save = AutoSaveListener::new(30); self.register_listener("auto_save".to_string(), Box::new(auto_save)).await;
}
if self.config.auto_context {
let context_listener = ContextGatheringListener::new(
self.config.auto_context,
self.config.max_context_size,
);
self.register_listener("context_gathering".to_string(), Box::new(context_listener)).await;
}
info!("Registered default event listeners for Edit integration");
Ok(())
}
pub async fn register_listener(&self, name: String, listener: Box<dyn EventListener>) {
let mut listeners = self.listeners.write().await;
listeners.insert(name, listener);
}
pub async fn unregister_listener(&self, name: &str) {
let mut listeners = self.listeners.write().await;
listeners.remove(name);
}
pub async fn handle_event(&self, event: HostEvent) -> Result<Vec<HostCommand>, CoderLibError> {
debug!("Handling event: {:?}", event);
self.update_state_from_event(&event).await?;
if let HostEvent::KeyPressed(key) = &event {
if let Some(command) = self.handle_hotkey(key).await? {
return Ok(vec![command]);
}
}
let mut commands = Vec::new();
let listeners = self.listeners.read().await;
let state = self.state.read().await;
let mut listener_pairs: Vec<_> = listeners.iter().collect();
listener_pairs.sort_by(|a, b| b.1.priority().cmp(&a.1.priority()));
for (name, listener) in listener_pairs {
let event_type = self.get_event_type(&event);
if listener.event_types().contains(&event_type) || listener.event_types().contains(&"*".to_string()) {
match listener.handle_event(&event, &state).await {
Ok(Some(command)) => {
debug!("Listener '{}' generated command: {:?}", name, command);
commands.push(command);
}
Ok(None) => {
debug!("Listener '{}' handled event without generating command", name);
}
Err(e) => {
warn!("Listener '{}' failed to handle event: {}", name, e);
}
}
}
}
Ok(commands)
}
async fn update_state_from_event(&self, event: &HostEvent) -> Result<(), CoderLibError> {
let mut state = self.state.write().await;
match event {
HostEvent::FileOpened(path) => {
state.current_file = Some(path.clone());
if !state.open_files.contains(path) {
state.open_files.push(path.clone());
}
}
HostEvent::FileClosed(path) => {
state.open_files.retain(|f| f != path);
if state.current_file.as_ref() == Some(path) {
state.current_file = state.open_files.first().cloned();
}
}
HostEvent::FileSaved(path) => {
if state.current_file.as_ref() == Some(path) {
state.has_unsaved_changes = false;
}
}
HostEvent::CursorMoved(position) => {
state.cursor_position = *position;
}
HostEvent::SelectionChanged(range) => {
state.selection = *range;
}
HostEvent::ProjectOpened(path) => {
state.working_directory = path.clone();
}
_ => {} }
Ok(())
}
async fn handle_hotkey(&self, key: &str) -> Result<Option<HostCommand>, CoderLibError> {
let mut tracker = self.hotkey_tracker.write().await;
let now = std::time::Instant::now();
if now.duration_since(tracker.last_key_time).as_millis() > tracker.sequence_timeout as u128 {
tracker.current_sequence.clear();
}
tracker.current_sequence.push(key.to_string());
tracker.last_key_time = now;
let ai_hotkey_parts: Vec<&str> = self.config.ai_hotkey.split('+').collect();
if tracker.current_sequence.len() == ai_hotkey_parts.len() {
let matches = tracker.current_sequence.iter()
.zip(ai_hotkey_parts.iter())
.all(|(pressed, expected)| pressed == expected);
if matches {
tracker.current_sequence.clear();
return Ok(Some(HostCommand::ShowDialog {
title: "AI Assistant".to_string(),
message: "How can I help you with your code?".to_string(),
buttons: vec!["Ask Question".to_string(), "Cancel".to_string()],
}));
}
}
Ok(None)
}
fn get_event_type(&self, event: &HostEvent) -> String {
match event {
HostEvent::ApplicationStarted => "application_started".to_string(),
HostEvent::ApplicationShutdown => "application_shutdown".to_string(),
HostEvent::FileOpened(_) => "file_opened".to_string(),
HostEvent::FileClosed(_) => "file_closed".to_string(),
HostEvent::FileSaved(_) => "file_saved".to_string(),
HostEvent::FileModified(_) => "file_modified".to_string(),
HostEvent::CursorMoved(_) => "cursor_moved".to_string(),
HostEvent::SelectionChanged(_) => "selection_changed".to_string(),
HostEvent::KeyPressed(_) => "key_pressed".to_string(),
HostEvent::CommandExecuted(_) => "command_executed".to_string(),
HostEvent::ProjectOpened(_) => "project_opened".to_string(),
HostEvent::ProjectClosed => "project_closed".to_string(),
}
}
pub async fn get_state(&self) -> EditState {
self.state.read().await.clone()
}
async fn send_command(&self, command: HostCommand) -> Result<(), CoderLibError> {
if let Some(sender) = &self.command_sender {
sender.send(command)
.map_err(|e| CoderLibError::Integration(
crate::core::error::IntegrationError::OperationFailed(
format!("Failed to send command: {}", e)
)
))?;
}
Ok(())
}
}
impl AIAssistantListener {
pub fn new(hotkey: &str) -> Self {
let hotkey_sequence = hotkey.split('+').map(|s| s.to_string()).collect();
Self { hotkey_sequence }
}
}
#[async_trait]
impl EventListener for AIAssistantListener {
async fn handle_event(&self, event: &HostEvent, state: &EditState) -> Result<Option<HostCommand>, CoderLibError> {
match event {
HostEvent::KeyPressed(key) => {
Ok(None)
}
HostEvent::CommandExecuted(cmd) if cmd == "ai_assistant" => {
Ok(Some(HostCommand::ShowDialog {
title: "AI Assistant".to_string(),
message: format!(
"Current file: {}\nCursor: line {}, column {}\n\nHow can I help?",
state.current_file.as_ref().map(|p| p.display().to_string()).unwrap_or("None".to_string()),
state.cursor_position.line,
state.cursor_position.character
),
buttons: vec!["Ask Question".to_string(), "Explain Code".to_string(), "Refactor".to_string(), "Cancel".to_string()],
}))
}
_ => Ok(None),
}
}
fn event_types(&self) -> Vec<String> {
vec!["key_pressed".to_string(), "command_executed".to_string()]
}
fn priority(&self) -> i32 { 100 } }
#[async_trait]
impl EventListener for FileOperationListener {
async fn handle_event(&self, event: &HostEvent, _state: &EditState) -> Result<Option<HostCommand>, CoderLibError> {
match event {
HostEvent::FileOpened(path) => {
info!("File opened: {}", path.display());
Ok(Some(HostCommand::ShowMessage {
message: format!("Opened: {}", path.file_name().unwrap_or_default().to_string_lossy()),
level: MessageLevel::Info,
}))
}
HostEvent::FileSaved(path) => {
info!("File saved: {}", path.display());
Ok(Some(HostCommand::ShowMessage {
message: format!("Saved: {}", path.file_name().unwrap_or_default().to_string_lossy()),
level: MessageLevel::Success,
}))
}
HostEvent::FileModified(path) => {
debug!("File modified: {}", path.display());
Ok(None)
}
_ => Ok(None),
}
}
fn event_types(&self) -> Vec<String> {
vec!["file_opened".to_string(), "file_closed".to_string(), "file_saved".to_string(), "file_modified".to_string()]
}
fn priority(&self) -> i32 { 50 }
}
#[async_trait]
impl EventListener for CursorTrackingListener {
async fn handle_event(&self, event: &HostEvent, state: &EditState) -> Result<Option<HostCommand>, CoderLibError> {
match event {
HostEvent::CursorMoved(position) => {
debug!("Cursor moved to line {}, column {}", position.line, position.character);
if let Some(current_file) = &state.current_file {
if current_file.extension().and_then(|ext| ext.to_str()) == Some("rs") {
debug!("Cursor in Rust file at {}:{}", position.line, position.character);
}
}
Ok(None)
}
HostEvent::SelectionChanged(Some(range)) => {
debug!("Selection changed: {}:{} to {}:{}",
range.start.line, range.start.character,
range.end.line, range.end.character);
Ok(None)
}
_ => Ok(None),
}
}
fn event_types(&self) -> Vec<String> {
vec!["cursor_moved".to_string(), "selection_changed".to_string()]
}
fn priority(&self) -> i32 { 10 } }
impl AutoSaveListener {
pub fn new(interval_seconds: u64) -> Self {
Self {
interval: interval_seconds,
last_saves: Arc::new(RwLock::new(HashMap::new())),
}
}
}
#[async_trait]
impl EventListener for AutoSaveListener {
async fn handle_event(&self, event: &HostEvent, state: &EditState) -> Result<Option<HostCommand>, CoderLibError> {
match event {
HostEvent::FileModified(path) => {
let now = std::time::Instant::now();
let mut last_saves = self.last_saves.write().await;
let should_save = if let Some(last_save) = last_saves.get(path) {
now.duration_since(*last_save).as_secs() >= self.interval
} else {
true
};
if should_save && state.has_unsaved_changes {
last_saves.insert(path.clone(), now);
return Ok(Some(HostCommand::SaveFile(path.clone())));
}
}
HostEvent::FileSaved(path) => {
let mut last_saves = self.last_saves.write().await;
last_saves.insert(path.clone(), std::time::Instant::now());
}
_ => {}
}
Ok(None)
}
fn event_types(&self) -> Vec<String> {
vec!["file_modified".to_string(), "file_saved".to_string()]
}
fn priority(&self) -> i32 { 20 }
}
impl ContextGatheringListener {
pub fn new(auto_gather: bool, max_context_size: usize) -> Self {
Self {
auto_gather,
max_context_size,
}
}
}
#[async_trait]
impl EventListener for ContextGatheringListener {
async fn handle_event(&self, event: &HostEvent, state: &EditState) -> Result<Option<HostCommand>, CoderLibError> {
if !self.auto_gather {
return Ok(None);
}
match event {
HostEvent::FileOpened(_) | HostEvent::CursorMoved(_) | HostEvent::SelectionChanged(_) => {
debug!("Gathering context for AI assistance");
if let Some(current_file) = &state.current_file {
debug!("Would gather context for file: {} at position {}:{}",
current_file.display(),
state.cursor_position.line,
state.cursor_position.character);
}
Ok(None)
}
_ => Ok(None),
}
}
fn event_types(&self) -> Vec<String> {
vec!["file_opened".to_string(), "cursor_moved".to_string(), "selection_changed".to_string()]
}
fn priority(&self) -> i32 { 5 } }