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