use std::path::Path;
use {
reovim_driver_codec::{
CodecSessionState, ContentClassifierStore, ContentCodecFactoryStore, ContentType,
},
reovim_driver_command::{
ArgKind, ArgSpec, Command, CommandContext, CommandHandler, CommandResult,
},
reovim_driver_session::{BufferApi, ExtensionApi, SessionRuntime},
reovim_kernel::api::v1::{CommandId, ModuleId, events::kernel::FileOpened},
};
const COMMANDS_MODULE: ModuleId = ModuleId::new("commands");
#[derive(Debug, Clone, Copy)]
pub struct EditCommand;
impl Command for EditCommand {
fn id(&self) -> CommandId {
CommandId::new(COMMANDS_MODULE, "edit")
}
fn description(&self) -> &'static str {
"Edit (open) a file in the current buffer"
}
fn args(&self) -> Vec<ArgSpec> {
vec![ArgSpec::optional("file", ArgKind::Rest, "File to edit")]
}
fn names(&self) -> &[&'static str] {
&["e", "edit"]
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
impl CommandHandler for EditCommand {
fn execute(&self, runtime: &mut SessionRuntime<'_>, ctx: &CommandContext) -> CommandResult {
let Some(filename) = ctx.string("file") else {
return CommandResult::Error(
"invalid arguments: No filename specified. Reload not yet implemented.".to_string(),
);
};
let Some(buffer_id) = ctx.buffer_id() else {
return CommandResult::Error("no buffer".to_string());
};
let Some(vfs) = ctx.vfs() else {
return CommandResult::Error("execution failed: VFS not available".to_string());
};
let bytes = match vfs.read(Path::new(filename)) {
Ok(b) => b,
Err(e) => {
return CommandResult::Error(format!(
"execution failed: Cannot read file '{filename}': {e}"
));
}
};
let content = match decode_file_content(&bytes, filename, runtime) {
Ok(text) => text,
Err(e) => {
return CommandResult::Error(format!("execution failed: {e}"));
}
};
{
let kernel = runtime.kernel();
let Some(buffer_arc) = kernel.buffers.get(buffer_id) else {
return CommandResult::Error(format!(
"execution failed: Buffer {} not found",
buffer_id.as_usize()
));
};
let canonical_path = std::fs::canonicalize(filename)
.map_or_else(|_| filename.to_string(), |p| p.to_string_lossy().into_owned());
{
let mut buffer = buffer_arc.write();
buffer.set_content(&content);
buffer.set_file_path(Some(canonical_path.clone()));
buffer.set_modified(false);
}
#[allow(clippy::cast_possible_truncation)]
let buffer_id_raw = buffer_id.as_usize() as u64;
kernel.event_bus.emit(FileOpened {
buffer_id: buffer_id_raw,
path: canonical_path,
});
}
runtime.record_buffer_modified(buffer_id);
CommandResult::Success
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
fn decode_file_content(
bytes: &[u8],
filename: &str,
runtime: &mut SessionRuntime<'_>,
) -> Result<String, String> {
let services = &runtime.kernel().services;
let classifier_store = services.get::<ContentClassifierStore>();
let factory_store = services.get::<ContentCodecFactoryStore>();
if let (Some(classifiers), Some(factories)) = (classifier_store, factory_store) {
let content_type = classifiers
.classify(bytes, filename)
.unwrap_or_else(|| ContentType::new(ContentType::UTF8));
if let Some(codec) = factories.find(&content_type) {
match codec.decode(bytes) {
Ok(result) => {
let content = result.content;
if let Some(buffer_id) = runtime.active_buffer()
&& let Some(codec_state) = runtime.shared_ext_mut::<CodecSessionState>()
{
codec_state.insert(buffer_id, result.metadata);
}
return Ok(content);
}
Err(e) => {
tracing::warn!(
"Codec decode failed for {filename}: {e}, falling back to UTF-8"
);
}
}
}
}
String::from_utf8(bytes.to_vec()).map_err(|e| {
let offset = e.utf8_error().valid_up_to();
format!("File is not valid UTF-8 (invalid byte at offset {offset})")
})
}
#[cfg(test)]
#[path = "edit_tests.rs"]
mod tests;