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