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,
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
164impl ProposedChangesEditorToolbar {
165 pub fn new() -> Self {
166 Self {
167 current_editor: None,
168 }
169 }
170
171 fn get_toolbar_item_location(&self) -> ToolbarItemLocation {
172 if self.current_editor.is_some() {
173 ToolbarItemLocation::PrimaryRight
174 } else {
175 ToolbarItemLocation::Hidden
176 }
177 }
178}
179
180impl Render for ProposedChangesEditorToolbar {
181 fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
182 let editor = self.current_editor.clone();
183 Button::new("apply-changes", "Apply All").on_click(move |_, cx| {
184 if let Some(editor) = &editor {
185 editor.update(cx, |editor, cx| {
186 editor.apply_all_changes(cx);
187 });
188 }
189 })
190 }
191}
192
193impl EventEmitter<ToolbarItemEvent> for ProposedChangesEditorToolbar {}
194
195impl ToolbarItemView for ProposedChangesEditorToolbar {
196 fn set_active_pane_item(
197 &mut self,
198 active_pane_item: Option<&dyn workspace::ItemHandle>,
199 _cx: &mut ViewContext<Self>,
200 ) -> workspace::ToolbarItemLocation {
201 self.current_editor =
202 active_pane_item.and_then(|item| item.downcast::<ProposedChangesEditor>());
203 self.get_toolbar_item_location()
204 }
205}