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