reovim_module_commands/
write.rs1use std::path::Path;
4
5use {
6 reovim_driver_codec::{CodecSessionState, ContentCodecFactoryStore},
7 reovim_driver_command::{
8 ArgKind, ArgSpec, Command, CommandContext, CommandHandler, CommandResult, RuntimeSignal,
9 },
10 reovim_driver_session::{BufferApi, CommandApi, ExtensionApi, SessionRuntime},
11 reovim_kernel::api::v1::{
12 CommandId, ModuleId,
13 events::kernel::{BufferSaved, BufferWillSave},
14 },
15};
16
17const COMMANDS_MODULE: ModuleId = ModuleId::new("commands");
18
19#[derive(Debug, Clone, Copy)]
25pub struct WriteCommand;
26
27pub const WRITE_CMD_ID: CommandId = CommandId::new(COMMANDS_MODULE, "write");
29
30impl Command for WriteCommand {
31 fn id(&self) -> CommandId {
32 WRITE_CMD_ID
33 }
34
35 fn description(&self) -> &'static str {
36 "Write the current buffer to disk. Use :w filename to save to a specific file."
37 }
38
39 fn args(&self) -> Vec<ArgSpec> {
40 vec![ArgSpec::optional("file", ArgKind::Rest, "File to write")]
41 }
42
43 fn names(&self) -> &[&'static str] {
44 &["w", "write"]
45 }
46}
47
48#[cfg_attr(coverage_nightly, coverage(off))]
50impl CommandHandler for WriteCommand {
51 fn execute(&self, runtime: &mut SessionRuntime<'_>, ctx: &CommandContext) -> CommandResult {
52 let Some(buffer_id) = ctx.buffer_id() else {
53 return CommandResult::Error("no buffer".to_string());
54 };
55
56 let explicit_file = ctx.string("file");
58 let path = if let Some(file) = explicit_file {
59 file.to_string()
60 } else if let Some(existing) = runtime.buffer_file_path(buffer_id) {
61 existing
62 } else {
63 return CommandResult::Error("No file name".to_string());
64 };
65
66 #[allow(clippy::cast_possible_truncation)]
68 runtime.kernel().event_bus.emit(BufferWillSave {
69 buffer_id: buffer_id.as_usize() as u64,
70 path: path.clone(),
71 });
72
73 let Some(content) = runtime.buffer_content(buffer_id) else {
75 return CommandResult::Error("buffer not found".to_string());
76 };
77
78 let Some(vfs) = ctx.vfs() else {
80 return CommandResult::Error("VFS not available".to_string());
81 };
82 if let Err(e) = encode_and_write(runtime, &path, &content, vfs.as_ref()) {
83 return CommandResult::Error(format!("Write failed: {e}"));
84 }
85
86 if explicit_file.is_some() {
88 runtime.rename_buffer(buffer_id, &path);
89 }
90
91 runtime.set_buffer_modified(buffer_id, false);
93
94 #[allow(clippy::cast_possible_truncation)]
96 let buffer_id_raw = buffer_id.as_usize() as u64;
97 runtime.kernel().event_bus.emit(BufferSaved {
98 buffer_id: buffer_id_raw,
99 path,
100 });
101
102 CommandResult::Success
103 }
104}
105
106#[cfg_attr(coverage_nightly, coverage(off))]
111fn encode_and_write(
112 runtime: &mut SessionRuntime<'_>,
113 path: &str,
114 content: &str,
115 vfs: &dyn reovim_driver_vfs::VfsDriver,
116) -> Result<(), String> {
117 if let Some(buffer_id) = runtime.active_buffer() {
119 let codec_state = runtime.shared_ext_mut::<CodecSessionState>();
120 if let Some(metadata) = codec_state.and_then(|cs| cs.get(buffer_id)) {
121 if metadata.get("readonly") == Some("true") {
123 return Err("buffer is read-only (lossy codec decode)".to_string());
124 }
125
126 let content_type = metadata.content_type().clone();
128 let metadata_clone = metadata.clone();
129
130 let services = &runtime.kernel().services;
131 if let Some(factory_store) = services.get::<ContentCodecFactoryStore>()
132 && let Some(codec) = factory_store.find(&content_type)
133 {
134 match codec.encode(content, &metadata_clone) {
135 Some(Ok(bytes)) => {
136 return vfs
137 .write(Path::new(path), &bytes)
138 .map_err(|e| e.to_string());
139 }
140 Some(Err(e)) => {
141 return Err(format!("codec encode failed: {e}"));
142 }
143 None => {
144 return Err("buffer is read-only (one-way codec)".to_string());
145 }
146 }
147 }
148 }
149 }
150
151 vfs.write_str(Path::new(path), content)
153 .map_err(|e| e.to_string())
154}
155
156#[derive(Debug, Clone, Copy)]
160pub struct WriteQuitCommand;
161
162impl Command for WriteQuitCommand {
163 fn id(&self) -> CommandId {
164 CommandId::new(COMMANDS_MODULE, "write-quit")
165 }
166
167 fn description(&self) -> &'static str {
168 "Write the current buffer and quit the editor."
169 }
170
171 fn names(&self) -> &[&'static str] {
172 &["wq"]
173 }
174}
175
176#[cfg_attr(coverage_nightly, coverage(off))]
178impl CommandHandler for WriteQuitCommand {
179 fn execute(&self, runtime: &mut SessionRuntime<'_>, ctx: &CommandContext) -> CommandResult {
180 let result = runtime.execute_command(WRITE_CMD_ID, ctx.clone());
182 if result.is_error() {
183 return result;
184 }
185
186 runtime.signal(RuntimeSignal::Quit);
188 CommandResult::Success
189 }
190}
191
192#[cfg(test)]
193#[path = "write_tests.rs"]
194mod tests;