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 settings::Settings;
  9use smol::stream::StreamExt;
 10use std::{any::TypeId, ops::Range, rc::Rc, time::Duration};
 11use text::ToOffset;
 12use ui::{prelude::*, KeyBinding};
 13use workspace::{
 14    searchable::SearchableItemHandle, Item, ItemHandle as _, ToolbarItemEvent, ToolbarItemLocation,
 15    ToolbarItemView, Workspace,
 16};
 17
 18pub struct ProposedChangesEditor {
 19    editor: View<Editor>,
 20    multibuffer: Model<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: Model<Buffer>,
 29    pub ranges: Vec<Range<T>>,
 30}
 31
 32struct BufferEntry {
 33    base: Model<Buffer>,
 34    branch: Model<Buffer>,
 35    _subscription: Subscription,
 36}
 37
 38pub struct ProposedChangesToolbarControls {
 39    current_editor: Option<View<ProposedChangesEditor>>,
 40}
 41
 42pub struct ProposedChangesToolbar {
 43    current_editor: Option<View<ProposedChangesEditor>>,
 44}
 45
 46struct RecalculateDiff {
 47    buffer: Model<Buffer>,
 48    debounce: bool,
 49}
 50
 51/// A provider of code semantics for branch buffers.
 52///
 53/// Requests in edited regions will return nothing, but requests in unchanged
 54/// regions will be translated into the base buffer's coordinates.
 55struct BranchBufferSemanticsProvider(Rc<dyn SemanticsProvider>);
 56
 57impl ProposedChangesEditor {
 58    pub fn new<T: ToOffset>(
 59        title: impl Into<SharedString>,
 60        locations: Vec<ProposedChangeLocation<T>>,
 61        project: Option<Model<Project>>,
 62        cx: &mut ViewContext<Self>,
 63    ) -> Self {
 64        let multibuffer = cx.new_model(|_| MultiBuffer::new(Capability::ReadWrite));
 65        let (recalculate_diffs_tx, mut recalculate_diffs_rx) = mpsc::unbounded();
 66        let mut this = Self {
 67            editor: cx.new_view(|cx| {
 68                let mut editor = Editor::for_multibuffer(multibuffer.clone(), project, true, cx);
 69                editor.set_expand_all_diff_hunks();
 70                editor.set_completion_provider(None);
 71                editor.clear_code_action_providers();
 72                editor.set_semantics_provider(
 73                    editor
 74                        .semantics_provider()
 75                        .map(|provider| Rc::new(BranchBufferSemanticsProvider(provider)) as _),
 76                );
 77                editor
 78            }),
 79            multibuffer,
 80            title: title.into(),
 81            buffer_entries: Vec::new(),
 82            recalculate_diffs_tx,
 83            _recalculate_diffs_task: cx.spawn(|_, mut cx| async move {
 84                let mut buffers_to_diff = HashSet::default();
 85                while let Some(mut recalculate_diff) = recalculate_diffs_rx.next().await {
 86                    buffers_to_diff.insert(recalculate_diff.buffer);
 87
 88                    while recalculate_diff.debounce {
 89                        cx.background_executor()
 90                            .timer(Duration::from_millis(50))
 91                            .await;
 92                        let mut had_further_changes = false;
 93                        while let Ok(next_recalculate_diff) = recalculate_diffs_rx.try_next() {
 94                            let next_recalculate_diff = next_recalculate_diff?;
 95                            recalculate_diff.debounce &= next_recalculate_diff.debounce;
 96                            buffers_to_diff.insert(next_recalculate_diff.buffer);
 97                            had_further_changes = true;
 98                        }
 99                        if !had_further_changes {
100                            break;
101                        }
102                    }
103
104                    join_all(buffers_to_diff.drain().filter_map(|buffer| {
105                        buffer
106                            .update(&mut cx, |buffer, cx| buffer.recalculate_diff(cx))
107                            .ok()?
108                    }))
109                    .await;
110                }
111                None
112            }),
113        };
114        this.reset_locations(locations, cx);
115        this
116    }
117
118    pub fn branch_buffer_for_base(&self, base_buffer: &Model<Buffer>) -> Option<Model<Buffer>> {
119        self.buffer_entries.iter().find_map(|entry| {
120            if &entry.base == base_buffer {
121                Some(entry.branch.clone())
122            } else {
123                None
124            }
125        })
126    }
127
128    pub fn set_title(&mut self, title: SharedString, cx: &mut ViewContext<Self>) {
129        self.title = title;
130        cx.notify();
131    }
132
133    pub fn reset_locations<T: ToOffset>(
134        &mut self,
135        locations: Vec<ProposedChangeLocation<T>>,
136        cx: &mut ViewContext<Self>,
137    ) {
138        // Undo all branch changes
139        for entry in &self.buffer_entries {
140            let base_version = entry.base.read(cx).version();
141            entry.branch.update(cx, |buffer, cx| {
142                let undo_counts = buffer
143                    .operations()
144                    .iter()
145                    .filter_map(|(timestamp, _)| {
146                        if !base_version.observed(*timestamp) {
147                            Some((*timestamp, u32::MAX))
148                        } else {
149                            None
150                        }
151                    })
152                    .collect();
153                buffer.undo_operations(undo_counts, cx);
154            });
155        }
156
157        self.multibuffer.update(cx, |multibuffer, cx| {
158            multibuffer.clear(cx);
159        });
160
161        let mut buffer_entries = Vec::new();
162        for location in locations {
163            let branch_buffer;
164            if let Some(ix) = self
165                .buffer_entries
166                .iter()
167                .position(|entry| entry.base == location.buffer)
168            {
169                let entry = self.buffer_entries.remove(ix);
170                branch_buffer = entry.branch.clone();
171                buffer_entries.push(entry);
172            } else {
173                branch_buffer = location.buffer.update(cx, |buffer, cx| buffer.branch(cx));
174                buffer_entries.push(BufferEntry {
175                    branch: branch_buffer.clone(),
176                    base: location.buffer.clone(),
177                    _subscription: cx.subscribe(&branch_buffer, Self::on_buffer_event),
178                });
179            }
180
181            self.multibuffer.update(cx, |multibuffer, cx| {
182                multibuffer.push_excerpts(
183                    branch_buffer,
184                    location.ranges.into_iter().map(|range| ExcerptRange {
185                        context: range,
186                        primary: None,
187                    }),
188                    cx,
189                );
190            });
191        }
192
193        self.buffer_entries = buffer_entries;
194        self.editor.update(cx, |editor, cx| {
195            editor.change_selections(None, cx, |selections| selections.refresh())
196        });
197    }
198
199    pub fn recalculate_all_buffer_diffs(&self) {
200        for (ix, entry) in self.buffer_entries.iter().enumerate().rev() {
201            self.recalculate_diffs_tx
202                .unbounded_send(RecalculateDiff {
203                    buffer: entry.branch.clone(),
204                    debounce: ix > 0,
205                })
206                .ok();
207        }
208    }
209
210    fn on_buffer_event(
211        &mut self,
212        buffer: Model<Buffer>,
213        event: &BufferEvent,
214        _cx: &mut ViewContext<Self>,
215    ) {
216        match event {
217            BufferEvent::Operation { .. } => {
218                self.recalculate_diffs_tx
219                    .unbounded_send(RecalculateDiff {
220                        buffer,
221                        debounce: true,
222                    })
223                    .ok();
224            }
225            BufferEvent::DiffBaseChanged => {
226                self.recalculate_diffs_tx
227                    .unbounded_send(RecalculateDiff {
228                        buffer,
229                        debounce: false,
230                    })
231                    .ok();
232            }
233            _ => (),
234        }
235    }
236
237    fn all_changes_accepted(&self) -> bool {
238        false // In the future, we plan to compute this based on the current state of patches.
239    }
240}
241
242impl Render for ProposedChangesEditor {
243    fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
244        div()
245            .size_full()
246            .key_context("ProposedChangesEditor")
247            .child(self.editor.clone())
248    }
249}
250
251impl FocusableView for ProposedChangesEditor {
252    fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
253        self.editor.focus_handle(cx)
254    }
255}
256
257impl EventEmitter<EditorEvent> for ProposedChangesEditor {}
258
259impl Item for ProposedChangesEditor {
260    type Event = EditorEvent;
261
262    fn tab_icon(&self, _cx: &ui::WindowContext) -> Option<Icon> {
263        if self.all_changes_accepted() {
264            Some(Icon::new(IconName::Check).color(Color::Success))
265        } else {
266            Some(Icon::new(IconName::ZedAssistant))
267        }
268    }
269
270    fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
271        Some(self.title.clone())
272    }
273
274    fn as_searchable(&self, _: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
275        Some(Box::new(self.editor.clone()))
276    }
277
278    fn act_as_type<'a>(
279        &'a self,
280        type_id: TypeId,
281        self_handle: &'a View<Self>,
282        _: &'a AppContext,
283    ) -> Option<gpui::AnyView> {
284        if type_id == TypeId::of::<Self>() {
285            Some(self_handle.to_any())
286        } else if type_id == TypeId::of::<Editor>() {
287            Some(self.editor.to_any())
288        } else {
289            None
290        }
291    }
292
293    fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
294        self.editor.update(cx, |editor, cx| {
295            Item::added_to_workspace(editor, workspace, cx)
296        });
297    }
298
299    fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
300        self.editor.update(cx, Item::deactivated);
301    }
302
303    fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
304        self.editor
305            .update(cx, |editor, cx| Item::navigate(editor, data, cx))
306    }
307
308    fn set_nav_history(
309        &mut self,
310        nav_history: workspace::ItemNavHistory,
311        cx: &mut ViewContext<Self>,
312    ) {
313        self.editor.update(cx, |editor, cx| {
314            Item::set_nav_history(editor, nav_history, cx)
315        });
316    }
317
318    fn can_save(&self, cx: &AppContext) -> bool {
319        self.editor.read(cx).can_save(cx)
320    }
321
322    fn save(
323        &mut self,
324        format: bool,
325        project: Model<Project>,
326        cx: &mut ViewContext<Self>,
327    ) -> Task<gpui::Result<()>> {
328        self.editor
329            .update(cx, |editor, cx| Item::save(editor, format, project, cx))
330    }
331}
332
333impl ProposedChangesToolbarControls {
334    pub fn new() -> Self {
335        Self {
336            current_editor: None,
337        }
338    }
339
340    fn get_toolbar_item_location(&self) -> ToolbarItemLocation {
341        if self.current_editor.is_some() {
342            ToolbarItemLocation::PrimaryRight
343        } else {
344            ToolbarItemLocation::Hidden
345        }
346    }
347}
348
349impl Render for ProposedChangesToolbarControls {
350    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
351        if let Some(editor) = &self.current_editor {
352            let focus_handle = editor.focus_handle(cx);
353            let action = &ApplyAllDiffHunks;
354            let keybinding = KeyBinding::for_action_in(action, &focus_handle, cx);
355
356            let editor = editor.read(cx);
357
358            let apply_all_button = if editor.all_changes_accepted() {
359                None
360            } else {
361                Some(
362                    Button::new("apply-changes", "Apply All")
363                        .style(ButtonStyle::Filled)
364                        .key_binding(keybinding)
365                        .on_click(move |_event, cx| focus_handle.dispatch_action(action, cx)),
366                )
367            };
368
369            h_flex()
370                .gap_1()
371                .children([apply_all_button].into_iter().flatten())
372                .into_any_element()
373        } else {
374            gpui::Empty.into_any_element()
375        }
376    }
377}
378
379impl EventEmitter<ToolbarItemEvent> for ProposedChangesToolbarControls {}
380
381impl ToolbarItemView for ProposedChangesToolbarControls {
382    fn set_active_pane_item(
383        &mut self,
384        active_pane_item: Option<&dyn workspace::ItemHandle>,
385        _cx: &mut ViewContext<Self>,
386    ) -> workspace::ToolbarItemLocation {
387        self.current_editor =
388            active_pane_item.and_then(|item| item.downcast::<ProposedChangesEditor>());
389        self.get_toolbar_item_location()
390    }
391}
392
393impl ProposedChangesToolbar {
394    pub fn new() -> Self {
395        Self {
396            current_editor: None,
397        }
398    }
399
400    fn get_toolbar_item_location(&self) -> ToolbarItemLocation {
401        if self.current_editor.is_some() {
402            ToolbarItemLocation::PrimaryLeft
403        } else {
404            ToolbarItemLocation::Hidden
405        }
406    }
407}
408
409impl Render for ProposedChangesToolbar {
410    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
411        if let Some(editor) = &self.current_editor {
412            let editor = editor.read(cx);
413            let all_changes_accepted = editor.all_changes_accepted();
414            let icon = if all_changes_accepted {
415                Icon::new(IconName::Check).color(Color::Success)
416            } else {
417                Icon::new(IconName::ZedAssistant)
418            };
419
420            h_flex()
421                .gap_2p5()
422                .font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
423                .child(icon.size(IconSize::Small))
424                .child(
425                    Label::new(editor.title.clone())
426                        .color(Color::Muted)
427                        .single_line()
428                        .strikethrough(all_changes_accepted),
429                )
430                .into_any_element()
431        } else {
432            gpui::Empty.into_any_element()
433        }
434    }
435}
436
437impl EventEmitter<ToolbarItemEvent> for ProposedChangesToolbar {}
438
439impl ToolbarItemView for ProposedChangesToolbar {
440    fn set_active_pane_item(
441        &mut self,
442        active_pane_item: Option<&dyn workspace::ItemHandle>,
443        _cx: &mut ViewContext<Self>,
444    ) -> workspace::ToolbarItemLocation {
445        self.current_editor =
446            active_pane_item.and_then(|item| item.downcast::<ProposedChangesEditor>());
447        self.get_toolbar_item_location()
448    }
449}
450
451impl BranchBufferSemanticsProvider {
452    fn to_base(
453        &self,
454        buffer: &Model<Buffer>,
455        positions: &[text::Anchor],
456        cx: &AppContext,
457    ) -> Option<Model<Buffer>> {
458        let base_buffer = buffer.read(cx).diff_base_buffer()?;
459        let version = base_buffer.read(cx).version();
460        if positions
461            .iter()
462            .any(|position| !version.observed(position.timestamp))
463        {
464            return None;
465        }
466        Some(base_buffer)
467    }
468}
469
470impl SemanticsProvider for BranchBufferSemanticsProvider {
471    fn hover(
472        &self,
473        buffer: &Model<Buffer>,
474        position: text::Anchor,
475        cx: &mut AppContext,
476    ) -> Option<Task<Vec<project::Hover>>> {
477        let buffer = self.to_base(buffer, &[position], cx)?;
478        self.0.hover(&buffer, position, cx)
479    }
480
481    fn inlay_hints(
482        &self,
483        buffer: Model<Buffer>,
484        range: Range<text::Anchor>,
485        cx: &mut AppContext,
486    ) -> Option<Task<anyhow::Result<Vec<project::InlayHint>>>> {
487        let buffer = self.to_base(&buffer, &[range.start, range.end], cx)?;
488        self.0.inlay_hints(buffer, range, cx)
489    }
490
491    fn resolve_inlay_hint(
492        &self,
493        hint: project::InlayHint,
494        buffer: Model<Buffer>,
495        server_id: lsp::LanguageServerId,
496        cx: &mut AppContext,
497    ) -> Option<Task<anyhow::Result<project::InlayHint>>> {
498        let buffer = self.to_base(&buffer, &[], cx)?;
499        self.0.resolve_inlay_hint(hint, buffer, server_id, cx)
500    }
501
502    fn supports_inlay_hints(&self, buffer: &Model<Buffer>, cx: &AppContext) -> bool {
503        if let Some(buffer) = self.to_base(&buffer, &[], cx) {
504            self.0.supports_inlay_hints(&buffer, cx)
505        } else {
506            false
507        }
508    }
509
510    fn document_highlights(
511        &self,
512        buffer: &Model<Buffer>,
513        position: text::Anchor,
514        cx: &mut AppContext,
515    ) -> Option<Task<gpui::Result<Vec<project::DocumentHighlight>>>> {
516        let buffer = self.to_base(&buffer, &[position], cx)?;
517        self.0.document_highlights(&buffer, position, cx)
518    }
519
520    fn definitions(
521        &self,
522        buffer: &Model<Buffer>,
523        position: text::Anchor,
524        kind: crate::GotoDefinitionKind,
525        cx: &mut AppContext,
526    ) -> Option<Task<gpui::Result<Vec<project::LocationLink>>>> {
527        let buffer = self.to_base(&buffer, &[position], cx)?;
528        self.0.definitions(&buffer, position, kind, cx)
529    }
530
531    fn range_for_rename(
532        &self,
533        _: &Model<Buffer>,
534        _: text::Anchor,
535        _: &mut AppContext,
536    ) -> Option<Task<gpui::Result<Option<Range<text::Anchor>>>>> {
537        None
538    }
539
540    fn perform_rename(
541        &self,
542        _: &Model<Buffer>,
543        _: text::Anchor,
544        _: String,
545        _: &mut AppContext,
546    ) -> Option<Task<gpui::Result<project::ProjectTransaction>>> {
547        None
548    }
549}