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