Allow multiple docks to be visible at once and only let them contain panels (#2450)
Closes https://linear.app/zed-industries/issue/Z-1188/allow-docks-to-be-visible-on-left-bottom-right To prepare the way for the AI assistant, we want to revamp the way docks and panels work. In this PR, we replace the dock as it currently exists with 3 independent docks at the left, bottom, and right edge of the workspace. To replace the "expanded" dock, we will introduce a zoom feature that works on any pane and some panels. When showing a dock with an active panel that is zoomed, it will automatically appear zoomed. This replicates the expanded dock experience. If you unzoom, the panel will still be visible. ### Panels only We want to simplify these docks to only allow them to contain *panels*. By doing this, we don't need to give each dock a tab bar, minimizing clutter in the workspace. Each panel will remember its size, and the dock will adjust to that size when the panel is toggled. This will allow each panel to take up the amount of space that makes sense for its use case. There will be 3 kinds of panels: * Project panel: This currently lives in the left "sidebar", which this PR renames to the left dock. Users will be able to left click the icon and switch the project panel to the right dock. * Terminal panel: This is the primary user of the previous dock. Now all terminals will live in a terminal panel, which can be docked at the left, bottom, or right. This dock will contain tabs, but only for terminals. Terminals will still be able to be dragged into the workspace, but by default, the first new terminal will live in the terminal panel which lives in one of the 3 docks. * Feedback: Instead of opening a pane, let's explore making this a panel instead. ### Status bar icons A nice feature of this change is that it simplifies our status bar by eliminating the dock button. We will only show icons for specific panels. Panel in the left dock will appear at the left side of the status bar. Panels in the right dock on the far right, panels in the bottom will appear on the right, to the immediate left of the buttons for right panels. Left clicking any panel button will allow it to be redocked to any valid location for that panel. ### Paving the way for the AI assistant I'm envisioning the assistant living in the far right panel by default. So in the stock config, you'll have the project panel on the left, terminal on the bottom, assistant on the right, and code in the middle. Let's fucking go!
This commit is contained in:
Generated
+1
@@ -4889,6 +4889,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"client",
|
||||
"context_menu",
|
||||
"db",
|
||||
"drag_and_drop",
|
||||
"editor",
|
||||
"futures 0.3.28",
|
||||
|
||||
@@ -39,8 +39,8 @@
|
||||
{
|
||||
"context": "Workspace",
|
||||
"bindings": {
|
||||
"cmd-\\": "workspace::ToggleLeftSidebar",
|
||||
"cmd-k cmd-b": "workspace::ToggleLeftSidebar",
|
||||
"cmd-\\": "workspace::ToggleLeftDock",
|
||||
"cmd-k cmd-b": "workspace::ToggleLeftDock",
|
||||
"cmd-t": "file_finder::Toggle",
|
||||
"cmd-shift-r": "project_symbols::Toggle"
|
||||
}
|
||||
@@ -62,9 +62,5 @@
|
||||
"ctrl-f": "project_panel::ExpandSelectedEntry",
|
||||
"ctrl-shift-c": "project_panel::CopyPath"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Dock",
|
||||
"bindings": {}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -39,7 +39,8 @@
|
||||
"cmd-shift-n": "workspace::NewWindow",
|
||||
"cmd-o": "workspace::Open",
|
||||
"alt-cmd-o": "projects::OpenRecent",
|
||||
"ctrl-`": "workspace::NewTerminal"
|
||||
"ctrl-~": "workspace::NewTerminal",
|
||||
"ctrl-`": "terminal_panel::ToggleFocus"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -223,7 +224,8 @@
|
||||
"cmd-shift-g": "search::SelectPrevMatch",
|
||||
"alt-cmd-c": "search::ToggleCaseSensitive",
|
||||
"alt-cmd-w": "search::ToggleWholeWord",
|
||||
"alt-cmd-r": "search::ToggleRegex"
|
||||
"alt-cmd-r": "search::ToggleRegex",
|
||||
"shift-escape": "workspace::ToggleZoom"
|
||||
}
|
||||
},
|
||||
// Bindings from VS Code
|
||||
@@ -365,7 +367,7 @@
|
||||
"workspace::ActivatePane",
|
||||
8
|
||||
],
|
||||
"cmd-b": "workspace::ToggleLeftSidebar",
|
||||
"cmd-b": "workspace::ToggleLeftDock",
|
||||
"cmd-shift-f": "workspace::NewSearch",
|
||||
"cmd-k cmd-t": "theme_selector::Toggle",
|
||||
"cmd-k cmd-s": "zed::OpenKeymap",
|
||||
@@ -459,32 +461,6 @@
|
||||
"cmd-enter": "project_search::SearchInNew"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Workspace",
|
||||
"bindings": {
|
||||
"shift-escape": "dock::FocusDock"
|
||||
}
|
||||
},
|
||||
{
|
||||
"bindings": {
|
||||
"cmd-shift-k cmd-shift-right": "dock::AnchorDockRight",
|
||||
"cmd-shift-k cmd-shift-down": "dock::AnchorDockBottom",
|
||||
"cmd-shift-k cmd-shift-up": "dock::ExpandDock"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Pane",
|
||||
"bindings": {
|
||||
"cmd-escape": "dock::AddTabToDock"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Pane && docked",
|
||||
"bindings": {
|
||||
"shift-escape": "dock::HideDock",
|
||||
"cmd-escape": "dock::RemoveTabFromDock"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ProjectPanel",
|
||||
"bindings": {
|
||||
|
||||
@@ -68,15 +68,8 @@
|
||||
"cmd-shift-o": "file_finder::Toggle",
|
||||
"cmd-shift-a": "command_palette::Toggle",
|
||||
"cmd-alt-o": "project_symbols::Toggle",
|
||||
"cmd-1": "workspace::ToggleLeftSidebar",
|
||||
"cmd-6": "diagnostics::Deploy",
|
||||
"alt-f12": "dock::FocusDock"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Dock",
|
||||
"bindings": {
|
||||
"alt-f12": "dock::HideDock"
|
||||
"cmd-1": "workspace::ToggleLeftDock",
|
||||
"cmd-6": "diagnostics::Deploy"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -45,18 +45,11 @@
|
||||
{
|
||||
"context": "Workspace",
|
||||
"bindings": {
|
||||
"ctrl-`": "dock::FocusDock",
|
||||
"cmd-k cmd-b": "workspace::ToggleLeftSidebar",
|
||||
"cmd-k cmd-b": "workspace::ToggleLeftDock",
|
||||
"cmd-t": "file_finder::Toggle",
|
||||
"shift-cmd-r": "project_symbols::Toggle",
|
||||
// Currently busted: https://github.com/zed-industries/feedback/issues/898
|
||||
"ctrl-0": "project_panel::ToggleFocus"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Dock",
|
||||
"bindings": {
|
||||
"ctrl-`": "dock::HideDock"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
{
|
||||
"context": "Workspace",
|
||||
"bindings": {
|
||||
"cmd-alt-ctrl-d": "workspace::ToggleLeftSidebar",
|
||||
"cmd-alt-ctrl-d": "workspace::ToggleLeftDock",
|
||||
"cmd-t": "file_finder::Toggle",
|
||||
"cmd-shift-t": "project_symbols::Toggle"
|
||||
}
|
||||
@@ -83,9 +83,5 @@
|
||||
{
|
||||
"context": "ProjectPanel",
|
||||
"bindings": {}
|
||||
},
|
||||
{
|
||||
"context": "Dock",
|
||||
"bindings": {}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -72,7 +72,11 @@
|
||||
},
|
||||
"project_panel": {
|
||||
// Whether to show the git status in the project panel.
|
||||
"git_status": true
|
||||
"git_status": true,
|
||||
// Where to dock project panel. Can be 'left' or 'right'.
|
||||
"dock": "left",
|
||||
// Default width of the project panel.
|
||||
"default_width": 240
|
||||
},
|
||||
// Whether the screen sharing icon is shown in the os status bar.
|
||||
"show_call_status_icon": true,
|
||||
@@ -90,16 +94,6 @@
|
||||
// 4. Save when idle for a certain amount of time:
|
||||
// "autosave": { "after_delay": {"milliseconds": 500} },
|
||||
"autosave": "off",
|
||||
// Where to place the dock by default. This setting can take three
|
||||
// values:
|
||||
//
|
||||
// 1. Position the dock attached to the bottom of the workspace
|
||||
// "default_dock_anchor": "bottom"
|
||||
// 2. Position the dock to the right of the workspace like a side panel
|
||||
// "default_dock_anchor": "right"
|
||||
// 3. Position the dock full screen over the entire workspace"
|
||||
// "default_dock_anchor": "expanded"
|
||||
"default_dock_anchor": "bottom",
|
||||
// Whether or not to remove any trailing whitespace from lines of a buffer
|
||||
// before saving it.
|
||||
"remove_trailing_whitespace_on_save": true,
|
||||
@@ -190,6 +184,12 @@
|
||||
// }
|
||||
// }
|
||||
"shell": "system",
|
||||
// Where to dock terminals panel. Can be 'left', 'right', 'bottom'.
|
||||
"dock": "bottom",
|
||||
// Default width when the terminal is docked to the left or right.
|
||||
"default_width": 640,
|
||||
// Default height when the terminal is docked to the bottom.
|
||||
"default_height": 320,
|
||||
// What working directory to use when launching the terminal.
|
||||
// May take 4 values:
|
||||
// 1. Use the current file's project directory. Will Fallback to the
|
||||
|
||||
@@ -192,8 +192,7 @@ impl TestServer {
|
||||
languages: Arc::new(LanguageRegistry::test()),
|
||||
fs: fs.clone(),
|
||||
build_window_options: |_, _, _| Default::default(),
|
||||
initialize_workspace: |_, _, _| unimplemented!(),
|
||||
dock_default_item_factory: |_, _| None,
|
||||
initialize_workspace: |_, _, _, _| unimplemented!(),
|
||||
background_actions: || &[],
|
||||
});
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
titlebar: None,
|
||||
center: false,
|
||||
focus: false,
|
||||
show: true,
|
||||
kind: WindowKind::PopUp,
|
||||
is_movable: false,
|
||||
screen: Some(screen),
|
||||
|
||||
@@ -35,6 +35,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
titlebar: None,
|
||||
center: false,
|
||||
focus: false,
|
||||
show: true,
|
||||
kind: WindowKind::PopUp,
|
||||
is_movable: false,
|
||||
screen: Some(screen),
|
||||
|
||||
@@ -73,6 +73,7 @@ fn create_copilot_auth_window(
|
||||
titlebar: None,
|
||||
center: true,
|
||||
focus: true,
|
||||
show: true,
|
||||
kind: WindowKind::Normal,
|
||||
is_movable: true,
|
||||
screen: None,
|
||||
|
||||
@@ -66,8 +66,8 @@ impl View for CopilotButton {
|
||||
let style = theme
|
||||
.workspace
|
||||
.status_bar
|
||||
.sidebar_buttons
|
||||
.item
|
||||
.panel_buttons
|
||||
.button
|
||||
.style_for(state, active);
|
||||
|
||||
Flex::row()
|
||||
|
||||
@@ -1231,27 +1231,27 @@ mod tests {
|
||||
}
|
||||
|
||||
fn as_local(&self) -> Option<&dyn language::LocalFile> {
|
||||
todo!()
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn mtime(&self) -> SystemTime {
|
||||
todo!()
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn file_name<'a>(&'a self, _: &'a gpui::AppContext) -> &'a std::ffi::OsStr {
|
||||
todo!()
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn is_deleted(&self) -> bool {
|
||||
todo!()
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
todo!()
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn to_proto(&self) -> rpc::proto::File {
|
||||
todo!()
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,8 +39,8 @@ impl View for DeployFeedbackButton {
|
||||
let style = &theme
|
||||
.workspace
|
||||
.status_bar
|
||||
.sidebar_buttons
|
||||
.item
|
||||
.panel_buttons
|
||||
.button
|
||||
.style_for(state, active);
|
||||
|
||||
Svg::new("icons/feedback_16.svg")
|
||||
|
||||
@@ -380,7 +380,7 @@ mod tests {
|
||||
use gpui::{TestAppContext, ViewHandle};
|
||||
use menu::{Confirm, SelectNext};
|
||||
use serde_json::json;
|
||||
use workspace::{AppState, Pane, Workspace};
|
||||
use workspace::{AppState, Workspace};
|
||||
|
||||
#[ctor::ctor]
|
||||
fn init_logger() {
|
||||
@@ -1161,9 +1161,13 @@ mod tests {
|
||||
assert!(insertion_result.is_none(), "Pane id {pane_id} collision");
|
||||
}
|
||||
});
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
Pane::close_active_item(workspace, &workspace::CloseActiveItem, cx);
|
||||
});
|
||||
active_pane
|
||||
.update(cx, |pane, cx| {
|
||||
pane.close_active_item(&workspace::CloseActiveItem, cx)
|
||||
.unwrap()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
deterministic.run_until_parked();
|
||||
cx.read(|cx| {
|
||||
for pane in workspace.read(cx).panes() {
|
||||
|
||||
+96
-19
@@ -1460,27 +1460,13 @@ impl AppContext {
|
||||
self.views_metadata.remove(&(window_id, view_id));
|
||||
let mut view = self.views.remove(&(window_id, view_id)).unwrap();
|
||||
view.release(self);
|
||||
let change_focus_to = self.windows.get_mut(&window_id).and_then(|window| {
|
||||
if let Some(window) = self.windows.get_mut(&window_id) {
|
||||
window.parents.remove(&view_id);
|
||||
window
|
||||
.invalidation
|
||||
.get_or_insert_with(Default::default)
|
||||
.removed
|
||||
.push(view_id);
|
||||
if window.focused_view_id == Some(view_id) {
|
||||
Some(window.root_view().id())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(view_id) = change_focus_to {
|
||||
self.pending_effects
|
||||
.push_back(Effect::Focus(FocusEffect::View {
|
||||
window_id,
|
||||
view_id: Some(view_id),
|
||||
is_forced: false,
|
||||
}));
|
||||
}
|
||||
|
||||
self.pending_effects
|
||||
@@ -1717,8 +1703,69 @@ impl AppContext {
|
||||
if let Some(invalidation) = invalidation {
|
||||
let appearance = cx.window.platform_window.appearance();
|
||||
cx.invalidate(invalidation, appearance);
|
||||
if cx.layout(refreshing).log_err().is_some() {
|
||||
if let Some(old_parents) = cx.layout(refreshing).log_err() {
|
||||
updated_windows.insert(window_id);
|
||||
|
||||
if let Some(focused_view_id) = cx.focused_view_id() {
|
||||
let old_ancestors = std::iter::successors(
|
||||
Some(focused_view_id),
|
||||
|&view_id| old_parents.get(&view_id).copied(),
|
||||
)
|
||||
.collect::<HashSet<_>>();
|
||||
let new_ancestors =
|
||||
cx.ancestors(focused_view_id).collect::<HashSet<_>>();
|
||||
|
||||
// Notify the old ancestors of the focused view when they don't contain it anymore.
|
||||
for old_ancestor in old_ancestors.iter().copied() {
|
||||
if !new_ancestors.contains(&old_ancestor) {
|
||||
if let Some(mut view) =
|
||||
cx.views.remove(&(window_id, old_ancestor))
|
||||
{
|
||||
view.focus_out(
|
||||
focused_view_id,
|
||||
cx,
|
||||
old_ancestor,
|
||||
);
|
||||
cx.views
|
||||
.insert((window_id, old_ancestor), view);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Notify the new ancestors of the focused view if they contain it now.
|
||||
for new_ancestor in new_ancestors.iter().copied() {
|
||||
if !old_ancestors.contains(&new_ancestor) {
|
||||
if let Some(mut view) =
|
||||
cx.views.remove(&(window_id, new_ancestor))
|
||||
{
|
||||
view.focus_in(
|
||||
focused_view_id,
|
||||
cx,
|
||||
new_ancestor,
|
||||
);
|
||||
cx.views
|
||||
.insert((window_id, new_ancestor), view);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// When the previously-focused view has been dropped and
|
||||
// there isn't any pending focus, focus the root view.
|
||||
let root_view_id = cx.window.root_view().id();
|
||||
if focused_view_id != root_view_id
|
||||
&& !cx.views.contains_key(&(window_id, focused_view_id))
|
||||
&& !focus_effects.contains_key(&window_id)
|
||||
{
|
||||
focus_effects.insert(
|
||||
window_id,
|
||||
FocusEffect::View {
|
||||
window_id,
|
||||
view_id: Some(root_view_id),
|
||||
is_forced: false,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1895,9 +1942,27 @@ impl AppContext {
|
||||
fn handle_focus_effect(&mut self, effect: FocusEffect) {
|
||||
let window_id = effect.window_id();
|
||||
self.update_window(window_id, |cx| {
|
||||
// Ensure the newly-focused view still exists, otherwise focus
|
||||
// the root view instead.
|
||||
let focused_id = match effect {
|
||||
FocusEffect::View { view_id, .. } => view_id,
|
||||
FocusEffect::ViewParent { view_id, .. } => cx.ancestors(view_id).skip(1).next(),
|
||||
FocusEffect::View { view_id, .. } => {
|
||||
if let Some(view_id) = view_id {
|
||||
if cx.views.contains_key(&(window_id, view_id)) {
|
||||
Some(view_id)
|
||||
} else {
|
||||
Some(cx.root_view().id())
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
FocusEffect::ViewParent { view_id, .. } => Some(
|
||||
cx.window
|
||||
.parents
|
||||
.get(&view_id)
|
||||
.copied()
|
||||
.unwrap_or(cx.root_view().id()),
|
||||
),
|
||||
};
|
||||
|
||||
let focus_changed = cx.window.focused_view_id != focused_id;
|
||||
@@ -3802,6 +3867,12 @@ impl<T> PartialEq for ViewHandle<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> PartialEq<AnyViewHandle> for ViewHandle<T> {
|
||||
fn eq(&self, other: &AnyViewHandle) -> bool {
|
||||
self.window_id == other.window_id && self.view_id == other.view_id
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> PartialEq<WeakViewHandle<T>> for ViewHandle<T> {
|
||||
fn eq(&self, other: &WeakViewHandle<T>) -> bool {
|
||||
self.window_id == other.window_id && self.view_id == other.view_id
|
||||
@@ -3952,6 +4023,12 @@ impl Clone for AnyViewHandle {
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for AnyViewHandle {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.window_id == other.window_id && self.view_id == other.view_id
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> PartialEq<ViewHandle<T>> for AnyViewHandle {
|
||||
fn eq(&self, other: &ViewHandle<T>) -> bool {
|
||||
self.window_id == other.window_id && self.view_id == other.view_id
|
||||
@@ -4198,7 +4275,7 @@ impl<T> Hash for WeakViewHandle<T> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub struct AnyWeakViewHandle {
|
||||
window_id: usize,
|
||||
view_id: usize,
|
||||
|
||||
@@ -29,6 +29,7 @@ use sqlez::{
|
||||
};
|
||||
use std::{
|
||||
any::TypeId,
|
||||
mem,
|
||||
ops::{Deref, DerefMut, Range},
|
||||
};
|
||||
use util::ResultExt;
|
||||
@@ -890,7 +891,7 @@ impl<'a> WindowContext<'a> {
|
||||
Ok(element)
|
||||
}
|
||||
|
||||
pub(crate) fn layout(&mut self, refreshing: bool) -> Result<()> {
|
||||
pub(crate) fn layout(&mut self, refreshing: bool) -> Result<HashMap<usize, usize>> {
|
||||
let window_size = self.window.platform_window.content_size();
|
||||
let root_view_id = self.window.root_view().id();
|
||||
let mut rendered_root = self.window.rendered_views.remove(&root_view_id).unwrap();
|
||||
@@ -923,11 +924,11 @@ impl<'a> WindowContext<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
self.window.parents = new_parents;
|
||||
let old_parents = mem::replace(&mut self.window.parents, new_parents);
|
||||
self.window
|
||||
.rendered_views
|
||||
.insert(root_view_id, rendered_root);
|
||||
Ok(())
|
||||
Ok(old_parents)
|
||||
}
|
||||
|
||||
pub(crate) fn paint(&mut self) -> Result<Scene> {
|
||||
|
||||
+12
-14
@@ -187,25 +187,23 @@ pub trait Element<V: View>: 'static {
|
||||
Tooltip::new::<Tag, V>(id, text, action, style, self.into_any(), cx)
|
||||
}
|
||||
|
||||
fn with_resize_handle<Tag: 'static>(
|
||||
fn resizable(
|
||||
self,
|
||||
element_id: usize,
|
||||
side: Side,
|
||||
handle_size: f32,
|
||||
initial_size: f32,
|
||||
cx: &mut ViewContext<V>,
|
||||
side: HandleSide,
|
||||
size: f32,
|
||||
on_resize: impl 'static + FnMut(&mut V, f32, &mut ViewContext<V>),
|
||||
) -> Resizable<V>
|
||||
where
|
||||
Self: 'static + Sized,
|
||||
{
|
||||
Resizable::new::<Tag, V>(
|
||||
self.into_any(),
|
||||
element_id,
|
||||
side,
|
||||
handle_size,
|
||||
initial_size,
|
||||
cx,
|
||||
)
|
||||
Resizable::new(self.into_any(), side, size, on_resize)
|
||||
}
|
||||
|
||||
fn mouse<Tag>(self, region_id: usize) -> MouseEventHandler<Tag, V>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
MouseEventHandler::for_child(self.into_any(), region_id)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -990,7 +990,7 @@ mod tests {
|
||||
_: &mut V,
|
||||
_: &mut ViewContext<V>,
|
||||
) {
|
||||
todo!()
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn rect_for_text_range(
|
||||
@@ -1003,7 +1003,7 @@ mod tests {
|
||||
_: &V,
|
||||
_: &ViewContext<V>,
|
||||
) -> Option<RectF> {
|
||||
todo!()
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn debug(&self, _: RectF, _: &(), _: &(), _: &V, _: &ViewContext<V>) -> serde_json::Value {
|
||||
|
||||
@@ -32,10 +32,25 @@ pub struct MouseEventHandler<Tag: 'static, V: View> {
|
||||
/// Element which provides a render_child callback with a MouseState and paints a mouse
|
||||
/// region under (or above) it for easy mouse event handling.
|
||||
impl<Tag, V: View> MouseEventHandler<Tag, V> {
|
||||
pub fn new<D, F>(region_id: usize, cx: &mut ViewContext<V>, render_child: F) -> Self
|
||||
pub fn for_child(child: impl Element<V>, region_id: usize) -> Self {
|
||||
Self {
|
||||
child: child.into_any(),
|
||||
region_id,
|
||||
cursor_style: None,
|
||||
handlers: Default::default(),
|
||||
notify_on_hover: false,
|
||||
notify_on_click: false,
|
||||
hoverable: false,
|
||||
above: false,
|
||||
padding: Default::default(),
|
||||
_tag: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new<E, F>(region_id: usize, cx: &mut ViewContext<V>, render_child: F) -> Self
|
||||
where
|
||||
D: Element<V>,
|
||||
F: FnOnce(&mut MouseState, &mut ViewContext<V>) -> D,
|
||||
E: Element<V>,
|
||||
F: FnOnce(&mut MouseState, &mut ViewContext<V>) -> E,
|
||||
{
|
||||
let mut mouse_state = cx.mouse_state::<Tag>(region_id);
|
||||
let child = render_child(&mut mouse_state, cx).into_any();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::{cell::Cell, rc::Rc};
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use pathfinder_geometry::vector::{vec2f, Vector2F};
|
||||
use serde_json::json;
|
||||
@@ -7,25 +7,23 @@ use crate::{
|
||||
geometry::rect::RectF,
|
||||
platform::{CursorStyle, MouseButton},
|
||||
scene::MouseDrag,
|
||||
AnyElement, Axis, Element, ElementStateHandle, LayoutContext, MouseRegion, SceneBuilder, View,
|
||||
AnyElement, Axis, Element, LayoutContext, MouseRegion, SceneBuilder, SizeConstraint, View,
|
||||
ViewContext,
|
||||
};
|
||||
|
||||
use super::{ConstrainedBox, Hook};
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum Side {
|
||||
pub enum HandleSide {
|
||||
Top,
|
||||
Bottom,
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
impl Side {
|
||||
impl HandleSide {
|
||||
fn axis(&self) -> Axis {
|
||||
match self {
|
||||
Side::Left | Side::Right => Axis::Horizontal,
|
||||
Side::Top | Side::Bottom => Axis::Vertical,
|
||||
HandleSide::Left | HandleSide::Right => Axis::Horizontal,
|
||||
HandleSide::Top | HandleSide::Bottom => Axis::Vertical,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,8 +31,8 @@ impl Side {
|
||||
/// then top-to-bottom
|
||||
fn before_content(self) -> bool {
|
||||
match self {
|
||||
Side::Left | Side::Top => true,
|
||||
Side::Right | Side::Bottom => false,
|
||||
HandleSide::Left | HandleSide::Top => true,
|
||||
HandleSide::Right | HandleSide::Bottom => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,14 +53,14 @@ impl Side {
|
||||
|
||||
fn of_rect(&self, bounds: RectF, handle_size: f32) -> RectF {
|
||||
match self {
|
||||
Side::Top => RectF::new(bounds.origin(), vec2f(bounds.width(), handle_size)),
|
||||
Side::Left => RectF::new(bounds.origin(), vec2f(handle_size, bounds.height())),
|
||||
Side::Bottom => {
|
||||
HandleSide::Top => RectF::new(bounds.origin(), vec2f(bounds.width(), handle_size)),
|
||||
HandleSide::Left => RectF::new(bounds.origin(), vec2f(handle_size, bounds.height())),
|
||||
HandleSide::Bottom => {
|
||||
let mut origin = bounds.lower_left();
|
||||
origin.set_y(origin.y() - handle_size);
|
||||
RectF::new(origin, vec2f(bounds.width(), handle_size))
|
||||
}
|
||||
Side::Right => {
|
||||
HandleSide::Right => {
|
||||
let mut origin = bounds.upper_right();
|
||||
origin.set_x(origin.x() - handle_size);
|
||||
RectF::new(origin, vec2f(handle_size, bounds.height()))
|
||||
@@ -71,69 +69,44 @@ impl Side {
|
||||
}
|
||||
}
|
||||
|
||||
struct ResizeHandleState {
|
||||
actual_dimension: Cell<f32>,
|
||||
custom_dimension: Cell<f32>,
|
||||
pub struct Resizable<V: View> {
|
||||
child: AnyElement<V>,
|
||||
handle_side: HandleSide,
|
||||
handle_size: f32,
|
||||
on_resize: Rc<RefCell<dyn FnMut(&mut V, f32, &mut ViewContext<V>)>>,
|
||||
}
|
||||
|
||||
pub struct Resizable<V: View> {
|
||||
side: Side,
|
||||
handle_size: f32,
|
||||
child: AnyElement<V>,
|
||||
state: Rc<ResizeHandleState>,
|
||||
_state_handle: ElementStateHandle<Rc<ResizeHandleState>>,
|
||||
}
|
||||
const DEFAULT_HANDLE_SIZE: f32 = 4.0;
|
||||
|
||||
impl<V: View> Resizable<V> {
|
||||
pub fn new<Tag: 'static, T: View>(
|
||||
pub fn new(
|
||||
child: AnyElement<V>,
|
||||
element_id: usize,
|
||||
side: Side,
|
||||
handle_size: f32,
|
||||
initial_size: f32,
|
||||
cx: &mut ViewContext<V>,
|
||||
handle_side: HandleSide,
|
||||
size: f32,
|
||||
on_resize: impl 'static + FnMut(&mut V, f32, &mut ViewContext<V>),
|
||||
) -> Self {
|
||||
let state_handle = cx.element_state::<Tag, Rc<ResizeHandleState>>(
|
||||
element_id,
|
||||
Rc::new(ResizeHandleState {
|
||||
actual_dimension: Cell::new(initial_size),
|
||||
custom_dimension: Cell::new(initial_size),
|
||||
}),
|
||||
);
|
||||
|
||||
let state = state_handle.read(cx).clone();
|
||||
|
||||
let child = Hook::new({
|
||||
let constrained = ConstrainedBox::new(child);
|
||||
match side.axis() {
|
||||
Axis::Horizontal => constrained.with_max_width(state.custom_dimension.get()),
|
||||
Axis::Vertical => constrained.with_max_height(state.custom_dimension.get()),
|
||||
}
|
||||
})
|
||||
.on_after_layout({
|
||||
let state = state.clone();
|
||||
move |size, _| {
|
||||
state.actual_dimension.set(side.relevant_component(size));
|
||||
}
|
||||
})
|
||||
let child = match handle_side.axis() {
|
||||
Axis::Horizontal => child.constrained().with_max_width(size),
|
||||
Axis::Vertical => child.constrained().with_max_height(size),
|
||||
}
|
||||
.into_any();
|
||||
|
||||
Self {
|
||||
side,
|
||||
child,
|
||||
handle_size,
|
||||
state,
|
||||
_state_handle: state_handle,
|
||||
handle_side,
|
||||
handle_size: DEFAULT_HANDLE_SIZE,
|
||||
on_resize: Rc::new(RefCell::new(on_resize)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn current_size(&self) -> f32 {
|
||||
self.state.actual_dimension.get()
|
||||
pub fn with_handle_size(mut self, handle_size: f32) -> Self {
|
||||
self.handle_size = handle_size;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: View> Element<V> for Resizable<V> {
|
||||
type LayoutState = ();
|
||||
type LayoutState = SizeConstraint;
|
||||
type PaintState = ();
|
||||
|
||||
fn layout(
|
||||
@@ -142,7 +115,7 @@ impl<V: View> Element<V> for Resizable<V> {
|
||||
view: &mut V,
|
||||
cx: &mut LayoutContext<V>,
|
||||
) -> (Vector2F, Self::LayoutState) {
|
||||
(self.child.layout(constraint, view, cx), ())
|
||||
(self.child.layout(constraint, view, cx), constraint)
|
||||
}
|
||||
|
||||
fn paint(
|
||||
@@ -150,34 +123,44 @@ impl<V: View> Element<V> for Resizable<V> {
|
||||
scene: &mut SceneBuilder,
|
||||
bounds: pathfinder_geometry::rect::RectF,
|
||||
visible_bounds: pathfinder_geometry::rect::RectF,
|
||||
_child_size: &mut Self::LayoutState,
|
||||
constraint: &mut SizeConstraint,
|
||||
view: &mut V,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> Self::PaintState {
|
||||
scene.push_stacking_context(None, None);
|
||||
|
||||
let handle_region = self.side.of_rect(bounds, self.handle_size);
|
||||
let handle_region = self.handle_side.of_rect(bounds, self.handle_size);
|
||||
|
||||
enum ResizeHandle {}
|
||||
scene.push_mouse_region(
|
||||
MouseRegion::new::<ResizeHandle>(cx.view_id(), self.side as usize, handle_region)
|
||||
.on_down(MouseButton::Left, |_, _: &mut V, _| {}) // This prevents the mouse down event from being propagated elsewhere
|
||||
.on_drag(MouseButton::Left, {
|
||||
let state = self.state.clone();
|
||||
let side = self.side;
|
||||
move |e, _: &mut V, cx| {
|
||||
let prev_width = state.actual_dimension.get();
|
||||
state
|
||||
.custom_dimension
|
||||
.set(0f32.max(prev_width + side.compute_delta(e)).round());
|
||||
cx.notify();
|
||||
MouseRegion::new::<ResizeHandle>(
|
||||
cx.view_id(),
|
||||
self.handle_side as usize,
|
||||
handle_region,
|
||||
)
|
||||
.on_down(MouseButton::Left, |_, _: &mut V, _| {}) // This prevents the mouse down event from being propagated elsewhere
|
||||
.on_drag(MouseButton::Left, {
|
||||
let bounds = bounds.clone();
|
||||
let side = self.handle_side;
|
||||
let prev_size = side.relevant_component(bounds.size());
|
||||
let min_size = side.relevant_component(constraint.min);
|
||||
let max_size = side.relevant_component(constraint.max);
|
||||
let on_resize = self.on_resize.clone();
|
||||
move |event, view: &mut V, cx| {
|
||||
let new_size = min_size
|
||||
.max(prev_size + side.compute_delta(event))
|
||||
.min(max_size)
|
||||
.round();
|
||||
if new_size != prev_size {
|
||||
on_resize.borrow_mut()(view, new_size, cx);
|
||||
}
|
||||
}),
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
scene.push_cursor_region(crate::CursorRegion {
|
||||
bounds: handle_region,
|
||||
style: match self.side.axis() {
|
||||
style: match self.handle_side.axis() {
|
||||
Axis::Horizontal => CursorStyle::ResizeLeftRight,
|
||||
Axis::Vertical => CursorStyle::ResizeUpDown,
|
||||
},
|
||||
|
||||
@@ -173,6 +173,7 @@ pub struct WindowOptions<'a> {
|
||||
pub titlebar: Option<TitlebarOptions<'a>>,
|
||||
pub center: bool,
|
||||
pub focus: bool,
|
||||
pub show: bool,
|
||||
pub kind: WindowKind,
|
||||
pub is_movable: bool,
|
||||
pub screen: Option<Rc<dyn Screen>>,
|
||||
@@ -222,21 +223,21 @@ impl Bind for WindowBounds {
|
||||
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
|
||||
let (region, next_index) = match self {
|
||||
WindowBounds::Fullscreen => {
|
||||
let next_index = statement.bind("Fullscreen", start_index)?;
|
||||
let next_index = statement.bind(&"Fullscreen", start_index)?;
|
||||
(None, next_index)
|
||||
}
|
||||
WindowBounds::Maximized => {
|
||||
let next_index = statement.bind("Maximized", start_index)?;
|
||||
let next_index = statement.bind(&"Maximized", start_index)?;
|
||||
(None, next_index)
|
||||
}
|
||||
WindowBounds::Fixed(region) => {
|
||||
let next_index = statement.bind("Fixed", start_index)?;
|
||||
let next_index = statement.bind(&"Fixed", start_index)?;
|
||||
(Some(*region), next_index)
|
||||
}
|
||||
};
|
||||
|
||||
statement.bind(
|
||||
region.map(|region| {
|
||||
®ion.map(|region| {
|
||||
(
|
||||
region.min_x(),
|
||||
region.min_y(),
|
||||
@@ -376,6 +377,7 @@ impl<'a> Default for WindowOptions<'a> {
|
||||
}),
|
||||
center: false,
|
||||
focus: true,
|
||||
show: true,
|
||||
kind: WindowKind::Normal,
|
||||
is_movable: true,
|
||||
screen: None,
|
||||
|
||||
@@ -614,7 +614,7 @@ impl Window {
|
||||
}
|
||||
if options.focus {
|
||||
native_window.makeKeyAndOrderFront_(nil);
|
||||
} else {
|
||||
} else if options.show {
|
||||
native_window.orderFront_(nil);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ doctest = false
|
||||
|
||||
[dependencies]
|
||||
context_menu = { path = "../context_menu" }
|
||||
db = { path = "../db" }
|
||||
drag_and_drop = { path = "../drag_and_drop" }
|
||||
editor = { path = "../editor" }
|
||||
gpui = { path = "../gpui" }
|
||||
@@ -26,7 +27,6 @@ serde_derive.workspace = true
|
||||
serde_json.workspace = true
|
||||
anyhow.workspace = true
|
||||
schemars.workspace = true
|
||||
|
||||
unicase = "2.6"
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
mod project_panel_settings;
|
||||
|
||||
use context_menu::{ContextMenu, ContextMenuItem};
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use drag_and_drop::{DragAndDrop, Draggable};
|
||||
use editor::{Cancel, Editor};
|
||||
use futures::stream::StreamExt;
|
||||
use gpui::{
|
||||
actions,
|
||||
anyhow::{anyhow, Result},
|
||||
anyhow::{self, anyhow, Result},
|
||||
elements::{
|
||||
AnchorCorner, ChildView, ContainerStyle, Empty, Flex, Label, MouseEventHandler,
|
||||
ParentElement, ScrollTarget, Stack, Svg, UniformList, UniformListState,
|
||||
@@ -14,15 +15,17 @@ use gpui::{
|
||||
geometry::vector::Vector2F,
|
||||
keymap_matcher::KeymapContext,
|
||||
platform::{CursorStyle, MouseButton, PromptLevel},
|
||||
AnyElement, AppContext, ClipboardItem, Element, Entity, ModelHandle, Task, View, ViewContext,
|
||||
ViewHandle, WeakViewHandle,
|
||||
AnyElement, AppContext, AsyncAppContext, ClipboardItem, Element, Entity, ModelHandle, Task,
|
||||
View, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
|
||||
};
|
||||
use menu::{Confirm, SelectNext, SelectPrev};
|
||||
use project::{
|
||||
repository::GitFileStatus, Entry, EntryKind, Project, ProjectEntryId, ProjectPath, Worktree,
|
||||
WorktreeId,
|
||||
repository::GitFileStatus, Entry, EntryKind, Fs, Project, ProjectEntryId, ProjectPath,
|
||||
Worktree, WorktreeId,
|
||||
};
|
||||
use project_panel_settings::ProjectPanelSettings;
|
||||
use project_panel_settings::{ProjectPanelDockPosition, ProjectPanelSettings};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::SettingsStore;
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
collections::{hash_map, HashMap},
|
||||
@@ -33,12 +36,18 @@ use std::{
|
||||
};
|
||||
use theme::ProjectPanelEntry;
|
||||
use unicase::UniCase;
|
||||
use workspace::Workspace;
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
use workspace::{
|
||||
dock::{DockPosition, Panel},
|
||||
Workspace,
|
||||
};
|
||||
|
||||
const PROJECT_PANEL_KEY: &'static str = "ProjectPanel";
|
||||
const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX;
|
||||
|
||||
pub struct ProjectPanel {
|
||||
project: ModelHandle<Project>,
|
||||
fs: Arc<dyn Fs>,
|
||||
list: UniformListState,
|
||||
visible_entries: Vec<(WorktreeId, Vec<Entry>)>,
|
||||
last_worktree_root_id: Option<ProjectEntryId>,
|
||||
@@ -50,6 +59,9 @@ pub struct ProjectPanel {
|
||||
context_menu: ViewHandle<ContextMenu>,
|
||||
dragged_entry_destination: Option<Arc<Path>>,
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
has_focus: bool,
|
||||
width: Option<f32>,
|
||||
pending_serialization: Task<Option<()>>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
@@ -146,10 +158,17 @@ pub enum Event {
|
||||
entry_id: ProjectEntryId,
|
||||
focus_opened_item: bool,
|
||||
},
|
||||
DockPositionChanged,
|
||||
Focus,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SerializedProjectPanel {
|
||||
width: Option<f32>,
|
||||
}
|
||||
|
||||
impl ProjectPanel {
|
||||
pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
|
||||
fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
|
||||
let project = workspace.project().clone();
|
||||
let project_panel = cx.add_view(|cx: &mut ViewContext<Self>| {
|
||||
cx.observe(&project, |this, _, cx| {
|
||||
@@ -210,6 +229,7 @@ impl ProjectPanel {
|
||||
let view_id = cx.view_id();
|
||||
let mut this = Self {
|
||||
project: project.clone(),
|
||||
fs: workspace.app_state().fs.clone(),
|
||||
list: Default::default(),
|
||||
visible_entries: Default::default(),
|
||||
last_worktree_root_id: Default::default(),
|
||||
@@ -221,8 +241,23 @@ impl ProjectPanel {
|
||||
context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)),
|
||||
dragged_entry_destination: None,
|
||||
workspace: workspace.weak_handle(),
|
||||
has_focus: false,
|
||||
width: None,
|
||||
pending_serialization: Task::ready(None),
|
||||
};
|
||||
this.update_visible_entries(None, cx);
|
||||
|
||||
// Update the dock position when the setting changes.
|
||||
let mut old_dock_position = this.position(cx);
|
||||
cx.observe_global::<SettingsStore, _>(move |this, cx| {
|
||||
let new_dock_position = this.position(cx);
|
||||
if new_dock_position != old_dock_position {
|
||||
old_dock_position = new_dock_position;
|
||||
cx.emit(Event::DockPositionChanged);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
this
|
||||
});
|
||||
|
||||
@@ -254,6 +289,7 @@ impl ProjectPanel {
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
@@ -261,6 +297,51 @@ impl ProjectPanel {
|
||||
project_panel
|
||||
}
|
||||
|
||||
pub fn load(
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
cx: AsyncAppContext,
|
||||
) -> Task<Result<ViewHandle<Self>>> {
|
||||
cx.spawn(|mut cx| async move {
|
||||
let serialized_panel = if let Some(panel) = cx
|
||||
.background()
|
||||
.spawn(async move { KEY_VALUE_STORE.read_kvp(PROJECT_PANEL_KEY) })
|
||||
.await
|
||||
.log_err()
|
||||
.flatten()
|
||||
{
|
||||
Some(serde_json::from_str::<SerializedProjectPanel>(&panel)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
let panel = ProjectPanel::new(workspace, cx);
|
||||
if let Some(serialized_panel) = serialized_panel {
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.width = serialized_panel.width;
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
panel
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn serialize(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let width = self.width;
|
||||
self.pending_serialization = cx.background().spawn(
|
||||
async move {
|
||||
KEY_VALUE_STORE
|
||||
.write_kvp(
|
||||
PROJECT_PANEL_KEY.into(),
|
||||
serde_json::to_string(&SerializedProjectPanel { width })?,
|
||||
)
|
||||
.await?;
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err(),
|
||||
);
|
||||
}
|
||||
|
||||
fn deploy_context_menu(
|
||||
&mut self,
|
||||
position: Vector2F,
|
||||
@@ -1352,16 +1433,103 @@ impl View for ProjectPanel {
|
||||
Self::reset_to_default_keymap_context(keymap);
|
||||
keymap.add_identifier("menu");
|
||||
}
|
||||
|
||||
fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
if !self.has_focus {
|
||||
self.has_focus = true;
|
||||
cx.emit(Event::Focus);
|
||||
}
|
||||
}
|
||||
|
||||
fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
|
||||
self.has_focus = false;
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for ProjectPanel {
|
||||
type Event = Event;
|
||||
}
|
||||
|
||||
impl workspace::sidebar::SidebarItem for ProjectPanel {
|
||||
fn should_show_badge(&self, _: &AppContext) -> bool {
|
||||
impl workspace::dock::Panel for ProjectPanel {
|
||||
fn position(&self, cx: &WindowContext) -> DockPosition {
|
||||
match settings::get::<ProjectPanelSettings>(cx).dock {
|
||||
ProjectPanelDockPosition::Left => DockPosition::Left,
|
||||
ProjectPanelDockPosition::Right => DockPosition::Right,
|
||||
}
|
||||
}
|
||||
|
||||
fn position_is_valid(&self, position: DockPosition) -> bool {
|
||||
matches!(position, DockPosition::Left | DockPosition::Right)
|
||||
}
|
||||
|
||||
fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
|
||||
settings::update_settings_file::<ProjectPanelSettings>(
|
||||
self.fs.clone(),
|
||||
cx,
|
||||
move |settings| {
|
||||
let dock = match position {
|
||||
DockPosition::Left | DockPosition::Bottom => ProjectPanelDockPosition::Left,
|
||||
DockPosition::Right => ProjectPanelDockPosition::Right,
|
||||
};
|
||||
settings.dock = Some(dock);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn size(&self, cx: &WindowContext) -> f32 {
|
||||
self.width
|
||||
.unwrap_or_else(|| settings::get::<ProjectPanelSettings>(cx).default_width)
|
||||
}
|
||||
|
||||
fn set_size(&mut self, size: f32, cx: &mut ViewContext<Self>) {
|
||||
self.width = Some(size);
|
||||
self.serialize(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn should_zoom_in_on_event(_: &Self::Event) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn should_zoom_out_on_event(_: &Self::Event) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn is_zoomed(&self, _: &WindowContext) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn set_zoomed(&mut self, _: bool, _: &mut ViewContext<Self>) {}
|
||||
|
||||
fn set_active(&mut self, _: bool, _: &mut ViewContext<Self>) {}
|
||||
|
||||
fn icon_path(&self) -> &'static str {
|
||||
"icons/folder_tree_16.svg"
|
||||
}
|
||||
|
||||
fn icon_tooltip(&self) -> String {
|
||||
"Project Panel".into()
|
||||
}
|
||||
|
||||
fn should_change_position_on_event(event: &Self::Event) -> bool {
|
||||
matches!(event, Event::DockPositionChanged)
|
||||
}
|
||||
|
||||
fn should_activate_on_event(_: &Self::Event) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn should_close_on_event(_: &Self::Event) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn has_focus(&self, _: &WindowContext) -> bool {
|
||||
self.has_focus
|
||||
}
|
||||
|
||||
fn is_focus_event(event: &Self::Event) -> bool {
|
||||
matches!(event, Event::Focus)
|
||||
}
|
||||
}
|
||||
|
||||
impl ClipboardEntry {
|
||||
@@ -2059,6 +2227,7 @@ mod tests {
|
||||
theme::init((), cx);
|
||||
language::init(cx);
|
||||
editor::init_settings(cx);
|
||||
crate::init(cx);
|
||||
workspace::init_settings(cx);
|
||||
});
|
||||
}
|
||||
@@ -2072,6 +2241,7 @@ mod tests {
|
||||
language::init(cx);
|
||||
editor::init(cx);
|
||||
pane::init(cx);
|
||||
crate::init(cx);
|
||||
workspace::init(app_state.clone(), cx);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,14 +3,25 @@ use schemars::JsonSchema;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use settings::Setting;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ProjectPanelDockPosition {
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct ProjectPanelSettings {
|
||||
pub git_status: bool,
|
||||
pub dock: ProjectPanelDockPosition,
|
||||
pub default_width: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
pub struct ProjectPanelSettingsContent {
|
||||
pub git_status: Option<bool>,
|
||||
pub dock: Option<ProjectPanelDockPosition>,
|
||||
pub default_width: Option<f32>,
|
||||
}
|
||||
|
||||
impl Setting for ProjectPanelSettings {
|
||||
|
||||
@@ -27,7 +27,7 @@ impl StaticColumnCount for bool {}
|
||||
impl Bind for bool {
|
||||
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
|
||||
statement
|
||||
.bind(self.then_some(1).unwrap_or(0), start_index)
|
||||
.bind(&self.then_some(1).unwrap_or(0), start_index)
|
||||
.with_context(|| format!("Failed to bind bool at index {start_index}"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,7 +236,7 @@ impl<'a> Statement<'a> {
|
||||
Ok(str::from_utf8(slice)?)
|
||||
}
|
||||
|
||||
pub fn bind<T: Bind>(&self, value: T, index: i32) -> Result<i32> {
|
||||
pub fn bind<T: Bind>(&self, value: &T, index: i32) -> Result<i32> {
|
||||
debug_assert!(index > 0);
|
||||
Ok(value.bind(self, index)?)
|
||||
}
|
||||
@@ -258,7 +258,7 @@ impl<'a> Statement<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_bindings(&mut self, bindings: impl Bind) -> Result<&mut Self> {
|
||||
pub fn with_bindings(&mut self, bindings: &impl Bind) -> Result<&mut Self> {
|
||||
self.bind(bindings, 1)?;
|
||||
Ok(self)
|
||||
}
|
||||
@@ -464,7 +464,7 @@ mod test {
|
||||
connection
|
||||
.exec(indoc! {"
|
||||
CREATE TABLE texts (
|
||||
text TEXT
|
||||
text TEXT
|
||||
)"})
|
||||
.unwrap()()
|
||||
.unwrap();
|
||||
|
||||
@@ -29,7 +29,7 @@ impl Connection {
|
||||
query: &str,
|
||||
) -> Result<impl 'a + FnMut(B) -> Result<()>> {
|
||||
let mut statement = Statement::prepare(self, query)?;
|
||||
Ok(move |bindings| statement.with_bindings(bindings)?.exec())
|
||||
Ok(move |bindings| statement.with_bindings(&bindings)?.exec())
|
||||
}
|
||||
|
||||
/// Prepare a statement which has no bindings and returns a `Vec<C>`.
|
||||
@@ -55,7 +55,7 @@ impl Connection {
|
||||
query: &str,
|
||||
) -> Result<impl 'a + FnMut(B) -> Result<Vec<C>>> {
|
||||
let mut statement = Statement::prepare(self, query)?;
|
||||
Ok(move |bindings| statement.with_bindings(bindings)?.rows::<C>())
|
||||
Ok(move |bindings| statement.with_bindings(&bindings)?.rows::<C>())
|
||||
}
|
||||
|
||||
/// Prepare a statement that selects a single row from the database.
|
||||
@@ -87,7 +87,7 @@ impl Connection {
|
||||
let mut statement = Statement::prepare(self, query)?;
|
||||
Ok(move |bindings| {
|
||||
statement
|
||||
.with_bindings(bindings)
|
||||
.with_bindings(&bindings)
|
||||
.context("Bindings failed")?
|
||||
.maybe_row::<C>()
|
||||
.context("Maybe row failed")
|
||||
|
||||
@@ -119,6 +119,14 @@ pub fn init(cx: &mut AppContext) {
|
||||
settings::register::<TerminalSettings>(cx);
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum TerminalDockPosition {
|
||||
Left,
|
||||
Bottom,
|
||||
Right,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct TerminalSettings {
|
||||
pub shell: Shell,
|
||||
@@ -132,6 +140,9 @@ pub struct TerminalSettings {
|
||||
pub alternate_scroll: AlternateScroll,
|
||||
pub option_as_meta: bool,
|
||||
pub copy_on_select: bool,
|
||||
pub dock: TerminalDockPosition,
|
||||
pub default_width: f32,
|
||||
pub default_height: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
@@ -147,6 +158,9 @@ pub struct TerminalSettingsContent {
|
||||
pub alternate_scroll: Option<AlternateScroll>,
|
||||
pub option_as_meta: Option<bool>,
|
||||
pub copy_on_select: Option<bool>,
|
||||
pub dock: Option<TerminalDockPosition>,
|
||||
pub default_width: Option<f32>,
|
||||
pub default_height: Option<f32>,
|
||||
}
|
||||
|
||||
impl TerminalSettings {
|
||||
|
||||
@@ -1,173 +0,0 @@
|
||||
use crate::TerminalView;
|
||||
use context_menu::{ContextMenu, ContextMenuItem};
|
||||
use gpui::{
|
||||
elements::*,
|
||||
platform::{CursorStyle, MouseButton},
|
||||
AnyElement, Element, Entity, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use std::any::TypeId;
|
||||
use workspace::{
|
||||
dock::{Dock, FocusDock},
|
||||
item::ItemHandle,
|
||||
NewTerminal, StatusItemView, Workspace,
|
||||
};
|
||||
|
||||
pub struct TerminalButton {
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
popup_menu: ViewHandle<ContextMenu>,
|
||||
}
|
||||
|
||||
impl Entity for TerminalButton {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for TerminalButton {
|
||||
fn ui_name() -> &'static str {
|
||||
"TerminalButton"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
let workspace = self.workspace.upgrade(cx);
|
||||
let project = match workspace {
|
||||
Some(workspace) => workspace.read(cx).project().read(cx),
|
||||
None => return Empty::new().into_any(),
|
||||
};
|
||||
|
||||
let focused_view = cx.focused_view_id();
|
||||
let active = focused_view
|
||||
.map(|view_id| {
|
||||
cx.view_type_id(cx.window_id(), view_id) == Some(TypeId::of::<TerminalView>())
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
let has_terminals = !project.local_terminal_handles().is_empty();
|
||||
let terminal_count = project.local_terminal_handles().len() as i32;
|
||||
let theme = theme::current(cx).clone();
|
||||
|
||||
Stack::new()
|
||||
.with_child(
|
||||
MouseEventHandler::<Self, _>::new(0, cx, {
|
||||
let theme = theme.clone();
|
||||
move |state, _cx| {
|
||||
let style = theme
|
||||
.workspace
|
||||
.status_bar
|
||||
.sidebar_buttons
|
||||
.item
|
||||
.style_for(state, active);
|
||||
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Svg::new("icons/terminal_12.svg")
|
||||
.with_color(style.icon_color)
|
||||
.constrained()
|
||||
.with_width(style.icon_size)
|
||||
.aligned()
|
||||
.into_any_named("terminals-icon"),
|
||||
)
|
||||
.with_children(has_terminals.then(|| {
|
||||
Label::new(terminal_count.to_string(), style.label.text.clone())
|
||||
.contained()
|
||||
.with_style(style.label.container)
|
||||
.aligned()
|
||||
}))
|
||||
.constrained()
|
||||
.with_height(style.icon_size)
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
}
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
if has_terminals {
|
||||
this.deploy_terminal_menu(cx);
|
||||
} else {
|
||||
if !active {
|
||||
if let Some(workspace) = this.workspace.upgrade(cx) {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
Dock::focus_dock(workspace, &Default::default(), cx)
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
})
|
||||
.with_tooltip::<Self>(
|
||||
0,
|
||||
"Show Terminal".into(),
|
||||
Some(Box::new(FocusDock)),
|
||||
theme.tooltip.clone(),
|
||||
cx,
|
||||
),
|
||||
)
|
||||
.with_child(ChildView::new(&self.popup_menu, cx).aligned().top().right())
|
||||
.into_any_named("terminal button")
|
||||
}
|
||||
}
|
||||
|
||||
impl TerminalButton {
|
||||
pub fn new(workspace: ViewHandle<Workspace>, cx: &mut ViewContext<Self>) -> Self {
|
||||
let button_view_id = cx.view_id();
|
||||
cx.observe(&workspace, |_, _, cx| cx.notify()).detach();
|
||||
Self {
|
||||
workspace: workspace.downgrade(),
|
||||
popup_menu: cx.add_view(|cx| {
|
||||
let mut menu = ContextMenu::new(button_view_id, cx);
|
||||
menu.set_position_mode(OverlayPositionMode::Local);
|
||||
menu
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deploy_terminal_menu(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let mut menu_options = vec![ContextMenuItem::action("New Terminal", NewTerminal)];
|
||||
|
||||
if let Some(workspace) = self.workspace.upgrade(cx) {
|
||||
let project = workspace.read(cx).project().read(cx);
|
||||
let local_terminal_handles = project.local_terminal_handles();
|
||||
|
||||
if !local_terminal_handles.is_empty() {
|
||||
menu_options.push(ContextMenuItem::Separator)
|
||||
}
|
||||
|
||||
for local_terminal_handle in local_terminal_handles {
|
||||
if let Some(terminal) = local_terminal_handle.upgrade(cx) {
|
||||
let workspace = self.workspace.clone();
|
||||
let local_terminal_handle = local_terminal_handle.clone();
|
||||
menu_options.push(ContextMenuItem::handler(
|
||||
terminal.read(cx).title(),
|
||||
move |cx| {
|
||||
if let Some(workspace) = workspace.upgrade(cx) {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
let terminal = workspace
|
||||
.items_of_type::<TerminalView>(cx)
|
||||
.find(|terminal| {
|
||||
terminal.read(cx).model().downgrade()
|
||||
== local_terminal_handle
|
||||
});
|
||||
if let Some(terminal) = terminal {
|
||||
workspace.activate_item(&terminal, cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.popup_menu.update(cx, |menu, cx| {
|
||||
menu.show(
|
||||
Default::default(),
|
||||
AnchorCorner::BottomRight,
|
||||
menu_options,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl StatusItemView for TerminalButton {
|
||||
fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, cx: &mut ViewContext<Self>) {
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,389 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::TerminalView;
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use gpui::{
|
||||
actions, anyhow::Result, elements::*, serde_json, AppContext, AsyncAppContext, Entity,
|
||||
Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
|
||||
};
|
||||
use project::Fs;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::SettingsStore;
|
||||
use terminal::{TerminalDockPosition, TerminalSettings};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
use workspace::{
|
||||
dock::{DockPosition, Panel},
|
||||
item::Item,
|
||||
pane, DraggedItem, Pane, Workspace,
|
||||
};
|
||||
|
||||
const TERMINAL_PANEL_KEY: &'static str = "TerminalPanel";
|
||||
|
||||
actions!(terminal_panel, [ToggleFocus]);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(TerminalPanel::add_terminal);
|
||||
}
|
||||
|
||||
pub enum Event {
|
||||
Close,
|
||||
DockPositionChanged,
|
||||
ZoomIn,
|
||||
ZoomOut,
|
||||
Focus,
|
||||
}
|
||||
|
||||
pub struct TerminalPanel {
|
||||
pane: ViewHandle<Pane>,
|
||||
fs: Arc<dyn Fs>,
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
width: Option<f32>,
|
||||
height: Option<f32>,
|
||||
pending_serialization: Task<Option<()>>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl TerminalPanel {
|
||||
fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
|
||||
let weak_self = cx.weak_handle();
|
||||
let pane = cx.add_view(|cx| {
|
||||
let window_id = cx.window_id();
|
||||
let mut pane = Pane::new(
|
||||
workspace.weak_handle(),
|
||||
workspace.app_state().background_actions,
|
||||
Default::default(),
|
||||
cx,
|
||||
);
|
||||
pane.set_can_split(false, cx);
|
||||
pane.on_can_drop(move |drag_and_drop, cx| {
|
||||
drag_and_drop
|
||||
.currently_dragged::<DraggedItem>(window_id)
|
||||
.map_or(false, |(_, item)| {
|
||||
item.handle.act_as::<TerminalView>(cx).is_some()
|
||||
})
|
||||
});
|
||||
pane.set_render_tab_bar_buttons(cx, move |_, cx| {
|
||||
let this = weak_self.clone();
|
||||
Pane::render_tab_bar_button(
|
||||
0,
|
||||
"icons/plus_12.svg",
|
||||
cx,
|
||||
move |_, cx| {
|
||||
let this = this.clone();
|
||||
cx.window_context().defer(move |cx| {
|
||||
if let Some(this) = this.upgrade(cx) {
|
||||
this.update(cx, |this, cx| {
|
||||
this.add_terminal(&Default::default(), cx);
|
||||
});
|
||||
}
|
||||
})
|
||||
},
|
||||
None,
|
||||
)
|
||||
});
|
||||
pane
|
||||
});
|
||||
let subscriptions = vec![
|
||||
cx.observe(&pane, |_, _, cx| cx.notify()),
|
||||
cx.subscribe(&pane, Self::handle_pane_event),
|
||||
];
|
||||
let this = Self {
|
||||
pane,
|
||||
fs: workspace.app_state().fs.clone(),
|
||||
workspace: workspace.weak_handle(),
|
||||
pending_serialization: Task::ready(None),
|
||||
width: None,
|
||||
height: None,
|
||||
_subscriptions: subscriptions,
|
||||
};
|
||||
let mut old_dock_position = this.position(cx);
|
||||
cx.observe_global::<SettingsStore, _>(move |this, cx| {
|
||||
let new_dock_position = this.position(cx);
|
||||
if new_dock_position != old_dock_position {
|
||||
old_dock_position = new_dock_position;
|
||||
cx.emit(Event::DockPositionChanged);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
this
|
||||
}
|
||||
|
||||
pub fn load(
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
cx: AsyncAppContext,
|
||||
) -> Task<Result<ViewHandle<Self>>> {
|
||||
cx.spawn(|mut cx| async move {
|
||||
let serialized_panel = if let Some(panel) = cx
|
||||
.background()
|
||||
.spawn(async move { KEY_VALUE_STORE.read_kvp(TERMINAL_PANEL_KEY) })
|
||||
.await
|
||||
.log_err()
|
||||
.flatten()
|
||||
{
|
||||
Some(serde_json::from_str::<SerializedTerminalPanel>(&panel)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let (panel, pane, items) = workspace.update(&mut cx, |workspace, cx| {
|
||||
let panel = cx.add_view(|cx| TerminalPanel::new(workspace, cx));
|
||||
let items = if let Some(serialized_panel) = serialized_panel.as_ref() {
|
||||
panel.update(cx, |panel, cx| {
|
||||
cx.notify();
|
||||
panel.height = serialized_panel.height;
|
||||
panel.width = serialized_panel.width;
|
||||
panel.pane.update(cx, |_, cx| {
|
||||
serialized_panel
|
||||
.items
|
||||
.iter()
|
||||
.map(|item_id| {
|
||||
TerminalView::deserialize(
|
||||
workspace.project().clone(),
|
||||
workspace.weak_handle(),
|
||||
workspace.database_id(),
|
||||
*item_id,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
})
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
let pane = panel.read(cx).pane.clone();
|
||||
(panel, pane, items)
|
||||
})?;
|
||||
|
||||
let items = futures::future::join_all(items).await;
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
let active_item_id = serialized_panel
|
||||
.as_ref()
|
||||
.and_then(|panel| panel.active_item_id);
|
||||
let mut active_ix = None;
|
||||
for item in items {
|
||||
if let Some(item) = item.log_err() {
|
||||
let item_id = item.id();
|
||||
Pane::add_item(workspace, &pane, Box::new(item), false, false, None, cx);
|
||||
if Some(item_id) == active_item_id {
|
||||
active_ix = Some(pane.read(cx).items_len() - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(active_ix) = active_ix {
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.activate_item(active_ix, false, false, cx)
|
||||
});
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(panel)
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_pane_event(
|
||||
&mut self,
|
||||
_pane: ViewHandle<Pane>,
|
||||
event: &pane::Event,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
pane::Event::ActivateItem { .. } => self.serialize(cx),
|
||||
pane::Event::RemoveItem { .. } => self.serialize(cx),
|
||||
pane::Event::Remove => cx.emit(Event::Close),
|
||||
pane::Event::ZoomIn => cx.emit(Event::ZoomIn),
|
||||
pane::Event::ZoomOut => cx.emit(Event::ZoomOut),
|
||||
pane::Event::Focus => cx.emit(Event::Focus),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn add_terminal(&mut self, _: &workspace::NewTerminal, cx: &mut ViewContext<Self>) {
|
||||
let workspace = self.workspace.clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let pane = this.read_with(&cx, |this, _| this.pane.clone())?;
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
let working_directory_strategy = settings::get::<TerminalSettings>(cx)
|
||||
.working_directory
|
||||
.clone();
|
||||
let working_directory =
|
||||
crate::get_working_directory(workspace, cx, working_directory_strategy);
|
||||
let window_id = cx.window_id();
|
||||
if let Some(terminal) = workspace.project().update(cx, |project, cx| {
|
||||
project
|
||||
.create_terminal(working_directory, window_id, cx)
|
||||
.log_err()
|
||||
}) {
|
||||
let terminal =
|
||||
Box::new(cx.add_view(|cx| {
|
||||
TerminalView::new(terminal, workspace.database_id(), cx)
|
||||
}));
|
||||
Pane::add_item(workspace, &pane, terminal, true, true, None, cx);
|
||||
}
|
||||
})?;
|
||||
this.update(&mut cx, |this, cx| this.serialize(cx))?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn serialize(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let items = self
|
||||
.pane
|
||||
.read(cx)
|
||||
.items()
|
||||
.map(|item| item.id())
|
||||
.collect::<Vec<_>>();
|
||||
let active_item_id = self.pane.read(cx).active_item().map(|item| item.id());
|
||||
let height = self.height;
|
||||
let width = self.width;
|
||||
self.pending_serialization = cx.background().spawn(
|
||||
async move {
|
||||
KEY_VALUE_STORE
|
||||
.write_kvp(
|
||||
TERMINAL_PANEL_KEY.into(),
|
||||
serde_json::to_string(&SerializedTerminalPanel {
|
||||
items,
|
||||
active_item_id,
|
||||
height,
|
||||
width,
|
||||
})?,
|
||||
)
|
||||
.await?;
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for TerminalPanel {
|
||||
type Event = Event;
|
||||
}
|
||||
|
||||
impl View for TerminalPanel {
|
||||
fn ui_name() -> &'static str {
|
||||
"TerminalPanel"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> gpui::AnyElement<Self> {
|
||||
ChildView::new(&self.pane, cx).into_any()
|
||||
}
|
||||
|
||||
fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
if cx.is_self_focused() {
|
||||
cx.focus(&self.pane);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Panel for TerminalPanel {
|
||||
fn position(&self, cx: &WindowContext) -> DockPosition {
|
||||
match settings::get::<TerminalSettings>(cx).dock {
|
||||
TerminalDockPosition::Left => DockPosition::Left,
|
||||
TerminalDockPosition::Bottom => DockPosition::Bottom,
|
||||
TerminalDockPosition::Right => DockPosition::Right,
|
||||
}
|
||||
}
|
||||
|
||||
fn position_is_valid(&self, _: DockPosition) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
|
||||
settings::update_settings_file::<TerminalSettings>(self.fs.clone(), cx, move |settings| {
|
||||
let dock = match position {
|
||||
DockPosition::Left => TerminalDockPosition::Left,
|
||||
DockPosition::Bottom => TerminalDockPosition::Bottom,
|
||||
DockPosition::Right => TerminalDockPosition::Right,
|
||||
};
|
||||
settings.dock = Some(dock);
|
||||
});
|
||||
}
|
||||
|
||||
fn size(&self, cx: &WindowContext) -> f32 {
|
||||
let settings = settings::get::<TerminalSettings>(cx);
|
||||
match self.position(cx) {
|
||||
DockPosition::Left | DockPosition::Right => {
|
||||
self.width.unwrap_or_else(|| settings.default_width)
|
||||
}
|
||||
DockPosition::Bottom => self.height.unwrap_or_else(|| settings.default_height),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_size(&mut self, size: f32, cx: &mut ViewContext<Self>) {
|
||||
match self.position(cx) {
|
||||
DockPosition::Left | DockPosition::Right => self.width = Some(size),
|
||||
DockPosition::Bottom => self.height = Some(size),
|
||||
}
|
||||
self.serialize(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn should_zoom_in_on_event(event: &Event) -> bool {
|
||||
matches!(event, Event::ZoomIn)
|
||||
}
|
||||
|
||||
fn should_zoom_out_on_event(event: &Event) -> bool {
|
||||
matches!(event, Event::ZoomOut)
|
||||
}
|
||||
|
||||
fn is_zoomed(&self, cx: &WindowContext) -> bool {
|
||||
self.pane.read(cx).is_zoomed()
|
||||
}
|
||||
|
||||
fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
|
||||
self.pane.update(cx, |pane, cx| pane.set_zoomed(zoomed, cx));
|
||||
}
|
||||
|
||||
fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
|
||||
if active && self.pane.read(cx).items_len() == 0 {
|
||||
self.add_terminal(&Default::default(), cx)
|
||||
}
|
||||
}
|
||||
|
||||
fn icon_path(&self) -> &'static str {
|
||||
"icons/terminal_12.svg"
|
||||
}
|
||||
|
||||
fn icon_tooltip(&self) -> String {
|
||||
"Terminals".to_string()
|
||||
}
|
||||
|
||||
fn icon_label(&self, cx: &WindowContext) -> Option<String> {
|
||||
let count = self.pane.read(cx).items_len();
|
||||
if count == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(count.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn should_change_position_on_event(event: &Self::Event) -> bool {
|
||||
matches!(event, Event::DockPositionChanged)
|
||||
}
|
||||
|
||||
fn should_activate_on_event(_: &Self::Event) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn should_close_on_event(event: &Event) -> bool {
|
||||
matches!(event, Event::Close)
|
||||
}
|
||||
|
||||
fn has_focus(&self, cx: &WindowContext) -> bool {
|
||||
self.pane.read(cx).has_focus()
|
||||
}
|
||||
|
||||
fn is_focus_event(event: &Self::Event) -> bool {
|
||||
matches!(event, Event::Focus)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SerializedTerminalPanel {
|
||||
items: Vec<usize>,
|
||||
active_item_id: Option<usize>,
|
||||
width: Option<f32>,
|
||||
height: Option<f32>,
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
mod persistence;
|
||||
pub mod terminal_button;
|
||||
pub mod terminal_element;
|
||||
pub mod terminal_panel;
|
||||
|
||||
use crate::{persistence::TERMINAL_DB, terminal_element::TerminalElement};
|
||||
use context_menu::{ContextMenu, ContextMenuItem};
|
||||
@@ -63,6 +63,7 @@ actions!(
|
||||
impl_actions!(terminal, [SendText, SendKeystroke]);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
terminal_panel::init(cx);
|
||||
terminal::init(cx);
|
||||
|
||||
cx.add_action(TerminalView::deploy);
|
||||
|
||||
+12
-20
@@ -82,19 +82,20 @@ pub struct Workspace {
|
||||
pub pane_divider: Border,
|
||||
pub leader_border_opacity: f32,
|
||||
pub leader_border_width: f32,
|
||||
pub sidebar: Sidebar,
|
||||
pub dock: Dock,
|
||||
pub status_bar: StatusBar,
|
||||
pub toolbar: Toolbar,
|
||||
pub breadcrumb_height: f32,
|
||||
pub breadcrumbs: Interactive<ContainedText>,
|
||||
pub disconnected_overlay: ContainedText,
|
||||
pub modal: ContainerStyle,
|
||||
pub zoomed_foreground: ContainerStyle,
|
||||
pub zoomed_background: ContainerStyle,
|
||||
pub notification: ContainerStyle,
|
||||
pub notifications: Notifications,
|
||||
pub joining_project_avatar: ImageStyle,
|
||||
pub joining_project_message: ContainedText,
|
||||
pub external_location_message: ContainedText,
|
||||
pub dock: Dock,
|
||||
pub drop_target_overlay_color: Color,
|
||||
}
|
||||
|
||||
@@ -317,15 +318,6 @@ pub struct Toolbar {
|
||||
pub nav_button: Interactive<IconButton>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
pub struct Dock {
|
||||
pub initial_size_right: f32,
|
||||
pub initial_size_bottom: f32,
|
||||
pub wash_color: Color,
|
||||
pub panel: ContainerStyle,
|
||||
pub maximized: ContainerStyle,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
pub struct Notifications {
|
||||
#[serde(flatten)]
|
||||
@@ -369,17 +361,17 @@ pub struct StatusBar {
|
||||
pub auto_update_progress_message: TextStyle,
|
||||
pub auto_update_done_message: TextStyle,
|
||||
pub lsp_status: Interactive<StatusBarLspStatus>,
|
||||
pub sidebar_buttons: StatusBarSidebarButtons,
|
||||
pub panel_buttons: StatusBarPanelButtons,
|
||||
pub diagnostic_summary: Interactive<StatusBarDiagnosticSummary>,
|
||||
pub diagnostic_message: Interactive<ContainedText>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
pub struct StatusBarSidebarButtons {
|
||||
pub struct StatusBarPanelButtons {
|
||||
pub group_left: ContainerStyle,
|
||||
pub group_bottom: ContainerStyle,
|
||||
pub group_right: ContainerStyle,
|
||||
pub item: Interactive<SidebarItem>,
|
||||
pub badge: ContainerStyle,
|
||||
pub button: Interactive<PanelButton>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
@@ -409,14 +401,14 @@ pub struct StatusBarLspStatus {
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
pub struct Sidebar {
|
||||
pub initial_size: f32,
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
pub struct Dock {
|
||||
pub left: ContainerStyle,
|
||||
pub bottom: ContainerStyle,
|
||||
pub right: ContainerStyle,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
pub struct SidebarItem {
|
||||
pub struct PanelButton {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
pub icon_color: Color,
|
||||
|
||||
@@ -11,7 +11,7 @@ use gpui::{
|
||||
use settings::{update_settings_file, SettingsStore};
|
||||
use std::{borrow::Cow, sync::Arc};
|
||||
use workspace::{
|
||||
item::Item, open_new, sidebar::SidebarSide, AppState, PaneBackdrop, Welcome, Workspace,
|
||||
dock::DockPosition, item::Item, open_new, AppState, PaneBackdrop, Welcome, Workspace,
|
||||
WorkspaceId,
|
||||
};
|
||||
|
||||
@@ -32,7 +32,7 @@ pub fn init(cx: &mut AppContext) {
|
||||
|
||||
pub fn show_welcome_experience(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
open_new(&app_state, cx, |workspace, cx| {
|
||||
workspace.toggle_sidebar(SidebarSide::Left, cx);
|
||||
workspace.toggle_dock(DockPosition::Left, cx);
|
||||
let welcome_page = cx.add_view(|cx| WelcomePage::new(workspace, cx));
|
||||
workspace.add_item_to_center(Box::new(welcome_page.clone()), cx);
|
||||
cx.focus(&welcome_page);
|
||||
|
||||
+639
-758
File diff suppressed because it is too large
Load Diff
@@ -1,125 +0,0 @@
|
||||
use super::{icon_for_dock_anchor, Dock, FocusDock, HideDock};
|
||||
use crate::{handle_dropped_item, StatusItemView, Workspace};
|
||||
use gpui::{
|
||||
elements::{Empty, MouseEventHandler, Svg},
|
||||
platform::CursorStyle,
|
||||
platform::MouseButton,
|
||||
AnyElement, Element, Entity, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
|
||||
pub struct ToggleDockButton {
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
}
|
||||
|
||||
impl ToggleDockButton {
|
||||
pub fn new(workspace: ViewHandle<Workspace>, cx: &mut ViewContext<Self>) -> Self {
|
||||
// When dock moves, redraw so that the icon and toggle status matches.
|
||||
cx.subscribe(&workspace, |_, _, _, cx| cx.notify()).detach();
|
||||
|
||||
Self {
|
||||
workspace: workspace.downgrade(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for ToggleDockButton {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for ToggleDockButton {
|
||||
fn ui_name() -> &'static str {
|
||||
"Dock Toggle"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> AnyElement<Self> {
|
||||
let workspace = self.workspace.upgrade(cx);
|
||||
|
||||
if workspace.is_none() {
|
||||
return Empty::new().into_any();
|
||||
}
|
||||
|
||||
let workspace = workspace.unwrap();
|
||||
let dock_position = workspace.read(cx).dock.position;
|
||||
let dock_pane = workspace.read(cx).dock_pane().clone();
|
||||
|
||||
let theme = theme::current(cx).clone();
|
||||
|
||||
let button = MouseEventHandler::<Self, _>::new(0, cx, {
|
||||
let theme = theme.clone();
|
||||
move |state, _| {
|
||||
let style = theme
|
||||
.workspace
|
||||
.status_bar
|
||||
.sidebar_buttons
|
||||
.item
|
||||
.style_for(state, dock_position.is_visible());
|
||||
|
||||
Svg::new(icon_for_dock_anchor(dock_position.anchor()))
|
||||
.with_color(style.icon_color)
|
||||
.constrained()
|
||||
.with_width(style.icon_size)
|
||||
.with_height(style.icon_size)
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
}
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_up(MouseButton::Left, move |event, this, cx| {
|
||||
let drop_index = dock_pane.read(cx).items_len() + 1;
|
||||
handle_dropped_item(
|
||||
event,
|
||||
this.workspace.clone(),
|
||||
&dock_pane.downgrade(),
|
||||
drop_index,
|
||||
false,
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
if dock_position.is_visible() {
|
||||
button
|
||||
.on_click(MouseButton::Left, |_, this, cx| {
|
||||
if let Some(workspace) = this.workspace.upgrade(cx) {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
Dock::hide_dock(workspace, &Default::default(), cx)
|
||||
})
|
||||
}
|
||||
})
|
||||
.with_tooltip::<Self>(
|
||||
0,
|
||||
"Hide Dock".into(),
|
||||
Some(Box::new(HideDock)),
|
||||
theme.tooltip.clone(),
|
||||
cx,
|
||||
)
|
||||
} else {
|
||||
button
|
||||
.on_click(MouseButton::Left, |_, this, cx| {
|
||||
if let Some(workspace) = this.workspace.upgrade(cx) {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
Dock::focus_dock(workspace, &Default::default(), cx)
|
||||
})
|
||||
}
|
||||
})
|
||||
.with_tooltip::<Self>(
|
||||
0,
|
||||
"Focus Dock".into(),
|
||||
Some(Box::new(FocusDock)),
|
||||
theme.tooltip.clone(),
|
||||
cx,
|
||||
)
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
|
||||
impl StatusItemView for ToggleDockButton {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
_active_pane_item: Option<&dyn crate::ItemHandle>,
|
||||
_cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
//Not applicable
|
||||
}
|
||||
}
|
||||
@@ -437,7 +437,7 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
|
||||
for item_event in T::to_item_events(event).into_iter() {
|
||||
match item_event {
|
||||
ItemEvent::CloseItem => {
|
||||
Pane::close_item_by_id(workspace, pane, item.id(), cx)
|
||||
pane.update(cx, |pane, cx| pane.close_item_by_id(item.id(), cx))
|
||||
.detach_and_log_err(cx);
|
||||
return;
|
||||
}
|
||||
@@ -769,7 +769,7 @@ impl<T: FollowableItem> FollowableItemHandle for ViewHandle<T> {
|
||||
#[cfg(test)]
|
||||
pub(crate) mod test {
|
||||
use super::{Item, ItemEvent};
|
||||
use crate::{sidebar::SidebarItem, ItemId, ItemNavHistory, Pane, Workspace, WorkspaceId};
|
||||
use crate::{ItemId, ItemNavHistory, Pane, Workspace, WorkspaceId};
|
||||
use gpui::{
|
||||
elements::Empty, AnyElement, AppContext, Element, Entity, ModelHandle, Task, View,
|
||||
ViewContext, ViewHandle, WeakViewHandle,
|
||||
@@ -1062,6 +1062,4 @@ pub(crate) mod test {
|
||||
Task::Ready(Some(anyhow::Ok(view)))
|
||||
}
|
||||
}
|
||||
|
||||
impl SidebarItem for TestItem {}
|
||||
}
|
||||
|
||||
+246
-380
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,7 @@ use gpui::{
|
||||
use project::ProjectEntryId;
|
||||
|
||||
pub fn dragged_item_receiver<Tag, D, F>(
|
||||
pane: &Pane,
|
||||
region_id: usize,
|
||||
drop_index: usize,
|
||||
allow_same_pane: bool,
|
||||
@@ -24,22 +25,24 @@ where
|
||||
D: Element<Pane>,
|
||||
F: FnOnce(&mut MouseState, &mut ViewContext<Pane>) -> D,
|
||||
{
|
||||
MouseEventHandler::<Tag, _>::above(region_id, cx, |state, cx| {
|
||||
let drag_and_drop = cx.global::<DragAndDrop<Workspace>>();
|
||||
let drag_position = if (pane.can_drop)(drag_and_drop, cx) {
|
||||
drag_and_drop
|
||||
.currently_dragged::<DraggedItem>(cx.window_id())
|
||||
.map(|(drag_position, _)| drag_position)
|
||||
.or_else(|| {
|
||||
drag_and_drop
|
||||
.currently_dragged::<ProjectEntryId>(cx.window_id())
|
||||
.map(|(drag_position, _)| drag_position)
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut handler = MouseEventHandler::<Tag, _>::above(region_id, cx, |state, cx| {
|
||||
// Observing hovered will cause a render when the mouse enters regardless
|
||||
// of if mouse position was accessed before
|
||||
let drag_position = if state.hovered() {
|
||||
cx.global::<DragAndDrop<Workspace>>()
|
||||
.currently_dragged::<DraggedItem>(cx.window_id())
|
||||
.map(|(drag_position, _)| drag_position)
|
||||
.or_else(|| {
|
||||
cx.global::<DragAndDrop<Workspace>>()
|
||||
.currently_dragged::<ProjectEntryId>(cx.window_id())
|
||||
.map(|(drag_position, _)| drag_position)
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let drag_position = if state.hovered() { drag_position } else { None };
|
||||
Stack::new()
|
||||
.with_child(render_child(state, cx))
|
||||
.with_children(drag_position.map(|drag_position| {
|
||||
@@ -64,38 +67,44 @@ where
|
||||
}
|
||||
})
|
||||
}))
|
||||
})
|
||||
.on_up(MouseButton::Left, {
|
||||
move |event, pane, cx| {
|
||||
let workspace = pane.workspace.clone();
|
||||
let pane = cx.weak_handle();
|
||||
handle_dropped_item(
|
||||
event,
|
||||
workspace,
|
||||
&pane,
|
||||
drop_index,
|
||||
allow_same_pane,
|
||||
split_margin,
|
||||
cx,
|
||||
);
|
||||
cx.notify();
|
||||
}
|
||||
})
|
||||
.on_move(|_, _, cx| {
|
||||
let drag_and_drop = cx.global::<DragAndDrop<Workspace>>();
|
||||
});
|
||||
|
||||
if drag_and_drop
|
||||
.currently_dragged::<DraggedItem>(cx.window_id())
|
||||
.is_some()
|
||||
|| drag_and_drop
|
||||
.currently_dragged::<ProjectEntryId>(cx.window_id())
|
||||
.is_some()
|
||||
{
|
||||
cx.notify();
|
||||
} else {
|
||||
cx.propagate_event();
|
||||
}
|
||||
})
|
||||
if drag_position.is_some() {
|
||||
handler = handler
|
||||
.on_up(MouseButton::Left, {
|
||||
move |event, pane, cx| {
|
||||
let workspace = pane.workspace.clone();
|
||||
let pane = cx.weak_handle();
|
||||
handle_dropped_item(
|
||||
event,
|
||||
workspace,
|
||||
&pane,
|
||||
drop_index,
|
||||
allow_same_pane,
|
||||
split_margin,
|
||||
cx,
|
||||
);
|
||||
cx.notify();
|
||||
}
|
||||
})
|
||||
.on_move(|_, _, cx| {
|
||||
let drag_and_drop = cx.global::<DragAndDrop<Workspace>>();
|
||||
|
||||
if drag_and_drop
|
||||
.currently_dragged::<DraggedItem>(cx.window_id())
|
||||
.is_some()
|
||||
|| drag_and_drop
|
||||
.currently_dragged::<ProjectEntryId>(cx.window_id())
|
||||
.is_some()
|
||||
{
|
||||
cx.notify();
|
||||
} else {
|
||||
cx.propagate_event();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
handler
|
||||
}
|
||||
|
||||
pub fn handle_dropped_item<V: View>(
|
||||
@@ -115,7 +124,7 @@ pub fn handle_dropped_item<V: View>(
|
||||
let action = if let Some((_, dragged_item)) =
|
||||
drag_and_drop.currently_dragged::<DraggedItem>(cx.window_id())
|
||||
{
|
||||
Action::Move(dragged_item.pane.clone(), dragged_item.item.id())
|
||||
Action::Move(dragged_item.pane.clone(), dragged_item.handle.id())
|
||||
} else if let Some((_, project_entry)) =
|
||||
drag_and_drop.currently_dragged::<ProjectEntryId>(cx.window_id())
|
||||
{
|
||||
|
||||
@@ -7,7 +7,7 @@ use gpui::{
|
||||
elements::*,
|
||||
geometry::{rect::RectF, vector::Vector2F},
|
||||
platform::{CursorStyle, MouseButton},
|
||||
Axis, Border, ModelHandle, ViewContext, ViewHandle,
|
||||
AnyViewHandle, Axis, Border, ModelHandle, ViewContext, ViewHandle,
|
||||
};
|
||||
use project::Project;
|
||||
use serde::Deserialize;
|
||||
@@ -71,6 +71,7 @@ impl PaneGroup {
|
||||
follower_states: &FollowerStatesByLeader,
|
||||
active_call: Option<&ModelHandle<ActiveCall>>,
|
||||
active_pane: &ViewHandle<Pane>,
|
||||
zoomed: Option<&AnyViewHandle>,
|
||||
app_state: &Arc<AppState>,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) -> AnyElement<Workspace> {
|
||||
@@ -80,6 +81,7 @@ impl PaneGroup {
|
||||
follower_states,
|
||||
active_call,
|
||||
active_pane,
|
||||
zoomed,
|
||||
app_state,
|
||||
cx,
|
||||
)
|
||||
@@ -134,6 +136,7 @@ impl Member {
|
||||
follower_states: &FollowerStatesByLeader,
|
||||
active_call: Option<&ModelHandle<ActiveCall>>,
|
||||
active_pane: &ViewHandle<Pane>,
|
||||
zoomed: Option<&AnyViewHandle>,
|
||||
app_state: &Arc<AppState>,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) -> AnyElement<Workspace> {
|
||||
@@ -141,6 +144,12 @@ impl Member {
|
||||
|
||||
match self {
|
||||
Member::Pane(pane) => {
|
||||
let pane_element = if Some(&**pane) == zoomed {
|
||||
Empty::new().into_any()
|
||||
} else {
|
||||
ChildView::new(pane, cx).into_any()
|
||||
};
|
||||
|
||||
let leader = follower_states
|
||||
.iter()
|
||||
.find_map(|(leader_id, follower_states)| {
|
||||
@@ -257,7 +266,7 @@ impl Member {
|
||||
};
|
||||
|
||||
Stack::new()
|
||||
.with_child(ChildView::new(pane, cx).contained().with_border(border))
|
||||
.with_child(pane_element.contained().with_border(border))
|
||||
.with_children(leader_status_box)
|
||||
.into_any()
|
||||
}
|
||||
@@ -267,6 +276,7 @@ impl Member {
|
||||
follower_states,
|
||||
active_call,
|
||||
active_pane,
|
||||
zoomed,
|
||||
app_state,
|
||||
cx,
|
||||
),
|
||||
@@ -371,6 +381,7 @@ impl PaneAxis {
|
||||
follower_state: &FollowerStatesByLeader,
|
||||
active_call: Option<&ModelHandle<ActiveCall>>,
|
||||
active_pane: &ViewHandle<Pane>,
|
||||
zoomed: Option<&AnyViewHandle>,
|
||||
app_state: &Arc<AppState>,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) -> AnyElement<Workspace> {
|
||||
@@ -388,6 +399,7 @@ impl PaneAxis {
|
||||
follower_state,
|
||||
active_call,
|
||||
active_pane,
|
||||
zoomed,
|
||||
app_state,
|
||||
cx,
|
||||
);
|
||||
|
||||
@@ -11,7 +11,6 @@ use gpui::{platform::WindowBounds, Axis};
|
||||
use util::{unzip_option, ResultExt};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::dock::DockPosition;
|
||||
use crate::WorkspaceId;
|
||||
|
||||
use model::{
|
||||
@@ -19,15 +18,17 @@ use model::{
|
||||
WorkspaceLocation,
|
||||
};
|
||||
|
||||
use self::model::DockStructure;
|
||||
|
||||
define_connection! {
|
||||
// Current schema shape using pseudo-rust syntax:
|
||||
//
|
||||
// workspaces(
|
||||
// workspace_id: usize, // Primary key for workspaces
|
||||
// workspace_location: Bincode<Vec<PathBuf>>,
|
||||
// dock_visible: bool,
|
||||
// dock_anchor: DockAnchor, // 'Bottom' / 'Right' / 'Expanded'
|
||||
// dock_pane: Option<usize>, // PaneId
|
||||
// dock_visible: bool, // Deprecated
|
||||
// dock_anchor: DockAnchor, // Deprecated
|
||||
// dock_pane: Option<usize>, // Deprecated
|
||||
// left_sidebar_open: boolean,
|
||||
// timestamp: String, // UTC YYYY-MM-DD HH:MM:SS
|
||||
// window_state: String, // WindowBounds Discriminant
|
||||
@@ -71,10 +72,10 @@ define_connection! {
|
||||
CREATE TABLE workspaces(
|
||||
workspace_id INTEGER PRIMARY KEY,
|
||||
workspace_location BLOB UNIQUE,
|
||||
dock_visible INTEGER, // Boolean
|
||||
dock_anchor TEXT, // Enum: 'Bottom' / 'Right' / 'Expanded'
|
||||
dock_pane INTEGER, // NULL indicates that we don't have a dock pane yet
|
||||
left_sidebar_open INTEGER, //Boolean
|
||||
dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed.
|
||||
dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed.
|
||||
dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed.
|
||||
left_sidebar_open INTEGER, // Boolean
|
||||
timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
FOREIGN KEY(dock_pane) REFERENCES panes(pane_id)
|
||||
) STRICT;
|
||||
@@ -131,6 +132,36 @@ define_connection! {
|
||||
ALTER TABLE workspaces ADD COLUMN window_width REAL;
|
||||
ALTER TABLE workspaces ADD COLUMN window_height REAL;
|
||||
ALTER TABLE workspaces ADD COLUMN display BLOB;
|
||||
),
|
||||
// Drop foreign key constraint from workspaces.dock_pane to panes table.
|
||||
sql!(
|
||||
CREATE TABLE workspaces_2(
|
||||
workspace_id INTEGER PRIMARY KEY,
|
||||
workspace_location BLOB UNIQUE,
|
||||
dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed.
|
||||
dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed.
|
||||
dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed.
|
||||
left_sidebar_open INTEGER, // Boolean
|
||||
timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
window_state TEXT,
|
||||
window_x REAL,
|
||||
window_y REAL,
|
||||
window_width REAL,
|
||||
window_height REAL,
|
||||
display BLOB
|
||||
) STRICT;
|
||||
INSERT INTO workspaces_2 SELECT * FROM workspaces;
|
||||
DROP TABLE workspaces;
|
||||
ALTER TABLE workspaces_2 RENAME TO workspaces;
|
||||
),
|
||||
// Add panels related information
|
||||
sql!(
|
||||
ALTER TABLE workspaces ADD COLUMN left_dock_visible INTEGER; //bool
|
||||
ALTER TABLE workspaces ADD COLUMN left_dock_active_panel TEXT;
|
||||
ALTER TABLE workspaces ADD COLUMN right_dock_visible INTEGER; //bool
|
||||
ALTER TABLE workspaces ADD COLUMN right_dock_active_panel TEXT;
|
||||
ALTER TABLE workspaces ADD COLUMN bottom_dock_visible INTEGER; //bool
|
||||
ALTER TABLE workspaces ADD COLUMN bottom_dock_active_panel TEXT;
|
||||
)];
|
||||
}
|
||||
|
||||
@@ -146,27 +177,29 @@ impl WorkspaceDb {
|
||||
|
||||
// Note that we re-assign the workspace_id here in case it's empty
|
||||
// and we've grabbed the most recent workspace
|
||||
let (workspace_id, workspace_location, left_sidebar_open, dock_position, bounds, display): (
|
||||
let (workspace_id, workspace_location, bounds, display, docks): (
|
||||
WorkspaceId,
|
||||
WorkspaceLocation,
|
||||
bool,
|
||||
DockPosition,
|
||||
Option<WindowBounds>,
|
||||
Option<Uuid>,
|
||||
DockStructure,
|
||||
) = self
|
||||
.select_row_bound(sql! {
|
||||
SELECT
|
||||
workspace_id,
|
||||
workspace_location,
|
||||
left_sidebar_open,
|
||||
dock_visible,
|
||||
dock_anchor,
|
||||
window_state,
|
||||
window_x,
|
||||
window_y,
|
||||
window_width,
|
||||
window_height,
|
||||
display
|
||||
display,
|
||||
left_dock_visible,
|
||||
left_dock_active_panel,
|
||||
right_dock_visible,
|
||||
right_dock_active_panel,
|
||||
bottom_dock_visible,
|
||||
bottom_dock_active_panel
|
||||
FROM workspaces
|
||||
WHERE workspace_location = ?
|
||||
})
|
||||
@@ -178,18 +211,13 @@ impl WorkspaceDb {
|
||||
Some(SerializedWorkspace {
|
||||
id: workspace_id,
|
||||
location: workspace_location.clone(),
|
||||
dock_pane: self
|
||||
.get_dock_pane(workspace_id)
|
||||
.context("Getting dock pane")
|
||||
.log_err()?,
|
||||
center_group: self
|
||||
.get_center_pane_group(workspace_id)
|
||||
.context("Getting center group")
|
||||
.log_err()?,
|
||||
dock_position,
|
||||
left_sidebar_open,
|
||||
bounds,
|
||||
display,
|
||||
docks,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -200,7 +228,6 @@ impl WorkspaceDb {
|
||||
conn.with_savepoint("update_worktrees", || {
|
||||
// Clear out panes and pane_groups
|
||||
conn.exec_bound(sql!(
|
||||
UPDATE workspaces SET dock_pane = NULL WHERE workspace_id = ?1;
|
||||
DELETE FROM pane_groups WHERE workspace_id = ?1;
|
||||
DELETE FROM panes WHERE workspace_id = ?1;))?(workspace.id)
|
||||
.expect("Clearing old panes");
|
||||
@@ -215,42 +242,32 @@ impl WorkspaceDb {
|
||||
INSERT INTO workspaces(
|
||||
workspace_id,
|
||||
workspace_location,
|
||||
left_sidebar_open,
|
||||
dock_visible,
|
||||
dock_anchor,
|
||||
left_dock_visible,
|
||||
left_dock_active_panel,
|
||||
right_dock_visible,
|
||||
right_dock_active_panel,
|
||||
bottom_dock_visible,
|
||||
bottom_dock_active_panel,
|
||||
timestamp
|
||||
)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, CURRENT_TIMESTAMP)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT DO
|
||||
UPDATE SET
|
||||
workspace_location = ?2,
|
||||
left_sidebar_open = ?3,
|
||||
dock_visible = ?4,
|
||||
dock_anchor = ?5,
|
||||
left_dock_visible = ?3,
|
||||
left_dock_active_panel = ?4,
|
||||
right_dock_visible = ?5,
|
||||
right_dock_active_panel = ?6,
|
||||
bottom_dock_visible = ?7,
|
||||
bottom_dock_active_panel = ?8,
|
||||
timestamp = CURRENT_TIMESTAMP
|
||||
))?((
|
||||
workspace.id,
|
||||
&workspace.location,
|
||||
workspace.left_sidebar_open,
|
||||
workspace.dock_position,
|
||||
))
|
||||
))?((workspace.id, &workspace.location, workspace.docks))
|
||||
.context("Updating workspace")?;
|
||||
|
||||
// Save center pane group and dock pane
|
||||
// Save center pane group
|
||||
Self::save_pane_group(conn, workspace.id, &workspace.center_group, None)
|
||||
.context("save pane group in save workspace")?;
|
||||
|
||||
let dock_id = Self::save_pane(conn, workspace.id, &workspace.dock_pane, None, true)
|
||||
.context("save pane in save workspace")?;
|
||||
|
||||
// Complete workspace initialization
|
||||
conn.exec_bound(sql!(
|
||||
UPDATE workspaces
|
||||
SET dock_pane = ?
|
||||
WHERE workspace_id = ?
|
||||
))?((dock_id, workspace.id))
|
||||
.context("Finishing initialization with dock pane")?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.log_err();
|
||||
@@ -402,32 +419,17 @@ impl WorkspaceDb {
|
||||
Ok(())
|
||||
}
|
||||
SerializedPaneGroup::Pane(pane) => {
|
||||
Self::save_pane(conn, workspace_id, &pane, parent, false)?;
|
||||
Self::save_pane(conn, workspace_id, &pane, parent)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_dock_pane(&self, workspace_id: WorkspaceId) -> Result<SerializedPane> {
|
||||
let (pane_id, active) = self.select_row_bound(sql!(
|
||||
SELECT pane_id, active
|
||||
FROM panes
|
||||
WHERE pane_id = (SELECT dock_pane FROM workspaces WHERE workspace_id = ?)
|
||||
))?(workspace_id)?
|
||||
.context("No dock pane for workspace")?;
|
||||
|
||||
Ok(SerializedPane::new(
|
||||
self.get_items(pane_id).context("Reading items")?,
|
||||
active,
|
||||
))
|
||||
}
|
||||
|
||||
fn save_pane(
|
||||
conn: &Connection,
|
||||
workspace_id: WorkspaceId,
|
||||
pane: &SerializedPane,
|
||||
parent: Option<(GroupId, usize)>, // None indicates BOTH dock pane AND center_pane
|
||||
dock: bool,
|
||||
parent: Option<(GroupId, usize)>,
|
||||
) -> Result<PaneId> {
|
||||
let pane_id = conn.select_row_bound::<_, i64>(sql!(
|
||||
INSERT INTO panes(workspace_id, active)
|
||||
@@ -436,13 +438,11 @@ impl WorkspaceDb {
|
||||
))?((workspace_id, pane.active))?
|
||||
.ok_or_else(|| anyhow!("Could not retrieve inserted pane_id"))?;
|
||||
|
||||
if !dock {
|
||||
let (parent_id, order) = unzip_option(parent);
|
||||
conn.exec_bound(sql!(
|
||||
INSERT INTO center_panes(pane_id, parent_group_id, position)
|
||||
VALUES (?, ?, ?)
|
||||
))?((pane_id, parent_id, order))?;
|
||||
}
|
||||
let (parent_id, order) = unzip_option(parent);
|
||||
conn.exec_bound(sql!(
|
||||
INSERT INTO center_panes(pane_id, parent_group_id, position)
|
||||
VALUES (?, ?, ?)
|
||||
))?((pane_id, parent_id, order))?;
|
||||
|
||||
Self::save_items(conn, workspace_id, pane_id, &pane.children).context("Saving items")?;
|
||||
|
||||
@@ -498,9 +498,7 @@ impl WorkspaceDb {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::DockAnchor;
|
||||
use db::open_test_db;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_next_id_stability() {
|
||||
@@ -575,23 +573,19 @@ mod tests {
|
||||
let mut workspace_1 = SerializedWorkspace {
|
||||
id: 1,
|
||||
location: (["/tmp", "/tmp2"]).into(),
|
||||
dock_position: crate::dock::DockPosition::Shown(DockAnchor::Bottom),
|
||||
center_group: Default::default(),
|
||||
dock_pane: Default::default(),
|
||||
left_sidebar_open: true,
|
||||
bounds: Default::default(),
|
||||
display: Default::default(),
|
||||
docks: Default::default(),
|
||||
};
|
||||
|
||||
let mut workspace_2 = SerializedWorkspace {
|
||||
let workspace_2 = SerializedWorkspace {
|
||||
id: 2,
|
||||
location: (["/tmp"]).into(),
|
||||
dock_position: crate::dock::DockPosition::Hidden(DockAnchor::Expanded),
|
||||
center_group: Default::default(),
|
||||
dock_pane: Default::default(),
|
||||
left_sidebar_open: false,
|
||||
bounds: Default::default(),
|
||||
display: Default::default(),
|
||||
docks: Default::default(),
|
||||
};
|
||||
|
||||
db.save_workspace(workspace_1.clone()).await;
|
||||
@@ -615,12 +609,6 @@ mod tests {
|
||||
workspace_1.location = (["/tmp", "/tmp3"]).into();
|
||||
db.save_workspace(workspace_1.clone()).await;
|
||||
db.save_workspace(workspace_1).await;
|
||||
|
||||
workspace_2.dock_pane.children.push(SerializedItem {
|
||||
kind: Arc::from("Test"),
|
||||
item_id: 10,
|
||||
active: true,
|
||||
});
|
||||
db.save_workspace(workspace_2).await;
|
||||
|
||||
let test_text_2 = db
|
||||
@@ -644,16 +632,6 @@ mod tests {
|
||||
|
||||
let db = WorkspaceDb(open_test_db("test_full_workspace_serialization").await);
|
||||
|
||||
let dock_pane = crate::persistence::model::SerializedPane {
|
||||
children: vec![
|
||||
SerializedItem::new("Terminal", 1, false),
|
||||
SerializedItem::new("Terminal", 2, false),
|
||||
SerializedItem::new("Terminal", 3, true),
|
||||
SerializedItem::new("Terminal", 4, false),
|
||||
],
|
||||
active: false,
|
||||
};
|
||||
|
||||
// -----------------
|
||||
// | 1,2 | 5,6 |
|
||||
// | - - - | |
|
||||
@@ -694,12 +672,10 @@ mod tests {
|
||||
let workspace = SerializedWorkspace {
|
||||
id: 5,
|
||||
location: (["/tmp", "/tmp2"]).into(),
|
||||
dock_position: DockPosition::Shown(DockAnchor::Bottom),
|
||||
center_group,
|
||||
dock_pane,
|
||||
left_sidebar_open: true,
|
||||
bounds: Default::default(),
|
||||
display: Default::default(),
|
||||
docks: Default::default(),
|
||||
};
|
||||
|
||||
db.save_workspace(workspace.clone()).await;
|
||||
@@ -724,23 +700,19 @@ mod tests {
|
||||
let workspace_1 = SerializedWorkspace {
|
||||
id: 1,
|
||||
location: (["/tmp", "/tmp2"]).into(),
|
||||
dock_position: crate::dock::DockPosition::Shown(DockAnchor::Bottom),
|
||||
center_group: Default::default(),
|
||||
dock_pane: Default::default(),
|
||||
left_sidebar_open: true,
|
||||
bounds: Default::default(),
|
||||
display: Default::default(),
|
||||
docks: Default::default(),
|
||||
};
|
||||
|
||||
let mut workspace_2 = SerializedWorkspace {
|
||||
id: 2,
|
||||
location: (["/tmp"]).into(),
|
||||
dock_position: crate::dock::DockPosition::Hidden(DockAnchor::Expanded),
|
||||
center_group: Default::default(),
|
||||
dock_pane: Default::default(),
|
||||
left_sidebar_open: false,
|
||||
bounds: Default::default(),
|
||||
display: Default::default(),
|
||||
docks: Default::default(),
|
||||
};
|
||||
|
||||
db.save_workspace(workspace_1.clone()).await;
|
||||
@@ -773,12 +745,10 @@ mod tests {
|
||||
let mut workspace_3 = SerializedWorkspace {
|
||||
id: 3,
|
||||
location: (&["/tmp", "/tmp2"]).into(),
|
||||
dock_position: DockPosition::Shown(DockAnchor::Right),
|
||||
center_group: Default::default(),
|
||||
dock_pane: Default::default(),
|
||||
left_sidebar_open: false,
|
||||
bounds: Default::default(),
|
||||
display: Default::default(),
|
||||
docks: Default::default(),
|
||||
};
|
||||
|
||||
db.save_workspace(workspace_3.clone()).await;
|
||||
@@ -798,52 +768,23 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
use crate::dock::DockPosition;
|
||||
use crate::persistence::model::SerializedWorkspace;
|
||||
use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup};
|
||||
|
||||
fn default_workspace<P: AsRef<Path>>(
|
||||
workspace_id: &[P],
|
||||
dock_pane: SerializedPane,
|
||||
center_group: &SerializedPaneGroup,
|
||||
) -> SerializedWorkspace {
|
||||
SerializedWorkspace {
|
||||
id: 4,
|
||||
location: workspace_id.into(),
|
||||
dock_position: crate::dock::DockPosition::Hidden(DockAnchor::Right),
|
||||
center_group: center_group.clone(),
|
||||
dock_pane,
|
||||
left_sidebar_open: true,
|
||||
bounds: Default::default(),
|
||||
display: Default::default(),
|
||||
docks: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_basic_dock_pane() {
|
||||
env_logger::try_init().ok();
|
||||
|
||||
let db = WorkspaceDb(open_test_db("basic_dock_pane").await);
|
||||
|
||||
let dock_pane = crate::persistence::model::SerializedPane::new(
|
||||
vec![
|
||||
SerializedItem::new("Terminal", 1, false),
|
||||
SerializedItem::new("Terminal", 4, false),
|
||||
SerializedItem::new("Terminal", 2, false),
|
||||
SerializedItem::new("Terminal", 3, true),
|
||||
],
|
||||
false,
|
||||
);
|
||||
|
||||
let workspace = default_workspace(&["/tmp"], dock_pane, &Default::default());
|
||||
|
||||
db.save_workspace(workspace.clone()).await;
|
||||
|
||||
let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
|
||||
|
||||
assert_eq!(workspace.dock_pane, new_workspace.dock_pane);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_simple_split() {
|
||||
env_logger::try_init().ok();
|
||||
@@ -887,7 +828,7 @@ mod tests {
|
||||
],
|
||||
};
|
||||
|
||||
let workspace = default_workspace(&["/tmp"], Default::default(), ¢er_pane);
|
||||
let workspace = default_workspace(&["/tmp"], ¢er_pane);
|
||||
|
||||
db.save_workspace(workspace.clone()).await;
|
||||
|
||||
@@ -936,7 +877,7 @@ mod tests {
|
||||
|
||||
let id = &["/tmp"];
|
||||
|
||||
let mut workspace = default_workspace(id, Default::default(), ¢er_pane);
|
||||
let mut workspace = default_workspace(id, ¢er_pane);
|
||||
|
||||
db.save_workspace(workspace.clone()).await;
|
||||
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
use crate::{
|
||||
dock::DockPosition, item::ItemHandle, DockAnchor, ItemDeserializers, Member, Pane, PaneAxis,
|
||||
Workspace, WorkspaceId,
|
||||
};
|
||||
use crate::{item::ItemHandle, ItemDeserializers, Member, Pane, PaneAxis, Workspace, WorkspaceId};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use async_recursion::async_recursion;
|
||||
use db::sqlez::{
|
||||
@@ -62,12 +59,68 @@ impl Column for WorkspaceLocation {
|
||||
pub struct SerializedWorkspace {
|
||||
pub id: WorkspaceId,
|
||||
pub location: WorkspaceLocation,
|
||||
pub dock_position: DockPosition,
|
||||
pub center_group: SerializedPaneGroup,
|
||||
pub dock_pane: SerializedPane,
|
||||
pub left_sidebar_open: bool,
|
||||
pub bounds: Option<WindowBounds>,
|
||||
pub display: Option<Uuid>,
|
||||
pub docks: DockStructure,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Default)]
|
||||
pub struct DockStructure {
|
||||
pub(crate) left: DockData,
|
||||
pub(crate) right: DockData,
|
||||
pub(crate) bottom: DockData,
|
||||
}
|
||||
|
||||
impl Column for DockStructure {
|
||||
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
|
||||
let (left, next_index) = DockData::column(statement, start_index)?;
|
||||
let (right, next_index) = DockData::column(statement, next_index)?;
|
||||
let (bottom, next_index) = DockData::column(statement, next_index)?;
|
||||
Ok((
|
||||
DockStructure {
|
||||
left,
|
||||
right,
|
||||
bottom,
|
||||
},
|
||||
next_index,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl Bind for DockStructure {
|
||||
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
|
||||
let next_index = statement.bind(&self.left, start_index)?;
|
||||
let next_index = statement.bind(&self.right, next_index)?;
|
||||
statement.bind(&self.bottom, next_index)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Default)]
|
||||
pub struct DockData {
|
||||
pub(crate) visible: bool,
|
||||
pub(crate) active_panel: Option<String>,
|
||||
}
|
||||
|
||||
impl Column for DockData {
|
||||
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
|
||||
let (visible, next_index) = Option::<bool>::column(statement, start_index)?;
|
||||
let (active_panel, next_index) = Option::<String>::column(statement, next_index)?;
|
||||
Ok((
|
||||
DockData {
|
||||
visible: visible.unwrap_or(false),
|
||||
active_panel,
|
||||
},
|
||||
next_index,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl Bind for DockData {
|
||||
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
|
||||
let next_index = statement.bind(&self.visible, start_index)?;
|
||||
statement.bind(&self.active_panel, next_index)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
@@ -266,9 +319,9 @@ impl StaticColumnCount for SerializedItem {
|
||||
}
|
||||
impl Bind for &SerializedItem {
|
||||
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
|
||||
let next_index = statement.bind(self.kind.clone(), start_index)?;
|
||||
let next_index = statement.bind(self.item_id, next_index)?;
|
||||
statement.bind(self.active, next_index)
|
||||
let next_index = statement.bind(&self.kind, start_index)?;
|
||||
let next_index = statement.bind(&self.item_id, next_index)?;
|
||||
statement.bind(&self.active, next_index)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,64 +340,3 @@ impl Column for SerializedItem {
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl StaticColumnCount for DockPosition {
|
||||
fn column_count() -> usize {
|
||||
2
|
||||
}
|
||||
}
|
||||
impl Bind for DockPosition {
|
||||
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
|
||||
let next_index = statement.bind(self.is_visible(), start_index)?;
|
||||
statement.bind(self.anchor(), next_index)
|
||||
}
|
||||
}
|
||||
|
||||
impl Column for DockPosition {
|
||||
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
|
||||
let (visible, next_index) = bool::column(statement, start_index)?;
|
||||
let (dock_anchor, next_index) = DockAnchor::column(statement, next_index)?;
|
||||
let position = if visible {
|
||||
DockPosition::Shown(dock_anchor)
|
||||
} else {
|
||||
DockPosition::Hidden(dock_anchor)
|
||||
};
|
||||
Ok((position, next_index))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::WorkspaceLocation;
|
||||
use crate::DockAnchor;
|
||||
use db::sqlez::connection::Connection;
|
||||
|
||||
#[test]
|
||||
fn test_workspace_round_trips() {
|
||||
let db = Connection::open_memory(Some("workspace_id_round_trips"));
|
||||
|
||||
db.exec(indoc::indoc! {"
|
||||
CREATE TABLE workspace_id_test(
|
||||
workspace_id INTEGER,
|
||||
dock_anchor TEXT
|
||||
);"})
|
||||
.unwrap()()
|
||||
.unwrap();
|
||||
|
||||
let workspace_id: WorkspaceLocation = WorkspaceLocation::from(&["\test2", "\test1"]);
|
||||
|
||||
db.exec_bound("INSERT INTO workspace_id_test(workspace_id, dock_anchor) VALUES (?,?)")
|
||||
.unwrap()((&workspace_id, DockAnchor::Bottom))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
db.select_row("SELECT workspace_id, dock_anchor FROM workspace_id_test LIMIT 1")
|
||||
.unwrap()()
|
||||
.unwrap(),
|
||||
Some((
|
||||
WorkspaceLocation::from(&["\test1", "\test2"]),
|
||||
DockAnchor::Bottom
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,321 +0,0 @@
|
||||
use crate::{StatusItemView, Workspace};
|
||||
use gpui::{
|
||||
elements::*, impl_actions, platform::CursorStyle, platform::MouseButton, AnyViewHandle,
|
||||
AppContext, Entity, Subscription, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use std::rc::Rc;
|
||||
|
||||
pub trait SidebarItem: View {
|
||||
fn should_activate_item_on_event(&self, _: &Self::Event, _: &AppContext) -> bool {
|
||||
false
|
||||
}
|
||||
fn should_show_badge(&self, _: &AppContext) -> bool {
|
||||
false
|
||||
}
|
||||
fn contains_focused_view(&self, _: &AppContext) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub trait SidebarItemHandle {
|
||||
fn id(&self) -> usize;
|
||||
fn should_show_badge(&self, cx: &WindowContext) -> bool;
|
||||
fn is_focused(&self, cx: &WindowContext) -> bool;
|
||||
fn as_any(&self) -> &AnyViewHandle;
|
||||
}
|
||||
|
||||
impl<T> SidebarItemHandle for ViewHandle<T>
|
||||
where
|
||||
T: SidebarItem,
|
||||
{
|
||||
fn id(&self) -> usize {
|
||||
self.id()
|
||||
}
|
||||
|
||||
fn should_show_badge(&self, cx: &WindowContext) -> bool {
|
||||
self.read(cx).should_show_badge(cx)
|
||||
}
|
||||
|
||||
fn is_focused(&self, cx: &WindowContext) -> bool {
|
||||
ViewHandle::is_focused(self, cx) || self.read(cx).contains_focused_view(cx)
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &AnyViewHandle {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&dyn SidebarItemHandle> for AnyViewHandle {
|
||||
fn from(val: &dyn SidebarItemHandle) -> Self {
|
||||
val.as_any().clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Sidebar {
|
||||
sidebar_side: SidebarSide,
|
||||
items: Vec<Item>,
|
||||
is_open: bool,
|
||||
active_item_ix: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
|
||||
pub enum SidebarSide {
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
impl SidebarSide {
|
||||
fn to_resizable_side(self) -> Side {
|
||||
match self {
|
||||
Self::Left => Side::Right,
|
||||
Self::Right => Side::Left,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Item {
|
||||
icon_path: &'static str,
|
||||
tooltip: String,
|
||||
view: Rc<dyn SidebarItemHandle>,
|
||||
_subscriptions: [Subscription; 2],
|
||||
}
|
||||
|
||||
pub struct SidebarButtons {
|
||||
sidebar: ViewHandle<Sidebar>,
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq)]
|
||||
pub struct ToggleSidebarItem {
|
||||
pub sidebar_side: SidebarSide,
|
||||
pub item_index: usize,
|
||||
}
|
||||
|
||||
impl_actions!(workspace, [ToggleSidebarItem]);
|
||||
|
||||
impl Sidebar {
|
||||
pub fn new(sidebar_side: SidebarSide) -> Self {
|
||||
Self {
|
||||
sidebar_side,
|
||||
items: Default::default(),
|
||||
active_item_ix: 0,
|
||||
is_open: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_open(&self) -> bool {
|
||||
self.is_open
|
||||
}
|
||||
|
||||
pub fn active_item_ix(&self) -> usize {
|
||||
self.active_item_ix
|
||||
}
|
||||
|
||||
pub fn set_open(&mut self, open: bool, cx: &mut ViewContext<Self>) {
|
||||
if open != self.is_open {
|
||||
self.is_open = open;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_open(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if self.is_open {}
|
||||
self.is_open = !self.is_open;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn add_item<T: SidebarItem>(
|
||||
&mut self,
|
||||
icon_path: &'static str,
|
||||
tooltip: String,
|
||||
view: ViewHandle<T>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let subscriptions = [
|
||||
cx.observe(&view, |_, _, cx| cx.notify()),
|
||||
cx.subscribe(&view, |this, view, event, cx| {
|
||||
if view.read(cx).should_activate_item_on_event(event, cx) {
|
||||
if let Some(ix) = this
|
||||
.items
|
||||
.iter()
|
||||
.position(|item| item.view.id() == view.id())
|
||||
{
|
||||
this.activate_item(ix, cx);
|
||||
}
|
||||
}
|
||||
}),
|
||||
];
|
||||
|
||||
self.items.push(Item {
|
||||
icon_path,
|
||||
tooltip,
|
||||
view: Rc::new(view),
|
||||
_subscriptions: subscriptions,
|
||||
});
|
||||
cx.notify()
|
||||
}
|
||||
|
||||
pub fn activate_item(&mut self, item_ix: usize, cx: &mut ViewContext<Self>) {
|
||||
self.active_item_ix = item_ix;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn toggle_item(&mut self, item_ix: usize, cx: &mut ViewContext<Self>) {
|
||||
if self.active_item_ix == item_ix {
|
||||
self.is_open = false;
|
||||
} else {
|
||||
self.active_item_ix = item_ix;
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn active_item(&self) -> Option<&Rc<dyn SidebarItemHandle>> {
|
||||
if self.is_open {
|
||||
self.items.get(self.active_item_ix).map(|item| &item.view)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for Sidebar {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for Sidebar {
|
||||
fn ui_name() -> &'static str {
|
||||
"Sidebar"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
if let Some(active_item) = self.active_item() {
|
||||
enum ResizeHandleTag {}
|
||||
let style = &theme::current(cx).workspace.sidebar;
|
||||
ChildView::new(active_item.as_any(), cx)
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.with_resize_handle::<ResizeHandleTag>(
|
||||
self.sidebar_side as usize,
|
||||
self.sidebar_side.to_resizable_side(),
|
||||
4.,
|
||||
style.initial_size,
|
||||
cx,
|
||||
)
|
||||
.into_any()
|
||||
} else {
|
||||
Empty::new().into_any()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SidebarButtons {
|
||||
pub fn new(
|
||||
sidebar: ViewHandle<Sidebar>,
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
cx.observe(&sidebar, |_, _, cx| cx.notify()).detach();
|
||||
Self { sidebar, workspace }
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for SidebarButtons {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for SidebarButtons {
|
||||
fn ui_name() -> &'static str {
|
||||
"SidebarToggleButton"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
let theme = &theme::current(cx);
|
||||
let tooltip_style = theme.tooltip.clone();
|
||||
let theme = &theme.workspace.status_bar.sidebar_buttons;
|
||||
let sidebar = self.sidebar.read(cx);
|
||||
let item_style = theme.item.clone();
|
||||
let badge_style = theme.badge;
|
||||
let active_ix = sidebar.active_item_ix;
|
||||
let is_open = sidebar.is_open;
|
||||
let sidebar_side = sidebar.sidebar_side;
|
||||
let group_style = match sidebar_side {
|
||||
SidebarSide::Left => theme.group_left,
|
||||
SidebarSide::Right => theme.group_right,
|
||||
};
|
||||
|
||||
#[allow(clippy::needless_collect)]
|
||||
let items = sidebar
|
||||
.items
|
||||
.iter()
|
||||
.map(|item| (item.icon_path, item.tooltip.clone(), item.view.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Flex::row()
|
||||
.with_children(items.into_iter().enumerate().map(
|
||||
|(ix, (icon_path, tooltip, item_view))| {
|
||||
let action = ToggleSidebarItem {
|
||||
sidebar_side,
|
||||
item_index: ix,
|
||||
};
|
||||
MouseEventHandler::<Self, _>::new(ix, cx, |state, cx| {
|
||||
let is_active = is_open && ix == active_ix;
|
||||
let style = item_style.style_for(state, is_active);
|
||||
Stack::new()
|
||||
.with_child(Svg::new(icon_path).with_color(style.icon_color))
|
||||
.with_children(if !is_active && item_view.should_show_badge(cx) {
|
||||
Some(
|
||||
Empty::new()
|
||||
.collapsed()
|
||||
.contained()
|
||||
.with_style(badge_style)
|
||||
.aligned()
|
||||
.bottom()
|
||||
.right(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.constrained()
|
||||
.with_width(style.icon_size)
|
||||
.with_height(style.icon_size)
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, {
|
||||
let action = action.clone();
|
||||
move |_, this, cx| {
|
||||
if let Some(workspace) = this.workspace.upgrade(cx) {
|
||||
let action = action.clone();
|
||||
cx.window_context().defer(move |cx| {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.toggle_sidebar_item(&action, cx)
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.with_tooltip::<Self>(
|
||||
ix,
|
||||
tooltip,
|
||||
Some(Box::new(action)),
|
||||
tooltip_style.clone(),
|
||||
cx,
|
||||
)
|
||||
},
|
||||
))
|
||||
.contained()
|
||||
.with_style(group_style)
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
|
||||
impl StatusItemView for SidebarButtons {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
_: Option<&dyn crate::ItemHandle>,
|
||||
_: &mut ViewContext<Self>,
|
||||
) {
|
||||
}
|
||||
}
|
||||
+559
-312
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,3 @@
|
||||
use anyhow::bail;
|
||||
use db::sqlez::{
|
||||
bindable::{Bind, Column, StaticColumnCount},
|
||||
statement::Statement,
|
||||
};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Setting;
|
||||
@@ -13,7 +8,6 @@ pub struct WorkspaceSettings {
|
||||
pub confirm_quit: bool,
|
||||
pub show_call_status_icon: bool,
|
||||
pub autosave: AutosaveSetting,
|
||||
pub default_dock_anchor: DockAnchor,
|
||||
pub git: GitSettings,
|
||||
}
|
||||
|
||||
@@ -23,7 +17,6 @@ pub struct WorkspaceSettingsContent {
|
||||
pub confirm_quit: Option<bool>,
|
||||
pub show_call_status_icon: Option<bool>,
|
||||
pub autosave: Option<AutosaveSetting>,
|
||||
pub default_dock_anchor: Option<DockAnchor>,
|
||||
pub git: Option<GitSettings>,
|
||||
}
|
||||
|
||||
@@ -36,15 +29,6 @@ pub enum AutosaveSetting {
|
||||
OnWindowChange,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, Default, Copy, Clone, Hash, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum DockAnchor {
|
||||
#[default]
|
||||
Bottom,
|
||||
Right,
|
||||
Expanded,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct GitSettings {
|
||||
pub git_gutter: Option<GitGutterSetting>,
|
||||
@@ -59,35 +43,6 @@ pub enum GitGutterSetting {
|
||||
Hide,
|
||||
}
|
||||
|
||||
impl StaticColumnCount for DockAnchor {}
|
||||
|
||||
impl Bind for DockAnchor {
|
||||
fn bind(&self, statement: &Statement, start_index: i32) -> anyhow::Result<i32> {
|
||||
match self {
|
||||
DockAnchor::Bottom => "Bottom",
|
||||
DockAnchor::Right => "Right",
|
||||
DockAnchor::Expanded => "Expanded",
|
||||
}
|
||||
.bind(statement, start_index)
|
||||
}
|
||||
}
|
||||
|
||||
impl Column for DockAnchor {
|
||||
fn column(statement: &mut Statement, start_index: i32) -> anyhow::Result<(Self, i32)> {
|
||||
String::column(statement, start_index).and_then(|(anchor_text, next_index)| {
|
||||
Ok((
|
||||
match anchor_text.as_ref() {
|
||||
"Bottom" => DockAnchor::Bottom,
|
||||
"Right" => DockAnchor::Right,
|
||||
"Expanded" => DockAnchor::Expanded,
|
||||
_ => bail!("Stored dock anchor is incorrect"),
|
||||
},
|
||||
next_index,
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Setting for WorkspaceSettings {
|
||||
const KEY: Option<&'static str> = None;
|
||||
|
||||
|
||||
@@ -56,8 +56,7 @@ use fs::RealFs;
|
||||
use staff_mode::StaffMode;
|
||||
use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt};
|
||||
use workspace::{
|
||||
dock::FocusDock, item::ItemHandle, notifications::NotifyResultExt, AppState, OpenSettings,
|
||||
Workspace,
|
||||
item::ItemHandle, notifications::NotifyResultExt, AppState, OpenSettings, Workspace,
|
||||
};
|
||||
use zed::{
|
||||
self, build_window_options, handle_keymap_file_changes, initialize_workspace, languages, menus,
|
||||
@@ -186,7 +185,6 @@ fn main() {
|
||||
fs,
|
||||
build_window_options,
|
||||
initialize_workspace,
|
||||
dock_default_item_factory,
|
||||
background_actions,
|
||||
});
|
||||
cx.set_global(Arc::downgrade(&app_state));
|
||||
@@ -816,7 +814,6 @@ pub fn background_actions() -> &'static [(&'static str, &'static dyn Action)] {
|
||||
&[
|
||||
("Go to file", &file_finder::Toggle),
|
||||
("Open command palette", &command_palette::Toggle),
|
||||
("Focus the dock", &FocusDock),
|
||||
("Open recent projects", &recent_projects::OpenRecent),
|
||||
("Change your settings", &OpenSettings),
|
||||
]
|
||||
|
||||
@@ -89,7 +89,7 @@ pub fn menus() -> Vec<Menu<'static>> {
|
||||
MenuItem::action("Zoom Out", super::DecreaseBufferFontSize),
|
||||
MenuItem::action("Reset Zoom", super::ResetBufferFontSize),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action("Toggle Left Sidebar", workspace::ToggleLeftSidebar),
|
||||
MenuItem::action("Toggle Left Dock", workspace::ToggleLeftDock),
|
||||
MenuItem::submenu(Menu {
|
||||
name: "Editor Layout",
|
||||
items: vec![
|
||||
|
||||
+139
-118
@@ -18,10 +18,11 @@ use feedback::{
|
||||
use futures::{channel::mpsc, StreamExt};
|
||||
use gpui::{
|
||||
actions,
|
||||
anyhow::{self, Result},
|
||||
geometry::vector::vec2f,
|
||||
impl_actions,
|
||||
platform::{Platform, PromptLevel, TitlebarOptions, WindowBounds, WindowKind, WindowOptions},
|
||||
AppContext, ViewContext,
|
||||
AppContext, AsyncAppContext, Task, ViewContext, WeakViewHandle,
|
||||
};
|
||||
pub use lsp;
|
||||
pub use project;
|
||||
@@ -31,13 +32,13 @@ use serde::Deserialize;
|
||||
use serde_json::to_string_pretty;
|
||||
use settings::{KeymapFileContent, SettingsStore, DEFAULT_SETTINGS_ASSET_PATH};
|
||||
use std::{borrow::Cow, str, sync::Arc};
|
||||
use terminal_view::terminal_button::TerminalButton;
|
||||
use terminal_view::terminal_panel::{self, TerminalPanel};
|
||||
use util::{channel::ReleaseChannel, paths, ResultExt};
|
||||
use uuid::Uuid;
|
||||
use welcome::BaseKeymap;
|
||||
pub use workspace;
|
||||
use workspace::{
|
||||
create_and_open_local_file, open_new, sidebar::SidebarSide, AppState, NewFile, NewWindow,
|
||||
create_and_open_local_file, dock::PanelHandle, open_new, AppState, NewFile, NewWindow,
|
||||
Workspace, WorkspaceSettings,
|
||||
};
|
||||
|
||||
@@ -223,7 +224,14 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
|
||||
|workspace: &mut Workspace,
|
||||
_: &project_panel::ToggleFocus,
|
||||
cx: &mut ViewContext<Workspace>| {
|
||||
workspace.toggle_sidebar_item_focus(SidebarSide::Left, 0, cx);
|
||||
workspace.toggle_panel_focus::<ProjectPanel>(cx);
|
||||
},
|
||||
);
|
||||
cx.add_action(
|
||||
|workspace: &mut Workspace,
|
||||
_: &terminal_panel::ToggleFocus,
|
||||
cx: &mut ViewContext<Workspace>| {
|
||||
workspace.toggle_panel_focus::<TerminalPanel>(cx);
|
||||
},
|
||||
);
|
||||
cx.add_global_action({
|
||||
@@ -252,85 +260,107 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
|
||||
}
|
||||
|
||||
pub fn initialize_workspace(
|
||||
workspace: &mut Workspace,
|
||||
app_state: &Arc<AppState>,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
let workspace_handle = cx.handle();
|
||||
cx.subscribe(&workspace_handle, {
|
||||
move |workspace, _, event, cx| {
|
||||
if let workspace::Event::PaneAdded(pane) = event {
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.toolbar().update(cx, |toolbar, cx| {
|
||||
let breadcrumbs = cx.add_view(|_| Breadcrumbs::new(workspace));
|
||||
toolbar.add_item(breadcrumbs, cx);
|
||||
let buffer_search_bar = cx.add_view(BufferSearchBar::new);
|
||||
toolbar.add_item(buffer_search_bar, cx);
|
||||
let project_search_bar = cx.add_view(|_| ProjectSearchBar::new());
|
||||
toolbar.add_item(project_search_bar, cx);
|
||||
let submit_feedback_button = cx.add_view(|_| SubmitFeedbackButton::new());
|
||||
toolbar.add_item(submit_feedback_button, cx);
|
||||
let feedback_info_text = cx.add_view(|_| FeedbackInfoText::new());
|
||||
toolbar.add_item(feedback_info_text, cx);
|
||||
let lsp_log_item = cx.add_view(|_| {
|
||||
lsp_log::LspLogToolbarItemView::new(workspace.project().clone())
|
||||
workspace_handle: WeakViewHandle<Workspace>,
|
||||
was_deserialized: bool,
|
||||
app_state: Arc<AppState>,
|
||||
cx: AsyncAppContext,
|
||||
) -> Task<Result<()>> {
|
||||
cx.spawn(|mut cx| async move {
|
||||
workspace_handle.update(&mut cx, |workspace, cx| {
|
||||
let workspace_handle = cx.handle();
|
||||
cx.subscribe(&workspace_handle, {
|
||||
move |workspace, _, event, cx| {
|
||||
if let workspace::Event::PaneAdded(pane) = event {
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.toolbar().update(cx, |toolbar, cx| {
|
||||
let breadcrumbs = cx.add_view(|_| Breadcrumbs::new(workspace));
|
||||
toolbar.add_item(breadcrumbs, cx);
|
||||
let buffer_search_bar = cx.add_view(BufferSearchBar::new);
|
||||
toolbar.add_item(buffer_search_bar, cx);
|
||||
let project_search_bar = cx.add_view(|_| ProjectSearchBar::new());
|
||||
toolbar.add_item(project_search_bar, cx);
|
||||
let submit_feedback_button =
|
||||
cx.add_view(|_| SubmitFeedbackButton::new());
|
||||
toolbar.add_item(submit_feedback_button, cx);
|
||||
let feedback_info_text = cx.add_view(|_| FeedbackInfoText::new());
|
||||
toolbar.add_item(feedback_info_text, cx);
|
||||
let lsp_log_item = cx.add_view(|_| {
|
||||
lsp_log::LspLogToolbarItemView::new(workspace.project().clone())
|
||||
});
|
||||
toolbar.add_item(lsp_log_item, cx);
|
||||
})
|
||||
});
|
||||
toolbar.add_item(lsp_log_item, cx);
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.emit(workspace::Event::PaneAdded(workspace.active_pane().clone()));
|
||||
|
||||
let collab_titlebar_item =
|
||||
cx.add_view(|cx| CollabTitlebarItem::new(workspace, &workspace_handle, cx));
|
||||
workspace.set_titlebar_item(collab_titlebar_item.into_any(), cx);
|
||||
|
||||
let copilot =
|
||||
cx.add_view(|cx| copilot_button::CopilotButton::new(app_state.fs.clone(), cx));
|
||||
let diagnostic_summary =
|
||||
cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx));
|
||||
let activity_indicator = activity_indicator::ActivityIndicator::new(
|
||||
workspace,
|
||||
app_state.languages.clone(),
|
||||
cx,
|
||||
);
|
||||
let active_buffer_language =
|
||||
cx.add_view(|_| language_selector::ActiveBufferLanguage::new(workspace));
|
||||
let feedback_button = cx.add_view(|_| {
|
||||
feedback::deploy_feedback_button::DeployFeedbackButton::new(workspace)
|
||||
});
|
||||
let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new());
|
||||
workspace.status_bar().update(cx, |status_bar, cx| {
|
||||
status_bar.add_left_item(diagnostic_summary, cx);
|
||||
status_bar.add_left_item(activity_indicator, cx);
|
||||
status_bar.add_right_item(feedback_button, cx);
|
||||
status_bar.add_right_item(copilot, cx);
|
||||
status_bar.add_right_item(active_buffer_language, cx);
|
||||
status_bar.add_right_item(cursor_position, cx);
|
||||
});
|
||||
|
||||
auto_update::notify_of_any_new_update(cx.weak_handle(), cx);
|
||||
|
||||
vim::observe_keystrokes(cx);
|
||||
|
||||
cx.on_window_should_close(|workspace, cx| {
|
||||
if let Some(task) = workspace.close(&Default::default(), cx) {
|
||||
task.detach_and_log_err(cx);
|
||||
}
|
||||
false
|
||||
});
|
||||
})?;
|
||||
|
||||
let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());
|
||||
let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone());
|
||||
let (project_panel, terminal_panel) = futures::try_join!(project_panel, terminal_panel)?;
|
||||
workspace_handle.update(&mut cx, |workspace, cx| {
|
||||
let project_panel_position = project_panel.position(cx);
|
||||
workspace.add_panel(project_panel, cx);
|
||||
if !was_deserialized
|
||||
&& workspace
|
||||
.project()
|
||||
.read(cx)
|
||||
.visible_worktrees(cx)
|
||||
.any(|tree| {
|
||||
tree.read(cx)
|
||||
.root_entry()
|
||||
.map_or(false, |entry| entry.is_dir())
|
||||
})
|
||||
});
|
||||
{
|
||||
workspace.toggle_dock(project_panel_position, cx);
|
||||
}
|
||||
}
|
||||
|
||||
workspace.add_panel(terminal_panel, cx)
|
||||
})?;
|
||||
Ok(())
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.emit(workspace::Event::PaneAdded(workspace.active_pane().clone()));
|
||||
cx.emit(workspace::Event::PaneAdded(workspace.dock_pane().clone()));
|
||||
|
||||
let collab_titlebar_item =
|
||||
cx.add_view(|cx| CollabTitlebarItem::new(workspace, &workspace_handle, cx));
|
||||
workspace.set_titlebar_item(collab_titlebar_item.into_any(), cx);
|
||||
|
||||
let project_panel = ProjectPanel::new(workspace, cx);
|
||||
workspace.left_sidebar().update(cx, |sidebar, cx| {
|
||||
sidebar.add_item(
|
||||
"icons/folder_tree_16.svg",
|
||||
"Project Panel".to_string(),
|
||||
project_panel,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let toggle_terminal = cx.add_view(|cx| TerminalButton::new(workspace_handle.clone(), cx));
|
||||
let copilot = cx.add_view(|cx| copilot_button::CopilotButton::new(app_state.fs.clone(), cx));
|
||||
let diagnostic_summary =
|
||||
cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx));
|
||||
let activity_indicator =
|
||||
activity_indicator::ActivityIndicator::new(workspace, app_state.languages.clone(), cx);
|
||||
let active_buffer_language =
|
||||
cx.add_view(|_| language_selector::ActiveBufferLanguage::new(workspace));
|
||||
let feedback_button =
|
||||
cx.add_view(|_| feedback::deploy_feedback_button::DeployFeedbackButton::new(workspace));
|
||||
let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new());
|
||||
workspace.status_bar().update(cx, |status_bar, cx| {
|
||||
status_bar.add_left_item(diagnostic_summary, cx);
|
||||
status_bar.add_left_item(activity_indicator, cx);
|
||||
status_bar.add_right_item(toggle_terminal, cx);
|
||||
status_bar.add_right_item(feedback_button, cx);
|
||||
status_bar.add_right_item(copilot, cx);
|
||||
status_bar.add_right_item(active_buffer_language, cx);
|
||||
status_bar.add_right_item(cursor_position, cx);
|
||||
});
|
||||
|
||||
auto_update::notify_of_any_new_update(cx.weak_handle(), cx);
|
||||
|
||||
vim::observe_keystrokes(cx);
|
||||
|
||||
cx.on_window_should_close(|workspace, cx| {
|
||||
if let Some(task) = workspace.close(&Default::default(), cx) {
|
||||
task.detach_and_log_err(cx);
|
||||
}
|
||||
false
|
||||
});
|
||||
}
|
||||
|
||||
pub fn build_window_options(
|
||||
@@ -348,7 +378,8 @@ pub fn build_window_options(
|
||||
traffic_light_position: Some(vec2f(8., 8.)),
|
||||
}),
|
||||
center: false,
|
||||
focus: true,
|
||||
focus: false,
|
||||
show: false,
|
||||
kind: WindowKind::Normal,
|
||||
is_movable: true,
|
||||
bounds,
|
||||
@@ -687,7 +718,7 @@ mod tests {
|
||||
.unwrap();
|
||||
workspace_1.update(cx, |workspace, cx| {
|
||||
assert_eq!(workspace.worktrees(cx).count(), 2);
|
||||
assert!(workspace.left_sidebar().read(cx).is_open());
|
||||
assert!(workspace.left_dock().read(cx).is_open());
|
||||
assert!(workspace.active_pane().is_focused(cx));
|
||||
});
|
||||
|
||||
@@ -730,7 +761,7 @@ mod tests {
|
||||
.collect::<Vec<_>>(),
|
||||
&[Path::new("/root/c").into(), Path::new("/root/d").into()]
|
||||
);
|
||||
assert!(workspace.left_sidebar().read(cx).is_open());
|
||||
assert!(workspace.left_dock().read(cx).is_open());
|
||||
assert!(workspace.active_pane().is_focused(cx));
|
||||
});
|
||||
}
|
||||
@@ -755,6 +786,7 @@ mod tests {
|
||||
.unwrap()
|
||||
.downcast::<Workspace>()
|
||||
.unwrap();
|
||||
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
|
||||
let editor = workspace.read_with(cx, |workspace, cx| {
|
||||
workspace
|
||||
.active_item(cx)
|
||||
@@ -777,9 +809,9 @@ mod tests {
|
||||
assert!(cx.is_window_edited(workspace.window_id()));
|
||||
|
||||
// Closing the item restores the window's edited state.
|
||||
let close = workspace.update(cx, |workspace, cx| {
|
||||
let close = pane.update(cx, |pane, cx| {
|
||||
drop(editor);
|
||||
Pane::close_active_item(workspace, &Default::default(), cx).unwrap()
|
||||
pane.close_active_item(&Default::default(), cx).unwrap()
|
||||
});
|
||||
executor.run_until_parked();
|
||||
cx.simulate_prompt_answer(workspace.window_id(), 1);
|
||||
@@ -1364,7 +1396,7 @@ mod tests {
|
||||
|
||||
cx.foreground().run_until_parked();
|
||||
workspace.read_with(cx, |workspace, _| {
|
||||
assert_eq!(workspace.panes().len(), 2); //Center pane + Dock pane
|
||||
assert_eq!(workspace.panes().len(), 1);
|
||||
assert_eq!(workspace.active_pane(), &pane_1);
|
||||
});
|
||||
|
||||
@@ -1374,7 +1406,7 @@ mod tests {
|
||||
cx.foreground().run_until_parked();
|
||||
|
||||
workspace.read_with(cx, |workspace, cx| {
|
||||
assert_eq!(workspace.panes().len(), 2);
|
||||
assert_eq!(workspace.panes().len(), 1);
|
||||
assert!(workspace.active_item(cx).is_none());
|
||||
});
|
||||
|
||||
@@ -1403,6 +1435,7 @@ mod tests {
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
|
||||
|
||||
let entries = cx.read(|cx| workspace.file_project_paths(cx));
|
||||
let file1 = entries[0].clone();
|
||||
@@ -1520,14 +1553,13 @@ mod tests {
|
||||
|
||||
// Go forward to an item that has been closed, ensuring it gets re-opened at the same
|
||||
// location.
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
let editor3_id = editor3.id();
|
||||
drop(editor3);
|
||||
Pane::close_item_by_id(workspace, workspace.active_pane().clone(), editor3_id, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
pane.update(cx, |pane, cx| {
|
||||
let editor3_id = editor3.id();
|
||||
drop(editor3);
|
||||
pane.close_item_by_id(editor3_id, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
workspace
|
||||
.update(cx, |w, cx| Pane::go_forward(w, None, cx))
|
||||
.await
|
||||
@@ -1556,14 +1588,13 @@ mod tests {
|
||||
);
|
||||
|
||||
// Go back to an item that has been closed and removed from disk, ensuring it gets skipped.
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
let editor2_id = editor2.id();
|
||||
drop(editor2);
|
||||
Pane::close_item_by_id(workspace, workspace.active_pane().clone(), editor2_id, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
pane.update(cx, |pane, cx| {
|
||||
let editor2_id = editor2.id();
|
||||
drop(editor2);
|
||||
pane.close_item_by_id(editor2_id, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
app_state
|
||||
.fs
|
||||
.remove_file(Path::new("/root/a/file2"), Default::default())
|
||||
@@ -1712,34 +1743,22 @@ mod tests {
|
||||
assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
|
||||
|
||||
// Close all the pane items in some arbitrary order.
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
Pane::close_item_by_id(workspace, pane.clone(), file1_item_id, cx)
|
||||
})
|
||||
pane.update(cx, |pane, cx| pane.close_item_by_id(file1_item_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
|
||||
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
Pane::close_item_by_id(workspace, pane.clone(), file4_item_id, cx)
|
||||
})
|
||||
pane.update(cx, |pane, cx| pane.close_item_by_id(file4_item_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
|
||||
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
Pane::close_item_by_id(workspace, pane.clone(), file2_item_id, cx)
|
||||
})
|
||||
pane.update(cx, |pane, cx| pane.close_item_by_id(file2_item_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
|
||||
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
Pane::close_item_by_id(workspace, pane.clone(), file3_item_id, cx)
|
||||
})
|
||||
pane.update(cx, |pane, cx| pane.close_item_by_id(file3_item_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(active_path(&workspace, cx), None);
|
||||
@@ -2070,6 +2089,8 @@ mod tests {
|
||||
editor::init(cx);
|
||||
project_panel::init_settings(cx);
|
||||
pane::init(cx);
|
||||
project_panel::init(cx);
|
||||
terminal_view::init(cx);
|
||||
app_state
|
||||
})
|
||||
}
|
||||
|
||||
@@ -93,10 +93,11 @@ export default function statusBar(colorScheme: ColorScheme) {
|
||||
},
|
||||
},
|
||||
},
|
||||
sidebarButtons: {
|
||||
panelButtons: {
|
||||
groupLeft: {},
|
||||
groupBottom: {},
|
||||
groupRight: {},
|
||||
item: {
|
||||
button: {
|
||||
...statusContainer,
|
||||
iconSize: 16,
|
||||
iconColor: foreground(layer, "variant"),
|
||||
|
||||
@@ -118,9 +118,25 @@ export default function workspace(colorScheme: ColorScheme) {
|
||||
},
|
||||
cursor: "Arrow",
|
||||
},
|
||||
sidebar: {
|
||||
initialSize: 240,
|
||||
border: border(layer, { left: true, right: true }),
|
||||
zoomedBackground: {
|
||||
padding: 10,
|
||||
cursor: "Arrow",
|
||||
background: withOpacity(background(colorScheme.lowest), 0.5)
|
||||
},
|
||||
zoomedForeground: {
|
||||
shadow: colorScheme.modalShadow,
|
||||
border: border(colorScheme.highest, { overlay: true }),
|
||||
},
|
||||
dock: {
|
||||
left: {
|
||||
border: border(layer, { right: true }),
|
||||
},
|
||||
bottom: {
|
||||
border: border(layer, { top: true }),
|
||||
},
|
||||
right: {
|
||||
border: border(layer, { left: true }),
|
||||
}
|
||||
},
|
||||
paneDivider: {
|
||||
color: borderColor(layer),
|
||||
@@ -310,19 +326,6 @@ export default function workspace(colorScheme: ColorScheme) {
|
||||
width: 400,
|
||||
margin: { right: 10, bottom: 10 },
|
||||
},
|
||||
dock: {
|
||||
initialSizeRight: 640,
|
||||
initialSizeBottom: 304,
|
||||
wash_color: withOpacity(background(colorScheme.highest), 0.5),
|
||||
panel: {
|
||||
border: border(colorScheme.middle),
|
||||
},
|
||||
maximized: {
|
||||
margin: 32,
|
||||
border: border(colorScheme.highest, { overlay: true }),
|
||||
shadow: colorScheme.modalShadow,
|
||||
},
|
||||
},
|
||||
dropTargetOverlayColor: withOpacity(foreground(layer, "variant"), 0.5),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user