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::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}