1use std::{fs, io, path::PathBuf};
2
3use nucleo_matcher::pattern::{AtomKind, CaseMatching, Normalization, Pattern};
4use nucleo_matcher::{Config as NucleoConfig, Matcher};
5use orgize::Org;
6use orgize::ast::PropertyDrawer;
7use orgize::export::{Container, Event, from_fn, from_fn_with_ctx};
8use walkdir::WalkDir;
9
10use crate::{Config, OrgModeError};
11
12#[cfg(test)]
13#[path = "org_mode_tests.rs"]
14mod org_mode_tests;
15
16#[derive(Debug)]
17pub struct OrgMode {
18 config: Config,
19}
20
21#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
22pub struct TreeNode {
23 pub label: String,
24 pub level: usize,
25 #[serde(skip_serializing_if = "Vec::is_empty", default)]
26 pub children: Vec<TreeNode>,
27 #[serde(skip_serializing_if = "Vec::is_empty", default)]
28 pub tags: Vec<String>,
29}
30
31#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
32pub struct SearchResult {
33 pub file_path: String,
34 pub snippet: String,
35 pub score: u32,
36 #[serde(skip_serializing_if = "Vec::is_empty", default)]
37 pub tags: Vec<String>,
38}
39
40impl TreeNode {
41 pub fn new(label: String) -> Self {
42 Self {
43 label,
44 level: 0,
45 children: Vec::new(),
46 tags: Vec::new(),
47 }
48 }
49
50 pub fn new_with_level(label: String, level: usize) -> Self {
51 Self {
52 label,
53 level,
54 children: Vec::new(),
55 tags: Vec::new(),
56 }
57 }
58
59 pub fn to_indented_string(&self, indent: usize) -> String {
60 let mut result = String::new();
61 let prefix = " ".repeat(indent);
62 result.push_str(&format!(
63 "{}{} {}\n",
64 prefix,
65 "*".repeat(self.level),
66 self.label
67 ));
68
69 for child in &self.children {
70 result.push_str(&child.to_indented_string(indent + 1));
71 }
72
73 result
74 }
75}
76
77impl OrgMode {
78 pub fn new(config: Config) -> Result<Self, OrgModeError> {
79 let config = config.validate()?;
80
81 Ok(OrgMode { config })
82 }
83
84 pub fn with_defaults() -> Result<Self, OrgModeError> {
85 Self::new(Config::load()?)
86 }
87
88 pub fn config(&self) -> &Config {
89 &self.config
90 }
91}
92
93impl OrgMode {
94 pub fn list_files(&self) -> Result<Vec<String>, OrgModeError> {
95 WalkDir::new(&self.config.org.org_directory)
96 .into_iter()
97 .filter_map(|entry| match entry {
98 Ok(dir_entry) => {
99 let path = dir_entry.path();
100
101 if path.is_file()
102 && let Some(extension) = path.extension()
103 && extension == "org"
104 && let Ok(relative_path) = path.strip_prefix(&self.config.org.org_directory)
105 && let Some(path_str) = relative_path.to_str()
106 {
107 Some(Ok(path_str.to_string()))
108 } else {
109 None
110 }
111 }
112 Err(e) => Some(Err(OrgModeError::WalkDirError(e))),
113 })
114 .collect::<Result<Vec<String>, OrgModeError>>()
115 }
116
117 pub fn search(
118 &self,
119 query: &str,
120 limit: Option<usize>,
121 snippet_max_size: Option<usize>,
122 ) -> Result<Vec<SearchResult>, OrgModeError> {
123 if query.trim().is_empty() {
124 return Ok(vec![]);
125 }
126
127 let mut matcher = Matcher::new(NucleoConfig::DEFAULT);
128 let pattern = Pattern::new(
129 query,
130 CaseMatching::Ignore,
131 Normalization::Smart,
132 AtomKind::Fuzzy,
133 );
134
135 let files = self.list_files()?;
136 let mut all_results = Vec::new();
137
138 for file in files {
139 let content = match self.read_file(&file) {
140 Ok(content) => content,
141 Err(_) => continue, };
143
144 let matches = pattern.match_list(
145 content.lines().map(|s| s.to_owned()).collect::<Vec<_>>(),
146 &mut matcher,
147 );
148
149 for (snippet, score) in matches {
150 let snippet = Self::snippet(&snippet, snippet_max_size.unwrap_or(100));
151 all_results.push(SearchResult {
152 file_path: file.clone(),
153 snippet,
154 score,
155 tags: self.tags_in_file(&file).unwrap_or_default(),
156 });
157 }
158 }
159
160 all_results.sort_by(|a, b| b.score.cmp(&a.score));
161 all_results.truncate(limit.unwrap_or(all_results.len()));
162
163 Ok(all_results)
164 }
165
166 pub fn search_with_tags(
167 &self,
168 query: &str,
169 tags: Option<&[String]>,
170 limit: Option<usize>,
171 snippet_max_size: Option<usize>,
172 ) -> Result<Vec<SearchResult>, OrgModeError> {
173 self.search(query, None, snippet_max_size)
174 .map(|results| match tags {
175 Some(filter_tags) => results
176 .into_iter()
177 .filter(|r| filter_tags.iter().any(|tag| r.tags.contains(tag)))
178 .collect(),
179 None => results,
180 })
181 .map(|mut all_results| {
182 all_results.truncate(limit.unwrap_or(all_results.len()));
183 all_results
184 })
185 }
186
187 fn tags_in_file(&self, path: &str) -> Result<Vec<String>, OrgModeError> {
188 let content = self.read_file(path)?;
189 let mut tags = Vec::new();
190
191 let mut handler = from_fn(|event| {
192 if let Event::Enter(Container::Headline(h)) = event {
193 tags.extend(h.tags().map(|s| s.to_string()));
194 }
195 });
196
197 Org::parse(&content).traverse(&mut handler);
198
199 Ok(tags)
200 }
201
202 pub fn list_files_by_tags(&self, tags: &[String]) -> Result<Vec<String>, OrgModeError> {
203 self.list_files().map(|files| {
204 files
205 .into_iter()
206 .filter(|path| {
207 let file_tags = self.tags_in_file(path).unwrap_or_default();
208 tags.iter().any(|tag| file_tags.contains(tag))
209 })
210 .collect::<Vec<String>>()
211 })
212 }
213
214 pub fn read_file(&self, path: &str) -> Result<String, OrgModeError> {
215 let org_dir = PathBuf::from(&self.config.org.org_directory);
216 let full_path = org_dir.join(path);
217
218 if !full_path.exists() {
219 return Err(OrgModeError::IoError(io::Error::new(
220 io::ErrorKind::NotFound,
221 format!("File not found: {}", path),
222 )));
223 }
224
225 if !full_path.is_file() {
226 return Err(OrgModeError::IoError(io::Error::new(
227 io::ErrorKind::InvalidInput,
228 format!("Path is not a file: {}", path),
229 )));
230 }
231
232 fs::read_to_string(full_path).map_err(OrgModeError::IoError)
233 }
234
235 pub fn get_outline(&self, path: &str) -> Result<TreeNode, OrgModeError> {
236 let content = self.read_file(path)?;
237
238 let mut root = TreeNode::new("Document".into());
239 let mut stack: Vec<TreeNode> = Vec::new();
240
241 let mut handler = from_fn(|event| {
242 if let Event::Enter(Container::Headline(h)) = event {
243 let level = h.level();
244 let label = h.title_raw();
245 let tags = h.tags().map(|s| s.to_string()).collect();
246 let node = TreeNode {
247 label,
248 level,
249 tags,
250 children: Vec::new(),
251 };
252
253 while let Some(n) = stack.last() {
254 if n.level < level {
255 break;
256 }
257 let finished_node = stack.pop().unwrap();
258 if let Some(parent) = stack.last_mut() {
259 parent.children.push(finished_node);
260 } else {
261 root.children.push(finished_node);
262 }
263 }
264
265 stack.push(node);
266 }
267 });
268
269 Org::parse(&content).traverse(&mut handler);
270
271 while let Some(node) = stack.pop() {
272 if let Some(parent) = stack.last_mut() {
273 parent.children.push(node);
274 } else {
275 root.children.push(node);
276 }
277 }
278
279 Ok(root)
280 }
281
282 pub fn get_heading(&self, path: &str, heading: &str) -> Result<String, OrgModeError> {
283 let content = self.read_file(path)?;
284
285 let heading_path: Vec<&str> = heading.split('/').collect();
286 let mut current_level = 0;
287 let mut found = None;
288
289 let mut handler = from_fn_with_ctx(|event, ctx| {
290 if let Event::Enter(Container::Headline(h)) = event {
291 let title = h.title_raw();
292 let level = h.level();
293
294 if let Some(part) = heading_path.get(current_level) {
295 if title == *part {
296 if level == heading_path.len() {
297 found = Some(h);
298 ctx.stop();
299 }
300 current_level += 1;
301 }
302 } else {
303 ctx.stop()
304 }
305 }
306 });
307
308 Org::parse(&content).traverse(&mut handler);
309
310 found
311 .ok_or_else(|| OrgModeError::InvalidHeadingPath(heading.into()))
312 .map(|h| h.raw())
313 }
314
315 pub fn get_element_by_id(&self, id: &str) -> Result<String, OrgModeError> {
316 let files = self.list_files()?;
317
318 let found = files.iter().find_map(|path| {
319 self.read_file(path)
320 .map(|content| self.search_id(content, id))
321 .unwrap_or_default()
322 });
323
324 found.ok_or_else(|| OrgModeError::InvalidElementId(id.into()))
325 }
326
327 fn search_id(&self, content: String, id: &str) -> Option<String> {
328 let mut found = None;
329 let has_id_property = |properties: Option<PropertyDrawer>| {
330 properties
331 .and_then(|props| {
332 props
333 .to_hash_map()
334 .into_iter()
335 .find(|(k, v)| k.to_lowercase() == "id" && v == id)
336 })
337 .is_some()
338 };
339 let mut handler = from_fn_with_ctx(|event, ctx| {
340 if let Event::Enter(Container::Headline(ref h)) = event
341 && has_id_property(h.properties())
342 {
343 found = Some(h.raw());
344 ctx.stop();
345 } else if let Event::Enter(Container::Document(ref d)) = event
346 && has_id_property(d.properties())
347 {
348 found = Some(d.raw());
349 ctx.stop();
350 }
351 });
352
353 Org::parse(&content).traverse(&mut handler);
354
355 found
356 }
357
358 fn snippet(s: &str, max: usize) -> String {
359 if s.chars().count() > max {
360 s.chars().take(max).collect::<String>() + "..."
361 } else {
362 s.to_string()
363 }
364 }
365}