1use std::{ops::Range, sync::Arc};
2
3use feature_flags::{FeatureFlag, FeatureFlagAppExt as _};
4use gpui::{
5 Action, AppContext as _, Entity, EventEmitter, Focusable, NoAction, Subscription, WeakEntity,
6};
7use language::{Buffer, Capability, LanguageRegistry};
8use multi_buffer::{Anchor, ExcerptRange, MultiBuffer, PathKey};
9use project::Project;
10use rope::Point;
11use ui::{
12 App, Context, InteractiveElement as _, IntoElement as _, ParentElement as _, Render,
13 Styled as _, Window, div,
14};
15use workspace::{
16 ActivePaneDecorator, Item, ItemHandle, Pane, PaneGroup, SplitDirection, Workspace,
17};
18
19use crate::{Editor, EditorEvent};
20
21struct SplitDiffFeatureFlag;
22
23impl FeatureFlag for SplitDiffFeatureFlag {
24 const NAME: &'static str = "split-diff";
25
26 fn enabled_for_staff() -> bool {
27 true
28 }
29}
30
31#[derive(Clone, Copy, PartialEq, Eq, Action, Default)]
32#[action(namespace = editor)]
33struct SplitDiff;
34
35#[derive(Clone, Copy, PartialEq, Eq, Action, Default)]
36#[action(namespace = editor)]
37struct UnsplitDiff;
38
39pub struct SplittableEditor {
40 primary_editor: Entity<Editor>,
41 secondary: Option<SecondaryEditor>,
42 panes: PaneGroup,
43 workspace: WeakEntity<Workspace>,
44 _subscriptions: Vec<Subscription>,
45}
46
47struct SecondaryEditor {
48 editor: Entity<Editor>,
49 pane: Entity<Pane>,
50 has_latest_selection: bool,
51 _subscriptions: Vec<Subscription>,
52}
53
54impl SplittableEditor {
55 pub fn primary_editor(&self) -> &Entity<Editor> {
56 &self.primary_editor
57 }
58
59 pub fn last_selected_editor(&self) -> &Entity<Editor> {
60 if let Some(secondary) = &self.secondary
61 && secondary.has_latest_selection
62 {
63 &secondary.editor
64 } else {
65 &self.primary_editor
66 }
67 }
68
69 pub fn new_unsplit(
70 buffer: Entity<MultiBuffer>,
71 project: Entity<Project>,
72 workspace: Entity<Workspace>,
73 window: &mut Window,
74 cx: &mut Context<Self>,
75 ) -> Self {
76 let primary_editor =
77 cx.new(|cx| Editor::for_multibuffer(buffer, Some(project.clone()), window, cx));
78 let pane = cx.new(|cx| {
79 let mut pane = Pane::new(
80 workspace.downgrade(),
81 project,
82 Default::default(),
83 None,
84 NoAction.boxed_clone(),
85 true,
86 window,
87 cx,
88 );
89 pane.set_should_display_tab_bar(|_, _| false);
90 pane.add_item(primary_editor.boxed_clone(), true, true, None, window, cx);
91 pane
92 });
93 let panes = PaneGroup::new(pane);
94 // TODO(split-diff) we might want to tag editor events with whether they came from primary/secondary
95 let subscriptions =
96 vec![
97 cx.subscribe(&primary_editor, |this, _, event: &EditorEvent, cx| {
98 if let EditorEvent::SelectionsChanged { .. } = event
99 && let Some(secondary) = &mut this.secondary
100 {
101 secondary.has_latest_selection = false;
102 }
103 cx.emit(event.clone())
104 }),
105 ];
106
107 window.defer(cx, {
108 let workspace = workspace.downgrade();
109 let primary_editor = primary_editor.downgrade();
110 move |window, cx| {
111 workspace
112 .update(cx, |workspace, cx| {
113 primary_editor.update(cx, |editor, cx| {
114 editor.added_to_workspace(workspace, window, cx);
115 })
116 })
117 .ok();
118 }
119 });
120 Self {
121 primary_editor,
122 secondary: None,
123 panes,
124 workspace: workspace.downgrade(),
125 _subscriptions: subscriptions,
126 }
127 }
128
129 fn split(&mut self, _: &SplitDiff, window: &mut Window, cx: &mut Context<Self>) {
130 if !cx.has_flag::<SplitDiffFeatureFlag>() {
131 return;
132 }
133 if self.secondary.is_some() {
134 return;
135 }
136 let Some(workspace) = self.workspace.upgrade() else {
137 return;
138 };
139 let project = workspace.read(cx).project().clone();
140
141 // FIXME
142 // - have to subscribe to the diffs to update the base text buffers (and handle language changed I think?)
143
144 let secondary_editor = cx.new(|cx| {
145 let multibuffer = cx.new(|cx| {
146 let mut multibuffer = MultiBuffer::new(Capability::ReadOnly);
147 multibuffer.set_all_diff_hunks_expanded(cx);
148 multibuffer
149 });
150 Editor::for_multibuffer(multibuffer, Some(project.clone()), window, cx)
151 });
152 let secondary_pane = cx.new(|cx| {
153 let mut pane = Pane::new(
154 workspace.downgrade(),
155 workspace.read(cx).project().clone(),
156 Default::default(),
157 None,
158 NoAction.boxed_clone(),
159 true,
160 window,
161 cx,
162 );
163 pane.set_should_display_tab_bar(|_, _| false);
164 pane.add_item(
165 ItemHandle::boxed_clone(&secondary_editor),
166 false,
167 false,
168 None,
169 window,
170 cx,
171 );
172 pane
173 });
174
175 let subscriptions =
176 vec![
177 cx.subscribe(&secondary_editor, |this, _, event: &EditorEvent, cx| {
178 if let EditorEvent::SelectionsChanged { .. } = event
179 && let Some(secondary) = &mut this.secondary
180 {
181 secondary.has_latest_selection = true;
182 }
183 cx.emit(event.clone())
184 }),
185 ];
186 let mut secondary = SecondaryEditor {
187 editor: secondary_editor,
188 pane: secondary_pane.clone(),
189 has_latest_selection: false,
190 _subscriptions: subscriptions,
191 };
192 self.primary_editor.update(cx, |editor, cx| {
193 editor.buffer().update(cx, |primary_multibuffer, cx| {
194 primary_multibuffer.set_show_deleted_hunks(false, cx);
195 let paths = primary_multibuffer.paths().collect::<Vec<_>>();
196 for path in paths {
197 secondary.sync_path_excerpts(
198 path,
199 primary_multibuffer,
200 project.read(cx).languages().clone(),
201 cx,
202 );
203 }
204 })
205 });
206 self.secondary = Some(secondary);
207
208 let primary_pane = self.panes.first_pane();
209 self.panes
210 .split(&primary_pane, &secondary_pane, SplitDirection::Left)
211 .unwrap();
212 cx.notify();
213 }
214
215 fn unsplit(&mut self, _: &UnsplitDiff, _: &mut Window, cx: &mut Context<Self>) {
216 let Some(secondary) = self.secondary.take() else {
217 return;
218 };
219 self.panes.remove(&secondary.pane).unwrap();
220 self.primary_editor.update(cx, |primary, cx| {
221 primary.buffer().update(cx, |buffer, cx| {
222 buffer.set_show_deleted_hunks(true, cx);
223 });
224 });
225 cx.notify();
226 }
227
228 pub fn added_to_workspace(
229 &mut self,
230 workspace: &mut Workspace,
231 window: &mut Window,
232 cx: &mut Context<Self>,
233 ) {
234 self.workspace = workspace.weak_handle();
235 self.primary_editor.update(cx, |primary_editor, cx| {
236 primary_editor.added_to_workspace(workspace, window, cx);
237 });
238 if let Some(secondary) = &self.secondary {
239 secondary.editor.update(cx, |secondary_editor, cx| {
240 secondary_editor.added_to_workspace(workspace, window, cx);
241 });
242 }
243 }
244
245 pub fn set_excerpts_for_path(
246 &mut self,
247 path: PathKey,
248 buffer: Entity<Buffer>,
249 ranges: impl IntoIterator<Item = Range<Point>>,
250 context_line_count: u32,
251 cx: &mut Context<Self>,
252 ) -> (Vec<Range<Anchor>>, bool) {
253 self.primary_editor.update(cx, |editor, cx| {
254 editor.buffer().update(cx, |primary_multibuffer, cx| {
255 let (anchors, added_a_new_excerpt) = primary_multibuffer.set_excerpts_for_path(
256 path.clone(),
257 buffer,
258 ranges,
259 context_line_count,
260 cx,
261 );
262 if let Some(secondary) = &mut self.secondary
263 && let Some(languages) = self
264 .workspace
265 .update(cx, |workspace, cx| {
266 workspace.project().read(cx).languages().clone()
267 })
268 .ok()
269 {
270 secondary.sync_path_excerpts(path, primary_multibuffer, languages, cx);
271 }
272 (anchors, added_a_new_excerpt)
273 })
274 })
275 }
276}
277
278impl EventEmitter<EditorEvent> for SplittableEditor {}
279impl Focusable for SplittableEditor {
280 fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
281 self.primary_editor.read(cx).focus_handle(cx)
282 }
283}
284
285impl Render for SplittableEditor {
286 fn render(
287 &mut self,
288 window: &mut ui::Window,
289 cx: &mut ui::Context<Self>,
290 ) -> impl ui::IntoElement {
291 let inner = if self.secondary.is_none() {
292 self.primary_editor.clone().into_any_element()
293 } else if let Some(active) = self.panes.panes().into_iter().next() {
294 self.panes
295 .render(
296 None,
297 &ActivePaneDecorator::new(active, &self.workspace),
298 window,
299 cx,
300 )
301 .into_any_element()
302 } else {
303 div().into_any_element()
304 };
305 div()
306 .id("splittable-editor")
307 .on_action(cx.listener(Self::split))
308 .on_action(cx.listener(Self::unsplit))
309 .size_full()
310 .child(inner)
311 }
312}
313
314impl SecondaryEditor {
315 fn sync_path_excerpts(
316 &mut self,
317 path_key: PathKey,
318 primary_multibuffer: &mut MultiBuffer,
319 languages: Arc<LanguageRegistry>,
320 cx: &mut App,
321 ) {
322 let excerpt_id = primary_multibuffer
323 .excerpts_for_path(&path_key)
324 .next()
325 .unwrap();
326 let primary_multibuffer_snapshot = primary_multibuffer.snapshot(cx);
327 let main_buffer = primary_multibuffer_snapshot
328 .buffer_for_excerpt(excerpt_id)
329 .unwrap();
330 let diff = primary_multibuffer
331 .diff_for(main_buffer.remote_id())
332 .unwrap();
333 let diff = diff.read(cx).snapshot(cx);
334 let base_text_buffer = self
335 .editor
336 .update(cx, |editor, cx| {
337 editor.buffer().update(cx, |secondary_multibuffer, cx| {
338 let excerpt_id = secondary_multibuffer.excerpts_for_path(&path_key).next()?;
339 let secondary_buffer_snapshot = secondary_multibuffer.snapshot(cx);
340 let buffer = secondary_buffer_snapshot
341 .buffer_for_excerpt(excerpt_id)
342 .unwrap();
343 Some(secondary_multibuffer.buffer(buffer.remote_id()).unwrap())
344 })
345 })
346 .unwrap_or_else(|| {
347 cx.new(|cx| {
348 let base_text = diff.base_text();
349 let mut buffer = Buffer::local_normalized(
350 base_text.as_rope().clone(),
351 base_text.line_ending(),
352 cx,
353 );
354 buffer.set_language(base_text.language().cloned(), cx);
355 buffer.set_language_registry(languages);
356 buffer
357 })
358 });
359 let base_text_buffer_snapshot = base_text_buffer.read(cx).snapshot();
360 let new = primary_multibuffer
361 .excerpts_for_buffer(main_buffer.remote_id(), cx)
362 .into_iter()
363 .map(|(_, excerpt_range)| {
364 let point_to_base_text_point = |point: Point| {
365 let row = diff.row_to_base_text_row(point.row, main_buffer);
366 let column = diff.base_text().line_len(row);
367 Point::new(row, column)
368 };
369 let primary = excerpt_range.primary.to_point(main_buffer);
370 let context = excerpt_range.context.to_point(main_buffer);
371 ExcerptRange {
372 primary: point_to_base_text_point(primary.start)
373 ..point_to_base_text_point(primary.end),
374 context: point_to_base_text_point(context.start)
375 ..point_to_base_text_point(context.end),
376 }
377 })
378 .collect();
379
380 self.editor.update(cx, |editor, cx| {
381 editor.buffer().update(cx, |buffer, cx| {
382 buffer.update_path_excerpts(
383 path_key,
384 base_text_buffer,
385 &base_text_buffer_snapshot,
386 new,
387 cx,
388 )
389 })
390 });
391 }
392}