1use std::path::Path;
10
11use mlua::{Function, Lua, Result as LuaResult, Table, Value};
12
13use super::vault_context::VaultContext;
14use crate::index::NoteQuery;
15use crate::types::validation::yaml_to_lua_table;
16
17pub fn register_index_bindings(lua: &Lua) -> LuaResult<()> {
22 let mdv: Table = lua.globals().get("mdv")?;
23
24 mdv.set("current_note", create_current_note_fn(lua)?)?;
25 mdv.set("backlinks", create_backlinks_fn(lua)?)?;
26 mdv.set("outlinks", create_outlinks_fn(lua)?)?;
27 mdv.set("query", create_query_fn(lua)?)?;
28 mdv.set("find_project", create_find_project_fn(lua)?)?;
29
30 Ok(())
31}
32
33fn create_current_note_fn(lua: &Lua) -> LuaResult<Function> {
47 lua.create_function(|lua, ()| {
48 let ctx = lua
49 .app_data_ref::<VaultContext>()
50 .ok_or_else(|| mlua::Error::runtime("VaultContext not available"))?;
51
52 let current = match &ctx.current_note {
53 Some(note) => note,
54 None => return Ok(Value::Nil),
55 };
56
57 let note_table = lua.create_table()?;
59 note_table.set("path", current.path.as_str())?;
60 note_table.set("type", current.note_type.as_str())?;
61 note_table.set("content", current.content.as_str())?;
62
63 if let Some(title) = ¤t.title {
64 note_table.set("title", title.as_str())?;
65 }
66
67 if let Some(fm) = ¤t.frontmatter {
68 let fm_table = yaml_to_lua_table(lua, fm)?;
69 note_table.set("frontmatter", fm_table)?;
70 }
71
72 Ok(Value::Table(note_table))
73 })
74}
75
76fn create_backlinks_fn(lua: &Lua) -> LuaResult<Function> {
89 lua.create_function(|lua, path: String| {
90 let ctx = lua
91 .app_data_ref::<VaultContext>()
92 .ok_or_else(|| mlua::Error::runtime("VaultContext not available"))?;
93
94 let db = match &ctx.index_db {
95 Some(db) => db,
96 None => {
97 return Err(mlua::Error::runtime(
98 "Index database not available. Run 'mdv reindex' first.",
99 ));
100 }
101 };
102
103 let resolved_path = resolve_note_path(&ctx.vault_root, &path);
105
106 let note = match db.get_note_by_path(Path::new(&resolved_path)) {
108 Ok(Some(n)) => n,
109 Ok(None) => {
110 return Ok(Value::Table(lua.create_table()?));
112 }
113 Err(e) => return Err(mlua::Error::runtime(format!("Index error: {}", e))),
114 };
115
116 let note_id = match note.id {
117 Some(id) => id,
118 None => return Ok(Value::Table(lua.create_table()?)),
119 };
120
121 let backlinks = db
123 .get_backlinks(note_id)
124 .map_err(|e| mlua::Error::runtime(format!("Index error: {}", e)))?;
125
126 let result = lua.create_table()?;
128 for (i, link) in backlinks.iter().enumerate() {
129 let link_table = lua.create_table()?;
130
131 if let Ok(Some(source_note)) = db.get_note_by_id(link.source_id) {
133 link_table
134 .set("source_path", source_note.path.to_string_lossy().to_string())?;
135 link_table.set("source_title", source_note.title)?;
136 link_table.set("source_type", source_note.note_type.as_str())?;
137 }
138
139 if let Some(text) = &link.link_text {
140 link_table.set("link_text", text.as_str())?;
141 }
142 if let Some(context) = &link.context {
143 link_table.set("context", context.as_str())?;
144 }
145 link_table.set("link_type", link.link_type.as_str())?;
146
147 result.set(i + 1, link_table)?;
148 }
149
150 Ok(Value::Table(result))
151 })
152}
153
154fn create_outlinks_fn(lua: &Lua) -> LuaResult<Function> {
167 lua.create_function(|lua, path: String| {
168 let ctx = lua
169 .app_data_ref::<VaultContext>()
170 .ok_or_else(|| mlua::Error::runtime("VaultContext not available"))?;
171
172 let db = match &ctx.index_db {
173 Some(db) => db,
174 None => {
175 return Err(mlua::Error::runtime(
176 "Index database not available. Run 'mdv reindex' first.",
177 ));
178 }
179 };
180
181 let resolved_path = resolve_note_path(&ctx.vault_root, &path);
183
184 let note = match db.get_note_by_path(Path::new(&resolved_path)) {
186 Ok(Some(n)) => n,
187 Ok(None) => {
188 return Ok(Value::Table(lua.create_table()?));
190 }
191 Err(e) => return Err(mlua::Error::runtime(format!("Index error: {}", e))),
192 };
193
194 let note_id = match note.id {
195 Some(id) => id,
196 None => return Ok(Value::Table(lua.create_table()?)),
197 };
198
199 let outlinks = db
201 .get_outgoing_links(note_id)
202 .map_err(|e| mlua::Error::runtime(format!("Index error: {}", e)))?;
203
204 let result = lua.create_table()?;
206 for (i, link) in outlinks.iter().enumerate() {
207 let link_table = lua.create_table()?;
208
209 link_table.set("target_path", link.target_path.as_str())?;
210
211 if let Some(target_id) = link.target_id {
213 if let Ok(Some(target_note)) = db.get_note_by_id(target_id) {
214 link_table.set("target_title", target_note.title)?;
215 link_table.set("target_type", target_note.note_type.as_str())?;
216 link_table.set("resolved", true)?;
217 } else {
218 link_table.set("resolved", false)?;
219 }
220 } else {
221 link_table.set("resolved", false)?;
222 }
223
224 if let Some(text) = &link.link_text {
225 link_table.set("link_text", text.as_str())?;
226 }
227 link_table.set("link_type", link.link_type.as_str())?;
228
229 result.set(i + 1, link_table)?;
230 }
231
232 Ok(Value::Table(result))
233 })
234}
235
236fn create_query_fn(lua: &Lua) -> LuaResult<Function> {
253 lua.create_function(|lua, opts: Option<Table>| {
254 let ctx = lua
255 .app_data_ref::<VaultContext>()
256 .ok_or_else(|| mlua::Error::runtime("VaultContext not available"))?;
257
258 let db = match &ctx.index_db {
259 Some(db) => db,
260 None => {
261 return Err(mlua::Error::runtime(
262 "Index database not available. Run 'mdv reindex' first.",
263 ));
264 }
265 };
266
267 let mut query = NoteQuery::default();
269
270 if let Some(opts) = opts {
271 if let Ok(type_str) = opts.get::<String>("type") {
273 query.note_type = Some(type_str.parse().unwrap_or_default());
274 }
275
276 if let Ok(prefix) = opts.get::<String>("path_prefix") {
278 query.path_prefix = Some(std::path::PathBuf::from(prefix));
279 }
280
281 if let Ok(limit) = opts.get::<i64>("limit") {
283 query.limit = Some(limit as u32);
284 }
285
286 if let Ok(offset) = opts.get::<i64>("offset") {
288 query.offset = Some(offset as u32);
289 }
290 }
291
292 let notes = db
294 .query_notes(&query)
295 .map_err(|e| mlua::Error::runtime(format!("Query error: {}", e)))?;
296
297 let result = lua.create_table()?;
299 for (i, note) in notes.iter().enumerate() {
300 let note_table = lua.create_table()?;
301 note_table.set("path", note.path.to_string_lossy().to_string())?;
302 note_table.set("type", note.note_type.as_str())?;
303 note_table.set("title", note.title.clone())?;
304 note_table.set("modified", note.modified.to_rfc3339())?;
305
306 if let Some(created) = note.created {
307 note_table.set("created", created.to_rfc3339())?;
308 }
309
310 if let Some(fm_json) = ¬e.frontmatter_json
312 && let Ok(fm) = serde_json::from_str::<serde_json::Value>(fm_json)
313 {
314 let fm_yaml = json_to_yaml(&fm);
315 let fm_lua = yaml_to_lua_table(lua, &fm_yaml)?;
316 note_table.set("frontmatter", fm_lua)?;
317 }
318
319 result.set(i + 1, note_table)?;
320 }
321
322 Ok(Value::Table(result))
323 })
324}
325
326#[allow(clippy::collapsible_if)]
331fn create_find_project_fn(lua: &Lua) -> LuaResult<Function> {
332 lua.create_function(|lua, id: String| {
333 let ctx = lua
334 .app_data_ref::<VaultContext>()
335 .ok_or_else(|| mlua::Error::runtime("VaultContext not available"))?;
336
337 let db = match &ctx.index_db {
338 Some(db) => db,
339 None => {
340 return Err(mlua::Error::runtime(
341 "Index database not available. Run 'mdv reindex' first.",
342 ));
343 }
344 };
345
346 let query = NoteQuery {
348 note_type: Some(crate::index::NoteType::Project),
349 ..Default::default()
350 };
351
352 let notes = db
353 .query_notes(&query)
354 .map_err(|e| mlua::Error::runtime(format!("Query error: {}", e)))?;
355
356 for note in notes {
358 if let Some(fm_json) = ¬e.frontmatter_json {
359 if let Ok(fm) = serde_json::from_str::<serde_json::Value>(fm_json) {
360 if fm.get("project-id").and_then(|v| v.as_str()) == Some(id.as_str())
361 {
362 let note_table = lua.create_table()?;
364 note_table
365 .set("path", note.path.to_string_lossy().to_string())?;
366 note_table.set("type", note.note_type.as_str())?;
367 note_table.set("title", note.title.clone())?;
368 note_table.set("modified", note.modified.to_rfc3339())?;
369
370 if let Some(created) = note.created {
371 note_table.set("created", created.to_rfc3339())?;
372 }
373
374 let fm_yaml = json_to_yaml(&fm);
375 let fm_lua = yaml_to_lua_table(lua, &fm_yaml)?;
376 note_table.set("frontmatter", fm_lua)?;
377
378 return Ok(Value::Table(note_table));
379 }
380 }
381 }
382 }
383
384 Ok(Value::Nil)
385 })
386}
387
388fn resolve_note_path(_vault_root: &std::path::Path, path: &str) -> String {
390 if path.ends_with(".md") { path.to_string() } else { format!("{}.md", path) }
392}
393
394fn json_to_yaml(json: &serde_json::Value) -> serde_yaml::Value {
396 match json {
397 serde_json::Value::Null => serde_yaml::Value::Null,
398 serde_json::Value::Bool(b) => serde_yaml::Value::Bool(*b),
399 serde_json::Value::Number(n) => {
400 if let Some(i) = n.as_i64() {
401 serde_yaml::Value::Number(i.into())
402 } else if let Some(f) = n.as_f64() {
403 serde_yaml::Value::Number(serde_yaml::Number::from(f))
404 } else {
405 serde_yaml::Value::Null
406 }
407 }
408 serde_json::Value::String(s) => serde_yaml::Value::String(s.clone()),
409 serde_json::Value::Array(arr) => {
410 serde_yaml::Value::Sequence(arr.iter().map(json_to_yaml).collect())
411 }
412 serde_json::Value::Object(obj) => {
413 let mut map = serde_yaml::Mapping::new();
414 for (k, v) in obj {
415 map.insert(serde_yaml::Value::String(k.clone()), json_to_yaml(v));
416 }
417 serde_yaml::Value::Mapping(map)
418 }
419 }
420}
421
422#[cfg(test)]
423mod tests {
424 use super::*;
425
426 #[test]
427 fn test_resolve_note_path_with_extension() {
428 let vault_root = std::path::Path::new("/vault");
429 let result = resolve_note_path(vault_root, "notes/test.md");
430 assert_eq!(result, "notes/test.md");
431 }
432
433 #[test]
434 fn test_resolve_note_path_without_extension() {
435 let vault_root = std::path::Path::new("/vault");
436 let result = resolve_note_path(vault_root, "notes/test");
437 assert_eq!(result, "notes/test.md");
438 }
439
440 #[test]
441 fn test_json_to_yaml() {
442 let json = serde_json::json!({
443 "string": "value",
444 "number": 42,
445 "bool": true,
446 "array": [1, 2, 3]
447 });
448
449 let yaml = json_to_yaml(&json);
450
451 if let serde_yaml::Value::Mapping(map) = yaml {
452 assert!(map.contains_key(serde_yaml::Value::String("string".into())));
453 assert!(map.contains_key(serde_yaml::Value::String("number".into())));
454 } else {
455 panic!("Expected mapping");
456 }
457 }
458}