1use std::path::PathBuf;
19use std::sync::{Arc, OnceLock};
20
21use lattice_core::BufferKind;
22use lattice_core::ui::pane::OpenTarget;
23use lattice_grammar::Effect;
24use lattice_mode::{
25 ActionContext, ActionHandler, ActionHandlerContribution, BufferLocal, CapabilitySet, Keymap,
26 KeymapEntry, LifecycleFuture, Mode, ModeContext, ModeId, ModeKind, ModeRegistry, keymap_entry,
27};
28
29pub struct OilMode;
33
34impl OilMode {
35 pub fn mode_id() -> ModeId {
36 ModeId::new("oil-mode")
37 }
38}
39
40fn oil_mode_keymap_entries() -> &'static [KeymapEntry] {
41 static ENTRIES: OnceLock<Vec<KeymapEntry>> = OnceLock::new();
42 ENTRIES.get_or_init(|| {
43 vec![
44 keymap_entry!(
45 mode: Normal,
46 chord: "-",
47 doc: "Navigate to the parent directory in the oil buffer.",
48 cmd: "action:oil-navigate-up"
49 ),
50 keymap_entry!(
51 mode: Normal,
52 chord: "<CR>",
53 doc: "Open the entry under the cursor: descend into a directory, or open a file in the current pane.",
54 cmd: "action:oil-follow"
55 ),
56 keymap_entry!(
57 mode: Normal,
58 chord: "<C-s>",
59 doc: "Open the entry under the cursor in a horizontal split.",
60 cmd: "action:oil-follow-split"
61 ),
62 keymap_entry!(
63 mode: Normal,
64 chord: "<C-v>",
65 doc: "Open the entry under the cursor in a vertical split.",
66 cmd: "action:oil-follow-vsplit"
67 ),
68 keymap_entry!(
69 mode: Normal,
70 chord: "<C-t>",
71 doc: "Open the entry under the cursor in a new tab.",
72 cmd: "action:oil-follow-tab"
73 ),
74 ]
75 })
76}
77
78impl Mode for OilMode {
79 type Guard = ();
80 fn id(&self) -> ModeId {
81 Self::mode_id()
82 }
83 fn kind(&self) -> ModeKind {
84 ModeKind::Major
85 }
86 fn target_buffer_kind(&self) -> Option<BufferKind> {
89 Some(BufferKind::Oil)
90 }
91 fn required_capabilities(&self) -> CapabilitySet {
92 CapabilitySet::empty()
93 }
94 fn invocation_runner(&self) -> Option<ModeId> {
97 Some(Self::mode_id())
98 }
99 fn keymap(&self) -> Keymap {
100 Keymap::from_entries(oil_mode_keymap_entries())
101 }
102 fn action_handlers(&self) -> Vec<ActionHandlerContribution> {
112 let follow: ActionHandler = Arc::new(|ctx: &ActionContext<'_>| -> Option<Effect> {
115 let (dir, name, is_dir) = oil_entry_at(ctx)?;
116 let view = lattice_core::BufferId(ctx.buffer_id.0 as u32);
117 let target = dir.join(&name);
118 Some(if is_dir {
119 Effect::OilNavigate {
120 view,
121 dir: target,
122 focus: None,
123 }
124 } else {
125 Effect::OpenBufferAt {
126 path: Some(target),
127 position: lattice_protocol::Position::ZERO,
128 force: false,
129 content: None,
130 activate_minor: None,
131 }
132 })
133 });
134 let up: ActionHandler = Arc::new(|ctx: &ActionContext<'_>| -> Option<Effect> {
137 let dir = ctx.buffer_local::<OilDir>()?.0.clone();
138 let parent = dir.parent()?.to_path_buf();
139 let view = lattice_core::BufferId(ctx.buffer_id.0 as u32);
140 let focus = dir.file_name().map(|n| n.to_string_lossy().into_owned());
141 Some(Effect::OilNavigate {
142 view,
143 dir: parent,
144 focus,
145 })
146 });
147 vec![
148 ActionHandlerContribution {
149 action_name: "action:oil-follow",
150 handler: follow,
151 },
152 ActionHandlerContribution {
153 action_name: "action:oil-navigate-up",
154 handler: up,
155 },
156 oil_open_in_target("action:oil-follow-split", OpenTarget::Split),
161 oil_open_in_target("action:oil-follow-vsplit", OpenTarget::VSplit),
162 oil_open_in_target("action:oil-follow-tab", OpenTarget::Tab),
163 ]
164 }
165 fn on_activate(&self, _ctx: ModeContext) -> LifecycleFuture<'_, ()> {
166 Box::pin(async { Ok(()) })
167 }
168}
169
170fn oil_entry_at(ctx: &ActionContext<'_>) -> Option<(PathBuf, String, bool)> {
175 let dir = ctx.buffer_local::<OilDir>()?.0.clone();
176 let snapshot = ctx.buffer_local::<OilSnapshotLocal>()?;
177 let entry = snapshot
178 .0
179 .snapshot_entries()
180 .get(ctx.cursor.line as usize)?;
181 Some((dir, entry.name.clone(), entry.is_dir))
182}
183
184fn oil_open_in_target(action_name: &'static str, target: OpenTarget) -> ActionHandlerContribution {
187 let handler: ActionHandler = Arc::new(move |ctx: &ActionContext<'_>| -> Option<Effect> {
188 let (dir, name, _is_dir) = oil_entry_at(ctx)?;
189 Some(Effect::OpenInTarget {
190 path: Some(dir.join(&name)),
191 position: lattice_protocol::Position::ZERO,
192 target,
193 })
194 });
195 ActionHandlerContribution {
196 action_name,
197 handler,
198 }
199}
200
201#[derive(Debug, Clone)]
208pub struct OilDir(pub PathBuf);
209
210#[derive(Debug, Clone, Default)]
217pub struct OilSnapshotLocal(pub super::OilSnapshot);
218
219impl BufferLocal for OilSnapshotLocal {
220 const NAME: &'static str = "oil-mode.snapshot";
221 const DOC: &'static str = "Directory entries as of open or the last successful `:w`. \
222 `:w` diffs the buffer's text against this to derive the renames, \
223 deletes and creates to execute.";
224 const OWNER_MODE: &'static str = "oil-mode";
225 fn describe(&self) -> String {
226 format!("{} entries", self.0.snapshot_entries().len())
227 }
228}
229
230impl BufferLocal for OilDir {
231 const NAME: &'static str = "oil-mode.dir";
232 const DOC: &'static str = "Directory the oil buffer's editable listing represents. \
233 Diff-on-:write applies filesystem ops relative to this \
234 path; status line shows it.";
235 const OWNER_MODE: &'static str = "oil-mode";
236 fn describe(&self) -> String {
237 self.0.display().to_string()
238 }
239}
240
241pub fn register_oil_modes(registry: &mut ModeRegistry) {
248 registry.register(OilMode).expect("oil-mode register");
249}
250
251#[cfg(test)]
252mod tests {
253 use super::*;
254
255 #[test]
256 fn oil_mode_id_and_kind() {
257 assert_eq!(OilMode.id(), OilMode::mode_id());
258 assert_eq!(OilMode::mode_id().as_str(), "oil-mode");
259 assert_eq!(OilMode.kind(), ModeKind::Major);
260 }
261
262 #[test]
263 fn oil_dir_buffer_local_owner_mode_is_oil_mode() {
264 assert_eq!(<OilDir as BufferLocal>::OWNER_MODE, "oil-mode");
265 assert_eq!(<OilDir as BufferLocal>::NAME, "oil-mode.dir");
266 let d = OilDir(PathBuf::from("/tmp/x"));
267 assert_eq!(d.describe(), "/tmp/x");
268 }
269
270 #[test]
271 fn register_oil_modes_populates_registry() {
272 let mut registry = ModeRegistry::new();
273 register_oil_modes(&mut registry);
274 assert!(registry.is_registered(OilMode::mode_id()));
275 }
276
277 #[test]
278 fn oil_mode_keymap_binds_navigation_and_open_chords() {
279 use lattice_mode::Mode as _;
280 let km = OilMode.keymap();
281 let bound: Vec<(&str, Option<&str>)> =
284 km.entries.iter().map(|e| (e.chord, e.command)).collect();
285 for (chord, cmd) in [
286 ("-", "action:oil-navigate-up"),
287 ("<CR>", "action:oil-follow"),
288 ("<C-s>", "action:oil-follow-split"),
289 ("<C-v>", "action:oil-follow-vsplit"),
290 ("<C-t>", "action:oil-follow-tab"),
291 ] {
292 assert!(
293 bound.contains(&(chord, Some(cmd))),
294 "oil-mode must bind {chord} → {cmd}; got {bound:?}",
295 );
296 }
297 }
298
299 #[test]
300 fn oil_mode_keymap_entry_chord_and_command() {
301 use lattice_mode::Mode as _;
302 let km = OilMode.keymap();
303 let e = &km.entries[0];
304 assert_eq!(e.chord, "-");
305 assert_eq!(e.command, Some("action:oil-navigate-up"));
306 }
307}