🔜 TODO: - [x] Add a utility pane to the left and right edges of the workspace - [x] Add a maximize button to the left and right side of the pane - [x] Add a new agents pane - [x] Add a feature flag turning these off POV: You're working agentically <img width="354" height="606" alt="Screenshot 2025-12-13 at 11 50 14 PM" src="https://github.com/user-attachments/assets/ce5469f9-adc2-47f5-a978-a48bf992f5f7" /> Release Notes: - N/A --------- Co-authored-by: Nathan Sobo <nathan@zed.dev> Co-authored-by: Zed <zed@zed.dev>
268 lines
8.5 KiB
Rust
268 lines
8.5 KiB
Rust
use feature_flags::{FeatureFlag, FeatureFlagAppExt as _};
|
|
use gpui::{
|
|
Action, AppContext as _, Entity, EventEmitter, Focusable, NoAction, Subscription, WeakEntity,
|
|
};
|
|
use multi_buffer::{MultiBuffer, MultiBufferFilterMode};
|
|
use project::Project;
|
|
use ui::{
|
|
App, Context, InteractiveElement as _, IntoElement as _, ParentElement as _, Render,
|
|
Styled as _, Window, div,
|
|
};
|
|
use workspace::{
|
|
ActivePaneDecorator, Item, ItemHandle, Pane, PaneGroup, SplitDirection, Workspace,
|
|
};
|
|
|
|
use crate::{Editor, EditorEvent};
|
|
|
|
struct SplitDiffFeatureFlag;
|
|
|
|
impl FeatureFlag for SplitDiffFeatureFlag {
|
|
const NAME: &'static str = "split-diff";
|
|
|
|
fn enabled_for_staff() -> bool {
|
|
true
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, PartialEq, Eq, Action, Default)]
|
|
#[action(namespace = editor)]
|
|
struct SplitDiff;
|
|
|
|
#[derive(Clone, Copy, PartialEq, Eq, Action, Default)]
|
|
#[action(namespace = editor)]
|
|
struct UnsplitDiff;
|
|
|
|
pub struct SplittableEditor {
|
|
primary_editor: Entity<Editor>,
|
|
secondary: Option<SecondaryEditor>,
|
|
panes: PaneGroup,
|
|
workspace: WeakEntity<Workspace>,
|
|
_subscriptions: Vec<Subscription>,
|
|
}
|
|
|
|
struct SecondaryEditor {
|
|
editor: Entity<Editor>,
|
|
pane: Entity<Pane>,
|
|
has_latest_selection: bool,
|
|
_subscriptions: Vec<Subscription>,
|
|
}
|
|
|
|
impl SplittableEditor {
|
|
pub fn primary_editor(&self) -> &Entity<Editor> {
|
|
&self.primary_editor
|
|
}
|
|
|
|
pub fn last_selected_editor(&self) -> &Entity<Editor> {
|
|
if let Some(secondary) = &self.secondary
|
|
&& secondary.has_latest_selection
|
|
{
|
|
&secondary.editor
|
|
} else {
|
|
&self.primary_editor
|
|
}
|
|
}
|
|
|
|
pub fn new_unsplit(
|
|
buffer: Entity<MultiBuffer>,
|
|
project: Entity<Project>,
|
|
workspace: Entity<Workspace>,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> Self {
|
|
let primary_editor =
|
|
cx.new(|cx| Editor::for_multibuffer(buffer, Some(project.clone()), window, cx));
|
|
let pane = cx.new(|cx| {
|
|
let mut pane = Pane::new(
|
|
workspace.downgrade(),
|
|
project,
|
|
Default::default(),
|
|
None,
|
|
NoAction.boxed_clone(),
|
|
true,
|
|
window,
|
|
cx,
|
|
);
|
|
pane.set_should_display_tab_bar(|_, _| false);
|
|
pane.add_item(primary_editor.boxed_clone(), true, true, None, window, cx);
|
|
pane
|
|
});
|
|
let panes = PaneGroup::new(pane);
|
|
// TODO(split-diff) we might want to tag editor events with whether they came from primary/secondary
|
|
let subscriptions =
|
|
vec![
|
|
cx.subscribe(&primary_editor, |this, _, event: &EditorEvent, cx| {
|
|
if let EditorEvent::SelectionsChanged { .. } = event
|
|
&& let Some(secondary) = &mut this.secondary
|
|
{
|
|
secondary.has_latest_selection = false;
|
|
}
|
|
cx.emit(event.clone())
|
|
}),
|
|
];
|
|
|
|
window.defer(cx, {
|
|
let workspace = workspace.downgrade();
|
|
let primary_editor = primary_editor.downgrade();
|
|
move |window, cx| {
|
|
workspace
|
|
.update(cx, |workspace, cx| {
|
|
primary_editor.update(cx, |editor, cx| {
|
|
editor.added_to_workspace(workspace, window, cx);
|
|
})
|
|
})
|
|
.ok();
|
|
}
|
|
});
|
|
Self {
|
|
primary_editor,
|
|
secondary: None,
|
|
panes,
|
|
workspace: workspace.downgrade(),
|
|
_subscriptions: subscriptions,
|
|
}
|
|
}
|
|
|
|
fn split(&mut self, _: &SplitDiff, window: &mut Window, cx: &mut Context<Self>) {
|
|
if !cx.has_flag::<SplitDiffFeatureFlag>() {
|
|
return;
|
|
}
|
|
if self.secondary.is_some() {
|
|
return;
|
|
}
|
|
let Some(workspace) = self.workspace.upgrade() else {
|
|
return;
|
|
};
|
|
let project = workspace.read(cx).project().clone();
|
|
let follower = self.primary_editor.update(cx, |primary, cx| {
|
|
primary.buffer().update(cx, |buffer, cx| {
|
|
let follower = buffer.get_or_create_follower(cx);
|
|
buffer.set_filter_mode(Some(MultiBufferFilterMode::KeepInsertions));
|
|
follower
|
|
})
|
|
});
|
|
follower.update(cx, |follower, _| {
|
|
follower.set_filter_mode(Some(MultiBufferFilterMode::KeepDeletions));
|
|
});
|
|
let secondary_editor = workspace.update(cx, |workspace, cx| {
|
|
cx.new(|cx| {
|
|
let mut editor = Editor::for_multibuffer(follower, Some(project), window, cx);
|
|
// TODO(split-diff) this should be at the multibuffer level
|
|
editor.set_use_base_text_line_numbers(true, cx);
|
|
editor.added_to_workspace(workspace, window, cx);
|
|
editor
|
|
})
|
|
});
|
|
let secondary_pane = cx.new(|cx| {
|
|
let mut pane = Pane::new(
|
|
workspace.downgrade(),
|
|
workspace.read(cx).project().clone(),
|
|
Default::default(),
|
|
None,
|
|
NoAction.boxed_clone(),
|
|
true,
|
|
window,
|
|
cx,
|
|
);
|
|
pane.set_should_display_tab_bar(|_, _| false);
|
|
pane.add_item(
|
|
ItemHandle::boxed_clone(&secondary_editor),
|
|
false,
|
|
false,
|
|
None,
|
|
window,
|
|
cx,
|
|
);
|
|
pane
|
|
});
|
|
|
|
let subscriptions =
|
|
vec![
|
|
cx.subscribe(&secondary_editor, |this, _, event: &EditorEvent, cx| {
|
|
if let EditorEvent::SelectionsChanged { .. } = event
|
|
&& let Some(secondary) = &mut this.secondary
|
|
{
|
|
secondary.has_latest_selection = true;
|
|
}
|
|
cx.emit(event.clone())
|
|
}),
|
|
];
|
|
self.secondary = Some(SecondaryEditor {
|
|
editor: secondary_editor,
|
|
pane: secondary_pane.clone(),
|
|
has_latest_selection: false,
|
|
_subscriptions: subscriptions,
|
|
});
|
|
let primary_pane = self.panes.first_pane();
|
|
self.panes
|
|
.split(&primary_pane, &secondary_pane, SplitDirection::Left, cx)
|
|
.unwrap();
|
|
cx.notify();
|
|
}
|
|
|
|
fn unsplit(&mut self, _: &UnsplitDiff, _: &mut Window, cx: &mut Context<Self>) {
|
|
let Some(secondary) = self.secondary.take() else {
|
|
return;
|
|
};
|
|
self.panes.remove(&secondary.pane, cx).unwrap();
|
|
self.primary_editor.update(cx, |primary, cx| {
|
|
primary.buffer().update(cx, |buffer, _| {
|
|
buffer.set_filter_mode(None);
|
|
});
|
|
});
|
|
cx.notify();
|
|
}
|
|
|
|
pub fn added_to_workspace(
|
|
&mut self,
|
|
workspace: &mut Workspace,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
self.workspace = workspace.weak_handle();
|
|
self.primary_editor.update(cx, |primary_editor, cx| {
|
|
primary_editor.added_to_workspace(workspace, window, cx);
|
|
});
|
|
if let Some(secondary) = &self.secondary {
|
|
secondary.editor.update(cx, |secondary_editor, cx| {
|
|
secondary_editor.added_to_workspace(workspace, window, cx);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
impl EventEmitter<EditorEvent> for SplittableEditor {}
|
|
impl Focusable for SplittableEditor {
|
|
fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
|
|
self.primary_editor.read(cx).focus_handle(cx)
|
|
}
|
|
}
|
|
|
|
impl Render for SplittableEditor {
|
|
fn render(
|
|
&mut self,
|
|
window: &mut ui::Window,
|
|
cx: &mut ui::Context<Self>,
|
|
) -> impl ui::IntoElement {
|
|
let inner = if self.secondary.is_none() {
|
|
self.primary_editor.clone().into_any_element()
|
|
} else if let Some(active) = self.panes.panes().into_iter().next() {
|
|
self.panes
|
|
.render(
|
|
None,
|
|
&ActivePaneDecorator::new(active, &self.workspace),
|
|
window,
|
|
cx,
|
|
)
|
|
.into_any_element()
|
|
} else {
|
|
div().into_any_element()
|
|
};
|
|
div()
|
|
.id("splittable-editor")
|
|
.on_action(cx.listener(Self::split))
|
|
.on_action(cx.listener(Self::unsplit))
|
|
.size_full()
|
|
.child(inner)
|
|
}
|
|
}
|