reovim_plugin_pickers/
files.rs

1//! File picker implementation
2
3use std::{future::Future, pin::Pin};
4
5use {
6    ignore::WalkBuilder,
7    reovim_plugin_microscope::{
8        MicroscopeAction, MicroscopeData, MicroscopeItem, Picker, PickerContext, PreviewContent,
9    },
10};
11
12/// Picker for finding files in the project
13pub struct FilesPicker {
14    /// Additional ignore patterns
15    ignore_patterns: Vec<String>,
16}
17
18impl FilesPicker {
19    /// Create a new files picker
20    #[must_use]
21    pub fn new() -> Self {
22        Self {
23            ignore_patterns: vec![
24                ".git".to_string(),
25                "node_modules".to_string(),
26                "target".to_string(),
27                ".cache".to_string(),
28            ],
29        }
30    }
31
32    /// Add an ignore pattern
33    pub fn add_ignore(&mut self, pattern: impl Into<String>) {
34        self.ignore_patterns.push(pattern.into());
35    }
36}
37
38impl Default for FilesPicker {
39    fn default() -> Self {
40        Self::new()
41    }
42}
43
44impl Picker for FilesPicker {
45    fn name(&self) -> &'static str {
46        "files"
47    }
48
49    fn title(&self) -> &'static str {
50        "Find Files"
51    }
52
53    fn prompt(&self) -> &'static str {
54        "Files> "
55    }
56
57    fn fetch(
58        &self,
59        ctx: &PickerContext,
60    ) -> Pin<Box<dyn Future<Output = Vec<MicroscopeItem>> + Send + '_>> {
61        let cwd = ctx.cwd.clone();
62        let max_items = ctx.max_items;
63        let ignore_patterns = self.ignore_patterns.clone();
64
65        Box::pin(async move {
66            let mut items = Vec::new();
67
68            let walker = WalkBuilder::new(&cwd)
69                .hidden(true)
70                .git_ignore(true)
71                .git_global(true)
72                .git_exclude(true)
73                .build();
74
75            for entry in walker.flatten() {
76                if items.len() >= max_items {
77                    break;
78                }
79
80                let path = entry.path();
81
82                // Skip directories
83                if path.is_dir() {
84                    continue;
85                }
86
87                // Skip ignored patterns
88                let path_str = path.to_string_lossy();
89                if ignore_patterns.iter().any(|p| path_str.contains(p)) {
90                    continue;
91                }
92
93                // Get relative path
94                let display = path
95                    .strip_prefix(&cwd)
96                    .unwrap_or(path)
97                    .to_string_lossy()
98                    .to_string();
99
100                // Determine icon based on extension
101                let icon = match path.extension().and_then(|e| e.to_str()) {
102                    Some("rs") => '\u{e7a8}', // Rust icon or fallback
103                    Some("js" | "ts") => '\u{e781}',
104                    Some("py") => '\u{e73c}',
105                    Some("md") => '\u{e73e}',
106                    Some("json" | "toml" | "yaml" | "yml") => '\u{e60b}',
107                    _ => '\u{f15b}', // Generic file
108                };
109
110                items.push(
111                    MicroscopeItem::new(
112                        &display,
113                        &display,
114                        MicroscopeData::FilePath(path.to_path_buf()),
115                        "files",
116                    )
117                    .with_icon(icon),
118                );
119            }
120
121            items
122        })
123    }
124
125    fn on_select(&self, item: &MicroscopeItem) -> MicroscopeAction {
126        match &item.data {
127            MicroscopeData::FilePath(path) => MicroscopeAction::OpenFile(path.clone()),
128            _ => MicroscopeAction::Nothing,
129        }
130    }
131
132    fn preview(
133        &self,
134        item: &MicroscopeItem,
135        ctx: &PickerContext,
136    ) -> Pin<Box<dyn Future<Output = Option<PreviewContent>> + Send + '_>> {
137        let path = match &item.data {
138            MicroscopeData::FilePath(p) => p.clone(),
139            _ => return Box::pin(async { None }),
140        };
141
142        let file_name = path.file_name().and_then(|n| n.to_str()).map(String::from);
143        let syntax_factory = ctx.syntax_factory.clone();
144
145        Box::pin(async move {
146            // Read file content for preview
147            tokio::fs::read_to_string(&path).await.ok().map(|content| {
148                let lines: Vec<String> = content.lines().map(String::from).collect();
149                let syntax_ext = path.extension().and_then(|e| e.to_str()).map(String::from);
150
151                // Compute highlights if factory available
152                let styled_lines = if let Some(factory) = syntax_factory {
153                    let file_path = path.to_string_lossy().to_string();
154                    crate::syntax_helper::compute_styled_lines(
155                        factory.as_ref(),
156                        &file_path,
157                        &content,
158                        &lines,
159                    )
160                } else {
161                    None
162                };
163
164                PreviewContent {
165                    lines,
166                    highlight_line: None,
167                    syntax: syntax_ext,
168                    title: file_name,
169                    styled_lines,
170                }
171            })
172        })
173    }
174}