1use crate::{ApplyAllDiffHunks, Editor, EditorEvent, SemanticsProvider};
2use buffer_diff::BufferDiff;
3use collections::HashSet;
4use futures::{channel::mpsc, future::join_all};
5use gpui::{App, Entity, EventEmitter, Focusable, Render, Subscription, Task};
6use language::{Buffer, BufferEvent, Capability};
7use multi_buffer::{ExcerptRange, MultiBuffer};
8use project::Project;
9use smol::stream::StreamExt;
10use std::{any::TypeId, ops::Range, rc::Rc, time::Duration};
11use text::ToOffset;
12use ui::{prelude::*, ButtonLike, KeyBinding};
13use workspace::{
14 searchable::SearchableItemHandle, Item, ItemHandle as _, ToolbarItemEvent, ToolbarItemLocation,
15 ToolbarItemView, Workspace,
16};
17
18pub struct ProposedChangesEditor {
19 editor: Entity<Editor>,
20 multibuffer: Entity<MultiBuffer>,
21 title: SharedString,
22 buffer_entries: Vec<BufferEntry>,
23 _recalculate_diffs_task: Task<Option<()>>,
24 recalculate_diffs_tx: mpsc::UnboundedSender<RecalculateDiff>,
25}
26
27pub struct ProposedChangeLocation<T> {
28 pub buffer: Entity<Buffer>,
29 pub ranges: Vec<Range<T>>,
30}
31
32struct BufferEntry {
33 base: Entity<Buffer>,
34 branch: Entity<Buffer>,
35 _subscription: Subscription,
36}
37
38pub struct ProposedChangesEditorToolbar {
39 current_editor: Option<Entity<ProposedChangesEditor>>,
40}
41
42struct RecalculateDiff {
43 buffer: Entity<Buffer>,
44 debounce: bool,
45}
46
47/// A provider of code semantics for branch buffers.
48///
49/// Requests in edited regions will return nothing, but requests in unchanged
50/// regions will be translated into the base buffer's coordinates.
51struct BranchBufferSemanticsProvider(Rc<dyn SemanticsProvider>);
52
53impl ProposedChangesEditor {
54 pub fn new<T: ToOffset>(
55 title: impl Into<SharedString>,
56 locations: Vec<ProposedChangeLocation<T>>,
57 project: Option<Entity<Project>>,
58 window: &mut Window,
59 cx: &mut Context<Self>,
60 ) -> Self {
61 let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
62 let (recalculate_diffs_tx, mut recalculate_diffs_rx) = mpsc::unbounded();
63 let mut this = Self {
64 editor: cx.new(|cx| {
65 let mut editor =
66 Editor::for_multibuffer(multibuffer.clone(), project, true, window, cx);
67 editor.set_expand_all_diff_hunks(cx);
68 editor.set_completion_provider(None);
69 editor.clear_code_action_providers();
70 editor.set_semantics_provider(
71 editor
72 .semantics_provider()
73 .map(|provider| Rc::new(BranchBufferSemanticsProvider(provider)) as _),
74 );
75 editor
76 }),
77 multibuffer,
78 title: title.into(),
79 buffer_entries: Vec::new(),
80 recalculate_diffs_tx,
81 _recalculate_diffs_task: cx.spawn_in(window, |this, mut cx| async move {
82 let mut buffers_to_diff = HashSet::default();
83 while let Some(mut recalculate_diff) = recalculate_diffs_rx.next().await {
84 buffers_to_diff.insert(recalculate_diff.buffer);
85
86 while recalculate_diff.debounce {
87 cx.background_executor()
88 .timer(Duration::from_millis(50))
89 .await;
90 let mut had_further_changes = false;
91 while let Ok(next_recalculate_diff) = recalculate_diffs_rx.try_next() {
92 let next_recalculate_diff = next_recalculate_diff?;
93 recalculate_diff.debounce &= next_recalculate_diff.debounce;
94 buffers_to_diff.insert(next_recalculate_diff.buffer);
95 had_further_changes = true;
96 }
97 if !had_further_changes {
98 break;
99 }
100 }
101
102 let recalculate_diff_futures = this
103 .update(&mut cx, |this, cx| {
104 buffers_to_diff
105 .drain()
106 .filter_map(|buffer| {
107 let buffer = buffer.read(cx);
108 let base_buffer = buffer.base_buffer()?;
109 let buffer = buffer.text_snapshot();
110 let diff =
111 this.multibuffer.read(cx).diff_for(buffer.remote_id())?;
112 Some(diff.update(cx, |diff, cx| {
113 diff.set_base_text(base_buffer.clone(), buffer, cx)
114 }))
115 })
116 .collect::<Vec<_>>()
117 })
118 .ok()?;
119
120 join_all(recalculate_diff_futures).await;
121 }
122 None
123 }),
124 };
125 this.reset_locations(locations, window, cx);
126 this
127 }
128
129 pub fn branch_buffer_for_base(&self, base_buffer: &Entity<Buffer>) -> Option<Entity<Buffer>> {
130 self.buffer_entries.iter().find_map(|entry| {
131 if &entry.base == base_buffer {
132 Some(entry.branch.clone())
133 } else {
134 None
135 }
136 })
137 }
138
139 pub fn set_title(&mut self, title: SharedString, cx: &mut Context<Self>) {
140 self.title = title;
141 cx.notify();
142 }
143
144 pub fn reset_locations<T: ToOffset>(
145 &mut self,
146 locations: Vec<ProposedChangeLocation<T>>,
147 window: &mut Window,
148 cx: &mut Context<Self>,
149 ) {
150 // Undo all branch changes
151 for entry in &self.buffer_entries {
152 let base_version = entry.base.read(cx).version();
153 entry.branch.update(cx, |buffer, cx| {
154 let undo_counts = buffer
155 .operations()
156 .iter()
157 .filter_map(|(timestamp, _)| {
158 if !base_version.observed(*timestamp) {
159 Some((*timestamp, u32::MAX))
160 } else {
161 None
162 }
163 })
164 .collect();
165 buffer.undo_operations(undo_counts, cx);
166 });
167 }
168
169 self.multibuffer.update(cx, |multibuffer, cx| {
170 multibuffer.clear(cx);
171 });
172
173 let mut buffer_entries = Vec::new();
174 let mut new_diffs = Vec::new();
175 for location in locations {
176 let branch_buffer;
177 if let Some(ix) = self
178 .buffer_entries
179 .iter()
180 .position(|entry| entry.base == location.buffer)
181 {
182 let entry = self.buffer_entries.remove(ix);
183 branch_buffer = entry.branch.clone();
184 buffer_entries.push(entry);
185 } else {
186 branch_buffer = location.buffer.update(cx, |buffer, cx| buffer.branch(cx));
187 new_diffs.push(cx.new(|cx| {
188 let mut diff = BufferDiff::new(&branch_buffer.read(cx).snapshot(), cx);
189 let _ = diff.set_base_text(
190 location.buffer.clone(),
191 branch_buffer.read(cx).text_snapshot(),
192 cx,
193 );
194 diff
195 }));
196 buffer_entries.push(BufferEntry {
197 branch: branch_buffer.clone(),
198 base: location.buffer.clone(),
199 _subscription: cx.subscribe(&branch_buffer, Self::on_buffer_event),
200 });
201 }
202
203 self.multibuffer.update(cx, |multibuffer, cx| {
204 multibuffer.push_excerpts(
205 branch_buffer,
206 location.ranges.into_iter().map(|range| ExcerptRange {
207 context: range,
208 primary: None,
209 }),
210 cx,
211 );
212 });
213 }
214
215 self.buffer_entries = buffer_entries;
216 self.editor.update(cx, |editor, cx| {
217 editor.change_selections(None, window, cx, |selections| selections.refresh());
218 editor.buffer.update(cx, |buffer, cx| {
219 for diff in new_diffs {
220 buffer.add_diff(diff, cx)
221 }
222 })
223 });
224 }
225
226 pub fn recalculate_all_buffer_diffs(&self) {
227 for (ix, entry) in self.buffer_entries.iter().enumerate().rev() {
228 self.recalculate_diffs_tx
229 .unbounded_send(RecalculateDiff {
230 buffer: entry.branch.clone(),
231 debounce: ix > 0,
232 })
233 .ok();
234 }
235 }
236
237 fn on_buffer_event(
238 &mut self,
239 buffer: Entity<Buffer>,
240 event: &BufferEvent,
241 _cx: &mut Context<Self>,
242 ) {
243 match event {
244 BufferEvent::Operation { .. } => {
245 self.recalculate_diffs_tx
246 .unbounded_send(RecalculateDiff {
247 buffer,
248 debounce: true,
249 })
250 .ok();
251 }
252 // BufferEvent::DiffBaseChanged => {
253 // self.recalculate_diffs_tx
254 // .unbounded_send(RecalculateDiff {
255 // buffer,
256 // debounce: false,
257 // })
258 // .ok();
259 // }
260 _ => (),
261 }
262 }
263}
264
265impl Render for ProposedChangesEditor {
266 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
267 div()
268 .size_full()
269 .key_context("ProposedChangesEditor")
270 .child(self.editor.clone())
271 }
272}
273
274impl Focusable for ProposedChangesEditor {
275 fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
276 self.editor.focus_handle(cx)
277 }
278}
279
280impl EventEmitter<EditorEvent> for ProposedChangesEditor {}
281
282impl Item for ProposedChangesEditor {
283 type Event = EditorEvent;
284
285 fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
286 Some(Icon::new(IconName::Diff))
287 }
288
289 fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option<SharedString> {
290 Some(self.title.clone())
291 }
292
293 fn as_searchable(&self, _: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
294 Some(Box::new(self.editor.clone()))
295 }
296
297 fn act_as_type<'a>(
298 &'a self,
299 type_id: TypeId,
300 self_handle: &'a Entity<Self>,
301 _: &'a App,
302 ) -> Option<gpui::AnyView> {
303 if type_id == TypeId::of::<Self>() {
304 Some(self_handle.to_any())
305 } else if type_id == TypeId::of::<Editor>() {
306 Some(self.editor.to_any())
307 } else {
308 None
309 }
310 }
311
312 fn added_to_workspace(
313 &mut self,
314 workspace: &mut Workspace,
315 window: &mut Window,
316 cx: &mut Context<Self>,
317 ) {
318 self.editor.update(cx, |editor, cx| {
319 Item::added_to_workspace(editor, workspace, window, cx)
320 });
321 }
322
323 fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
324 self.editor
325 .update(cx, |editor, cx| editor.deactivated(window, cx));
326 }
327
328 fn navigate(
329 &mut self,
330 data: Box<dyn std::any::Any>,
331 window: &mut Window,
332 cx: &mut Context<Self>,
333 ) -> bool {
334 self.editor
335 .update(cx, |editor, cx| Item::navigate(editor, data, window, cx))
336 }
337
338 fn set_nav_history(
339 &mut self,
340 nav_history: workspace::ItemNavHistory,
341 window: &mut Window,
342 cx: &mut Context<Self>,
343 ) {
344 self.editor.update(cx, |editor, cx| {
345 Item::set_nav_history(editor, nav_history, window, cx)
346 });
347 }
348
349 fn can_save(&self, cx: &App) -> bool {
350 self.editor.read(cx).can_save(cx)
351 }
352
353 fn save(
354 &mut self,
355 format: bool,
356 project: Entity<Project>,
357 window: &mut Window,
358 cx: &mut Context<Self>,
359 ) -> Task<gpui::Result<()>> {
360 self.editor.update(cx, |editor, cx| {
361 Item::save(editor, format, project, window, cx)
362 })
363 }
364}
365
366impl ProposedChangesEditorToolbar {
367 pub fn new() -> Self {
368 Self {
369 current_editor: None,
370 }
371 }
372
373 fn get_toolbar_item_location(&self) -> ToolbarItemLocation {
374 if self.current_editor.is_some() {
375 ToolbarItemLocation::PrimaryRight
376 } else {
377 ToolbarItemLocation::Hidden
378 }
379 }
380}
381
382impl Render for ProposedChangesEditorToolbar {
383 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
384 let button_like = ButtonLike::new("apply-changes").child(Label::new("Apply All"));
385
386 match &self.current_editor {
387 Some(editor) => {
388 let focus_handle = editor.focus_handle(cx);
389 let keybinding =
390 KeyBinding::for_action_in(&ApplyAllDiffHunks, &focus_handle, window, cx)
391 .map(|binding| binding.into_any_element());
392
393 button_like.children(keybinding).on_click({
394 move |_event, window, cx| {
395 focus_handle.dispatch_action(&ApplyAllDiffHunks, window, cx)
396 }
397 })
398 }
399 None => button_like.disabled(true),
400 }
401 }
402}
403
404impl EventEmitter<ToolbarItemEvent> for ProposedChangesEditorToolbar {}
405
406impl ToolbarItemView for ProposedChangesEditorToolbar {
407 fn set_active_pane_item(
408 &mut self,
409 active_pane_item: Option<&dyn workspace::ItemHandle>,
410 _window: &mut Window,
411 _cx: &mut Context<Self>,
412 ) -> workspace::ToolbarItemLocation {
413 self.current_editor =
414 active_pane_item.and_then(|item| item.downcast::<ProposedChangesEditor>());
415 self.get_toolbar_item_location()
416 }
417}
418
419impl BranchBufferSemanticsProvider {
420 fn to_base(
421 &self,
422 buffer: &Entity<Buffer>,
423 positions: &[text::Anchor],
424 cx: &App,
425 ) -> Option<Entity<Buffer>> {
426 let base_buffer = buffer.read(cx).base_buffer()?;
427 let version = base_buffer.read(cx).version();
428 if positions
429 .iter()
430 .any(|position| !version.observed(position.timestamp))
431 {
432 return None;
433 }
434 Some(base_buffer)
435 }
436}
437
438impl SemanticsProvider for BranchBufferSemanticsProvider {
439 fn hover(
440 &self,
441 buffer: &Entity<Buffer>,
442 position: text::Anchor,
443 cx: &mut App,
444 ) -> Option<Task<Vec<project::Hover>>> {
445 let buffer = self.to_base(buffer, &[position], cx)?;
446 self.0.hover(&buffer, position, cx)
447 }
448
449 fn inlay_hints(
450 &self,
451 buffer: Entity<Buffer>,
452 range: Range<text::Anchor>,
453 cx: &mut App,
454 ) -> Option<Task<anyhow::Result<Vec<project::InlayHint>>>> {
455 let buffer = self.to_base(&buffer, &[range.start, range.end], cx)?;
456 self.0.inlay_hints(buffer, range, cx)
457 }
458
459 fn resolve_inlay_hint(
460 &self,
461 hint: project::InlayHint,
462 buffer: Entity<Buffer>,
463 server_id: lsp::LanguageServerId,
464 cx: &mut App,
465 ) -> Option<Task<anyhow::Result<project::InlayHint>>> {
466 let buffer = self.to_base(&buffer, &[], cx)?;
467 self.0.resolve_inlay_hint(hint, buffer, server_id, cx)
468 }
469
470 fn supports_inlay_hints(&self, buffer: &Entity<Buffer>, cx: &mut App) -> bool {
471 if let Some(buffer) = self.to_base(&buffer, &[], cx) {
472 self.0.supports_inlay_hints(&buffer, cx)
473 } else {
474 false
475 }
476 }
477
478 fn document_highlights(
479 &self,
480 buffer: &Entity<Buffer>,
481 position: text::Anchor,
482 cx: &mut App,
483 ) -> Option<Task<gpui::Result<Vec<project::DocumentHighlight>>>> {
484 let buffer = self.to_base(&buffer, &[position], cx)?;
485 self.0.document_highlights(&buffer, position, cx)
486 }
487
488 fn definitions(
489 &self,
490 buffer: &Entity<Buffer>,
491 position: text::Anchor,
492 kind: crate::GotoDefinitionKind,
493 cx: &mut App,
494 ) -> Option<Task<gpui::Result<Vec<project::LocationLink>>>> {
495 let buffer = self.to_base(&buffer, &[position], cx)?;
496 self.0.definitions(&buffer, position, kind, cx)
497 }
498
499 fn range_for_rename(
500 &self,
501 _: &Entity<Buffer>,
502 _: text::Anchor,
503 _: &mut App,
504 ) -> Option<Task<gpui::Result<Option<Range<text::Anchor>>>>> {
505 None
506 }
507
508 fn perform_rename(
509 &self,
510 _: &Entity<Buffer>,
511 _: text::Anchor,
512 _: String,
513 _: &mut App,
514 ) -> Option<Task<gpui::Result<project::ProjectTransaction>>> {
515 None
516 }
517}