1use feature_flags::{FeatureFlag, FeatureFlagAppExt as _};
2use gpui::{
3 Action, AppContext as _, Entity, EventEmitter, Focusable, NoAction, Subscription, WeakEntity,
4};
5use multi_buffer::{MultiBuffer, MultiBufferFilterMode};
6use project::Project;
7use ui::{
8 App, Context, InteractiveElement as _, IntoElement as _, ParentElement as _, Render,
9 Styled as _, Window, div,
10};
11use workspace::{
12 ActivePaneDecorator, Item, ItemHandle, Pane, PaneGroup, SplitDirection, Workspace,
13};
14
15use crate::{Editor, EditorEvent};
16
17struct SplitDiffFeatureFlag;
18
19impl FeatureFlag for SplitDiffFeatureFlag {
20 const NAME: &'static str = "split-diff";
21
22 fn enabled_for_staff() -> bool {
23 true
24 }
25}
26
27#[derive(Clone, Copy, PartialEq, Eq, Action, Default)]
28#[action(namespace = editor)]
29struct SplitDiff;
30
31#[derive(Clone, Copy, PartialEq, Eq, Action, Default)]
32#[action(namespace = editor)]
33struct UnsplitDiff;
34
35pub struct SplittableEditor {
36 primary_editor: Entity<Editor>,
37 secondary: Option<SecondaryEditor>,
38 panes: PaneGroup,
39 workspace: WeakEntity<Workspace>,
40 _subscriptions: Vec<Subscription>,
41}
42
43struct SecondaryEditor {
44 editor: Entity<Editor>,
45 pane: Entity<Pane>,
46 has_latest_selection: bool,
47 _subscriptions: Vec<Subscription>,
48}
49
50impl SplittableEditor {
51 pub fn primary_editor(&self) -> &Entity<Editor> {
52 &self.primary_editor
53 }
54
55 pub fn last_selected_editor(&self) -> &Entity<Editor> {
56 if let Some(secondary) = &self.secondary
57 && secondary.has_latest_selection
58 {
59 &secondary.editor
60 } else {
61 &self.primary_editor
62 }
63 }
64
65 pub fn new_unsplit(
66 buffer: Entity<MultiBuffer>,
67 project: Entity<Project>,
68 workspace: Entity<Workspace>,
69 window: &mut Window,
70 cx: &mut Context<Self>,
71 ) -> Self {
72 let primary_editor =
73 cx.new(|cx| Editor::for_multibuffer(buffer, Some(project.clone()), window, cx));
74 let pane = cx.new(|cx| {
75 let mut pane = Pane::new(
76 workspace.downgrade(),
77 project,
78 Default::default(),
79 None,
80 NoAction.boxed_clone(),
81 true,
82 window,
83 cx,
84 );
85 pane.set_should_display_tab_bar(|_, _| false);
86 pane.add_item(primary_editor.boxed_clone(), true, true, None, window, cx);
87 pane
88 });
89 let panes = PaneGroup::new(pane);
90 // TODO(split-diff) we might want to tag editor events with whether they came from primary/secondary
91 let subscriptions =
92 vec![
93 cx.subscribe(&primary_editor, |this, _, event: &EditorEvent, cx| {
94 if let EditorEvent::SelectionsChanged { .. } = event
95 && let Some(secondary) = &mut this.secondary
96 {
97 secondary.has_latest_selection = false;
98 }
99 cx.emit(event.clone())
100 }),
101 ];
102
103 window.defer(cx, {
104 let workspace = workspace.downgrade();
105 let primary_editor = primary_editor.downgrade();
106 move |window, cx| {
107 workspace
108 .update(cx, |workspace, cx| {
109 primary_editor.update(cx, |editor, cx| {
110 editor.added_to_workspace(workspace, window, cx);
111 })
112 })
113 .ok();
114 }
115 });
116 Self {
117 primary_editor,
118 secondary: None,
119 panes,
120 workspace: workspace.downgrade(),
121 _subscriptions: subscriptions,
122 }
123 }
124
125 fn split(&mut self, _: &SplitDiff, window: &mut Window, cx: &mut Context<Self>) {
126 if !cx.has_flag::<SplitDiffFeatureFlag>() {
127 return;
128 }
129 if self.secondary.is_some() {
130 return;
131 }
132 let Some(workspace) = self.workspace.upgrade() else {
133 return;
134 };
135 let project = workspace.read(cx).project().clone();
136 let follower = self.primary_editor.update(cx, |primary, cx| {
137 primary.buffer().update(cx, |buffer, cx| {
138 let follower = buffer.get_or_create_follower(cx);
139 buffer.set_filter_mode(Some(MultiBufferFilterMode::KeepInsertions));
140 follower
141 })
142 });
143 follower.update(cx, |follower, _| {
144 follower.set_filter_mode(Some(MultiBufferFilterMode::KeepDeletions));
145 });
146 let secondary_editor = workspace.update(cx, |workspace, cx| {
147 cx.new(|cx| {
148 let mut editor = Editor::for_multibuffer(follower, Some(project), window, cx);
149 // TODO(split-diff) this should be at the multibuffer level
150 editor.set_use_base_text_line_numbers(true, cx);
151 editor.added_to_workspace(workspace, window, cx);
152 editor
153 })
154 });
155 let secondary_pane = cx.new(|cx| {
156 let mut pane = Pane::new(
157 workspace.downgrade(),
158 workspace.read(cx).project().clone(),
159 Default::default(),
160 None,
161 NoAction.boxed_clone(),
162 true,
163 window,
164 cx,
165 );
166 pane.set_should_display_tab_bar(|_, _| false);
167 pane.add_item(
168 ItemHandle::boxed_clone(&secondary_editor),
169 false,
170 false,
171 None,
172 window,
173 cx,
174 );
175 pane
176 });
177
178 let subscriptions =
179 vec![
180 cx.subscribe(&secondary_editor, |this, _, event: &EditorEvent, cx| {
181 if let EditorEvent::SelectionsChanged { .. } = event
182 && let Some(secondary) = &mut this.secondary
183 {
184 secondary.has_latest_selection = true;
185 }
186 cx.emit(event.clone())
187 }),
188 ];
189 self.secondary = Some(SecondaryEditor {
190 editor: secondary_editor,
191 pane: secondary_pane.clone(),
192 has_latest_selection: false,
193 _subscriptions: subscriptions,
194 });
195 let primary_pane = self.panes.first_pane();
196 self.panes
197 .split(&primary_pane, &secondary_pane, SplitDirection::Left)
198 .unwrap();
199 cx.notify();
200 }
201
202 fn unsplit(&mut self, _: &UnsplitDiff, _: &mut Window, cx: &mut Context<Self>) {
203 let Some(secondary) = self.secondary.take() else {
204 return;
205 };
206 self.panes.remove(&secondary.pane).unwrap();
207 self.primary_editor.update(cx, |primary, cx| {
208 primary.buffer().update(cx, |buffer, _| {
209 buffer.set_filter_mode(None);
210 });
211 });
212 cx.notify();
213 }
214
215 pub fn added_to_workspace(
216 &mut self,
217 workspace: &mut Workspace,
218 window: &mut Window,
219 cx: &mut Context<Self>,
220 ) {
221 self.workspace = workspace.weak_handle();
222 self.primary_editor.update(cx, |primary_editor, cx| {
223 primary_editor.added_to_workspace(workspace, window, cx);
224 });
225 if let Some(secondary) = &self.secondary {
226 secondary.editor.update(cx, |secondary_editor, cx| {
227 secondary_editor.added_to_workspace(workspace, window, cx);
228 });
229 }
230 }
231}
232
233impl EventEmitter<EditorEvent> for SplittableEditor {}
234impl Focusable for SplittableEditor {
235 fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
236 self.primary_editor.read(cx).focus_handle(cx)
237 }
238}
239
240impl Render for SplittableEditor {
241 fn render(
242 &mut self,
243 window: &mut ui::Window,
244 cx: &mut ui::Context<Self>,
245 ) -> impl ui::IntoElement {
246 let Some(active) = self.panes.panes().into_iter().next() else {
247 return div().into_any_element();
248 };
249 div()
250 .id("splittable-editor")
251 .on_action(cx.listener(Self::split))
252 .on_action(cx.listener(Self::unsplit))
253 .size_full()
254 .child(self.panes.render(
255 None,
256 &ActivePaneDecorator::new(active, &self.workspace),
257 window,
258 cx,
259 ))
260 .into_any_element()
261 }
262}