Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T02:57:43.659Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

picker_preview.rs

372 lines · 13.9 KB · rust
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
Served at tenant.openagents/omega Member data and write actions are omitted.