proposed_changes_editor.rs

  1use crate::{Editor, EditorEvent};
  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, time::Duration};
 10use text::ToOffset;
 11use ui::prelude::*;
 12use workspace::{
 13    searchable::SearchableItemHandle, Item, ItemHandle as _, ToolbarItemEvent, ToolbarItemLocation,
 14    ToolbarItemView, Workspace,
 15};
 16
 17pub struct ProposedChangesEditor {
 18    editor: View<Editor>,
 19    _subscriptions: Vec<Subscription>,
 20    _recalculate_diffs_task: Task<Option<()>>,
 21    recalculate_diffs_tx: mpsc::UnboundedSender<RecalculateDiff>,
 22}
 23
 24pub struct ProposedChangesBuffer<T> {
 25    pub buffer: Model<Buffer>,
 26    pub ranges: Vec<Range<T>>,
 27}
 28
 29pub struct ProposedChangesEditorToolbar {
 30    current_editor: Option<View<ProposedChangesEditor>>,
 31}
 32
 33struct RecalculateDiff {
 34    buffer: Model<Buffer>,
 35    debounce: bool,
 36}
 37
 38impl ProposedChangesEditor {
 39    pub fn new<T: ToOffset>(
 40        buffers: Vec<ProposedChangesBuffer<T>>,
 41        project: Option<Model<Project>>,
 42        cx: &mut ViewContext<Self>,
 43    ) -> Self {
 44        let mut subscriptions = Vec::new();
 45        let multibuffer = cx.new_model(|_| MultiBuffer::new(Capability::ReadWrite));
 46
 47        for buffer in buffers {
 48            let branch_buffer = buffer.buffer.update(cx, |buffer, cx| buffer.branch(cx));
 49            subscriptions.push(cx.subscribe(&branch_buffer, Self::on_buffer_event));
 50
 51            multibuffer.update(cx, |multibuffer, cx| {
 52                multibuffer.push_excerpts(
 53                    branch_buffer,
 54                    buffer.ranges.into_iter().map(|range| ExcerptRange {
 55                        context: range,
 56                        primary: None,
 57                    }),
 58                    cx,
 59                );
 60            });
 61        }
 62
 63        let (recalculate_diffs_tx, mut recalculate_diffs_rx) = mpsc::unbounded();
 64
 65        Self {
 66            editor: cx
 67                .new_view(|cx| Editor::for_multibuffer(multibuffer.clone(), project, true, cx)),
 68            recalculate_diffs_tx,
 69            _recalculate_diffs_task: cx.spawn(|_, mut cx| async move {
 70                let mut buffers_to_diff = HashSet::default();
 71                while let Some(mut recalculate_diff) = recalculate_diffs_rx.next().await {
 72                    buffers_to_diff.insert(recalculate_diff.buffer);
 73
 74                    while recalculate_diff.debounce {
 75                        cx.background_executor()
 76                            .timer(Duration::from_millis(250))
 77                            .await;
 78                        let mut had_further_changes = false;
 79                        while let Ok(next_recalculate_diff) = recalculate_diffs_rx.try_next() {
 80                            let next_recalculate_diff = next_recalculate_diff?;
 81                            recalculate_diff.debounce &= next_recalculate_diff.debounce;
 82                            buffers_to_diff.insert(next_recalculate_diff.buffer);
 83                            had_further_changes = true;
 84                        }
 85                        if !had_further_changes {
 86                            break;
 87                        }
 88                    }
 89
 90                    join_all(buffers_to_diff.drain().filter_map(|buffer| {
 91                        buffer
 92                            .update(&mut cx, |buffer, cx| buffer.recalculate_diff(cx))
 93                            .ok()?
 94                    }))
 95                    .await;
 96                }
 97                None
 98            }),
 99            _subscriptions: subscriptions,
100        }
101    }
102
103    fn on_buffer_event(
104        &mut self,
105        buffer: Model<Buffer>,
106        event: &BufferEvent,
107        _cx: &mut ViewContext<Self>,
108    ) {
109        match event {
110            BufferEvent::Operation { .. } => {
111                self.recalculate_diffs_tx
112                    .unbounded_send(RecalculateDiff {
113                        buffer,
114                        debounce: true,
115                    })
116                    .ok();
117            }
118            BufferEvent::DiffBaseChanged => {
119                self.recalculate_diffs_tx
120                    .unbounded_send(RecalculateDiff {
121                        buffer,
122                        debounce: false,
123                    })
124                    .ok();
125            }
126            _ => (),
127        }
128    }
129}
130
131impl Render for ProposedChangesEditor {
132    fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
133        self.editor.clone()
134    }
135}
136
137impl FocusableView for ProposedChangesEditor {
138    fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
139        self.editor.focus_handle(cx)
140    }
141}
142
143impl EventEmitter<EditorEvent> for ProposedChangesEditor {}
144
145impl Item for ProposedChangesEditor {
146    type Event = EditorEvent;
147
148    fn tab_icon(&self, _cx: &ui::WindowContext) -> Option<Icon> {
149        Some(Icon::new(IconName::Pencil))
150    }
151
152    fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
153        Some("Proposed changes".into())
154    }
155
156    fn as_searchable(&self, _: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
157        Some(Box::new(self.editor.clone()))
158    }
159
160    fn act_as_type<'a>(
161        &'a self,
162        type_id: TypeId,
163        self_handle: &'a View<Self>,
164        _: &'a AppContext,
165    ) -> Option<gpui::AnyView> {
166        if type_id == TypeId::of::<Self>() {
167            Some(self_handle.to_any())
168        } else if type_id == TypeId::of::<Editor>() {
169            Some(self.editor.to_any())
170        } else {
171            None
172        }
173    }
174
175    fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
176        self.editor.update(cx, |editor, cx| {
177            Item::added_to_workspace(editor, workspace, cx)
178        });
179    }
180
181    fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
182        self.editor.update(cx, Item::deactivated);
183    }
184
185    fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
186        self.editor
187            .update(cx, |editor, cx| Item::navigate(editor, data, cx))
188    }
189
190    fn set_nav_history(
191        &mut self,
192        nav_history: workspace::ItemNavHistory,
193        cx: &mut ViewContext<Self>,
194    ) {
195        self.editor.update(cx, |editor, cx| {
196            Item::set_nav_history(editor, nav_history, cx)
197        });
198    }
199}
200
201impl ProposedChangesEditorToolbar {
202    pub fn new() -> Self {
203        Self {
204            current_editor: None,
205        }
206    }
207
208    fn get_toolbar_item_location(&self) -> ToolbarItemLocation {
209        if self.current_editor.is_some() {
210            ToolbarItemLocation::PrimaryRight
211        } else {
212            ToolbarItemLocation::Hidden
213        }
214    }
215}
216
217impl Render for ProposedChangesEditorToolbar {
218    fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
219        let editor = self.current_editor.clone();
220        Button::new("apply-changes", "Apply All").on_click(move |_, cx| {
221            if let Some(editor) = &editor {
222                editor.update(cx, |editor, cx| {
223                    editor.editor.update(cx, |editor, cx| {
224                        editor.apply_all_changes(cx);
225                    })
226                });
227            }
228        })
229    }
230}
231
232impl EventEmitter<ToolbarItemEvent> for ProposedChangesEditorToolbar {}
233
234impl ToolbarItemView for ProposedChangesEditorToolbar {
235    fn set_active_pane_item(
236        &mut self,
237        active_pane_item: Option<&dyn workspace::ItemHandle>,
238        _cx: &mut ViewContext<Self>,
239    ) -> workspace::ToolbarItemLocation {
240        self.current_editor =
241            active_pane_item.and_then(|item| item.downcast::<ProposedChangesEditor>());
242        self.get_toolbar_item_location()
243    }
244}