Detailed changes
@@ -2,4 +2,12 @@
Release Notes:
-* [[Added foo / Fixed bar / No notes]]
+Use `N/A` in this section if this item should be skipped in the release notes.
+
+Add release note lines here:
+
+* (Added|Fixed|Improved) ... ([#<public_issue_number_if_exists>](https://github.com/zed-industries/community/issues/<public_issue_number_if_exists>)).
+* ...
+
+If the release notes are only intended for a specific release channel only, add `(<release_channel>-only)` to the end of the release note line.
+These will be removed by the person making the release.
@@ -2054,7 +2054,6 @@ dependencies = [
"futures 0.3.28",
"fuzzy",
"git",
- "glob",
"gpui",
"indoc",
"itertools",
@@ -3458,7 +3457,7 @@ dependencies = [
"futures 0.3.28",
"fuzzy",
"git",
- "glob",
+ "globset",
"gpui",
"indoc",
"lazy_static",
@@ -4867,7 +4866,7 @@ dependencies = [
"fuzzy",
"git",
"git2",
- "glob",
+ "globset",
"gpui",
"ignore",
"itertools",
@@ -4903,8 +4902,10 @@ dependencies = [
name = "project_panel"
version = "0.1.0"
dependencies = [
+ "anyhow",
"client",
"context_menu",
+ "db",
"drag_and_drop",
"editor",
"futures 0.3.28",
@@ -4913,6 +4914,9 @@ dependencies = [
"menu",
"postage",
"project",
+ "schemars",
+ "serde",
+ "serde_derive",
"serde_json",
"settings",
"theme",
@@ -5965,7 +5969,7 @@ dependencies = [
"collections",
"editor",
"futures 0.3.28",
- "glob",
+ "globset",
"gpui",
"language",
"log",
@@ -6127,7 +6131,6 @@ dependencies = [
"collections",
"fs",
"futures 0.3.28",
- "glob",
"gpui",
"json_comments",
"lazy_static",
@@ -78,7 +78,7 @@ async-trait = { version = "0.1" }
ctor = { version = "0.1" }
env_logger = { version = "0.9" }
futures = { version = "0.3" }
-glob = { version = "0.3.1" }
+globset = { version = "0.4" }
indoc = "1"
isahc = "1.7.2"
lazy_static = { version = "1.4.0" }
@@ -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"
}
},
{
@@ -67,10 +68,12 @@
"cmd-z": "editor::Undo",
"cmd-shift-z": "editor::Redo",
"up": "editor::MoveUp",
+ "ctrl-up": "editor::MoveToStartOfParagraph",
"pageup": "editor::PageUp",
"shift-pageup": "editor::MovePageUp",
"home": "editor::MoveToBeginningOfLine",
"down": "editor::MoveDown",
+ "ctrl-down": "editor::MoveToEndOfParagraph",
"pagedown": "editor::PageDown",
"shift-pagedown": "editor::MovePageDown",
"end": "editor::MoveToEndOfLine",
@@ -103,6 +106,8 @@
"alt-shift-b": "editor::SelectToPreviousWordStart",
"alt-shift-right": "editor::SelectToNextWordEnd",
"alt-shift-f": "editor::SelectToNextWordEnd",
+ "ctrl-shift-up": "editor::SelectToStartOfParagraph",
+ "ctrl-shift-down": "editor::SelectToEndOfParagraph",
"cmd-shift-up": "editor::SelectToBeginning",
"cmd-shift-down": "editor::SelectToEnd",
"cmd-a": "editor::SelectAll",
@@ -225,7 +230,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
@@ -367,7 +373,30 @@
"workspace::ActivatePane",
8
],
- "cmd-b": "workspace::ToggleLeftSidebar",
+ "cmd-b": [
+ "workspace::ToggleLeftDock",
+ { "focus": true }
+ ],
+ "cmd-shift-b": [
+ "workspace::ToggleLeftDock",
+ { "focus": false }
+ ],
+ "cmd-r": [
+ "workspace::ToggleRightDock",
+ { "focus": true }
+ ],
+ "cmd-shift-r": [
+ "workspace::ToggleRightDock",
+ { "focus": false }
+ ],
+ "cmd-j": [
+ "workspace::ToggleBottomDock",
+ { "focus": true }
+ ],
+ "cmd-shift-j": [
+ "workspace::ToggleBottomDock",
+ { "focus": false }
+ ],
"cmd-shift-f": "workspace::NewSearch",
"cmd-k cmd-t": "theme_selector::Toggle",
"cmd-k cmd-s": "zed::OpenKeymap",
@@ -461,32 +490,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": {}
}
]
@@ -52,19 +52,32 @@
// 3. Draw all invisible symbols:
// "all"
"show_whitespaces": "selection",
- // Whether to show the scrollbar in the editor.
- // This setting can take four values:
- //
- // 1. Show the scrollbar if there's important information or
- // follow the system's configured behavior (default):
- // "auto"
- // 2. Match the system's configured behavior:
- // "system"
- // 3. Always show the scrollbar:
- // "always"
- // 4. Never show the scrollbar:
- // "never"
- "show_scrollbars": "auto",
+ // Scrollbar related settings
+ "scrollbar": {
+ // When to show the scrollbar in the editor.
+ // This setting can take four values:
+ //
+ // 1. Show the scrollbar if there's important information or
+ // follow the system's configured behavior (default):
+ // "auto"
+ // 2. Match the system's configured behavior:
+ // "system"
+ // 3. Always show the scrollbar:
+ // "always"
+ // 4. Never show the scrollbar:
+ // "never"
+ "show": "auto",
+ // Whether to show git diff indicators in the scrollbar.
+ "git_diff": true
+ },
+ "project_panel": {
+ // Whether to show the git status in the project panel.
+ "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,
// Whether to use language servers to provide code intelligence.
@@ -81,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,
@@ -181,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
@@ -21,3 +21,6 @@ workspace = { path = "../workspace" }
futures.workspace = true
smallvec.workspace = true
+
+[dev-dependencies]
+editor = { path = "../editor", features = ["test-support"] }
@@ -339,7 +339,7 @@ pub struct TelemetrySettings {
pub metrics: bool,
}
-#[derive(Clone, Serialize, Deserialize, JsonSchema)]
+#[derive(Default, Clone, Serialize, Deserialize, JsonSchema)]
pub struct TelemetrySettingsContent {
pub diagnostics: Option<bool>,
pub metrics: Option<bool>,
@@ -10,6 +10,7 @@ use parking_lot::Mutex;
use serde::Serialize;
use serde_json::json;
use std::{
+ env,
io::Write,
mem,
path::PathBuf,
@@ -33,8 +34,9 @@ struct TelemetryState {
installation_id: Option<Arc<str>>, // Per app installation
app_version: Option<Arc<str>>,
release_channel: Option<&'static str>,
- os_version: Option<Arc<str>>,
os_name: &'static str,
+ os_version: Option<Arc<str>>,
+ architecture: &'static str,
mixpanel_events_queue: Vec<MixpanelEvent>,
clickhouse_events_queue: Vec<ClickhouseEventWrapper>,
next_mixpanel_event_id: usize,
@@ -63,6 +65,7 @@ struct ClickhouseEventRequestBody {
app_version: Option<Arc<str>>,
os_name: &'static str,
os_version: Option<Arc<str>>,
+ architecture: &'static str,
release_channel: Option<&'static str>,
events: Vec<ClickhouseEventWrapper>,
}
@@ -153,12 +156,14 @@ impl Telemetry {
} else {
None
};
+ // TODO: Replace all hardware stuff with nested SystemSpecs json
let this = Arc::new(Self {
http_client: client,
executor: cx.background().clone(),
state: Mutex::new(TelemetryState {
- os_version: platform.os_version().ok().map(|v| v.to_string().into()),
os_name: platform.os_name().into(),
+ os_version: platform.os_version().ok().map(|v| v.to_string().into()),
+ architecture: env::consts::ARCH,
app_version: platform.app_version().ok().map(|v| v.to_string().into()),
release_channel,
installation_id: None,
@@ -451,6 +456,8 @@ impl Telemetry {
app_version: state.app_version.clone(),
os_name: state.os_name,
os_version: state.os_version.clone(),
+ architecture: state.architecture,
+
release_channel: state.release_channel,
events,
},
@@ -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: || &[],
});
@@ -2437,7 +2437,7 @@ async fn test_git_diff_base_change(
buffer_local_a.read_with(cx_a, |buffer, _| {
assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
git::diff::assert_hunks(
- buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
+ buffer.snapshot().git_diff_hunks_in_row_range(0..4),
&buffer,
&diff_base,
&[(1..2, "", "two\n")],
@@ -2457,7 +2457,7 @@ async fn test_git_diff_base_change(
buffer_remote_a.read_with(cx_b, |buffer, _| {
assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
git::diff::assert_hunks(
- buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
+ buffer.snapshot().git_diff_hunks_in_row_range(0..4),
&buffer,
&diff_base,
&[(1..2, "", "two\n")],
@@ -2481,7 +2481,7 @@ async fn test_git_diff_base_change(
assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
git::diff::assert_hunks(
- buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
+ buffer.snapshot().git_diff_hunks_in_row_range(0..4),
&buffer,
&diff_base,
&[(2..3, "", "three\n")],
@@ -2492,7 +2492,7 @@ async fn test_git_diff_base_change(
buffer_remote_a.read_with(cx_b, |buffer, _| {
assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
git::diff::assert_hunks(
- buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
+ buffer.snapshot().git_diff_hunks_in_row_range(0..4),
&buffer,
&diff_base,
&[(2..3, "", "three\n")],
@@ -2535,7 +2535,7 @@ async fn test_git_diff_base_change(
buffer_local_b.read_with(cx_a, |buffer, _| {
assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
git::diff::assert_hunks(
- buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
+ buffer.snapshot().git_diff_hunks_in_row_range(0..4),
&buffer,
&diff_base,
&[(1..2, "", "two\n")],
@@ -2555,7 +2555,7 @@ async fn test_git_diff_base_change(
buffer_remote_b.read_with(cx_b, |buffer, _| {
assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
git::diff::assert_hunks(
- buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
+ buffer.snapshot().git_diff_hunks_in_row_range(0..4),
&buffer,
&diff_base,
&[(1..2, "", "two\n")],
@@ -2583,12 +2583,12 @@ async fn test_git_diff_base_change(
"{:?}",
buffer
.snapshot()
- .git_diff_hunks_in_row_range(0..4, false)
+ .git_diff_hunks_in_row_range(0..4)
.collect::<Vec<_>>()
);
git::diff::assert_hunks(
- buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
+ buffer.snapshot().git_diff_hunks_in_row_range(0..4),
&buffer,
&diff_base,
&[(2..3, "", "three\n")],
@@ -2599,7 +2599,7 @@ async fn test_git_diff_base_change(
buffer_remote_b.read_with(cx_b, |buffer, _| {
assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
git::diff::assert_hunks(
- buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
+ buffer.snapshot().git_diff_hunks_in_row_range(0..4),
&buffer,
&diff_base,
&[(2..3, "", "three\n")],
@@ -2688,6 +2688,7 @@ async fn test_git_branch_name(
});
let project_remote_c = client_c.build_remote_project(project_id, cx_c).await;
+ deterministic.run_until_parked();
project_remote_c.read_with(cx_c, |project, cx| {
assert_branch(Some("branch-2"), project, cx)
});
@@ -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,
@@ -23,3 +23,6 @@ workspace = { path = "../workspace" }
anyhow.workspace = true
smol.workspace = true
futures.workspace = true
+
+[dev-dependencies]
+editor = { path = "../editor", features = ["test-support"] }
@@ -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()
@@ -335,10 +335,9 @@ async fn configure_disabled_globs(
.get::<AllLanguageSettings>(None)
.copilot
.disabled_globs
- .clone()
.iter()
- .map(|glob| glob.as_str().to_string())
- .collect::<Vec<_>>()
+ .map(|glob| glob.glob().to_string())
+ .collect()
});
if let Some(path_to_disable) = &path_to_disable {
@@ -33,7 +33,7 @@ use theme::ThemeSettings;
use util::TryFutureExt;
use workspace::{
item::{BreadcrumbText, Item, ItemEvent, ItemHandle},
- ItemNavHistory, Pane, ToolbarItemLocation, Workspace,
+ ItemNavHistory, Pane, PaneBackdrop, ToolbarItemLocation, Workspace,
};
actions!(diagnostics, [Deploy]);
@@ -90,11 +90,15 @@ impl View for ProjectDiagnosticsEditor {
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
if self.path_states.is_empty() {
let theme = &theme::current(cx).project_diagnostics;
- Label::new("No problems in workspace", theme.empty_message.clone())
- .aligned()
- .contained()
- .with_style(theme.container)
- .into_any()
+ PaneBackdrop::new(
+ cx.view_id(),
+ Label::new("No problems in workspace", theme.empty_message.clone())
+ .aligned()
+ .contained()
+ .with_style(theme.container)
+ .into_any(),
+ )
+ .into_any()
} else {
ChildView::new(&self.editor, cx).into_any()
}
@@ -161,8 +165,13 @@ impl ProjectDiagnosticsEditor {
editor.set_vertical_scroll_margin(5, cx);
editor
});
- cx.subscribe(&editor, |_, _, event, cx| cx.emit(event.clone()))
- .detach();
+ cx.subscribe(&editor, |this, _, event, cx| {
+ cx.emit(event.clone());
+ if event == &editor::Event::Focused && this.path_states.is_empty() {
+ cx.focus_self()
+ }
+ })
+ .detach();
let project = project_handle.read(cx);
let paths_to_update = project
@@ -49,8 +49,7 @@ workspace = { path = "../workspace" }
aho-corasick = "0.7"
anyhow.workspace = true
futures.workspace = true
-glob.workspace = true
-indoc.workspace = true
+indoc = "1.0.4"
itertools = "0.10"
lazy_static.workspace = true
log.workspace = true
@@ -82,7 +81,6 @@ workspace = { path = "../workspace", features = ["test-support"] }
ctor.workspace = true
env_logger.workspace = true
-glob.workspace = true
rand.workspace = true
unindent.workspace = true
tree-sitter = "0.20"
@@ -20,6 +20,7 @@ mod editor_tests;
#[cfg(any(test, feature = "test-support"))]
pub mod test;
+use ::git::diff::DiffHunk;
use aho_corasick::AhoCorasick;
use anyhow::{anyhow, Result};
use blink_manager::BlinkManager;
@@ -215,6 +216,8 @@ actions!(
MoveToNextSubwordEnd,
MoveToBeginningOfLine,
MoveToEndOfLine,
+ MoveToStartOfParagraph,
+ MoveToEndOfParagraph,
MoveToBeginning,
MoveToEnd,
SelectUp,
@@ -225,6 +228,8 @@ actions!(
SelectToPreviousSubwordStart,
SelectToNextWordEnd,
SelectToNextSubwordEnd,
+ SelectToStartOfParagraph,
+ SelectToEndOfParagraph,
SelectToBeginning,
SelectToEnd,
SelectAll,
@@ -336,6 +341,8 @@ pub fn init(cx: &mut AppContext) {
cx.add_action(Editor::move_to_next_subword_end);
cx.add_action(Editor::move_to_beginning_of_line);
cx.add_action(Editor::move_to_end_of_line);
+ cx.add_action(Editor::move_to_start_of_paragraph);
+ cx.add_action(Editor::move_to_end_of_paragraph);
cx.add_action(Editor::move_to_beginning);
cx.add_action(Editor::move_to_end);
cx.add_action(Editor::select_up);
@@ -348,6 +355,8 @@ pub fn init(cx: &mut AppContext) {
cx.add_action(Editor::select_to_next_subword_end);
cx.add_action(Editor::select_to_beginning_of_line);
cx.add_action(Editor::select_to_end_of_line);
+ cx.add_action(Editor::select_to_start_of_paragraph);
+ cx.add_action(Editor::select_to_end_of_paragraph);
cx.add_action(Editor::select_to_beginning);
cx.add_action(Editor::select_to_end);
cx.add_action(Editor::select_all);
@@ -524,15 +533,6 @@ pub struct EditorSnapshot {
ongoing_scroll: OngoingScroll,
}
-impl EditorSnapshot {
- fn has_scrollbar_info(&self) -> bool {
- self.buffer_snapshot
- .git_diff_hunks_in_range(0..self.max_point().row(), false)
- .next()
- .is_some()
- }
-}
-
#[derive(Clone, Debug)]
struct SelectionHistoryEntry {
selections: Arc<[Selection<Anchor>]>,
@@ -4761,6 +4761,80 @@ impl Editor {
});
}
+ pub fn move_to_start_of_paragraph(
+ &mut self,
+ _: &MoveToStartOfParagraph,
+ cx: &mut ViewContext<Self>,
+ ) {
+ if matches!(self.mode, EditorMode::SingleLine) {
+ cx.propagate_action();
+ return;
+ }
+
+ self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.move_with(|map, selection| {
+ selection.collapse_to(
+ movement::start_of_paragraph(map, selection.head()),
+ SelectionGoal::None,
+ )
+ });
+ })
+ }
+
+ pub fn move_to_end_of_paragraph(
+ &mut self,
+ _: &MoveToEndOfParagraph,
+ cx: &mut ViewContext<Self>,
+ ) {
+ if matches!(self.mode, EditorMode::SingleLine) {
+ cx.propagate_action();
+ return;
+ }
+
+ self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.move_with(|map, selection| {
+ selection.collapse_to(
+ movement::end_of_paragraph(map, selection.head()),
+ SelectionGoal::None,
+ )
+ });
+ })
+ }
+
+ pub fn select_to_start_of_paragraph(
+ &mut self,
+ _: &SelectToStartOfParagraph,
+ cx: &mut ViewContext<Self>,
+ ) {
+ if matches!(self.mode, EditorMode::SingleLine) {
+ cx.propagate_action();
+ return;
+ }
+
+ self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.move_heads_with(|map, head, _| {
+ (movement::start_of_paragraph(map, head), SelectionGoal::None)
+ });
+ })
+ }
+
+ pub fn select_to_end_of_paragraph(
+ &mut self,
+ _: &SelectToEndOfParagraph,
+ cx: &mut ViewContext<Self>,
+ ) {
+ if matches!(self.mode, EditorMode::SingleLine) {
+ cx.propagate_action();
+ return;
+ }
+
+ self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.move_heads_with(|map, head, _| {
+ (movement::end_of_paragraph(map, head), SelectionGoal::None)
+ });
+ })
+ }
+
pub fn move_to_beginning(&mut self, _: &MoveToBeginning, cx: &mut ViewContext<Self>) {
if matches!(self.mode, EditorMode::SingleLine) {
cx.propagate_action();
@@ -5569,68 +5643,91 @@ impl Editor {
}
fn go_to_hunk(&mut self, _: &GoToHunk, cx: &mut ViewContext<Self>) {
- self.go_to_hunk_impl(Direction::Next, cx)
- }
+ let snapshot = self
+ .display_map
+ .update(cx, |display_map, cx| display_map.snapshot(cx));
+ let selection = self.selections.newest::<Point>(cx);
- fn go_to_prev_hunk(&mut self, _: &GoToPrevHunk, cx: &mut ViewContext<Self>) {
- self.go_to_hunk_impl(Direction::Prev, cx)
+ if !self.seek_in_direction(
+ &snapshot,
+ selection.head(),
+ false,
+ snapshot
+ .buffer_snapshot
+ .git_diff_hunks_in_range((selection.head().row + 1)..u32::MAX),
+ cx,
+ ) {
+ let wrapped_point = Point::zero();
+ self.seek_in_direction(
+ &snapshot,
+ wrapped_point,
+ true,
+ snapshot
+ .buffer_snapshot
+ .git_diff_hunks_in_range((wrapped_point.row + 1)..u32::MAX),
+ cx,
+ );
+ }
}
- pub fn go_to_hunk_impl(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
+ fn go_to_prev_hunk(&mut self, _: &GoToPrevHunk, cx: &mut ViewContext<Self>) {
let snapshot = self
.display_map
.update(cx, |display_map, cx| display_map.snapshot(cx));
let selection = self.selections.newest::<Point>(cx);
- fn seek_in_direction(
- this: &mut Editor,
- snapshot: &DisplaySnapshot,
- initial_point: Point,
- is_wrapped: bool,
- direction: Direction,
- cx: &mut ViewContext<Editor>,
- ) -> bool {
- let hunks = if direction == Direction::Next {
- snapshot
- .buffer_snapshot
- .git_diff_hunks_in_range(initial_point.row..u32::MAX, false)
- } else {
+ if !self.seek_in_direction(
+ &snapshot,
+ selection.head(),
+ false,
+ snapshot
+ .buffer_snapshot
+ .git_diff_hunks_in_range_rev(0..selection.head().row),
+ cx,
+ ) {
+ let wrapped_point = snapshot.buffer_snapshot.max_point();
+ self.seek_in_direction(
+ &snapshot,
+ wrapped_point,
+ true,
snapshot
.buffer_snapshot
- .git_diff_hunks_in_range(0..initial_point.row, true)
- };
-
- let display_point = initial_point.to_display_point(snapshot);
- let mut hunks = hunks
- .map(|hunk| diff_hunk_to_display(hunk, &snapshot))
- .skip_while(|hunk| {
- if is_wrapped {
- false
- } else {
- hunk.contains_display_row(display_point.row())
- }
- })
- .dedup();
+ .git_diff_hunks_in_range_rev(0..wrapped_point.row),
+ cx,
+ );
+ }
+ }
- if let Some(hunk) = hunks.next() {
- this.change_selections(Some(Autoscroll::fit()), cx, |s| {
- let row = hunk.start_display_row();
- let point = DisplayPoint::new(row, 0);
- s.select_display_ranges([point..point]);
- });
+ fn seek_in_direction(
+ &mut self,
+ snapshot: &DisplaySnapshot,
+ initial_point: Point,
+ is_wrapped: bool,
+ hunks: impl Iterator<Item = DiffHunk<u32>>,
+ cx: &mut ViewContext<Editor>,
+ ) -> bool {
+ let display_point = initial_point.to_display_point(snapshot);
+ let mut hunks = hunks
+ .map(|hunk| diff_hunk_to_display(hunk, &snapshot))
+ .skip_while(|hunk| {
+ if is_wrapped {
+ false
+ } else {
+ hunk.contains_display_row(display_point.row())
+ }
+ })
+ .dedup();
- true
- } else {
- false
- }
- }
+ if let Some(hunk) = hunks.next() {
+ self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ let row = hunk.start_display_row();
+ let point = DisplayPoint::new(row, 0);
+ s.select_display_ranges([point..point]);
+ });
- if !seek_in_direction(self, &snapshot, selection.head(), false, direction, cx) {
- let wrapped_point = match direction {
- Direction::Next => Point::zero(),
- Direction::Prev => snapshot.buffer_snapshot.max_point(),
- };
- seek_in_direction(self, &snapshot, wrapped_point, true, direction, cx);
+ true
+ } else {
+ false
}
}
@@ -7104,6 +7201,7 @@ pub enum Event {
BufferEdited,
Edited,
Reparsed,
+ Focused,
Blurred,
DirtyChanged,
Saved,
@@ -7157,6 +7255,7 @@ impl View for Editor {
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
if cx.is_self_focused() {
let focused_event = EditorFocused(cx.handle());
+ cx.emit(Event::Focused);
cx.emit_global(focused_event);
}
if let Some(rename) = self.pending_rename.as_ref() {
@@ -7,25 +7,36 @@ pub struct EditorSettings {
pub cursor_blink: bool,
pub hover_popover_enabled: bool,
pub show_completions_on_input: bool,
- pub show_scrollbars: ShowScrollbars,
+ pub scrollbar: Scrollbar,
}
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+pub struct Scrollbar {
+ pub show: ShowScrollbar,
+ pub git_diff: bool,
+}
+
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
-pub enum ShowScrollbars {
- #[default]
+pub enum ShowScrollbar {
Auto,
System,
Always,
Never,
}
-#[derive(Clone, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
pub struct EditorSettingsContent {
pub cursor_blink: Option<bool>,
pub hover_popover_enabled: Option<bool>,
pub show_completions_on_input: Option<bool>,
- pub show_scrollbars: Option<ShowScrollbars>,
+ pub scrollbar: Option<ScrollbarContent>,
+}
+
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+pub struct ScrollbarContent {
+ pub show: Option<ShowScrollbar>,
+ pub git_diff: Option<bool>,
}
impl Setting for EditorSettings {
@@ -1243,6 +1243,118 @@ fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
});
}
+#[gpui::test]
+async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+ let mut cx = EditorTestContext::new(cx);
+
+ let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache()));
+ cx.simulate_window_resize(cx.window_id, vec2f(100., 4. * line_height));
+
+ cx.set_state(
+ &r#"ˇone
+ two
+
+ three
+ fourˇ
+ five
+
+ six"#
+ .unindent(),
+ );
+
+ cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx));
+ cx.assert_editor_state(
+ &r#"one
+ two
+ ˇ
+ three
+ four
+ five
+ ˇ
+ six"#
+ .unindent(),
+ );
+
+ cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx));
+ cx.assert_editor_state(
+ &r#"one
+ two
+
+ three
+ four
+ five
+ ˇ
+ sixˇ"#
+ .unindent(),
+ );
+
+ cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx));
+ cx.assert_editor_state(
+ &r#"ˇone
+ two
+
+ three
+ four
+ five
+
+ sixˇ"#
+ .unindent(),
+ );
+
+ cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx));
+ cx.assert_editor_state(
+ &r#"ˇone
+ two
+ ˇ
+ three
+ four
+ five
+
+ six"#
+ .unindent(),
+ );
+
+ cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
+ cx.assert_editor_state(
+ &r#"ˇone
+ two
+
+ three
+ four
+ five
+
+ sixˇ"#
+ .unindent(),
+ );
+
+ cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
+ cx.assert_editor_state(
+ &r#"one
+ two
+
+ three
+ four
+ five
+ ˇ
+ sixˇ"#
+ .unindent(),
+ );
+
+ cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
+ cx.assert_editor_state(
+ &r#"one
+ two
+ ˇ
+ three
+ four
+ five
+ ˇ
+ six"#
+ .unindent(),
+ );
+}
+
#[gpui::test]
async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
@@ -5,7 +5,7 @@ use super::{
};
use crate::{
display_map::{BlockStyle, DisplaySnapshot, FoldStatus, TransformBlock},
- editor_settings::ShowScrollbars,
+ editor_settings::ShowScrollbar,
git::{diff_hunk_to_display, DisplayDiffHunk},
hover_popover::{
hide_hover, hover_at, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH,
@@ -50,6 +50,7 @@ use std::{
ops::Range,
sync::Arc,
};
+use text::Point;
use workspace::{item::Item, GitGutterSetting, WorkspaceSettings};
enum FoldMarkers {}
@@ -651,7 +652,7 @@ impl EditorElement {
//TODO: This rendering is entirely a horrible hack
DiffHunkStatus::Removed => {
- let row = *display_row_range.start();
+ let row = display_row_range.start;
let offset = line_height / 2.;
let start_y = row as f32 * line_height - offset - scroll_top;
@@ -673,11 +674,11 @@ impl EditorElement {
}
};
- let start_row = *display_row_range.start();
- let end_row = *display_row_range.end();
+ let start_row = display_row_range.start;
+ let end_row = display_row_range.end;
let start_y = start_row as f32 * line_height - scroll_top;
- let end_y = end_row as f32 * line_height - scroll_top + line_height;
+ let end_y = end_row as f32 * line_height - scroll_top;
let width = diff_style.width_em * line_height;
let highlight_origin = bounds.origin() + vec2f(-width, start_y);
@@ -1051,47 +1052,53 @@ impl EditorElement {
..Default::default()
});
- let diff_style = theme::current(cx).editor.diff.clone();
- for hunk in layout
- .position_map
- .snapshot
- .buffer_snapshot
- .git_diff_hunks_in_range(0..(max_row.floor() as u32), false)
- {
- let start_y = y_for_row(hunk.buffer_range.start as f32);
- let mut end_y = if hunk.buffer_range.start == hunk.buffer_range.end {
- y_for_row((hunk.buffer_range.end + 1) as f32)
- } else {
- y_for_row((hunk.buffer_range.end) as f32)
- };
+ if layout.is_singleton && settings::get::<EditorSettings>(cx).scrollbar.git_diff {
+ let diff_style = theme::current(cx).editor.scrollbar.git.clone();
+ for hunk in layout
+ .position_map
+ .snapshot
+ .buffer_snapshot
+ .git_diff_hunks_in_range(0..(max_row.floor() as u32))
+ {
+ let start_display = Point::new(hunk.buffer_range.start, 0)
+ .to_display_point(&layout.position_map.snapshot.display_snapshot);
+ let end_display = Point::new(hunk.buffer_range.end, 0)
+ .to_display_point(&layout.position_map.snapshot.display_snapshot);
+ let start_y = y_for_row(start_display.row() as f32);
+ let mut end_y = if hunk.buffer_range.start == hunk.buffer_range.end {
+ y_for_row((end_display.row() + 1) as f32)
+ } else {
+ y_for_row((end_display.row()) as f32)
+ };
- if end_y - start_y < 1. {
- end_y = start_y + 1.;
- }
- let bounds = RectF::from_points(vec2f(left, start_y), vec2f(right, end_y));
-
- let color = match hunk.status() {
- DiffHunkStatus::Added => diff_style.inserted,
- DiffHunkStatus::Modified => diff_style.modified,
- DiffHunkStatus::Removed => diff_style.deleted,
- };
-
- let border = Border {
- width: 1.,
- color: style.thumb.border.color,
- overlay: false,
- top: false,
- right: true,
- bottom: false,
- left: true,
- };
+ if end_y - start_y < 1. {
+ end_y = start_y + 1.;
+ }
+ let bounds = RectF::from_points(vec2f(left, start_y), vec2f(right, end_y));
- scene.push_quad(Quad {
- bounds,
- background: Some(color),
- border,
- corner_radius: style.thumb.corner_radius,
- })
+ let color = match hunk.status() {
+ DiffHunkStatus::Added => diff_style.inserted,
+ DiffHunkStatus::Modified => diff_style.modified,
+ DiffHunkStatus::Removed => diff_style.deleted,
+ };
+
+ let border = Border {
+ width: 1.,
+ color: style.thumb.border.color,
+ overlay: false,
+ top: false,
+ right: true,
+ bottom: false,
+ left: true,
+ };
+
+ scene.push_quad(Quad {
+ bounds,
+ background: Some(color),
+ border,
+ corner_radius: style.thumb.corner_radius,
+ })
+ }
}
scene.push_quad(Quad {
@@ -1269,7 +1276,7 @@ impl EditorElement {
.row;
buffer_snapshot
- .git_diff_hunks_in_range(buffer_start_row..buffer_end_row, false)
+ .git_diff_hunks_in_range(buffer_start_row..buffer_end_row)
.map(|hunk| diff_hunk_to_display(hunk, snapshot))
.dedup()
.collect()
@@ -2060,13 +2067,17 @@ impl Element<Editor> for EditorElement {
));
}
- let show_scrollbars = match settings::get::<EditorSettings>(cx).show_scrollbars {
- ShowScrollbars::Auto => {
- snapshot.has_scrollbar_info() || editor.scroll_manager.scrollbars_visible()
+ let scrollbar_settings = &settings::get::<EditorSettings>(cx).scrollbar;
+ let show_scrollbars = match scrollbar_settings.show {
+ ShowScrollbar::Auto => {
+ // Git
+ (is_singleton && scrollbar_settings.git_diff && snapshot.buffer_snapshot.has_git_diffs())
+ // Scrollmanager
+ || editor.scroll_manager.scrollbars_visible()
}
- ShowScrollbars::System => editor.scroll_manager.scrollbars_visible(),
- ShowScrollbars::Always => true,
- ShowScrollbars::Never => false,
+ ShowScrollbar::System => editor.scroll_manager.scrollbars_visible(),
+ ShowScrollbar::Always => true,
+ ShowScrollbar::Never => false,
};
let include_root = editor
@@ -2285,6 +2296,7 @@ impl Element<Editor> for EditorElement {
text_size,
scrollbar_row_range,
show_scrollbars,
+ is_singleton,
max_row,
gutter_margin,
active_rows,
@@ -2440,6 +2452,7 @@ pub struct LayoutState {
selections: Vec<(ReplicaId, Vec<SelectionLayout>)>,
scrollbar_row_range: Range<f32>,
show_scrollbars: bool,
+ is_singleton: bool,
max_row: u32,
context_menu: Option<(DisplayPoint, AnyElement<Editor>)>,
code_actions_indicator: Option<(u32, AnyElement<Editor>)>,
@@ -1,4 +1,4 @@
-use std::ops::RangeInclusive;
+use std::ops::Range;
use git::diff::{DiffHunk, DiffHunkStatus};
use language::Point;
@@ -15,7 +15,7 @@ pub enum DisplayDiffHunk {
},
Unfolded {
- display_row_range: RangeInclusive<u32>,
+ display_row_range: Range<u32>,
status: DiffHunkStatus,
},
}
@@ -26,7 +26,7 @@ impl DisplayDiffHunk {
&DisplayDiffHunk::Folded { display_row } => display_row,
DisplayDiffHunk::Unfolded {
display_row_range, ..
- } => *display_row_range.start(),
+ } => display_row_range.start,
}
}
@@ -36,7 +36,7 @@ impl DisplayDiffHunk {
DisplayDiffHunk::Unfolded {
display_row_range, ..
- } => display_row_range.clone(),
+ } => display_row_range.start..=display_row_range.end - 1,
};
range.contains(&display_row)
@@ -77,16 +77,12 @@ pub fn diff_hunk_to_display(hunk: DiffHunk<u32>, snapshot: &DisplaySnapshot) ->
} else {
let start = hunk_start_point.to_display_point(snapshot).row();
- let hunk_end_row_inclusive = hunk
- .buffer_range
- .end
- .saturating_sub(1)
- .max(hunk.buffer_range.start);
+ let hunk_end_row_inclusive = hunk.buffer_range.end.max(hunk.buffer_range.start);
let hunk_end_point = Point::new(hunk_end_row_inclusive, 0);
let end = hunk_end_point.to_display_point(snapshot).row();
DisplayDiffHunk::Unfolded {
- display_row_range: start..=end,
+ display_row_range: start..end,
status: hunk.status(),
}
}
@@ -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!()
}
}
}
@@ -193,6 +193,44 @@ pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPo
})
}
+pub fn start_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
+ let point = display_point.to_point(map);
+ if point.row == 0 {
+ return map.max_point();
+ }
+
+ let mut found_non_blank_line = false;
+ for row in (0..point.row + 1).rev() {
+ let blank = map.buffer_snapshot.is_line_blank(row);
+ if found_non_blank_line && blank {
+ return Point::new(row, 0).to_display_point(map);
+ }
+
+ found_non_blank_line |= !blank;
+ }
+
+ DisplayPoint::zero()
+}
+
+pub fn end_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
+ let point = display_point.to_point(map);
+ if point.row == map.max_buffer_row() {
+ return DisplayPoint::zero();
+ }
+
+ let mut found_non_blank_line = false;
+ for row in point.row..map.max_buffer_row() + 1 {
+ let blank = map.buffer_snapshot.is_line_blank(row);
+ if found_non_blank_line && blank {
+ return Point::new(row, 0).to_display_point(map);
+ }
+
+ found_non_blank_line |= !blank;
+ }
+
+ map.max_point()
+}
+
/// Scans for a boundary preceding the given start point `from` until a boundary is found, indicated by the
/// given predicate returning true. The predicate is called with the character to the left and right
/// of the candidate boundary location, and will be called with `\n` characters indicating the start
@@ -1140,6 +1140,10 @@ impl MultiBuffer {
let mut result = Vec::new();
let mut cursor = snapshot.excerpts.cursor::<usize>();
cursor.seek(&start, Bias::Right, &());
+ if cursor.item().is_none() {
+ cursor.prev(&());
+ }
+
while let Some(excerpt) = cursor.item() {
if *cursor.start() > end {
break;
@@ -2841,20 +2845,24 @@ impl MultiBufferSnapshot {
})
}
- pub fn git_diff_hunks_in_range<'a>(
+ pub fn has_git_diffs(&self) -> bool {
+ for excerpt in self.excerpts.iter() {
+ if !excerpt.buffer.git_diff.is_empty() {
+ return true;
+ }
+ }
+ false
+ }
+
+ pub fn git_diff_hunks_in_range_rev<'a>(
&'a self,
row_range: Range<u32>,
- reversed: bool,
) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
let mut cursor = self.excerpts.cursor::<Point>();
- if reversed {
- cursor.seek(&Point::new(row_range.end, 0), Bias::Left, &());
- if cursor.item().is_none() {
- cursor.prev(&());
- }
- } else {
- cursor.seek(&Point::new(row_range.start, 0), Bias::Right, &());
+ cursor.seek(&Point::new(row_range.end, 0), Bias::Left, &());
+ if cursor.item().is_none() {
+ cursor.prev(&());
}
std::iter::from_fn(move || {
@@ -2884,7 +2892,7 @@ impl MultiBufferSnapshot {
let buffer_hunks = excerpt
.buffer
- .git_diff_hunks_intersecting_range(buffer_start..buffer_end, reversed)
+ .git_diff_hunks_intersecting_range_rev(buffer_start..buffer_end)
.filter_map(move |hunk| {
let start = multibuffer_start.row
+ hunk
@@ -2904,12 +2912,70 @@ impl MultiBufferSnapshot {
})
});
- if reversed {
- cursor.prev(&());
- } else {
- cursor.next(&());
+ cursor.prev(&());
+
+ Some(buffer_hunks)
+ })
+ .flatten()
+ }
+
+ pub fn git_diff_hunks_in_range<'a>(
+ &'a self,
+ row_range: Range<u32>,
+ ) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
+ let mut cursor = self.excerpts.cursor::<Point>();
+
+ cursor.seek(&Point::new(row_range.start, 0), Bias::Right, &());
+
+ std::iter::from_fn(move || {
+ let excerpt = cursor.item()?;
+ let multibuffer_start = *cursor.start();
+ let multibuffer_end = multibuffer_start + excerpt.text_summary.lines;
+ if multibuffer_start.row >= row_range.end {
+ return None;
+ }
+
+ let mut buffer_start = excerpt.range.context.start;
+ let mut buffer_end = excerpt.range.context.end;
+ let excerpt_start_point = buffer_start.to_point(&excerpt.buffer);
+ let excerpt_end_point = excerpt_start_point + excerpt.text_summary.lines;
+
+ if row_range.start > multibuffer_start.row {
+ let buffer_start_point =
+ excerpt_start_point + Point::new(row_range.start - multibuffer_start.row, 0);
+ buffer_start = excerpt.buffer.anchor_before(buffer_start_point);
}
+ if row_range.end < multibuffer_end.row {
+ let buffer_end_point =
+ excerpt_start_point + Point::new(row_range.end - multibuffer_start.row, 0);
+ buffer_end = excerpt.buffer.anchor_before(buffer_end_point);
+ }
+
+ let buffer_hunks = excerpt
+ .buffer
+ .git_diff_hunks_intersecting_range(buffer_start..buffer_end)
+ .filter_map(move |hunk| {
+ let start = multibuffer_start.row
+ + hunk
+ .buffer_range
+ .start
+ .saturating_sub(excerpt_start_point.row);
+ let end = multibuffer_start.row
+ + hunk
+ .buffer_range
+ .end
+ .min(excerpt_end_point.row + 1)
+ .saturating_sub(excerpt_start_point.row);
+
+ Some(DiffHunk {
+ buffer_range: start..end,
+ diff_base_byte_range: hunk.diff_base_byte_range.clone(),
+ })
+ });
+
+ cursor.next(&());
+
Some(buffer_hunks)
})
.flatten()
@@ -4647,7 +4713,7 @@ mod tests {
assert_eq!(
snapshot
- .git_diff_hunks_in_range(0..12, false)
+ .git_diff_hunks_in_range(0..12)
.map(|hunk| (hunk.status(), hunk.buffer_range))
.collect::<Vec<_>>(),
&expected,
@@ -4655,7 +4721,7 @@ mod tests {
assert_eq!(
snapshot
- .git_diff_hunks_in_range(0..12, true)
+ .git_diff_hunks_in_range_rev(0..12)
.map(|hunk| (hunk.status(), hunk.buffer_range))
.collect::<Vec<_>>(),
expected
@@ -5010,16 +5076,19 @@ mod tests {
.read(cx)
.range_to_buffer_ranges(start_ix..end_ix, cx);
let excerpted_buffers_text = excerpted_buffer_ranges
- .into_iter()
+ .iter()
.map(|(buffer, buffer_range)| {
buffer
.read(cx)
- .text_for_range(buffer_range)
+ .text_for_range(buffer_range.clone())
.collect::<String>()
})
.collect::<Vec<_>>()
.join("\n");
assert_eq!(excerpted_buffers_text, text_for_range);
+ if !expected_excerpts.is_empty() {
+ assert!(!excerpted_buffer_ranges.is_empty());
+ }
let expected_summary = TextSummary::from(&expected_text[start_ix..end_ix]);
assert_eq!(
@@ -204,6 +204,7 @@ impl<'a> EditorTestContext<'a> {
self.assert_selections(expected_selections, marked_text.to_string())
}
+ #[track_caller]
pub fn assert_editor_background_highlights<Tag: 'static>(&mut self, marked_text: &str) {
let expected_ranges = self.ranges(marked_text);
let actual_ranges: Vec<Range<usize>> = self.update_editor(|editor, cx| {
@@ -220,6 +221,7 @@ impl<'a> EditorTestContext<'a> {
assert_set_eq!(actual_ranges, expected_ranges);
}
+ #[track_caller]
pub fn assert_editor_text_highlights<Tag: ?Sized + 'static>(&mut self, marked_text: &str) {
let expected_ranges = self.ranges(marked_text);
let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
@@ -233,12 +235,14 @@ impl<'a> EditorTestContext<'a> {
assert_set_eq!(actual_ranges, expected_ranges);
}
+ #[track_caller]
pub fn assert_editor_selections(&mut self, expected_selections: Vec<Range<usize>>) {
let expected_marked_text =
generate_marked_text(&self.buffer_text(), &expected_selections, true);
self.assert_selections(expected_selections, expected_marked_text)
}
+ #[track_caller]
fn assert_selections(
&mut self,
expected_selections: Vec<Range<usize>>,
@@ -35,3 +35,6 @@ serde_derive.workspace = true
sysinfo = "0.27.1"
tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" }
urlencoding = "2.1.2"
+
+[dev-dependencies]
+editor = { path = "../editor", features = ["test-support"] }
@@ -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")
@@ -23,6 +23,7 @@ workspace = { path = "../workspace" }
postage.workspace = true
[dev-dependencies]
+editor = { path = "../editor", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
language = { path = "../language", features = ["test-support"] }
workspace = { path = "../workspace", features = ["test-support"] }
@@ -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() {
@@ -1,4 +1,4 @@
-use std::ops::Range;
+use std::{iter, ops::Range};
use sum_tree::SumTree;
use text::{Anchor, BufferSnapshot, OffsetRangeExt, Point};
@@ -71,22 +71,25 @@ impl BufferDiff {
}
}
+ pub fn is_empty(&self) -> bool {
+ self.tree.is_empty()
+ }
+
pub fn hunks_in_row_range<'a>(
&'a self,
range: Range<u32>,
buffer: &'a BufferSnapshot,
- reversed: bool,
) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
let start = buffer.anchor_before(Point::new(range.start, 0));
let end = buffer.anchor_after(Point::new(range.end, 0));
- self.hunks_intersecting_range(start..end, buffer, reversed)
+
+ self.hunks_intersecting_range(start..end, buffer)
}
pub fn hunks_intersecting_range<'a>(
&'a self,
range: Range<Anchor>,
buffer: &'a BufferSnapshot,
- reversed: bool,
) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
let mut cursor = self.tree.filter::<_, DiffHunkSummary>(move |summary| {
let before_start = summary.buffer_range.end.cmp(&range.start, buffer).is_lt();
@@ -94,15 +97,51 @@ impl BufferDiff {
!before_start && !after_end
});
- std::iter::from_fn(move || {
- if reversed {
- cursor.prev(buffer);
+ let anchor_iter = std::iter::from_fn(move || {
+ cursor.next(buffer);
+ cursor.item()
+ })
+ .flat_map(move |hunk| {
+ [
+ (&hunk.buffer_range.start, hunk.diff_base_byte_range.start),
+ (&hunk.buffer_range.end, hunk.diff_base_byte_range.end),
+ ]
+ .into_iter()
+ });
+
+ let mut summaries = buffer.summaries_for_anchors_with_payload::<Point, _, _>(anchor_iter);
+ iter::from_fn(move || {
+ let (start_point, start_base) = summaries.next()?;
+ let (end_point, end_base) = summaries.next()?;
+
+ let end_row = if end_point.column > 0 {
+ end_point.row + 1
} else {
- cursor.next(buffer);
- }
+ end_point.row
+ };
- let hunk = cursor.item()?;
+ Some(DiffHunk {
+ buffer_range: start_point.row..end_row,
+ diff_base_byte_range: start_base..end_base,
+ })
+ })
+ }
+ pub fn hunks_intersecting_range_rev<'a>(
+ &'a self,
+ range: Range<Anchor>,
+ buffer: &'a BufferSnapshot,
+ ) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
+ let mut cursor = self.tree.filter::<_, DiffHunkSummary>(move |summary| {
+ let before_start = summary.buffer_range.end.cmp(&range.start, buffer).is_lt();
+ let after_end = summary.buffer_range.start.cmp(&range.end, buffer).is_gt();
+ !before_start && !after_end
+ });
+
+ std::iter::from_fn(move || {
+ cursor.prev(buffer);
+
+ let hunk = cursor.item()?;
let range = hunk.buffer_range.to_point(buffer);
let end_row = if range.end.column > 0 {
range.end.row + 1
@@ -151,7 +190,7 @@ impl BufferDiff {
fn hunks<'a>(&'a self, text: &'a BufferSnapshot) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
let start = text.anchor_before(Point::new(0, 0));
let end = text.anchor_after(Point::new(u32::MAX, u32::MAX));
- self.hunks_intersecting_range(start..end, text, false)
+ self.hunks_intersecting_range(start..end, text)
}
fn diff<'a>(head: &'a str, current: &'a str) -> Option<GitPatch<'a>> {
@@ -279,6 +318,8 @@ pub fn assert_hunks<Iter>(
#[cfg(test)]
mod tests {
+ use std::assert_eq;
+
use super::*;
use text::Buffer;
use unindent::Unindent as _;
@@ -365,7 +406,7 @@ mod tests {
assert_eq!(diff.hunks(&buffer).count(), 8);
assert_hunks(
- diff.hunks_in_row_range(7..12, &buffer, false),
+ diff.hunks_in_row_range(7..12, &buffer),
&buffer,
&diff_base,
&[
@@ -18,3 +18,6 @@ workspace = { path = "../workspace" }
postage.workspace = true
theme = { path = "../theme" }
util = { path = "../util" }
+
+[dev-dependencies]
+editor = { path = "../editor", features = ["test-support"] }
@@ -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,
@@ -270,7 +270,7 @@ impl TestAppContext {
.borrow_mut()
.pop_front()
.expect("prompt was not called");
- let _ = done_tx.try_send(answer);
+ done_tx.try_send(answer).ok();
}
pub fn has_pending_prompt(&self, window_id: usize) -> bool {
@@ -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> {
@@ -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> {
- side: Side,
- handle_size: f32,
child: AnyElement<V>,
- state: Rc<ResizeHandleState>,
- _state_handle: ElementStateHandle<Rc<ResizeHandleState>>,
+ handle_side: HandleSide,
+ handle_size: f32,
+ on_resize: Rc<RefCell<dyn FnMut(&mut V, f32, &mut ViewContext<V>)>>,
}
+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);
}
@@ -22,3 +22,6 @@ serde.workspace = true
schemars.workspace = true
log.workspace = true
shellexpand = "2.1.0"
+
+[dev-dependencies]
+editor = { path = "../editor", features = ["test-support"] }
@@ -41,7 +41,7 @@ anyhow.workspace = true
async-broadcast = "0.4"
async-trait.workspace = true
futures.workspace = true
-glob.workspace = true
+globset.workspace = true
lazy_static.workspace = true
log.workspace = true
parking_lot.workspace = true
@@ -1644,10 +1644,17 @@ impl Buffer {
cx: &mut ModelContext<Self>,
) {
if lamport_timestamp > self.diagnostics_timestamp {
- match self.diagnostics.binary_search_by_key(&server_id, |e| e.0) {
- Err(ix) => self.diagnostics.insert(ix, (server_id, diagnostics)),
- Ok(ix) => self.diagnostics[ix].1 = diagnostics,
- };
+ let ix = self.diagnostics.binary_search_by_key(&server_id, |e| e.0);
+ if diagnostics.len() == 0 {
+ if let Ok(ix) = ix {
+ self.diagnostics.remove(ix);
+ }
+ } else {
+ match ix {
+ Err(ix) => self.diagnostics.insert(ix, (server_id, diagnostics)),
+ Ok(ix) => self.diagnostics[ix].1 = diagnostics,
+ };
+ }
self.diagnostics_timestamp = lamport_timestamp;
self.diagnostics_update_count += 1;
self.text.lamport_clock.observe(lamport_timestamp);
@@ -2509,18 +2516,22 @@ impl BufferSnapshot {
pub fn git_diff_hunks_in_row_range<'a>(
&'a self,
range: Range<u32>,
- reversed: bool,
) -> impl 'a + Iterator<Item = git::diff::DiffHunk<u32>> {
- self.git_diff.hunks_in_row_range(range, self, reversed)
+ self.git_diff.hunks_in_row_range(range, self)
}
pub fn git_diff_hunks_intersecting_range<'a>(
&'a self,
range: Range<Anchor>,
- reversed: bool,
) -> impl 'a + Iterator<Item = git::diff::DiffHunk<u32>> {
- self.git_diff
- .hunks_intersecting_range(range, self, reversed)
+ self.git_diff.hunks_intersecting_range(range, self)
+ }
+
+ pub fn git_diff_hunks_intersecting_range_rev<'a>(
+ &'a self,
+ range: Range<Anchor>,
+ ) -> impl 'a + Iterator<Item = git::diff::DiffHunk<u32>> {
+ self.git_diff.hunks_intersecting_range_rev(range, self)
}
pub fn diagnostics_in_range<'a, T, O>(
@@ -80,6 +80,10 @@ impl DiagnosticSet {
}
}
+ pub fn len(&self) -> usize {
+ self.diagnostics.summary().count
+ }
+
pub fn iter(&self) -> impl Iterator<Item = &DiagnosticEntry<Anchor>> {
self.diagnostics.iter()
}
@@ -1,5 +1,6 @@
use anyhow::Result;
use collections::HashMap;
+use globset::GlobMatcher;
use gpui::AppContext;
use schemars::{
schema::{InstanceType, ObjectValidation, Schema, SchemaObject},
@@ -45,10 +46,10 @@ pub struct LanguageSettings {
#[derive(Clone, Debug, Default)]
pub struct CopilotSettings {
pub feature_enabled: bool,
- pub disabled_globs: Vec<glob::Pattern>,
+ pub disabled_globs: Vec<GlobMatcher>,
}
-#[derive(Clone, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
pub struct AllLanguageSettingsContent {
#[serde(default)]
pub features: Option<FeaturesContent>,
@@ -151,7 +152,7 @@ impl AllLanguageSettings {
.copilot
.disabled_globs
.iter()
- .any(|glob| glob.matches_path(path))
+ .any(|glob| glob.is_match(path))
}
pub fn copilot_enabled(&self, language_name: Option<&str>, path: Option<&Path>) -> bool {
@@ -236,7 +237,7 @@ impl settings::Setting for AllLanguageSettings {
feature_enabled: copilot_enabled,
disabled_globs: copilot_globs
.iter()
- .filter_map(|pattern| glob::Pattern::new(pattern).ok())
+ .filter_map(|g| Some(globset::Glob::new(g).ok()?.compile_matcher()))
.collect(),
},
defaults,
@@ -20,3 +20,6 @@ settings = { path = "../settings" }
util = { path = "../util" }
workspace = { path = "../workspace" }
anyhow.workspace = true
+
+[dev-dependencies]
+editor = { path = "../editor", features = ["test-support"] }
@@ -6,8 +6,8 @@
"repositoryURL": "https://github.com/livekit/client-sdk-swift.git",
"state": {
"branch": null,
- "revision": "f6ca534eb334e99acb8e82cc99b491717df28d8a",
- "version": null
+ "revision": "7331b813a5ab8a95cfb81fb2b4ed10519428b9ff",
+ "version": "1.0.12"
}
},
{
@@ -15,8 +15,8 @@
"repositoryURL": "https://github.com/google/promises.git",
"state": {
"branch": null,
- "revision": "3e4e743631e86c8c70dbc6efdc7beaa6e90fd3bb",
- "version": "2.1.1"
+ "revision": "ec957ccddbcc710ccc64c9dcbd4c7006fcf8b73a",
+ "version": "2.2.0"
}
},
{
@@ -24,8 +24,8 @@
"repositoryURL": "https://github.com/webrtc-sdk/Specs.git",
"state": {
"branch": null,
- "revision": "38ac06261e62f980652278c69b70284324c769e0",
- "version": "104.5112.5"
+ "revision": "2f6bab30c8df0fe59ab3e58bc99097f757f85f65",
+ "version": "104.5112.17"
}
},
{
@@ -33,8 +33,8 @@
"repositoryURL": "https://github.com/apple/swift-log.git",
"state": {
"branch": null,
- "revision": "6fe203dc33195667ce1759bf0182975e4653ba1c",
- "version": "1.4.4"
+ "revision": "32e8d724467f8fe623624570367e3d50c5638e46",
+ "version": "1.5.2"
}
},
{
@@ -42,8 +42,8 @@
"repositoryURL": "https://github.com/apple/swift-protobuf.git",
"state": {
"branch": null,
- "revision": "88c7d15e1242fdb6ecbafbc7926426a19be1e98a",
- "version": "1.20.2"
+ "revision": "0af9125c4eae12a4973fb66574c53a54962a9e1e",
+ "version": "1.21.0"
}
}
]
@@ -15,7 +15,7 @@ let package = Package(
targets: ["LiveKitBridge"]),
],
dependencies: [
- .package(url: "https://github.com/livekit/client-sdk-swift.git", revision: "f6ca534eb334e99acb8e82cc99b491717df28d8a"),
+ .package(url: "https://github.com/livekit/client-sdk-swift.git", .exact("1.0.12")),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
@@ -24,6 +24,7 @@ serde.workspace = true
anyhow.workspace = true
[dev-dependencies]
+editor = { path = "../editor", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }
unindent.workspace = true
@@ -22,3 +22,6 @@ workspace = { path = "../workspace" }
ordered-float.workspace = true
postage.workspace = true
smol.workspace = true
+
+[dev-dependencies]
+editor = { path = "../editor", features = ["test-support"] }
@@ -20,6 +20,7 @@ workspace = { path = "../workspace" }
parking_lot.workspace = true
[dev-dependencies]
+editor = { path = "../editor", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
serde_json.workspace = true
workspace = { path = "../workspace", features = ["test-support"] }
@@ -42,7 +42,7 @@ anyhow.workspace = true
async-trait.workspace = true
backtrace = "0.3"
futures.workspace = true
-glob.workspace = true
+globset.workspace = true
ignore = "0.4"
lazy_static.workspace = true
log.workspace = true
@@ -1,121 +0,0 @@
-use anyhow::{anyhow, Result};
-use std::path::Path;
-
-#[derive(Default)]
-pub struct LspGlobSet {
- patterns: Vec<glob::Pattern>,
-}
-
-impl LspGlobSet {
- pub fn clear(&mut self) {
- self.patterns.clear();
- }
-
- /// Add a pattern to the glob set.
- ///
- /// LSP's glob syntax supports bash-style brace expansion. For example,
- /// the pattern '*.{js,ts}' would match all JavaScript or TypeScript files.
- /// This is not a part of the standard libc glob syntax, and isn't supported
- /// by the `glob` crate. So we pre-process the glob patterns, producing a
- /// separate glob `Pattern` object for each part of a brace expansion.
- pub fn add_pattern(&mut self, pattern: &str) -> Result<()> {
- // Find all of the ranges of `pattern` that contain matched curly braces.
- let mut expansion_ranges = Vec::new();
- let mut expansion_start_ix = None;
- for (ix, c) in pattern.match_indices(|c| ['{', '}'].contains(&c)) {
- match c {
- "{" => {
- if expansion_start_ix.is_some() {
- return Err(anyhow!("nested braces in glob patterns aren't supported"));
- }
- expansion_start_ix = Some(ix);
- }
- "}" => {
- if let Some(start_ix) = expansion_start_ix {
- expansion_ranges.push(start_ix..ix + 1);
- }
- expansion_start_ix = None;
- }
- _ => {}
- }
- }
-
- // Starting with a single pattern, process each brace expansion by cloning
- // the pattern once per element of the expansion.
- let mut unexpanded_patterns = vec![];
- let mut expanded_patterns = vec![pattern.to_string()];
-
- for outer_range in expansion_ranges.into_iter().rev() {
- let inner_range = (outer_range.start + 1)..(outer_range.end - 1);
- std::mem::swap(&mut unexpanded_patterns, &mut expanded_patterns);
- for unexpanded_pattern in unexpanded_patterns.drain(..) {
- for part in unexpanded_pattern[inner_range.clone()].split(',') {
- let mut expanded_pattern = unexpanded_pattern.clone();
- expanded_pattern.replace_range(outer_range.clone(), part);
- expanded_patterns.push(expanded_pattern);
- }
- }
- }
-
- // Parse the final glob patterns and add them to the set.
- for pattern in expanded_patterns {
- let pattern = glob::Pattern::new(&pattern)?;
- self.patterns.push(pattern);
- }
-
- Ok(())
- }
-
- pub fn matches(&self, path: &Path) -> bool {
- self.patterns
- .iter()
- .any(|pattern| pattern.matches_path(path))
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn test_glob_set() {
- let mut watch = LspGlobSet::default();
- watch.add_pattern("/a/**/*.rs").unwrap();
- watch.add_pattern("/a/**/Cargo.toml").unwrap();
-
- assert!(watch.matches("/a/b.rs".as_ref()));
- assert!(watch.matches("/a/b/c.rs".as_ref()));
-
- assert!(!watch.matches("/b/c.rs".as_ref()));
- assert!(!watch.matches("/a/b.ts".as_ref()));
- }
-
- #[test]
- fn test_brace_expansion() {
- let mut watch = LspGlobSet::default();
- watch.add_pattern("/a/*.{ts,js,tsx}").unwrap();
-
- assert!(watch.matches("/a/one.js".as_ref()));
- assert!(watch.matches("/a/two.ts".as_ref()));
- assert!(watch.matches("/a/three.tsx".as_ref()));
-
- assert!(!watch.matches("/a/one.j".as_ref()));
- assert!(!watch.matches("/a/two.s".as_ref()));
- assert!(!watch.matches("/a/three.t".as_ref()));
- assert!(!watch.matches("/a/four.t".as_ref()));
- assert!(!watch.matches("/a/five.xt".as_ref()));
- }
-
- #[test]
- fn test_multiple_brace_expansion() {
- let mut watch = LspGlobSet::default();
- watch.add_pattern("/a/{one,two,three}.{b*c,d*e}").unwrap();
-
- assert!(watch.matches("/a/one.bic".as_ref()));
- assert!(watch.matches("/a/two.dole".as_ref()));
- assert!(watch.matches("/a/three.deeee".as_ref()));
-
- assert!(!watch.matches("/a/four.bic".as_ref()));
- assert!(!watch.matches("/a/one.be".as_ref()));
- }
-}
@@ -1,6 +1,5 @@
mod ignore;
mod lsp_command;
-mod lsp_glob_set;
mod project_settings;
pub mod search;
pub mod terminals;
@@ -17,8 +16,10 @@ use copilot::Copilot;
use futures::{
channel::mpsc::{self, UnboundedReceiver},
future::{try_join_all, Shared},
+ stream::FuturesUnordered,
AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt,
};
+use globset::{Glob, GlobSet, GlobSetBuilder};
use gpui::{
AnyModelHandle, AppContext, AsyncAppContext, BorrowAppContext, Entity, ModelContext,
ModelHandle, Task, WeakModelHandle,
@@ -41,7 +42,6 @@ use lsp::{
DocumentHighlightKind, LanguageServer, LanguageServerId,
};
use lsp_command::*;
-use lsp_glob_set::LspGlobSet;
use postage::watch;
use project_settings::ProjectSettings;
use rand::prelude::*;
@@ -213,6 +213,7 @@ pub enum Event {
RemoteIdChanged(Option<u64>),
DisconnectedFromHost,
Closed,
+ DeletedEntry(ProjectEntryId),
CollaboratorUpdated {
old_peer_id: proto::PeerId,
new_peer_id: proto::PeerId,
@@ -226,7 +227,7 @@ pub enum LanguageServerState {
language: Arc<Language>,
adapter: Arc<CachedLspAdapter>,
server: Arc<LanguageServer>,
- watched_paths: LspGlobSet,
+ watched_paths: HashMap<WorktreeId, GlobSet>,
simulate_disk_based_diagnostics_completion: Option<Task<()>>,
},
}
@@ -977,6 +978,9 @@ impl Project {
cx: &mut ModelContext<Self>,
) -> Option<Task<Result<()>>> {
let worktree = self.worktree_for_entry(entry_id, cx)?;
+
+ cx.emit(Event::DeletedEntry(entry_id));
+
if self.is_local() {
worktree.update(cx, |worktree, cx| {
worktree.as_local_mut().unwrap().delete_entry(entry_id, cx)
@@ -1371,7 +1375,7 @@ impl Project {
return Task::ready(Ok(existing_buffer));
}
- let mut loading_watch = match self.loading_buffers_by_path.entry(project_path.clone()) {
+ let loading_watch = match self.loading_buffers_by_path.entry(project_path.clone()) {
// If the given path is already being loaded, then wait for that existing
// task to complete and return the same buffer.
hash_map::Entry::Occupied(e) => e.get().clone(),
@@ -1402,15 +1406,9 @@ impl Project {
};
cx.foreground().spawn(async move {
- loop {
- if let Some(result) = loading_watch.borrow().as_ref() {
- match result {
- Ok(buffer) => return Ok(buffer.clone()),
- Err(error) => return Err(anyhow!("{}", error)),
- }
- }
- loading_watch.next().await;
- }
+ pump_loading_buffer_reciever(loading_watch)
+ .await
+ .map_err(|error| anyhow!("{}", error))
})
}
@@ -2562,6 +2560,23 @@ impl Project {
}
}
+ for buffer in self.opened_buffers.values() {
+ if let Some(buffer) = buffer.upgrade(cx) {
+ buffer.update(cx, |buffer, cx| {
+ buffer.update_diagnostics(server_id, Default::default(), cx);
+ });
+ }
+ }
+ for worktree in &self.worktrees {
+ if let Some(worktree) = worktree.upgrade(cx) {
+ worktree.update(cx, |worktree, cx| {
+ if let Some(worktree) = worktree.as_local_mut() {
+ worktree.clear_diagnostics_for_language_server(server_id, cx);
+ }
+ });
+ }
+ }
+
self.language_server_statuses.remove(&server_id);
cx.notify();
@@ -2867,10 +2882,37 @@ impl Project {
if let Some(LanguageServerState::Running { watched_paths, .. }) =
self.language_servers.get_mut(&language_server_id)
{
- watched_paths.clear();
+ let mut builders = HashMap::default();
for watcher in params.watchers {
- watched_paths.add_pattern(&watcher.glob_pattern).log_err();
+ for worktree in &self.worktrees {
+ if let Some(worktree) = worktree.upgrade(cx) {
+ let worktree = worktree.read(cx);
+ if let Some(abs_path) = worktree.abs_path().to_str() {
+ if let Some(suffix) = watcher
+ .glob_pattern
+ .strip_prefix(abs_path)
+ .and_then(|s| s.strip_prefix(std::path::MAIN_SEPARATOR))
+ {
+ if let Some(glob) = Glob::new(suffix).log_err() {
+ builders
+ .entry(worktree.id())
+ .or_insert_with(|| GlobSetBuilder::new())
+ .add(glob);
+ }
+ break;
+ }
+ }
+ }
+ }
}
+
+ watched_paths.clear();
+ for (worktree_id, builder) in builders {
+ if let Ok(globset) = builder.build() {
+ watched_paths.insert(worktree_id, globset);
+ }
+ }
+
cx.notify();
}
}
@@ -4707,25 +4749,39 @@ impl Project {
changes: &HashMap<(Arc<Path>, ProjectEntryId), PathChange>,
cx: &mut ModelContext<Self>,
) {
+ if changes.is_empty() {
+ return;
+ }
+
let worktree_id = worktree_handle.read(cx).id();
+ let mut language_server_ids = self
+ .language_server_ids
+ .iter()
+ .filter_map(|((server_worktree_id, _), server_id)| {
+ (*server_worktree_id == worktree_id).then_some(*server_id)
+ })
+ .collect::<Vec<_>>();
+ language_server_ids.sort();
+ language_server_ids.dedup();
+
let abs_path = worktree_handle.read(cx).abs_path();
- for ((server_worktree_id, _), server_id) in &self.language_server_ids {
- if *server_worktree_id == worktree_id {
- if let Some(server) = self.language_servers.get(server_id) {
- if let LanguageServerState::Running {
- server,
- watched_paths,
- ..
- } = server
- {
+ for server_id in &language_server_ids {
+ if let Some(server) = self.language_servers.get(server_id) {
+ if let LanguageServerState::Running {
+ server,
+ watched_paths,
+ ..
+ } = server
+ {
+ if let Some(watched_paths) = watched_paths.get(&worktree_id) {
let params = lsp::DidChangeWatchedFilesParams {
changes: changes
.iter()
.filter_map(|((path, _), change)| {
- let path = abs_path.join(path);
- if watched_paths.matches(&path) {
+ if watched_paths.is_match(&path) {
Some(lsp::FileEvent {
- uri: lsp::Url::from_file_path(path).unwrap(),
+ uri: lsp::Url::from_file_path(abs_path.join(path))
+ .unwrap(),
typ: match change {
PathChange::Added => lsp::FileChangeType::CREATED,
PathChange::Removed => lsp::FileChangeType::DELETED,
@@ -4761,6 +4817,51 @@ impl Project {
) {
debug_assert!(worktree_handle.read(cx).is_local());
+ // Setup the pending buffers
+ let future_buffers = self
+ .loading_buffers_by_path
+ .iter()
+ .filter_map(|(path, receiver)| {
+ let path = &path.path;
+ let (work_directory, repo) = repos
+ .iter()
+ .find(|(work_directory, _)| path.starts_with(work_directory))?;
+
+ let repo_relative_path = path.strip_prefix(work_directory).log_err()?;
+
+ let receiver = receiver.clone();
+ let repo_ptr = repo.repo_ptr.clone();
+ let repo_relative_path = repo_relative_path.to_owned();
+ Some(async move {
+ pump_loading_buffer_reciever(receiver)
+ .await
+ .ok()
+ .map(|buffer| (buffer, repo_relative_path, repo_ptr))
+ })
+ })
+ .collect::<FuturesUnordered<_>>()
+ .filter_map(|result| async move {
+ let (buffer_handle, repo_relative_path, repo_ptr) = result?;
+
+ let lock = repo_ptr.lock();
+ lock.load_index_text(&repo_relative_path)
+ .map(|diff_base| (diff_base, buffer_handle))
+ });
+
+ let update_diff_base_fn = update_diff_base(self);
+ cx.spawn(|_, mut cx| async move {
+ let diff_base_tasks = cx
+ .background()
+ .spawn(future_buffers.collect::<Vec<_>>())
+ .await;
+
+ for (diff_base, buffer) in diff_base_tasks.into_iter() {
+ update_diff_base_fn(Some(diff_base), buffer, &mut cx);
+ }
+ })
+ .detach();
+
+ // And the current buffers
for (_, buffer) in &self.opened_buffers {
if let Some(buffer) = buffer.upgrade(cx) {
let file = match File::from_dyn(buffer.read(cx).file()) {
@@ -4780,18 +4881,17 @@ impl Project {
.find(|(work_directory, _)| path.starts_with(work_directory))
{
Some(repo) => repo.clone(),
- None => return,
+ None => continue,
};
let relative_repo = match path.strip_prefix(work_directory).log_err() {
Some(relative_repo) => relative_repo.to_owned(),
- None => return,
+ None => continue,
};
drop(worktree);
- let remote_id = self.remote_id();
- let client = self.client.clone();
+ let update_diff_base_fn = update_diff_base(self);
let git_ptr = repo.repo_ptr.clone();
let diff_base_task = cx
.background()
@@ -4799,21 +4899,7 @@ impl Project {
cx.spawn(|_, mut cx| async move {
let diff_base = diff_base_task.await;
-
- let buffer_id = buffer.update(&mut cx, |buffer, cx| {
- buffer.set_diff_base(diff_base.clone(), cx);
- buffer.remote_id()
- });
-
- if let Some(project_id) = remote_id {
- client
- .send(proto::UpdateDiffBase {
- project_id,
- buffer_id: buffer_id as u64,
- diff_base,
- })
- .log_err();
- }
+ update_diff_base_fn(diff_base, buffer, &mut cx);
})
.detach();
}
@@ -5146,6 +5232,9 @@ impl Project {
mut cx: AsyncAppContext,
) -> Result<proto::ProjectEntryResponse> {
let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id);
+
+ this.update(&mut cx, |_, cx| cx.emit(Event::DeletedEntry(entry_id)));
+
let worktree = this.read_with(&cx, |this, cx| {
this.worktree_for_entry(entry_id, cx)
.ok_or_else(|| anyhow!("worktree not found"))
@@ -6700,3 +6789,40 @@ impl Item for Buffer {
})
}
}
+
+async fn pump_loading_buffer_reciever(
+ mut receiver: postage::watch::Receiver<Option<Result<ModelHandle<Buffer>, Arc<anyhow::Error>>>>,
+) -> Result<ModelHandle<Buffer>, Arc<anyhow::Error>> {
+ loop {
+ if let Some(result) = receiver.borrow().as_ref() {
+ match result {
+ Ok(buffer) => return Ok(buffer.to_owned()),
+ Err(e) => return Err(e.to_owned()),
+ }
+ }
+ receiver.next().await;
+ }
+}
+
+fn update_diff_base(
+ project: &Project,
+) -> impl Fn(Option<String>, ModelHandle<Buffer>, &mut AsyncAppContext) {
+ let remote_id = project.remote_id();
+ let client = project.client().clone();
+ move |diff_base, buffer, cx| {
+ let buffer_id = buffer.update(cx, |buffer, cx| {
+ buffer.set_diff_base(diff_base.clone(), cx);
+ buffer.remote_id()
+ });
+
+ if let Some(project_id) = remote_id {
+ client
+ .send(proto::UpdateDiffBase {
+ project_id,
+ buffer_id: buffer_id as u64,
+ diff_base,
+ })
+ .log_err();
+ }
+ }
+}
@@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};
use settings::Setting;
use std::sync::Arc;
-#[derive(Clone, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
pub struct ProjectSettings {
#[serde(default)]
pub lsp: HashMap<Arc<str>, LspSettings>,
@@ -1,6 +1,7 @@
use crate::{worktree::WorktreeHandle, Event, *};
use fs::{FakeFs, LineEnding, RealFs};
use futures::{future, StreamExt};
+use globset::Glob;
use gpui::{executor::Deterministic, test::subscribe, AppContext};
use language::{
language_settings::{AllLanguageSettings, LanguageSettingsContent},
@@ -505,7 +506,7 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
register_options: serde_json::to_value(
lsp::DidChangeWatchedFilesRegistrationOptions {
watchers: vec![lsp::FileSystemWatcher {
- glob_pattern: "*.{rs,c}".to_string(),
+ glob_pattern: "/the-root/*.{rs,c}".to_string(),
kind: None,
}],
},
@@ -925,6 +926,95 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC
});
}
+#[gpui::test]
+async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui::TestAppContext) {
+ init_test(cx);
+
+ let mut language = Language::new(
+ LanguageConfig {
+ path_suffixes: vec!["rs".to_string()],
+ ..Default::default()
+ },
+ None,
+ );
+ let mut fake_servers = language
+ .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+ ..Default::default()
+ }))
+ .await;
+
+ let fs = FakeFs::new(cx.background());
+ fs.insert_tree("/dir", json!({ "a.rs": "x" })).await;
+
+ let project = Project::test(fs, ["/dir".as_ref()], cx).await;
+ project.update(cx, |project, _| project.languages.add(Arc::new(language)));
+
+ let buffer = project
+ .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
+ .await
+ .unwrap();
+
+ // Publish diagnostics
+ let fake_server = fake_servers.next().await.unwrap();
+ fake_server.notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
+ uri: Url::from_file_path("/dir/a.rs").unwrap(),
+ version: None,
+ diagnostics: vec![lsp::Diagnostic {
+ range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
+ severity: Some(lsp::DiagnosticSeverity::ERROR),
+ message: "the message".to_string(),
+ ..Default::default()
+ }],
+ });
+
+ cx.foreground().run_until_parked();
+ buffer.read_with(cx, |buffer, _| {
+ assert_eq!(
+ buffer
+ .snapshot()
+ .diagnostics_in_range::<_, usize>(0..1, false)
+ .map(|entry| entry.diagnostic.message.clone())
+ .collect::<Vec<_>>(),
+ ["the message".to_string()]
+ );
+ });
+ project.read_with(cx, |project, cx| {
+ assert_eq!(
+ project.diagnostic_summary(cx),
+ DiagnosticSummary {
+ error_count: 1,
+ warning_count: 0,
+ }
+ );
+ });
+
+ project.update(cx, |project, cx| {
+ project.restart_language_servers_for_buffers([buffer.clone()], cx);
+ });
+
+ // The diagnostics are cleared.
+ cx.foreground().run_until_parked();
+ buffer.read_with(cx, |buffer, _| {
+ assert_eq!(
+ buffer
+ .snapshot()
+ .diagnostics_in_range::<_, usize>(0..1, false)
+ .map(|entry| entry.diagnostic.message.clone())
+ .collect::<Vec<_>>(),
+ Vec::<String>::new(),
+ );
+ });
+ project.read_with(cx, |project, cx| {
+ assert_eq!(
+ project.diagnostic_summary(cx),
+ DiagnosticSummary {
+ error_count: 0,
+ warning_count: 0,
+ }
+ );
+ });
+}
+
#[gpui::test]
async fn test_restarted_server_reporting_invalid_buffer_version(cx: &mut gpui::TestAppContext) {
init_test(cx);
@@ -3393,7 +3483,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
search_query,
false,
true,
- vec![glob::Pattern::new("*.odd").unwrap()],
+ vec![Glob::new("*.odd").unwrap().compile_matcher()],
Vec::new()
),
cx
@@ -3411,7 +3501,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
search_query,
false,
true,
- vec![glob::Pattern::new("*.rs").unwrap()],
+ vec![Glob::new("*.rs").unwrap().compile_matcher()],
Vec::new()
),
cx
@@ -3433,8 +3523,8 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
false,
true,
vec![
- glob::Pattern::new("*.ts").unwrap(),
- glob::Pattern::new("*.odd").unwrap(),
+ Glob::new("*.ts").unwrap().compile_matcher(),
+ Glob::new("*.odd").unwrap().compile_matcher(),
],
Vec::new()
),
@@ -3457,9 +3547,9 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
false,
true,
vec![
- glob::Pattern::new("*.rs").unwrap(),
- glob::Pattern::new("*.ts").unwrap(),
- glob::Pattern::new("*.odd").unwrap(),
+ Glob::new("*.rs").unwrap().compile_matcher(),
+ Glob::new("*.ts").unwrap().compile_matcher(),
+ Glob::new("*.odd").unwrap().compile_matcher(),
],
Vec::new()
),
@@ -3504,7 +3594,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
false,
true,
Vec::new(),
- vec![glob::Pattern::new("*.odd").unwrap()],
+ vec![Glob::new("*.odd").unwrap().compile_matcher()],
),
cx
)
@@ -3527,7 +3617,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
false,
true,
Vec::new(),
- vec![glob::Pattern::new("*.rs").unwrap()],
+ vec![Glob::new("*.rs").unwrap().compile_matcher()],
),
cx
)
@@ -3549,8 +3639,8 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
true,
Vec::new(),
vec![
- glob::Pattern::new("*.ts").unwrap(),
- glob::Pattern::new("*.odd").unwrap(),
+ Glob::new("*.ts").unwrap().compile_matcher(),
+ Glob::new("*.odd").unwrap().compile_matcher(),
],
),
cx
@@ -3573,9 +3663,9 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
true,
Vec::new(),
vec![
- glob::Pattern::new("*.rs").unwrap(),
- glob::Pattern::new("*.ts").unwrap(),
- glob::Pattern::new("*.odd").unwrap(),
+ Glob::new("*.rs").unwrap().compile_matcher(),
+ Glob::new("*.ts").unwrap().compile_matcher(),
+ Glob::new("*.odd").unwrap().compile_matcher(),
],
),
cx
@@ -3612,8 +3702,8 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
search_query,
false,
true,
- vec![glob::Pattern::new("*.odd").unwrap()],
- vec![glob::Pattern::new("*.odd").unwrap()],
+ vec![Glob::new("*.odd").unwrap().compile_matcher()],
+ vec![Glob::new("*.odd").unwrap().compile_matcher()],
),
cx
)
@@ -3630,8 +3720,8 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
search_query,
false,
true,
- vec![glob::Pattern::new("*.ts").unwrap()],
- vec![glob::Pattern::new("*.ts").unwrap()],
+ vec![Glob::new("*.ts").unwrap().compile_matcher()],
+ vec![Glob::new("*.ts").unwrap().compile_matcher()],
),
cx
)
@@ -3649,12 +3739,12 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
false,
true,
vec![
- glob::Pattern::new("*.ts").unwrap(),
- glob::Pattern::new("*.odd").unwrap()
+ Glob::new("*.ts").unwrap().compile_matcher(),
+ Glob::new("*.odd").unwrap().compile_matcher()
],
vec![
- glob::Pattern::new("*.ts").unwrap(),
- glob::Pattern::new("*.odd").unwrap()
+ Glob::new("*.ts").unwrap().compile_matcher(),
+ Glob::new("*.odd").unwrap().compile_matcher()
],
),
cx
@@ -3673,12 +3763,12 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
false,
true,
vec![
- glob::Pattern::new("*.ts").unwrap(),
- glob::Pattern::new("*.odd").unwrap()
+ Glob::new("*.ts").unwrap().compile_matcher(),
+ Glob::new("*.odd").unwrap().compile_matcher()
],
vec![
- glob::Pattern::new("*.rs").unwrap(),
- glob::Pattern::new("*.odd").unwrap()
+ Glob::new("*.rs").unwrap().compile_matcher(),
+ Glob::new("*.odd").unwrap().compile_matcher()
],
),
cx
@@ -1,6 +1,7 @@
use aho_corasick::{AhoCorasick, AhoCorasickBuilder};
use anyhow::Result;
use client::proto;
+use globset::{Glob, GlobMatcher};
use itertools::Itertools;
use language::{char_kind, Rope};
use regex::{Regex, RegexBuilder};
@@ -19,8 +20,8 @@ pub enum SearchQuery {
query: Arc<str>,
whole_word: bool,
case_sensitive: bool,
- files_to_include: Vec<glob::Pattern>,
- files_to_exclude: Vec<glob::Pattern>,
+ files_to_include: Vec<GlobMatcher>,
+ files_to_exclude: Vec<GlobMatcher>,
},
Regex {
regex: Regex,
@@ -28,8 +29,8 @@ pub enum SearchQuery {
multiline: bool,
whole_word: bool,
case_sensitive: bool,
- files_to_include: Vec<glob::Pattern>,
- files_to_exclude: Vec<glob::Pattern>,
+ files_to_include: Vec<GlobMatcher>,
+ files_to_exclude: Vec<GlobMatcher>,
},
}
@@ -38,8 +39,8 @@ impl SearchQuery {
query: impl ToString,
whole_word: bool,
case_sensitive: bool,
- files_to_include: Vec<glob::Pattern>,
- files_to_exclude: Vec<glob::Pattern>,
+ files_to_include: Vec<GlobMatcher>,
+ files_to_exclude: Vec<GlobMatcher>,
) -> Self {
let query = query.to_string();
let search = AhoCorasickBuilder::new()
@@ -60,8 +61,8 @@ impl SearchQuery {
query: impl ToString,
whole_word: bool,
case_sensitive: bool,
- files_to_include: Vec<glob::Pattern>,
- files_to_exclude: Vec<glob::Pattern>,
+ files_to_include: Vec<GlobMatcher>,
+ files_to_exclude: Vec<GlobMatcher>,
) -> Result<Self> {
let mut query = query.to_string();
let initial_query = Arc::from(query.as_str());
@@ -95,40 +96,16 @@ impl SearchQuery {
message.query,
message.whole_word,
message.case_sensitive,
- message
- .files_to_include
- .split(',')
- .map(str::trim)
- .filter(|glob_str| !glob_str.is_empty())
- .map(|glob_str| glob::Pattern::new(glob_str))
- .collect::<Result<_, _>>()?,
- message
- .files_to_exclude
- .split(',')
- .map(str::trim)
- .filter(|glob_str| !glob_str.is_empty())
- .map(|glob_str| glob::Pattern::new(glob_str))
- .collect::<Result<_, _>>()?,
+ deserialize_globs(&message.files_to_include)?,
+ deserialize_globs(&message.files_to_exclude)?,
)
} else {
Ok(Self::text(
message.query,
message.whole_word,
message.case_sensitive,
- message
- .files_to_include
- .split(',')
- .map(str::trim)
- .filter(|glob_str| !glob_str.is_empty())
- .map(|glob_str| glob::Pattern::new(glob_str))
- .collect::<Result<_, _>>()?,
- message
- .files_to_exclude
- .split(',')
- .map(str::trim)
- .filter(|glob_str| !glob_str.is_empty())
- .map(|glob_str| glob::Pattern::new(glob_str))
- .collect::<Result<_, _>>()?,
+ deserialize_globs(&message.files_to_include)?,
+ deserialize_globs(&message.files_to_exclude)?,
))
}
}
@@ -143,12 +120,12 @@ impl SearchQuery {
files_to_include: self
.files_to_include()
.iter()
- .map(ToString::to_string)
+ .map(|g| g.glob().to_string())
.join(","),
files_to_exclude: self
.files_to_exclude()
.iter()
- .map(ToString::to_string)
+ .map(|g| g.glob().to_string())
.join(","),
}
}
@@ -289,7 +266,7 @@ impl SearchQuery {
matches!(self, Self::Regex { .. })
}
- pub fn files_to_include(&self) -> &[glob::Pattern] {
+ pub fn files_to_include(&self) -> &[GlobMatcher] {
match self {
Self::Text {
files_to_include, ..
@@ -300,7 +277,7 @@ impl SearchQuery {
}
}
- pub fn files_to_exclude(&self) -> &[glob::Pattern] {
+ pub fn files_to_exclude(&self) -> &[GlobMatcher] {
match self {
Self::Text {
files_to_exclude, ..
@@ -317,14 +294,23 @@ impl SearchQuery {
!self
.files_to_exclude()
.iter()
- .any(|exclude_glob| exclude_glob.matches_path(file_path))
+ .any(|exclude_glob| exclude_glob.is_match(file_path))
&& (self.files_to_include().is_empty()
|| self
.files_to_include()
.iter()
- .any(|include_glob| include_glob.matches_path(file_path)))
+ .any(|include_glob| include_glob.is_match(file_path)))
}
None => self.files_to_include().is_empty(),
}
}
}
+
+fn deserialize_globs(glob_set: &str) -> Result<Vec<GlobMatcher>> {
+ glob_set
+ .split(',')
+ .map(str::trim)
+ .filter(|glob_str| !glob_str.is_empty())
+ .map(|glob_str| Ok(Glob::new(glob_str)?.compile_matcher()))
+ .collect()
+}
@@ -120,25 +120,6 @@ pub struct Snapshot {
completed_scan_id: usize,
}
-impl Snapshot {
- pub fn repo_for(&self, path: &Path) -> Option<RepositoryEntry> {
- let mut max_len = 0;
- let mut current_candidate = None;
- for (work_directory, repo) in (&self.repository_entries).iter() {
- if repo.contains(self, path) {
- if work_directory.0.as_os_str().len() >= max_len {
- current_candidate = Some(repo);
- max_len = work_directory.0.as_os_str().len();
- } else {
- break;
- }
- }
- }
-
- current_candidate.map(|entry| entry.to_owned())
- }
-}
-
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RepositoryEntry {
pub(crate) work_directory: WorkDirectoryEntry,
@@ -169,17 +150,6 @@ impl RepositoryEntry {
.map(|entry| RepositoryWorkDirectory(entry.path.clone()))
}
- pub(crate) fn contains(&self, snapshot: &Snapshot, path: &Path) -> bool {
- self.work_directory.contains(snapshot, path)
- }
-
- pub fn status_for_file(&self, snapshot: &Snapshot, path: &Path) -> Option<GitFileStatus> {
- self.work_directory
- .relativize(snapshot, path)
- .and_then(|repo_path| self.statuses.get(&repo_path))
- .cloned()
- }
-
pub fn status_for_path(&self, snapshot: &Snapshot, path: &Path) -> Option<GitFileStatus> {
self.work_directory
.relativize(snapshot, path)
@@ -205,6 +175,14 @@ impl RepositoryEntry {
})
}
+ #[cfg(any(test, feature = "test-support"))]
+ pub fn status_for_file(&self, snapshot: &Snapshot, path: &Path) -> Option<GitFileStatus> {
+ self.work_directory
+ .relativize(snapshot, path)
+ .and_then(|repo_path| (&self.statuses).get(&repo_path))
+ .cloned()
+ }
+
pub fn build_update(&self, other: &Self) -> proto::RepositoryEntry {
let mut updated_statuses: Vec<proto::StatusEntry> = Vec::new();
let mut removed_statuses: Vec<String> = Vec::new();
@@ -250,7 +228,7 @@ impl RepositoryEntry {
work_directory_id: self.work_directory_id().to_proto(),
branch: self.branch.as_ref().map(|str| str.to_string()),
removed_repo_paths: removed_statuses,
- updated_statuses: updated_statuses,
+ updated_statuses,
}
}
}
@@ -305,14 +283,6 @@ impl AsRef<Path> for RepositoryWorkDirectory {
pub struct WorkDirectoryEntry(ProjectEntryId);
impl WorkDirectoryEntry {
- // Note that these paths should be relative to the worktree root.
- pub(crate) fn contains(&self, snapshot: &Snapshot, path: &Path) -> bool {
- snapshot
- .entry_for_id(self.0)
- .map(|entry| path.starts_with(&entry.path))
- .unwrap_or(false)
- }
-
pub(crate) fn relativize(&self, worktree: &Snapshot, path: &Path) -> Option<RepoPath> {
worktree.entry_for_id(self.0).and_then(|entry| {
path.strip_prefix(&entry.path)
@@ -338,19 +308,28 @@ impl<'a> From<ProjectEntryId> for WorkDirectoryEntry {
#[derive(Debug, Clone)]
pub struct LocalSnapshot {
- ignores_by_parent_abs_path: HashMap<Arc<Path>, (Arc<Gitignore>, bool)>, // (gitignore, needs_update)
- // The ProjectEntryId corresponds to the entry for the .git dir
- // work_directory_id
+ snapshot: Snapshot,
+ /// All of the gitignore files in the worktree, indexed by their relative path.
+ /// The boolean indicates whether the gitignore needs to be updated.
+ ignores_by_parent_abs_path: HashMap<Arc<Path>, (Arc<Gitignore>, bool)>,
+ /// All of the git repositories in the worktree, indexed by the project entry
+ /// id of their parent directory.
git_repositories: TreeMap<ProjectEntryId, LocalRepositoryEntry>,
+}
+
+pub struct LocalMutableSnapshot {
+ snapshot: LocalSnapshot,
+ /// The ids of all of the entries that were removed from the snapshot
+ /// as part of the current update. These entry ids may be re-used
+ /// if the same inode is discovered at a new path, or if the given
+ /// path is re-created after being deleted.
removed_entry_ids: HashMap<u64, ProjectEntryId>,
- next_entry_id: Arc<AtomicUsize>,
- snapshot: Snapshot,
}
#[derive(Debug, Clone)]
pub struct LocalRepositoryEntry {
pub(crate) scan_id: usize,
- pub(crate) full_scan_id: usize,
+ pub(crate) git_dir_scan_id: usize,
pub(crate) repo_ptr: Arc<Mutex<dyn GitRepository>>,
/// Path to the actual .git folder.
/// Note: if .git is a file, this points to the folder indicated by the .git file
@@ -378,6 +357,20 @@ impl DerefMut for LocalSnapshot {
}
}
+impl Deref for LocalMutableSnapshot {
+ type Target = LocalSnapshot;
+
+ fn deref(&self) -> &Self::Target {
+ &self.snapshot
+ }
+}
+
+impl DerefMut for LocalMutableSnapshot {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.snapshot
+ }
+}
+
enum ScanState {
Started,
Updated {
@@ -428,9 +421,7 @@ impl Worktree {
let mut snapshot = LocalSnapshot {
ignores_by_parent_abs_path: Default::default(),
- removed_entry_ids: Default::default(),
git_repositories: Default::default(),
- next_entry_id,
snapshot: Snapshot {
id: WorktreeId::from_usize(cx.model_id()),
abs_path: abs_path.clone(),
@@ -449,7 +440,7 @@ impl Worktree {
Entry::new(
Arc::from(Path::new("")),
&metadata,
- &snapshot.next_entry_id,
+ &next_entry_id,
snapshot.root_char_bag,
),
fs.as_ref(),
@@ -493,6 +484,7 @@ impl Worktree {
let events = fs.watch(&abs_path, Duration::from_millis(100)).await;
BackgroundScanner::new(
snapshot,
+ next_entry_id,
fs,
scan_states_tx,
background,
@@ -745,6 +737,45 @@ impl LocalWorktree {
self.diagnostics.get(path).cloned().unwrap_or_default()
}
+ pub fn clear_diagnostics_for_language_server(
+ &mut self,
+ server_id: LanguageServerId,
+ _: &mut ModelContext<Worktree>,
+ ) {
+ let worktree_id = self.id().to_proto();
+ self.diagnostic_summaries
+ .retain(|path, summaries_by_server_id| {
+ if summaries_by_server_id.remove(&server_id).is_some() {
+ if let Some(share) = self.share.as_ref() {
+ self.client
+ .send(proto::UpdateDiagnosticSummary {
+ project_id: share.project_id,
+ worktree_id,
+ summary: Some(proto::DiagnosticSummary {
+ path: path.to_string_lossy().to_string(),
+ language_server_id: server_id.0 as u64,
+ error_count: 0,
+ warning_count: 0,
+ }),
+ })
+ .log_err();
+ }
+ !summaries_by_server_id.is_empty()
+ } else {
+ true
+ }
+ });
+
+ self.diagnostics.retain(|_, diagnostics_by_server_id| {
+ if let Ok(ix) = diagnostics_by_server_id.binary_search_by_key(&server_id, |e| e.0) {
+ diagnostics_by_server_id.remove(ix);
+ !diagnostics_by_server_id.is_empty()
+ } else {
+ true
+ }
+ });
+ }
+
pub fn update_diagnostics(
&mut self,
server_id: LanguageServerId,
@@ -808,6 +839,7 @@ impl LocalWorktree {
fn set_snapshot(&mut self, new_snapshot: LocalSnapshot, cx: &mut ModelContext<Worktree>) {
let updated_repos =
self.changed_repos(&self.git_repositories, &new_snapshot.git_repositories);
+
self.snapshot = new_snapshot;
if let Some(share) = self.share.as_mut() {
@@ -838,7 +870,7 @@ impl LocalWorktree {
old_repos.next();
}
Ordering::Equal => {
- if old_repo.scan_id != new_repo.scan_id {
+ if old_repo.git_dir_scan_id != new_repo.git_dir_scan_id {
if let Some(entry) = self.entry_for_id(**new_entry_id) {
diff.insert(entry.path.clone(), (*new_repo).clone());
}
@@ -913,7 +945,7 @@ impl LocalWorktree {
let mut index_task = None;
- if let Some(repo) = snapshot.repo_for(&path) {
+ if let Some(repo) = snapshot.repository_for_path(&path) {
let repo_path = repo.work_directory.relativize(self, &path).unwrap();
if let Some(repo) = self.git_repositories.get(&*repo.work_directory) {
let repo = repo.repo_ptr.to_owned();
@@ -1240,8 +1272,6 @@ impl LocalWorktree {
let mut share_tx = Some(share_tx);
let mut prev_snapshot = LocalSnapshot {
ignores_by_parent_abs_path: Default::default(),
- removed_entry_ids: Default::default(),
- next_entry_id: Default::default(),
git_repositories: Default::default(),
snapshot: Snapshot {
id: WorktreeId(worktree_id as usize),
@@ -1643,8 +1673,63 @@ impl Snapshot {
self.traverse_from_offset(true, include_ignored, 0)
}
- pub fn repositories(&self) -> impl Iterator<Item = &RepositoryEntry> {
- self.repository_entries.values()
+ pub fn repositories(&self) -> impl Iterator<Item = (&Arc<Path>, &RepositoryEntry)> {
+ self.repository_entries
+ .iter()
+ .map(|(path, entry)| (&path.0, entry))
+ }
+
+ /// Get the repository whose work directory contains the given path.
+ pub fn repository_for_work_directory(&self, path: &Path) -> Option<RepositoryEntry> {
+ self.repository_entries
+ .get(&RepositoryWorkDirectory(path.into()))
+ .cloned()
+ }
+
+ /// Get the repository whose work directory contains the given path.
+ pub fn repository_for_path(&self, path: &Path) -> Option<RepositoryEntry> {
+ let mut max_len = 0;
+ let mut current_candidate = None;
+ for (work_directory, repo) in (&self.repository_entries).iter() {
+ if path.starts_with(&work_directory.0) {
+ if work_directory.0.as_os_str().len() >= max_len {
+ current_candidate = Some(repo);
+ max_len = work_directory.0.as_os_str().len();
+ } else {
+ break;
+ }
+ }
+ }
+
+ current_candidate.cloned()
+ }
+
+ /// Given an ordered iterator of entries, returns an iterator of those entries,
+ /// along with their containing git repository.
+ pub fn entries_with_repositories<'a>(
+ &'a self,
+ entries: impl 'a + Iterator<Item = &'a Entry>,
+ ) -> impl 'a + Iterator<Item = (&'a Entry, Option<&'a RepositoryEntry>)> {
+ let mut containing_repos = Vec::<(&Arc<Path>, &RepositoryEntry)>::new();
+ let mut repositories = self.repositories().peekable();
+ entries.map(move |entry| {
+ while let Some((repo_path, _)) = containing_repos.last() {
+ if !entry.path.starts_with(repo_path) {
+ containing_repos.pop();
+ } else {
+ break;
+ }
+ }
+ while let Some((repo_path, _)) = repositories.peek() {
+ if entry.path.starts_with(repo_path) {
+ containing_repos.push(repositories.next().unwrap());
+ } else {
+ break;
+ }
+ }
+ let repo = containing_repos.last().map(|(_, repo)| *repo);
+ (entry, repo)
+ })
}
pub fn paths(&self) -> impl Iterator<Item = &Arc<Path>> {
@@ -1895,8 +1980,6 @@ impl LocalSnapshot {
}
}
- self.reuse_entry_id(&mut entry);
-
if entry.kind == EntryKind::PendingDir {
if let Some(existing_entry) =
self.entries_by_path.get(&PathKey(entry.path.clone()), &())
@@ -1925,60 +2008,6 @@ impl LocalSnapshot {
entry
}
- fn populate_dir(
- &mut self,
- parent_path: Arc<Path>,
- entries: impl IntoIterator<Item = Entry>,
- ignore: Option<Arc<Gitignore>>,
- fs: &dyn Fs,
- ) {
- let mut parent_entry = if let Some(parent_entry) =
- self.entries_by_path.get(&PathKey(parent_path.clone()), &())
- {
- parent_entry.clone()
- } else {
- log::warn!(
- "populating a directory {:?} that has been removed",
- parent_path
- );
- return;
- };
-
- match parent_entry.kind {
- EntryKind::PendingDir => {
- parent_entry.kind = EntryKind::Dir;
- }
- EntryKind::Dir => {}
- _ => return,
- }
-
- if let Some(ignore) = ignore {
- self.ignores_by_parent_abs_path
- .insert(self.abs_path.join(&parent_path).into(), (ignore, false));
- }
-
- if parent_path.file_name() == Some(&DOT_GIT) {
- self.build_repo(parent_path, fs);
- }
-
- let mut entries_by_path_edits = vec![Edit::Insert(parent_entry)];
- let mut entries_by_id_edits = Vec::new();
-
- for mut entry in entries {
- self.reuse_entry_id(&mut entry);
- entries_by_id_edits.push(Edit::Insert(PathEntry {
- id: entry.id,
- path: entry.path.clone(),
- is_ignored: entry.is_ignored,
- scan_id: self.scan_id,
- }));
- entries_by_path_edits.push(Edit::Insert(entry));
- }
-
- self.entries_by_path.edit(entries_by_path_edits, &());
- self.entries_by_id.edit(entries_by_id_edits, &());
- }
-
fn build_repo(&mut self, parent_path: Arc<Path>, fs: &dyn Fs) -> Option<()> {
let abs_path = self.abs_path.join(&parent_path);
let work_dir: Arc<Path> = parent_path.parent().unwrap().into();
@@ -2017,7 +2046,7 @@ impl LocalSnapshot {
work_dir_id,
LocalRepositoryEntry {
scan_id,
- full_scan_id: scan_id,
+ git_dir_scan_id: scan_id,
repo_ptr: repo,
git_dir_path: parent_path.clone(),
},
@@ -2026,46 +2055,6 @@ impl LocalSnapshot {
Some(())
}
- fn reuse_entry_id(&mut self, entry: &mut Entry) {
- if let Some(removed_entry_id) = self.removed_entry_ids.remove(&entry.inode) {
- entry.id = removed_entry_id;
- } else if let Some(existing_entry) = self.entry_for_path(&entry.path) {
- entry.id = existing_entry.id;
- }
- }
-
- fn remove_path(&mut self, path: &Path) {
- let mut new_entries;
- let removed_entries;
- {
- let mut cursor = self.entries_by_path.cursor::<TraversalProgress>();
- new_entries = cursor.slice(&TraversalTarget::Path(path), Bias::Left, &());
- removed_entries = cursor.slice(&TraversalTarget::PathSuccessor(path), Bias::Left, &());
- new_entries.push_tree(cursor.suffix(&()), &());
- }
- self.entries_by_path = new_entries;
-
- let mut entries_by_id_edits = Vec::new();
- for entry in removed_entries.cursor::<()>() {
- let removed_entry_id = self
- .removed_entry_ids
- .entry(entry.inode)
- .or_insert(entry.id);
- *removed_entry_id = cmp::max(*removed_entry_id, entry.id);
- entries_by_id_edits.push(Edit::Remove(entry.id));
- }
- self.entries_by_id.edit(entries_by_id_edits, &());
-
- if path.file_name() == Some(&GITIGNORE) {
- let abs_parent_path = self.abs_path.join(path.parent().unwrap());
- if let Some((_, needs_update)) = self
- .ignores_by_parent_abs_path
- .get_mut(abs_parent_path.as_path())
- {
- *needs_update = true;
- }
- }
- }
fn ancestor_inodes_for_path(&self, path: &Path) -> TreeSet<u64> {
let mut inodes = TreeSet::default();
@@ -2105,36 +2094,139 @@ impl LocalSnapshot {
}
}
-async fn build_gitignore(abs_path: &Path, fs: &dyn Fs) -> Result<Gitignore> {
- let contents = fs.load(abs_path).await?;
- let parent = abs_path.parent().unwrap_or_else(|| Path::new("/"));
- let mut builder = GitignoreBuilder::new(parent);
- for line in contents.lines() {
- builder.add_line(Some(abs_path.into()), line)?;
+impl LocalMutableSnapshot {
+ fn reuse_entry_id(&mut self, entry: &mut Entry) {
+ if let Some(removed_entry_id) = self.removed_entry_ids.remove(&entry.inode) {
+ entry.id = removed_entry_id;
+ } else if let Some(existing_entry) = self.entry_for_path(&entry.path) {
+ entry.id = existing_entry.id;
+ }
}
- Ok(builder.build()?)
-}
-impl WorktreeId {
- pub fn from_usize(handle_id: usize) -> Self {
- Self(handle_id)
+ fn insert_entry(&mut self, mut entry: Entry, fs: &dyn Fs) -> Entry {
+ self.reuse_entry_id(&mut entry);
+ self.snapshot.insert_entry(entry, fs)
}
- pub(crate) fn from_proto(id: u64) -> Self {
- Self(id as usize)
- }
+ fn populate_dir(
+ &mut self,
+ parent_path: Arc<Path>,
+ entries: impl IntoIterator<Item = Entry>,
+ ignore: Option<Arc<Gitignore>>,
+ fs: &dyn Fs,
+ ) {
+ let mut parent_entry = if let Some(parent_entry) =
+ self.entries_by_path.get(&PathKey(parent_path.clone()), &())
+ {
+ parent_entry.clone()
+ } else {
+ log::warn!(
+ "populating a directory {:?} that has been removed",
+ parent_path
+ );
+ return;
+ };
- pub fn to_proto(&self) -> u64 {
- self.0 as u64
- }
+ match parent_entry.kind {
+ EntryKind::PendingDir => {
+ parent_entry.kind = EntryKind::Dir;
+ }
+ EntryKind::Dir => {}
+ _ => return,
+ }
- pub fn to_usize(&self) -> usize {
- self.0
- }
-}
+ if let Some(ignore) = ignore {
+ let abs_parent_path = self.abs_path.join(&parent_path).into();
+ self.ignores_by_parent_abs_path
+ .insert(abs_parent_path, (ignore, false));
+ }
-impl fmt::Display for WorktreeId {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ if parent_path.file_name() == Some(&DOT_GIT) {
+ self.build_repo(parent_path, fs);
+ }
+
+ let mut entries_by_path_edits = vec![Edit::Insert(parent_entry)];
+ let mut entries_by_id_edits = Vec::new();
+
+ for mut entry in entries {
+ self.reuse_entry_id(&mut entry);
+ entries_by_id_edits.push(Edit::Insert(PathEntry {
+ id: entry.id,
+ path: entry.path.clone(),
+ is_ignored: entry.is_ignored,
+ scan_id: self.scan_id,
+ }));
+ entries_by_path_edits.push(Edit::Insert(entry));
+ }
+
+ self.entries_by_path.edit(entries_by_path_edits, &());
+ self.entries_by_id.edit(entries_by_id_edits, &());
+ }
+
+ fn remove_path(&mut self, path: &Path) {
+ let mut new_entries;
+ let removed_entries;
+ {
+ let mut cursor = self.entries_by_path.cursor::<TraversalProgress>();
+ new_entries = cursor.slice(&TraversalTarget::Path(path), Bias::Left, &());
+ removed_entries = cursor.slice(&TraversalTarget::PathSuccessor(path), Bias::Left, &());
+ new_entries.push_tree(cursor.suffix(&()), &());
+ }
+ self.entries_by_path = new_entries;
+
+ let mut entries_by_id_edits = Vec::new();
+ for entry in removed_entries.cursor::<()>() {
+ let removed_entry_id = self
+ .removed_entry_ids
+ .entry(entry.inode)
+ .or_insert(entry.id);
+ *removed_entry_id = cmp::max(*removed_entry_id, entry.id);
+ entries_by_id_edits.push(Edit::Remove(entry.id));
+ }
+ self.entries_by_id.edit(entries_by_id_edits, &());
+
+ if path.file_name() == Some(&GITIGNORE) {
+ let abs_parent_path = self.abs_path.join(path.parent().unwrap());
+ if let Some((_, needs_update)) = self
+ .ignores_by_parent_abs_path
+ .get_mut(abs_parent_path.as_path())
+ {
+ *needs_update = true;
+ }
+ }
+ }
+}
+
+async fn build_gitignore(abs_path: &Path, fs: &dyn Fs) -> Result<Gitignore> {
+ let contents = fs.load(abs_path).await?;
+ let parent = abs_path.parent().unwrap_or_else(|| Path::new("/"));
+ let mut builder = GitignoreBuilder::new(parent);
+ for line in contents.lines() {
+ builder.add_line(Some(abs_path.into()), line)?;
+ }
+ Ok(builder.build()?)
+}
+
+impl WorktreeId {
+ pub fn from_usize(handle_id: usize) -> Self {
+ Self(handle_id)
+ }
+
+ pub(crate) fn from_proto(id: u64) -> Self {
+ Self(id as usize)
+ }
+
+ pub fn to_proto(&self) -> u64 {
+ self.0 as u64
+ }
+
+ pub fn to_usize(&self) -> usize {
+ self.0
+ }
+}
+
+impl fmt::Display for WorktreeId {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
@@ -2547,12 +2639,13 @@ impl<'a> sum_tree::Dimension<'a, EntrySummary> for PathKey {
}
struct BackgroundScanner {
- snapshot: Mutex<LocalSnapshot>,
+ snapshot: Mutex<LocalMutableSnapshot>,
fs: Arc<dyn Fs>,
status_updates_tx: UnboundedSender<ScanState>,
executor: Arc<executor::Background>,
refresh_requests_rx: channel::Receiver<(Vec<PathBuf>, barrier::Sender)>,
prev_state: Mutex<BackgroundScannerState>,
+ next_entry_id: Arc<AtomicUsize>,
finished_initial_scan: bool,
}
@@ -2564,6 +2657,7 @@ struct BackgroundScannerState {
impl BackgroundScanner {
fn new(
snapshot: LocalSnapshot,
+ next_entry_id: Arc<AtomicUsize>,
fs: Arc<dyn Fs>,
status_updates_tx: UnboundedSender<ScanState>,
executor: Arc<executor::Background>,
@@ -2574,11 +2668,15 @@ impl BackgroundScanner {
status_updates_tx,
executor,
refresh_requests_rx,
+ next_entry_id,
prev_state: Mutex::new(BackgroundScannerState {
snapshot: snapshot.snapshot.clone(),
event_paths: Default::default(),
}),
- snapshot: Mutex::new(snapshot),
+ snapshot: Mutex::new(LocalMutableSnapshot {
+ snapshot,
+ removed_entry_ids: Default::default(),
+ }),
finished_initial_scan: false,
}
}
@@ -2732,10 +2830,7 @@ impl BackgroundScanner {
.is_some()
});
snapshot.snapshot.repository_entries = git_repository_entries;
-
- snapshot.removed_entry_ids.clear();
snapshot.completed_scan_id = snapshot.scan_id;
-
drop(snapshot);
self.send_status_update(false, None);
@@ -2846,7 +2941,7 @@ impl BackgroundScanner {
(
snapshot.abs_path().clone(),
snapshot.root_char_bag,
- snapshot.next_entry_id.clone(),
+ self.next_entry_id.clone(),
)
};
let mut child_paths = self.fs.read_dir(&job.abs_path).await?;
@@ -3018,7 +3113,7 @@ impl BackgroundScanner {
let mut fs_entry = Entry::new(
path.clone(),
&metadata,
- snapshot.next_entry_id.as_ref(),
+ self.next_entry_id.as_ref(),
snapshot.root_char_bag,
);
fs_entry.is_ignored = ignore_stack.is_all();
@@ -3058,7 +3153,18 @@ impl BackgroundScanner {
.any(|component| component.as_os_str() == *DOT_GIT)
{
let scan_id = snapshot.scan_id;
- let repo = snapshot.repo_for(&path)?;
+
+ if let Some(repository) = snapshot.repository_for_work_directory(path) {
+ let entry = repository.work_directory.0;
+ snapshot.git_repositories.remove(&entry);
+ snapshot
+ .snapshot
+ .repository_entries
+ .remove(&RepositoryWorkDirectory(path.into()));
+ return Some(());
+ }
+
+ let repo = snapshot.repository_for_path(&path)?;
let repo_path = repo.work_directory.relativize(&snapshot, &path)?;
@@ -3100,7 +3206,7 @@ impl BackgroundScanner {
snapshot.build_repo(dot_git_dir.into(), fs);
return None;
};
- if repo.full_scan_id == scan_id {
+ if repo.git_dir_scan_id == scan_id {
return None;
}
(*entry_id, repo.repo_ptr.to_owned())
@@ -3117,7 +3223,7 @@ impl BackgroundScanner {
snapshot.git_repositories.update(&entry_id, |entry| {
entry.scan_id = scan_id;
- entry.full_scan_id = scan_id;
+ entry.git_dir_scan_id = scan_id;
});
snapshot.repository_entries.update(&work_dir, |entry| {
@@ -3134,7 +3240,7 @@ impl BackgroundScanner {
return None;
}
- let repo = snapshot.repo_for(&path)?;
+ let repo = snapshot.repository_for_path(&path)?;
let work_dir = repo.work_directory(snapshot)?;
let work_dir_id = repo.work_directory.clone();
@@ -3146,7 +3252,7 @@ impl BackgroundScanner {
let local_repo = snapshot.get_local_repo(&repo)?.to_owned();
// Short circuit if we've already scanned everything
- if local_repo.full_scan_id == scan_id {
+ if local_repo.git_dir_scan_id == scan_id {
return None;
}
@@ -3928,6 +4034,8 @@ mod tests {
#[gpui::test]
async fn test_rescan_with_gitignore(cx: &mut TestAppContext) {
+ // .gitignores are handled explicitly by Zed and do not use the git
+ // machinery that the git_tests module checks
let parent_dir = temp_tree(json!({
".gitignore": "ancestor-ignored-file1\nancestor-ignored-file2\n",
"tree": {
@@ -4006,30 +4114,19 @@ mod tests {
}
#[gpui::test]
- async fn test_git_repository_for_path(cx: &mut TestAppContext) {
- let root = temp_tree(json!({
- "dir1": {
- ".git": {},
- "deps": {
- "dep1": {
- ".git": {},
- "src": {
- "a.txt": ""
- }
- }
- },
- "src": {
- "b.txt": ""
- }
- },
- "c.txt": "",
+ async fn test_write_file(cx: &mut TestAppContext) {
+ let dir = temp_tree(json!({
+ ".git": {},
+ ".gitignore": "ignored-dir\n",
+ "tracked-dir": {},
+ "ignored-dir": {}
}));
- let http_client = FakeHttpClient::with_404_response();
- let client = cx.read(|cx| Client::new(http_client, cx));
+ let client = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
+
let tree = Worktree::local(
client,
- root.path(),
+ dir.path(),
true,
Arc::new(RealFs),
Default::default(),
@@ -4037,475 +4134,121 @@ mod tests {
)
.await
.unwrap();
-
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
.await;
tree.flush_fs_events(cx).await;
- tree.read_with(cx, |tree, _cx| {
- let tree = tree.as_local().unwrap();
-
- assert!(tree.repo_for("c.txt".as_ref()).is_none());
-
- let entry = tree.repo_for("dir1/src/b.txt".as_ref()).unwrap();
- assert_eq!(
- entry
- .work_directory(tree)
- .map(|directory| directory.as_ref().to_owned()),
- Some(Path::new("dir1").to_owned())
- );
+ tree.update(cx, |tree, cx| {
+ tree.as_local().unwrap().write_file(
+ Path::new("tracked-dir/file.txt"),
+ "hello".into(),
+ Default::default(),
+ cx,
+ )
+ })
+ .await
+ .unwrap();
+ tree.update(cx, |tree, cx| {
+ tree.as_local().unwrap().write_file(
+ Path::new("ignored-dir/file.txt"),
+ "world".into(),
+ Default::default(),
+ cx,
+ )
+ })
+ .await
+ .unwrap();
- let entry = tree.repo_for("dir1/deps/dep1/src/a.txt".as_ref()).unwrap();
- assert_eq!(
- entry
- .work_directory(tree)
- .map(|directory| directory.as_ref().to_owned()),
- Some(Path::new("dir1/deps/dep1").to_owned())
- );
+ tree.read_with(cx, |tree, _| {
+ let tracked = tree.entry_for_path("tracked-dir/file.txt").unwrap();
+ let ignored = tree.entry_for_path("ignored-dir/file.txt").unwrap();
+ assert!(!tracked.is_ignored);
+ assert!(ignored.is_ignored);
});
+ }
- let repo_update_events = Arc::new(Mutex::new(vec![]));
- tree.update(cx, |_, cx| {
- let repo_update_events = repo_update_events.clone();
- cx.subscribe(&tree, move |_, _, event, _| {
- if let Event::UpdatedGitRepositories(update) = event {
- repo_update_events.lock().push(update.clone());
- }
- })
- .detach();
- });
+ #[gpui::test(iterations = 30)]
+ async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
+ let client = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
- std::fs::write(root.path().join("dir1/.git/random_new_file"), "hello").unwrap();
- tree.flush_fs_events(cx).await;
+ let fs = FakeFs::new(cx.background());
+ fs.insert_tree(
+ "/root",
+ json!({
+ "b": {},
+ "c": {},
+ "d": {},
+ }),
+ )
+ .await;
- assert_eq!(
- repo_update_events.lock()[0]
- .keys()
- .cloned()
- .collect::<Vec<Arc<Path>>>(),
- vec![Path::new("dir1").into()]
- );
+ let tree = Worktree::local(
+ client,
+ "/root".as_ref(),
+ true,
+ fs,
+ Default::default(),
+ &mut cx.to_async(),
+ )
+ .await
+ .unwrap();
- std::fs::remove_dir_all(root.path().join("dir1/.git")).unwrap();
- tree.flush_fs_events(cx).await;
+ let mut snapshot1 = tree.update(cx, |tree, _| tree.as_local().unwrap().snapshot());
- tree.read_with(cx, |tree, _cx| {
- let tree = tree.as_local().unwrap();
+ let entry = tree
+ .update(cx, |tree, cx| {
+ tree.as_local_mut()
+ .unwrap()
+ .create_entry("a/e".as_ref(), true, cx)
+ })
+ .await
+ .unwrap();
+ assert!(entry.is_dir());
- assert!(tree.repo_for("dir1/src/b.txt".as_ref()).is_none());
+ cx.foreground().run_until_parked();
+ tree.read_with(cx, |tree, _| {
+ assert_eq!(tree.entry_for_path("a/e").unwrap().kind, EntryKind::Dir);
});
- }
- #[gpui::test]
- async fn test_git_status(cx: &mut TestAppContext) {
- #[track_caller]
- fn git_init(path: &Path) -> git2::Repository {
- git2::Repository::init(path).expect("Failed to initialize git repository")
- }
+ let snapshot2 = tree.update(cx, |tree, _| tree.as_local().unwrap().snapshot());
+ let update = snapshot2.build_update(&snapshot1, 0, 0, true);
+ snapshot1.apply_remote_update(update).unwrap();
+ assert_eq!(snapshot1.to_vec(true), snapshot2.to_vec(true),);
+ }
- #[track_caller]
- fn git_add(path: &Path, repo: &git2::Repository) {
- let mut index = repo.index().expect("Failed to get index");
- index.add_path(path).expect("Failed to add a.txt");
- index.write().expect("Failed to write index");
- }
+ #[gpui::test(iterations = 100)]
+ async fn test_random_worktree_operations_during_initial_scan(
+ cx: &mut TestAppContext,
+ mut rng: StdRng,
+ ) {
+ let operations = env::var("OPERATIONS")
+ .map(|o| o.parse().unwrap())
+ .unwrap_or(5);
+ let initial_entries = env::var("INITIAL_ENTRIES")
+ .map(|o| o.parse().unwrap())
+ .unwrap_or(20);
- #[track_caller]
- fn git_remove_index(path: &Path, repo: &git2::Repository) {
- let mut index = repo.index().expect("Failed to get index");
- index.remove_path(path).expect("Failed to add a.txt");
- index.write().expect("Failed to write index");
+ let root_dir = Path::new("/test");
+ let fs = FakeFs::new(cx.background()) as Arc<dyn Fs>;
+ fs.as_fake().insert_tree(root_dir, json!({})).await;
+ for _ in 0..initial_entries {
+ randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
}
+ log::info!("generated initial tree");
- #[track_caller]
- fn git_commit(msg: &'static str, repo: &git2::Repository) {
- use git2::Signature;
-
- let signature = Signature::now("test", "test@zed.dev").unwrap();
- let oid = repo.index().unwrap().write_tree().unwrap();
- let tree = repo.find_tree(oid).unwrap();
- if let Some(head) = repo.head().ok() {
- let parent_obj = head.peel(git2::ObjectType::Commit).unwrap();
+ let client = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
+ let worktree = Worktree::local(
+ client.clone(),
+ root_dir,
+ true,
+ fs.clone(),
+ Default::default(),
+ &mut cx.to_async(),
+ )
+ .await
+ .unwrap();
- let parent_commit = parent_obj.as_commit().unwrap();
-
- repo.commit(
- Some("HEAD"),
- &signature,
- &signature,
- msg,
- &tree,
- &[parent_commit],
- )
- .expect("Failed to commit with parent");
- } else {
- repo.commit(Some("HEAD"), &signature, &signature, msg, &tree, &[])
- .expect("Failed to commit");
- }
- }
-
- #[track_caller]
- fn git_stash(repo: &mut git2::Repository) {
- use git2::Signature;
-
- let signature = Signature::now("test", "test@zed.dev").unwrap();
- repo.stash_save(&signature, "N/A", None)
- .expect("Failed to stash");
- }
-
- #[track_caller]
- fn git_reset(offset: usize, repo: &git2::Repository) {
- let head = repo.head().expect("Couldn't get repo head");
- let object = head.peel(git2::ObjectType::Commit).unwrap();
- let commit = object.as_commit().unwrap();
- let new_head = commit
- .parents()
- .inspect(|parnet| {
- parnet.message();
- })
- .skip(offset)
- .next()
- .expect("Not enough history");
- repo.reset(&new_head.as_object(), git2::ResetType::Soft, None)
- .expect("Could not reset");
- }
-
- #[allow(dead_code)]
- #[track_caller]
- fn git_status(repo: &git2::Repository) -> HashMap<String, git2::Status> {
- repo.statuses(None)
- .unwrap()
- .iter()
- .map(|status| (status.path().unwrap().to_string(), status.status()))
- .collect()
- }
-
- const IGNORE_RULE: &'static str = "**/target";
-
- let root = temp_tree(json!({
- "project": {
- "a.txt": "a",
- "b.txt": "bb",
- "c": {
- "d": {
- "e.txt": "eee"
- }
- },
- "f.txt": "ffff",
- "target": {
- "build_file": "???"
- },
- ".gitignore": IGNORE_RULE
- },
-
- }));
-
- let http_client = FakeHttpClient::with_404_response();
- let client = cx.read(|cx| Client::new(http_client, cx));
- let tree = Worktree::local(
- client,
- root.path(),
- true,
- Arc::new(RealFs),
- Default::default(),
- &mut cx.to_async(),
- )
- .await
- .unwrap();
-
- cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
- .await;
-
- const A_TXT: &'static str = "a.txt";
- const B_TXT: &'static str = "b.txt";
- const E_TXT: &'static str = "c/d/e.txt";
- const F_TXT: &'static str = "f.txt";
- const DOTGITIGNORE: &'static str = ".gitignore";
- const BUILD_FILE: &'static str = "target/build_file";
-
- let work_dir = root.path().join("project");
- let mut repo = git_init(work_dir.as_path());
- repo.add_ignore_rule(IGNORE_RULE).unwrap();
- git_add(Path::new(A_TXT), &repo);
- git_add(Path::new(E_TXT), &repo);
- git_add(Path::new(DOTGITIGNORE), &repo);
- git_commit("Initial commit", &repo);
-
- std::fs::write(work_dir.join(A_TXT), "aa").unwrap();
-
- tree.flush_fs_events(cx).await;
-
- // Check that the right git state is observed on startup
- tree.read_with(cx, |tree, _cx| {
- let snapshot = tree.snapshot();
- assert_eq!(snapshot.repository_entries.iter().count(), 1);
- let (dir, repo) = snapshot.repository_entries.iter().next().unwrap();
- assert_eq!(dir.0.as_ref(), Path::new("project"));
-
- assert_eq!(repo.statuses.iter().count(), 3);
- assert_eq!(
- repo.statuses.get(&Path::new(A_TXT).into()),
- Some(&GitFileStatus::Modified)
- );
- assert_eq!(
- repo.statuses.get(&Path::new(B_TXT).into()),
- Some(&GitFileStatus::Added)
- );
- assert_eq!(
- repo.statuses.get(&Path::new(F_TXT).into()),
- Some(&GitFileStatus::Added)
- );
- });
-
- git_add(Path::new(A_TXT), &repo);
- git_add(Path::new(B_TXT), &repo);
- git_commit("Committing modified and added", &repo);
- tree.flush_fs_events(cx).await;
-
- // Check that repo only changes are tracked
- tree.read_with(cx, |tree, _cx| {
- let snapshot = tree.snapshot();
- let (_, repo) = snapshot.repository_entries.iter().next().unwrap();
-
- assert_eq!(repo.statuses.iter().count(), 1);
- assert_eq!(
- repo.statuses.get(&Path::new(F_TXT).into()),
- Some(&GitFileStatus::Added)
- );
- });
-
- git_reset(0, &repo);
- git_remove_index(Path::new(B_TXT), &repo);
- git_stash(&mut repo);
- std::fs::write(work_dir.join(E_TXT), "eeee").unwrap();
- std::fs::write(work_dir.join(BUILD_FILE), "this should be ignored").unwrap();
- tree.flush_fs_events(cx).await;
-
- // Check that more complex repo changes are tracked
- tree.read_with(cx, |tree, _cx| {
- let snapshot = tree.snapshot();
- let (_, repo) = snapshot.repository_entries.iter().next().unwrap();
-
- assert_eq!(repo.statuses.iter().count(), 3);
- assert_eq!(repo.statuses.get(&Path::new(A_TXT).into()), None);
- assert_eq!(
- repo.statuses.get(&Path::new(B_TXT).into()),
- Some(&GitFileStatus::Added)
- );
- assert_eq!(
- repo.statuses.get(&Path::new(E_TXT).into()),
- Some(&GitFileStatus::Modified)
- );
- assert_eq!(
- repo.statuses.get(&Path::new(F_TXT).into()),
- Some(&GitFileStatus::Added)
- );
- });
-
- std::fs::remove_file(work_dir.join(B_TXT)).unwrap();
- std::fs::remove_dir_all(work_dir.join("c")).unwrap();
- std::fs::write(
- work_dir.join(DOTGITIGNORE),
- [IGNORE_RULE, "f.txt"].join("\n"),
- )
- .unwrap();
-
- git_add(Path::new(DOTGITIGNORE), &repo);
- git_commit("Committing modified git ignore", &repo);
-
- tree.flush_fs_events(cx).await;
-
- // Check that non-repo behavior is tracked
- tree.read_with(cx, |tree, _cx| {
- let snapshot = tree.snapshot();
- let (_, repo) = snapshot.repository_entries.iter().next().unwrap();
-
- assert_eq!(repo.statuses.iter().count(), 0);
- });
-
- let mut renamed_dir_name = "first_directory/second_directory";
- const RENAMED_FILE: &'static str = "rf.txt";
-
- std::fs::create_dir_all(work_dir.join(renamed_dir_name)).unwrap();
- std::fs::write(
- work_dir.join(renamed_dir_name).join(RENAMED_FILE),
- "new-contents",
- )
- .unwrap();
-
- tree.flush_fs_events(cx).await;
-
- tree.read_with(cx, |tree, _cx| {
- let snapshot = tree.snapshot();
- let (_, repo) = snapshot.repository_entries.iter().next().unwrap();
-
- assert_eq!(repo.statuses.iter().count(), 1);
- assert_eq!(
- repo.statuses
- .get(&Path::new(renamed_dir_name).join(RENAMED_FILE).into()),
- Some(&GitFileStatus::Added)
- );
- });
-
- renamed_dir_name = "new_first_directory/second_directory";
-
- std::fs::rename(
- work_dir.join("first_directory"),
- work_dir.join("new_first_directory"),
- )
- .unwrap();
-
- tree.flush_fs_events(cx).await;
-
- tree.read_with(cx, |tree, _cx| {
- let snapshot = tree.snapshot();
- let (_, repo) = snapshot.repository_entries.iter().next().unwrap();
-
- assert_eq!(repo.statuses.iter().count(), 1);
- assert_eq!(
- repo.statuses
- .get(&Path::new(renamed_dir_name).join(RENAMED_FILE).into()),
- Some(&GitFileStatus::Added)
- );
- });
- }
-
- #[gpui::test]
- async fn test_write_file(cx: &mut TestAppContext) {
- let dir = temp_tree(json!({
- ".git": {},
- ".gitignore": "ignored-dir\n",
- "tracked-dir": {},
- "ignored-dir": {}
- }));
-
- let client = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
-
- let tree = Worktree::local(
- client,
- dir.path(),
- true,
- Arc::new(RealFs),
- Default::default(),
- &mut cx.to_async(),
- )
- .await
- .unwrap();
- cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
- .await;
- tree.flush_fs_events(cx).await;
-
- tree.update(cx, |tree, cx| {
- tree.as_local().unwrap().write_file(
- Path::new("tracked-dir/file.txt"),
- "hello".into(),
- Default::default(),
- cx,
- )
- })
- .await
- .unwrap();
- tree.update(cx, |tree, cx| {
- tree.as_local().unwrap().write_file(
- Path::new("ignored-dir/file.txt"),
- "world".into(),
- Default::default(),
- cx,
- )
- })
- .await
- .unwrap();
-
- tree.read_with(cx, |tree, _| {
- let tracked = tree.entry_for_path("tracked-dir/file.txt").unwrap();
- let ignored = tree.entry_for_path("ignored-dir/file.txt").unwrap();
- assert!(!tracked.is_ignored);
- assert!(ignored.is_ignored);
- });
- }
-
- #[gpui::test(iterations = 30)]
- async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
- let client = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
-
- let fs = FakeFs::new(cx.background());
- fs.insert_tree(
- "/root",
- json!({
- "b": {},
- "c": {},
- "d": {},
- }),
- )
- .await;
-
- let tree = Worktree::local(
- client,
- "/root".as_ref(),
- true,
- fs,
- Default::default(),
- &mut cx.to_async(),
- )
- .await
- .unwrap();
-
- let mut snapshot1 = tree.update(cx, |tree, _| tree.as_local().unwrap().snapshot());
-
- let entry = tree
- .update(cx, |tree, cx| {
- tree.as_local_mut()
- .unwrap()
- .create_entry("a/e".as_ref(), true, cx)
- })
- .await
- .unwrap();
- assert!(entry.is_dir());
-
- cx.foreground().run_until_parked();
- tree.read_with(cx, |tree, _| {
- assert_eq!(tree.entry_for_path("a/e").unwrap().kind, EntryKind::Dir);
- });
-
- let snapshot2 = tree.update(cx, |tree, _| tree.as_local().unwrap().snapshot());
- let update = snapshot2.build_update(&snapshot1, 0, 0, true);
- snapshot1.apply_remote_update(update).unwrap();
- assert_eq!(snapshot1.to_vec(true), snapshot2.to_vec(true),);
- }
-
- #[gpui::test(iterations = 100)]
- async fn test_random_worktree_operations_during_initial_scan(
- cx: &mut TestAppContext,
- mut rng: StdRng,
- ) {
- let operations = env::var("OPERATIONS")
- .map(|o| o.parse().unwrap())
- .unwrap_or(5);
- let initial_entries = env::var("INITIAL_ENTRIES")
- .map(|o| o.parse().unwrap())
- .unwrap_or(20);
-
- let root_dir = Path::new("/test");
- let fs = FakeFs::new(cx.background()) as Arc<dyn Fs>;
- fs.as_fake().insert_tree(root_dir, json!({})).await;
- for _ in 0..initial_entries {
- randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
- }
- log::info!("generated initial tree");
-
- let client = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
- let worktree = Worktree::local(
- client.clone(),
- root_dir,
- true,
- fs.clone(),
- Default::default(),
- &mut cx.to_async(),
- )
- .await
- .unwrap();
-
- let mut snapshot = worktree.update(cx, |tree, _| tree.as_local().unwrap().snapshot());
+ let mut snapshot = worktree.update(cx, |tree, _| tree.as_local().unwrap().snapshot());
for _ in 0..operations {
worktree
@@ -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" }
@@ -21,6 +22,11 @@ util = { path = "../util" }
workspace = { path = "../workspace" }
postage.workspace = true
futures.workspace = true
+serde.workspace = true
+serde_derive.workspace = true
+serde_json.workspace = true
+anyhow.workspace = true
+schemars.workspace = true
unicase = "2.6"
[dev-dependencies]
@@ -1,25 +1,31 @@
+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, ComponentHost, ContainerStyle, Empty, Flex, MouseEventHandler,
+ AnchorCorner, ChildView, ContainerStyle, Empty, Flex, Label, MouseEventHandler,
ParentElement, ScrollTarget, Stack, Svg, UniformList, UniformListState,
},
geometry::vector::Vector2F,
keymap_matcher::KeymapContext,
platform::{CursorStyle, MouseButton, PromptLevel},
- AnyElement, AppContext, ClipboardItem, Element, Entity, ModelHandle, Task, View, ViewContext,
- ViewHandle, WeakViewHandle,
+ Action, 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::{ProjectPanelDockPosition, ProjectPanelSettings};
+use serde::{Deserialize, Serialize};
+use settings::SettingsStore;
use std::{
cmp::Ordering,
collections::{hash_map, HashMap},
@@ -28,14 +34,20 @@ use std::{
path::Path,
sync::Arc,
};
-use theme::{ui::FileName, ProjectPanelEntry};
+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>,
@@ -47,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)]
@@ -110,7 +125,12 @@ actions!(
]
);
+pub fn init_settings(cx: &mut AppContext) {
+ settings::register::<ProjectPanelSettings>(cx);
+}
+
pub fn init(cx: &mut AppContext) {
+ init_settings(cx);
cx.add_action(ProjectPanel::expand_selected_entry);
cx.add_action(ProjectPanel::collapse_selected_entry);
cx.add_action(ProjectPanel::select_prev);
@@ -138,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| {
@@ -202,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(),
@@ -213,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
});
@@ -246,6 +289,7 @@ impl ProjectPanel {
}
}
}
+ _ => {}
}
})
.detach();
@@ -253,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,
@@ -1000,6 +1089,7 @@ impl ProjectPanel {
}
let end_ix = range.end.min(ix + visible_worktree_entries.len());
+ let git_status_setting = settings::get::<ProjectPanelSettings>(cx).git_status;
if let Some(worktree) = self.project.read(cx).worktree_for_id(*worktree_id, cx) {
let snapshot = worktree.read(cx).snapshot();
let root_name = OsStr::new(snapshot.root_name());
@@ -1010,14 +1100,13 @@ impl ProjectPanel {
.unwrap_or(&[]);
let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
- for entry in &visible_worktree_entries[entry_range] {
- let path = &entry.path;
- let status = (entry.path.parent().is_some() && !entry.is_ignored)
- .then(|| {
- snapshot
- .repo_for(path)
- .and_then(|entry| entry.status_for_path(&snapshot, path))
- })
+ for (entry, repo) in
+ snapshot.entries_with_repositories(visible_worktree_entries[entry_range].iter())
+ {
+ let status = (git_status_setting
+ && entry.path.parent().is_some()
+ && !entry.is_ignored)
+ .then(|| repo.and_then(|repo| repo.status_for_path(&snapshot, &entry.path)))
.flatten();
let mut details = EntryDetails {
@@ -1082,6 +1171,17 @@ impl ProjectPanel {
let kind = details.kind;
let show_editor = details.is_editing && !details.is_processing;
+ let mut filename_text_style = style.text.clone();
+ filename_text_style.color = details
+ .git_status
+ .as_ref()
+ .map(|status| match status {
+ GitFileStatus::Added => style.status.git.inserted,
+ GitFileStatus::Modified => style.status.git.modified,
+ GitFileStatus::Conflict => style.status.git.conflict,
+ })
+ .unwrap_or(style.text.color);
+
Flex::row()
.with_child(
if kind == EntryKind::Dir {
@@ -1109,16 +1209,12 @@ impl ProjectPanel {
.flex(1.0, true)
.into_any()
} else {
- ComponentHost::new(FileName::new(
- details.filename.clone(),
- details.git_status,
- FileName::style(style.text.clone(), &theme::current(cx)),
- ))
- .contained()
- .with_margin_left(style.icon_spacing)
- .aligned()
- .left()
- .into_any()
+ Label::new(details.filename.clone(), filename_text_style)
+ .contained()
+ .with_margin_left(style.icon_spacing)
+ .aligned()
+ .left()
+ .into_any()
})
.constrained()
.with_height(style.height)
@@ -1337,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, Option<Box<dyn Action>>) {
+ ("Project Panel".into(), Some(Box::new(ToggleFocus)))
+ }
+
+ 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 {
@@ -1378,6 +1561,7 @@ mod tests {
use serde_json::json;
use settings::SettingsStore;
use std::{collections::HashSet, path::Path};
+ use workspace::{pane, AppState};
#[gpui::test]
async fn test_visible_list(cx: &mut gpui::TestAppContext) {
@@ -1853,6 +2037,95 @@ mod tests {
);
}
+ #[gpui::test]
+ async fn test_remove_opened_file(cx: &mut gpui::TestAppContext) {
+ init_test_with_editor(cx);
+
+ let fs = FakeFs::new(cx.background());
+ fs.insert_tree(
+ "/src",
+ json!({
+ "test": {
+ "first.rs": "// First Rust file",
+ "second.rs": "// Second Rust file",
+ "third.rs": "// Third Rust file",
+ }
+ }),
+ )
+ .await;
+
+ let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
+ let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+ let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
+
+ toggle_expand_dir(&panel, "src/test", cx);
+ select_path(&panel, "src/test/first.rs", cx);
+ panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
+ cx.foreground().run_until_parked();
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &[
+ "v src",
+ " v test",
+ " first.rs <== selected",
+ " second.rs",
+ " third.rs"
+ ]
+ );
+ ensure_single_file_is_opened(window_id, &workspace, "test/first.rs", cx);
+
+ submit_deletion(window_id, &panel, cx);
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &[
+ "v src",
+ " v test",
+ " second.rs",
+ " third.rs"
+ ],
+ "Project panel should have no deleted file, no other file is selected in it"
+ );
+ ensure_no_open_items_and_panes(window_id, &workspace, cx);
+
+ select_path(&panel, "src/test/second.rs", cx);
+ panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
+ cx.foreground().run_until_parked();
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &[
+ "v src",
+ " v test",
+ " second.rs <== selected",
+ " third.rs"
+ ]
+ );
+ ensure_single_file_is_opened(window_id, &workspace, "test/second.rs", cx);
+
+ cx.update_window(window_id, |cx| {
+ let active_items = workspace
+ .read(cx)
+ .panes()
+ .iter()
+ .filter_map(|pane| pane.read(cx).active_item())
+ .collect::<Vec<_>>();
+ assert_eq!(active_items.len(), 1);
+ let open_editor = active_items
+ .into_iter()
+ .next()
+ .unwrap()
+ .downcast::<Editor>()
+ .expect("Open item should be an editor");
+ open_editor.update(cx, |editor, cx| editor.set_text("Another text!", cx));
+ });
+ submit_deletion(window_id, &panel, cx);
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &["v src", " v test", " third.rs"],
+ "Project panel should have no deleted file, with one last file remaining"
+ );
+ ensure_no_open_items_and_panes(window_id, &workspace, cx);
+ }
+
fn toggle_expand_dir(
panel: &ViewHandle<ProjectPanel>,
path: impl AsRef<Path>,
@@ -1950,10 +2223,104 @@ mod tests {
cx.foreground().forbid_parking();
cx.update(|cx| {
cx.set_global(SettingsStore::test(cx));
+ init_settings(cx);
theme::init((), cx);
language::init(cx);
editor::init_settings(cx);
+ crate::init(cx);
workspace::init_settings(cx);
});
}
+
+ fn init_test_with_editor(cx: &mut TestAppContext) {
+ cx.foreground().forbid_parking();
+ cx.update(|cx| {
+ let app_state = AppState::test(cx);
+ theme::init((), cx);
+ init_settings(cx);
+ language::init(cx);
+ editor::init(cx);
+ pane::init(cx);
+ crate::init(cx);
+ workspace::init(app_state.clone(), cx);
+ });
+ }
+
+ fn ensure_single_file_is_opened(
+ window_id: usize,
+ workspace: &ViewHandle<Workspace>,
+ expected_path: &str,
+ cx: &mut TestAppContext,
+ ) {
+ cx.read_window(window_id, |cx| {
+ let workspace = workspace.read(cx);
+ let worktrees = workspace.worktrees(cx).collect::<Vec<_>>();
+ assert_eq!(worktrees.len(), 1);
+ let worktree_id = WorktreeId::from_usize(worktrees[0].id());
+
+ let open_project_paths = workspace
+ .panes()
+ .iter()
+ .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
+ .collect::<Vec<_>>();
+ assert_eq!(
+ open_project_paths,
+ vec![ProjectPath {
+ worktree_id,
+ path: Arc::from(Path::new(expected_path))
+ }],
+ "Should have opened file, selected in project panel"
+ );
+ });
+ }
+
+ fn submit_deletion(
+ window_id: usize,
+ panel: &ViewHandle<ProjectPanel>,
+ cx: &mut TestAppContext,
+ ) {
+ assert!(
+ !cx.has_pending_prompt(window_id),
+ "Should have no prompts before the deletion"
+ );
+ panel.update(cx, |panel, cx| {
+ panel
+ .delete(&Delete, cx)
+ .expect("Deletion start")
+ .detach_and_log_err(cx);
+ });
+ assert!(
+ cx.has_pending_prompt(window_id),
+ "Should have a prompt after the deletion"
+ );
+ cx.simulate_prompt_answer(window_id, 0);
+ assert!(
+ !cx.has_pending_prompt(window_id),
+ "Should have no prompts after prompt was replied to"
+ );
+ cx.foreground().run_until_parked();
+ }
+
+ fn ensure_no_open_items_and_panes(
+ window_id: usize,
+ workspace: &ViewHandle<Workspace>,
+ cx: &mut TestAppContext,
+ ) {
+ assert!(
+ !cx.has_pending_prompt(window_id),
+ "Should have no prompts after deletion operation closes the file"
+ );
+ cx.read_window(window_id, |cx| {
+ let open_project_paths = workspace
+ .read(cx)
+ .panes()
+ .iter()
+ .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
+ .collect::<Vec<_>>();
+ assert!(
+ open_project_paths.is_empty(),
+ "Deleted file's buffer should be closed, but got open files: {open_project_paths:?}"
+ );
+ });
+ }
}
@@ -0,0 +1,39 @@
+use anyhow;
+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 {
+ const KEY: Option<&'static str> = Some("project_panel");
+
+ type FileContent = ProjectPanelSettingsContent;
+
+ fn load(
+ default_value: &Self::FileContent,
+ user_values: &[&Self::FileContent],
+ _: &gpui::AppContext,
+ ) -> anyhow::Result<Self> {
+ Self::load_via_json_merge(default_value, user_values)
+ }
+}
@@ -27,6 +27,7 @@ smol.workspace = true
[dev-dependencies]
futures.workspace = true
+editor = { path = "../editor", features = ["test-support"] }
settings = { path = "../settings", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
language = { path = "../language", features = ["test-support"] }
@@ -24,3 +24,6 @@ workspace = { path = "../workspace" }
ordered-float.workspace = true
postage.workspace = true
smol.workspace = true
+
+[dev-dependencies]
+editor = { path = "../editor", features = ["test-support"] }
@@ -27,7 +27,7 @@ serde.workspace = true
serde_derive.workspace = true
smallvec.workspace = true
smol.workspace = true
-glob.workspace = true
+globset.workspace = true
[dev-dependencies]
client = { path = "../client", features = ["test-support"] }
@@ -2,12 +2,14 @@ use crate::{
SearchOption, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleRegex,
ToggleWholeWord,
};
+use anyhow::Result;
use collections::HashMap;
use editor::{
items::active_match_index, scroll::autoscroll::Autoscroll, Anchor, Editor, MultiBuffer,
SelectAll, MAX_TAB_TITLE_LEN,
};
use futures::StreamExt;
+use globset::{Glob, GlobMatcher};
use gpui::{
actions,
elements::*,
@@ -46,7 +48,7 @@ pub fn init(cx: &mut AppContext) {
cx.add_action(ProjectSearchBar::search_in_new);
cx.add_action(ProjectSearchBar::select_next_match);
cx.add_action(ProjectSearchBar::select_prev_match);
- cx.add_action(ProjectSearchBar::toggle_focus);
+ cx.add_action(ProjectSearchBar::move_focus_to_results);
cx.capture_action(ProjectSearchBar::tab);
cx.capture_action(ProjectSearchBar::tab_previous);
add_toggle_option_action::<ToggleCaseSensitive>(SearchOption::CaseSensitive, cx);
@@ -571,46 +573,30 @@ impl ProjectSearchView {
fn build_search_query(&mut self, cx: &mut ViewContext<Self>) -> Option<SearchQuery> {
let text = self.query_editor.read(cx).text(cx);
- let included_files = match self
- .included_files_editor
- .read(cx)
- .text(cx)
- .split(',')
- .map(str::trim)
- .filter(|glob_str| !glob_str.is_empty())
- .map(|glob_str| glob::Pattern::new(glob_str))
- .collect::<Result<_, _>>()
- {
- Ok(included_files) => {
- self.panels_with_errors.remove(&InputPanel::Include);
- included_files
- }
- Err(_e) => {
- self.panels_with_errors.insert(InputPanel::Include);
- cx.notify();
- return None;
- }
- };
- let excluded_files = match self
- .excluded_files_editor
- .read(cx)
- .text(cx)
- .split(',')
- .map(str::trim)
- .filter(|glob_str| !glob_str.is_empty())
- .map(|glob_str| glob::Pattern::new(glob_str))
- .collect::<Result<_, _>>()
- {
- Ok(excluded_files) => {
- self.panels_with_errors.remove(&InputPanel::Exclude);
- excluded_files
- }
- Err(_e) => {
- self.panels_with_errors.insert(InputPanel::Exclude);
- cx.notify();
- return None;
- }
- };
+ let included_files =
+ match Self::load_glob_set(&self.included_files_editor.read(cx).text(cx)) {
+ Ok(included_files) => {
+ self.panels_with_errors.remove(&InputPanel::Include);
+ included_files
+ }
+ Err(_e) => {
+ self.panels_with_errors.insert(InputPanel::Include);
+ cx.notify();
+ return None;
+ }
+ };
+ let excluded_files =
+ match Self::load_glob_set(&self.excluded_files_editor.read(cx).text(cx)) {
+ Ok(excluded_files) => {
+ self.panels_with_errors.remove(&InputPanel::Exclude);
+ excluded_files
+ }
+ Err(_e) => {
+ self.panels_with_errors.insert(InputPanel::Exclude);
+ cx.notify();
+ return None;
+ }
+ };
if self.regex {
match SearchQuery::regex(
text,
@@ -640,6 +626,14 @@ impl ProjectSearchView {
}
}
+ fn load_glob_set(text: &str) -> Result<Vec<GlobMatcher>> {
+ text.split(',')
+ .map(str::trim)
+ .filter(|glob_str| !glob_str.is_empty())
+ .map(|glob_str| anyhow::Ok(Glob::new(glob_str)?.compile_matcher()))
+ .collect()
+ }
+
fn select_match(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
if let Some(index) = self.active_match_index {
let match_ranges = self.model.read(cx).match_ranges.clone();
@@ -800,18 +794,16 @@ impl ProjectSearchBar {
}
}
- fn toggle_focus(pane: &mut Pane, _: &ToggleFocus, cx: &mut ViewContext<Pane>) {
+ fn move_focus_to_results(pane: &mut Pane, _: &ToggleFocus, cx: &mut ViewContext<Pane>) {
if let Some(search_view) = pane
.active_item()
.and_then(|item| item.downcast::<ProjectSearchView>())
{
search_view.update(cx, |search_view, cx| {
- if search_view.query_editor.is_focused(cx) {
- if !search_view.model.read(cx).match_ranges.is_empty() {
- search_view.focus_results_editor(cx);
- }
- } else {
- search_view.focus_query_editor(cx);
+ if search_view.query_editor.is_focused(cx)
+ && !search_view.model.read(cx).match_ranges.is_empty()
+ {
+ search_view.focus_results_editor(cx);
}
});
} else {
@@ -22,7 +22,6 @@ util = { path = "../util" }
anyhow.workspace = true
futures.workspace = true
-glob.workspace = true
json_comments = "0.2"
lazy_static.workspace = true
postage.workspace = true
@@ -25,7 +25,7 @@ pub trait Setting: 'static {
const KEY: Option<&'static str>;
/// The type that is stored in an individual JSON file.
- type FileContent: Clone + Serialize + DeserializeOwned + JsonSchema;
+ type FileContent: Clone + Default + Serialize + DeserializeOwned + JsonSchema;
/// The logic for combining together values from one or more JSON files into the
/// final value for this setting.
@@ -460,11 +460,12 @@ impl SettingsStore {
// If the global settings file changed, reload the global value for the field.
if changed_local_path.is_none() {
- setting_value.set_global_value(setting_value.load_setting(
- &default_settings,
- &user_settings_stack,
- cx,
- )?);
+ if let Some(value) = setting_value
+ .load_setting(&default_settings, &user_settings_stack, cx)
+ .log_err()
+ {
+ setting_value.set_global_value(value);
+ }
}
// Reload the local values for the setting.
@@ -495,14 +496,12 @@ impl SettingsStore {
continue;
}
- setting_value.set_local_value(
- path.clone(),
- setting_value.load_setting(
- &default_settings,
- &user_settings_stack,
- cx,
- )?,
- );
+ if let Some(value) = setting_value
+ .load_setting(&default_settings, &user_settings_stack, cx)
+ .log_err()
+ {
+ setting_value.set_local_value(path.clone(), value);
+ }
}
}
}
@@ -536,7 +535,12 @@ impl<T: Setting> AnySettingValue for SettingValue<T> {
fn deserialize_setting(&self, mut json: &serde_json::Value) -> Result<DeserializedSetting> {
if let Some(key) = T::KEY {
- json = json.get(key).unwrap_or(&serde_json::Value::Null);
+ if let Some(value) = json.get(key) {
+ json = value;
+ } else {
+ let value = T::FileContent::default();
+ return Ok(DeserializedSetting(Box::new(value)));
+ }
}
let value = T::FileContent::deserialize(json)?;
Ok(DeserializedSetting(Box::new(value)))
@@ -826,37 +830,6 @@ mod tests {
store.register_setting::<UserSettings>(cx);
store.register_setting::<TurboSetting>(cx);
store.register_setting::<MultiKeySettings>(cx);
-
- // error - missing required field in default settings
- store
- .set_default_settings(
- r#"{
- "user": {
- "name": "John Doe",
- "age": 30,
- "staff": false
- }
- }"#,
- cx,
- )
- .unwrap_err();
-
- // error - type error in default settings
- store
- .set_default_settings(
- r#"{
- "turbo": "the-wrong-type",
- "user": {
- "name": "John Doe",
- "age": 30,
- "staff": false
- }
- }"#,
- cx,
- )
- .unwrap_err();
-
- // valid default settings.
store
.set_default_settings(
r#"{
@@ -1126,7 +1099,7 @@ mod tests {
staff: bool,
}
- #[derive(Clone, Serialize, Deserialize, JsonSchema)]
+ #[derive(Default, Clone, Serialize, Deserialize, JsonSchema)]
struct UserSettingsJson {
name: Option<String>,
age: Option<u32>,
@@ -1170,7 +1143,7 @@ mod tests {
key2: String,
}
- #[derive(Clone, Serialize, Deserialize, JsonSchema)]
+ #[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
struct MultiKeySettingsJson {
key1: Option<String>,
key2: Option<String>,
@@ -1203,7 +1176,7 @@ mod tests {
Hour24,
}
- #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
+ #[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
struct JournalSettingsJson {
pub path: Option<String>,
pub hour_format: Option<HourFormat>,
@@ -1223,7 +1196,7 @@ mod tests {
}
}
- #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
+ #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
struct LanguageSettings {
#[serde(default)]
languages: HashMap<String, LanguageSettingEntry>,
@@ -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 {
@@ -39,6 +39,7 @@ serde_derive.workspace = true
[dev-dependencies]
+editor = { path = "../editor", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
client = { path = "../client", features = ["test-support"]}
project = { path = "../project", features = ["test-support"]}
@@ -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,408 @@
+use std::sync::Arc;
+
+use crate::TerminalView;
+use db::kvp::KEY_VALUE_STORE;
+use gpui::{
+ actions, anyhow::Result, elements::*, serde_json, Action, 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 |pane, cx| {
+ let this = weak_self.clone();
+ Flex::row()
+ .with_child(Pane::render_tab_bar_button(
+ 0,
+ "icons/plus_12.svg",
+ Some((
+ "New Terminal".into(),
+ Some(Box::new(workspace::NewTerminal)),
+ )),
+ 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,
+ ))
+ .with_child(Pane::render_tab_bar_button(
+ 1,
+ if pane.is_zoomed() {
+ "icons/minimize_8.svg"
+ } else {
+ "icons/maximize_8.svg"
+ },
+ Some(("Toggle Zoom".into(), Some(Box::new(workspace::ToggleZoom)))),
+ cx,
+ move |pane, cx| pane.toggle_zoom(&Default::default(), cx),
+ None,
+ ))
+ .into_any()
+ });
+ 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)
+ }));
+ let focus = pane.read(cx).has_focus();
+ Pane::add_item(workspace, &pane, terminal, true, focus, 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, Option<Box<dyn Action>>) {
+ ("Terminal Panel".into(), Some(Box::new(ToggleFocus)))
+ }
+
+ 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);
@@ -1783,6 +1783,19 @@ impl BufferSnapshot {
where
D: 'a + TextDimension,
A: 'a + IntoIterator<Item = &'a Anchor>,
+ {
+ let anchors = anchors.into_iter();
+ self.summaries_for_anchors_with_payload::<D, _, ()>(anchors.map(|a| (a, ())))
+ .map(|d| d.0)
+ }
+
+ pub fn summaries_for_anchors_with_payload<'a, D, A, T>(
+ &'a self,
+ anchors: A,
+ ) -> impl 'a + Iterator<Item = (D, T)>
+ where
+ D: 'a + TextDimension,
+ A: 'a + IntoIterator<Item = (&'a Anchor, T)>,
{
let anchors = anchors.into_iter();
let mut insertion_cursor = self.insertions.cursor::<InsertionFragmentKey>();
@@ -1790,11 +1803,11 @@ impl BufferSnapshot {
let mut text_cursor = self.visible_text.cursor(0);
let mut position = D::default();
- anchors.map(move |anchor| {
+ anchors.map(move |(anchor, payload)| {
if *anchor == Anchor::MIN {
- return D::default();
+ return (D::default(), payload);
} else if *anchor == Anchor::MAX {
- return D::from_text_summary(&self.visible_text.summary());
+ return (D::from_text_summary(&self.visible_text.summary()), payload);
}
let anchor_key = InsertionFragmentKey {
@@ -1825,7 +1838,7 @@ impl BufferSnapshot {
}
position.add_assign(&text_cursor.summary(fragment_offset));
- position.clone()
+ (position.clone(), payload)
})
}
@@ -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,
@@ -446,6 +438,19 @@ pub struct ProjectPanelEntry {
pub icon_color: Color,
pub icon_size: f32,
pub icon_spacing: f32,
+ pub status: EntryStatus,
+}
+
+#[derive(Clone, Debug, Deserialize, Default)]
+pub struct EntryStatus {
+ pub git: GitProjectStatus,
+}
+
+#[derive(Clone, Debug, Deserialize, Default)]
+pub struct GitProjectStatus {
+ pub modified: Color,
+ pub inserted: Color,
+ pub conflict: Color,
}
#[derive(Clone, Debug, Deserialize, Default)]
@@ -670,6 +675,14 @@ pub struct Scrollbar {
pub thumb: ContainerStyle,
pub width: f32,
pub min_height_factor: f32,
+ pub git: GitDiffColors,
+}
+
+#[derive(Clone, Deserialize, Default)]
+pub struct GitDiffColors {
+ pub inserted: Color,
+ pub modified: Color,
+ pub deleted: Color,
}
#[derive(Clone, Deserialize, Default)]
@@ -1,10 +1,9 @@
use std::borrow::Cow;
-use fs::repository::GitFileStatus;
use gpui::{
color::Color,
elements::{
- ConstrainedBox, Container, ContainerStyle, Empty, Flex, KeystrokeLabel, Label, LabelStyle,
+ ConstrainedBox, Container, ContainerStyle, Empty, Flex, KeystrokeLabel, Label,
MouseEventHandler, ParentElement, Stack, Svg,
},
fonts::TextStyle,
@@ -12,11 +11,11 @@ use gpui::{
platform,
platform::MouseButton,
scene::MouseClick,
- Action, AnyElement, Element, EventContext, MouseState, View, ViewContext,
+ Action, Element, EventContext, MouseState, View, ViewContext,
};
use serde::Deserialize;
-use crate::{ContainedText, Interactive, Theme};
+use crate::{ContainedText, Interactive};
#[derive(Clone, Deserialize, Default)]
pub struct CheckboxStyle {
@@ -253,53 +252,3 @@ where
.constrained()
.with_height(style.dimensions().y())
}
-
-pub struct FileName {
- filename: String,
- git_status: Option<GitFileStatus>,
- style: FileNameStyle,
-}
-
-pub struct FileNameStyle {
- template_style: LabelStyle,
- git_inserted: Color,
- git_modified: Color,
- git_deleted: Color,
-}
-
-impl FileName {
- pub fn new(filename: String, git_status: Option<GitFileStatus>, style: FileNameStyle) -> Self {
- FileName {
- filename,
- git_status,
- style,
- }
- }
-
- pub fn style<I: Into<LabelStyle>>(style: I, theme: &Theme) -> FileNameStyle {
- FileNameStyle {
- template_style: style.into(),
- git_inserted: theme.editor.diff.inserted,
- git_modified: theme.editor.diff.modified,
- git_deleted: theme.editor.diff.deleted,
- }
- }
-}
-
-impl<V: View> gpui::elements::Component<V> for FileName {
- fn render(&self, _: &mut V, _: &mut ViewContext<V>) -> AnyElement<V> {
- // Prepare colors for git statuses
- let mut filename_text_style = self.style.template_style.text.clone();
- filename_text_style.color = self
- .git_status
- .as_ref()
- .map(|status| match status {
- GitFileStatus::Added => self.style.git_inserted,
- GitFileStatus::Modified => self.style.git_modified,
- GitFileStatus::Conflict => self.style.git_deleted,
- })
- .unwrap_or(self.style.template_style.text.color);
-
- Label::new(self.filename.clone(), filename_text_style).into_any()
- }
-}
@@ -23,3 +23,6 @@ log.workspace = true
parking_lot.workspace = true
postage.workspace = true
smol.workspace = true
+
+[dev-dependencies]
+editor = { path = "../editor", features = ["test-support"] }
@@ -3,15 +3,12 @@ use std::env;
use lazy_static::lazy_static;
lazy_static! {
- // TODO: Put this back!
- pub static ref RELEASE_CHANNEL_NAME: String = env::var("ZED_RELEASE_CHANNEL")
- .unwrap_or_else(|_| include_str!("../../zed/RELEASE_CHANNEL").to_string());
- // pub static ref RELEASE_CHANNEL_NAME: String = if cfg!(debug_assertions) {
- // env::var("ZED_RELEASE_CHANNEL")
- // .unwrap_or_else(|_| include_str!("../../zed/RELEASE_CHANNEL").to_string())
- // } else {
- // include_str!("../../zed/RELEASE_CHANNEL").to_string()
- // };
+ pub static ref RELEASE_CHANNEL_NAME: String = if cfg!(debug_assertions) {
+ env::var("ZED_RELEASE_CHANNEL")
+ .unwrap_or_else(|_| include_str!("../../zed/RELEASE_CHANNEL").to_string())
+ } else {
+ include_str!("../../zed/RELEASE_CHANNEL").to_string()
+ };
pub static ref RELEASE_CHANNEL: ReleaseChannel = match RELEASE_CHANNEL_NAME.as_str() {
"dev" => ReleaseChannel::Dev,
"preview" => ReleaseChannel::Preview,
@@ -30,3 +30,6 @@ anyhow.workspace = true
log.workspace = true
schemars.workspace = true
serde.workspace = true
+
+[dev-dependencies]
+editor = { path = "../editor", features = ["test-support"] }
@@ -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, false, 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);
@@ -1,826 +1,699 @@
-mod toggle_dock_button;
-
-use crate::{
- sidebar::SidebarSide, BackgroundActions, DockAnchor, ItemHandle, Pane, Workspace,
- WorkspaceSettings,
-};
-use collections::HashMap;
+use crate::{StatusItemView, Workspace};
+use context_menu::{ContextMenu, ContextMenuItem};
use gpui::{
- actions,
- elements::{ChildView, Empty, MouseEventHandler, ParentElement, Side, Stack},
- geometry::vector::Vector2F,
- platform::{CursorStyle, MouseButton},
- AnyElement, AppContext, Border, Element, SizeConstraint, ViewContext, ViewHandle,
+ elements::*, platform::CursorStyle, platform::MouseButton, Action, AnyViewHandle, AppContext,
+ Axis, Entity, Subscription, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
};
-use std::sync::{atomic::AtomicUsize, Arc};
-use theme::Theme;
-pub use toggle_dock_button::ToggleDockButton;
-
-actions!(
- dock,
- [
- FocusDock,
- HideDock,
- AnchorDockRight,
- AnchorDockBottom,
- ExpandDock,
- AddTabToDock,
- RemoveTabFromDock,
- ]
-);
-
-pub fn init(cx: &mut AppContext) {
- cx.add_action(Dock::focus_dock);
- cx.add_action(Dock::hide_dock);
- cx.add_action(
- |workspace: &mut Workspace, _: &AnchorDockRight, cx: &mut ViewContext<Workspace>| {
- Dock::move_dock(workspace, DockAnchor::Right, true, cx);
- },
- );
- cx.add_action(
- |workspace: &mut Workspace, _: &AnchorDockBottom, cx: &mut ViewContext<Workspace>| {
- Dock::move_dock(workspace, DockAnchor::Bottom, true, cx)
- },
- );
- cx.add_action(
- |workspace: &mut Workspace, _: &ExpandDock, cx: &mut ViewContext<Workspace>| {
- Dock::move_dock(workspace, DockAnchor::Expanded, true, cx)
- },
- );
- cx.add_action(
- |workspace: &mut Workspace, _: &AddTabToDock, cx: &mut ViewContext<Workspace>| {
- if let Some(active_item) = workspace.active_item(cx) {
- let item_id = active_item.id();
-
- let from = workspace.active_pane();
- let to = workspace.dock_pane();
- if from.id() == to.id() {
- return;
- }
+use serde::Deserialize;
+use std::rc::Rc;
+use theme::ThemeSettings;
+
+pub trait Panel: View {
+ fn position(&self, cx: &WindowContext) -> DockPosition;
+ fn position_is_valid(&self, position: DockPosition) -> bool;
+ fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>);
+ fn size(&self, cx: &WindowContext) -> f32;
+ fn set_size(&mut self, size: f32, cx: &mut ViewContext<Self>);
+ fn icon_path(&self) -> &'static str;
+ fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>);
+ fn icon_label(&self, _: &WindowContext) -> Option<String> {
+ None
+ }
+ fn should_change_position_on_event(_: &Self::Event) -> bool;
+ fn should_zoom_in_on_event(_: &Self::Event) -> bool;
+ fn should_zoom_out_on_event(_: &Self::Event) -> bool;
+ fn is_zoomed(&self, cx: &WindowContext) -> bool;
+ fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>);
+ fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>);
+ fn should_activate_on_event(_: &Self::Event) -> bool;
+ fn should_close_on_event(_: &Self::Event) -> bool;
+ fn has_focus(&self, cx: &WindowContext) -> bool;
+ fn is_focus_event(_: &Self::Event) -> bool;
+}
- let destination_index = to.read(cx).items_len() + 1;
+pub trait PanelHandle {
+ fn id(&self) -> usize;
+ fn position(&self, cx: &WindowContext) -> DockPosition;
+ fn position_is_valid(&self, position: DockPosition, cx: &WindowContext) -> bool;
+ fn set_position(&self, position: DockPosition, cx: &mut WindowContext);
+ fn is_zoomed(&self, cx: &WindowContext) -> bool;
+ fn set_zoomed(&self, zoomed: bool, cx: &mut WindowContext);
+ fn set_active(&self, active: bool, cx: &mut WindowContext);
+ fn size(&self, cx: &WindowContext) -> f32;
+ fn set_size(&self, size: f32, cx: &mut WindowContext);
+ fn icon_path(&self, cx: &WindowContext) -> &'static str;
+ fn icon_tooltip(&self, cx: &WindowContext) -> (String, Option<Box<dyn Action>>);
+ fn icon_label(&self, cx: &WindowContext) -> Option<String>;
+ fn has_focus(&self, cx: &WindowContext) -> bool;
+ fn as_any(&self) -> &AnyViewHandle;
+}
- Pane::move_item(
- workspace,
- from.clone(),
- to.clone(),
- item_id,
- destination_index,
- cx,
- );
- }
- },
- );
- cx.add_action(
- |workspace: &mut Workspace, _: &RemoveTabFromDock, cx: &mut ViewContext<Workspace>| {
- if let Some(active_item) = workspace.active_item(cx) {
- let item_id = active_item.id();
-
- let from = workspace.dock_pane();
- let to = workspace
- .last_active_center_pane
- .as_ref()
- .and_then(|pane| pane.upgrade(cx))
- .unwrap_or_else(|| {
- workspace
- .panes
- .first()
- .expect("There must be a pane")
- .clone()
- });
-
- if from.id() == to.id() {
- return;
- }
+impl<T> PanelHandle for ViewHandle<T>
+where
+ T: Panel,
+{
+ fn id(&self) -> usize {
+ self.id()
+ }
- let destination_index = to.read(cx).items_len() + 1;
+ fn position(&self, cx: &WindowContext) -> DockPosition {
+ self.read(cx).position(cx)
+ }
- Pane::move_item(
- workspace,
- from.clone(),
- to.clone(),
- item_id,
- destination_index,
- cx,
- );
- }
- },
- );
-}
+ fn position_is_valid(&self, position: DockPosition, cx: &WindowContext) -> bool {
+ self.read(cx).position_is_valid(position)
+ }
-#[derive(Copy, Clone, PartialEq, Eq, Debug)]
-pub enum DockPosition {
- Shown(DockAnchor),
- Hidden(DockAnchor),
-}
+ fn set_position(&self, position: DockPosition, cx: &mut WindowContext) {
+ self.update(cx, |this, cx| this.set_position(position, cx))
+ }
-impl Default for DockPosition {
- fn default() -> Self {
- DockPosition::Hidden(Default::default())
+ fn size(&self, cx: &WindowContext) -> f32 {
+ self.read(cx).size(cx)
+ }
+
+ fn set_size(&self, size: f32, cx: &mut WindowContext) {
+ self.update(cx, |this, cx| this.set_size(size, cx))
+ }
+
+ fn is_zoomed(&self, cx: &WindowContext) -> bool {
+ self.read(cx).is_zoomed(cx)
+ }
+
+ fn set_zoomed(&self, zoomed: bool, cx: &mut WindowContext) {
+ self.update(cx, |this, cx| this.set_zoomed(zoomed, cx))
+ }
+
+ fn set_active(&self, active: bool, cx: &mut WindowContext) {
+ self.update(cx, |this, cx| this.set_active(active, cx))
+ }
+
+ fn icon_path(&self, cx: &WindowContext) -> &'static str {
+ self.read(cx).icon_path()
+ }
+
+ fn icon_tooltip(&self, cx: &WindowContext) -> (String, Option<Box<dyn Action>>) {
+ self.read(cx).icon_tooltip()
+ }
+
+ fn icon_label(&self, cx: &WindowContext) -> Option<String> {
+ self.read(cx).icon_label(cx)
+ }
+
+ fn has_focus(&self, cx: &WindowContext) -> bool {
+ self.read(cx).has_focus(cx)
}
-}
-pub fn icon_for_dock_anchor(anchor: DockAnchor) -> &'static str {
- match anchor {
- DockAnchor::Right => "icons/dock_right_12.svg",
- DockAnchor::Bottom => "icons/dock_bottom_12.svg",
- DockAnchor::Expanded => "icons/dock_modal_12.svg",
+ fn as_any(&self) -> &AnyViewHandle {
+ self
}
}
-impl DockPosition {
- pub fn is_visible(&self) -> bool {
- match self {
- DockPosition::Shown(_) => true,
- DockPosition::Hidden(_) => false,
- }
+impl From<&dyn PanelHandle> for AnyViewHandle {
+ fn from(val: &dyn PanelHandle) -> Self {
+ val.as_any().clone()
}
+}
+
+pub struct Dock {
+ position: DockPosition,
+ panel_entries: Vec<PanelEntry>,
+ is_open: bool,
+ active_panel_index: usize,
+}
+
+#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
+pub enum DockPosition {
+ Left,
+ Bottom,
+ Right,
+}
- pub fn anchor(&self) -> DockAnchor {
+impl DockPosition {
+ fn to_label(&self) -> &'static str {
match self {
- DockPosition::Shown(anchor) | DockPosition::Hidden(anchor) => *anchor,
+ Self::Left => "left",
+ Self::Bottom => "bottom",
+ Self::Right => "right",
}
}
- fn hide(self) -> Self {
+ fn to_resize_handle_side(self) -> HandleSide {
match self {
- DockPosition::Shown(anchor) => DockPosition::Hidden(anchor),
- DockPosition::Hidden(_) => self,
+ Self::Left => HandleSide::Right,
+ Self::Bottom => HandleSide::Top,
+ Self::Right => HandleSide::Left,
}
}
- fn show(self) -> Self {
+ pub fn axis(&self) -> Axis {
match self {
- DockPosition::Hidden(anchor) => DockPosition::Shown(anchor),
- DockPosition::Shown(_) => self,
+ Self::Left | Self::Right => Axis::Horizontal,
+ Self::Bottom => Axis::Vertical,
}
}
}
-pub type DockDefaultItemFactory =
- fn(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> Option<Box<dyn ItemHandle>>;
+struct PanelEntry {
+ panel: Rc<dyn PanelHandle>,
+ context_menu: ViewHandle<ContextMenu>,
+ _subscriptions: [Subscription; 2],
+}
-pub struct Dock {
- position: DockPosition,
- panel_sizes: HashMap<DockAnchor, f32>,
- pane: ViewHandle<Pane>,
- default_item_factory: DockDefaultItemFactory,
+pub struct PanelButtons {
+ dock: ViewHandle<Dock>,
+ workspace: WeakViewHandle<Workspace>,
}
impl Dock {
- pub fn new(
- default_item_factory: DockDefaultItemFactory,
- background_actions: BackgroundActions,
- pane_history_timestamp: Arc<AtomicUsize>,
- cx: &mut ViewContext<Workspace>,
- ) -> Self {
- let position =
- DockPosition::Hidden(settings::get::<WorkspaceSettings>(cx).default_dock_anchor);
- let workspace = cx.weak_handle();
- let pane = cx.add_view(|cx| {
- Pane::new(
- workspace,
- Some(position.anchor()),
- background_actions,
- pane_history_timestamp,
- cx,
- )
- });
- pane.update(cx, |pane, cx| {
- pane.set_active(false, cx);
- });
- cx.subscribe(&pane, Workspace::handle_pane_event).detach();
-
+ pub fn new(position: DockPosition) -> Self {
Self {
- pane,
- panel_sizes: Default::default(),
position,
- default_item_factory,
+ panel_entries: Default::default(),
+ active_panel_index: 0,
+ is_open: false,
}
}
- pub fn pane(&self) -> &ViewHandle<Pane> {
- &self.pane
+ pub fn is_open(&self) -> bool {
+ self.is_open
}
- pub fn visible_pane(&self) -> Option<&ViewHandle<Pane>> {
- self.position.is_visible().then(|| self.pane())
+ pub fn has_focus(&self, cx: &WindowContext) -> bool {
+ self.active_panel()
+ .map_or(false, |panel| panel.has_focus(cx))
}
- pub fn is_anchored_at(&self, anchor: DockAnchor) -> bool {
- self.position.is_visible() && self.position.anchor() == anchor
+ pub fn panel_index_for_type<T: Panel>(&self) -> Option<usize> {
+ self.panel_entries
+ .iter()
+ .position(|entry| entry.panel.as_any().is::<T>())
}
- pub(crate) fn set_dock_position(
- workspace: &mut Workspace,
- new_position: DockPosition,
- focus: bool,
- cx: &mut ViewContext<Workspace>,
- ) {
- workspace.dock.position = new_position;
- // Tell the pane about the new anchor position
- workspace.dock.pane.update(cx, |pane, cx| {
- pane.set_docked(Some(new_position.anchor()), cx)
- });
+ pub fn panel_index_for_ui_name(&self, ui_name: &str, cx: &AppContext) -> Option<usize> {
+ self.panel_entries.iter().position(|entry| {
+ let panel = entry.panel.as_any();
+ cx.view_ui_name(panel.window_id(), panel.id()) == Some(ui_name)
+ })
+ }
- if workspace.dock.position.is_visible() {
- // Close the right sidebar if the dock is on the right side and the right sidebar is open
- if workspace.dock.position.anchor() == DockAnchor::Right {
- if workspace.right_sidebar().read(cx).is_open() {
- workspace.toggle_sidebar(SidebarSide::Right, cx);
- }
+ pub fn active_panel_index(&self) -> usize {
+ self.active_panel_index
+ }
+
+ pub fn set_open(&mut self, open: bool, cx: &mut ViewContext<Self>) {
+ if open != self.is_open {
+ self.is_open = open;
+ if let Some(active_panel) = self.panel_entries.get(self.active_panel_index) {
+ active_panel.panel.set_active(open, cx);
}
- // Ensure that the pane has at least one item or construct a default item to put in it
- let pane = workspace.dock.pane.clone();
- if pane.read(cx).items().next().is_none() {
- if let Some(item_to_add) = (workspace.dock.default_item_factory)(workspace, cx) {
- Pane::add_item(workspace, &pane, item_to_add, focus, focus, None, cx);
- } else {
- workspace.dock.position = workspace.dock.position.hide();
- }
- } else {
- if focus {
- cx.focus(&pane);
+ cx.notify();
+ }
+ }
+
+ pub fn toggle_open(&mut self, cx: &mut ViewContext<Self>) {
+ self.set_open(!self.is_open, cx);
+ cx.notify();
+ }
+
+ pub fn set_panel_zoomed(
+ &mut self,
+ panel: &AnyViewHandle,
+ zoomed: bool,
+ cx: &mut ViewContext<Self>,
+ ) {
+ for entry in &mut self.panel_entries {
+ if entry.panel.as_any() == panel {
+ if zoomed != entry.panel.is_zoomed(cx) {
+ entry.panel.set_zoomed(zoomed, cx);
}
- }
- } else if let Some(last_active_center_pane) = workspace
- .last_active_center_pane
- .as_ref()
- .and_then(|pane| pane.upgrade(cx))
- {
- if focus {
- cx.focus(&last_active_center_pane);
+ } else if entry.panel.is_zoomed(cx) {
+ entry.panel.set_zoomed(false, cx);
}
}
- cx.emit(crate::Event::DockAnchorChanged);
- workspace.serialize_workspace(cx);
+
cx.notify();
}
- pub fn hide(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
- Self::set_dock_position(workspace, workspace.dock.position.hide(), true, cx);
+ pub fn zoom_out(&mut self, cx: &mut ViewContext<Self>) {
+ for entry in &mut self.panel_entries {
+ if entry.panel.is_zoomed(cx) {
+ entry.panel.set_zoomed(false, cx);
+ }
+ }
}
- pub fn show(workspace: &mut Workspace, focus: bool, cx: &mut ViewContext<Workspace>) {
- Self::set_dock_position(workspace, workspace.dock.position.show(), focus, cx);
+ pub fn add_panel<T: Panel>(&mut self, panel: ViewHandle<T>, cx: &mut ViewContext<Self>) {
+ let subscriptions = [
+ cx.observe(&panel, |_, _, cx| cx.notify()),
+ cx.subscribe(&panel, |this, panel, event, cx| {
+ if T::should_activate_on_event(event) {
+ if let Some(ix) = this
+ .panel_entries
+ .iter()
+ .position(|entry| entry.panel.id() == panel.id())
+ {
+ this.set_open(true, cx);
+ this.activate_panel(ix, cx);
+ cx.focus(&panel);
+ }
+ } else if T::should_close_on_event(event)
+ && this.active_panel().map_or(false, |p| p.id() == panel.id())
+ {
+ this.set_open(false, cx);
+ }
+ }),
+ ];
+
+ let dock_view_id = cx.view_id();
+ self.panel_entries.push(PanelEntry {
+ panel: Rc::new(panel),
+ context_menu: cx.add_view(|cx| {
+ let mut menu = ContextMenu::new(dock_view_id, cx);
+ menu.set_position_mode(OverlayPositionMode::Local);
+ menu
+ }),
+ _subscriptions: subscriptions,
+ });
+ cx.notify()
}
- pub fn hide_on_sidebar_shown(
- workspace: &mut Workspace,
- sidebar_side: SidebarSide,
- cx: &mut ViewContext<Workspace>,
- ) {
- if (sidebar_side == SidebarSide::Right && workspace.dock.is_anchored_at(DockAnchor::Right))
- || workspace.dock.is_anchored_at(DockAnchor::Expanded)
+ pub fn remove_panel<T: Panel>(&mut self, panel: &ViewHandle<T>, cx: &mut ViewContext<Self>) {
+ if let Some(panel_ix) = self
+ .panel_entries
+ .iter()
+ .position(|entry| entry.panel.id() == panel.id())
{
- Self::hide(workspace, cx);
+ if panel_ix == self.active_panel_index {
+ self.active_panel_index = 0;
+ self.set_open(false, cx);
+ } else if panel_ix < self.active_panel_index {
+ self.active_panel_index -= 1;
+ }
+ self.panel_entries.remove(panel_ix);
+ cx.notify();
}
}
- pub fn focus_dock(workspace: &mut Workspace, _: &FocusDock, cx: &mut ViewContext<Workspace>) {
- Self::set_dock_position(workspace, workspace.dock.position.show(), true, cx);
+ pub fn panels_len(&self) -> usize {
+ self.panel_entries.len()
}
- pub fn hide_dock(workspace: &mut Workspace, _: &HideDock, cx: &mut ViewContext<Workspace>) {
- Self::set_dock_position(workspace, workspace.dock.position.hide(), true, cx);
+ pub fn activate_panel(&mut self, panel_ix: usize, cx: &mut ViewContext<Self>) {
+ if panel_ix != self.active_panel_index {
+ if let Some(active_panel) = self.panel_entries.get(self.active_panel_index) {
+ active_panel.panel.set_active(false, cx);
+ }
+
+ self.active_panel_index = panel_ix;
+ if let Some(active_panel) = self.panel_entries.get(self.active_panel_index) {
+ active_panel.panel.set_active(true, cx);
+ }
+
+ cx.notify();
+ }
}
- pub fn move_dock(
- workspace: &mut Workspace,
- new_anchor: DockAnchor,
- focus: bool,
- cx: &mut ViewContext<Workspace>,
- ) {
- Self::set_dock_position(workspace, DockPosition::Shown(new_anchor), focus, cx);
- }
-
- pub fn render(
- &self,
- theme: &Theme,
- anchor: DockAnchor,
- cx: &mut ViewContext<Workspace>,
- ) -> Option<AnyElement<Workspace>> {
- let style = &theme.workspace.dock;
-
- self.position
- .is_visible()
- .then(|| self.position.anchor())
- .filter(|current_anchor| *current_anchor == anchor)
- .map(|anchor| match anchor {
- DockAnchor::Bottom | DockAnchor::Right => {
- let mut panel_style = style.panel.clone();
- let (resize_side, initial_size) = if anchor == DockAnchor::Bottom {
- panel_style.border = Border {
- top: true,
- bottom: false,
- left: false,
- right: false,
- ..panel_style.border
- };
-
- (Side::Top, style.initial_size_bottom)
- } else {
- panel_style.border = Border {
- top: false,
- bottom: false,
- left: true,
- right: false,
- ..panel_style.border
- };
- (Side::Left, style.initial_size_right)
- };
-
- enum DockResizeHandle {}
-
- let resizable = ChildView::new(&self.pane, cx)
- .contained()
- .with_style(panel_style)
- .with_resize_handle::<DockResizeHandle>(
- resize_side as usize,
- resize_side,
- 4.,
- self.panel_sizes
- .get(&anchor)
- .copied()
- .unwrap_or(initial_size),
- cx,
- );
-
- let size = resizable.current_size();
- cx.defer(move |workspace, _| {
- workspace.dock.panel_sizes.insert(anchor, size);
- });
-
- if anchor == DockAnchor::Right {
- resizable.constrained().dynamically(|constraint, _, cx| {
- SizeConstraint::new(
- Vector2F::new(20., constraint.min.y()),
- Vector2F::new(cx.window_size().x() * 0.8, constraint.max.y()),
- )
- })
- } else {
- resizable.constrained().dynamically(|constraint, _, cx| {
- SizeConstraint::new(
- Vector2F::new(constraint.min.x(), 50.),
- Vector2F::new(constraint.max.x(), cx.window_size().y() * 0.8),
- )
- })
- }
- .into_any()
- }
- DockAnchor::Expanded => {
- enum ExpandedDockWash {}
- enum ExpandedDockPane {}
+ pub fn active_panel(&self) -> Option<&Rc<dyn PanelHandle>> {
+ let entry = self.active_entry()?;
+ Some(&entry.panel)
+ }
+
+ fn active_entry(&self) -> Option<&PanelEntry> {
+ if self.is_open {
+ self.panel_entries.get(self.active_panel_index)
+ } else {
+ None
+ }
+ }
+
+ pub fn zoomed_panel(&self, cx: &WindowContext) -> Option<Rc<dyn PanelHandle>> {
+ let entry = self.active_entry()?;
+ if entry.panel.is_zoomed(cx) {
+ Some(entry.panel.clone())
+ } else {
+ None
+ }
+ }
+
+ pub fn panel_size(&self, panel: &dyn PanelHandle, cx: &WindowContext) -> Option<f32> {
+ self.panel_entries
+ .iter()
+ .find(|entry| entry.panel.id() == panel.id())
+ .map(|entry| entry.panel.size(cx))
+ }
+
+ pub fn active_panel_size(&self, cx: &WindowContext) -> Option<f32> {
+ if self.is_open {
+ self.panel_entries
+ .get(self.active_panel_index)
+ .map(|entry| entry.panel.size(cx))
+ } else {
+ None
+ }
+ }
+
+ pub fn resize_active_panel(&mut self, size: f32, cx: &mut ViewContext<Self>) {
+ if let Some(entry) = self.panel_entries.get_mut(self.active_panel_index) {
+ entry.panel.set_size(size, cx);
+ cx.notify();
+ }
+ }
+
+ pub fn render_placeholder(&self, cx: &WindowContext) -> AnyElement<Workspace> {
+ if let Some(active_entry) = self.active_entry() {
+ Empty::new()
+ .into_any()
+ .contained()
+ .with_style(self.style(cx))
+ .resizable(
+ self.position.to_resize_handle_side(),
+ active_entry.panel.size(cx),
+ |_, _, _| {},
+ )
+ .into_any()
+ } else {
+ Empty::new().into_any()
+ }
+ }
+
+ fn style(&self, cx: &WindowContext) -> ContainerStyle {
+ let theme = &settings::get::<ThemeSettings>(cx).theme;
+ let style = match self.position {
+ DockPosition::Left => theme.workspace.dock.left,
+ DockPosition::Bottom => theme.workspace.dock.bottom,
+ DockPosition::Right => theme.workspace.dock.right,
+ };
+ style
+ }
+}
+
+impl Entity for Dock {
+ type Event = ();
+}
+
+impl View for Dock {
+ fn ui_name() -> &'static str {
+ "Dock"
+ }
+
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+ if let Some(active_entry) = self.active_entry() {
+ let style = self.style(cx);
+ ChildView::new(active_entry.panel.as_any(), cx)
+ .contained()
+ .with_style(style)
+ .resizable(
+ self.position.to_resize_handle_side(),
+ active_entry.panel.size(cx),
+ |dock: &mut Self, size, cx| dock.resize_active_panel(size, cx),
+ )
+ .into_any()
+ } else {
+ Empty::new().into_any()
+ }
+ }
+
+ fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
+ if cx.is_self_focused() {
+ if let Some(active_entry) = self.active_entry() {
+ cx.focus(active_entry.panel.as_any());
+ } else {
+ cx.focus_parent();
+ }
+ }
+ }
+}
+
+impl PanelButtons {
+ pub fn new(
+ dock: ViewHandle<Dock>,
+ workspace: WeakViewHandle<Workspace>,
+ cx: &mut ViewContext<Self>,
+ ) -> Self {
+ cx.observe(&dock, |_, _, cx| cx.notify()).detach();
+ Self { dock, workspace }
+ }
+}
+
+impl Entity for PanelButtons {
+ type Event = ();
+}
+
+impl View for PanelButtons {
+ fn ui_name() -> &'static str {
+ "PanelButtons"
+ }
+
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+ let theme = &settings::get::<ThemeSettings>(cx).theme;
+ let tooltip_style = theme.tooltip.clone();
+ let theme = &theme.workspace.status_bar.panel_buttons;
+ let button_style = theme.button.clone();
+ let dock = self.dock.read(cx);
+ let active_ix = dock.active_panel_index;
+ let is_open = dock.is_open;
+ let dock_position = dock.position;
+ let group_style = match dock_position {
+ DockPosition::Left => theme.group_left,
+ DockPosition::Bottom => theme.group_bottom,
+ DockPosition::Right => theme.group_right,
+ };
+ let menu_corner = match dock_position {
+ DockPosition::Left => AnchorCorner::BottomLeft,
+ DockPosition::Bottom | DockPosition::Right => AnchorCorner::BottomRight,
+ };
+
+ let panels = dock
+ .panel_entries
+ .iter()
+ .map(|item| (item.panel.clone(), item.context_menu.clone()))
+ .collect::<Vec<_>>();
+ Flex::row()
+ .with_children(panels.into_iter().enumerate().map(
+ |(panel_ix, (view, context_menu))| {
+ let (tooltip, tooltip_action) = view.icon_tooltip(cx);
Stack::new()
.with_child(
- // Render wash under the dock which when clicked hides it
- MouseEventHandler::<ExpandedDockWash, _>::new(0, cx, |_, _| {
- Empty::new()
+ MouseEventHandler::<Self, _>::new(panel_ix, cx, |state, cx| {
+ let is_active = is_open && panel_ix == active_ix;
+ let style = button_style.style_for(state, is_active);
+ Flex::row()
+ .with_child(
+ Svg::new(view.icon_path(cx))
+ .with_color(style.icon_color)
+ .constrained()
+ .with_width(style.icon_size)
+ .aligned(),
+ )
+ .with_children(if let Some(label) = view.icon_label(cx) {
+ Some(
+ Label::new(label, style.label.text.clone())
+ .contained()
+ .with_style(style.label.container)
+ .aligned(),
+ )
+ } else {
+ None
+ })
+ .constrained()
+ .with_height(style.icon_size)
.contained()
- .with_background_color(style.wash_color)
+ .with_style(style.container)
})
- .capture_all()
- .on_down(MouseButton::Left, |_, workspace, cx| {
- Dock::hide_dock(workspace, &Default::default(), cx)
+ .with_cursor_style(CursorStyle::PointingHand)
+ .on_click(MouseButton::Left, {
+ move |_, this, cx| {
+ if let Some(workspace) = this.workspace.upgrade(cx) {
+ cx.window_context().defer(move |cx| {
+ workspace.update(cx, |workspace, cx| {
+ workspace.toggle_panel(dock_position, panel_ix, cx)
+ });
+ });
+ }
+ }
})
- .with_cursor_style(CursorStyle::Arrow),
- )
- .with_child(
- MouseEventHandler::<ExpandedDockPane, _>::new(0, cx, |_state, cx| {
- ChildView::new(&self.pane, cx)
+ .on_click(MouseButton::Right, {
+ let view = view.clone();
+ let menu = context_menu.clone();
+ move |_, _, cx| {
+ const POSITIONS: [DockPosition; 3] = [
+ DockPosition::Left,
+ DockPosition::Right,
+ DockPosition::Bottom,
+ ];
+
+ menu.update(cx, |menu, cx| {
+ let items = POSITIONS
+ .into_iter()
+ .filter(|position| {
+ *position != dock_position
+ && view.position_is_valid(*position, cx)
+ })
+ .map(|position| {
+ let view = view.clone();
+ ContextMenuItem::handler(
+ format!("Dock {}", position.to_label()),
+ move |cx| view.set_position(position, cx),
+ )
+ })
+ .collect();
+ menu.show(Default::default(), menu_corner, items, cx);
+ })
+ }
})
- // Make sure all events directly under the dock pane
- // are captured
- .capture_all()
- .contained()
- .with_style(style.maximized),
+ .with_tooltip::<Self>(
+ panel_ix,
+ tooltip,
+ tooltip_action,
+ tooltip_style.clone(),
+ cx,
+ ),
)
- .into_any()
- }
- })
+ .with_child(ChildView::new(&context_menu, cx))
+ },
+ ))
+ .contained()
+ .with_style(group_style)
+ .into_any()
}
+}
- pub fn position(&self) -> DockPosition {
- self.position
+impl StatusItemView for PanelButtons {
+ fn set_active_pane_item(
+ &mut self,
+ _: Option<&dyn crate::ItemHandle>,
+ _: &mut ViewContext<Self>,
+ ) {
}
}
#[cfg(test)]
-mod tests {
- use std::{
- ops::{Deref, DerefMut},
- path::PathBuf,
- sync::Arc,
- };
-
- use gpui::{AppContext, BorrowWindowContext, TestAppContext, ViewContext, WindowContext};
- use project::{FakeFs, Project};
-
+pub(crate) mod test {
use super::*;
- use crate::{
- dock,
- item::{self, test::TestItem},
- persistence::model::{
- SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace,
- },
- register_deserializable_item,
- sidebar::Sidebar,
- tests::init_test,
- AppState, ItemHandle, Workspace,
- };
-
- pub fn default_item_factory(
- _workspace: &mut Workspace,
- cx: &mut ViewContext<Workspace>,
- ) -> Option<Box<dyn ItemHandle>> {
- Some(Box::new(cx.add_view(|_| TestItem::new())))
- }
-
- #[gpui::test]
- async fn test_dock_workspace_infinite_loop(cx: &mut TestAppContext) {
- init_test(cx);
-
- cx.update(|cx| {
- register_deserializable_item::<item::test::TestItem>(cx);
- });
-
- let serialized_workspace = SerializedWorkspace {
- id: 0,
- location: Vec::<PathBuf>::new().into(),
- dock_position: dock::DockPosition::Shown(DockAnchor::Expanded),
- center_group: SerializedPaneGroup::Pane(SerializedPane {
- active: false,
- children: vec![],
- }),
- dock_pane: SerializedPane {
- active: true,
- children: vec![SerializedItem {
- active: true,
- item_id: 0,
- kind: "TestItem".into(),
- }],
- },
- left_sidebar_open: false,
- bounds: Default::default(),
- display: Default::default(),
- };
-
- let fs = FakeFs::new(cx.background());
- let project = Project::test(fs, [], cx).await;
-
- let (_, _workspace) = cx.add_window(|cx| {
- Workspace::new(
- 0,
- project.clone(),
- Arc::new(AppState {
- languages: project.read(cx).languages().clone(),
- client: project.read(cx).client(),
- user_store: project.read(cx).user_store(),
- fs: project.read(cx).fs().clone(),
- build_window_options: |_, _, _| Default::default(),
- initialize_workspace: |_, _, _| {},
- dock_default_item_factory: default_item_factory,
- background_actions: || &[],
- }),
- cx,
- )
- });
+ use gpui::{ViewContext, WindowContext};
+
+ pub enum TestPanelEvent {
+ PositionChanged,
+ Activated,
+ Closed,
+ ZoomIn,
+ ZoomOut,
+ Focus,
+ }
- cx.update(|cx| {
- Workspace::load_workspace(_workspace.downgrade(), serialized_workspace, Vec::new(), cx)
- })
- .await;
-
- cx.foreground().run_until_parked();
- //Should terminate
- }
-
- #[gpui::test]
- async fn test_dock_hides_when_pane_empty(cx: &mut TestAppContext) {
- let mut cx = DockTestContext::new(cx).await;
-
- // Closing the last item in the dock hides the dock
- cx.move_dock(DockAnchor::Right);
- let old_items = cx.dock_items();
- assert!(!old_items.is_empty());
- cx.close_dock_items().await;
- cx.assert_dock_position(DockPosition::Hidden(DockAnchor::Right));
-
- // Reopening the dock adds a new item
- cx.move_dock(DockAnchor::Right);
- let new_items = cx.dock_items();
- assert!(!new_items.is_empty());
- assert!(new_items
- .into_iter()
- .all(|new_item| !old_items.contains(&new_item)));
- }
-
- #[gpui::test]
- async fn test_dock_panel_collisions(cx: &mut TestAppContext) {
- let mut cx = DockTestContext::new(cx).await;
-
- // Dock closes when expanded for either panel
- cx.move_dock(DockAnchor::Expanded);
- cx.open_sidebar(SidebarSide::Left);
- cx.assert_dock_position(DockPosition::Hidden(DockAnchor::Expanded));
- cx.close_sidebar(SidebarSide::Left);
- cx.move_dock(DockAnchor::Expanded);
- cx.open_sidebar(SidebarSide::Right);
- cx.assert_dock_position(DockPosition::Hidden(DockAnchor::Expanded));
-
- // Dock closes in the right position if the right sidebar is opened
- cx.move_dock(DockAnchor::Right);
- cx.open_sidebar(SidebarSide::Left);
- cx.assert_dock_position(DockPosition::Shown(DockAnchor::Right));
- cx.open_sidebar(SidebarSide::Right);
- cx.assert_dock_position(DockPosition::Hidden(DockAnchor::Right));
- cx.close_sidebar(SidebarSide::Right);
-
- // Dock in bottom position ignores sidebars
- cx.move_dock(DockAnchor::Bottom);
- cx.open_sidebar(SidebarSide::Left);
- cx.open_sidebar(SidebarSide::Right);
- cx.assert_dock_position(DockPosition::Shown(DockAnchor::Bottom));
-
- // Opening the dock in the right position closes the right sidebar
- cx.move_dock(DockAnchor::Right);
- cx.assert_sidebar_closed(SidebarSide::Right);
- }
-
- #[gpui::test]
- async fn test_focusing_panes_shows_and_hides_dock(cx: &mut TestAppContext) {
- let mut cx = DockTestContext::new(cx).await;
-
- // Focusing an item not in the dock when expanded hides the dock
- let center_item = cx.add_item_to_center_pane();
- cx.move_dock(DockAnchor::Expanded);
- let dock_item = cx
- .dock_items()
- .get(0)
- .cloned()
- .expect("Dock should have an item at this point");
- center_item.update(&mut cx, |_, cx| cx.focus_self());
- cx.assert_dock_position(DockPosition::Hidden(DockAnchor::Expanded));
-
- // Focusing an item not in the dock when not expanded, leaves the dock open but inactive
- cx.move_dock(DockAnchor::Right);
- center_item.update(&mut cx, |_, cx| cx.focus_self());
- cx.assert_dock_position(DockPosition::Shown(DockAnchor::Right));
- cx.assert_dock_pane_inactive();
- cx.assert_workspace_pane_active();
-
- // Focusing an item in the dock activates it's pane
- dock_item.update(&mut cx, |_, cx| cx.focus_self());
- cx.assert_dock_position(DockPosition::Shown(DockAnchor::Right));
- cx.assert_dock_pane_active();
- cx.assert_workspace_pane_inactive();
- }
-
- #[gpui::test]
- async fn test_toggle_dock_focus(cx: &mut TestAppContext) {
- let mut cx = DockTestContext::new(cx).await;
-
- cx.move_dock(DockAnchor::Right);
- cx.assert_dock_pane_active();
- cx.hide_dock();
- cx.move_dock(DockAnchor::Right);
- cx.assert_dock_pane_active();
- }
-
- #[gpui::test]
- async fn test_activate_next_and_prev_pane(cx: &mut TestAppContext) {
- let mut cx = DockTestContext::new(cx).await;
-
- cx.move_dock(DockAnchor::Right);
- cx.assert_dock_pane_active();
-
- cx.update_workspace(|workspace, cx| workspace.activate_next_pane(cx));
- cx.assert_dock_pane_active();
-
- cx.update_workspace(|workspace, cx| workspace.activate_previous_pane(cx));
- cx.assert_dock_pane_active();
- }
-
- struct DockTestContext<'a> {
- pub cx: &'a mut TestAppContext,
- pub window_id: usize,
- pub workspace: ViewHandle<Workspace>,
- }
-
- impl<'a> DockTestContext<'a> {
- pub async fn new(cx: &'a mut TestAppContext) -> DockTestContext<'a> {
- init_test(cx);
- let fs = FakeFs::new(cx.background());
-
- cx.update(|cx| init(cx));
- let project = Project::test(fs, [], cx).await;
- let (window_id, workspace) = cx.add_window(|cx| {
- Workspace::new(
- 0,
- project.clone(),
- Arc::new(AppState {
- languages: project.read(cx).languages().clone(),
- client: project.read(cx).client(),
- user_store: project.read(cx).user_store(),
- fs: project.read(cx).fs().clone(),
- build_window_options: |_, _, _| Default::default(),
- initialize_workspace: |_, _, _| {},
- dock_default_item_factory: default_item_factory,
- background_actions: || &[],
- }),
- cx,
- )
- });
-
- workspace.update(cx, |workspace, cx| {
- let left_panel = cx.add_view(|_| TestItem::new());
- workspace.left_sidebar().update(cx, |sidebar, cx| {
- sidebar.add_item(
- "icons/folder_tree_16.svg",
- "Left Test Panel".to_string(),
- left_panel.clone(),
- cx,
- );
- });
-
- let right_panel = cx.add_view(|_| TestItem::new());
- workspace.right_sidebar().update(cx, |sidebar, cx| {
- sidebar.add_item(
- "icons/folder_tree_16.svg",
- "Right Test Panel".to_string(),
- right_panel.clone(),
- cx,
- );
- });
- });
+ pub struct TestPanel {
+ pub position: DockPosition,
+ pub zoomed: bool,
+ pub active: bool,
+ pub has_focus: bool,
+ pub size: f32,
+ }
+ impl TestPanel {
+ pub fn new(position: DockPosition) -> Self {
Self {
- cx,
- window_id,
- workspace,
+ position,
+ zoomed: false,
+ active: false,
+ has_focus: false,
+ size: 300.,
}
}
+ }
- pub fn workspace<F, T>(&self, read: F) -> T
- where
- F: FnOnce(&Workspace, &ViewContext<Workspace>) -> T,
- {
- self.workspace.read_with(self.cx, read)
+ impl Entity for TestPanel {
+ type Event = TestPanelEvent;
+ }
+
+ impl View for TestPanel {
+ fn ui_name() -> &'static str {
+ "TestPanel"
}
- pub fn update_workspace<F, T>(&mut self, update: F) -> T
- where
- F: FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
- {
- self.workspace.update(self.cx, update)
+ fn render(&mut self, _: &mut ViewContext<'_, '_, Self>) -> AnyElement<Self> {
+ Empty::new().into_any()
}
- pub fn sidebar<F, T>(&self, sidebar_side: SidebarSide, read: F) -> T
- where
- F: FnOnce(&Sidebar, &AppContext) -> T,
- {
- self.workspace(|workspace, cx| {
- let sidebar = match sidebar_side {
- SidebarSide::Left => workspace.left_sidebar(),
- SidebarSide::Right => workspace.right_sidebar(),
- }
- .read(cx);
-
- read(sidebar, cx)
- })
- }
-
- pub fn center_pane_handle(&self) -> ViewHandle<Pane> {
- self.workspace(|workspace, cx| {
- workspace
- .last_active_center_pane
- .clone()
- .and_then(|pane| pane.upgrade(cx))
- .unwrap_or_else(|| workspace.center.panes()[0].clone())
- })
- }
-
- pub fn add_item_to_center_pane(&mut self) -> ViewHandle<TestItem> {
- self.update_workspace(|workspace, cx| {
- let item = cx.add_view(|_| TestItem::new());
- let pane = workspace
- .last_active_center_pane
- .clone()
- .and_then(|pane| pane.upgrade(cx))
- .unwrap_or_else(|| workspace.center.panes()[0].clone());
- Pane::add_item(
- workspace,
- &pane,
- Box::new(item.clone()),
- true,
- true,
- None,
- cx,
- );
- item
- })
- }
-
- pub fn dock_pane<F, T>(&self, read: F) -> T
- where
- F: FnOnce(&Pane, &AppContext) -> T,
- {
- self.workspace(|workspace, cx| {
- let dock_pane = workspace.dock_pane().read(cx);
- read(dock_pane, cx)
- })
+ fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
+ self.has_focus = true;
+ cx.emit(TestPanelEvent::Focus);
}
- pub fn move_dock(&mut self, anchor: DockAnchor) {
- self.update_workspace(|workspace, cx| Dock::move_dock(workspace, anchor, true, cx));
+ fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
+ self.has_focus = false;
}
+ }
- pub fn hide_dock(&mut self) {
- self.cx.dispatch_action(self.window_id, HideDock);
+ impl Panel for TestPanel {
+ fn position(&self, _: &gpui::WindowContext) -> super::DockPosition {
+ self.position
}
- pub fn open_sidebar(&mut self, sidebar_side: SidebarSide) {
- if !self.sidebar(sidebar_side, |sidebar, _| sidebar.is_open()) {
- self.update_workspace(|workspace, cx| workspace.toggle_sidebar(sidebar_side, cx));
- }
+ fn position_is_valid(&self, _: super::DockPosition) -> bool {
+ true
}
- pub fn close_sidebar(&mut self, sidebar_side: SidebarSide) {
- if self.sidebar(sidebar_side, |sidebar, _| sidebar.is_open()) {
- self.update_workspace(|workspace, cx| workspace.toggle_sidebar(sidebar_side, cx));
- }
+ fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
+ self.position = position;
+ cx.emit(TestPanelEvent::PositionChanged);
}
- pub fn dock_items(&self) -> Vec<ViewHandle<TestItem>> {
- self.dock_pane(|pane, cx| {
- pane.items()
- .map(|item| {
- item.act_as::<TestItem>(cx)
- .expect("Dock Test Context uses TestItems in the dock")
- })
- .collect()
- })
+ fn is_zoomed(&self, _: &WindowContext) -> bool {
+ self.zoomed
}
- pub async fn close_dock_items(&mut self) {
- self.update_workspace(|workspace, cx| {
- Pane::close_items(workspace, workspace.dock_pane().clone(), cx, |_| true)
- })
- .await
- .expect("Could not close dock items")
+ fn set_zoomed(&mut self, zoomed: bool, _cx: &mut ViewContext<Self>) {
+ self.zoomed = zoomed;
}
- pub fn assert_dock_position(&self, expected_position: DockPosition) {
- self.workspace(|workspace, _| assert_eq!(workspace.dock.position, expected_position));
+ fn set_active(&mut self, active: bool, _cx: &mut ViewContext<Self>) {
+ self.active = active;
}
- pub fn assert_sidebar_closed(&self, sidebar_side: SidebarSide) {
- assert!(!self.sidebar(sidebar_side, |sidebar, _| sidebar.is_open()));
+ fn size(&self, _: &WindowContext) -> f32 {
+ self.size
}
- pub fn assert_workspace_pane_active(&self) {
- assert!(self
- .center_pane_handle()
- .read_with(self.cx, |pane, _| pane.is_active()));
+ fn set_size(&mut self, size: f32, _: &mut ViewContext<Self>) {
+ self.size = size;
}
- pub fn assert_workspace_pane_inactive(&self) {
- assert!(!self
- .center_pane_handle()
- .read_with(self.cx, |pane, _| pane.is_active()));
+ fn icon_path(&self) -> &'static str {
+ "icons/test_panel.svg"
}
- pub fn assert_dock_pane_active(&self) {
- assert!(self.dock_pane(|pane, _| pane.is_active()))
+ fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>) {
+ ("Test Panel".into(), None)
}
- pub fn assert_dock_pane_inactive(&self) {
- assert!(!self.dock_pane(|pane, _| pane.is_active()))
+ fn should_change_position_on_event(event: &Self::Event) -> bool {
+ matches!(event, TestPanelEvent::PositionChanged)
}
- }
- impl<'a> Deref for DockTestContext<'a> {
- type Target = gpui::TestAppContext;
+ fn should_zoom_in_on_event(event: &Self::Event) -> bool {
+ matches!(event, TestPanelEvent::ZoomIn)
+ }
- fn deref(&self) -> &Self::Target {
- self.cx
+ fn should_zoom_out_on_event(event: &Self::Event) -> bool {
+ matches!(event, TestPanelEvent::ZoomOut)
}
- }
- impl<'a> DerefMut for DockTestContext<'a> {
- fn deref_mut(&mut self) -> &mut Self::Target {
- &mut self.cx
+ fn should_activate_on_event(event: &Self::Event) -> bool {
+ matches!(event, TestPanelEvent::Activated)
+ }
+
+ fn should_close_on_event(event: &Self::Event) -> bool {
+ matches!(event, TestPanelEvent::Closed)
}
- }
- impl BorrowWindowContext for DockTestContext<'_> {
- fn read_with<T, F: FnOnce(&WindowContext) -> T>(&self, window_id: usize, f: F) -> T {
- BorrowWindowContext::read_with(self.cx, window_id, f)
+ fn has_focus(&self, _cx: &WindowContext) -> bool {
+ self.has_focus
}
- fn update<T, F: FnOnce(&mut WindowContext) -> T>(&mut self, window_id: usize, f: F) -> T {
- BorrowWindowContext::update(self.cx, window_id, f)
+ fn is_focus_event(event: &Self::Event) -> bool {
+ matches!(event, TestPanelEvent::Focus)
}
}
}
@@ -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 {}
}
@@ -2,17 +2,14 @@ mod dragged_item_receiver;
use super::{ItemHandle, SplitDirection};
use crate::{
- dock::{icon_for_dock_anchor, AnchorDockBottom, AnchorDockRight, Dock, ExpandDock},
- item::WeakItemHandle,
- toolbar::Toolbar,
- AutosaveSetting, DockAnchor, Item, NewFile, NewSearch, NewTerminal, Workspace,
- WorkspaceSettings,
+ item::WeakItemHandle, toolbar::Toolbar, AutosaveSetting, Item, NewFile, NewSearch, NewTerminal,
+ ToggleZoom, Workspace, WorkspaceSettings,
};
use anyhow::{anyhow, Result};
use collections::{HashMap, HashSet, VecDeque};
use context_menu::{ContextMenu, ContextMenuItem};
-use drag_and_drop::Draggable;
-pub use dragged_item_receiver::{dragged_item_receiver, handle_dropped_item};
+use drag_and_drop::{DragAndDrop, Draggable};
+use dragged_item_receiver::dragged_item_receiver;
use futures::StreamExt;
use gpui::{
actions,
@@ -41,7 +38,7 @@ use std::{
Arc,
},
};
-use theme::Theme;
+use theme::{Theme, ThemeSettings};
use util::ResultExt;
#[derive(Clone, Deserialize, PartialEq)]
@@ -104,6 +101,7 @@ const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
pub type BackgroundActions = fn() -> &'static [(&'static str, &'static dyn Action)];
pub fn init(cx: &mut AppContext) {
+ cx.add_action(Pane::toggle_zoom);
cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| {
pane.activate_item(action.0, true, true, cx);
});
@@ -145,12 +143,15 @@ pub enum Event {
Split(SplitDirection),
ChangeItemTitle,
Focus,
+ ZoomIn,
+ ZoomOut,
}
pub struct Pane {
items: Vec<Box<dyn ItemHandle>>,
activation_history: Vec<usize>,
is_active: bool,
+ zoomed: bool,
active_item_index: usize,
last_focused_view_by_item: HashMap<usize, AnyWeakViewHandle>,
autoscroll: bool,
@@ -158,10 +159,12 @@ pub struct Pane {
toolbar: ViewHandle<Toolbar>,
tab_bar_context_menu: TabBarContextMenu,
tab_context_menu: ViewHandle<ContextMenu>,
- docked: Option<DockAnchor>,
_background_actions: BackgroundActions,
workspace: WeakViewHandle<Workspace>,
has_focus: bool,
+ can_drop: Rc<dyn Fn(&DragAndDrop<Workspace>, &WindowContext) -> bool>,
+ can_split: bool,
+ render_tab_bar_buttons: Rc<dyn Fn(&mut Pane, &mut ViewContext<Pane>) -> AnyElement<Pane>>,
}
pub struct ItemNavHistory {
@@ -203,9 +206,9 @@ pub struct NavigationEntry {
pub timestamp: usize,
}
-struct DraggedItem {
- item: Box<dyn ItemHandle>,
- pane: WeakViewHandle<Pane>,
+pub struct DraggedItem {
+ pub handle: Box<dyn ItemHandle>,
+ pub pane: WeakViewHandle<Pane>,
}
pub enum ReorderBehavior {
@@ -218,7 +221,6 @@ pub enum ReorderBehavior {
enum TabBarContextMenuKind {
New,
Split,
- Dock,
}
struct TabBarContextMenu {
@@ -238,7 +240,6 @@ impl TabBarContextMenu {
impl Pane {
pub fn new(
workspace: WeakViewHandle<Workspace>,
- docked: Option<DockAnchor>,
background_actions: BackgroundActions,
next_timestamp: Arc<AtomicUsize>,
cx: &mut ViewContext<Self>,
@@ -254,6 +255,7 @@ impl Pane {
items: Vec::new(),
activation_history: Vec::new(),
is_active: true,
+ zoomed: false,
active_item_index: 0,
last_focused_view_by_item: Default::default(),
autoscroll: false,
@@ -272,10 +274,46 @@ impl Pane {
handle: context_menu,
},
tab_context_menu: cx.add_view(|cx| ContextMenu::new(pane_view_id, cx)),
- docked,
_background_actions: background_actions,
workspace,
has_focus: false,
+ can_drop: Rc::new(|_, _| true),
+ can_split: true,
+ render_tab_bar_buttons: Rc::new(|pane, cx| {
+ Flex::row()
+ // New menu
+ .with_child(Self::render_tab_bar_button(
+ 0,
+ "icons/plus_12.svg",
+ Some(("New...".into(), None)),
+ cx,
+ |pane, cx| pane.deploy_new_menu(cx),
+ pane.tab_bar_context_menu
+ .handle_if_kind(TabBarContextMenuKind::New),
+ ))
+ .with_child(Self::render_tab_bar_button(
+ 1,
+ "icons/split_12.svg",
+ Some(("Split Pane".into(), None)),
+ cx,
+ |pane, cx| pane.deploy_split_menu(cx),
+ pane.tab_bar_context_menu
+ .handle_if_kind(TabBarContextMenuKind::Split),
+ ))
+ .with_child(Pane::render_tab_bar_button(
+ 2,
+ if pane.is_zoomed() {
+ "icons/minimize_8.svg"
+ } else {
+ "icons/maximize_8.svg"
+ },
+ Some(("Toggle Zoom".into(), Some(Box::new(ToggleZoom)))),
+ cx,
+ move |pane, cx| pane.toggle_zoom(&Default::default(), cx),
+ None,
+ ))
+ .into_any()
+ }),
}
}
@@ -296,8 +334,23 @@ impl Pane {
self.has_focus
}
- pub fn set_docked(&mut self, docked: Option<DockAnchor>, cx: &mut ViewContext<Self>) {
- self.docked = docked;
+ pub fn on_can_drop<F>(&mut self, can_drop: F)
+ where
+ F: 'static + Fn(&DragAndDrop<Workspace>, &WindowContext) -> bool,
+ {
+ self.can_drop = Rc::new(can_drop);
+ }
+
+ pub fn set_can_split(&mut self, can_split: bool, cx: &mut ViewContext<Self>) {
+ self.can_split = can_split;
+ cx.notify();
+ }
+
+ pub fn set_render_tab_bar_buttons<F>(&mut self, cx: &mut ViewContext<Self>, render: F)
+ where
+ F: 'static + Fn(&mut Pane, &mut ViewContext<Pane>) -> AnyElement<Pane>,
+ {
+ self.render_tab_bar_buttons = Rc::new(render);
cx.notify();
}
@@ -515,7 +568,7 @@ impl Pane {
}
}
- pub(crate) fn add_item(
+ pub fn add_item(
workspace: &mut Workspace,
pane: &ViewHandle<Pane>,
item: Box<dyn ItemHandle>,
@@ -641,6 +694,17 @@ impl Pane {
self.items.iter().position(|i| i.id() == item.id())
}
+ pub fn toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext<Self>) {
+ if self.zoomed {
+ cx.emit(Event::ZoomOut);
+ } else if !self.items.is_empty() {
+ if !self.has_focus {
+ cx.focus_self();
+ }
+ cx.emit(Event::ZoomIn);
+ }
+ }
+
pub fn activate_item(
&mut self,
index: usize,
@@ -704,187 +768,118 @@ impl Pane {
}
pub fn close_active_item(
- workspace: &mut Workspace,
+ &mut self,
_: &CloseActiveItem,
- cx: &mut ViewContext<Workspace>,
+ cx: &mut ViewContext<Self>,
) -> Option<Task<Result<()>>> {
- let pane_handle = workspace.active_pane().clone();
- let pane = pane_handle.read(cx);
-
- if pane.items.is_empty() {
+ if self.items.is_empty() {
return None;
}
- let active_item_id = pane.items[pane.active_item_index].id();
-
- let task = Self::close_item_by_id(workspace, pane_handle, active_item_id, cx);
-
- Some(cx.foreground().spawn(async move {
- task.await?;
- Ok(())
- }))
+ let active_item_id = self.items[self.active_item_index].id();
+ Some(self.close_item_by_id(active_item_id, cx))
}
pub fn close_item_by_id(
- workspace: &mut Workspace,
- pane: ViewHandle<Pane>,
+ &mut self,
item_id_to_close: usize,
- cx: &mut ViewContext<Workspace>,
+ cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
- Self::close_items(workspace, pane, cx, move |view_id| {
- view_id == item_id_to_close
- })
+ self.close_items(cx, move |view_id| view_id == item_id_to_close)
}
pub fn close_inactive_items(
- workspace: &mut Workspace,
+ &mut self,
_: &CloseInactiveItems,
- cx: &mut ViewContext<Workspace>,
+ cx: &mut ViewContext<Self>,
) -> Option<Task<Result<()>>> {
- let pane_handle = workspace.active_pane().clone();
- let pane = pane_handle.read(cx);
- let active_item_id = pane.items[pane.active_item_index].id();
-
- let task = Self::close_items(workspace, pane_handle, cx, move |item_id| {
- item_id != active_item_id
- });
+ if self.items.is_empty() {
+ return None;
+ }
- Some(cx.foreground().spawn(async move {
- task.await?;
- Ok(())
- }))
+ let active_item_id = self.items[self.active_item_index].id();
+ Some(self.close_items(cx, move |item_id| item_id != active_item_id))
}
pub fn close_clean_items(
- workspace: &mut Workspace,
+ &mut self,
_: &CloseCleanItems,
- cx: &mut ViewContext<Workspace>,
+ cx: &mut ViewContext<Self>,
) -> Option<Task<Result<()>>> {
- let pane_handle = workspace.active_pane().clone();
- let pane = pane_handle.read(cx);
-
- let item_ids: Vec<_> = pane
+ let item_ids: Vec<_> = self
.items()
.filter(|item| !item.is_dirty(cx))
.map(|item| item.id())
.collect();
-
- let task = Self::close_items(workspace, pane_handle, cx, move |item_id| {
- item_ids.contains(&item_id)
- });
-
- Some(cx.foreground().spawn(async move {
- task.await?;
- Ok(())
- }))
+ Some(self.close_items(cx, move |item_id| item_ids.contains(&item_id)))
}
pub fn close_items_to_the_left(
- workspace: &mut Workspace,
+ &mut self,
_: &CloseItemsToTheLeft,
- cx: &mut ViewContext<Workspace>,
+ cx: &mut ViewContext<Self>,
) -> Option<Task<Result<()>>> {
- let pane_handle = workspace.active_pane().clone();
- let pane = pane_handle.read(cx);
- let active_item_id = pane.items[pane.active_item_index].id();
-
- let task = Self::close_items_to_the_left_by_id(workspace, pane_handle, active_item_id, cx);
-
- Some(cx.foreground().spawn(async move {
- task.await?;
- Ok(())
- }))
+ if self.items.is_empty() {
+ return None;
+ }
+ let active_item_id = self.items[self.active_item_index].id();
+ Some(self.close_items_to_the_left_by_id(active_item_id, cx))
}
pub fn close_items_to_the_left_by_id(
- workspace: &mut Workspace,
- pane: ViewHandle<Pane>,
+ &mut self,
item_id: usize,
- cx: &mut ViewContext<Workspace>,
+ cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
- let item_ids: Vec<_> = pane
- .read(cx)
+ let item_ids: Vec<_> = self
.items()
.take_while(|item| item.id() != item_id)
.map(|item| item.id())
.collect();
-
- let task = Self::close_items(workspace, pane, cx, move |item_id| {
- item_ids.contains(&item_id)
- });
-
- cx.foreground().spawn(async move {
- task.await?;
- Ok(())
- })
+ self.close_items(cx, move |item_id| item_ids.contains(&item_id))
}
pub fn close_items_to_the_right(
- workspace: &mut Workspace,
+ &mut self,
_: &CloseItemsToTheRight,
- cx: &mut ViewContext<Workspace>,
+ cx: &mut ViewContext<Self>,
) -> Option<Task<Result<()>>> {
- let pane_handle = workspace.active_pane().clone();
- let pane = pane_handle.read(cx);
- let active_item_id = pane.items[pane.active_item_index].id();
-
- let task = Self::close_items_to_the_right_by_id(workspace, pane_handle, active_item_id, cx);
-
- Some(cx.foreground().spawn(async move {
- task.await?;
- Ok(())
- }))
+ if self.items.is_empty() {
+ return None;
+ }
+ let active_item_id = self.items[self.active_item_index].id();
+ Some(self.close_items_to_the_right_by_id(active_item_id, cx))
}
pub fn close_items_to_the_right_by_id(
- workspace: &mut Workspace,
- pane: ViewHandle<Pane>,
+ &mut self,
item_id: usize,
- cx: &mut ViewContext<Workspace>,
+ cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
- let item_ids: Vec<_> = pane
- .read(cx)
+ let item_ids: Vec<_> = self
.items()
.rev()
.take_while(|item| item.id() != item_id)
.map(|item| item.id())
.collect();
-
- let task = Self::close_items(workspace, pane, cx, move |item_id| {
- item_ids.contains(&item_id)
- });
-
- cx.foreground().spawn(async move {
- task.await?;
- Ok(())
- })
+ self.close_items(cx, move |item_id| item_ids.contains(&item_id))
}
pub fn close_all_items(
- workspace: &mut Workspace,
+ &mut self,
_: &CloseAllItems,
- cx: &mut ViewContext<Workspace>,
+ cx: &mut ViewContext<Self>,
) -> Option<Task<Result<()>>> {
- let pane_handle = workspace.active_pane().clone();
-
- let task = Self::close_items(workspace, pane_handle, cx, move |_| true);
-
- Some(cx.foreground().spawn(async move {
- task.await?;
- Ok(())
- }))
+ Some(self.close_items(cx, move |_| true))
}
pub fn close_items(
- workspace: &mut Workspace,
- pane: ViewHandle<Pane>,
- cx: &mut ViewContext<Workspace>,
+ &mut self,
+ cx: &mut ViewContext<Pane>,
should_close: impl 'static + Fn(usize) -> bool,
) -> Task<Result<()>> {
- let project = workspace.project().clone();
-
// Find the items to close.
let mut items_to_close = Vec::new();
- for item in &pane.read(cx).items {
+ for item in &self.items {
if should_close(item.id()) {
items_to_close.push(item.boxed_clone());
}
@@ -896,8 +891,8 @@ impl Pane {
// of what content they would be saving.
items_to_close.sort_by_key(|item| !item.is_singleton(cx));
- let pane = pane.downgrade();
- cx.spawn(|workspace, mut cx| async move {
+ let workspace = self.workspace.clone();
+ cx.spawn(|pane, mut cx| async move {
let mut saved_project_items_ids = HashSet::default();
for item in items_to_close.clone() {
// Find the item's current index and its set of project item models. Avoid
@@ -915,7 +910,7 @@ impl Pane {
// Check if this view has any project items that are not open anywhere else
// in the workspace, AND that the user has not already been prompted to save.
// If there are any such project entries, prompt the user to save this item.
- workspace.read_with(&cx, |workspace, cx| {
+ let project = workspace.read_with(&cx, |workspace, cx| {
for item in workspace.items(cx) {
if !items_to_close
.iter()
@@ -925,6 +920,7 @@ impl Pane {
project_item_ids.retain(|id| !other_project_item_ids.contains(id));
}
}
+ workspace.project().clone()
})?;
let should_save = project_item_ids
.iter()
@@ -967,7 +963,8 @@ impl Pane {
// to activating the item to the left
.unwrap_or_else(|| item_index.min(self.items.len()).saturating_sub(1));
- self.activate_item(index_to_activate, activate_pane, activate_pane, cx);
+ let should_activate = activate_pane || self.has_focus;
+ self.activate_item(index_to_activate, should_activate, should_activate, cx);
}
let item = self.items.remove(item_index);
@@ -1003,6 +1000,10 @@ impl Pane {
.remove(&item.id());
}
+ if self.items.is_empty() && self.zoomed {
+ cx.emit(Event::ZoomOut);
+ }
+
cx.notify();
}
@@ -1177,23 +1178,6 @@ impl Pane {
self.tab_bar_context_menu.kind = TabBarContextMenuKind::Split;
}
- fn deploy_dock_menu(&mut self, cx: &mut ViewContext<Self>) {
- self.tab_bar_context_menu.handle.update(cx, |menu, cx| {
- menu.show(
- Default::default(),
- AnchorCorner::TopRight,
- vec![
- ContextMenuItem::action("Anchor Dock Right", AnchorDockRight),
- ContextMenuItem::action("Anchor Dock Bottom", AnchorDockBottom),
- ContextMenuItem::action("Expand Dock", ExpandDock),
- ],
- cx,
- );
- });
-
- self.tab_bar_context_menu.kind = TabBarContextMenuKind::Dock;
- }
-
fn deploy_new_menu(&mut self, cx: &mut ViewContext<Self>) {
self.tab_bar_context_menu.handle.update(cx, |menu, cx| {
menu.show(
@@ -1240,14 +1224,11 @@ impl Pane {
// In the case of the user right clicking on a non-active tab, for some item-closing commands, we need to provide the id of the tab, for the others, we can reuse the existing command.
vec![
ContextMenuItem::handler("Close Inactive Item", {
- let workspace = self.workspace.clone();
let pane = target_pane.clone();
move |cx| {
- if let Some((workspace, pane)) =
- workspace.upgrade(cx).zip(pane.upgrade(cx))
- {
- workspace.update(cx, |workspace, cx| {
- Self::close_item_by_id(workspace, pane, target_item_id, cx)
+ if let Some(pane) = pane.upgrade(cx) {
+ pane.update(cx, |pane, cx| {
+ pane.close_item_by_id(target_item_id, cx)
.detach_and_log_err(cx);
})
}
@@ -1256,39 +1237,23 @@ impl Pane {
ContextMenuItem::action("Close Inactive Items", CloseInactiveItems),
ContextMenuItem::action("Close Clean Items", CloseCleanItems),
ContextMenuItem::handler("Close Items To The Left", {
- let workspace = self.workspace.clone();
let pane = target_pane.clone();
move |cx| {
- if let Some((workspace, pane)) =
- workspace.upgrade(cx).zip(pane.upgrade(cx))
- {
- workspace.update(cx, |workspace, cx| {
- Self::close_items_to_the_left_by_id(
- workspace,
- pane,
- target_item_id,
- cx,
- )
- .detach_and_log_err(cx);
+ if let Some(pane) = pane.upgrade(cx) {
+ pane.update(cx, |pane, cx| {
+ pane.close_items_to_the_left_by_id(target_item_id, cx)
+ .detach_and_log_err(cx);
})
}
}
}),
ContextMenuItem::handler("Close Items To The Right", {
- let workspace = self.workspace.clone();
let pane = target_pane.clone();
move |cx| {
- if let Some((workspace, pane)) =
- workspace.upgrade(cx).zip(pane.upgrade(cx))
- {
- workspace.update(cx, |workspace, cx| {
- Self::close_items_to_the_right_by_id(
- workspace,
- pane,
- target_item_id,
- cx,
- )
- .detach_and_log_err(cx);
+ if let Some(pane) = pane.upgrade(cx) {
+ pane.update(cx, |pane, cx| {
+ pane.close_items_to_the_right_by_id(target_item_id, cx)
+ .detach_and_log_err(cx);
})
}
}
@@ -1305,6 +1270,25 @@ impl Pane {
&self.toolbar
}
+ pub fn handle_deleted_project_item(
+ &mut self,
+ entry_id: ProjectEntryId,
+ cx: &mut ViewContext<Pane>,
+ ) -> Option<()> {
+ let (item_index_to_delete, item_id) = self.items().enumerate().find_map(|(i, item)| {
+ if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] {
+ Some((i, item.id()))
+ } else {
+ None
+ }
+ })?;
+
+ self.remove_item(item_index_to_delete, false, cx);
+ self.nav_history.borrow_mut().remove_item(item_id);
+
+ Some(())
+ }
+
fn update_toolbar(&mut self, cx: &mut ViewContext<Self>) {
let active_item = self
.items
@@ -1342,7 +1326,7 @@ impl Pane {
row.add_child({
enum TabDragReceiver {}
let mut receiver =
- dragged_item_receiver::<TabDragReceiver, _, _>(ix, ix, true, None, cx, {
+ dragged_item_receiver::<TabDragReceiver, _, _>(self, ix, ix, true, None, cx, {
let item = item.clone();
let pane = pane.clone();
let detail = detail.clone();
@@ -1376,20 +1360,7 @@ impl Pane {
.on_click(MouseButton::Middle, {
let item_id = item.id();
move |_, pane, cx| {
- let workspace = pane.workspace.clone();
- let pane = cx.weak_handle();
- cx.window_context().defer(move |cx| {
- if let Some((workspace, pane)) =
- workspace.upgrade(cx).zip(pane.upgrade(cx))
- {
- workspace.update(cx, |workspace, cx| {
- Self::close_item_by_id(
- workspace, pane, item_id, cx,
- )
- .detach_and_log_err(cx);
- });
- }
- });
+ pane.close_item_by_id(item_id, cx).detach_and_log_err(cx);
}
})
.on_down(
@@ -1421,7 +1392,7 @@ impl Pane {
receiver.as_draggable(
DraggedItem {
- item,
+ handle: item,
pane: pane.clone(),
},
{
@@ -1431,7 +1402,7 @@ impl Pane {
move |dragged_item: &DraggedItem, cx: &mut ViewContext<Workspace>| {
let tab_style = &theme.workspace.tab_bar.dragged_tab;
Self::render_dragged_tab(
- &dragged_item.item,
+ &dragged_item.handle,
dragged_item.pane.clone(),
false,
detail,
@@ -1451,7 +1422,7 @@ impl Pane {
let filler_style = theme.workspace.tab_bar.tab_style(pane_active, false);
enum Filler {}
row.add_child(
- dragged_item_receiver::<Filler, _, _>(0, filler_index, true, None, cx, |_, _| {
+ dragged_item_receiver::<Filler, _, _>(self, 0, filler_index, true, None, cx, |_, _| {
Empty::new()
.contained()
.with_style(filler_style.container)
@@ -1596,12 +1567,9 @@ impl Pane {
let pane = pane.clone();
cx.window_context().defer(move |cx| {
if let Some(pane) = pane.upgrade(cx) {
- if let Some(workspace) = pane.read(cx).workspace.upgrade(cx) {
- workspace.update(cx, |workspace, cx| {
- Self::close_item_by_id(workspace, pane, item_id, cx)
- .detach_and_log_err(cx);
- });
- }
+ pane.update(cx, |pane, cx| {
+ pane.close_item_by_id(item_id, cx).detach_and_log_err(cx);
+ });
}
});
}
@@ -1621,83 +1589,63 @@ impl Pane {
.into_any()
}
- fn render_tab_bar_buttons(
- &mut self,
- theme: &Theme,
- cx: &mut ViewContext<Self>,
- ) -> AnyElement<Self> {
- Flex::row()
- // New menu
- .with_child(render_tab_bar_button(
- 0,
- "icons/plus_12.svg",
- cx,
- |pane, cx| pane.deploy_new_menu(cx),
- self.tab_bar_context_menu
- .handle_if_kind(TabBarContextMenuKind::New),
- ))
- .with_child(
- self.docked
- .map(|anchor| {
- // Add the dock menu button if this pane is a dock
- let dock_icon = icon_for_dock_anchor(anchor);
-
- render_tab_bar_button(
- 1,
- dock_icon,
- cx,
- |pane, cx| pane.deploy_dock_menu(cx),
- self.tab_bar_context_menu
- .handle_if_kind(TabBarContextMenuKind::Dock),
- )
- })
- .unwrap_or_else(|| {
- // Add the split menu if this pane is not a dock
- render_tab_bar_button(
- 2,
- "icons/split_12.svg",
- cx,
- |pane, cx| pane.deploy_split_menu(cx),
- self.tab_bar_context_menu
- .handle_if_kind(TabBarContextMenuKind::Split),
- )
- }),
+ pub fn render_tab_bar_button<F: 'static + Fn(&mut Pane, &mut EventContext<Pane>)>(
+ index: usize,
+ icon: &'static str,
+ tooltip: Option<(String, Option<Box<dyn Action>>)>,
+ cx: &mut ViewContext<Pane>,
+ on_click: F,
+ context_menu: Option<ViewHandle<ContextMenu>>,
+ ) -> AnyElement<Pane> {
+ enum TabBarButton {}
+
+ let mut button = MouseEventHandler::<TabBarButton, _>::new(index, cx, |mouse_state, cx| {
+ let theme = &settings::get::<ThemeSettings>(cx).theme.workspace.tab_bar;
+ let style = theme.pane_button.style_for(mouse_state, false);
+ Svg::new(icon)
+ .with_color(style.color)
+ .constrained()
+ .with_width(style.icon_width)
+ .aligned()
+ .constrained()
+ .with_width(style.button_width)
+ .with_height(style.button_width)
+ })
+ .with_cursor_style(CursorStyle::PointingHand)
+ .on_click(MouseButton::Left, move |_, pane, cx| on_click(pane, cx))
+ .into_any();
+ if let Some((tooltip, action)) = tooltip {
+ let tooltip_style = settings::get::<ThemeSettings>(cx).theme.tooltip.clone();
+ button = button
+ .with_tooltip::<TabBarButton>(index, tooltip, action, tooltip_style, cx)
+ .into_any();
+ }
+
+ Stack::new()
+ .with_child(button)
+ .with_children(
+ context_menu.map(|menu| ChildView::new(&menu, cx).aligned().bottom().right()),
)
- // Add the close dock button if this pane is a dock
- .with_children(self.docked.map(|_| {
- render_tab_bar_button(
- 3,
- "icons/x_mark_8.svg",
- cx,
- |this, cx| {
- if let Some(workspace) = this.workspace.upgrade(cx) {
- cx.window_context().defer(move |cx| {
- workspace.update(cx, |workspace, cx| {
- Dock::hide_dock(workspace, &Default::default(), cx)
- })
- });
- }
- },
- None,
- )
- }))
- .contained()
- .with_style(theme.workspace.tab_bar.pane_button_container)
.flex(1., false)
- .into_any()
+ .into_any_named("tab bar button")
}
- fn render_blank_pane(
- &mut self,
- theme: &Theme,
- _cx: &mut ViewContext<Self>,
- ) -> AnyElement<Self> {
+ fn render_blank_pane(&self, theme: &Theme, _cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let background = theme.workspace.background;
Empty::new()
.contained()
.with_background_color(background)
.into_any()
}
+
+ pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
+ self.zoomed = zoomed;
+ cx.notify();
+ }
+
+ pub fn is_zoomed(&self) -> bool {
+ self.zoomed
+ }
}
impl Entity for Pane {
@@ -1741,7 +1689,14 @@ impl View for Pane {
.with_child(self.render_tabs(cx).flex(1., true).into_any_named("tabs"));
if self.is_active {
- tab_row.add_child(self.render_tab_bar_buttons(&theme, cx))
+ let render_tab_bar_buttons = self.render_tab_bar_buttons.clone();
+ tab_row.add_child(
+ (render_tab_bar_buttons)(self, cx)
+ .contained()
+ .with_style(theme.workspace.tab_bar.pane_button_container)
+ .flex(1., false)
+ .into_any(),
+ )
}
stack.add_child(tab_row);
@@ -1754,14 +1709,11 @@ impl View for Pane {
.with_child({
enum PaneContentTabDropTarget {}
dragged_item_receiver::<PaneContentTabDropTarget, _, _>(
+ self,
0,
self.active_item_index + 1,
- false,
- if self.docked.is_some() {
- None
- } else {
- Some(100.)
- },
+ !self.can_split,
+ if self.can_split { Some(100.) } else { None },
cx,
{
let toolbar = self.toolbar.clone();
@@ -1786,7 +1738,7 @@ impl View for Pane {
enum EmptyPane {}
let theme = theme::current(cx).clone();
- dragged_item_receiver::<EmptyPane, _, _>(0, 0, false, None, cx, |_, cx| {
+ dragged_item_receiver::<EmptyPane, _, _>(self, 0, 0, false, None, cx, |_, cx| {
self.render_blank_pane(&theme, cx)
})
.on_down(MouseButton::Left, |_, _, cx| {
@@ -1824,7 +1776,11 @@ impl View for Pane {
}
fn focus_in(&mut self, focused: AnyViewHandle, cx: &mut ViewContext<Self>) {
- self.has_focus = true;
+ if !self.has_focus {
+ self.has_focus = true;
+ cx.emit(Event::Focus);
+ }
+
self.toolbar.update(cx, |toolbar, cx| {
toolbar.pane_focus_update(true, cx);
});
@@ -1850,8 +1806,6 @@ impl View for Pane {
.insert(active_item.id(), focused.downgrade());
}
}
-
- cx.emit(Event::Focus);
}
fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
@@ -1863,45 +1817,9 @@ impl View for Pane {
fn update_keymap_context(&self, keymap: &mut KeymapContext, _: &AppContext) {
Self::reset_to_default_keymap_context(keymap);
- if self.docked.is_some() {
- keymap.add_identifier("docked");
- }
}
}
-fn render_tab_bar_button<F: 'static + Fn(&mut Pane, &mut EventContext<Pane>)>(
- index: usize,
- icon: &'static str,
- cx: &mut ViewContext<Pane>,
- on_click: F,
- context_menu: Option<ViewHandle<ContextMenu>>,
-) -> AnyElement<Pane> {
- enum TabBarButton {}
-
- Stack::new()
- .with_child(
- MouseEventHandler::<TabBarButton, _>::new(index, cx, |mouse_state, cx| {
- let theme = &theme::current(cx).workspace.tab_bar;
- let style = theme.pane_button.style_for(mouse_state, false);
- Svg::new(icon)
- .with_color(style.color)
- .constrained()
- .with_width(style.icon_width)
- .aligned()
- .constrained()
- .with_width(style.button_width)
- .with_height(style.button_width)
- })
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, move |_, pane, cx| on_click(pane, cx)),
- )
- .with_children(
- context_menu.map(|menu| ChildView::new(&menu, cx).aligned().bottom().right()),
- )
- .flex(1., false)
- .into_any_named("tab bar button")
-}
-
impl ItemNavHistory {
pub fn push<D: 'static + Any>(&self, data: Option<D>, cx: &mut WindowContext) {
self.history.borrow_mut().push(data, self.item.clone(), cx);
@@ -2007,6 +1925,15 @@ impl NavHistory {
});
}
}
+
+ fn remove_item(&mut self, item_id: usize) {
+ self.paths_by_item.remove(&item_id);
+ self.backward_stack
+ .retain(|entry| entry.item.id() != item_id);
+ self.forward_stack
+ .retain(|entry| entry.item.id() != item_id);
+ self.closed_stack.retain(|entry| entry.item.id() != item_id);
+ }
}
impl PaneNavHistory {
@@ -2130,11 +2057,9 @@ impl<V: View> Element<V> for PaneBackdrop<V> {
#[cfg(test)]
mod tests {
- use std::sync::Arc;
-
use super::*;
use crate::item::test::{TestItem, TestProjectItem};
- use gpui::{executor::Deterministic, TestAppContext};
+ use gpui::TestAppContext;
use project::FakeFs;
use settings::SettingsStore;
@@ -2145,9 +2070,10 @@ mod tests {
let project = Project::test(fs, None, 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());
- workspace.update(cx, |workspace, cx| {
- assert!(Pane::close_active_item(workspace, &CloseActiveItem, cx).is_none())
+ pane.update(cx, |pane, cx| {
+ assert!(pane.close_active_item(&CloseActiveItem, cx).is_none())
});
}
@@ -2426,7 +2352,7 @@ mod tests {
}
#[gpui::test]
- async fn test_remove_item_ordering(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
+ async fn test_remove_item_ordering(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.background());
@@ -2444,36 +2370,36 @@ mod tests {
add_labeled_item(&workspace, &pane, "1", false, cx);
assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
- workspace.update(cx, |workspace, cx| {
- Pane::close_active_item(workspace, &CloseActiveItem, cx);
- });
- deterministic.run_until_parked();
+ pane.update(cx, |pane, cx| pane.close_active_item(&CloseActiveItem, cx))
+ .unwrap()
+ .await
+ .unwrap();
assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
- workspace.update(cx, |workspace, cx| {
- Pane::close_active_item(workspace, &CloseActiveItem, cx);
- });
- deterministic.run_until_parked();
+ pane.update(cx, |pane, cx| pane.close_active_item(&CloseActiveItem, cx))
+ .unwrap()
+ .await
+ .unwrap();
assert_item_labels(&pane, ["A", "B*", "C"], cx);
- workspace.update(cx, |workspace, cx| {
- Pane::close_active_item(workspace, &CloseActiveItem, cx);
- });
- deterministic.run_until_parked();
+ pane.update(cx, |pane, cx| pane.close_active_item(&CloseActiveItem, cx))
+ .unwrap()
+ .await
+ .unwrap();
assert_item_labels(&pane, ["A", "C*"], cx);
- workspace.update(cx, |workspace, cx| {
- Pane::close_active_item(workspace, &CloseActiveItem, cx);
- });
- deterministic.run_until_parked();
+ pane.update(cx, |pane, cx| pane.close_active_item(&CloseActiveItem, cx))
+ .unwrap()
+ .await
+ .unwrap();
assert_item_labels(&pane, ["A*"], cx);
}
#[gpui::test]
- async fn test_close_inactive_items(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
+ async fn test_close_inactive_items(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.background());
@@ -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>,
- ) {
- }
-}
@@ -1,8 +1,8 @@
+pub mod dock;
/// NOTE: Focus only 'takes' after an update has flushed_effects.
///
/// This may cause issues when you're trying to write tests that use workspace focus to add items at
/// specific locations.
-pub mod dock;
pub mod item;
pub mod notifications;
pub mod pane;
@@ -10,7 +10,6 @@ pub mod pane_group;
mod persistence;
pub mod searchable;
pub mod shared_screen;
-pub mod sidebar;
mod status_bar;
mod toolbar;
mod workspace_settings;
@@ -23,7 +22,6 @@ use client::{
Client, TypedEnvelope, UserStore,
};
use collections::{hash_map, HashMap, HashSet};
-use dock::{Dock, DockDefaultItemFactory, ToggleDockButton};
use drag_and_drop::DragAndDrop;
use futures::{
channel::{mpsc, oneshot},
@@ -62,10 +60,12 @@ use std::{
use crate::{
notifications::simple_message_notification::MessageNotification,
- persistence::model::{SerializedPane, SerializedPaneGroup, SerializedWorkspace},
+ persistence::model::{
+ DockData, DockStructure, SerializedPane, SerializedPaneGroup, SerializedWorkspace,
+ },
};
+use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle};
use lazy_static::lazy_static;
-use log::warn;
use notifications::{NotificationHandle, NotifyResultExt};
pub use pane::*;
pub use pane_group::*;
@@ -78,13 +78,12 @@ use postage::prelude::Stream;
use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
use serde::Deserialize;
use shared_screen::SharedScreen;
-use sidebar::{Sidebar, SidebarButtons, SidebarSide, ToggleSidebarItem};
use status_bar::StatusBar;
pub use status_bar::StatusItemView;
use theme::Theme;
pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
use util::{async_iife, paths, ResultExt};
-pub use workspace_settings::{AutosaveSetting, DockAnchor, GitGutterSetting, WorkspaceSettings};
+pub use workspace_settings::{AutosaveSetting, GitGutterSetting, WorkspaceSettings};
lazy_static! {
static ref ZED_WINDOW_SIZE: Option<Vector2F> = env::var("ZED_WINDOW_SIZE")
@@ -104,6 +103,21 @@ pub trait Modal: View {
#[derive(Clone, PartialEq)]
pub struct RemoveWorktreeFromProject(pub WorktreeId);
+#[derive(Copy, Clone, Default, Deserialize, PartialEq)]
+pub struct ToggleLeftDock {
+ pub focus: bool,
+}
+
+#[derive(Copy, Clone, Default, Deserialize, PartialEq)]
+pub struct ToggleBottomDock {
+ pub focus: bool,
+}
+
+#[derive(Copy, Clone, Default, Deserialize, PartialEq)]
+pub struct ToggleRightDock {
+ pub focus: bool,
+}
+
actions!(
workspace,
[
@@ -119,17 +133,23 @@ actions!(
ActivatePreviousPane,
ActivateNextPane,
FollowNextCollaborator,
- ToggleLeftSidebar,
NewTerminal,
+ ToggleTerminalFocus,
NewSearch,
Feedback,
Restart,
- Welcome
+ Welcome,
+ ToggleZoom,
]
);
actions!(zed, [OpenSettings]);
+impl_actions!(
+ workspace,
+ [ToggleLeftDock, ToggleBottomDock, ToggleRightDock]
+);
+
#[derive(Clone, PartialEq)]
pub struct OpenPaths {
pub paths: Vec<PathBuf>,
@@ -192,7 +212,6 @@ pub fn init_settings(cx: &mut AppContext) {
pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
init_settings(cx);
pane::init(cx);
- dock::init(cx);
notifications::init(cx);
cx.add_global_action({
@@ -240,15 +259,20 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
workspace.save_active_item(true, cx).detach_and_log_err(cx);
},
);
- cx.add_action(Workspace::toggle_sidebar_item);
cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| {
workspace.activate_previous_pane(cx)
});
cx.add_action(|workspace: &mut Workspace, _: &ActivateNextPane, cx| {
workspace.activate_next_pane(cx)
});
- cx.add_action(|workspace: &mut Workspace, _: &ToggleLeftSidebar, cx| {
- workspace.toggle_sidebar(SidebarSide::Left, cx);
+ cx.add_action(|workspace: &mut Workspace, action: &ToggleLeftDock, cx| {
+ workspace.toggle_dock(DockPosition::Left, action.focus, cx);
+ });
+ cx.add_action(|workspace: &mut Workspace, action: &ToggleRightDock, cx| {
+ workspace.toggle_dock(DockPosition::Right, action.focus, cx);
+ });
+ cx.add_action(|workspace: &mut Workspace, action: &ToggleBottomDock, cx| {
+ workspace.toggle_dock(DockPosition::Bottom, action.focus, cx);
});
cx.add_action(Workspace::activate_pane_at_index);
@@ -366,8 +390,8 @@ pub struct AppState {
pub fs: Arc<dyn fs::Fs>,
pub build_window_options:
fn(Option<WindowBounds>, Option<uuid::Uuid>, &dyn Platform) -> WindowOptions<'static>,
- pub initialize_workspace: fn(&mut Workspace, &Arc<AppState>, &mut ViewContext<Workspace>),
- pub dock_default_item_factory: DockDefaultItemFactory,
+ pub initialize_workspace:
+ fn(WeakViewHandle<Workspace>, bool, Arc<AppState>, AsyncAppContext) -> Task<Result<()>>,
pub background_actions: BackgroundActions,
}
@@ -395,9 +419,8 @@ impl AppState {
fs,
languages,
user_store,
- initialize_workspace: |_, _, _| {},
+ initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
build_window_options: |_, _, _| Default::default(),
- dock_default_item_factory: |_, _| None,
background_actions: || &[],
})
}
@@ -450,7 +473,6 @@ impl DelayedDebouncedEditAction {
}
pub enum Event {
- DockAnchorChanged,
PaneAdded(ViewHandle<Pane>),
ContactRequestedJoin(u64),
}
@@ -460,15 +482,15 @@ pub struct Workspace {
remote_entity_subscription: Option<client::Subscription>,
modal: Option<AnyViewHandle>,
center: PaneGroup,
- left_sidebar: ViewHandle<Sidebar>,
- right_sidebar: ViewHandle<Sidebar>,
+ left_dock: ViewHandle<Dock>,
+ bottom_dock: ViewHandle<Dock>,
+ right_dock: ViewHandle<Dock>,
panes: Vec<ViewHandle<Pane>>,
panes_by_item: HashMap<usize, WeakViewHandle<Pane>>,
active_pane: ViewHandle<Pane>,
last_active_center_pane: Option<WeakViewHandle<Pane>>,
status_bar: ViewHandle<StatusBar>,
titlebar_item: Option<AnyViewHandle>,
- dock: Dock,
notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
project: ModelHandle<Project>,
leader_state: LeaderState,
@@ -479,7 +501,7 @@ pub struct Workspace {
leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
database_id: WorkspaceId,
app_state: Arc<AppState>,
- _window_subscriptions: [Subscription; 3],
+ subscriptions: Vec<Subscription>,
_apply_leader_updates: Task<Result<()>>,
_observe_current_user: Task<Result<()>>,
pane_history_timestamp: Arc<AtomicUsize>,
@@ -537,6 +559,14 @@ impl Workspace {
cx.remove_window();
}
+ project::Event::DeletedEntry(entry_id) => {
+ for pane in this.panes.iter() {
+ pane.update(cx, |pane, cx| {
+ pane.handle_deleted_project_item(*entry_id, cx)
+ });
+ }
+ }
+
_ => {}
}
cx.notify()
@@ -549,7 +579,6 @@ impl Workspace {
let center_pane = cx.add_view(|cx| {
Pane::new(
weak_handle.clone(),
- None,
app_state.background_actions,
pane_history_timestamp.clone(),
cx,
@@ -558,13 +587,6 @@ impl Workspace {
cx.subscribe(¢er_pane, Self::handle_pane_event).detach();
cx.focus(¢er_pane);
cx.emit(Event::PaneAdded(center_pane.clone()));
- let dock = Dock::new(
- app_state.dock_default_item_factory,
- app_state.background_actions,
- pane_history_timestamp.clone(),
- cx,
- );
- let dock_pane = dock.pane().clone();
let mut current_user = app_state.user_store.read(cx).watch_current_user();
let mut connection_status = app_state.client.status();
@@ -579,7 +601,6 @@ impl Workspace {
}
anyhow::Ok(())
});
- let handle = cx.handle();
// All leader updates are enqueued and then processed in a single task, so
// that each asynchronous operation can be run in order.
@@ -597,18 +618,20 @@ impl Workspace {
cx.emit_global(WorkspaceCreated(weak_handle.clone()));
- let left_sidebar = cx.add_view(|_| Sidebar::new(SidebarSide::Left));
- let right_sidebar = cx.add_view(|_| Sidebar::new(SidebarSide::Right));
- let left_sidebar_buttons =
- cx.add_view(|cx| SidebarButtons::new(left_sidebar.clone(), weak_handle.clone(), cx));
- let toggle_dock = cx.add_view(|cx| ToggleDockButton::new(handle, cx));
- let right_sidebar_buttons =
- cx.add_view(|cx| SidebarButtons::new(right_sidebar.clone(), weak_handle.clone(), cx));
+ let left_dock = cx.add_view(|_| Dock::new(DockPosition::Left));
+ let bottom_dock = cx.add_view(|_| Dock::new(DockPosition::Bottom));
+ let right_dock = cx.add_view(|_| Dock::new(DockPosition::Right));
+ let left_dock_buttons =
+ cx.add_view(|cx| PanelButtons::new(left_dock.clone(), weak_handle.clone(), cx));
+ let bottom_dock_buttons =
+ cx.add_view(|cx| PanelButtons::new(bottom_dock.clone(), weak_handle.clone(), cx));
+ let right_dock_buttons =
+ cx.add_view(|cx| PanelButtons::new(right_dock.clone(), weak_handle.clone(), cx));
let status_bar = cx.add_view(|cx| {
let mut status_bar = StatusBar::new(¢er_pane.clone(), cx);
- status_bar.add_left_item(left_sidebar_buttons, cx);
- status_bar.add_right_item(right_sidebar_buttons, cx);
- status_bar.add_right_item(toggle_dock, cx);
+ status_bar.add_left_item(left_dock_buttons, cx);
+ status_bar.add_right_item(right_dock_buttons, cx);
+ status_bar.add_right_item(bottom_dock_buttons, cx);
status_bar
});
@@ -624,7 +647,7 @@ impl Workspace {
active_call = Some((call, subscriptions));
}
- let subscriptions = [
+ let subscriptions = vec![
cx.observe_fullscreen(|_, _, cx| cx.notify()),
cx.observe_window_activation(Self::on_window_activation_changed),
cx.observe_window_bounds(move |_, mut bounds, display, cx| {
@@ -644,17 +667,25 @@ impl Workspace {
.spawn(DB.set_window_bounds(workspace_id, bounds, display))
.detach_and_log_err(cx);
}),
+ cx.observe(&left_dock, |this, _, cx| {
+ this.serialize_workspace(cx);
+ cx.notify();
+ }),
+ cx.observe(&bottom_dock, |this, _, cx| {
+ this.serialize_workspace(cx);
+ cx.notify();
+ }),
+ cx.observe(&right_dock, |this, _, cx| {
+ this.serialize_workspace(cx);
+ cx.notify();
+ }),
];
let mut this = Workspace {
- modal: None,
weak_self: weak_handle.clone(),
+ modal: None,
center: PaneGroup::new(center_pane.clone()),
- dock,
- // When removing an item, the last element remaining in this array
- // is used to find where focus should fallback to. As such, the order
- // of these two variables is important.
- panes: vec![dock_pane.clone(), center_pane.clone()],
+ panes: vec![center_pane.clone()],
panes_by_item: Default::default(),
active_pane: center_pane.clone(),
last_active_center_pane: Some(center_pane.downgrade()),
@@ -662,8 +693,9 @@ impl Workspace {
titlebar_item: None,
notifications: Default::default(),
remote_entity_subscription: None,
- left_sidebar,
- right_sidebar,
+ left_dock,
+ bottom_dock,
+ right_dock,
project: project.clone(),
leader_state: Default::default(),
follower_states_by_leader: Default::default(),
@@ -675,12 +707,11 @@ impl Workspace {
_observe_current_user,
_apply_leader_updates,
leader_updates_tx,
- _window_subscriptions: subscriptions,
+ subscriptions,
pane_history_timestamp,
};
this.project_remote_id_changed(project.read(cx).remote_id(), cx);
cx.defer(|this, cx| this.update_window_title(cx));
-
this
}
@@ -742,11 +773,7 @@ impl Workspace {
});
let build_workspace = |cx: &mut ViewContext<Workspace>| {
- let mut workspace =
- Workspace::new(workspace_id, project_handle.clone(), app_state.clone(), cx);
- (app_state.initialize_workspace)(&mut workspace, &app_state, cx);
-
- workspace
+ Workspace::new(workspace_id, project_handle.clone(), app_state.clone(), cx)
};
let workspace = requesting_window_id
@@ -794,6 +821,17 @@ impl Workspace {
.1
});
+ (app_state.initialize_workspace)(
+ workspace.downgrade(),
+ serialized_workspace.is_some(),
+ app_state.clone(),
+ cx.clone(),
+ )
+ .await
+ .log_err();
+
+ cx.update_window(workspace.window_id(), |cx| cx.activate_window());
+
let workspace = workspace.downgrade();
notify_if_database_failed(&workspace, &mut cx);
let opened_items = open_items(
@@ -813,12 +851,66 @@ impl Workspace {
self.weak_self.clone()
}
- pub fn left_sidebar(&self) -> &ViewHandle<Sidebar> {
- &self.left_sidebar
+ pub fn left_dock(&self) -> &ViewHandle<Dock> {
+ &self.left_dock
}
- pub fn right_sidebar(&self) -> &ViewHandle<Sidebar> {
- &self.right_sidebar
+ pub fn bottom_dock(&self) -> &ViewHandle<Dock> {
+ &self.bottom_dock
+ }
+
+ pub fn right_dock(&self) -> &ViewHandle<Dock> {
+ &self.right_dock
+ }
+
+ pub fn add_panel<T: Panel>(&mut self, panel: ViewHandle<T>, cx: &mut ViewContext<Self>) {
+ let dock = match panel.position(cx) {
+ DockPosition::Left => &self.left_dock,
+ DockPosition::Bottom => &self.bottom_dock,
+ DockPosition::Right => &self.right_dock,
+ };
+
+ self.subscriptions.push(cx.subscribe(&panel, {
+ let mut dock = dock.clone();
+ let mut prev_position = panel.position(cx);
+ move |this, panel, event, cx| {
+ if T::should_change_position_on_event(event) {
+ let new_position = panel.read(cx).position(cx);
+ let mut was_visible = false;
+ dock.update(cx, |dock, cx| {
+ prev_position = new_position;
+
+ was_visible = dock.is_open()
+ && dock
+ .active_panel()
+ .map_or(false, |active_panel| active_panel.id() == panel.id());
+ dock.remove_panel(&panel, cx);
+ });
+ dock = match panel.read(cx).position(cx) {
+ DockPosition::Left => &this.left_dock,
+ DockPosition::Bottom => &this.bottom_dock,
+ DockPosition::Right => &this.right_dock,
+ }
+ .clone();
+ dock.update(cx, |dock, cx| {
+ dock.add_panel(panel.clone(), cx);
+ if was_visible {
+ dock.set_open(true, cx);
+ dock.activate_panel(dock.panels_len() - 1, cx);
+ }
+ });
+ } else if T::should_zoom_in_on_event(event) {
+ this.zoom_out(cx);
+ dock.update(cx, |dock, cx| dock.set_panel_zoomed(&panel, true, cx));
+ } else if T::should_zoom_out_on_event(event) {
+ this.zoom_out(cx);
+ } else if T::is_focus_event(event) {
+ cx.notify();
+ }
+ }
+ }));
+
+ dock.update(cx, |dock, cx| dock.add_panel(panel, cx));
}
pub fn status_bar(&self) -> &ViewHandle<StatusBar> {
@@ -1264,6 +1356,44 @@ impl Workspace {
}
}
+ fn zoomed(&self, cx: &WindowContext) -> Option<AnyViewHandle> {
+ self.zoomed_panel_for_dock(DockPosition::Left, cx)
+ .or_else(|| self.zoomed_panel_for_dock(DockPosition::Bottom, cx))
+ .or_else(|| self.zoomed_panel_for_dock(DockPosition::Right, cx))
+ .or_else(|| self.zoomed_pane(cx))
+ }
+
+ fn zoomed_panel_for_dock(
+ &self,
+ position: DockPosition,
+ cx: &WindowContext,
+ ) -> Option<AnyViewHandle> {
+ let (dock, other_docks) = match position {
+ DockPosition::Left => (&self.left_dock, [&self.bottom_dock, &self.right_dock]),
+ DockPosition::Bottom => (&self.bottom_dock, [&self.left_dock, &self.right_dock]),
+ DockPosition::Right => (&self.right_dock, [&self.left_dock, &self.bottom_dock]),
+ };
+
+ let zoomed_panel = dock.read(&cx).zoomed_panel(cx)?;
+ if other_docks.iter().all(|dock| !dock.read(cx).has_focus(cx))
+ && !self.active_pane.read(cx).has_focus()
+ {
+ Some(zoomed_panel.as_any().clone())
+ } else {
+ None
+ }
+ }
+
+ fn zoomed_pane(&self, cx: &WindowContext) -> Option<AnyViewHandle> {
+ let active_pane = self.active_pane.read(cx);
+ let docks = [&self.left_dock, &self.bottom_dock, &self.right_dock];
+ if active_pane.is_zoomed() && docks.iter().all(|dock| !dock.read(cx).has_focus(cx)) {
+ Some(self.active_pane.clone().into_any())
+ } else {
+ None
+ }
+ }
+
pub fn items<'a>(
&'a self,
cx: &'a AppContext,
@@ -1341,47 +1471,55 @@ impl Workspace {
}
}
- pub fn toggle_sidebar(&mut self, sidebar_side: SidebarSide, cx: &mut ViewContext<Self>) {
- let sidebar = match sidebar_side {
- SidebarSide::Left => &mut self.left_sidebar,
- SidebarSide::Right => &mut self.right_sidebar,
+ pub fn toggle_dock(
+ &mut self,
+ dock_side: DockPosition,
+ focus: bool,
+ cx: &mut ViewContext<Self>,
+ ) {
+ let dock = match dock_side {
+ DockPosition::Left => &self.left_dock,
+ DockPosition::Bottom => &self.bottom_dock,
+ DockPosition::Right => &self.right_dock,
};
- let open = sidebar.update(cx, |sidebar, cx| {
- let open = !sidebar.is_open();
- sidebar.set_open(open, cx);
- open
+ dock.update(cx, |dock, cx| {
+ let open = !dock.is_open();
+ dock.set_open(open, cx);
});
- if open {
- Dock::hide_on_sidebar_shown(self, sidebar_side, cx);
+ if dock.read(cx).is_open() && focus {
+ cx.focus(dock);
+ } else {
+ cx.focus_self();
}
-
- self.serialize_workspace(cx);
-
- cx.focus_self();
cx.notify();
+ self.serialize_workspace(cx);
}
- pub fn toggle_sidebar_item(&mut self, action: &ToggleSidebarItem, cx: &mut ViewContext<Self>) {
- let sidebar = match action.sidebar_side {
- SidebarSide::Left => &mut self.left_sidebar,
- SidebarSide::Right => &mut self.right_sidebar,
+ pub fn toggle_panel(
+ &mut self,
+ position: DockPosition,
+ panel_index: usize,
+ cx: &mut ViewContext<Self>,
+ ) {
+ let dock = match position {
+ DockPosition::Left => &mut self.left_dock,
+ DockPosition::Bottom => &mut self.bottom_dock,
+ DockPosition::Right => &mut self.right_dock,
};
- let active_item = sidebar.update(cx, move |sidebar, cx| {
- if sidebar.is_open() && sidebar.active_item_ix() == action.item_index {
- sidebar.set_open(false, cx);
+ let active_item = dock.update(cx, move |dock, cx| {
+ if dock.is_open() && dock.active_panel_index() == panel_index {
+ dock.set_open(false, cx);
None
} else {
- sidebar.set_open(true, cx);
- sidebar.activate_item(action.item_index, cx);
- sidebar.active_item().cloned()
+ dock.set_open(true, cx);
+ dock.activate_panel(panel_index, cx);
+ dock.active_panel().cloned()
}
});
if let Some(active_item) = active_item {
- Dock::hide_on_sidebar_shown(self, action.sidebar_side, cx);
-
- if active_item.is_focused(cx) {
+ if active_item.has_focus(cx) {
cx.focus_self();
} else {
cx.focus(active_item.as_any());
@@ -1395,32 +1533,37 @@ impl Workspace {
cx.notify();
}
- pub fn toggle_sidebar_item_focus(
- &mut self,
- sidebar_side: SidebarSide,
- item_index: usize,
- cx: &mut ViewContext<Self>,
- ) {
- let sidebar = match sidebar_side {
- SidebarSide::Left => &mut self.left_sidebar,
- SidebarSide::Right => &mut self.right_sidebar,
- };
- let active_item = sidebar.update(cx, |sidebar, cx| {
- sidebar.set_open(true, cx);
- sidebar.activate_item(item_index, cx);
- sidebar.active_item().cloned()
- });
- if let Some(active_item) = active_item {
- Dock::hide_on_sidebar_shown(self, sidebar_side, cx);
+ pub fn toggle_panel_focus<T: Panel>(&mut self, cx: &mut ViewContext<Self>) {
+ for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
+ if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
+ let active_item = dock.update(cx, |dock, cx| {
+ dock.set_open(true, cx);
+ dock.activate_panel(panel_index, cx);
+ dock.active_panel().cloned()
+ });
+ if let Some(active_item) = active_item {
+ if active_item.has_focus(cx) {
+ cx.focus_self();
+ } else {
+ cx.focus(active_item.as_any());
+ }
+ }
- if active_item.is_focused(cx) {
- cx.focus_self();
- } else {
- cx.focus(active_item.as_any());
+ self.serialize_workspace(cx);
+ cx.notify();
+ break;
}
}
+ }
- self.serialize_workspace(cx);
+ fn zoom_out(&mut self, cx: &mut ViewContext<Self>) {
+ for pane in &self.panes {
+ pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
+ }
+
+ self.left_dock.update(cx, |dock, cx| dock.zoom_out(cx));
+ self.bottom_dock.update(cx, |dock, cx| dock.zoom_out(cx));
+ self.right_dock.update(cx, |dock, cx| dock.zoom_out(cx));
cx.notify();
}
@@ -1429,7 +1572,6 @@ impl Workspace {
let pane = cx.add_view(|cx| {
Pane::new(
self.weak_handle(),
- None,
self.app_state.background_actions,
self.pane_history_timestamp.clone(),
cx,
@@ -1472,16 +1614,12 @@ impl Workspace {
cx: &mut ViewContext<Self>,
) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
let pane = pane.unwrap_or_else(|| {
- if !self.dock_active() {
- self.active_pane().downgrade()
- } else {
- self.last_active_center_pane.clone().unwrap_or_else(|| {
- self.panes
- .first()
- .expect("There must be an active pane")
- .downgrade()
- })
- }
+ self.last_active_center_pane.clone().unwrap_or_else(|| {
+ self.panes
+ .first()
+ .expect("There must be an active pane")
+ .downgrade()
+ })
});
let task = self.load_path(path.into(), cx);
@@ -1560,9 +1698,6 @@ impl Workspace {
.map(|ix| (pane.clone(), ix))
});
if let Some((pane, ix)) = result {
- if &pane == self.dock_pane() {
- Dock::show(self, false, cx);
- }
pane.update(cx, |pane, cx| pane.activate_item(ix, true, true, cx));
true
} else {
@@ -1608,16 +1743,7 @@ impl Workspace {
status_bar.set_active_pane(&self.active_pane, cx);
});
self.active_item_path_changed(cx);
-
- if &pane == self.dock_pane() {
- Dock::show(self, false, cx);
- } else {
- self.last_active_center_pane = Some(pane.downgrade());
- if self.dock.is_anchored_at(DockAnchor::Expanded) {
- Dock::hide(self, cx);
- }
- }
- cx.notify();
+ self.last_active_center_pane = Some(pane.downgrade());
}
self.update_followers(
@@ -1631,6 +1757,8 @@ impl Workspace {
}),
cx,
);
+
+ cx.notify();
}
fn handle_pane_event(
@@ -1639,13 +1767,11 @@ impl Workspace {
event: &pane::Event,
cx: &mut ViewContext<Self>,
) {
- let is_dock = &pane == self.dock.pane();
match event {
- pane::Event::Split(direction) if !is_dock => {
+ pane::Event::Split(direction) => {
self.split_pane(pane, *direction, cx);
}
- pane::Event::Remove if !is_dock => self.remove_pane(pane, cx),
- pane::Event::Remove if is_dock => Dock::hide(self, cx),
+ pane::Event::Remove => self.remove_pane(pane, cx),
pane::Event::ActivateItem { local } => {
if *local {
self.unfollow(&pane, cx);
@@ -1671,7 +1797,14 @@ impl Workspace {
pane::Event::Focus => {
self.handle_pane_focused(pane.clone(), cx);
}
- _ => {}
+ pane::Event::ZoomIn => {
+ if pane == self.active_pane {
+ self.zoom_out(cx);
+ pane.update(cx, |pane, cx| pane.set_zoomed(true, cx));
+ cx.notify();
+ }
+ }
+ pane::Event::ZoomOut => self.zoom_out(cx),
}
self.serialize_workspace(cx);
@@ -1683,11 +1816,6 @@ impl Workspace {
direction: SplitDirection,
cx: &mut ViewContext<Self>,
) -> Option<ViewHandle<Pane>> {
- if &pane == self.dock_pane() {
- warn!("Can't split dock pane.");
- return None;
- }
-
let item = pane.read(cx).active_item()?;
let maybe_pane_handle = if let Some(clone) = item.clone_on_split(self.database_id(), cx) {
let new_pane = self.add_pane(cx);
@@ -1711,10 +1839,6 @@ impl Workspace {
) {
let Some(pane_to_split) = pane_to_split.upgrade(cx) else { return; };
let Some(from) = from.upgrade(cx) else { return; };
- if &pane_to_split == self.dock_pane() {
- warn!("Can't split dock pane.");
- return;
- }
let new_pane = self.add_pane(cx);
Pane::move_item(self, from.clone(), new_pane.clone(), item_id_to_move, 0, cx);
@@ -1732,11 +1856,6 @@ impl Workspace {
cx: &mut ViewContext<Self>,
) -> Option<Task<Result<()>>> {
let pane_to_split = pane_to_split.upgrade(cx)?;
- if &pane_to_split == self.dock_pane() {
- warn!("Can't split dock pane.");
- return None;
- }
-
let new_pane = self.add_pane(cx);
self.center
.split(&pane_to_split, &new_pane, split_direction)
@@ -1773,14 +1892,6 @@ impl Workspace {
&self.active_pane
}
- pub fn dock_pane(&self) -> &ViewHandle<Pane> {
- self.dock.pane()
- }
-
- fn dock_active(&self) -> bool {
- &self.active_pane == self.dock.pane()
- }
-
fn project_remote_id_changed(&mut self, remote_id: Option<u64>, cx: &mut ViewContext<Self>) {
if let Some(remote_id) = remote_id {
self.remote_entity_subscription = Some(
@@ -2522,23 +2633,65 @@ impl Workspace {
}
}
+ fn build_serialized_docks(this: &Workspace, cx: &AppContext) -> DockStructure {
+ let left_dock = this.left_dock.read(cx);
+ let left_visible = left_dock.is_open();
+ let left_active_panel = left_dock.active_panel().and_then(|panel| {
+ Some(
+ cx.view_ui_name(panel.as_any().window_id(), panel.id())?
+ .to_string(),
+ )
+ });
+
+ let right_dock = this.right_dock.read(cx);
+ let right_visible = right_dock.is_open();
+ let right_active_panel = right_dock.active_panel().and_then(|panel| {
+ Some(
+ cx.view_ui_name(panel.as_any().window_id(), panel.id())?
+ .to_string(),
+ )
+ });
+
+ let bottom_dock = this.bottom_dock.read(cx);
+ let bottom_visible = bottom_dock.is_open();
+ let bottom_active_panel = bottom_dock.active_panel().and_then(|panel| {
+ Some(
+ cx.view_ui_name(panel.as_any().window_id(), panel.id())?
+ .to_string(),
+ )
+ });
+
+ DockStructure {
+ left: DockData {
+ visible: left_visible,
+ active_panel: left_active_panel,
+ },
+ right: DockData {
+ visible: right_visible,
+ active_panel: right_active_panel,
+ },
+ bottom: DockData {
+ visible: bottom_visible,
+ active_panel: bottom_active_panel,
+ },
+ }
+ }
+
if let Some(location) = self.location(cx) {
// Load bearing special case:
// - with_local_workspace() relies on this to not have other stuff open
// when you open your log
if !location.paths().is_empty() {
- let dock_pane = serialize_pane_handle(self.dock.pane(), cx);
let center_group = build_serialized_pane_group(&self.center.root, cx);
+ let docks = build_serialized_docks(self, cx);
let serialized_workspace = SerializedWorkspace {
id: self.database_id,
location,
- dock_position: self.dock.position(),
- dock_pane,
center_group,
- left_sidebar_open: self.left_sidebar.read(cx).is_open(),
bounds: Default::default(),
display: Default::default(),
+ docks,
};
cx.background()
@@ -2556,26 +2709,14 @@ impl Workspace {
) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>> {
cx.spawn(|mut cx| async move {
let result = async_iife! {{
- let (project, dock_pane_handle, old_center_pane) =
+ let (project, old_center_pane) =
workspace.read_with(&cx, |workspace, _| {
(
workspace.project().clone(),
- workspace.dock_pane().downgrade(),
workspace.last_active_center_pane.clone(),
)
})?;
- let dock_items = serialized_workspace
- .dock_pane
- .deserialize_to(
- &project,
- &dock_pane_handle,
- serialized_workspace.id,
- &workspace,
- &mut cx,
- )
- .await?;
-
let mut center_items = None;
let mut center_group = None;
// Traverse the splits tree and add to things
@@ -2591,7 +2732,6 @@ impl Workspace {
let mut opened_items = center_items
.unwrap_or_default()
.into_iter()
- .chain(dock_items.into_iter())
.filter_map(|item| {
let item = item?;
let project_path = item.project_path(cx)?;
@@ -2637,22 +2777,30 @@ impl Workspace {
}
}
- if workspace.left_sidebar().read(cx).is_open()
- != serialized_workspace.left_sidebar_open
- {
- workspace.toggle_sidebar(SidebarSide::Left, cx);
- }
-
- // Note that without after_window, the focus_self() and
- // the focus the dock generates start generating alternating
- // focus due to the deferred execution each triggering each other
- cx.after_window_update(move |workspace, cx| {
- Dock::set_dock_position(
- workspace,
- serialized_workspace.dock_position,
- false,
- cx,
- );
+ let docks = serialized_workspace.docks;
+ workspace.left_dock.update(cx, |dock, cx| {
+ dock.set_open(docks.left.visible, cx);
+ if let Some(active_panel) = docks.left.active_panel {
+ if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
+ dock.activate_panel(ix, cx);
+ }
+ }
+ });
+ workspace.right_dock.update(cx, |dock, cx| {
+ dock.set_open(docks.right.visible, cx);
+ if let Some(active_panel) = docks.right.active_panel {
+ if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
+ dock.activate_panel(ix, cx);
+ }
+ }
+ });
+ workspace.bottom_dock.update(cx, |dock, cx| {
+ dock.set_open(docks.bottom.visible, cx);
+ if let Some(active_panel) = docks.bottom.active_panel {
+ if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
+ dock.activate_panel(ix, cx);
+ }
+ }
});
cx.notify();
@@ -2676,12 +2824,38 @@ impl Workspace {
user_store: project.read(cx).user_store(),
fs: project.read(cx).fs().clone(),
build_window_options: |_, _, _| Default::default(),
- initialize_workspace: |_, _, _| {},
- dock_default_item_factory: |_, _| None,
+ initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
background_actions: || &[],
});
Self::new(0, project, app_state, cx)
}
+
+ fn render_dock(&self, position: DockPosition, cx: &WindowContext) -> Option<AnyElement<Self>> {
+ let dock = match position {
+ DockPosition::Left => &self.left_dock,
+ DockPosition::Right => &self.right_dock,
+ DockPosition::Bottom => &self.bottom_dock,
+ };
+ let active_panel = dock.read(cx).active_panel()?;
+ let element = if Some(active_panel.as_any()) == self.zoomed(cx).as_ref() {
+ dock.read(cx).render_placeholder(cx)
+ } else {
+ ChildView::new(dock, cx).into_any()
+ };
+
+ Some(
+ element
+ .constrained()
+ .dynamically(move |constraint, _, cx| match position {
+ DockPosition::Left | DockPosition::Right => SizeConstraint::new(
+ Vector2F::new(20., constraint.min.y()),
+ Vector2F::new(cx.window_size().x() * 0.8, constraint.max.y()),
+ ),
+ _ => constraint,
+ })
+ .into_any(),
+ )
+ }
}
async fn open_items(
@@ -2827,76 +3001,46 @@ impl View for Workspace {
.with_child({
let project = self.project.clone();
Flex::row()
- .with_children(
- if self.left_sidebar.read(cx).active_item().is_some() {
- Some(
- ChildView::new(&self.left_sidebar, cx)
- .constrained()
- .dynamically(|constraint, _, cx| {
- SizeConstraint::new(
- Vector2F::new(20., constraint.min.y()),
- Vector2F::new(
- cx.window_size().x() * 0.8,
- constraint.max.y(),
- ),
- )
- }),
- )
- } else {
- None
- },
- )
+ .with_children(self.render_dock(DockPosition::Left, cx))
.with_child(
- FlexItem::new(
- Flex::column()
- .with_child(
- FlexItem::new(self.center.render(
- &project,
- &theme,
- &self.follower_states_by_leader,
- self.active_call(),
- self.active_pane(),
- &self.app_state,
- cx,
- ))
- .flex(1., true),
- )
- .with_children(self.dock.render(
+ Flex::column()
+ .with_child(
+ FlexItem::new(self.center.render(
+ &project,
&theme,
- DockAnchor::Bottom,
+ &self.follower_states_by_leader,
+ self.active_call(),
+ self.active_pane(),
+ self.zoomed(cx).as_ref(),
+ &self.app_state,
cx,
- )),
- )
- .flex(1., true),
- )
- .with_children(self.dock.render(&theme, DockAnchor::Right, cx))
- .with_children(
- if self.right_sidebar.read(cx).active_item().is_some() {
- Some(
- ChildView::new(&self.right_sidebar, cx)
- .constrained()
- .dynamically(|constraint, _, cx| {
- SizeConstraint::new(
- Vector2F::new(20., constraint.min.y()),
- Vector2F::new(
- cx.window_size().x() * 0.8,
- constraint.max.y(),
- ),
- )
- }),
+ ))
+ .flex(1., true),
+ )
+ .with_children(
+ self.render_dock(DockPosition::Bottom, cx),
)
- } else {
- None
- },
+ .flex(1., true),
)
+ .with_children(self.render_dock(DockPosition::Right, cx))
})
.with_child(Overlay::new(
Stack::new()
- .with_children(self.dock.render(
- &theme,
- DockAnchor::Expanded,
- cx,
- ))
+ .with_children(self.zoomed(cx).map(|zoomed| {
+ enum ZoomBackground {}
+
+ ChildView::new(&zoomed, cx)
+ .contained()
+ .with_style(theme.workspace.zoomed_foreground)
+ .aligned()
+ .contained()
+ .with_style(theme.workspace.zoomed_background)
+ .mouse::<ZoomBackground>(0)
+ .capture_all()
+ .on_down(MouseButton::Left, |_, this: &mut Self, cx| {
+ this.zoom_out(cx);
+ })
+ }))
.with_children(self.modal.as_ref().map(|modal| {
ChildView::new(modal, cx)
.contained()
@@ -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,17 +8,15 @@ pub struct WorkspaceSettings {
pub confirm_quit: bool,
pub show_call_status_icon: bool,
pub autosave: AutosaveSetting,
- pub default_dock_anchor: DockAnchor,
pub git: GitSettings,
}
-#[derive(Clone, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
pub struct WorkspaceSettingsContent {
pub active_pane_magnification: Option<f32>,
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,
@@ -187,7 +186,6 @@ fn main() {
fs,
build_window_options,
initialize_workspace,
- dock_default_item_factory,
background_actions,
});
cx.set_global(Arc::downgrade(&app_state));
@@ -817,7 +815,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,18 @@ 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 { focus: false },
+ ),
+ MenuItem::action(
+ "Toggle Right Dock",
+ workspace::ToggleRightDock { focus: false },
+ ),
+ MenuItem::action(
+ "Toggle Bottom Dock",
+ workspace::ToggleBottomDock { focus: false },
+ ),
MenuItem::submenu(Menu {
name: "Editor Layout",
items: vec![
@@ -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();
+ }
+ }
+ })
+ .detach();
- cx.emit(workspace::Event::PaneAdded(workspace.active_pane().clone()));
- cx.emit(workspace::Event::PaneAdded(workspace.dock_pane().clone()));
+ 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 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 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);
+ });
- 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);
- auto_update::notify_of_any_new_update(cx.weak_handle(), cx);
+ vim::observe_keystrokes(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, false, cx);
+ }
- cx.on_window_should_close(|workspace, cx| {
- if let Some(task) = workspace.close(&Default::default(), cx) {
- task.detach_and_log_err(cx);
- }
- false
- });
+ workspace.add_panel(terminal_panel, cx)
+ })?;
+ Ok(())
+ })
}
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);
@@ -2068,7 +2087,10 @@ mod tests {
workspace::init(app_state.clone(), cx);
language::init(cx);
editor::init(cx);
+ project_panel::init_settings(cx);
pane::init(cx);
+ project_panel::init(cx);
+ terminal_view::init(cx);
app_state
})
}
@@ -2,8 +2,8 @@
const { execFileSync } = require("child_process");
const { GITHUB_ACCESS_TOKEN } = process.env;
-const PR_REGEX = /pull request #(\d+)/;
-const FIXES_REGEX = /(fixes|closes) (.+[/#]\d+.*)$/im;
+const PR_REGEX = /#\d+/ // Ex: matches on #4241
+const FIXES_REGEX = /(fixes|closes|completes) (.+[/#]\d+.*)$/im;
main();
@@ -15,7 +15,7 @@ async function main() {
{ encoding: "utf8" }
)
.split("\n")
- .filter((t) => t.startsWith("v") && t.endsWith('-pre'));
+ .filter((t) => t.startsWith("v") && t.endsWith("-pre"));
// Print the previous release
console.log(`Changes from ${oldTag} to ${newTag}\n`);
@@ -34,42 +34,16 @@ async function main() {
}
// Get the PRs merged between those two tags.
- const pullRequestNumbers = execFileSync(
- "git",
- [
- "log",
- `${oldTag}..${newTag}`,
- "--oneline",
- "--grep",
- "Merge pull request",
- ],
- { encoding: "utf8" }
- )
- .split("\n")
- .filter((line) => line.length > 0)
- .map((line) => line.match(PR_REGEX)[1]);
+ const pullRequestNumbers = getPullRequestNumbers(oldTag, newTag);
// Get the PRs that were cherry-picked between main and the old tag.
- const existingPullRequestNumbers = new Set(execFileSync(
- "git",
- [
- "log",
- `main..${oldTag}`,
- "--oneline",
- "--grep",
- "Merge pull request",
- ],
- { encoding: "utf8" }
- )
- .split("\n")
- .filter((line) => line.length > 0)
- .map((line) => line.match(PR_REGEX)[1]));
-
+ const existingPullRequestNumbers = new Set(getPullRequestNumbers("main", oldTag));
+
// Filter out those existing PRs from the set of new PRs.
const newPullRequestNumbers = pullRequestNumbers.filter(number => !existingPullRequestNumbers.has(number));
// Fetch the pull requests from the GitHub API.
- console.log("Merged Pull requests:")
+ console.log("Merged Pull requests:");
for (const pullRequestNumber of newPullRequestNumbers) {
const webURL = `https://github.com/zed-industries/zed/pull/${pullRequestNumber}`;
const apiURL = `https://api.github.com/repos/zed-industries/zed/pulls/${pullRequestNumber}`;
@@ -83,13 +57,47 @@ async function main() {
// Print the pull request title and URL.
const pullRequest = await response.json();
console.log("*", pullRequest.title);
- console.log(" URL: ", webURL);
+ console.log(" PR URL: ", webURL);
// If the pull request contains a 'closes' line, print the closed issue.
- const fixesMatch = (pullRequest.body || '').match(FIXES_REGEX);
+ const fixesMatch = (pullRequest.body || "").match(FIXES_REGEX);
if (fixesMatch) {
const fixedIssueURL = fixesMatch[2];
- console.log(" Issue: ", fixedIssueURL);
+ console.log(" Issue URL: ", fixedIssueURL);
}
+
+ let releaseNotes = (pullRequest.body || "").split("Release Notes:")[1];
+
+ if (releaseNotes) {
+ releaseNotes = releaseNotes.trim().split("\n")
+ console.log(" Release Notes:");
+
+ for (const line of releaseNotes) {
+ console.log(` ${line}`);
+ }
+ }
+
+ console.log()
}
}
+
+function getPullRequestNumbers(oldTag, newTag) {
+ const pullRequestNumbers = execFileSync(
+ "git",
+ [
+ "log",
+ `${oldTag}..${newTag}`,
+ "--oneline"
+ ],
+ { encoding: "utf8" }
+ )
+ .split("\n")
+ .filter(line => line.length > 0)
+ .map(line => {
+ const match = line.match(/#(\d+)/);
+ return match ? match[1] : null;
+ })
+ .filter(line => line);
+
+ return pullRequestNumbers;
+}
@@ -6,6 +6,8 @@ import hoverPopover from "./hoverPopover"
import { SyntaxHighlightStyle, buildSyntax } from "../themes/common/syntax"
export default function editor(colorScheme: ColorScheme) {
+ const { isLight } = colorScheme
+
let layer = colorScheme.highest
const autocompleteItem = {
@@ -97,12 +99,18 @@ export default function editor(colorScheme: ColorScheme) {
foldBackground: foreground(layer, "variant"),
},
diff: {
- deleted: foreground(layer, "negative"),
- modified: foreground(layer, "warning"),
- inserted: foreground(layer, "positive"),
+ deleted: isLight
+ ? colorScheme.ramps.red(0.5).hex()
+ : colorScheme.ramps.red(0.4).hex(),
+ modified: isLight
+ ? colorScheme.ramps.yellow(0.3).hex()
+ : colorScheme.ramps.yellow(0.5).hex(),
+ inserted: isLight
+ ? colorScheme.ramps.green(0.4).hex()
+ : colorScheme.ramps.green(0.5).hex(),
removedWidthEm: 0.275,
- widthEm: 0.22,
- cornerRadius: 0.2,
+ widthEm: 0.15,
+ cornerRadius: 0.05,
},
/** Highlights matching occurences of what is under the cursor
* as well as matched brackets
@@ -234,12 +242,27 @@ export default function editor(colorScheme: ColorScheme) {
border: border(layer, "variant", { left: true }),
},
thumb: {
- background: withOpacity(background(layer, "inverted"), 0.4),
+ background: withOpacity(background(layer, "inverted"), 0.3),
border: {
- width: 1,
- color: borderColor(layer, "variant"),
- },
+ width: 1,
+ color: borderColor(layer, "variant"),
+ top: false,
+ right: true,
+ left: true,
+ bottom: false,
+ }
},
+ git: {
+ deleted: isLight
+ ? withOpacity(colorScheme.ramps.red(0.5).hex(), 0.8)
+ : withOpacity(colorScheme.ramps.red(0.4).hex(), 0.8),
+ modified: isLight
+ ? withOpacity(colorScheme.ramps.yellow(0.5).hex(), 0.8)
+ : withOpacity(colorScheme.ramps.yellow(0.4).hex(), 0.8),
+ inserted: isLight
+ ? withOpacity(colorScheme.ramps.green(0.5).hex(), 0.8)
+ : withOpacity(colorScheme.ramps.green(0.4).hex(), 0.8),
+ }
},
compositionMark: {
underline: {
@@ -3,6 +3,8 @@ import { withOpacity } from "../utils/color"
import { background, border, foreground, text } from "./components"
export default function projectPanel(colorScheme: ColorScheme) {
+ const { isLight } = colorScheme
+
let layer = colorScheme.middle
let baseEntry = {
@@ -12,6 +14,20 @@ export default function projectPanel(colorScheme: ColorScheme) {
iconSpacing: 8,
}
+ let status = {
+ git: {
+ modified: isLight
+ ? colorScheme.ramps.yellow(0.6).hex()
+ : colorScheme.ramps.yellow(0.5).hex(),
+ inserted: isLight
+ ? colorScheme.ramps.green(0.45).hex()
+ : colorScheme.ramps.green(0.5).hex(),
+ conflict: isLight
+ ? colorScheme.ramps.red(0.6).hex()
+ : colorScheme.ramps.red(0.5).hex()
+ }
+ }
+
let entry = {
...baseEntry,
text: text(layer, "mono", "variant", { size: "sm" }),
@@ -28,6 +44,7 @@ export default function projectPanel(colorScheme: ColorScheme) {
background: background(layer, "active"),
text: text(layer, "mono", "active", { size: "sm" }),
},
+ status
}
return {
@@ -62,6 +79,7 @@ export default function projectPanel(colorScheme: ColorScheme) {
text: text(layer, "mono", "on", { size: "sm" }),
background: withOpacity(background(layer, "on"), 0.9),
border: border(layer),
+ status
},
ignoredEntry: {
...entry,
@@ -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),
}
}