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.new_view(|cx| {
 67                let mut editor = Editor::for_multibuffer(multibuffer.clone(), project, true, cx);
 68                editor.set_expand_all_diff_hunks();
 69                editor
 70            }),
 71            recalculate_diffs_tx,
 72            _recalculate_diffs_task: cx.spawn(|_, mut cx| async move {
 73                let mut buffers_to_diff = HashSet::default();
 74                while let Some(mut recalculate_diff) = recalculate_diffs_rx.next().await {
 75                    buffers_to_diff.insert(recalculate_diff.buffer);
 76
 77                    while recalculate_diff.debounce {
 78                        cx.background_executor()
 79                            .timer(Duration::from_millis(250))
 80                            .await;
 81                        let mut had_further_changes = false;
 82                        while let Ok(next_recalculate_diff) = recalculate_diffs_rx.try_next() {
 83                            let next_recalculate_diff = next_recalculate_diff?;
 84                            recalculate_diff.debounce &= next_recalculate_diff.debounce;
 85                            buffers_to_diff.insert(next_recalculate_diff.buffer);
 86                            had_further_changes = true;
 87                        }
 88                        if !had_further_changes {
 89                            break;
 90                        }
 91                    }
 92
 93                    join_all(buffers_to_diff.drain().filter_map(|buffer| {
 94                        buffer
 95                            .update(&mut cx, |buffer, cx| buffer.recalculate_diff(cx))
 96                            .ok()?
 97                    }))
 98                    .await;
 99                }
100                None
101            }),
102            _subscriptions: subscriptions,
103        }
104    }
105
106    fn on_buffer_event(
107        &mut self,
108        buffer: Model<Buffer>,
109        event: &BufferEvent,
110        _cx: &mut ViewContext<Self>,
111    ) {
112        match event {
113            BufferEvent::Operation { .. } => {
114                self.recalculate_diffs_tx
115                    .unbounded_send(RecalculateDiff {
116                        buffer,
117                        debounce: true,
118                    })
119                    .ok();
120            }
121            BufferEvent::DiffBaseChanged => {
122                self.recalculate_diffs_tx
123                    .unbounded_send(RecalculateDiff {
124                        buffer,
125                        debounce: false,
126                    })
127                    .ok();
128            }
129            _ => (),
130        }
131    }
132}
133
134impl Render for ProposedChangesEditor {
135    fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
136        self.editor.clone()
137    }
138}
139
140impl FocusableView for ProposedChangesEditor {
141    fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
142        self.editor.focus_handle(cx)
143    }
144}
145
146impl EventEmitter<EditorEvent> for ProposedChangesEditor {}
147
148impl Item for ProposedChangesEditor {
149    type Event = EditorEvent;
150
151    fn tab_icon(&self, _cx: &ui::WindowContext) -> Option<Icon> {
152        Some(Icon::new(IconName::Pencil))
153    }
154
155    fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
156        Some("Proposed changes".into())
157    }
158
159    fn as_searchable(&self, _: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
160        Some(Box::new(self.editor.clone()))
161    }
162
163    fn act_as_type<'a>(
164        &'a self,
165        type_id: TypeId,
166        self_handle: &'a View<Self>,
167        _: &'a AppContext,
168    ) -> Option<gpui::AnyView> {
169        if type_id == TypeId::of::<Self>() {
170            Some(self_handle.to_any())
171        } else if type_id == TypeId::of::<Editor>() {
172            Some(self.editor.to_any())
173        } else {
174            None
175        }
176    }
177
178    fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
179        self.editor.update(cx, |editor, cx| {
180            Item::added_to_workspace(editor, workspace, cx)
181        });
182    }
183
184    fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
185        self.editor.update(cx, Item::deactivated);
186    }
187
188    fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
189        self.editor
190            .update(cx, |editor, cx| Item::navigate(editor, data, cx))
191    }
192
193    fn set_nav_history(
194        &mut self,
195        nav_history: workspace::ItemNavHistory,
196        cx: &mut ViewContext<Self>,
197    ) {
198        self.editor.update(cx, |editor, cx| {
199            Item::set_nav_history(editor, nav_history, cx)
200        });
201    }
202}
203
204impl ProposedChangesEditorToolbar {
205    pub fn new() -> Self {
206        Self {
207            current_editor: None,
208        }
209    }
210
211    fn get_toolbar_item_location(&self) -> ToolbarItemLocation {
212        if self.current_editor.is_some() {
213            ToolbarItemLocation::PrimaryRight
214        } else {
215            ToolbarItemLocation::Hidden
216        }
217    }
218}
219
220impl Render for ProposedChangesEditorToolbar {
221    fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
222        let editor = self.current_editor.clone();
223        Button::new("apply-changes", "Apply All").on_click(move |_, cx| {
224            if let Some(editor) = &editor {
225                editor.update(cx, |editor, cx| {
226                    editor.editor.update(cx, |editor, cx| {
227                        editor.apply_all_changes(cx);
228                    })
229                });
230            }
231        })
232    }
233}
234
235impl EventEmitter<ToolbarItemEvent> for ProposedChangesEditorToolbar {}
236
237impl ToolbarItemView for ProposedChangesEditorToolbar {
238    fn set_active_pane_item(
239        &mut self,
240        active_pane_item: Option<&dyn workspace::ItemHandle>,
241        _cx: &mut ViewContext<Self>,
242    ) -> workspace::ToolbarItemLocation {
243        self.current_editor =
244            active_pane_item.and_then(|item| item.downcast::<ProposedChangesEditor>());
245        self.get_toolbar_item_location()
246    }
247}