1use crate::{ApplyAllDiffHunks, Editor, EditorEvent, SelectionEffects, SemanticsProvider};
2use buffer_diff::BufferDiff;
3use collections::{HashMap, HashSet};
4use futures::{channel::mpsc, future::join_all};
5use gpui::{App, Entity, EventEmitter, Focusable, Render, Subscription, Task};
6use language::{Buffer, BufferEvent, BufferRow, Capability};
7use multi_buffer::{ExcerptRange, MultiBuffer};
8use project::{InvalidationStrategy, Project, lsp_store::CacheInlayHints};
9use smol::stream::StreamExt;
10use std::{any::TypeId, ops::Range, rc::Rc, time::Duration};
11use text::{BufferId, ToOffset};
12use ui::{ButtonLike, KeyBinding, prelude::*};
13use workspace::{
14 Item, ItemHandle as _, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
15 item::SaveOptions, searchable::SearchableItemHandle,
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: Clone + 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 = Editor::for_multibuffer(multibuffer.clone(), project, 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, async move |this, cx| {
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(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 diff =
110 this.multibuffer.read(cx).diff_for(buffer.remote_id())?;
111 Some(diff.update(cx, |diff, cx| {
112 diff.set_base_text_buffer(base_buffer.clone(), buffer, cx)
113 }))
114 })
115 .collect::<Vec<_>>()
116 })
117 .ok()?;
118
119 join_all(recalculate_diff_futures).await;
120 }
121 None
122 }),
123 };
124 this.reset_locations(locations, window, cx);
125 this
126 }
127
128 pub fn branch_buffer_for_base(&self, base_buffer: &Entity<Buffer>) -> Option<Entity<Buffer>> {
129 self.buffer_entries.iter().find_map(|entry| {
130 if &entry.base == base_buffer {
131 Some(entry.branch.clone())
132 } else {
133 None
134 }
135 })
136 }
137
138 pub fn set_title(&mut self, title: SharedString, cx: &mut Context<Self>) {
139 self.title = title;
140 cx.notify();
141 }
142
143 pub fn reset_locations<T: Clone + ToOffset>(
144 &mut self,
145 locations: Vec<ProposedChangeLocation<T>>,
146 window: &mut Window,
147 cx: &mut Context<Self>,
148 ) {
149 // Undo all branch changes
150 for entry in &self.buffer_entries {
151 let base_version = entry.base.read(cx).version();
152 entry.branch.update(cx, |buffer, cx| {
153 let undo_counts = buffer
154 .operations()
155 .iter()
156 .filter_map(|(timestamp, _)| {
157 if !base_version.observed(*timestamp) {
158 Some((*timestamp, u32::MAX))
159 } else {
160 None
161 }
162 })
163 .collect();
164 buffer.undo_operations(undo_counts, cx);
165 });
166 }
167
168 self.multibuffer.update(cx, |multibuffer, cx| {
169 multibuffer.clear(cx);
170 });
171
172 let mut buffer_entries = Vec::new();
173 let mut new_diffs = Vec::new();
174 for location in locations {
175 let branch_buffer;
176 if let Some(ix) = self
177 .buffer_entries
178 .iter()
179 .position(|entry| entry.base == location.buffer)
180 {
181 let entry = self.buffer_entries.remove(ix);
182 branch_buffer = entry.branch.clone();
183 buffer_entries.push(entry);
184 } else {
185 branch_buffer = location.buffer.update(cx, |buffer, cx| buffer.branch(cx));
186 new_diffs.push(cx.new(|cx| {
187 let mut diff = BufferDiff::new(&branch_buffer.read(cx).snapshot(), cx);
188 let _ = diff.set_base_text_buffer(
189 location.buffer.clone(),
190 branch_buffer.read(cx).text_snapshot(),
191 cx,
192 );
193 diff
194 }));
195 buffer_entries.push(BufferEntry {
196 branch: branch_buffer.clone(),
197 base: location.buffer.clone(),
198 _subscription: cx.subscribe(&branch_buffer, Self::on_buffer_event),
199 });
200 }
201
202 self.multibuffer.update(cx, |multibuffer, cx| {
203 multibuffer.push_excerpts(
204 branch_buffer,
205 location
206 .ranges
207 .into_iter()
208 .map(|range| ExcerptRange::new(range)),
209 cx,
210 );
211 });
212 }
213
214 self.buffer_entries = buffer_entries;
215 self.editor.update(cx, |editor, cx| {
216 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
217 selections.refresh()
218 });
219 editor.buffer.update(cx, |buffer, cx| {
220 for diff in new_diffs {
221 buffer.add_diff(diff, cx)
222 }
223 })
224 });
225 }
226
227 pub fn recalculate_all_buffer_diffs(&self) {
228 for (ix, entry) in self.buffer_entries.iter().enumerate().rev() {
229 self.recalculate_diffs_tx
230 .unbounded_send(RecalculateDiff {
231 buffer: entry.branch.clone(),
232 debounce: ix > 0,
233 })
234 .ok();
235 }
236 }
237
238 fn on_buffer_event(
239 &mut self,
240 buffer: Entity<Buffer>,
241 event: &BufferEvent,
242 _cx: &mut Context<Self>,
243 ) {
244 if let BufferEvent::Operation { .. } = event {
245 self.recalculate_diffs_tx
246 .unbounded_send(RecalculateDiff {
247 buffer,
248 debounce: true,
249 })
250 .ok();
251 }
252 }
253}
254
255impl Render for ProposedChangesEditor {
256 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
257 div()
258 .size_full()
259 .key_context("ProposedChangesEditor")
260 .child(self.editor.clone())
261 }
262}
263
264impl Focusable for ProposedChangesEditor {
265 fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
266 self.editor.focus_handle(cx)
267 }
268}
269
270impl EventEmitter<EditorEvent> for ProposedChangesEditor {}
271
272impl Item for ProposedChangesEditor {
273 type Event = EditorEvent;
274
275 fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
276 Some(Icon::new(IconName::Diff))
277 }
278
279 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
280 self.title.clone()
281 }
282
283 fn as_searchable(&self, _: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
284 Some(Box::new(self.editor.clone()))
285 }
286
287 fn act_as_type<'a>(
288 &'a self,
289 type_id: TypeId,
290 self_handle: &'a Entity<Self>,
291 _: &'a App,
292 ) -> Option<gpui::AnyView> {
293 if type_id == TypeId::of::<Self>() {
294 Some(self_handle.to_any())
295 } else if type_id == TypeId::of::<Editor>() {
296 Some(self.editor.to_any())
297 } else {
298 None
299 }
300 }
301
302 fn added_to_workspace(
303 &mut self,
304 workspace: &mut Workspace,
305 window: &mut Window,
306 cx: &mut Context<Self>,
307 ) {
308 self.editor.update(cx, |editor, cx| {
309 Item::added_to_workspace(editor, workspace, window, cx)
310 });
311 }
312
313 fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
314 self.editor
315 .update(cx, |editor, cx| editor.deactivated(window, cx));
316 }
317
318 fn navigate(
319 &mut self,
320 data: Box<dyn std::any::Any>,
321 window: &mut Window,
322 cx: &mut Context<Self>,
323 ) -> bool {
324 self.editor
325 .update(cx, |editor, cx| Item::navigate(editor, data, window, cx))
326 }
327
328 fn set_nav_history(
329 &mut self,
330 nav_history: workspace::ItemNavHistory,
331 window: &mut Window,
332 cx: &mut Context<Self>,
333 ) {
334 self.editor.update(cx, |editor, cx| {
335 Item::set_nav_history(editor, nav_history, window, cx)
336 });
337 }
338
339 fn can_save(&self, cx: &App) -> bool {
340 self.editor.read(cx).can_save(cx)
341 }
342
343 fn save(
344 &mut self,
345 options: SaveOptions,
346 project: Entity<Project>,
347 window: &mut Window,
348 cx: &mut Context<Self>,
349 ) -> Task<anyhow::Result<()>> {
350 self.editor.update(cx, |editor, cx| {
351 Item::save(editor, options, project, window, cx)
352 })
353 }
354}
355
356impl ProposedChangesEditorToolbar {
357 pub fn new() -> Self {
358 Self {
359 current_editor: None,
360 }
361 }
362
363 fn get_toolbar_item_location(&self) -> ToolbarItemLocation {
364 if self.current_editor.is_some() {
365 ToolbarItemLocation::PrimaryRight
366 } else {
367 ToolbarItemLocation::Hidden
368 }
369 }
370}
371
372impl Render for ProposedChangesEditorToolbar {
373 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
374 let button_like = ButtonLike::new("apply-changes").child(Label::new("Apply All"));
375
376 match &self.current_editor {
377 Some(editor) => {
378 let focus_handle = editor.focus_handle(cx);
379 let keybinding = KeyBinding::for_action_in(&ApplyAllDiffHunks, &focus_handle, cx);
380
381 button_like.child(keybinding).on_click({
382 move |_event, window, cx| {
383 focus_handle.dispatch_action(&ApplyAllDiffHunks, window, cx)
384 }
385 })
386 }
387 None => button_like.disabled(true),
388 }
389 }
390}
391
392impl EventEmitter<ToolbarItemEvent> for ProposedChangesEditorToolbar {}
393
394impl ToolbarItemView for ProposedChangesEditorToolbar {
395 fn set_active_pane_item(
396 &mut self,
397 active_pane_item: Option<&dyn workspace::ItemHandle>,
398 _window: &mut Window,
399 _cx: &mut Context<Self>,
400 ) -> workspace::ToolbarItemLocation {
401 self.current_editor =
402 active_pane_item.and_then(|item| item.downcast::<ProposedChangesEditor>());
403 self.get_toolbar_item_location()
404 }
405}
406
407impl BranchBufferSemanticsProvider {
408 fn to_base(
409 &self,
410 buffer: &Entity<Buffer>,
411 positions: &[text::Anchor],
412 cx: &App,
413 ) -> Option<Entity<Buffer>> {
414 let base_buffer = buffer.read(cx).base_buffer()?;
415 let version = base_buffer.read(cx).version();
416 if positions
417 .iter()
418 .any(|position| !version.observed(position.timestamp))
419 {
420 return None;
421 }
422 Some(base_buffer)
423 }
424}
425
426impl SemanticsProvider for BranchBufferSemanticsProvider {
427 fn hover(
428 &self,
429 buffer: &Entity<Buffer>,
430 position: text::Anchor,
431 cx: &mut App,
432 ) -> Option<Task<Option<Vec<project::Hover>>>> {
433 let buffer = self.to_base(buffer, &[position], cx)?;
434 self.0.hover(&buffer, position, cx)
435 }
436
437 fn applicable_inlay_chunks(
438 &self,
439 buffer: &Entity<Buffer>,
440 ranges: &[Range<text::Anchor>],
441 cx: &mut App,
442 ) -> Vec<Range<BufferRow>> {
443 self.0.applicable_inlay_chunks(buffer, ranges, cx)
444 }
445
446 fn invalidate_inlay_hints(&self, for_buffers: &HashSet<BufferId>, cx: &mut App) {
447 self.0.invalidate_inlay_hints(for_buffers, cx);
448 }
449
450 fn inlay_hints(
451 &self,
452 invalidate: InvalidationStrategy,
453 buffer: Entity<Buffer>,
454 ranges: Vec<Range<text::Anchor>>,
455 known_chunks: Option<(clock::Global, HashSet<Range<BufferRow>>)>,
456 cx: &mut App,
457 ) -> Option<HashMap<Range<BufferRow>, Task<anyhow::Result<CacheInlayHints>>>> {
458 let positions = ranges
459 .iter()
460 .flat_map(|range| [range.start, range.end])
461 .collect::<Vec<_>>();
462 let buffer = self.to_base(&buffer, &positions, cx)?;
463 self.0
464 .inlay_hints(invalidate, buffer, ranges, known_chunks, cx)
465 }
466
467 fn inline_values(
468 &self,
469 _: Entity<Buffer>,
470 _: Range<text::Anchor>,
471 _: &mut App,
472 ) -> Option<Task<anyhow::Result<Vec<project::InlayHint>>>> {
473 None
474 }
475
476 fn supports_inlay_hints(&self, buffer: &Entity<Buffer>, cx: &mut App) -> bool {
477 if let Some(buffer) = self.to_base(buffer, &[], cx) {
478 self.0.supports_inlay_hints(&buffer, cx)
479 } else {
480 false
481 }
482 }
483
484 fn document_highlights(
485 &self,
486 buffer: &Entity<Buffer>,
487 position: text::Anchor,
488 cx: &mut App,
489 ) -> Option<Task<anyhow::Result<Vec<project::DocumentHighlight>>>> {
490 let buffer = self.to_base(buffer, &[position], cx)?;
491 self.0.document_highlights(&buffer, position, cx)
492 }
493
494 fn definitions(
495 &self,
496 buffer: &Entity<Buffer>,
497 position: text::Anchor,
498 kind: crate::GotoDefinitionKind,
499 cx: &mut App,
500 ) -> Option<Task<anyhow::Result<Option<Vec<project::LocationLink>>>>> {
501 let buffer = self.to_base(buffer, &[position], cx)?;
502 self.0.definitions(&buffer, position, kind, cx)
503 }
504
505 fn range_for_rename(
506 &self,
507 _: &Entity<Buffer>,
508 _: text::Anchor,
509 _: &mut App,
510 ) -> Option<Task<anyhow::Result<Option<Range<text::Anchor>>>>> {
511 None
512 }
513
514 fn perform_rename(
515 &self,
516 _: &Entity<Buffer>,
517 _: text::Anchor,
518 _: String,
519 _: &mut App,
520 ) -> Option<Task<anyhow::Result<project::ProjectTransaction>>> {
521 None
522 }
523}