proposed_changes_editor.rs

  1use crate::{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 smol::stream::StreamExt;
  9use std::{any::TypeId, ops::Range, rc::Rc, 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
 38/// A provider of code semantics for branch buffers.
 39///
 40/// Requests in edited regions will return nothing, but requests in unchanged
 41/// regions will be translated into the base buffer's coordinates.
 42struct BranchBufferSemanticsProvider(Rc<dyn SemanticsProvider>);
 43
 44impl ProposedChangesEditor {
 45    pub fn new<T: ToOffset>(
 46        buffers: Vec<ProposedChangesBuffer<T>>,
 47        project: Option<Model<Project>>,
 48        cx: &mut ViewContext<Self>,
 49    ) -> Self {
 50        let mut subscriptions = Vec::new();
 51        let multibuffer = cx.new_model(|_| MultiBuffer::new(Capability::ReadWrite));
 52
 53        for buffer in buffers {
 54            let branch_buffer = buffer.buffer.update(cx, |buffer, cx| buffer.branch(cx));
 55            subscriptions.push(cx.subscribe(&branch_buffer, Self::on_buffer_event));
 56
 57            multibuffer.update(cx, |multibuffer, cx| {
 58                multibuffer.push_excerpts(
 59                    branch_buffer,
 60                    buffer.ranges.into_iter().map(|range| ExcerptRange {
 61                        context: range,
 62                        primary: None,
 63                    }),
 64                    cx,
 65                );
 66            });
 67        }
 68
 69        let (recalculate_diffs_tx, mut recalculate_diffs_rx) = mpsc::unbounded();
 70
 71        Self {
 72            editor: cx.new_view(|cx| {
 73                let mut editor = Editor::for_multibuffer(multibuffer.clone(), project, true, cx);
 74                editor.set_expand_all_diff_hunks();
 75                editor.set_completion_provider(None);
 76                editor.clear_code_action_providers();
 77                editor.set_semantics_provider(
 78                    editor
 79                        .semantics_provider()
 80                        .map(|provider| Rc::new(BranchBufferSemanticsProvider(provider)) as _),
 81                );
 82                editor
 83            }),
 84            recalculate_diffs_tx,
 85            _recalculate_diffs_task: cx.spawn(|_, mut cx| async move {
 86                let mut buffers_to_diff = HashSet::default();
 87                while let Some(mut recalculate_diff) = recalculate_diffs_rx.next().await {
 88                    buffers_to_diff.insert(recalculate_diff.buffer);
 89
 90                    while recalculate_diff.debounce {
 91                        cx.background_executor()
 92                            .timer(Duration::from_millis(50))
 93                            .await;
 94                        let mut had_further_changes = false;
 95                        while let Ok(next_recalculate_diff) = recalculate_diffs_rx.try_next() {
 96                            let next_recalculate_diff = next_recalculate_diff?;
 97                            recalculate_diff.debounce &= next_recalculate_diff.debounce;
 98                            buffers_to_diff.insert(next_recalculate_diff.buffer);
 99                            had_further_changes = true;
100                        }
101                        if !had_further_changes {
102                            break;
103                        }
104                    }
105
106                    join_all(buffers_to_diff.drain().filter_map(|buffer| {
107                        buffer
108                            .update(&mut cx, |buffer, cx| buffer.recalculate_diff(cx))
109                            .ok()?
110                    }))
111                    .await;
112                }
113                None
114            }),
115            _subscriptions: subscriptions,
116        }
117    }
118
119    fn on_buffer_event(
120        &mut self,
121        buffer: Model<Buffer>,
122        event: &BufferEvent,
123        _cx: &mut ViewContext<Self>,
124    ) {
125        match event {
126            BufferEvent::Operation { .. } => {
127                self.recalculate_diffs_tx
128                    .unbounded_send(RecalculateDiff {
129                        buffer,
130                        debounce: true,
131                    })
132                    .ok();
133            }
134            BufferEvent::DiffBaseChanged => {
135                self.recalculate_diffs_tx
136                    .unbounded_send(RecalculateDiff {
137                        buffer,
138                        debounce: false,
139                    })
140                    .ok();
141            }
142            _ => (),
143        }
144    }
145}
146
147impl Render for ProposedChangesEditor {
148    fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
149        self.editor.clone()
150    }
151}
152
153impl FocusableView for ProposedChangesEditor {
154    fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
155        self.editor.focus_handle(cx)
156    }
157}
158
159impl EventEmitter<EditorEvent> for ProposedChangesEditor {}
160
161impl Item for ProposedChangesEditor {
162    type Event = EditorEvent;
163
164    fn tab_icon(&self, _cx: &ui::WindowContext) -> Option<Icon> {
165        Some(Icon::new(IconName::Pencil))
166    }
167
168    fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
169        Some("Proposed changes".into())
170    }
171
172    fn as_searchable(&self, _: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
173        Some(Box::new(self.editor.clone()))
174    }
175
176    fn act_as_type<'a>(
177        &'a self,
178        type_id: TypeId,
179        self_handle: &'a View<Self>,
180        _: &'a AppContext,
181    ) -> Option<gpui::AnyView> {
182        if type_id == TypeId::of::<Self>() {
183            Some(self_handle.to_any())
184        } else if type_id == TypeId::of::<Editor>() {
185            Some(self.editor.to_any())
186        } else {
187            None
188        }
189    }
190
191    fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
192        self.editor.update(cx, |editor, cx| {
193            Item::added_to_workspace(editor, workspace, cx)
194        });
195    }
196
197    fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
198        self.editor.update(cx, Item::deactivated);
199    }
200
201    fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
202        self.editor
203            .update(cx, |editor, cx| Item::navigate(editor, data, cx))
204    }
205
206    fn set_nav_history(
207        &mut self,
208        nav_history: workspace::ItemNavHistory,
209        cx: &mut ViewContext<Self>,
210    ) {
211        self.editor.update(cx, |editor, cx| {
212            Item::set_nav_history(editor, nav_history, cx)
213        });
214    }
215}
216
217impl ProposedChangesEditorToolbar {
218    pub fn new() -> Self {
219        Self {
220            current_editor: None,
221        }
222    }
223
224    fn get_toolbar_item_location(&self) -> ToolbarItemLocation {
225        if self.current_editor.is_some() {
226            ToolbarItemLocation::PrimaryRight
227        } else {
228            ToolbarItemLocation::Hidden
229        }
230    }
231}
232
233impl Render for ProposedChangesEditorToolbar {
234    fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
235        let editor = self.current_editor.clone();
236        Button::new("apply-changes", "Apply All").on_click(move |_, cx| {
237            if let Some(editor) = &editor {
238                editor.update(cx, |editor, cx| {
239                    editor.editor.update(cx, |editor, cx| {
240                        editor.apply_all_changes(cx);
241                    })
242                });
243            }
244        })
245    }
246}
247
248impl EventEmitter<ToolbarItemEvent> for ProposedChangesEditorToolbar {}
249
250impl ToolbarItemView for ProposedChangesEditorToolbar {
251    fn set_active_pane_item(
252        &mut self,
253        active_pane_item: Option<&dyn workspace::ItemHandle>,
254        _cx: &mut ViewContext<Self>,
255    ) -> workspace::ToolbarItemLocation {
256        self.current_editor =
257            active_pane_item.and_then(|item| item.downcast::<ProposedChangesEditor>());
258        self.get_toolbar_item_location()
259    }
260}
261
262impl BranchBufferSemanticsProvider {
263    fn to_base(
264        &self,
265        buffer: &Model<Buffer>,
266        positions: &[text::Anchor],
267        cx: &AppContext,
268    ) -> Option<Model<Buffer>> {
269        let base_buffer = buffer.read(cx).diff_base_buffer()?;
270        let version = base_buffer.read(cx).version();
271        if positions
272            .iter()
273            .any(|position| !version.observed(position.timestamp))
274        {
275            return None;
276        }
277        Some(base_buffer)
278    }
279}
280
281impl SemanticsProvider for BranchBufferSemanticsProvider {
282    fn hover(
283        &self,
284        buffer: &Model<Buffer>,
285        position: text::Anchor,
286        cx: &mut AppContext,
287    ) -> Option<Task<Vec<project::Hover>>> {
288        let buffer = self.to_base(buffer, &[position], cx)?;
289        self.0.hover(&buffer, position, cx)
290    }
291
292    fn inlay_hints(
293        &self,
294        buffer: Model<Buffer>,
295        range: Range<text::Anchor>,
296        cx: &mut AppContext,
297    ) -> Option<Task<anyhow::Result<Vec<project::InlayHint>>>> {
298        let buffer = self.to_base(&buffer, &[range.start, range.end], cx)?;
299        self.0.inlay_hints(buffer, range, cx)
300    }
301
302    fn resolve_inlay_hint(
303        &self,
304        hint: project::InlayHint,
305        buffer: Model<Buffer>,
306        server_id: lsp::LanguageServerId,
307        cx: &mut AppContext,
308    ) -> Option<Task<anyhow::Result<project::InlayHint>>> {
309        let buffer = self.to_base(&buffer, &[], cx)?;
310        self.0.resolve_inlay_hint(hint, buffer, server_id, cx)
311    }
312
313    fn supports_inlay_hints(&self, buffer: &Model<Buffer>, cx: &AppContext) -> bool {
314        if let Some(buffer) = self.to_base(&buffer, &[], cx) {
315            self.0.supports_inlay_hints(&buffer, cx)
316        } else {
317            false
318        }
319    }
320
321    fn document_highlights(
322        &self,
323        buffer: &Model<Buffer>,
324        position: text::Anchor,
325        cx: &mut AppContext,
326    ) -> Option<Task<gpui::Result<Vec<project::DocumentHighlight>>>> {
327        let buffer = self.to_base(&buffer, &[position], cx)?;
328        self.0.document_highlights(&buffer, position, cx)
329    }
330
331    fn definitions(
332        &self,
333        buffer: &Model<Buffer>,
334        position: text::Anchor,
335        kind: crate::GotoDefinitionKind,
336        cx: &mut AppContext,
337    ) -> Option<Task<gpui::Result<Vec<project::LocationLink>>>> {
338        let buffer = self.to_base(&buffer, &[position], cx)?;
339        self.0.definitions(&buffer, position, kind, cx)
340    }
341
342    fn range_for_rename(
343        &self,
344        _: &Model<Buffer>,
345        _: text::Anchor,
346        _: &mut AppContext,
347    ) -> Option<Task<gpui::Result<Option<Range<text::Anchor>>>>> {
348        None
349    }
350
351    fn perform_rename(
352        &self,
353        _: &Model<Buffer>,
354        _: text::Anchor,
355        _: String,
356        _: &mut AppContext,
357    ) -> Option<Task<gpui::Result<project::ProjectTransaction>>> {
358        None
359    }
360}