reovim_picker_grep/
lib.rs1#![cfg_attr(coverage_nightly, allow(unused_features))]
2#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
3use std::{
9 fs,
10 io::{BufRead, BufReader},
11 path::Path,
12 process::Command,
13 sync::Arc,
14};
15
16use {
17 reovim_driver_picker::{
18 Picker, PickerAction, PickerContext, PickerData, PickerItem, PickerRegistry,
19 PreviewContent, SessionRuntime,
20 },
21 reovim_driver_session::{BufferApi, ChangeTracker, WindowApi},
22 reovim_driver_vfs::VfsInstance,
23 reovim_kernel::api::v1::{Module, ModuleContext, ModuleError, ModuleId, ProbeResult, Version},
24};
25
26const PREVIEW_CONTEXT_LINES: usize = 5;
28
29const MAX_RESULTS: usize = 1000;
31
32pub struct GrepPicker;
41
42impl GrepPicker {
43 #[must_use]
45 pub const fn new() -> Self {
46 Self
47 }
48}
49
50impl Default for GrepPicker {
51 fn default() -> Self {
52 Self::new()
53 }
54}
55
56impl Picker for GrepPicker {
57 fn name(&self) -> &'static str {
58 "grep"
59 }
60
61 fn title(&self) -> &'static str {
62 "Grep"
63 }
64
65 fn prompt(&self) -> &'static str {
66 "rg> "
67 }
68
69 fn items(
70 &self,
71 ctx: &PickerContext,
72 _services: &reovim_kernel::api::v1::ServiceRegistry,
73 ) -> Vec<PickerItem> {
74 if ctx.query.is_empty() {
75 return Vec::new();
76 }
77
78 run_ripgrep(&ctx.query, &ctx.cwd)
79 }
80
81 fn on_select(&self, item: &PickerItem) -> PickerAction {
82 match &item.data {
83 PickerData::GotoLocation { path, line, col } => PickerAction::GotoLocation {
84 path: path.clone(),
85 line: *line,
86 col: *col,
87 },
88 _ => PickerAction::Close,
89 }
90 }
91
92 fn preview(
93 &self,
94 item: &PickerItem,
95 _services: &reovim_kernel::api::v1::ServiceRegistry,
96 ) -> Option<PreviewContent> {
97 let PickerData::GotoLocation { path, line, .. } = &item.data else {
98 return None;
99 };
100
101 let file = fs::File::open(path).ok()?;
102 let reader = BufReader::new(file);
103
104 let start = line.saturating_sub(PREVIEW_CONTEXT_LINES);
105 let end = line + PREVIEW_CONTEXT_LINES;
106
107 let lines: Vec<String> = reader
108 .lines()
109 .skip(start)
110 .take(end - start + 1)
111 .filter_map(Result::ok)
112 .collect();
113
114 if lines.is_empty() {
115 return None;
116 }
117
118 let highlight = line.saturating_sub(start);
120
121 Some(PreviewContent {
122 lines,
123 highlight_line: Some(highlight),
124 file_path: Some(path.clone()),
125 ..Default::default()
126 })
127 }
128
129 fn is_static(&self) -> bool {
130 false
131 }
132
133 #[cfg_attr(coverage_nightly, coverage(off))]
134 fn execute(&self, action: PickerAction, runtime: &mut SessionRuntime<'_>) {
135 if let PickerAction::GotoLocation { path, line, col } = action {
136 open_file(runtime, &path);
137 if let Some(window) = runtime.windows_mut().active_mut() {
139 window.cursor.line = line.saturating_sub(1);
140 window.cursor.column = col.saturating_sub(1);
141 }
142 if let Some(buf_id) = runtime.active_buffer() {
143 runtime.record_cursor_move(buf_id);
144 }
145 }
146 }
147}
148
149fn run_ripgrep(query: &str, cwd: &std::path::Path) -> Vec<PickerItem> {
154 let output = Command::new("rg")
155 .args(["--line-number", "--column", "--no-heading", "--", query])
156 .current_dir(cwd)
157 .output();
158
159 let Ok(output) = output else {
160 return Vec::new();
162 };
163
164 let stdout = String::from_utf8_lossy(&output.stdout);
165 stdout
166 .lines()
167 .take(MAX_RESULTS)
168 .filter_map(|line| parse_rg_line(line, cwd))
169 .collect()
170}
171
172fn parse_rg_line(line: &str, cwd: &std::path::Path) -> Option<PickerItem> {
176 let mut parts = line.splitn(4, ':');
178 let path_str = parts.next()?;
179 let line_str = parts.next()?;
180 let col_str = parts.next()?;
181 let text = parts.next().unwrap_or("");
182
183 let line_num: usize = line_str.parse().ok()?;
184 let col_num: usize = col_str.parse().ok()?;
185 let path = cwd.join(path_str);
186
187 Some(PickerItem {
188 display: format!("{path_str}:{line_num}:{text}"),
189 detail: None,
190 data: PickerData::GotoLocation {
191 path,
192 line: line_num,
193 col: col_num,
194 },
195 icon: None,
196 })
197}
198
199#[cfg_attr(coverage_nightly, coverage(off))]
205fn open_file(runtime: &mut SessionRuntime<'_>, path: &Path) {
206 use reovim_kernel::api::v1::events::kernel::FileOpened;
207
208 let canonical = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
209 let path_str = canonical.to_string_lossy();
210
211 let existing = runtime.kernel().buffers.list().into_iter().find(|&id| {
212 runtime
213 .buffer_file_path(id)
214 .is_some_and(|p| Path::new(&p) == canonical)
215 });
216
217 let is_new = existing.is_none();
218 let buf_id = existing.unwrap_or_else(|| {
219 let content = runtime
220 .kernel()
221 .services
222 .get::<VfsInstance>()
223 .and_then(|vfs| vfs.driver().read_to_string(&canonical).ok())
224 .unwrap_or_default();
225 let id = runtime.create_buffer(Some(&path_str), &content);
226 runtime.set_buffer_modified(id, false);
227 id
228 });
229
230 if let Some(win) = runtime.active_window() {
231 let _ = runtime.set_window_buffer(win, buf_id);
232 }
233 runtime.set_active_buffer(Some(buf_id));
234
235 if is_new {
236 runtime.record_buffer_modified(buf_id);
237 #[allow(clippy::cast_possible_truncation)]
238 let buf_id_raw = buf_id.as_usize() as u64;
239 runtime.kernel().event_bus.emit(FileOpened {
240 buffer_id: buf_id_raw,
241 path: path_str.to_string(),
242 });
243 }
244}
245
246pub struct PickerGrepModule;
254
255impl PickerGrepModule {
256 #[must_use]
258 pub const fn new() -> Self {
259 Self
260 }
261}
262
263impl Default for PickerGrepModule {
264 fn default() -> Self {
265 Self::new()
266 }
267}
268
269impl Module for PickerGrepModule {
270 fn id(&self) -> ModuleId {
271 ModuleId::new("picker-grep")
272 }
273
274 fn name(&self) -> &'static str {
275 "Grep Picker"
276 }
277
278 fn version(&self) -> Version {
279 Version::new(0, 1, 0)
280 }
281
282 fn init(&mut self, ctx: &ModuleContext) -> ProbeResult {
283 let registry = ctx.services.get_or_create::<PickerRegistry>();
284 registry.register(Arc::new(GrepPicker));
285 ProbeResult::Success
286 }
287
288 fn exit(&mut self) -> Result<(), ModuleError> {
289 Ok(())
290 }
291}
292
293#[cfg(feature = "dynamic")]
294reovim_module_macros::declare_module!(PickerGrepModule);
295
296#[cfg(test)]
297#[path = "lib_tests.rs"]
298mod tests;