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<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 fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
164 self.editor.update(cx, |editor, cx| {
165 Item::added_to_workspace(editor, workspace, cx)
166 });
167 }
168
169 fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
170 self.editor.update(cx, Item::deactivated);
171 }
172
173 fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
174 self.editor
175 .update(cx, |editor, cx| Item::navigate(editor, data, cx))
176 }
177
178 fn set_nav_history(
179 &mut self,
180 nav_history: workspace::ItemNavHistory,
181 cx: &mut ViewContext<Self>,
182 ) {
183 self.editor.update(cx, |editor, cx| {
184 Item::set_nav_history(editor, nav_history, cx)
185 });
186 }
187}
188
189impl ProposedChangesEditorToolbar {
190 pub fn new() -> Self {
191 Self {
192 current_editor: None,
193 }
194 }
195
196 fn get_toolbar_item_location(&self) -> ToolbarItemLocation {
197 if self.current_editor.is_some() {
198 ToolbarItemLocation::PrimaryRight
199 } else {
200 ToolbarItemLocation::Hidden
201 }
202 }
203}
204
205impl Render for ProposedChangesEditorToolbar {
206 fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
207 let editor = self.current_editor.clone();
208 Button::new("apply-changes", "Apply All").on_click(move |_, cx| {
209 if let Some(editor) = &editor {
210 editor.update(cx, |editor, cx| {
211 editor.apply_all_changes(cx);
212 });
213 }
214 })
215 }
216}
217
218impl EventEmitter<ToolbarItemEvent> for ProposedChangesEditorToolbar {}
219
220impl ToolbarItemView for ProposedChangesEditorToolbar {
221 fn set_active_pane_item(
222 &mut self,
223 active_pane_item: Option<&dyn workspace::ItemHandle>,
224 _cx: &mut ViewContext<Self>,
225 ) -> workspace::ToolbarItemLocation {
226 self.current_editor =
227 active_pane_item.and_then(|item| item.downcast::<ProposedChangesEditor>());
228 self.get_toolbar_item_location()
229 }
230}