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