proposed_changes_editor.rs

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