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,
 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
164impl ProposedChangesEditorToolbar {
165    pub fn new() -> Self {
166        Self {
167            current_editor: None,
168        }
169    }
170
171    fn get_toolbar_item_location(&self) -> ToolbarItemLocation {
172        if self.current_editor.is_some() {
173            ToolbarItemLocation::PrimaryRight
174        } else {
175            ToolbarItemLocation::Hidden
176        }
177    }
178}
179
180impl Render for ProposedChangesEditorToolbar {
181    fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
182        let editor = self.current_editor.clone();
183        Button::new("apply-changes", "Apply All").on_click(move |_, cx| {
184            if let Some(editor) = &editor {
185                editor.update(cx, |editor, cx| {
186                    editor.apply_all_changes(cx);
187                });
188            }
189        })
190    }
191}
192
193impl EventEmitter<ToolbarItemEvent> for ProposedChangesEditorToolbar {}
194
195impl ToolbarItemView for ProposedChangesEditorToolbar {
196    fn set_active_pane_item(
197        &mut self,
198        active_pane_item: Option<&dyn workspace::ItemHandle>,
199        _cx: &mut ViewContext<Self>,
200    ) -> workspace::ToolbarItemLocation {
201        self.current_editor =
202            active_pane_item.and_then(|item| item.downcast::<ProposedChangesEditor>());
203        self.get_toolbar_item_location()
204    }
205}