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