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 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(|_, 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 join_all(buffers_to_diff.drain().filter_map(|buffer| {
100 buffer
101 .update(&mut cx, |buffer, cx| buffer.recalculate_diff(cx))
102 .ok()?
103 }))
104 .await;
105 }
106 None
107 }),
108 };
109 this.reset_locations(locations, cx);
110 this
111 }
112
113 pub fn branch_buffer_for_base(&self, base_buffer: &Model<Buffer>) -> Option<Model<Buffer>> {
114 self.buffer_entries.iter().find_map(|entry| {
115 if &entry.base == base_buffer {
116 Some(entry.branch.clone())
117 } else {
118 None
119 }
120 })
121 }
122
123 pub fn set_title(&mut self, title: SharedString, cx: &mut ViewContext<Self>) {
124 self.title = title;
125 cx.notify();
126 }
127
128 pub fn reset_locations<T: ToOffset>(
129 &mut self,
130 locations: Vec<ProposedChangeLocation<T>>,
131 cx: &mut ViewContext<Self>,
132 ) {
133 // Undo all branch changes
134 for entry in &self.buffer_entries {
135 let base_version = entry.base.read(cx).version();
136 entry.branch.update(cx, |buffer, cx| {
137 let undo_counts = buffer
138 .operations()
139 .iter()
140 .filter_map(|(timestamp, _)| {
141 if !base_version.observed(*timestamp) {
142 Some((*timestamp, u32::MAX))
143 } else {
144 None
145 }
146 })
147 .collect();
148 buffer.undo_operations(undo_counts, cx);
149 });
150 }
151
152 self.multibuffer.update(cx, |multibuffer, cx| {
153 multibuffer.clear(cx);
154 });
155
156 let mut buffer_entries = Vec::new();
157 for location in locations {
158 let branch_buffer;
159 if let Some(ix) = self
160 .buffer_entries
161 .iter()
162 .position(|entry| entry.base == location.buffer)
163 {
164 let entry = self.buffer_entries.remove(ix);
165 branch_buffer = entry.branch.clone();
166 buffer_entries.push(entry);
167 } else {
168 branch_buffer = location.buffer.update(cx, |buffer, cx| buffer.branch(cx));
169 buffer_entries.push(BufferEntry {
170 branch: branch_buffer.clone(),
171 base: location.buffer.clone(),
172 _subscription: cx.subscribe(&branch_buffer, Self::on_buffer_event),
173 });
174 }
175
176 self.multibuffer.update(cx, |multibuffer, cx| {
177 multibuffer.push_excerpts(
178 branch_buffer,
179 location.ranges.into_iter().map(|range| ExcerptRange {
180 context: range,
181 primary: None,
182 }),
183 cx,
184 );
185 });
186 }
187
188 self.buffer_entries = buffer_entries;
189 self.editor.update(cx, |editor, cx| {
190 editor.change_selections(None, cx, |selections| selections.refresh())
191 });
192 }
193
194 pub fn recalculate_all_buffer_diffs(&self) {
195 for (ix, entry) in self.buffer_entries.iter().enumerate().rev() {
196 self.recalculate_diffs_tx
197 .unbounded_send(RecalculateDiff {
198 buffer: entry.branch.clone(),
199 debounce: ix > 0,
200 })
201 .ok();
202 }
203 }
204
205 fn on_buffer_event(
206 &mut self,
207 buffer: Model<Buffer>,
208 event: &BufferEvent,
209 _cx: &mut ViewContext<Self>,
210 ) {
211 match event {
212 BufferEvent::Operation { .. } => {
213 self.recalculate_diffs_tx
214 .unbounded_send(RecalculateDiff {
215 buffer,
216 debounce: true,
217 })
218 .ok();
219 }
220 BufferEvent::DiffBaseChanged => {
221 self.recalculate_diffs_tx
222 .unbounded_send(RecalculateDiff {
223 buffer,
224 debounce: false,
225 })
226 .ok();
227 }
228 _ => (),
229 }
230 }
231}
232
233impl Render for ProposedChangesEditor {
234 fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
235 div()
236 .size_full()
237 .key_context("ProposedChangesEditor")
238 .child(self.editor.clone())
239 }
240}
241
242impl FocusableView for ProposedChangesEditor {
243 fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
244 self.editor.focus_handle(cx)
245 }
246}
247
248impl EventEmitter<EditorEvent> for ProposedChangesEditor {}
249
250impl Item for ProposedChangesEditor {
251 type Event = EditorEvent;
252
253 fn tab_icon(&self, _cx: &ui::WindowContext) -> Option<Icon> {
254 Some(Icon::new(IconName::Diff))
255 }
256
257 fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
258 Some(self.title.clone())
259 }
260
261 fn as_searchable(&self, _: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
262 Some(Box::new(self.editor.clone()))
263 }
264
265 fn act_as_type<'a>(
266 &'a self,
267 type_id: TypeId,
268 self_handle: &'a View<Self>,
269 _: &'a AppContext,
270 ) -> Option<gpui::AnyView> {
271 if type_id == TypeId::of::<Self>() {
272 Some(self_handle.to_any())
273 } else if type_id == TypeId::of::<Editor>() {
274 Some(self.editor.to_any())
275 } else {
276 None
277 }
278 }
279
280 fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
281 self.editor.update(cx, |editor, cx| {
282 Item::added_to_workspace(editor, workspace, cx)
283 });
284 }
285
286 fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
287 self.editor.update(cx, Item::deactivated);
288 }
289
290 fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
291 self.editor
292 .update(cx, |editor, cx| Item::navigate(editor, data, cx))
293 }
294
295 fn set_nav_history(
296 &mut self,
297 nav_history: workspace::ItemNavHistory,
298 cx: &mut ViewContext<Self>,
299 ) {
300 self.editor.update(cx, |editor, cx| {
301 Item::set_nav_history(editor, nav_history, cx)
302 });
303 }
304
305 fn can_save(&self, cx: &AppContext) -> bool {
306 self.editor.read(cx).can_save(cx)
307 }
308
309 fn save(
310 &mut self,
311 format: bool,
312 project: Model<Project>,
313 cx: &mut ViewContext<Self>,
314 ) -> Task<gpui::Result<()>> {
315 self.editor
316 .update(cx, |editor, cx| Item::save(editor, format, project, cx))
317 }
318}
319
320impl ProposedChangesEditorToolbar {
321 pub fn new() -> Self {
322 Self {
323 current_editor: None,
324 }
325 }
326
327 fn get_toolbar_item_location(&self) -> ToolbarItemLocation {
328 if self.current_editor.is_some() {
329 ToolbarItemLocation::PrimaryRight
330 } else {
331 ToolbarItemLocation::Hidden
332 }
333 }
334}
335
336impl Render for ProposedChangesEditorToolbar {
337 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
338 let button_like = ButtonLike::new("apply-changes").child(Label::new("Apply All"));
339
340 match &self.current_editor {
341 Some(editor) => {
342 let focus_handle = editor.focus_handle(cx);
343 let keybinding = KeyBinding::for_action_in(&ApplyAllDiffHunks, &focus_handle, cx)
344 .map(|binding| binding.into_any_element());
345
346 button_like.children(keybinding).on_click({
347 move |_event, cx| focus_handle.dispatch_action(&ApplyAllDiffHunks, cx)
348 })
349 }
350 None => button_like.disabled(true),
351 }
352 }
353}
354
355impl EventEmitter<ToolbarItemEvent> for ProposedChangesEditorToolbar {}
356
357impl ToolbarItemView for ProposedChangesEditorToolbar {
358 fn set_active_pane_item(
359 &mut self,
360 active_pane_item: Option<&dyn workspace::ItemHandle>,
361 _cx: &mut ViewContext<Self>,
362 ) -> workspace::ToolbarItemLocation {
363 self.current_editor =
364 active_pane_item.and_then(|item| item.downcast::<ProposedChangesEditor>());
365 self.get_toolbar_item_location()
366 }
367}
368
369impl BranchBufferSemanticsProvider {
370 fn to_base(
371 &self,
372 buffer: &Model<Buffer>,
373 positions: &[text::Anchor],
374 cx: &AppContext,
375 ) -> Option<Model<Buffer>> {
376 let base_buffer = buffer.read(cx).diff_base_buffer()?;
377 let version = base_buffer.read(cx).version();
378 if positions
379 .iter()
380 .any(|position| !version.observed(position.timestamp))
381 {
382 return None;
383 }
384 Some(base_buffer)
385 }
386}
387
388impl SemanticsProvider for BranchBufferSemanticsProvider {
389 fn hover(
390 &self,
391 buffer: &Model<Buffer>,
392 position: text::Anchor,
393 cx: &mut AppContext,
394 ) -> Option<Task<Vec<project::Hover>>> {
395 let buffer = self.to_base(buffer, &[position], cx)?;
396 self.0.hover(&buffer, position, cx)
397 }
398
399 fn inlay_hints(
400 &self,
401 buffer: Model<Buffer>,
402 range: Range<text::Anchor>,
403 cx: &mut AppContext,
404 ) -> Option<Task<anyhow::Result<Vec<project::InlayHint>>>> {
405 let buffer = self.to_base(&buffer, &[range.start, range.end], cx)?;
406 self.0.inlay_hints(buffer, range, cx)
407 }
408
409 fn resolve_inlay_hint(
410 &self,
411 hint: project::InlayHint,
412 buffer: Model<Buffer>,
413 server_id: lsp::LanguageServerId,
414 cx: &mut AppContext,
415 ) -> Option<Task<anyhow::Result<project::InlayHint>>> {
416 let buffer = self.to_base(&buffer, &[], cx)?;
417 self.0.resolve_inlay_hint(hint, buffer, server_id, cx)
418 }
419
420 fn supports_inlay_hints(&self, buffer: &Model<Buffer>, cx: &AppContext) -> bool {
421 if let Some(buffer) = self.to_base(&buffer, &[], cx) {
422 self.0.supports_inlay_hints(&buffer, cx)
423 } else {
424 false
425 }
426 }
427
428 fn document_highlights(
429 &self,
430 buffer: &Model<Buffer>,
431 position: text::Anchor,
432 cx: &mut AppContext,
433 ) -> Option<Task<gpui::Result<Vec<project::DocumentHighlight>>>> {
434 let buffer = self.to_base(&buffer, &[position], cx)?;
435 self.0.document_highlights(&buffer, position, cx)
436 }
437
438 fn definitions(
439 &self,
440 buffer: &Model<Buffer>,
441 position: text::Anchor,
442 kind: crate::GotoDefinitionKind,
443 cx: &mut AppContext,
444 ) -> Option<Task<gpui::Result<Vec<project::LocationLink>>>> {
445 let buffer = self.to_base(&buffer, &[position], cx)?;
446 self.0.definitions(&buffer, position, kind, cx)
447 }
448
449 fn range_for_rename(
450 &self,
451 _: &Model<Buffer>,
452 _: text::Anchor,
453 _: &mut AppContext,
454 ) -> Option<Task<gpui::Result<Option<Range<text::Anchor>>>>> {
455 None
456 }
457
458 fn perform_rename(
459 &self,
460 _: &Model<Buffer>,
461 _: text::Anchor,
462 _: String,
463 _: &mut AppContext,
464 ) -> Option<Task<gpui::Result<project::ProjectTransaction>>> {
465 None
466 }
467}