diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 8d16a59bc1..67a7ed31c9 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -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) ... ([#](https://github.com/zed-industries/community/issues/)). +* ... + +If the release notes are only intended for a specific release channel only, add `(-only)` to the end of the release note line. +These will be removed by the person making the release. diff --git a/Cargo.lock b/Cargo.lock index 5728a27002..4200f8c917 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index 7411dd53ad..f1362e059d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" } diff --git a/Untitled b/Untitled deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/assets/keymaps/atom.json b/assets/keymaps/atom.json index 99aae2638f..634aed322a 100644 --- a/assets/keymaps/atom.json +++ b/assets/keymaps/atom.json @@ -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": {} } ] diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 746d614786..35182dfaa6 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -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": { diff --git a/assets/keymaps/jetbrains.json b/assets/keymaps/jetbrains.json index 383de07904..4825d3e8b5 100644 --- a/assets/keymaps/jetbrains.json +++ b/assets/keymaps/jetbrains.json @@ -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" } } ] diff --git a/assets/keymaps/sublime_text.json b/assets/keymaps/sublime_text.json index 373f3f4d3a..2d32b77d58 100644 --- a/assets/keymaps/sublime_text.json +++ b/assets/keymaps/sublime_text.json @@ -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" - } } ] diff --git a/assets/keymaps/textmate.json b/assets/keymaps/textmate.json index 1abcaa376c..06be727429 100644 --- a/assets/keymaps/textmate.json +++ b/assets/keymaps/textmate.json @@ -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": {} } ] diff --git a/assets/settings/default.json b/assets/settings/default.json index 4f149edb10..246e28cc8e 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -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 diff --git a/crates/activity_indicator/Cargo.toml b/crates/activity_indicator/Cargo.toml index 917383234a..43d16e6b9b 100644 --- a/crates/activity_indicator/Cargo.toml +++ b/crates/activity_indicator/Cargo.toml @@ -21,3 +21,6 @@ workspace = { path = "../workspace" } futures.workspace = true smallvec.workspace = true + +[dev-dependencies] +editor = { path = "../editor", features = ["test-support"] } diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 311d9a2b88..c9b83d805a 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -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, pub metrics: Option, diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index b3bdc72c91..cf47700dfe 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -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>, // Per app installation app_version: Option>, release_channel: Option<&'static str>, - os_version: Option>, os_name: &'static str, + os_version: Option>, + architecture: &'static str, mixpanel_events_queue: Vec, clickhouse_events_queue: Vec, next_mixpanel_event_id: usize, @@ -63,6 +65,7 @@ struct ClickhouseEventRequestBody { app_version: Option>, os_name: &'static str, os_version: Option>, + architecture: &'static str, release_channel: Option<&'static str>, events: Vec, } @@ -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, }, diff --git a/crates/collab/src/tests.rs b/crates/collab/src/tests.rs index 3c571327eb..b51c5240a8 100644 --- a/crates/collab/src/tests.rs +++ b/crates/collab/src/tests.rs @@ -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: || &[], }); diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index d0625066d5..439ee0786a 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -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::>() ); 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) }); diff --git a/crates/collab_ui/src/incoming_call_notification.rs b/crates/collab_ui/src/incoming_call_notification.rs index ed3e648555..12fad467e3 100644 --- a/crates/collab_ui/src/incoming_call_notification.rs +++ b/crates/collab_ui/src/incoming_call_notification.rs @@ -41,6 +41,7 @@ pub fn init(app_state: &Arc, cx: &mut AppContext) { titlebar: None, center: false, focus: false, + show: true, kind: WindowKind::PopUp, is_movable: false, screen: Some(screen), diff --git a/crates/collab_ui/src/project_shared_notification.rs b/crates/collab_ui/src/project_shared_notification.rs index 155209f470..fea6118bdf 100644 --- a/crates/collab_ui/src/project_shared_notification.rs +++ b/crates/collab_ui/src/project_shared_notification.rs @@ -35,6 +35,7 @@ pub fn init(app_state: &Arc, cx: &mut AppContext) { titlebar: None, center: false, focus: false, + show: true, kind: WindowKind::PopUp, is_movable: false, screen: Some(screen), diff --git a/crates/copilot/src/sign_in.rs b/crates/copilot/src/sign_in.rs index 764a0e4df1..0993a33e6c 100644 --- a/crates/copilot/src/sign_in.rs +++ b/crates/copilot/src/sign_in.rs @@ -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, diff --git a/crates/copilot_button/Cargo.toml b/crates/copilot_button/Cargo.toml index ad3febd68c..50fbaa64ee 100644 --- a/crates/copilot_button/Cargo.toml +++ b/crates/copilot_button/Cargo.toml @@ -23,3 +23,6 @@ workspace = { path = "../workspace" } anyhow.workspace = true smol.workspace = true futures.workspace = true + +[dev-dependencies] +editor = { path = "../editor", features = ["test-support"] } diff --git a/crates/copilot_button/src/copilot_button.rs b/crates/copilot_button/src/copilot_button.rs index 73cd8f6a1d..17d27ca41f 100644 --- a/crates/copilot_button/src/copilot_button.rs +++ b/crates/copilot_button/src/copilot_button.rs @@ -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::(None) .copilot .disabled_globs - .clone() .iter() - .map(|glob| glob.as_str().to_string()) - .collect::>() + .map(|glob| glob.glob().to_string()) + .collect() }); if let Some(path_to_disable) = &path_to_disable { diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index a202a6082c..182efdfdd6 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -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) -> AnyElement { 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 diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index 482923fee7..325883b7c0 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -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" diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index baa1ca9cfd..41fd03bf7f 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -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]>, @@ -4761,6 +4761,80 @@ impl Editor { }); } + pub fn move_to_start_of_paragraph( + &mut self, + _: &MoveToStartOfParagraph, + cx: &mut ViewContext, + ) { + 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, + ) { + 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, + ) { + 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, + ) { + 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) { 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.go_to_hunk_impl(Direction::Next, cx) - } - - fn go_to_prev_hunk(&mut self, _: &GoToPrevHunk, cx: &mut ViewContext) { - self.go_to_hunk_impl(Direction::Prev, cx) - } - - pub fn go_to_hunk_impl(&mut self, direction: Direction, cx: &mut ViewContext) { let snapshot = self .display_map .update(cx, |display_map, cx| display_map.snapshot(cx)); let selection = self.selections.newest::(cx); - fn seek_in_direction( - this: &mut Editor, - snapshot: &DisplaySnapshot, - initial_point: Point, - is_wrapped: bool, - direction: Direction, - cx: &mut ViewContext, - ) -> bool { - let hunks = if direction == Direction::Next { + 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(initial_point.row..u32::MAX, false) - } else { - 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(); - - 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]); - }); - - true - } else { - false - } + .git_diff_hunks_in_range((wrapped_point.row + 1)..u32::MAX), + cx, + ); } + } - 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); + fn go_to_prev_hunk(&mut self, _: &GoToPrevHunk, cx: &mut ViewContext) { + let snapshot = self + .display_map + .update(cx, |display_map, cx| display_map.snapshot(cx)); + let selection = self.selections.newest::(cx); + + 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_rev(0..wrapped_point.row), + cx, + ); + } + } + + fn seek_in_direction( + &mut self, + snapshot: &DisplaySnapshot, + initial_point: Point, + is_wrapped: bool, + hunks: impl Iterator>, + cx: &mut ViewContext, + ) -> 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(); + + 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]); + }); + + 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) { 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() { diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index 5108d27408..7f01834b16 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -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, pub hover_popover_enabled: Option, pub show_completions_on_input: Option, - pub show_scrollbars: Option, + pub scrollbar: Option, +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +pub struct ScrollbarContent { + pub show: Option, + pub git_diff: Option, } impl Setting for EditorSettings { diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 9a21429301..180de155e9 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -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, |_| {}); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 0a17fc8baf..4e5863407f 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -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::(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.; + 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, + }; + + scene.push_quad(Quad { + bounds, + background: Some(color), + border, + corner_radius: style.thumb.corner_radius, + }) } - 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, - }; - - 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 for EditorElement { )); } - let show_scrollbars = match settings::get::(cx).show_scrollbars { - ShowScrollbars::Auto => { - snapshot.has_scrollbar_info() || editor.scroll_manager.scrollbars_visible() + let scrollbar_settings = &settings::get::(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 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)>, scrollbar_row_range: Range, show_scrollbars: bool, + is_singleton: bool, max_row: u32, context_menu: Option<(DisplayPoint, AnyElement)>, code_actions_indicator: Option<(u32, AnyElement)>, diff --git a/crates/editor/src/git.rs b/crates/editor/src/git.rs index 549d74a0b5..3452138126 100644 --- a/crates/editor/src/git.rs +++ b/crates/editor/src/git.rs @@ -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, + display_row_range: Range, 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, 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(), } } diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 988f263337..483fd56cc5 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -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!() } } } diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 6c9bd6cb4f..523a0af964 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -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 diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index f3e8fd7440..6b1ad6c5b2 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -1140,6 +1140,10 @@ impl MultiBuffer { let mut result = Vec::new(); let mut cursor = snapshot.excerpts.cursor::(); 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, - reversed: bool, ) -> impl 'a + Iterator> { let mut cursor = self.excerpts.cursor::(); - 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, + ) -> impl 'a + Iterator> { + let mut cursor = self.excerpts.cursor::(); + + 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::>(), &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::>(), 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::() }) .collect::>() .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!( diff --git a/crates/editor/src/test/editor_test_context.rs b/crates/editor/src/test/editor_test_context.rs index ced99a3f23..e520562ebb 100644 --- a/crates/editor/src/test/editor_test_context.rs +++ b/crates/editor/src/test/editor_test_context.rs @@ -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(&mut self, marked_text: &str) { let expected_ranges = self.ranges(marked_text); let actual_ranges: Vec> = 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(&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>) { 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>, diff --git a/crates/feedback/Cargo.toml b/crates/feedback/Cargo.toml index ae8d0f1569..cd35afbda8 100644 --- a/crates/feedback/Cargo.toml +++ b/crates/feedback/Cargo.toml @@ -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"] } diff --git a/crates/feedback/src/deploy_feedback_button.rs b/crates/feedback/src/deploy_feedback_button.rs index 9133174475..d32a3e5b4c 100644 --- a/crates/feedback/src/deploy_feedback_button.rs +++ b/crates/feedback/src/deploy_feedback_button.rs @@ -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") diff --git a/crates/file_finder/Cargo.toml b/crates/file_finder/Cargo.toml index cae3fa25ca..6f6be7427b 100644 --- a/crates/file_finder/Cargo.toml +++ b/crates/file_finder/Cargo.toml @@ -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"] } diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 5270b694b2..6d2ba115b7 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -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() { diff --git a/crates/git/src/diff.rs b/crates/git/src/diff.rs index b28af26f16..8260dfc98d 100644 --- a/crates/git/src/diff.rs +++ b/crates/git/src/diff.rs @@ -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,66 @@ impl BufferDiff { } } + pub fn is_empty(&self) -> bool { + self.tree.is_empty() + } + pub fn hunks_in_row_range<'a>( &'a self, range: Range, buffer: &'a BufferSnapshot, - reversed: bool, ) -> impl 'a + Iterator> { 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, buffer: &'a BufferSnapshot, - reversed: bool, + ) -> impl 'a + Iterator> { + 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 + }); + + 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::(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 { + end_point.row + }; + + 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, + buffer: &'a BufferSnapshot, ) -> impl 'a + Iterator> { let mut cursor = self.tree.filter::<_, DiffHunkSummary>(move |summary| { let before_start = summary.buffer_range.end.cmp(&range.start, buffer).is_lt(); @@ -95,14 +139,9 @@ impl BufferDiff { }); std::iter::from_fn(move || { - if reversed { - cursor.prev(buffer); - } else { - cursor.next(buffer); - } + 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> { 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> { @@ -279,6 +318,8 @@ pub fn assert_hunks( #[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, &[ diff --git a/crates/go_to_line/Cargo.toml b/crates/go_to_line/Cargo.toml index 441f7ef7e4..b32b4aaf13 100644 --- a/crates/go_to_line/Cargo.toml +++ b/crates/go_to_line/Cargo.toml @@ -18,3 +18,6 @@ workspace = { path = "../workspace" } postage.workspace = true theme = { path = "../theme" } util = { path = "../util" } + +[dev-dependencies] +editor = { path = "../editor", features = ["test-support"] } diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 5def0bed9d..0fafe76942 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -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::>(); + let new_ancestors = + cx.ancestors(focused_view_id).collect::>(); + + // 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 PartialEq for ViewHandle { } } +impl PartialEq for ViewHandle { + fn eq(&self, other: &AnyViewHandle) -> bool { + self.window_id == other.window_id && self.view_id == other.view_id + } +} + impl PartialEq> for ViewHandle { fn eq(&self, other: &WeakViewHandle) -> 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 PartialEq> for AnyViewHandle { fn eq(&self, other: &ViewHandle) -> bool { self.window_id == other.window_id && self.view_id == other.view_id @@ -4198,7 +4275,7 @@ impl Hash for WeakViewHandle { } } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Eq, PartialEq)] pub struct AnyWeakViewHandle { window_id: usize, view_id: usize, diff --git a/crates/gpui/src/app/test_app_context.rs b/crates/gpui/src/app/test_app_context.rs index 4af436a7b8..e956c4ca0d 100644 --- a/crates/gpui/src/app/test_app_context.rs +++ b/crates/gpui/src/app/test_app_context.rs @@ -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 { diff --git a/crates/gpui/src/app/window.rs b/crates/gpui/src/app/window.rs index bd9bd6d2db..24a3686563 100644 --- a/crates/gpui/src/app/window.rs +++ b/crates/gpui/src/app/window.rs @@ -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> { 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 { diff --git a/crates/gpui/src/elements.rs b/crates/gpui/src/elements.rs index 27b01a8db2..779f4b6ec3 100644 --- a/crates/gpui/src/elements.rs +++ b/crates/gpui/src/elements.rs @@ -187,25 +187,23 @@ pub trait Element: 'static { Tooltip::new::(id, text, action, style, self.into_any(), cx) } - fn with_resize_handle( + fn resizable( self, - element_id: usize, - side: Side, - handle_size: f32, - initial_size: f32, - cx: &mut ViewContext, + side: HandleSide, + size: f32, + on_resize: impl 'static + FnMut(&mut V, f32, &mut ViewContext), ) -> Resizable where Self: 'static + Sized, { - Resizable::new::( - self.into_any(), - element_id, - side, - handle_size, - initial_size, - cx, - ) + Resizable::new(self.into_any(), side, size, on_resize) + } + + fn mouse(self, region_id: usize) -> MouseEventHandler + where + Self: Sized, + { + MouseEventHandler::for_child(self.into_any(), region_id) } } diff --git a/crates/gpui/src/elements/list.rs b/crates/gpui/src/elements/list.rs index ca73196c8b..1cf8cc986f 100644 --- a/crates/gpui/src/elements/list.rs +++ b/crates/gpui/src/elements/list.rs @@ -990,7 +990,7 @@ mod tests { _: &mut V, _: &mut ViewContext, ) { - todo!() + unimplemented!() } fn rect_for_text_range( @@ -1003,7 +1003,7 @@ mod tests { _: &V, _: &ViewContext, ) -> Option { - todo!() + unimplemented!() } fn debug(&self, _: RectF, _: &(), _: &(), _: &V, _: &ViewContext) -> serde_json::Value { diff --git a/crates/gpui/src/elements/mouse_event_handler.rs b/crates/gpui/src/elements/mouse_event_handler.rs index ed624922d5..6f2762db66 100644 --- a/crates/gpui/src/elements/mouse_event_handler.rs +++ b/crates/gpui/src/elements/mouse_event_handler.rs @@ -32,10 +32,25 @@ pub struct MouseEventHandler { /// 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 MouseEventHandler { - pub fn new(region_id: usize, cx: &mut ViewContext, render_child: F) -> Self + pub fn for_child(child: impl Element, 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(region_id: usize, cx: &mut ViewContext, render_child: F) -> Self where - D: Element, - F: FnOnce(&mut MouseState, &mut ViewContext) -> D, + E: Element, + F: FnOnce(&mut MouseState, &mut ViewContext) -> E, { let mut mouse_state = cx.mouse_state::(region_id); let child = render_child(&mut mouse_state, cx).into_any(); diff --git a/crates/gpui/src/elements/resizable.rs b/crates/gpui/src/elements/resizable.rs index 0e78cc07fb..da4b3473b3 100644 --- a/crates/gpui/src/elements/resizable.rs +++ b/crates/gpui/src/elements/resizable.rs @@ -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, - custom_dimension: Cell, +pub struct Resizable { + child: AnyElement, + handle_side: HandleSide, + handle_size: f32, + on_resize: Rc)>>, } -pub struct Resizable { - side: Side, - handle_size: f32, - child: AnyElement, - state: Rc, - _state_handle: ElementStateHandle>, -} +const DEFAULT_HANDLE_SIZE: f32 = 4.0; impl Resizable { - pub fn new( + pub fn new( child: AnyElement, - element_id: usize, - side: Side, - handle_size: f32, - initial_size: f32, - cx: &mut ViewContext, + handle_side: HandleSide, + size: f32, + on_resize: impl 'static + FnMut(&mut V, f32, &mut ViewContext), ) -> Self { - let state_handle = cx.element_state::>( - 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 Element for Resizable { - type LayoutState = (); + type LayoutState = SizeConstraint; type PaintState = (); fn layout( @@ -142,7 +115,7 @@ impl Element for Resizable { view: &mut V, cx: &mut LayoutContext, ) -> (Vector2F, Self::LayoutState) { - (self.child.layout(constraint, view, cx), ()) + (self.child.layout(constraint, view, cx), constraint) } fn paint( @@ -150,34 +123,44 @@ impl Element for Resizable { 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, ) -> 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::(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::( + 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, }, diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index ec301939f7..9b4fd7ca51 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -173,6 +173,7 @@ pub struct WindowOptions<'a> { pub titlebar: Option>, pub center: bool, pub focus: bool, + pub show: bool, pub kind: WindowKind, pub is_movable: bool, pub screen: Option>, @@ -222,21 +223,21 @@ impl Bind for WindowBounds { fn bind(&self, statement: &Statement, start_index: i32) -> Result { 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, diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 50fcec52ec..3c82538611 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -614,7 +614,7 @@ impl Window { } if options.focus { native_window.makeKeyAndOrderFront_(nil); - } else { + } else if options.show { native_window.orderFront_(nil); } diff --git a/crates/journal/Cargo.toml b/crates/journal/Cargo.toml index c1d9bde89e..b7cbc62559 100644 --- a/crates/journal/Cargo.toml +++ b/crates/journal/Cargo.toml @@ -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"] } diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index 79121b3799..9722b618f3 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -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 diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index aee646091a..5539d1d941 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1644,10 +1644,17 @@ impl Buffer { cx: &mut ModelContext, ) { 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, - reversed: bool, ) -> impl 'a + Iterator> { - 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, - reversed: bool, ) -> impl 'a + Iterator> { - 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, + ) -> impl 'a + Iterator> { + self.git_diff.hunks_intersecting_range_rev(range, self) } pub fn diagnostics_in_range<'a, T, O>( diff --git a/crates/language/src/diagnostic_set.rs b/crates/language/src/diagnostic_set.rs index 948a7ee394..f269fce88d 100644 --- a/crates/language/src/diagnostic_set.rs +++ b/crates/language/src/diagnostic_set.rs @@ -80,6 +80,10 @@ impl DiagnosticSet { } } + pub fn len(&self) -> usize { + self.diagnostics.summary().count + } + pub fn iter(&self) -> impl Iterator> { self.diagnostics.iter() } diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index b47982819a..c98297c036 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -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, + pub disabled_globs: Vec, } -#[derive(Clone, Serialize, Deserialize, JsonSchema)] +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] pub struct AllLanguageSettingsContent { #[serde(default)] pub features: Option, @@ -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, diff --git a/crates/language_selector/Cargo.toml b/crates/language_selector/Cargo.toml index e7b3d8d4be..f6e213f25f 100644 --- a/crates/language_selector/Cargo.toml +++ b/crates/language_selector/Cargo.toml @@ -20,3 +20,6 @@ settings = { path = "../settings" } util = { path = "../util" } workspace = { path = "../workspace" } anyhow.workspace = true + +[dev-dependencies] +editor = { path = "../editor", features = ["test-support"] } diff --git a/crates/live_kit_client/LiveKitBridge/Package.resolved b/crates/live_kit_client/LiveKitBridge/Package.resolved index 9318cc0184..85ae088565 100644 --- a/crates/live_kit_client/LiveKitBridge/Package.resolved +++ b/crates/live_kit_client/LiveKitBridge/Package.resolved @@ -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" } } ] diff --git a/crates/live_kit_client/LiveKitBridge/Package.swift b/crates/live_kit_client/LiveKitBridge/Package.swift index bdd664c6fb..d7b5c271b9 100644 --- a/crates/live_kit_client/LiveKitBridge/Package.swift +++ b/crates/live_kit_client/LiveKitBridge/Package.swift @@ -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. diff --git a/crates/lsp_log/Cargo.toml b/crates/lsp_log/Cargo.toml index 8741f0a4cf..6f47057b44 100644 --- a/crates/lsp_log/Cargo.toml +++ b/crates/lsp_log/Cargo.toml @@ -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 diff --git a/crates/outline/Cargo.toml b/crates/outline/Cargo.toml index 95272b063e..f4e2b849fa 100644 --- a/crates/outline/Cargo.toml +++ b/crates/outline/Cargo.toml @@ -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"] } diff --git a/crates/picker/Cargo.toml b/crates/picker/Cargo.toml index b723cd788c..54e4b15ad5 100644 --- a/crates/picker/Cargo.toml +++ b/crates/picker/Cargo.toml @@ -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"] } diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index 190f1d96a8..d6578c87ba 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -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 diff --git a/crates/project/src/lsp_glob_set.rs b/crates/project/src/lsp_glob_set.rs deleted file mode 100644 index daac344a0a..0000000000 --- a/crates/project/src/lsp_glob_set.rs +++ /dev/null @@ -1,121 +0,0 @@ -use anyhow::{anyhow, Result}; -use std::path::Path; - -#[derive(Default)] -pub struct LspGlobSet { - patterns: Vec, -} - -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())); - } -} diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 13809622f9..dd53c30d14 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -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), DisconnectedFromHost, Closed, + DeletedEntry(ProjectEntryId), CollaboratorUpdated { old_peer_id: proto::PeerId, new_peer_id: proto::PeerId, @@ -226,7 +227,7 @@ pub enum LanguageServerState { language: Arc, adapter: Arc, server: Arc, - watched_paths: LspGlobSet, + watched_paths: HashMap, simulate_disk_based_diagnostics_completion: Option>, }, } @@ -977,6 +978,9 @@ impl Project { cx: &mut ModelContext, ) -> Option>> { 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, ProjectEntryId), PathChange>, cx: &mut ModelContext, ) { + 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::>(); + 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::>() + .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::>()) + .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 { 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, Arc>>>, +) -> Result, Arc> { + 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, ModelHandle, &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(); + } + } +} diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index 92e8cfcca7..c542d1d13f 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -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, LspSettings>, diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index e7b1a84924..e48ce6258b 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -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::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::>(), + ["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::::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 diff --git a/crates/project/src/search.rs b/crates/project/src/search.rs index ed139c97d3..4b4126fef2 100644 --- a/crates/project/src/search.rs +++ b/crates/project/src/search.rs @@ -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, whole_word: bool, case_sensitive: bool, - files_to_include: Vec, - files_to_exclude: Vec, + files_to_include: Vec, + files_to_exclude: Vec, }, Regex { regex: Regex, @@ -28,8 +29,8 @@ pub enum SearchQuery { multiline: bool, whole_word: bool, case_sensitive: bool, - files_to_include: Vec, - files_to_exclude: Vec, + files_to_include: Vec, + files_to_exclude: Vec, }, } @@ -38,8 +39,8 @@ impl SearchQuery { query: impl ToString, whole_word: bool, case_sensitive: bool, - files_to_include: Vec, - files_to_exclude: Vec, + files_to_include: Vec, + files_to_exclude: Vec, ) -> 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, - files_to_exclude: Vec, + files_to_include: Vec, + files_to_exclude: Vec, ) -> Result { 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::>()?, - message - .files_to_exclude - .split(',') - .map(str::trim) - .filter(|glob_str| !glob_str.is_empty()) - .map(|glob_str| glob::Pattern::new(glob_str)) - .collect::>()?, + 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::>()?, - message - .files_to_exclude - .split(',') - .map(str::trim) - .filter(|glob_str| !glob_str.is_empty()) - .map(|glob_str| glob::Pattern::new(glob_str)) - .collect::>()?, + 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> { + glob_set + .split(',') + .map(str::trim) + .filter(|glob_str| !glob_str.is_empty()) + .map(|glob_str| Ok(Glob::new(glob_str)?.compile_matcher())) + .collect() +} diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 403d893425..d2c035f916 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -120,25 +120,6 @@ pub struct Snapshot { completed_scan_id: usize, } -impl Snapshot { - pub fn repo_for(&self, path: &Path) -> Option { - 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 { - 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 { 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 { + 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 = Vec::new(); let mut removed_statuses: Vec = 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 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 { worktree.entry_for_id(self.0).and_then(|entry| { path.strip_prefix(&entry.path) @@ -338,19 +308,28 @@ impl<'a> From for WorkDirectoryEntry { #[derive(Debug, Clone)] pub struct LocalSnapshot { - ignores_by_parent_abs_path: HashMap, (Arc, bool)>, // (gitignore, needs_update) - // The ProjectEntryId corresponds to the entry for the .git dir - // work_directory_id - git_repositories: TreeMap, - removed_entry_ids: HashMap, - next_entry_id: Arc, 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, bool)>, + /// All of the git repositories in the worktree, indexed by the project entry + /// id of their parent directory. + git_repositories: TreeMap, +} + +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, } #[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>, /// 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, + ) { + 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) { 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 { - self.repository_entries.values() + pub fn repositories(&self) -> impl Iterator, &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 { + 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 { + 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, + ) -> impl 'a + Iterator)> { + let mut containing_repos = Vec::<(&Arc, &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> { @@ -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, - entries: impl IntoIterator, - ignore: Option>, - 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, fs: &dyn Fs) -> Option<()> { let abs_path = self.abs_path.join(&parent_path); let work_dir: Arc = 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::(); - 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 { let mut inodes = TreeSet::default(); @@ -2105,6 +2094,109 @@ impl LocalSnapshot { } } +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; + } + } + + fn insert_entry(&mut self, mut entry: Entry, fs: &dyn Fs) -> Entry { + self.reuse_entry_id(&mut entry); + self.snapshot.insert_entry(entry, fs) + } + + fn populate_dir( + &mut self, + parent_path: Arc, + entries: impl IntoIterator, + ignore: Option>, + 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 { + let abs_parent_path = self.abs_path.join(&parent_path).into(); + self.ignores_by_parent_abs_path + .insert(abs_parent_path, (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 remove_path(&mut self, path: &Path) { + let mut new_entries; + let removed_entries; + { + let mut cursor = self.entries_by_path.cursor::(); + 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 { let contents = fs.load(abs_path).await?; let parent = abs_path.parent().unwrap_or_else(|| Path::new("/")); @@ -2547,12 +2639,13 @@ impl<'a> sum_tree::Dimension<'a, EntrySummary> for PathKey { } struct BackgroundScanner { - snapshot: Mutex, + snapshot: Mutex, fs: Arc, status_updates_tx: UnboundedSender, executor: Arc, refresh_requests_rx: channel::Receiver<(Vec, barrier::Sender)>, prev_state: Mutex, + next_entry_id: Arc, finished_initial_scan: bool, } @@ -2564,6 +2657,7 @@ struct BackgroundScannerState { impl BackgroundScanner { fn new( snapshot: LocalSnapshot, + next_entry_id: Arc, fs: Arc, status_updates_tx: UnboundedSender, executor: Arc, @@ -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": { @@ -4005,371 +4113,6 @@ 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": "", - })); - - 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; - 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()) - ); - - 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()) - ); - }); - - 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(); - }); - - std::fs::write(root.path().join("dir1/.git/random_new_file"), "hello").unwrap(); - tree.flush_fs_events(cx).await; - - assert_eq!( - repo_update_events.lock()[0] - .keys() - .cloned() - .collect::>>(), - vec![Path::new("dir1").into()] - ); - - std::fs::remove_dir_all(root.path().join("dir1/.git")).unwrap(); - tree.flush_fs_events(cx).await; - - tree.read_with(cx, |tree, _cx| { - let tree = tree.as_local().unwrap(); - - assert!(tree.repo_for("dir1/src/b.txt".as_ref()).is_none()); - }); - } - - #[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") - } - - #[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"); - } - - #[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"); - } - - #[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 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 { - 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!({ @@ -5022,4 +4765,478 @@ mod tests { paths } } + + mod git_tests { + use super::*; + use pretty_assertions::assert_eq; + + #[gpui::test] + async fn test_rename_work_directory(cx: &mut TestAppContext) { + let root = temp_tree(json!({ + "projects": { + "project1": { + "a": "", + "b": "", + } + }, + + })); + let root_path = root.path(); + + 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(); + + let repo = git_init(&root_path.join("projects/project1")); + git_add("a", &repo); + git_commit("init", &repo); + std::fs::write(root_path.join("projects/project1/a"), "aa").ok(); + + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) + .await; + + tree.flush_fs_events(cx).await; + + cx.read(|cx| { + let tree = tree.read(cx); + let (work_dir, repo) = tree.repositories().next().unwrap(); + assert_eq!(work_dir.as_ref(), Path::new("projects/project1")); + assert_eq!( + repo.status_for_file(tree, Path::new("projects/project1/a")), + Some(GitFileStatus::Modified) + ); + assert_eq!( + repo.status_for_file(tree, Path::new("projects/project1/b")), + Some(GitFileStatus::Added) + ); + }); + + std::fs::rename( + root_path.join("projects/project1"), + root_path.join("projects/project2"), + ) + .ok(); + tree.flush_fs_events(cx).await; + + cx.read(|cx| { + let tree = tree.read(cx); + let (work_dir, repo) = tree.repositories().next().unwrap(); + assert_eq!(work_dir.as_ref(), Path::new("projects/project2")); + assert_eq!( + repo.status_for_file(tree, Path::new("projects/project2/a")), + Some(GitFileStatus::Modified) + ); + assert_eq!( + repo.status_for_file(tree, Path::new("projects/project2/b")), + Some(GitFileStatus::Added) + ); + }); + } + + #[gpui::test] + async fn test_git_repository_for_path(cx: &mut TestAppContext) { + let root = temp_tree(json!({ + "c.txt": "", + "dir1": { + ".git": {}, + "deps": { + "dep1": { + ".git": {}, + "src": { + "a.txt": "" + } + } + }, + "src": { + "b.txt": "" + } + }, + })); + + 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; + tree.flush_fs_events(cx).await; + + tree.read_with(cx, |tree, _cx| { + let tree = tree.as_local().unwrap(); + + assert!(tree.repository_for_path("c.txt".as_ref()).is_none()); + + let entry = tree.repository_for_path("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()) + ); + + let entry = tree + .repository_for_path("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()) + ); + + let entries = tree.files(false, 0); + + let paths_with_repos = tree + .entries_with_repositories(entries) + .map(|(entry, repo)| { + ( + entry.path.as_ref(), + repo.and_then(|repo| { + repo.work_directory(&tree) + .map(|work_directory| work_directory.0.to_path_buf()) + }), + ) + }) + .collect::>(); + + assert_eq!( + paths_with_repos, + &[ + (Path::new("c.txt"), None), + ( + Path::new("dir1/deps/dep1/src/a.txt"), + Some(Path::new("dir1/deps/dep1").into()) + ), + (Path::new("dir1/src/b.txt"), Some(Path::new("dir1").into())), + ] + ); + }); + + 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(); + }); + + std::fs::write(root.path().join("dir1/.git/random_new_file"), "hello").unwrap(); + tree.flush_fs_events(cx).await; + + assert_eq!( + repo_update_events.lock()[0] + .keys() + .cloned() + .collect::>>(), + vec![Path::new("dir1").into()] + ); + + std::fs::remove_dir_all(root.path().join("dir1/.git")).unwrap(); + tree.flush_fs_events(cx).await; + + tree.read_with(cx, |tree, _cx| { + let tree = tree.as_local().unwrap(); + + assert!(tree + .repository_for_path("dir1/src/b.txt".as_ref()) + .is_none()); + }); + } + + #[gpui::test] + async fn test_git_status(cx: &mut TestAppContext) { + 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) + ); + }); + } + + #[track_caller] + fn git_init(path: &Path) -> git2::Repository { + git2::Repository::init(path).expect("Failed to initialize git repository") + } + + #[track_caller] + fn git_add>(path: P, repo: &git2::Repository) { + let path = path.as_ref(); + 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"); + } + + #[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"); + } + + #[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 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 { + repo.statuses(None) + .unwrap() + .iter() + .map(|status| (status.path().unwrap().to_string(), status.status())) + .collect() + } + } } diff --git a/crates/project_panel/Cargo.toml b/crates/project_panel/Cargo.toml index 6fcdf06d2c..55efc09deb 100644 --- a/crates/project_panel/Cargo.toml +++ b/crates/project_panel/Cargo.toml @@ -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] diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 683ce8ad06..6604472d86 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -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, + fs: Arc, list: UniformListState, visible_entries: Vec<(WorktreeId, Vec)>, last_worktree_root_id: Option, @@ -47,6 +59,9 @@ pub struct ProjectPanel { context_menu: ViewHandle, dragged_entry_destination: Option>, workspace: WeakViewHandle, + has_focus: bool, + width: Option, + pending_serialization: Task>, } #[derive(Copy, Clone)] @@ -110,7 +125,12 @@ actions!( ] ); +pub fn init_settings(cx: &mut AppContext) { + settings::register::(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, } impl ProjectPanel { - pub fn new(workspace: &mut Workspace, cx: &mut ViewContext) -> ViewHandle { + fn new(workspace: &mut Workspace, cx: &mut ViewContext) -> ViewHandle { let project = workspace.project().clone(); let project_panel = cx.add_view(|cx: &mut ViewContext| { 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::(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, + cx: AsyncAppContext, + ) -> Task>> { + 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::(&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) { + 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::(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) { + if !self.has_focus { + self.has_focus = true; + cx.emit(Event::Focus); + } + } + + fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext) { + 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::(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) { + settings::update_settings_file::( + 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::(cx).default_width) + } + + fn set_size(&mut self, size: f32, cx: &mut ViewContext) { + 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) {} + + fn set_active(&mut self, _: bool, _: &mut ViewContext) {} + + fn icon_path(&self) -> &'static str { + "icons/folder_tree_16.svg" + } + + fn icon_tooltip(&self) -> (String, Option>) { + ("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::>(); + assert_eq!(active_items.len(), 1); + let open_editor = active_items + .into_iter() + .next() + .unwrap() + .downcast::() + .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, path: impl AsRef, @@ -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, + expected_path: &str, + cx: &mut TestAppContext, + ) { + cx.read_window(window_id, |cx| { + let workspace = workspace.read(cx); + let worktrees = workspace.worktrees(cx).collect::>(); + 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::>(); + 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, + 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, + 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::>(); + assert!( + open_project_paths.is_empty(), + "Deleted file's buffer should be closed, but got open files: {open_project_paths:?}" + ); + }); + } } diff --git a/crates/project_panel/src/project_panel_settings.rs b/crates/project_panel/src/project_panel_settings.rs new file mode 100644 index 0000000000..1d6c590710 --- /dev/null +++ b/crates/project_panel/src/project_panel_settings.rs @@ -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, + pub dock: Option, + pub default_width: Option, +} + +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::load_via_json_merge(default_value, user_values) + } +} diff --git a/crates/project_symbols/Cargo.toml b/crates/project_symbols/Cargo.toml index 7e23e42b26..85939634ad 100644 --- a/crates/project_symbols/Cargo.toml +++ b/crates/project_symbols/Cargo.toml @@ -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"] } diff --git a/crates/recent_projects/Cargo.toml b/crates/recent_projects/Cargo.toml index d9e7546f34..14f8853c9c 100644 --- a/crates/recent_projects/Cargo.toml +++ b/crates/recent_projects/Cargo.toml @@ -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"] } diff --git a/crates/search/Cargo.toml b/crates/search/Cargo.toml index 14e658e8f8..7ef388f7c0 100644 --- a/crates/search/Cargo.toml +++ b/crates/search/Cargo.toml @@ -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"] } diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 17f86c153c..915957401d 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -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::(SearchOption::CaseSensitive, cx); @@ -571,46 +573,30 @@ impl ProjectSearchView { fn build_search_query(&mut self, cx: &mut ViewContext) -> Option { 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::>() - { - 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::>() - { - 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> { + 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) { 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) { + fn move_focus_to_results(pane: &mut Pane, _: &ToggleFocus, cx: &mut ViewContext) { if let Some(search_view) = pane .active_item() .and_then(|item| item.downcast::()) { 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 { diff --git a/crates/settings/Cargo.toml b/crates/settings/Cargo.toml index 2cb6637ead..1ec0ff4a63 100644 --- a/crates/settings/Cargo.toml +++ b/crates/settings/Cargo.toml @@ -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 diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index dd81b05434..71b3cc635f 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -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 AnySettingValue for SettingValue { fn deserialize_setting(&self, mut json: &serde_json::Value) -> Result { 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::(cx); store.register_setting::(cx); store.register_setting::(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, age: Option, @@ -1170,7 +1143,7 @@ mod tests { key2: String, } - #[derive(Clone, Serialize, Deserialize, JsonSchema)] + #[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] struct MultiKeySettingsJson { key1: Option, key2: Option, @@ -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, pub hour_format: Option, @@ -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, diff --git a/crates/sqlez/src/bindable.rs b/crates/sqlez/src/bindable.rs index 86d69afe5f..4c874c4585 100644 --- a/crates/sqlez/src/bindable.rs +++ b/crates/sqlez/src/bindable.rs @@ -27,7 +27,7 @@ impl StaticColumnCount for bool {} impl Bind for bool { fn bind(&self, statement: &Statement, start_index: i32) -> Result { 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}")) } } diff --git a/crates/sqlez/src/statement.rs b/crates/sqlez/src/statement.rs index 69d5685ba0..de0ad626a5 100644 --- a/crates/sqlez/src/statement.rs +++ b/crates/sqlez/src/statement.rs @@ -236,7 +236,7 @@ impl<'a> Statement<'a> { Ok(str::from_utf8(slice)?) } - pub fn bind(&self, value: T, index: i32) -> Result { + pub fn bind(&self, value: &T, index: i32) -> Result { 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(); diff --git a/crates/sqlez/src/typed_statements.rs b/crates/sqlez/src/typed_statements.rs index 488ee27c0c..d7f25cde51 100644 --- a/crates/sqlez/src/typed_statements.rs +++ b/crates/sqlez/src/typed_statements.rs @@ -29,7 +29,7 @@ impl Connection { query: &str, ) -> Result 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`. @@ -55,7 +55,7 @@ impl Connection { query: &str, ) -> Result Result>> { let mut statement = Statement::prepare(self, query)?; - Ok(move |bindings| statement.with_bindings(bindings)?.rows::()) + Ok(move |bindings| statement.with_bindings(&bindings)?.rows::()) } /// 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::() .context("Maybe row failed") diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 98d85d00e1..576719526d 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -119,6 +119,14 @@ pub fn init(cx: &mut AppContext) { settings::register::(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, pub option_as_meta: Option, pub copy_on_select: Option, + pub dock: Option, + pub default_width: Option, + pub default_height: Option, } impl TerminalSettings { diff --git a/crates/terminal_view/Cargo.toml b/crates/terminal_view/Cargo.toml index 3a25317870..a42d6c550e 100644 --- a/crates/terminal_view/Cargo.toml +++ b/crates/terminal_view/Cargo.toml @@ -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"]} diff --git a/crates/terminal_view/src/terminal_button.rs b/crates/terminal_view/src/terminal_button.rs deleted file mode 100644 index fcb5e7feb3..0000000000 --- a/crates/terminal_view/src/terminal_button.rs +++ /dev/null @@ -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, - popup_menu: ViewHandle, -} - -impl Entity for TerminalButton { - type Event = (); -} - -impl View for TerminalButton { - fn ui_name() -> &'static str { - "TerminalButton" - } - - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - 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::()) - }) - .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::::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::( - 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, cx: &mut ViewContext) -> 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) { - 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::(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) { - cx.notify(); - } -} diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs new file mode 100644 index 0000000000..791a8b21c5 --- /dev/null +++ b/crates/terminal_view/src/terminal_panel.rs @@ -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, + fs: Arc, + workspace: WeakViewHandle, + width: Option, + height: Option, + pending_serialization: Task>, + _subscriptions: Vec, +} + +impl TerminalPanel { + fn new(workspace: &Workspace, cx: &mut ViewContext) -> 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::(window_id) + .map_or(false, |(_, item)| { + item.handle.act_as::(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::(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, + cx: AsyncAppContext, + ) -> Task>> { + 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::(&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::>() + }) + }) + } 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, + event: &pane::Event, + cx: &mut ViewContext, + ) { + 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) { + 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::(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) { + let items = self + .pane + .read(cx) + .items() + .map(|item| item.id()) + .collect::>(); + 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) -> gpui::AnyElement { + ChildView::new(&self.pane, cx).into_any() + } + + fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { + if cx.is_self_focused() { + cx.focus(&self.pane); + } + } +} + +impl Panel for TerminalPanel { + fn position(&self, cx: &WindowContext) -> DockPosition { + match settings::get::(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) { + settings::update_settings_file::(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::(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) { + 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.pane.update(cx, |pane, cx| pane.set_zoomed(zoomed, cx)); + } + + fn set_active(&mut self, active: bool, cx: &mut ViewContext) { + 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>) { + ("Terminal Panel".into(), Some(Box::new(ToggleFocus))) + } + + fn icon_label(&self, cx: &WindowContext) -> Option { + 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, + active_item_id: Option, + width: Option, + height: Option, +} diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 0a7a69bf73..767e3bf4db 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -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); diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index 86bb7f4a26..dcfaf818d1 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -1783,6 +1783,19 @@ impl BufferSnapshot { where D: 'a + TextDimension, A: 'a + IntoIterator, + { + let anchors = anchors.into_iter(); + self.summaries_for_anchors_with_payload::(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 + where + D: 'a + TextDimension, + A: 'a + IntoIterator, { let anchors = anchors.into_iter(); let mut insertion_cursor = self.insertions.cursor::(); @@ -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) }) } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index eb404cdaad..b1c9e9c215 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -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, 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, } -#[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, - pub sidebar_buttons: StatusBarSidebarButtons, + pub panel_buttons: StatusBarPanelButtons, pub diagnostic_summary: Interactive, pub diagnostic_message: Interactive, } #[derive(Deserialize, Default)] -pub struct StatusBarSidebarButtons { +pub struct StatusBarPanelButtons { pub group_left: ContainerStyle, + pub group_bottom: ContainerStyle, pub group_right: ContainerStyle, - pub item: Interactive, - pub badge: ContainerStyle, + pub button: Interactive, } #[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)] diff --git a/crates/theme/src/ui.rs b/crates/theme/src/ui.rs index e4df24c89f..b86bfca8c4 100644 --- a/crates/theme/src/ui.rs +++ b/crates/theme/src/ui.rs @@ -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, - 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, style: FileNameStyle) -> Self { - FileName { - filename, - git_status, - style, - } - } - - pub fn style>(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 gpui::elements::Component for FileName { - fn render(&self, _: &mut V, _: &mut ViewContext) -> AnyElement { - // 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() - } -} diff --git a/crates/theme_selector/Cargo.toml b/crates/theme_selector/Cargo.toml index ac3a85d89a..377f64aad6 100644 --- a/crates/theme_selector/Cargo.toml +++ b/crates/theme_selector/Cargo.toml @@ -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"] } diff --git a/crates/util/src/channel.rs b/crates/util/src/channel.rs index 82181cbacd..274fd576a0 100644 --- a/crates/util/src/channel.rs +++ b/crates/util/src/channel.rs @@ -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, diff --git a/crates/welcome/Cargo.toml b/crates/welcome/Cargo.toml index 65f5151584..ea01f822a7 100644 --- a/crates/welcome/Cargo.toml +++ b/crates/welcome/Cargo.toml @@ -30,3 +30,6 @@ anyhow.workspace = true log.workspace = true schemars.workspace = true serde.workspace = true + +[dev-dependencies] +editor = { path = "../editor", features = ["test-support"] } diff --git a/crates/welcome/src/welcome.rs b/crates/welcome/src/welcome.rs index 000c2fd8d9..cef6f53a6e 100644 --- a/crates/welcome/src/welcome.rs +++ b/crates/welcome/src/welcome.rs @@ -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, 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); diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index beec6a0515..73d4f79399 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -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; +use serde::Deserialize; +use std::rc::Rc; +use theme::ThemeSettings; -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| { - Dock::move_dock(workspace, DockAnchor::Right, true, cx); - }, - ); - cx.add_action( - |workspace: &mut Workspace, _: &AnchorDockBottom, cx: &mut ViewContext| { - Dock::move_dock(workspace, DockAnchor::Bottom, true, cx) - }, - ); - cx.add_action( - |workspace: &mut Workspace, _: &ExpandDock, cx: &mut ViewContext| { - Dock::move_dock(workspace, DockAnchor::Expanded, true, cx) - }, - ); - cx.add_action( - |workspace: &mut Workspace, _: &AddTabToDock, cx: &mut ViewContext| { - 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; - } - - let destination_index = to.read(cx).items_len() + 1; - - Pane::move_item( - workspace, - from.clone(), - to.clone(), - item_id, - destination_index, - cx, - ); - } - }, - ); - cx.add_action( - |workspace: &mut Workspace, _: &RemoveTabFromDock, cx: &mut ViewContext| { - 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; - } - - let destination_index = to.read(cx).items_len() + 1; - - Pane::move_item( - workspace, - from.clone(), - to.clone(), - item_id, - destination_index, - cx, - ); - } - }, - ); +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); + fn size(&self, cx: &WindowContext) -> f32; + fn set_size(&mut self, size: f32, cx: &mut ViewContext); + fn icon_path(&self) -> &'static str; + fn icon_tooltip(&self) -> (String, Option>); + fn icon_label(&self, _: &WindowContext) -> Option { + 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); + fn set_active(&mut self, active: bool, cx: &mut ViewContext); + 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; } -#[derive(Copy, Clone, PartialEq, Eq, Debug)] -pub enum DockPosition { - Shown(DockAnchor), - Hidden(DockAnchor), +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>); + fn icon_label(&self, cx: &WindowContext) -> Option; + fn has_focus(&self, cx: &WindowContext) -> bool; + fn as_any(&self) -> &AnyViewHandle; } -impl Default for DockPosition { - fn default() -> Self { - DockPosition::Hidden(Default::default()) +impl PanelHandle for ViewHandle +where + T: Panel, +{ + fn id(&self) -> usize { + self.id() + } + + fn position(&self, cx: &WindowContext) -> DockPosition { + self.read(cx).position(cx) + } + + fn position_is_valid(&self, position: DockPosition, cx: &WindowContext) -> bool { + self.read(cx).position_is_valid(position) + } + + fn set_position(&self, position: DockPosition, cx: &mut WindowContext) { + self.update(cx, |this, cx| this.set_position(position, cx)) + } + + 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>) { + self.read(cx).icon_tooltip() + } + + fn icon_label(&self, cx: &WindowContext) -> Option { + self.read(cx).icon_label(cx) + } + + fn has_focus(&self, cx: &WindowContext) -> bool { + self.read(cx).has_focus(cx) + } + + fn as_any(&self) -> &AnyViewHandle { + self } } -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", +impl From<&dyn PanelHandle> for AnyViewHandle { + fn from(val: &dyn PanelHandle) -> Self { + val.as_any().clone() } } -impl DockPosition { - pub fn is_visible(&self) -> bool { - match self { - DockPosition::Shown(_) => true, - DockPosition::Hidden(_) => false, - } - } - - pub fn anchor(&self) -> DockAnchor { - match self { - DockPosition::Shown(anchor) | DockPosition::Hidden(anchor) => *anchor, - } - } - - fn hide(self) -> Self { - match self { - DockPosition::Shown(anchor) => DockPosition::Hidden(anchor), - DockPosition::Hidden(_) => self, - } - } - - fn show(self) -> Self { - match self { - DockPosition::Hidden(anchor) => DockPosition::Shown(anchor), - DockPosition::Shown(_) => self, - } - } -} - -pub type DockDefaultItemFactory = - fn(workspace: &mut Workspace, cx: &mut ViewContext) -> Option>; - pub struct Dock { position: DockPosition, - panel_sizes: HashMap, - pane: ViewHandle, - default_item_factory: DockDefaultItemFactory, + panel_entries: Vec, + is_open: bool, + active_panel_index: usize, +} + +#[derive(Clone, Copy, Debug, Deserialize, PartialEq)] +pub enum DockPosition { + Left, + Bottom, + Right, +} + +impl DockPosition { + fn to_label(&self) -> &'static str { + match self { + Self::Left => "left", + Self::Bottom => "bottom", + Self::Right => "right", + } + } + + fn to_resize_handle_side(self) -> HandleSide { + match self { + Self::Left => HandleSide::Right, + Self::Bottom => HandleSide::Top, + Self::Right => HandleSide::Left, + } + } + + pub fn axis(&self) -> Axis { + match self { + Self::Left | Self::Right => Axis::Horizontal, + Self::Bottom => Axis::Vertical, + } + } +} + +struct PanelEntry { + panel: Rc, + context_menu: ViewHandle, + _subscriptions: [Subscription; 2], +} + +pub struct PanelButtons { + dock: ViewHandle, + workspace: WeakViewHandle, } impl Dock { - pub fn new( - default_item_factory: DockDefaultItemFactory, - background_actions: BackgroundActions, - pane_history_timestamp: Arc, - cx: &mut ViewContext, - ) -> Self { - let position = - DockPosition::Hidden(settings::get::(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 { - &self.pane + pub fn is_open(&self) -> bool { + self.is_open } - pub fn visible_pane(&self) -> Option<&ViewHandle> { - 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(&self) -> Option { + self.panel_entries + .iter() + .position(|entry| entry.panel.as_any().is::()) } - pub(crate) fn set_dock_position( - workspace: &mut Workspace, - new_position: DockPosition, - focus: bool, - cx: &mut ViewContext, - ) { - 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 { + 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) { + 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); - } - } - } 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); - } + cx.notify(); } - cx.emit(crate::Event::DockAnchorChanged); - workspace.serialize_workspace(cx); + } + + pub fn toggle_open(&mut self, cx: &mut ViewContext) { + self.set_open(!self.is_open, cx); cx.notify(); } - pub fn hide(workspace: &mut Workspace, cx: &mut ViewContext) { - Self::set_dock_position(workspace, workspace.dock.position.hide(), true, cx); - } - - pub fn show(workspace: &mut Workspace, focus: bool, cx: &mut ViewContext) { - Self::set_dock_position(workspace, workspace.dock.position.show(), focus, cx); - } - - pub fn hide_on_sidebar_shown( - workspace: &mut Workspace, - sidebar_side: SidebarSide, - cx: &mut ViewContext, + pub fn set_panel_zoomed( + &mut self, + panel: &AnyViewHandle, + zoomed: bool, + cx: &mut ViewContext, ) { - if (sidebar_side == SidebarSide::Right && workspace.dock.is_anchored_at(DockAnchor::Right)) - || workspace.dock.is_anchored_at(DockAnchor::Expanded) - { - Self::hide(workspace, cx); + 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 entry.panel.is_zoomed(cx) { + entry.panel.set_zoomed(false, cx); + } + } + + cx.notify(); + } + + pub fn zoom_out(&mut self, cx: &mut ViewContext) { + for entry in &mut self.panel_entries { + if entry.panel.is_zoomed(cx) { + entry.panel.set_zoomed(false, cx); + } } } - pub fn focus_dock(workspace: &mut Workspace, _: &FocusDock, cx: &mut ViewContext) { - Self::set_dock_position(workspace, workspace.dock.position.show(), true, cx); - } - - pub fn hide_dock(workspace: &mut Workspace, _: &HideDock, cx: &mut ViewContext) { - Self::set_dock_position(workspace, workspace.dock.position.hide(), true, cx); - } - - pub fn move_dock( - workspace: &mut Workspace, - new_anchor: DockAnchor, - focus: bool, - cx: &mut ViewContext, - ) { - Self::set_dock_position(workspace, DockPosition::Shown(new_anchor), focus, cx); - } - - pub fn render( - &self, - theme: &Theme, - anchor: DockAnchor, - cx: &mut ViewContext, - ) -> Option> { - 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::( - 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), - ) - }) + pub fn add_panel(&mut self, panel: ViewHandle, cx: &mut ViewContext) { + 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); } - .into_any() + } else if T::should_close_on_event(event) + && this.active_panel().map_or(false, |p| p.id() == panel.id()) + { + this.set_open(false, cx); } - DockAnchor::Expanded => { - enum ExpandedDockWash {} - enum ExpandedDockPane {} + }), + ]; + + 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 remove_panel(&mut self, panel: &ViewHandle, cx: &mut ViewContext) { + if let Some(panel_ix) = self + .panel_entries + .iter() + .position(|entry| entry.panel.id() == panel.id()) + { + 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 panels_len(&self) -> usize { + self.panel_entries.len() + } + + pub fn activate_panel(&mut self, panel_ix: usize, cx: &mut ViewContext) { + 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 active_panel(&self) -> Option<&Rc> { + 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> { + 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 { + 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 { + 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) { + 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 { + 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::(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) -> AnyElement { + 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) { + 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, + workspace: WeakViewHandle, + cx: &mut ViewContext, + ) -> 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) -> AnyElement { + let theme = &settings::get::(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::>(); + 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::::new(0, cx, |_, _| { - Empty::new() + MouseEventHandler::::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::::new(0, cx, |_state, cx| { - ChildView::new(&self.pane, cx) - }) - // Make sure all events directly under the dock pane - // are captured - .capture_all() - .contained() - .with_style(style.maximized), - ) - .into_any() - } - }) - } + .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, + ]; - pub fn position(&self) -> DockPosition { - self.position + 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); + }) + } + }) + .with_tooltip::( + panel_ix, + tooltip, + tooltip_action, + tooltip_style.clone(), + cx, + ), + ) + .with_child(ChildView::new(&context_menu, cx)) + }, + )) + .contained() + .with_style(group_style) + .into_any() + } +} + +impl StatusItemView for PanelButtons { + fn set_active_pane_item( + &mut self, + _: Option<&dyn crate::ItemHandle>, + _: &mut ViewContext, + ) { } } #[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, - }; + use gpui::{ViewContext, WindowContext}; - pub fn default_item_factory( - _workspace: &mut Workspace, - cx: &mut ViewContext, - ) -> Option> { - Some(Box::new(cx.add_view(|_| TestItem::new()))) + pub enum TestPanelEvent { + PositionChanged, + Activated, + Closed, + ZoomIn, + ZoomOut, + Focus, } - #[gpui::test] - async fn test_dock_workspace_infinite_loop(cx: &mut TestAppContext) { - init_test(cx); - - cx.update(|cx| { - register_deserializable_item::(cx); - }); - - let serialized_workspace = SerializedWorkspace { - id: 0, - location: Vec::::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, - ) - }); - - cx.update(|cx| { - Workspace::load_workspace(_workspace.downgrade(), serialized_workspace, Vec::new(), cx) - }) - .await; - - cx.foreground().run_until_parked(); - //Should terminate + pub struct TestPanel { + pub position: DockPosition, + pub zoomed: bool, + pub active: bool, + pub has_focus: bool, + pub size: f32, } - #[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, - } - - 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, - ); - }); - }); - + 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(&self, read: F) -> T - where - F: FnOnce(&Workspace, &ViewContext) -> T, - { - self.workspace.read_with(self.cx, read) - } - - pub fn update_workspace(&mut self, update: F) -> T - where - F: FnOnce(&mut Workspace, &mut ViewContext) -> T, - { - self.workspace.update(self.cx, update) - } - - pub fn sidebar(&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 { - 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 { - 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(&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) - }) - } - - pub fn move_dock(&mut self, anchor: DockAnchor) { - self.update_workspace(|workspace, cx| Dock::move_dock(workspace, anchor, true, cx)); - } - - pub fn hide_dock(&mut self) { - self.cx.dispatch_action(self.window_id, HideDock); - } - - 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)); - } - } - - 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)); - } - } - - pub fn dock_items(&self) -> Vec> { - self.dock_pane(|pane, cx| { - pane.items() - .map(|item| { - item.act_as::(cx) - .expect("Dock Test Context uses TestItems in the dock") - }) - .collect() - }) - } - - 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") - } - - pub fn assert_dock_position(&self, expected_position: DockPosition) { - self.workspace(|workspace, _| assert_eq!(workspace.dock.position, expected_position)); - } - - pub fn assert_sidebar_closed(&self, sidebar_side: SidebarSide) { - assert!(!self.sidebar(sidebar_side, |sidebar, _| sidebar.is_open())); - } - - pub fn assert_workspace_pane_active(&self) { - assert!(self - .center_pane_handle() - .read_with(self.cx, |pane, _| pane.is_active())); - } - - pub fn assert_workspace_pane_inactive(&self) { - assert!(!self - .center_pane_handle() - .read_with(self.cx, |pane, _| pane.is_active())); - } - - pub fn assert_dock_pane_active(&self) { - assert!(self.dock_pane(|pane, _| pane.is_active())) - } - - pub fn assert_dock_pane_inactive(&self) { - assert!(!self.dock_pane(|pane, _| pane.is_active())) - } } - impl<'a> Deref for DockTestContext<'a> { - type Target = gpui::TestAppContext; + impl Entity for TestPanel { + type Event = TestPanelEvent; + } - fn deref(&self) -> &Self::Target { - self.cx + impl View for TestPanel { + fn ui_name() -> &'static str { + "TestPanel" + } + + fn render(&mut self, _: &mut ViewContext<'_, '_, Self>) -> AnyElement { + Empty::new().into_any() + } + + fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { + self.has_focus = true; + cx.emit(TestPanelEvent::Focus); + } + + fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext) { + self.has_focus = false; } } - impl<'a> DerefMut for DockTestContext<'a> { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.cx - } - } - - impl BorrowWindowContext for DockTestContext<'_> { - fn read_with T>(&self, window_id: usize, f: F) -> T { - BorrowWindowContext::read_with(self.cx, window_id, f) + impl Panel for TestPanel { + fn position(&self, _: &gpui::WindowContext) -> super::DockPosition { + self.position } - fn update T>(&mut self, window_id: usize, f: F) -> T { - BorrowWindowContext::update(self.cx, window_id, f) + fn position_is_valid(&self, _: super::DockPosition) -> bool { + true + } + + fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext) { + self.position = position; + cx.emit(TestPanelEvent::PositionChanged); + } + + fn is_zoomed(&self, _: &WindowContext) -> bool { + self.zoomed + } + + fn set_zoomed(&mut self, zoomed: bool, _cx: &mut ViewContext) { + self.zoomed = zoomed; + } + + fn set_active(&mut self, active: bool, _cx: &mut ViewContext) { + self.active = active; + } + + fn size(&self, _: &WindowContext) -> f32 { + self.size + } + + fn set_size(&mut self, size: f32, _: &mut ViewContext) { + self.size = size; + } + + fn icon_path(&self) -> &'static str { + "icons/test_panel.svg" + } + + fn icon_tooltip(&self) -> (String, Option>) { + ("Test Panel".into(), None) + } + + fn should_change_position_on_event(event: &Self::Event) -> bool { + matches!(event, TestPanelEvent::PositionChanged) + } + + fn should_zoom_in_on_event(event: &Self::Event) -> bool { + matches!(event, TestPanelEvent::ZoomIn) + } + + fn should_zoom_out_on_event(event: &Self::Event) -> bool { + matches!(event, TestPanelEvent::ZoomOut) + } + + 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) + } + + fn has_focus(&self, _cx: &WindowContext) -> bool { + self.has_focus + } + + fn is_focus_event(event: &Self::Event) -> bool { + matches!(event, TestPanelEvent::Focus) } } } diff --git a/crates/workspace/src/dock/toggle_dock_button.rs b/crates/workspace/src/dock/toggle_dock_button.rs deleted file mode 100644 index 9ab7a8996b..0000000000 --- a/crates/workspace/src/dock/toggle_dock_button.rs +++ /dev/null @@ -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, -} - -impl ToggleDockButton { - pub fn new(workspace: ViewHandle, cx: &mut ViewContext) -> 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) -> AnyElement { - 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::::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::( - 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::( - 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, - ) { - //Not applicable - } -} diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 16905849a9..c947078015 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -437,7 +437,7 @@ impl ItemHandle for ViewHandle { 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 FollowableItemHandle for ViewHandle { #[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 {} } diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 368afcd16c..26f3d17453 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -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>, activation_history: Vec, is_active: bool, + zoomed: bool, active_item_index: usize, last_focused_view_by_item: HashMap, autoscroll: bool, @@ -158,10 +159,12 @@ pub struct Pane { toolbar: ViewHandle, tab_bar_context_menu: TabBarContextMenu, tab_context_menu: ViewHandle, - docked: Option, _background_actions: BackgroundActions, workspace: WeakViewHandle, has_focus: bool, + can_drop: Rc, &WindowContext) -> bool>, + can_split: bool, + render_tab_bar_buttons: Rc) -> AnyElement>, } pub struct ItemNavHistory { @@ -203,9 +206,9 @@ pub struct NavigationEntry { pub timestamp: usize, } -struct DraggedItem { - item: Box, - pane: WeakViewHandle, +pub struct DraggedItem { + pub handle: Box, + pub pane: WeakViewHandle, } 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, - docked: Option, background_actions: BackgroundActions, next_timestamp: Arc, cx: &mut ViewContext, @@ -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, cx: &mut ViewContext) { - self.docked = docked; + pub fn on_can_drop(&mut self, can_drop: F) + where + F: 'static + Fn(&DragAndDrop, &WindowContext) -> bool, + { + self.can_drop = Rc::new(can_drop); + } + + pub fn set_can_split(&mut self, can_split: bool, cx: &mut ViewContext) { + self.can_split = can_split; + cx.notify(); + } + + pub fn set_render_tab_bar_buttons(&mut self, cx: &mut ViewContext, render: F) + where + F: 'static + Fn(&mut Pane, &mut ViewContext) -> AnyElement, + { + 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, item: Box, @@ -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) { + 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, + cx: &mut ViewContext, ) -> Option>> { - 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, + &mut self, item_id_to_close: usize, - cx: &mut ViewContext, + cx: &mut ViewContext, ) -> Task> { - 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, + cx: &mut ViewContext, ) -> Option>> { - let pane_handle = workspace.active_pane().clone(); - let pane = pane_handle.read(cx); - let active_item_id = pane.items[pane.active_item_index].id(); + if self.items.is_empty() { + return None; + } - let task = Self::close_items(workspace, pane_handle, cx, move |item_id| { - item_id != active_item_id - }); - - 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, + cx: &mut ViewContext, ) -> Option>> { - 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, + cx: &mut ViewContext, ) -> Option>> { - 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, + &mut self, item_id: usize, - cx: &mut ViewContext, + cx: &mut ViewContext, ) -> Task> { - 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, + cx: &mut ViewContext, ) -> Option>> { - 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, + &mut self, item_id: usize, - cx: &mut ViewContext, + cx: &mut ViewContext, ) -> Task> { - 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, + cx: &mut ViewContext, ) -> Option>> { - 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, - cx: &mut ViewContext, + &mut self, + cx: &mut ViewContext, should_close: impl 'static + Fn(usize) -> bool, ) -> Task> { - 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.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.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, + ) -> 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) { let active_item = self .items @@ -1342,7 +1326,7 @@ impl Pane { row.add_child({ enum TabDragReceiver {} let mut receiver = - dragged_item_receiver::(ix, ix, true, None, cx, { + dragged_item_receiver::(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| { 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::(0, filler_index, true, None, cx, |_, _| { + dragged_item_receiver::(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, - ) -> AnyElement { - 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); + pub fn render_tab_bar_button)>( + index: usize, + icon: &'static str, + tooltip: Option<(String, Option>)>, + cx: &mut ViewContext, + on_click: F, + context_menu: Option>, + ) -> AnyElement { + enum TabBarButton {} - 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), - ) - }), + let mut button = MouseEventHandler::::new(index, cx, |mouse_state, cx| { + let theme = &settings::get::(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::(cx).theme.tooltip.clone(); + button = button + .with_tooltip::(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, - ) -> AnyElement { + fn render_blank_pane(&self, theme: &Theme, _cx: &mut ViewContext) -> AnyElement { 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.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::( + 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::(0, 0, false, None, cx, |_, cx| { + dragged_item_receiver::(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.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) { @@ -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)>( - index: usize, - icon: &'static str, - cx: &mut ViewContext, - on_click: F, - context_menu: Option>, -) -> AnyElement { - enum TabBarButton {} - - Stack::new() - .with_child( - MouseEventHandler::::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(&self, data: Option, 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 Element for PaneBackdrop { #[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, 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, cx: &mut TestAppContext) { + async fn test_close_inactive_items(cx: &mut TestAppContext) { init_test(cx); let fs = FakeFs::new(cx.background()); @@ -2483,16 +2409,17 @@ mod tests { set_labeled_items(&workspace, &pane, ["A", "B", "C*", "D", "E"], cx); - workspace.update(cx, |workspace, cx| { - Pane::close_inactive_items(workspace, &CloseInactiveItems, cx); - }); - - deterministic.run_until_parked(); + pane.update(cx, |pane, cx| { + pane.close_inactive_items(&CloseInactiveItems, cx) + }) + .unwrap() + .await + .unwrap(); assert_item_labels(&pane, ["C*"], cx); } #[gpui::test] - async fn test_close_clean_items(deterministic: Arc, cx: &mut TestAppContext) { + async fn test_close_clean_items(cx: &mut TestAppContext) { init_test(cx); let fs = FakeFs::new(cx.background()); @@ -2507,19 +2434,15 @@ mod tests { add_labeled_item(&workspace, &pane, "E", false, cx); assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx); - workspace.update(cx, |workspace, cx| { - Pane::close_clean_items(workspace, &CloseCleanItems, cx); - }); - - deterministic.run_until_parked(); + pane.update(cx, |pane, cx| pane.close_clean_items(&CloseCleanItems, cx)) + .unwrap() + .await + .unwrap(); assert_item_labels(&pane, ["A^", "C*^"], cx); } #[gpui::test] - async fn test_close_items_to_the_left( - deterministic: Arc, - cx: &mut TestAppContext, - ) { + async fn test_close_items_to_the_left(cx: &mut TestAppContext) { init_test(cx); let fs = FakeFs::new(cx.background()); @@ -2529,19 +2452,17 @@ mod tests { set_labeled_items(&workspace, &pane, ["A", "B", "C*", "D", "E"], cx); - workspace.update(cx, |workspace, cx| { - Pane::close_items_to_the_left(workspace, &CloseItemsToTheLeft, cx); - }); - - deterministic.run_until_parked(); + pane.update(cx, |pane, cx| { + pane.close_items_to_the_left(&CloseItemsToTheLeft, cx) + }) + .unwrap() + .await + .unwrap(); assert_item_labels(&pane, ["C*", "D", "E"], cx); } #[gpui::test] - async fn test_close_items_to_the_right( - deterministic: Arc, - cx: &mut TestAppContext, - ) { + async fn test_close_items_to_the_right(cx: &mut TestAppContext) { init_test(cx); let fs = FakeFs::new(cx.background()); @@ -2551,16 +2472,17 @@ mod tests { set_labeled_items(&workspace, &pane, ["A", "B", "C*", "D", "E"], cx); - workspace.update(cx, |workspace, cx| { - Pane::close_items_to_the_right(workspace, &CloseItemsToTheRight, cx); - }); - - deterministic.run_until_parked(); + pane.update(cx, |pane, cx| { + pane.close_items_to_the_right(&CloseItemsToTheRight, cx) + }) + .unwrap() + .await + .unwrap(); assert_item_labels(&pane, ["A", "B", "C*"], cx); } #[gpui::test] - async fn test_close_all_items(deterministic: Arc, cx: &mut TestAppContext) { + async fn test_close_all_items(cx: &mut TestAppContext) { init_test(cx); let fs = FakeFs::new(cx.background()); @@ -2573,11 +2495,10 @@ mod tests { add_labeled_item(&workspace, &pane, "C", false, cx); assert_item_labels(&pane, ["A", "B", "C*"], cx); - workspace.update(cx, |workspace, cx| { - Pane::close_all_items(workspace, &CloseAllItems, cx); - }); - - deterministic.run_until_parked(); + pane.update(cx, |pane, cx| pane.close_all_items(&CloseAllItems, cx)) + .unwrap() + .await + .unwrap(); assert_item_labels(&pane, [], cx); } diff --git a/crates/workspace/src/pane/dragged_item_receiver.rs b/crates/workspace/src/pane/dragged_item_receiver.rs index 532e6bff5c..bb5d3a2464 100644 --- a/crates/workspace/src/pane/dragged_item_receiver.rs +++ b/crates/workspace/src/pane/dragged_item_receiver.rs @@ -12,6 +12,7 @@ use gpui::{ use project::ProjectEntryId; pub fn dragged_item_receiver( + pane: &Pane, region_id: usize, drop_index: usize, allow_same_pane: bool, @@ -24,22 +25,24 @@ where D: Element, F: FnOnce(&mut MouseState, &mut ViewContext) -> D, { - MouseEventHandler::::above(region_id, cx, |state, cx| { + let drag_and_drop = cx.global::>(); + let drag_position = if (pane.can_drop)(drag_and_drop, cx) { + drag_and_drop + .currently_dragged::(cx.window_id()) + .map(|(drag_position, _)| drag_position) + .or_else(|| { + drag_and_drop + .currently_dragged::(cx.window_id()) + .map(|(drag_position, _)| drag_position) + }) + } else { + None + }; + + let mut handler = MouseEventHandler::::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::>() - .currently_dragged::(cx.window_id()) - .map(|(drag_position, _)| drag_position) - .or_else(|| { - cx.global::>() - .currently_dragged::(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::>(); + }); - if drag_and_drop - .currently_dragged::(cx.window_id()) - .is_some() - || drag_and_drop - .currently_dragged::(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::>(); + + if drag_and_drop + .currently_dragged::(cx.window_id()) + .is_some() + || drag_and_drop + .currently_dragged::(cx.window_id()) + .is_some() + { + cx.notify(); + } else { + cx.propagate_event(); + } + }) + } + + handler } pub fn handle_dropped_item( @@ -115,7 +124,7 @@ pub fn handle_dropped_item( let action = if let Some((_, dragged_item)) = drag_and_drop.currently_dragged::(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::(cx.window_id()) { diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index 6e7580a103..5e5a5a98ba 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -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>, active_pane: &ViewHandle, + zoomed: Option<&AnyViewHandle>, app_state: &Arc, cx: &mut ViewContext, ) -> AnyElement { @@ -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>, active_pane: &ViewHandle, + zoomed: Option<&AnyViewHandle>, app_state: &Arc, cx: &mut ViewContext, ) -> AnyElement { @@ -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>, active_pane: &ViewHandle, + zoomed: Option<&AnyViewHandle>, app_state: &Arc, cx: &mut ViewContext, ) -> AnyElement { @@ -388,6 +399,7 @@ impl PaneAxis { follower_state, active_call, active_pane, + zoomed, app_state, cx, ); diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index 4ffae0d7e3..d27818d202 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -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>, - // dock_visible: bool, - // dock_anchor: DockAnchor, // 'Bottom' / 'Right' / 'Expanded' - // dock_pane: Option, // PaneId + // dock_visible: bool, // Deprecated + // dock_anchor: DockAnchor, // Deprecated + // dock_pane: Option, // 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, Option, + 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 { - 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 { 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>( 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; diff --git a/crates/workspace/src/persistence/model.rs b/crates/workspace/src/persistence/model.rs index b73dfa495d..fe7735753f 100644 --- a/crates/workspace/src/persistence/model.rs +++ b/crates/workspace/src/persistence/model.rs @@ -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, pub display: Option, + 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 { + 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, +} + +impl Column for DockData { + fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { + let (visible, next_index) = Option::::column(statement, start_index)?; + let (active_panel, next_index) = Option::::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 { + 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 { - 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 { - 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 - )) - ); - } -} diff --git a/crates/workspace/src/sidebar.rs b/crates/workspace/src/sidebar.rs deleted file mode 100644 index 50148fa211..0000000000 --- a/crates/workspace/src/sidebar.rs +++ /dev/null @@ -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 SidebarItemHandle for ViewHandle -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, - 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, - _subscriptions: [Subscription; 2], -} - -pub struct SidebarButtons { - sidebar: ViewHandle, - workspace: WeakViewHandle, -} - -#[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) { - if open != self.is_open { - self.is_open = open; - cx.notify(); - } - } - - pub fn toggle_open(&mut self, cx: &mut ViewContext) { - if self.is_open {} - self.is_open = !self.is_open; - cx.notify(); - } - - pub fn add_item( - &mut self, - icon_path: &'static str, - tooltip: String, - view: ViewHandle, - cx: &mut ViewContext, - ) { - 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.active_item_ix = item_ix; - cx.notify(); - } - - pub fn toggle_item(&mut self, item_ix: usize, cx: &mut ViewContext) { - 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> { - 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) -> AnyElement { - 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::( - 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, - workspace: WeakViewHandle, - cx: &mut ViewContext, - ) -> 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) -> AnyElement { - 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::>(); - - Flex::row() - .with_children(items.into_iter().enumerate().map( - |(ix, (icon_path, tooltip, item_view))| { - let action = ToggleSidebarItem { - sidebar_side, - item_index: ix, - }; - MouseEventHandler::::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::( - 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, - ) { - } -} diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 28ad294798..1c2b33214e 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -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 = 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, @@ -192,7 +212,6 @@ pub fn init_settings(cx: &mut AppContext) { pub fn init(app_state: Arc, 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, 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, pub build_window_options: fn(Option, Option, &dyn Platform) -> WindowOptions<'static>, - pub initialize_workspace: fn(&mut Workspace, &Arc, &mut ViewContext), - pub dock_default_item_factory: DockDefaultItemFactory, + pub initialize_workspace: + fn(WeakViewHandle, bool, Arc, AsyncAppContext) -> Task>, 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), ContactRequestedJoin(u64), } @@ -460,15 +482,15 @@ pub struct Workspace { remote_entity_subscription: Option, modal: Option, center: PaneGroup, - left_sidebar: ViewHandle, - right_sidebar: ViewHandle, + left_dock: ViewHandle, + bottom_dock: ViewHandle, + right_dock: ViewHandle, panes: Vec>, panes_by_item: HashMap>, active_pane: ViewHandle, last_active_center_pane: Option>, status_bar: ViewHandle, titlebar_item: Option, - dock: Dock, notifications: Vec<(TypeId, usize, Box)>, project: ModelHandle, leader_state: LeaderState, @@ -479,7 +501,7 @@ pub struct Workspace { leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>, database_id: WorkspaceId, app_state: Arc, - _window_subscriptions: [Subscription; 3], + subscriptions: Vec, _apply_leader_updates: Task>, _observe_current_user: Task>, pane_history_timestamp: Arc, @@ -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| { - 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 { - &self.left_sidebar + pub fn left_dock(&self) -> &ViewHandle { + &self.left_dock } - pub fn right_sidebar(&self) -> &ViewHandle { - &self.right_sidebar + pub fn bottom_dock(&self) -> &ViewHandle { + &self.bottom_dock + } + + pub fn right_dock(&self) -> &ViewHandle { + &self.right_dock + } + + pub fn add_panel(&mut self, panel: ViewHandle, cx: &mut ViewContext) { + 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 { @@ -1264,6 +1356,44 @@ impl Workspace { } } + fn zoomed(&self, cx: &WindowContext) -> Option { + 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 { + 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 { + 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) { - 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, + ) { + 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) { - 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, + ) { + 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, - ) { - 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(&mut self, cx: &mut ViewContext) { + for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] { + if let Some(panel_index) = dock.read(cx).panel_index_for_type::() { + 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) { + 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, ) -> Task, 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, ) { - 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, ) -> Option> { - 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, ) -> Option>> { 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 { - self.dock.pane() - } - - fn dock_active(&self) -> bool { - &self.active_pane == self.dock.pane() - } - fn project_remote_id_changed(&mut self, remote_id: Option, cx: &mut ViewContext) { 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, 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> { + 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), ) - } else { - None - }, + .with_children( + self.render_dock(DockPosition::Bottom, cx), + ) + .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::(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() @@ -2921,6 +3065,7 @@ impl View for Workspace { if cx.is_self_focused() { cx.focus(&self.active_pane); } + cx.notify(); } } @@ -3030,28 +3175,11 @@ pub fn open_paths( .await, )) } else { - let contains_directory = - futures::future::join_all(abs_paths.iter().map(|path| app_state.fs.is_file(path))) - .await - .contains(&false); - - cx.update(|cx| { - let task = - Workspace::new_local(abs_paths, app_state.clone(), requesting_window_id, cx); - - cx.spawn(|mut cx| async move { - let (workspace, items) = task.await; - - workspace.update(&mut cx, |workspace, cx| { - if contains_directory { - workspace.toggle_sidebar(SidebarSide::Left, cx); - } - })?; - - anyhow::Ok((workspace, items)) + Ok(cx + .update(|cx| { + Workspace::new_local(abs_paths, app_state.clone(), requesting_window_id, cx) }) - }) - .await + .await) } }) } @@ -3140,12 +3268,17 @@ pub fn join_remote_project( let (_, workspace) = cx.add_window( (app_state.build_window_options)(None, None, cx.platform().as_ref()), - |cx| { - let mut workspace = Workspace::new(0, project, app_state.clone(), cx); - (app_state.initialize_workspace)(&mut workspace, &app_state, cx); - workspace - }, + |cx| Workspace::new(0, project, app_state.clone(), cx), ); + (app_state.initialize_workspace)( + workspace.downgrade(), + false, + app_state.clone(), + cx.clone(), + ) + .await + .log_err(); + workspace.downgrade() }; @@ -3248,7 +3381,10 @@ fn parse_pixel_position_env_var(value: &str) -> Option { #[cfg(test)] mod tests { use super::*; - use crate::item::test::{TestItem, TestItemEvent, TestProjectItem}; + use crate::{ + dock::test::{TestPanel, TestPanelEvent}, + item::test::{TestItem, TestItemEvent, TestProjectItem}, + }; use fs::FakeFs; use gpui::{executor::Deterministic, TestAppContext}; use project::{Project, ProjectEntryId}; @@ -3327,6 +3463,7 @@ mod tests { let project = Project::test(fs, ["root1".as_ref()], cx).await; let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); let worktree_id = project.read_with(cx, |project, cx| { project.worktrees(cx).next().unwrap().read(cx).id() }); @@ -3369,12 +3506,11 @@ mod tests { }); // Close the active item - workspace - .update(cx, |workspace, cx| { - Pane::close_active_item(workspace, &Default::default(), cx).unwrap() - }) - .await - .unwrap(); + pane.update(cx, |pane, cx| { + pane.close_active_item(&Default::default(), cx).unwrap() + }) + .await + .unwrap(); assert_eq!( cx.current_window_title(window_id).as_deref(), Some("one.txt — root1") @@ -3483,18 +3619,13 @@ mod tests { workspace.active_pane().clone() }); - let close_items = workspace.update(cx, |workspace, cx| { - pane.update(cx, |pane, cx| { - pane.activate_item(1, true, true, cx); - assert_eq!(pane.active_item().unwrap().id(), item2.id()); - }); - + let close_items = pane.update(cx, |pane, cx| { + pane.activate_item(1, true, true, cx); + assert_eq!(pane.active_item().unwrap().id(), item2.id()); let item1_id = item1.id(); let item3_id = item3.id(); let item4_id = item4.id(); - Pane::close_items(workspace, pane.clone(), cx, move |id| { - [item1_id, item3_id, item4_id].contains(&id) - }) + pane.close_items(cx, move |id| [item1_id, item3_id, item4_id].contains(&id)) }); cx.foreground().run_until_parked(); @@ -3628,10 +3759,7 @@ mod tests { // once for project entry 0, and once for project entry 2. After those two // prompts, the task should complete. - let close = workspace.update(cx, |workspace, cx| { - Pane::close_items(workspace, left_pane.clone(), cx, |_| true) - }); - + let close = left_pane.update(cx, |pane, cx| pane.close_items(cx, |_| true)); cx.foreground().run_until_parked(); left_pane.read_with(cx, |pane, cx| { assert_eq!( @@ -3665,6 +3793,7 @@ mod tests { let project = Project::test(fs, [], cx).await; let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); + let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); let item = cx.add_view(window_id, |cx| { TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]) @@ -3745,11 +3874,7 @@ mod tests { item.is_dirty = true; }); - workspace - .update(cx, |workspace, cx| { - let pane = workspace.active_pane().clone(); - Pane::close_items(workspace, pane, cx, move |id| id == item_id) - }) + pane.update(cx, |pane, cx| pane.close_items(cx, move |id| id == item_id)) .await .unwrap(); assert!(!cx.has_pending_prompt(window_id)); @@ -3770,10 +3895,8 @@ mod tests { item.read_with(cx, |item, _| assert_eq!(item.save_count, 5)); // Ensure autosave is prevented for deleted files also when closing the buffer. - let _close_items = workspace.update(cx, |workspace, cx| { - let pane = workspace.active_pane().clone(); - Pane::close_items(workspace, pane, cx, move |id| id == item_id) - }); + let _close_items = + pane.update(cx, |pane, cx| pane.close_items(cx, move |id| id == item_id)); deterministic.run_until_parked(); assert!(cx.has_pending_prompt(window_id)); item.read_with(cx, |item, _| assert_eq!(item.save_count, 5)); @@ -3835,6 +3958,175 @@ mod tests { }); } + #[gpui::test] + async fn test_panels(cx: &mut gpui::TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.background()); + + let project = Project::test(fs, [], cx).await; + let (_window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); + + let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| { + // Add panel_1 on the left, panel_2 on the right. + let panel_1 = cx.add_view(|_| TestPanel::new(DockPosition::Left)); + workspace.add_panel(panel_1.clone(), cx); + workspace + .left_dock() + .update(cx, |left_dock, cx| left_dock.set_open(true, cx)); + let panel_2 = cx.add_view(|_| TestPanel::new(DockPosition::Right)); + workspace.add_panel(panel_2.clone(), cx); + workspace + .right_dock() + .update(cx, |right_dock, cx| right_dock.set_open(true, cx)); + + let left_dock = workspace.left_dock(); + assert_eq!( + left_dock.read(cx).active_panel().unwrap().id(), + panel_1.id() + ); + assert_eq!( + left_dock.read(cx).active_panel_size(cx).unwrap(), + panel_1.size(cx) + ); + + left_dock.update(cx, |left_dock, cx| left_dock.resize_active_panel(1337., cx)); + assert_eq!( + workspace.right_dock().read(cx).active_panel().unwrap().id(), + panel_2.id() + ); + + (panel_1, panel_2) + }); + + // Move panel_1 to the right + panel_1.update(cx, |panel_1, cx| { + panel_1.set_position(DockPosition::Right, cx) + }); + + workspace.update(cx, |workspace, cx| { + // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right. + // Since it was the only panel on the left, the left dock should now be closed. + assert!(!workspace.left_dock().read(cx).is_open()); + assert!(workspace.left_dock().read(cx).active_panel().is_none()); + let right_dock = workspace.right_dock(); + assert_eq!( + right_dock.read(cx).active_panel().unwrap().id(), + panel_1.id() + ); + assert_eq!(right_dock.read(cx).active_panel_size(cx).unwrap(), 1337.); + + // Now we move panel_2 to the left + panel_2.set_position(DockPosition::Left, cx); + }); + + workspace.update(cx, |workspace, cx| { + // Since panel_2 was not visible on the right, we don't open the left dock. + assert!(!workspace.left_dock().read(cx).is_open()); + // And the right dock is unaffected in it's displaying of panel_1 + assert!(workspace.right_dock().read(cx).is_open()); + assert_eq!( + workspace.right_dock().read(cx).active_panel().unwrap().id(), + panel_1.id() + ); + }); + + // Move panel_1 back to the left + panel_1.update(cx, |panel_1, cx| { + panel_1.set_position(DockPosition::Left, cx) + }); + + workspace.update(cx, |workspace, cx| { + // Since panel_1 was visible on the right, we open the left dock and make panel_1 active. + let left_dock = workspace.left_dock(); + assert!(left_dock.read(cx).is_open()); + assert_eq!( + left_dock.read(cx).active_panel().unwrap().id(), + panel_1.id() + ); + assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), 1337.); + // And right the dock should be closed as it no longer has any panels. + assert!(!workspace.right_dock().read(cx).is_open()); + + // Now we move panel_1 to the bottom + panel_1.set_position(DockPosition::Bottom, cx); + }); + + workspace.update(cx, |workspace, cx| { + // Since panel_1 was visible on the left, we close the left dock. + assert!(!workspace.left_dock().read(cx).is_open()); + // The bottom dock is sized based on the panel's default size, + // since the panel orientation changed from vertical to horizontal. + let bottom_dock = workspace.bottom_dock(); + assert_eq!( + bottom_dock.read(cx).active_panel_size(cx).unwrap(), + panel_1.size(cx), + ); + // Close bottom dock and move panel_1 back to the left. + bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx)); + panel_1.set_position(DockPosition::Left, cx); + }); + + // Emit activated event on panel 1 + panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Activated)); + + // Now the left dock is open and panel_1 is active and focused. + workspace.read_with(cx, |workspace, cx| { + let left_dock = workspace.left_dock(); + assert!(left_dock.read(cx).is_open()); + assert_eq!( + left_dock.read(cx).active_panel().unwrap().id(), + panel_1.id() + ); + assert!(panel_1.is_focused(cx)); + }); + + // Emit closed event on panel 2, which is not active + panel_2.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed)); + + // Wo don't close the left dock, because panel_2 wasn't the active panel + workspace.read_with(cx, |workspace, cx| { + let left_dock = workspace.left_dock(); + assert!(left_dock.read(cx).is_open()); + assert_eq!( + left_dock.read(cx).active_panel().unwrap().id(), + panel_1.id() + ); + }); + + // Emitting a ZoomIn event shows the panel as zoomed. + panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomIn)); + workspace.read_with(cx, |workspace, cx| { + assert_eq!(workspace.zoomed(cx), Some(panel_1.clone().into_any())); + }); + + // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed. + workspace.update(cx, |_, cx| cx.focus_self()); + workspace.read_with(cx, |workspace, cx| { + assert_eq!(workspace.zoomed(cx), None); + }); + + // When focus is transferred back to the panel, it is zoomed again. + panel_1.update(cx, |_, cx| cx.focus_self()); + workspace.read_with(cx, |workspace, cx| { + assert_eq!(workspace.zoomed(cx), Some(panel_1.clone().into_any())); + }); + + // Emitting a ZoomOut event unzooms the panel. + panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomOut)); + workspace.read_with(cx, |workspace, cx| { + assert_eq!(workspace.zoomed(cx), None); + }); + + // Emit closed event on panel 1, which is active + panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed)); + + // Now the left dock is closed, because panel_1 was the active panel + workspace.read_with(cx, |workspace, cx| { + let left_dock = workspace.left_dock(); + assert!(!left_dock.read(cx).is_open()); + }); + } + pub fn init_test(cx: &mut TestAppContext) { cx.foreground().forbid_parking(); cx.update(|cx| { diff --git a/crates/workspace/src/workspace_settings.rs b/crates/workspace/src/workspace_settings.rs index 41e4796491..4202c00a8d 100644 --- a/crates/workspace/src/workspace_settings.rs +++ b/crates/workspace/src/workspace_settings.rs @@ -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, pub confirm_quit: Option, pub show_call_status_icon: Option, pub autosave: Option, - pub default_dock_anchor: Option, pub git: Option, } @@ -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, @@ -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 { - 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; diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index eb2d693700..31f331ef93 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -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), ] diff --git a/crates/zed/src/menus.rs b/crates/zed/src/menus.rs index 741b61f323..b242b0f183 100644 --- a/crates/zed/src/menus.rs +++ b/crates/zed/src/menus.rs @@ -89,7 +89,18 @@ pub fn menus() -> Vec> { 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![ diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 376ecfc06b..1dfe9c24e5 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -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, cx: &mut gpui::AppContext) { |workspace: &mut Workspace, _: &project_panel::ToggleFocus, cx: &mut ViewContext| { - workspace.toggle_sidebar_item_focus(SidebarSide::Left, 0, cx); + workspace.toggle_panel_focus::(cx); + }, + ); + cx.add_action( + |workspace: &mut Workspace, + _: &terminal_panel::ToggleFocus, + cx: &mut ViewContext| { + workspace.toggle_panel_focus::(cx); }, ); cx.add_global_action({ @@ -252,85 +260,107 @@ pub fn init(app_state: &Arc, cx: &mut gpui::AppContext) { } pub fn initialize_workspace( - workspace: &mut Workspace, - app_state: &Arc, - cx: &mut ViewContext, -) { - 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, + was_deserialized: bool, + app_state: Arc, + cx: AsyncAppContext, +) -> Task> { + cx.spawn(|mut cx| async move { + workspace_handle.update(&mut cx, |workspace, cx| { + let workspace_handle = cx.handle(); + cx.subscribe(&workspace_handle, { + move |workspace, _, event, cx| { + if let workspace::Event::PaneAdded(pane) = event { + pane.update(cx, |pane, cx| { + pane.toolbar().update(cx, |toolbar, cx| { + let breadcrumbs = cx.add_view(|_| Breadcrumbs::new(workspace)); + toolbar.add_item(breadcrumbs, cx); + let buffer_search_bar = cx.add_view(BufferSearchBar::new); + toolbar.add_item(buffer_search_bar, cx); + let project_search_bar = cx.add_view(|_| ProjectSearchBar::new()); + toolbar.add_item(project_search_bar, cx); + let submit_feedback_button = + cx.add_view(|_| SubmitFeedbackButton::new()); + toolbar.add_item(submit_feedback_button, cx); + let feedback_info_text = cx.add_view(|_| FeedbackInfoText::new()); + toolbar.add_item(feedback_info_text, cx); + let lsp_log_item = cx.add_view(|_| { + lsp_log::LspLogToolbarItemView::new(workspace.project().clone()) + }); + toolbar.add_item(lsp_log_item, cx); + }) }); - toolbar.add_item(lsp_log_item, cx); + } + } + }) + .detach(); + + cx.emit(workspace::Event::PaneAdded(workspace.active_pane().clone())); + + let collab_titlebar_item = + cx.add_view(|cx| CollabTitlebarItem::new(workspace, &workspace_handle, cx)); + workspace.set_titlebar_item(collab_titlebar_item.into_any(), cx); + + let copilot = + cx.add_view(|cx| copilot_button::CopilotButton::new(app_state.fs.clone(), cx)); + let diagnostic_summary = + cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx)); + let activity_indicator = activity_indicator::ActivityIndicator::new( + workspace, + app_state.languages.clone(), + cx, + ); + let active_buffer_language = + cx.add_view(|_| language_selector::ActiveBufferLanguage::new(workspace)); + let feedback_button = cx.add_view(|_| { + feedback::deploy_feedback_button::DeployFeedbackButton::new(workspace) + }); + let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new()); + workspace.status_bar().update(cx, |status_bar, cx| { + status_bar.add_left_item(diagnostic_summary, cx); + status_bar.add_left_item(activity_indicator, cx); + status_bar.add_right_item(feedback_button, cx); + status_bar.add_right_item(copilot, cx); + status_bar.add_right_item(active_buffer_language, cx); + status_bar.add_right_item(cursor_position, cx); + }); + + auto_update::notify_of_any_new_update(cx.weak_handle(), cx); + + vim::observe_keystrokes(cx); + + cx.on_window_should_close(|workspace, cx| { + if let Some(task) = workspace.close(&Default::default(), cx) { + task.detach_and_log_err(cx); + } + false + }); + })?; + + let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone()); + let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone()); + let (project_panel, terminal_panel) = futures::try_join!(project_panel, terminal_panel)?; + workspace_handle.update(&mut cx, |workspace, cx| { + let project_panel_position = project_panel.position(cx); + workspace.add_panel(project_panel, cx); + if !was_deserialized + && workspace + .project() + .read(cx) + .visible_worktrees(cx) + .any(|tree| { + tree.read(cx) + .root_entry() + .map_or(false, |entry| entry.is_dir()) }) - }); + { + workspace.toggle_dock(project_panel_position, false, cx); } - } + + workspace.add_panel(terminal_panel, cx) + })?; + Ok(()) }) - .detach(); - - cx.emit(workspace::Event::PaneAdded(workspace.active_pane().clone())); - cx.emit(workspace::Event::PaneAdded(workspace.dock_pane().clone())); - - let collab_titlebar_item = - cx.add_view(|cx| CollabTitlebarItem::new(workspace, &workspace_handle, cx)); - workspace.set_titlebar_item(collab_titlebar_item.into_any(), cx); - - let project_panel = ProjectPanel::new(workspace, cx); - workspace.left_sidebar().update(cx, |sidebar, cx| { - sidebar.add_item( - "icons/folder_tree_16.svg", - "Project Panel".to_string(), - project_panel, - cx, - ) - }); - - let toggle_terminal = cx.add_view(|cx| TerminalButton::new(workspace_handle.clone(), cx)); - let copilot = cx.add_view(|cx| copilot_button::CopilotButton::new(app_state.fs.clone(), cx)); - let diagnostic_summary = - cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx)); - let activity_indicator = - activity_indicator::ActivityIndicator::new(workspace, app_state.languages.clone(), cx); - let active_buffer_language = - cx.add_view(|_| language_selector::ActiveBufferLanguage::new(workspace)); - let feedback_button = - cx.add_view(|_| feedback::deploy_feedback_button::DeployFeedbackButton::new(workspace)); - let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new()); - workspace.status_bar().update(cx, |status_bar, cx| { - status_bar.add_left_item(diagnostic_summary, cx); - status_bar.add_left_item(activity_indicator, cx); - status_bar.add_right_item(toggle_terminal, cx); - status_bar.add_right_item(feedback_button, cx); - status_bar.add_right_item(copilot, cx); - status_bar.add_right_item(active_buffer_language, cx); - status_bar.add_right_item(cursor_position, cx); - }); - - auto_update::notify_of_any_new_update(cx.weak_handle(), cx); - - vim::observe_keystrokes(cx); - - cx.on_window_should_close(|workspace, cx| { - if let Some(task) = workspace.close(&Default::default(), cx) { - task.detach_and_log_err(cx); - } - false - }); } pub fn build_window_options( @@ -348,7 +378,8 @@ pub fn build_window_options( traffic_light_position: Some(vec2f(8., 8.)), }), center: false, - focus: true, + focus: false, + show: false, kind: WindowKind::Normal, is_movable: true, bounds, @@ -687,7 +718,7 @@ mod tests { .unwrap(); workspace_1.update(cx, |workspace, cx| { assert_eq!(workspace.worktrees(cx).count(), 2); - assert!(workspace.left_sidebar().read(cx).is_open()); + assert!(workspace.left_dock().read(cx).is_open()); assert!(workspace.active_pane().is_focused(cx)); }); @@ -730,7 +761,7 @@ mod tests { .collect::>(), &[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::() .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 }) } diff --git a/script/get-preview-channel-changes b/script/get-preview-channel-changes index ac1dcb5b6e..5a0be3ed66 100755 --- a/script/get-preview-channel-changes +++ b/script/get-preview-channel-changes @@ -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; +} diff --git a/styles/src/styleTree/editor.ts b/styles/src/styleTree/editor.ts index 7caa8b1c67..deeff855ff 100644 --- a/styles/src/styleTree/editor.ts +++ b/styles/src/styleTree/editor.ts @@ -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: { diff --git a/styles/src/styleTree/projectPanel.ts b/styles/src/styleTree/projectPanel.ts index 3d06a683ab..08117bf6b0 100644 --- a/styles/src/styleTree/projectPanel.ts +++ b/styles/src/styleTree/projectPanel.ts @@ -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, diff --git a/styles/src/styleTree/statusBar.ts b/styles/src/styleTree/statusBar.ts index c55160c336..eca537c150 100644 --- a/styles/src/styleTree/statusBar.ts +++ b/styles/src/styleTree/statusBar.ts @@ -93,10 +93,11 @@ export default function statusBar(colorScheme: ColorScheme) { }, }, }, - sidebarButtons: { + panelButtons: { groupLeft: {}, + groupBottom: {}, groupRight: {}, - item: { + button: { ...statusContainer, iconSize: 16, iconColor: foreground(layer, "variant"), diff --git a/styles/src/styleTree/workspace.ts b/styles/src/styleTree/workspace.ts index 9d0c4de9f7..737d225784 100644 --- a/styles/src/styleTree/workspace.ts +++ b/styles/src/styleTree/workspace.ts @@ -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), } }