proposed_changes_editor.rs

  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}