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