reovim_module_snippet/
lib.rs1#![cfg_attr(coverage_nightly, allow(unused_features))]
2#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
3pub mod ast;
30pub mod command;
31pub mod engine;
32mod expander;
33pub mod ids;
34pub mod inheritance;
35mod keybinding;
36pub mod loader;
37pub mod loader_friendly;
38pub mod parser;
39pub mod project;
40pub mod provider;
41pub mod resolver;
42pub mod state;
43pub mod transform;
44pub mod variables;
45
46use std::path::Path;
47
48use std::sync::Arc;
49
50use {
51 reovim_driver_command::{CommandHandler, CommandHandlerStore, CommandProvider},
52 reovim_driver_input::{KeybindingStore, ModeInfo, ModeInfoStore, ResolverRegistry},
53 reovim_driver_session::SnippetExpanderRegistry,
54 reovim_kernel::api::v1::{
55 CursorStyle, KeybindingRegistration, Module, ModuleContext, ModuleError, ModuleId,
56 ProbeResult, Version, pr_info,
57 },
58};
59
60use crate::{
61 loader::JsonSnippetProvider,
62 loader_friendly::FriendlySnippetsProvider,
63 provider::{SnippetRegistry, SnippetRegistryHandle},
64};
65
66#[must_use]
73pub fn build_registry(data_dir: &Path) -> SnippetRegistry {
74 let mut registry = SnippetRegistry::new();
75
76 let user_dir = data_dir.join("user");
78 let legacy_dir = data_dir.join("snippets");
79
80 let user_source = if user_dir.exists() {
81 &user_dir
82 } else {
83 &legacy_dir
85 };
86
87 if let Ok(provider) = JsonSnippetProvider::load_directory(user_source) {
88 registry.register(Box::new(provider));
89 }
90
91 let friendly_dir = data_dir.join("friendly-snippets");
93 if friendly_dir.join("package.json").exists()
94 && let Ok(provider) = FriendlySnippetsProvider::load(&friendly_dir)
95 {
96 registry.register(Box::new(provider));
97 }
98
99 let builtin_dir = data_dir.join("built-in");
101 if let Ok(provider) = JsonSnippetProvider::load_directory(&builtin_dir) {
102 registry.register(Box::new(provider));
103 }
104
105 registry
106}
107
108pub struct SnippetModule {
113 handle: Option<SnippetRegistryHandle>,
115 data_dir: Option<std::path::PathBuf>,
117}
118
119impl SnippetModule {
120 #[must_use]
122 pub const fn new() -> Self {
123 Self {
124 handle: None,
125 data_dir: None,
126 }
127 }
128}
129
130impl Default for SnippetModule {
131 fn default() -> Self {
132 Self::new()
133 }
134}
135
136impl Module for SnippetModule {
137 fn id(&self) -> ModuleId {
138 ids::MODULE
139 }
140
141 fn name(&self) -> &'static str {
142 "Snippet"
143 }
144
145 fn version(&self) -> Version {
146 Version::new(0, 1, 0)
147 }
148
149 fn dependencies(&self) -> Vec<ModuleId> {
150 vec![]
151 }
152
153 fn optional_dependencies(&self) -> Vec<ModuleId> {
154 vec![ModuleId::new("vim")]
157 }
158
159 #[cfg_attr(coverage_nightly, coverage(off))]
160 fn init(&mut self, ctx: &ModuleContext) -> ProbeResult {
161 let handle = SnippetRegistryHandle::new(build_registry(&ctx.data_dir));
163 self.handle = Some(handle.clone());
164 self.data_dir = Some(ctx.data_dir.clone());
165
166 let modes = ctx.services.get_or_create::<ModeInfoStore>();
169 let parent_insert = resolve_snippet_parent(ctx, &modes);
170
171 let effective_parent = parent_insert.unwrap_or(ids::NAVIGATING_MODE);
173
174 let store = ctx.services.get_or_create::<CommandHandlerStore>();
176 let commands =
177 command::all_commands(handle, effective_parent.clone(), ctx.data_dir.clone());
178 let command_count = commands.len();
179 for cmd_handler in commands {
180 store.add(cmd_handler);
181 }
182
183 let resolvers = ctx.services.get_or_create::<ResolverRegistry>();
185 resolvers.register(resolver::SnippetResolver::with_parent(effective_parent.clone()));
186
187 modes.add(ModeInfo {
189 id: ids::NAVIGATING_MODE,
190 display_name: "SNIPPET",
191 cursor_style: CursorStyle::Bar,
192 accepts_char_input: true,
193 has_selection: true,
194 inherits_from: Some(effective_parent),
195 is_entry: false,
196 });
197
198 let keybinding_store = ctx.services.get_or_create::<KeybindingStore>();
200 keybinding_store.add_all(self.keybindings());
201
202 let expander_registry = ctx.services.get_or_create::<SnippetExpanderRegistry>();
204 expander_registry.register(Arc::new(expander::SnippetExpanderImpl));
205
206 pr_info!("Snippet module initialized with {command_count} commands");
207 ProbeResult::Success
208 }
209
210 #[cfg_attr(coverage_nightly, coverage(off))]
211 fn exit(&mut self) -> Result<(), ModuleError> {
212 self.handle = None;
213 self.data_dir = None;
214 Ok(())
215 }
216
217 fn provides(&self) -> &[&'static str] {
218 &[reovim_capabilities::SNIPPET_PROVIDER]
219 }
220
221 #[cfg_attr(coverage_nightly, coverage(off))]
222 fn keybindings(&self) -> Vec<KeybindingRegistration> {
223 let mut bindings = keybinding::all();
225 bindings.extend([
227 KeybindingRegistration::new("<Tab>", ids::JUMP_NEXT)
229 .with_modes(&["snippet:navigating"])
230 .with_description("Jump to next tab stop")
231 .with_category("snippet"),
232 KeybindingRegistration::new("<S-Tab>", ids::JUMP_PREV)
234 .with_modes(&["snippet:navigating"])
235 .with_description("Jump to previous tab stop")
236 .with_category("snippet"),
237 KeybindingRegistration::new("<Esc>", ids::CANCEL)
239 .with_modes(&["snippet:navigating"])
240 .with_description("Cancel snippet navigation")
241 .with_category("snippet"),
242 ]);
243 bindings
244 }
245}
246
247impl CommandProvider for SnippetModule {
248 fn command_handlers(&self) -> Vec<Box<dyn CommandHandler>> {
249 let handle = self
250 .handle
251 .clone()
252 .unwrap_or_else(|| SnippetRegistryHandle::new(SnippetRegistry::new()));
253 let fallback_mode = reovim_kernel::api::v1::ModeId::new(ModuleId::new("editor"), "insert");
256 let data_dir = self
257 .data_dir
258 .clone()
259 .unwrap_or_else(|| std::path::PathBuf::from("/tmp/reovim-snippet-fallback"));
260 command::all_commands(handle, fallback_mode, data_dir)
261 }
262}
263
264#[cfg_attr(coverage_nightly, coverage(off))]
268fn resolve_snippet_parent(
269 ctx: &ModuleContext,
270 modes: &ModeInfoStore,
271) -> Option<reovim_kernel::api::v1::ModeId> {
272 use reovim_driver_manifest::ModeBridgeStore;
273
274 let bridge_store = ctx.services.get::<ModeBridgeStore>()?;
275 let parent_str = bridge_store.find_parent("snippet:navigating")?;
276 let (module, name) = parent_str.split_once(':')?;
277
278 if let Some(mode_id) = modes.find_by_name(module, name) {
279 return Some(mode_id);
280 }
281 tracing::warn!("Mode bridge parent '{parent_str}' not found in ModeInfoStore");
282 None
283}
284
285#[cfg(feature = "dynamic")]
287reovim_module_macros::declare_module!(SnippetModule);
288
289#[cfg(test)]
290#[path = "lib_tests.rs"]
291mod tests;