Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T02:29:32.589Z 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

multi_diff_view.rs

351 lines · 10.4 KB · rust
1use crate::file_diff_view::build_buffer_diff;
2use anyhow::Result;
3use buffer_diff::BufferDiff;
4use editor::{
5    Editor, EditorEvent, MultiBuffer, RestoreOnlyUnstagedDiffHunkDelegate,
6    multibuffer_context_lines,
7};
8use gpui::{
9    AnyElement, App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, FocusHandle,
10    Focusable, Font, IntoElement, Render, SharedString, Task, Window,
11};
12use language::{Buffer, Capability, HighlightedText, OffsetRangeExt};
13use multi_buffer::PathKey;
14use project::{Project, ProjectPath};
15use std::{
16    any::{Any, TypeId},
17    path::{Path, PathBuf},
18    sync::Arc,
19};
20use ui::{Color, Icon, IconName, Label, LabelCommon as _};
21use util::paths::PathStyle;
22use util::rel_path::RelPath;
23use workspace::{
24    Item, ItemHandle as _, ItemNavHistory, ToolbarItemLocation, Workspace,
25    item::{ItemEvent, SaveOptions, TabContentParams},
26    searchable::SearchableItemHandle,
27};
28
29pub struct MultiDiffView {
30    editor: Entity<Editor>,
31    file_count: usize,
32}
33
34struct Entry {
35    index: usize,
36    new_path: PathBuf,
37    new_buffer: Entity<Buffer>,
38    diff: Entity<BufferDiff>,
39}
40
41async fn load_entries(
42    diff_pairs: Vec<[String; 2]>,
43    project: &Entity<Project>,
44    cx: &mut AsyncApp,
45) -> Result<(Vec<Entry>, Option<PathBuf>)> {
46    let mut entries = Vec::with_capacity(diff_pairs.len());
47    let mut all_paths = Vec::with_capacity(diff_pairs.len());
48
49    for (ix, pair) in diff_pairs.into_iter().enumerate() {
50        let old_path = PathBuf::from(&pair[0]);
51        let new_path = PathBuf::from(&pair[1]);
52
53        let old_buffer = project
54            .update(cx, |project, cx| project.open_local_buffer(&old_path, cx))
55            .await?;
56        let new_buffer = project
57            .update(cx, |project, cx| project.open_local_buffer(&new_path, cx))
58            .await?;
59
60        let diff = build_buffer_diff(&old_buffer, &new_buffer, cx).await?;
61
62        all_paths.push(new_path.clone());
63        entries.push(Entry {
64            index: ix,
65            new_path,
66            new_buffer: new_buffer.clone(),
67            diff,
68        });
69    }
70
71    let common_root = common_prefix(&all_paths);
72    Ok((entries, common_root))
73}
74
75fn register_entry(
76    multibuffer: &Entity<MultiBuffer>,
77    entry: Entry,
78    common_root: &Option<PathBuf>,
79    context_lines: u32,
80    cx: &mut Context<Workspace>,
81) {
82    let snapshot = entry.new_buffer.read(cx).snapshot();
83    let diff_snapshot = entry.diff.read(cx).snapshot(cx);
84
85    let ranges: Vec<std::ops::Range<language::Point>> = diff_snapshot
86        .hunks(&snapshot)
87        .map(|hunk| hunk.buffer_range.to_point(&snapshot))
88        .collect();
89
90    let display_rel = common_root
91        .as_ref()
92        .and_then(|root| entry.new_path.strip_prefix(root).ok())
93        .map(|rel| {
94            RelPath::new(rel, PathStyle::local())
95                .map(|r| r.into_owned().into())
96                .unwrap_or_else(|_| {
97                    RelPath::new(Path::new(MultiBuffer::DEFAULT_TITLE), PathStyle::Unix)
98                        .unwrap()
99                        .into_owned()
100                        .into()
101                })
102        })
103        .unwrap_or_else(|| {
104            entry
105                .new_path
106                .file_name()
107                .and_then(|n| n.to_str())
108                .and_then(|s| RelPath::new(Path::new(s), PathStyle::Unix).ok())
109                .map(|r| r.into_owned().into())
110                .unwrap_or_else(|| {
111                    RelPath::new(Path::new(MultiBuffer::DEFAULT_TITLE), PathStyle::Unix)
112                        .unwrap()
113                        .into_owned()
114                        .into()
115                })
116        });
117
118    let path_key = PathKey::with_sort_prefix(entry.index as u64, display_rel);
119
120    multibuffer.update(cx, |multibuffer, cx| {
121        multibuffer.set_excerpts_for_path(
122            path_key,
123            entry.new_buffer.clone(),
124            ranges,
125            context_lines,
126            cx,
127        );
128        multibuffer.add_diff(entry.diff.clone(), cx);
129    });
130}
131
132fn common_prefix(paths: &[PathBuf]) -> Option<PathBuf> {
133    let mut iter = paths.iter();
134    let mut prefix = iter.next()?.clone();
135
136    for path in iter {
137        while !path.starts_with(&prefix) {
138            if !prefix.pop() {
139                return Some(PathBuf::new());
140            }
141        }
142    }
143
144    Some(prefix)
145}
146
147impl MultiDiffView {
148    pub fn open(
149        diff_pairs: Vec<[String; 2]>,
150        workspace: &Workspace,
151        window: &mut Window,
152        cx: &mut App,
153    ) -> Task<Result<Entity<Self>>> {
154        let project = workspace.project().clone();
155        let workspace = workspace.weak_handle();
156        let context_lines = multibuffer_context_lines(cx);
157
158        window.spawn(cx, async move |cx| {
159            let (entries, common_root) = load_entries(diff_pairs, &project, cx).await?;
160
161            workspace.update_in(cx, |workspace, window, cx| {
162                let multibuffer = cx.new(|cx| {
163                    let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
164                    multibuffer.set_all_diff_hunks_expanded(cx);
165                    multibuffer
166                });
167
168                let file_count = entries.len();
169                for entry in entries {
170                    register_entry(&multibuffer, entry, &common_root, context_lines, cx);
171                }
172
173                let diff_view = cx.new(|cx| {
174                    Self::new(multibuffer.clone(), project.clone(), file_count, window, cx)
175                });
176
177                let pane = workspace.active_pane();
178                pane.update(cx, |pane, cx| {
179                    pane.add_item(Box::new(diff_view.clone()), true, true, None, window, cx);
180                });
181
182                // Hide the left dock (file explorer) for a cleaner diff view
183                workspace.left_dock().update(cx, |dock, cx| {
184                    dock.set_open(false, window, cx);
185                });
186
187                diff_view
188            })
189        })
190    }
191
192    fn new(
193        multibuffer: Entity<MultiBuffer>,
194        project: Entity<Project>,
195        file_count: usize,
196        window: &mut Window,
197        cx: &mut Context<Self>,
198    ) -> Self {
199        let editor = cx.new(|cx| {
200            let mut editor =
201                Editor::for_multibuffer(multibuffer, Some(project.clone()), window, cx);
202            editor.set_diff_hunk_delegate(Some(Arc::new(RestoreOnlyUnstagedDiffHunkDelegate)), cx);
203            editor.disable_diagnostics(cx);
204            editor.set_expand_all_diff_hunks(cx);
205            editor
206        });
207
208        Self { editor, file_count }
209    }
210
211    fn title(&self) -> SharedString {
212        let suffix = if self.file_count == 1 {
213            "1 file".to_string()
214        } else {
215            format!("{} files", self.file_count)
216        };
217        format!("Diff ({suffix})").into()
218    }
219}
220
221impl EventEmitter<EditorEvent> for MultiDiffView {}
222
223impl Focusable for MultiDiffView {
224    fn focus_handle(&self, cx: &App) -> FocusHandle {
225        self.editor.focus_handle(cx)
226    }
227}
228
229impl Item for MultiDiffView {
230    type Event = EditorEvent;
231
232    fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
233        Some(Icon::new(IconName::Diff).color(Color::Muted))
234    }
235
236    fn tab_content(&self, params: TabContentParams, _window: &Window, _cx: &App) -> AnyElement {
237        Label::new(self.title())
238            .color(if params.selected {
239                Color::Default
240            } else {
241                Color::Muted
242            })
243            .into_any_element()
244    }
245
246    fn tab_tooltip_text(&self, _cx: &App) -> Option<ui::SharedString> {
247        Some(self.title())
248    }
249
250    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
251        self.title()
252    }
253
254    fn to_item_events(event: &EditorEvent, f: &mut dyn FnMut(ItemEvent)) {
255        Editor::to_item_events(event, f)
256    }
257
258    fn telemetry_event_text(&self) -> Option<&'static str> {
259        Some("Diff View Opened")
260    }
261
262    fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
263        self.editor
264            .update(cx, |editor, cx| editor.deactivated(window, cx));
265    }
266
267    fn act_as_type<'a>(
268        &'a self,
269        type_id: TypeId,
270        self_handle: &'a Entity<Self>,
271        _: &'a App,
272    ) -> Option<gpui::AnyEntity> {
273        if type_id == TypeId::of::<Self>() {
274            Some(self_handle.clone().into())
275        } else if type_id == TypeId::of::<Editor>() {
276            Some(self.editor.clone().into())
277        } else {
278            None
279        }
280    }
281
282    fn as_searchable(&self, _: &Entity<Self>, _: &App) -> Option<Box<dyn SearchableItemHandle>> {
283        Some(Box::new(self.editor.clone()))
284    }
285
286    fn active_project_path(&self, cx: &App) -> Option<ProjectPath> {
287        self.editor.read(cx).active_project_path(cx)
288    }
289
290    fn set_nav_history(
291        &mut self,
292        nav_history: ItemNavHistory,
293        _: &mut Window,
294        cx: &mut Context<Self>,
295    ) {
296        self.editor.update(cx, |editor, _| {
297            editor.set_nav_history(Some(nav_history));
298        });
299    }
300
301    fn navigate(
302        &mut self,
303        data: Arc<dyn Any + Send>,
304        window: &mut Window,
305        cx: &mut Context<Self>,
306    ) -> bool {
307        self.editor
308            .update(cx, |editor, cx| editor.navigate(data, window, cx))
309    }
310
311    fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation {
312        ToolbarItemLocation::PrimaryLeft
313    }
314
315    fn breadcrumbs(&self, cx: &App) -> Option<(Vec<HighlightedText>, Option<Font>)> {
316        self.editor.breadcrumbs(cx)
317    }
318
319    fn added_to_workspace(
320        &mut self,
321        workspace: &mut Workspace,
322        window: &mut Window,
323        cx: &mut Context<Self>,
324    ) {
325        self.editor.update(cx, |editor, cx| {
326            editor.added_to_workspace(workspace, window, cx)
327        });
328    }
329
330    fn can_save(&self, cx: &App) -> bool {
331        self.editor.read(cx).can_save(cx)
332    }
333
334    fn save(
335        &mut self,
336        options: SaveOptions,
337        project: Entity<Project>,
338        window: &mut Window,
339        cx: &mut Context<Self>,
340    ) -> gpui::Task<Result<()>> {
341        self.editor
342            .update(cx, |editor, cx| editor.save(options, project, window, cx))
343    }
344}
345
346impl Render for MultiDiffView {
347    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
348        self.editor.clone()
349    }
350}
351
Served at tenant.openagents/omega Member data and write actions are omitted.