Skip to repository content372 lines · 13.9 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T04:08:22.783Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
picker_preview.rs
1//! The editor-backed implementation of [`picker::PreviewBackend`].
2
3use std::sync::Arc;
4
5use gpui::{
6 AnyElement, App, AppContext as _, Context, Entity, IntoElement, Pixels, StyledText, Task,
7 Window, px,
8};
9use language::{Bias, Buffer, HighlightedText, HighlightedTextBuilder, ToPoint};
10use picker::{MatchLocation, PreviewBackend, PreviewLayout, PreviewSource, PreviewUpdate};
11use project::{Project, Symbol};
12use rope::Point;
13use settings::Settings;
14use ui::{ActiveTheme, Color, div, prelude::*, v_flex};
15use util::ResultExt as _;
16use util::rel_path::RelPath;
17
18use editor::{Editor, EditorSettings, RowHighlightOptions, display_map::HighlightKey};
19use multi_buffer::{MultiBuffer, MultiBufferOffset};
20
21pub fn editor_preview(
22 project: Entity<Project>,
23 window: &mut Window,
24 cx: &mut App,
25) -> Arc<dyn PreviewBackend> {
26 Arc::new(EditorPreviewHandle(
27 cx.new(|cx| EditorPreview::new(project, window, cx)),
28 ))
29}
30
31struct EditorPreviewHandle(Entity<EditorPreview>);
32
33impl PreviewBackend for EditorPreviewHandle {
34 fn update(&self, update: PreviewUpdate, window: &mut Window, cx: &mut App) {
35 self.0
36 .update(cx, |content, cx| content.update(update, window, cx));
37 }
38
39 fn render(&self, layout: PreviewLayout, cx: &mut App) -> AnyElement {
40 self.0.update(cx, |content, cx| {
41 content.render(layout, cx).into_any_element()
42 })
43 }
44
45 fn adjust_to_new_size(&self, window: &mut Window, cx: &mut App) {
46 self.0
47 .update(cx, |content, cx| content.scroll_to_focus_match(window, cx));
48 }
49
50 fn clear(&self, cx: &mut App) {
51 self.0.update(cx, |content, _| content.clear());
52 }
53}
54
55struct SearchMatchLineHighlight;
56
57struct EditorPreview {
58 project: Entity<Project>,
59 current_path: Option<Arc<RelPath>>,
60 /// When set show a text message instead of a preview
61 message: Option<HighlightedText>,
62 preview_editor: Entity<Editor>,
63 /// Store the load preview task so we have only one at the time
64 pending_update: Task<()>,
65}
66
67impl EditorPreview {
68 fn new(project: Entity<Project>, window: &mut Window, cx: &mut Context<Self>) -> Self {
69 let preview_editor = cx.new(|cx: &mut Context<Editor>| {
70 let capability = language::Capability::ReadWrite; // Later narrowed per buffer
71 let multi_buffer = cx.new(|_| MultiBuffer::without_headers(capability));
72 let mut editor = Editor::for_multibuffer(multi_buffer, None, window, cx);
73 let editor_settings = EditorSettings::get_global(cx).clone();
74
75 // We want editing to happen in the multibuffer not in the modal. The editor acts
76 // as one big <send to multibuffer> button.
77 editor.set_read_only(true);
78 editor.set_input_enabled(false);
79 editor.scroll_manager.set_forbid_vertical_scroll(true);
80 editor.disable_scrollbars_and_minimap(window, cx);
81 editor.disable_inline_diagnostics();
82 editor.disable_diagnostics(cx);
83 editor.disable_expand_excerpt_buttons(cx);
84 editor.disable_mouse_wheel_zoom();
85 editor.set_show_gutter(editor_settings.gutter.line_numbers, cx); // needed for line numbers
86 editor.set_show_line_numbers(editor_settings.gutter.line_numbers, cx);
87 editor.set_show_breakpoints(false, cx);
88 editor.set_show_bookmarks(false, cx);
89 editor.set_show_code_actions(false, cx);
90 editor.set_show_runnables(false, cx);
91 editor.set_show_git_diff_gutter(false, cx);
92 editor.set_show_wrap_guides(false, cx);
93 editor.set_show_indent_guides(false, cx);
94 editor.set_show_cursor_when_unfocused(true, cx);
95 editor.set_soft_wrap_mode(language::language_settings::SoftWrap::None, cx);
96 editor
97 });
98
99 let mut this = Self {
100 project,
101 preview_editor,
102 current_path: None,
103 message: None,
104 pending_update: Task::ready(()),
105 };
106 this.clear(); // picker starts with no results.
107 this
108 }
109
110 fn clear(&mut self) {
111 let mut message = HighlightedTextBuilder::default();
112 message.push_plain("No results to preview");
113 self.message = Some(message.build());
114 }
115
116 fn update(&mut self, update: PreviewUpdate, window: &mut Window, cx: &mut Context<Self>) {
117 let PreviewUpdate {
118 source,
119 match_location: highlight,
120 } = update;
121
122 match source {
123 PreviewSource::Path(abs_path) => {
124 self.update_from_path(abs_path, highlight, window, cx);
125 }
126 PreviewSource::Buffer(buffer) => {
127 self.update_from_buffer(buffer, highlight, window, cx);
128 cx.notify();
129 }
130 PreviewSource::Symbol(symbol) => {
131 self.update_from_symbol(symbol, window, cx);
132 }
133 PreviewSource::Message(message) => {
134 self.message = Some(message);
135 cx.notify();
136 }
137 }
138 }
139
140 fn update_from_path(
141 &mut self,
142 abs_path: std::path::PathBuf,
143 highlight: Option<MatchLocation>,
144 window: &mut Window,
145 cx: &mut Context<Self>,
146 ) {
147 let open_task = self.project.update(cx, |project, cx| {
148 match project.project_path_for_absolute_path(&abs_path, cx) {
149 Some(project_path) => {
150 if let Some(buffer) = project.get_open_buffer(&project_path, cx) {
151 Task::ready(Ok(buffer))
152 } else {
153 project.open_buffer(project_path, cx)
154 }
155 }
156 None => project.open_local_buffer(&abs_path, cx),
157 }
158 });
159
160 self.pending_update = cx.spawn_in(window, async move |this, cx| {
161 let Some(buffer) = open_task.await.log_err() else {
162 return;
163 };
164 this.update_in(cx, |this, window, cx| {
165 this.update_from_buffer(buffer, highlight, window, cx);
166 cx.notify();
167 })
168 .ok();
169 });
170 }
171
172 fn update_from_symbol(&mut self, symbol: Symbol, window: &mut Window, cx: &mut Context<Self>) {
173 let open_task = self.project.update(cx, |project, cx| {
174 project.open_buffer_for_symbol(&symbol, cx)
175 });
176
177 self.pending_update = cx.spawn_in(window, async move |this, cx| {
178 let Some(buffer) = open_task.await.log_err() else {
179 return;
180 };
181 this.update_in(cx, |this, window, cx| {
182 let snapshot = buffer.read(cx).text_snapshot();
183 let start = snapshot.clip_point_utf16(symbol.range.start, Bias::Left);
184 let end = snapshot.clip_point_utf16(symbol.range.end, Bias::Left);
185 let highlight = MatchLocation {
186 anchor_range: snapshot.anchor_before(start)..snapshot.anchor_after(end),
187 range: snapshot.point_utf16_to_offset(start)
188 ..snapshot.point_utf16_to_offset(end),
189 };
190 this.update_from_buffer(buffer, Some(highlight), window, cx);
191 cx.notify();
192 })
193 .ok();
194 });
195 }
196
197 fn update_from_buffer(
198 &mut self,
199 buffer: Entity<Buffer>,
200 highlight: Option<MatchLocation>,
201 window: &mut Window,
202 cx: &mut Context<Self>,
203 ) {
204 self.message = None; // show the preview editor
205 self.current_path = buffer.read(cx).file().map(|file| file.path().clone());
206
207 const MIN_LINE_HEIGHT_PX: Pixels = px(6.0);
208 const MARGIN: u32 = 2; // scrolling can offset things;
209 let max_visible_rows =
210 (window.viewport_size().height / MIN_LINE_HEIGHT_PX).ceil() as u32 + MARGIN;
211
212 self.preview_editor.update(cx, |editor, cx| {
213 let focus_row = highlight
214 .as_ref()
215 .map(|hl| {
216 hl.anchor_range
217 .start
218 .to_point(&buffer.read(cx).text_snapshot())
219 .row
220 })
221 .unwrap_or_default();
222
223 let multi_buffer = editor.buffer().clone();
224 multi_buffer.update(cx, |multi_buffer, cx| {
225 multi_buffer.clear(cx);
226 multi_buffer.set_excerpts_for_buffer(
227 buffer,
228 [Point::new(focus_row, 0)..Point::new(focus_row, 0)],
229 max_visible_rows,
230 cx,
231 );
232 });
233
234 editor.clear_row_highlights::<SearchMatchLineHighlight>();
235 editor.clear_background_highlights(HighlightKey::PickerPreview, cx);
236
237 let Some(highlight) = highlight else {
238 return;
239 };
240
241 let mb = multi_buffer.read(cx).snapshot(cx);
242 let Some(range) = mb
243 .anchor_in_excerpt(highlight.anchor_range.start)
244 .zip(mb.anchor_in_excerpt(highlight.anchor_range.end))
245 .map(|(start, end)| start..end)
246 else {
247 return;
248 };
249
250 editor.highlight_rows::<SearchMatchLineHighlight>(
251 range.clone(),
252 |cx| cx.theme().colors().editor_active_line_background,
253 RowHighlightOptions::default(),
254 cx,
255 );
256
257 editor.highlight_background(
258 HighlightKey::PickerPreview,
259 &[range],
260 |_, theme| theme.colors().search_match_background,
261 cx,
262 );
263 });
264 self.scroll_to_focus_match(window, cx);
265 }
266
267 /// Keep the scroll as far left as possible while showing the match.
268 /// Vertically center the match as much as possible
269 fn scroll_to_focus_match(&mut self, window: &mut Window, cx: &mut Context<Self>) {
270 self.preview_editor.update(cx, |editor, cx| {
271 let display_snapshot = editor.display_snapshot(cx);
272 let buffer_snapshot = display_snapshot.buffer_snapshot();
273 let search_range = buffer_snapshot.anchor_before(MultiBufferOffset(0))
274 ..buffer_snapshot.anchor_after(buffer_snapshot.len());
275
276 // There is at most one highlighted match in the preview, so take the
277 // first background highlight range as the match to focus.
278 let Some((range, _)) = editor
279 .background_highlights_in_range(search_range, &display_snapshot, cx.theme())
280 .into_iter()
281 .next()
282 else {
283 return;
284 };
285
286 let target_row = range.start.row().0 as f64;
287 let centered_y = editor
288 .visible_line_count()
289 .map_or(target_row, |visible_lines| {
290 (target_row - (visible_lines - 1.) / 2.).max(0.)
291 });
292
293 let start_column = range.start.column() as f64;
294 let end_column = range.end.column() as f64;
295 let centered_x = editor.visible_column_count().map_or(0., |visible_columns| {
296 let min_x_for_end = (end_column - visible_columns + 1.).max(0.);
297 min_x_for_end.min(start_column)
298 });
299
300 editor.scroll_manager.set_forbid_vertical_scroll(false);
301 editor.set_scroll_position(gpui::Point::new(centered_x, centered_y), window, cx);
302 editor.scroll_manager.set_forbid_vertical_scroll(true);
303 })
304 }
305
306 fn render(&self, layout: PreviewLayout, cx: &App) -> impl IntoElement {
307 match layout {
308 PreviewLayout::Below => self.render_preview_below(cx).into_any_element(),
309 PreviewLayout::Right => self.render_preview_right(cx).into_any_element(),
310 PreviewLayout::Hidden => gpui::Empty.into_any_element(),
311 }
312 }
313
314 fn render_preview_right(&self, cx: &App) -> impl IntoElement {
315 v_flex()
316 .size_full()
317 .rounded_t_md()
318 .rounded_b_md()
319 .child(self.render_message_or_editor(cx))
320 }
321
322 fn render_preview_below(&self, cx: &App) -> impl IntoElement {
323 v_flex()
324 .size_full()
325 .rounded_b_md()
326 .child(self.render_message_or_editor(cx))
327 }
328
329 fn render_message_or_editor(&self, cx: &App) -> impl IntoElement {
330 if let Some(message) = &self.message {
331 self.render_message(message, cx).into_any_element()
332 } else {
333 div()
334 .flex_1()
335 .overflow_hidden()
336 .child(self.occluded_editor())
337 .into_any_element()
338 }
339 }
340
341 fn render_message(&self, message: &HighlightedText, cx: &App) -> impl IntoElement {
342 // `with_highlights` inherits the container's text style (set below),
343 // while keeping the message's own highlights (e.g. the file path in
344 // the file finder's "Create new file" entry).
345 let content = StyledText::new(message.text.clone())
346 .with_highlights(message.highlights.iter().cloned())
347 .into_any_element();
348 v_flex()
349 .size_full()
350 .items_center()
351 .justify_center()
352 .font_ui(cx)
353 .text_ui(cx)
354 .text_color(Color::Muted.color(cx))
355 .child(content)
356 }
357
358 fn occluded_editor(&self) -> impl IntoElement {
359 div()
360 .relative()
361 .size_full()
362 .child(self.preview_editor.clone())
363 .child(
364 div()
365 .id("picker-preview-editor")
366 .absolute()
367 .inset_0()
368 .occlude(),
369 )
370 }
371}
372