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}