use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use tokio::sync::mpsc;
use crate::core::CoderLibError;
use crate::lsp::{Position, Range};
use crate::integration::{HostIntegration, HostCapabilities, HostInfo, CoderEvent, MessageLevel};
use crate::core::error::IntegrationError;
use crate::tools::Permission;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EditState {
pub current_file: Option<PathBuf>,
pub cursor_position: Position,
pub selection: Option<Range>,
pub open_files: Vec<PathBuf>,
pub working_directory: PathBuf,
pub editor_mode: String,
pub current_line: String,
pub visible_range: Range,
pub has_unsaved_changes: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EditConfig {
pub ai_hotkey: String,
pub auto_context: bool,
pub max_context_files: usize,
pub max_context_size: usize,
pub enable_streaming: bool,
pub show_token_usage: bool,
pub auto_apply_simple: bool,
}
impl Default for EditConfig {
fn default() -> Self {
Self {
ai_hotkey: "Ctrl+I".to_string(),
auto_context: true,
max_context_files: 10,
max_context_size: 50000,
enable_streaming: true,
show_token_usage: true,
auto_apply_simple: false,
}
}
}
pub struct EditHost {
state: Arc<Mutex<EditState>>,
config: EditConfig,
event_sender: Option<mpsc::UnboundedSender<EditCommand>>,
file_cache: Arc<Mutex<HashMap<PathBuf, String>>>,
capabilities: HostCapabilities,
host_info: HostInfo,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum EditCommand {
OpenFile { path: PathBuf },
CloseFile { path: PathBuf },
MoveCursor { position: Position },
SetSelection { range: Range },
InsertText { text: String },
ReplaceText { range: Range, text: String },
DeleteText { range: Range },
SaveFile,
SaveFileAs { path: PathBuf },
ShowMessage { level: MessageLevel, message: String },
ShowAIAssistant { initial_prompt: Option<String> },
UpdateStatus { message: String },
RefreshFiles,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum EditResponse {
Success,
Error { message: String },
FileContent { path: PathBuf, content: String },
StateUpdate { state: EditState },
UserInput { input: String },
}
impl EditHost {
pub fn new(config: EditConfig) -> Self {
let initial_state = EditState {
current_file: None,
cursor_position: Position { line: 1, character: 1 },
selection: None,
open_files: Vec::new(),
working_directory: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
editor_mode: "normal".to_string(),
current_line: String::new(),
visible_range: Range {
start: Position { line: 1, character: 1 },
end: Position { line: 50, character: 1 },
},
has_unsaved_changes: false,
};
let capabilities = HostCapabilities {
file_modification: true,
ui_dialogs: true,
command_execution: true,
notifications: true,
project_access: true,
cursor_control: true,
syntax_highlighting: false,
multi_file: true,
};
let host_info = HostInfo {
name: "Microsoft Edit".to_string(),
version: "1.0.0".to_string(),
capabilities: capabilities.clone(),
metadata: serde_json::json!({
"editor_type": "console",
"supports_streaming": config.enable_streaming,
"max_file_size": 10 * 1024 * 1024,
"supported_languages": [
"rust", "python", "javascript", "typescript", "go",
"java", "c", "cpp", "csharp", "html", "css",
"json", "yaml", "toml", "markdown"
]
}),
};
Self {
state: Arc::new(Mutex::new(initial_state)),
config,
event_sender: None,
file_cache: Arc::new(Mutex::new(HashMap::new())),
capabilities,
host_info,
}
}
pub fn set_event_sender(&mut self, sender: mpsc::UnboundedSender<EditCommand>) {
self.event_sender = Some(sender);
}
pub fn update_state(&self, new_state: EditState) {
if let Ok(mut state) = self.state.lock() {
*state = new_state;
}
}
pub fn get_state(&self) -> EditState {
self.state.lock().unwrap().clone()
}
async fn send_command(&self, command: EditCommand) -> Result<(), CoderLibError> {
if let Some(sender) = &self.event_sender {
sender.send(command)
.map_err(|e| CoderLibError::Integration(IntegrationError::OperationFailed(format!("Failed to send command: {}", e))))?;
}
Ok(())
}
pub async fn get_cursor_context(&self, lines_before: usize, lines_after: usize) -> Result<String, CoderLibError> {
let state = self.get_state();
if let Some(current_file) = &state.current_file {
let content = self.get_file_content(current_file).await?;
let lines: Vec<&str> = content.lines().collect();
let current_line = (state.cursor_position.line as usize).saturating_sub(1);
let start_line = current_line.saturating_sub(lines_before);
let end_line = (current_line + lines_after + 1).min(lines.len());
let context_lines = &lines[start_line..end_line];
Ok(context_lines.join("\n"))
} else {
Ok(String::new())
}
}
pub async fn get_project_context(&self) -> Result<String, CoderLibError> {
let state = self.get_state();
let mut context = String::new();
if let Some(current_file) = &state.current_file {
context.push_str(&format!("Current file: {}\n", current_file.display()));
context.push_str(&format!("Cursor: line {}, column {}\n",
state.cursor_position.line, state.cursor_position.character));
if let Some(selection) = &state.selection {
context.push_str(&format!("Selection: {}:{} to {}:{}\n",
selection.start.line, selection.start.character,
selection.end.line, selection.end.character));
}
}
if !state.open_files.is_empty() {
context.push_str("\nOpen files:\n");
for file in &state.open_files {
context.push_str(&format!("- {}\n", file.display()));
}
}
context.push_str(&format!("\nWorking directory: {}\n", state.working_directory.display()));
Ok(context)
}
pub async fn gather_intelligent_context(&self) -> Result<String, CoderLibError> {
let mut context = String::new();
context.push_str(&self.get_project_context().await?);
context.push_str("\n");
let cursor_context = self.get_cursor_context(5, 5).await?;
if !cursor_context.is_empty() {
context.push_str("Context around cursor:\n");
context.push_str("```\n");
context.push_str(&cursor_context);
context.push_str("\n```\n\n");
}
if context.len() > self.config.max_context_size {
let truncated = &context[..self.config.max_context_size];
context = format!("{}...\n[Context truncated]", truncated);
}
Ok(context)
}
}
#[async_trait]
impl HostIntegration for EditHost {
async fn on_event(&self, event: CoderEvent) -> Result<(), IntegrationError> {
match event {
CoderEvent::ProcessingStarted { session_id } => {
self.send_command(EditCommand::UpdateStatus {
message: format!("AI processing started ({})", session_id)
}).await.map_err(|e| IntegrationError::OperationFailed(e.to_string()))?;
}
CoderEvent::ProcessingCompleted { session_id, response: _ } => {
self.send_command(EditCommand::UpdateStatus {
message: format!("AI processing completed ({})", session_id)
}).await.map_err(|e| IntegrationError::OperationFailed(e.to_string()))?;
}
CoderEvent::ProcessingFailed { session_id, error } => {
self.send_command(EditCommand::ShowMessage {
level: MessageLevel::Error,
message: format!("AI processing failed ({}): {}", session_id, error)
}).await.map_err(|e| IntegrationError::OperationFailed(e.to_string()))?;
}
_ => {} }
Ok(())
}
async fn request_permission(&self, _permission: Permission) -> Result<bool, IntegrationError> {
Ok(true)
}
async fn get_file_content(&self, path: &Path) -> Result<String, IntegrationError> {
if let Ok(cache) = self.file_cache.lock() {
if let Some(content) = cache.get(path) {
return Ok(content.clone());
}
}
let content = tokio::fs::read_to_string(path).await
.map_err(|e| IntegrationError::FileNotFound(path.to_path_buf()))?;
if let Ok(mut cache) = self.file_cache.lock() {
cache.insert(path.to_path_buf(), content.clone());
}
Ok(content)
}
async fn update_file_content(&self, path: &Path, content: &str) -> Result<(), IntegrationError> {
if let Ok(mut cache) = self.file_cache.lock() {
cache.insert(path.to_path_buf(), content.to_string());
}
self.send_command(EditCommand::ReplaceText {
range: Range {
start: Position { line: 1, character: 1 },
end: Position { line: u32::MAX, character: u32::MAX },
},
text: content.to_string(),
}).await.map_err(|e| IntegrationError::OperationFailed(e.to_string()))?;
Ok(())
}
async fn get_cursor_position(&self) -> Result<Position, IntegrationError> {
let state = self.get_state();
Ok(state.cursor_position)
}
async fn set_cursor_position(&self, position: Position) -> Result<(), IntegrationError> {
self.send_command(EditCommand::MoveCursor { position }).await
.map_err(|e| IntegrationError::OperationFailed(e.to_string()))
}
async fn get_selection(&self) -> Result<Option<Range>, IntegrationError> {
let state = self.get_state();
Ok(state.selection)
}
async fn set_selection(&self, range: Range) -> Result<(), IntegrationError> {
self.send_command(EditCommand::SetSelection { range }).await
.map_err(|e| IntegrationError::OperationFailed(e.to_string()))
}
async fn insert_text(&self, text: &str) -> Result<(), IntegrationError> {
self.send_command(EditCommand::InsertText { text: text.to_string() }).await
.map_err(|e| IntegrationError::OperationFailed(e.to_string()))
}
async fn replace_text(&self, range: Range, text: &str) -> Result<(), IntegrationError> {
self.send_command(EditCommand::ReplaceText {
range,
text: text.to_string()
}).await.map_err(|e| IntegrationError::OperationFailed(e.to_string()))
}
async fn get_active_file(&self) -> Result<Option<PathBuf>, IntegrationError> {
let state = self.get_state();
Ok(state.current_file)
}
async fn get_open_files(&self) -> Result<Vec<PathBuf>, IntegrationError> {
let state = self.get_state();
Ok(state.open_files)
}
async fn get_project_root(&self) -> Result<Option<PathBuf>, IntegrationError> {
let state = self.get_state();
Ok(Some(state.working_directory))
}
async fn show_message(&self, message: &str, level: MessageLevel) -> Result<(), IntegrationError> {
self.send_command(EditCommand::ShowMessage {
level,
message: message.to_string()
}).await.map_err(|e| IntegrationError::OperationFailed(e.to_string()))
}
async fn show_notification(&self, title: &str, message: &str) -> Result<(), IntegrationError> {
let full_message = format!("{}: {}", title, message);
self.show_message(&full_message, MessageLevel::Info).await
}
async fn request_input(&self, prompt: &str, _default: Option<&str>) -> Result<Option<String>, IntegrationError> {
self.show_message(prompt, MessageLevel::Info).await?;
Ok(None) }
async fn confirm(&self, message: &str) -> Result<bool, IntegrationError> {
self.show_message(message, MessageLevel::Info).await?;
Ok(true) }
async fn execute_command(&self, command: &str, args: &[&str]) -> Result<String, IntegrationError> {
let output = tokio::process::Command::new(command)
.args(args)
.current_dir(&self.get_state().working_directory)
.output()
.await
.map_err(|e| IntegrationError::OperationFailed(format!("Failed to execute command: {}", e)))?;
if output.status.success() {
Ok(String::from_utf8_lossy(&output.stdout).to_string())
} else {
Err(IntegrationError::OperationFailed(format!(
"Command failed: {}",
String::from_utf8_lossy(&output.stderr)
)))
}
}
fn get_host_info(&self) -> HostInfo {
self.host_info.clone()
}
}