proposed_changes_editor.rs

  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}