Skip to repository content351 lines · 10.4 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T03:27:38.524Z 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
multi_diff_view.rs
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