mirror of
https://github.com/zed-industries/zed.git
synced 2024-12-27 15:13:48 +03:00
Merge remote-tracking branch 'origin/main' into zmd
This commit is contained in:
commit
747322a02d
10
.github/pull_request_template.md
vendored
10
.github/pull_request_template.md
vendored
@ -2,4 +2,12 @@
|
||||
|
||||
Release Notes:
|
||||
|
||||
* [[Added foo / Fixed bar / No notes]]
|
||||
Use `N/A` in this section if this item should be skipped in the release notes.
|
||||
|
||||
Add release note lines here:
|
||||
|
||||
* (Added|Fixed|Improved) ... ([#<public_issue_number_if_exists>](https://github.com/zed-industries/community/issues/<public_issue_number_if_exists>)).
|
||||
* ...
|
||||
|
||||
If the release notes are only intended for a specific release channel only, add `(<release_channel>-only)` to the end of the release note line.
|
||||
These will be removed by the person making the release.
|
||||
|
13
Cargo.lock
generated
13
Cargo.lock
generated
@ -2054,7 +2054,6 @@ dependencies = [
|
||||
"futures 0.3.28",
|
||||
"fuzzy",
|
||||
"git",
|
||||
"glob",
|
||||
"gpui",
|
||||
"indoc",
|
||||
"itertools",
|
||||
@ -3458,7 +3457,7 @@ dependencies = [
|
||||
"futures 0.3.28",
|
||||
"fuzzy",
|
||||
"git",
|
||||
"glob",
|
||||
"globset",
|
||||
"gpui",
|
||||
"indoc",
|
||||
"lazy_static",
|
||||
@ -4867,7 +4866,7 @@ dependencies = [
|
||||
"fuzzy",
|
||||
"git",
|
||||
"git2",
|
||||
"glob",
|
||||
"globset",
|
||||
"gpui",
|
||||
"ignore",
|
||||
"itertools",
|
||||
@ -4903,8 +4902,10 @@ dependencies = [
|
||||
name = "project_panel"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"client",
|
||||
"context_menu",
|
||||
"db",
|
||||
"drag_and_drop",
|
||||
"editor",
|
||||
"futures 0.3.28",
|
||||
@ -4913,6 +4914,9 @@ dependencies = [
|
||||
"menu",
|
||||
"postage",
|
||||
"project",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"theme",
|
||||
@ -5965,7 +5969,7 @@ dependencies = [
|
||||
"collections",
|
||||
"editor",
|
||||
"futures 0.3.28",
|
||||
"glob",
|
||||
"globset",
|
||||
"gpui",
|
||||
"language",
|
||||
"log",
|
||||
@ -6127,7 +6131,6 @@ dependencies = [
|
||||
"collections",
|
||||
"fs",
|
||||
"futures 0.3.28",
|
||||
"glob",
|
||||
"gpui",
|
||||
"json_comments",
|
||||
"lazy_static",
|
||||
|
@ -78,7 +78,7 @@ async-trait = { version = "0.1" }
|
||||
ctor = { version = "0.1" }
|
||||
env_logger = { version = "0.9" }
|
||||
futures = { version = "0.3" }
|
||||
glob = { version = "0.3.1" }
|
||||
globset = { version = "0.4" }
|
||||
indoc = "1"
|
||||
isahc = "1.7.2"
|
||||
lazy_static = { version = "1.4.0" }
|
||||
|
@ -39,8 +39,8 @@
|
||||
{
|
||||
"context": "Workspace",
|
||||
"bindings": {
|
||||
"cmd-\\": "workspace::ToggleLeftSidebar",
|
||||
"cmd-k cmd-b": "workspace::ToggleLeftSidebar",
|
||||
"cmd-\\": "workspace::ToggleLeftDock",
|
||||
"cmd-k cmd-b": "workspace::ToggleLeftDock",
|
||||
"cmd-t": "file_finder::Toggle",
|
||||
"cmd-shift-r": "project_symbols::Toggle"
|
||||
}
|
||||
@ -62,9 +62,5 @@
|
||||
"ctrl-f": "project_panel::ExpandSelectedEntry",
|
||||
"ctrl-shift-c": "project_panel::CopyPath"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Dock",
|
||||
"bindings": {}
|
||||
}
|
||||
]
|
||||
|
@ -39,7 +39,8 @@
|
||||
"cmd-shift-n": "workspace::NewWindow",
|
||||
"cmd-o": "workspace::Open",
|
||||
"alt-cmd-o": "projects::OpenRecent",
|
||||
"ctrl-`": "workspace::NewTerminal"
|
||||
"ctrl-~": "workspace::NewTerminal",
|
||||
"ctrl-`": "terminal_panel::ToggleFocus"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -67,10 +68,12 @@
|
||||
"cmd-z": "editor::Undo",
|
||||
"cmd-shift-z": "editor::Redo",
|
||||
"up": "editor::MoveUp",
|
||||
"ctrl-up": "editor::MoveToStartOfParagraph",
|
||||
"pageup": "editor::PageUp",
|
||||
"shift-pageup": "editor::MovePageUp",
|
||||
"home": "editor::MoveToBeginningOfLine",
|
||||
"down": "editor::MoveDown",
|
||||
"ctrl-down": "editor::MoveToEndOfParagraph",
|
||||
"pagedown": "editor::PageDown",
|
||||
"shift-pagedown": "editor::MovePageDown",
|
||||
"end": "editor::MoveToEndOfLine",
|
||||
@ -103,6 +106,8 @@
|
||||
"alt-shift-b": "editor::SelectToPreviousWordStart",
|
||||
"alt-shift-right": "editor::SelectToNextWordEnd",
|
||||
"alt-shift-f": "editor::SelectToNextWordEnd",
|
||||
"ctrl-shift-up": "editor::SelectToStartOfParagraph",
|
||||
"ctrl-shift-down": "editor::SelectToEndOfParagraph",
|
||||
"cmd-shift-up": "editor::SelectToBeginning",
|
||||
"cmd-shift-down": "editor::SelectToEnd",
|
||||
"cmd-a": "editor::SelectAll",
|
||||
@ -225,7 +230,8 @@
|
||||
"cmd-shift-g": "search::SelectPrevMatch",
|
||||
"alt-cmd-c": "search::ToggleCaseSensitive",
|
||||
"alt-cmd-w": "search::ToggleWholeWord",
|
||||
"alt-cmd-r": "search::ToggleRegex"
|
||||
"alt-cmd-r": "search::ToggleRegex",
|
||||
"shift-escape": "workspace::ToggleZoom"
|
||||
}
|
||||
},
|
||||
// Bindings from VS Code
|
||||
@ -367,7 +373,30 @@
|
||||
"workspace::ActivatePane",
|
||||
8
|
||||
],
|
||||
"cmd-b": "workspace::ToggleLeftSidebar",
|
||||
"cmd-b": [
|
||||
"workspace::ToggleLeftDock",
|
||||
{ "focus": true }
|
||||
],
|
||||
"cmd-shift-b": [
|
||||
"workspace::ToggleLeftDock",
|
||||
{ "focus": false }
|
||||
],
|
||||
"cmd-r": [
|
||||
"workspace::ToggleRightDock",
|
||||
{ "focus": true }
|
||||
],
|
||||
"cmd-shift-r": [
|
||||
"workspace::ToggleRightDock",
|
||||
{ "focus": false }
|
||||
],
|
||||
"cmd-j": [
|
||||
"workspace::ToggleBottomDock",
|
||||
{ "focus": true }
|
||||
],
|
||||
"cmd-shift-j": [
|
||||
"workspace::ToggleBottomDock",
|
||||
{ "focus": false }
|
||||
],
|
||||
"cmd-shift-f": "workspace::NewSearch",
|
||||
"cmd-k cmd-t": "theme_selector::Toggle",
|
||||
"cmd-k cmd-s": "zed::OpenKeymap",
|
||||
@ -461,32 +490,6 @@
|
||||
"cmd-enter": "project_search::SearchInNew"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Workspace",
|
||||
"bindings": {
|
||||
"shift-escape": "dock::FocusDock"
|
||||
}
|
||||
},
|
||||
{
|
||||
"bindings": {
|
||||
"cmd-shift-k cmd-shift-right": "dock::AnchorDockRight",
|
||||
"cmd-shift-k cmd-shift-down": "dock::AnchorDockBottom",
|
||||
"cmd-shift-k cmd-shift-up": "dock::ExpandDock"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Pane",
|
||||
"bindings": {
|
||||
"cmd-escape": "dock::AddTabToDock"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Pane && docked",
|
||||
"bindings": {
|
||||
"shift-escape": "dock::HideDock",
|
||||
"cmd-escape": "dock::RemoveTabFromDock"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ProjectPanel",
|
||||
"bindings": {
|
||||
|
@ -68,15 +68,8 @@
|
||||
"cmd-shift-o": "file_finder::Toggle",
|
||||
"cmd-shift-a": "command_palette::Toggle",
|
||||
"cmd-alt-o": "project_symbols::Toggle",
|
||||
"cmd-1": "workspace::ToggleLeftSidebar",
|
||||
"cmd-6": "diagnostics::Deploy",
|
||||
"alt-f12": "dock::FocusDock"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Dock",
|
||||
"bindings": {
|
||||
"alt-f12": "dock::HideDock"
|
||||
"cmd-1": "workspace::ToggleLeftDock",
|
||||
"cmd-6": "diagnostics::Deploy"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -45,18 +45,11 @@
|
||||
{
|
||||
"context": "Workspace",
|
||||
"bindings": {
|
||||
"ctrl-`": "dock::FocusDock",
|
||||
"cmd-k cmd-b": "workspace::ToggleLeftSidebar",
|
||||
"cmd-k cmd-b": "workspace::ToggleLeftDock",
|
||||
"cmd-t": "file_finder::Toggle",
|
||||
"shift-cmd-r": "project_symbols::Toggle",
|
||||
// Currently busted: https://github.com/zed-industries/feedback/issues/898
|
||||
"ctrl-0": "project_panel::ToggleFocus"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Dock",
|
||||
"bindings": {
|
||||
"ctrl-`": "dock::HideDock"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -68,7 +68,7 @@
|
||||
{
|
||||
"context": "Workspace",
|
||||
"bindings": {
|
||||
"cmd-alt-ctrl-d": "workspace::ToggleLeftSidebar",
|
||||
"cmd-alt-ctrl-d": "workspace::ToggleLeftDock",
|
||||
"cmd-t": "file_finder::Toggle",
|
||||
"cmd-shift-t": "project_symbols::Toggle"
|
||||
}
|
||||
@ -83,9 +83,5 @@
|
||||
{
|
||||
"context": "ProjectPanel",
|
||||
"bindings": {}
|
||||
},
|
||||
{
|
||||
"context": "Dock",
|
||||
"bindings": {}
|
||||
}
|
||||
]
|
||||
|
@ -52,7 +52,9 @@
|
||||
// 3. Draw all invisible symbols:
|
||||
// "all"
|
||||
"show_whitespaces": "selection",
|
||||
// Whether to show the scrollbar in the editor.
|
||||
// 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
|
||||
@ -64,7 +66,18 @@
|
||||
// "always"
|
||||
// 4. Never show the scrollbar:
|
||||
// "never"
|
||||
"show_scrollbars": "auto",
|
||||
"show": "auto",
|
||||
// Whether to show git diff indicators in the scrollbar.
|
||||
"git_diff": true
|
||||
},
|
||||
"project_panel": {
|
||||
// Whether to show the git status in the project panel.
|
||||
"git_status": true,
|
||||
// Where to dock project panel. Can be 'left' or 'right'.
|
||||
"dock": "left",
|
||||
// Default width of the project panel.
|
||||
"default_width": 240
|
||||
},
|
||||
// Whether the screen sharing icon is shown in the os status bar.
|
||||
"show_call_status_icon": true,
|
||||
// Whether to use language servers to provide code intelligence.
|
||||
@ -81,16 +94,6 @@
|
||||
// 4. Save when idle for a certain amount of time:
|
||||
// "autosave": { "after_delay": {"milliseconds": 500} },
|
||||
"autosave": "off",
|
||||
// Where to place the dock by default. This setting can take three
|
||||
// values:
|
||||
//
|
||||
// 1. Position the dock attached to the bottom of the workspace
|
||||
// "default_dock_anchor": "bottom"
|
||||
// 2. Position the dock to the right of the workspace like a side panel
|
||||
// "default_dock_anchor": "right"
|
||||
// 3. Position the dock full screen over the entire workspace"
|
||||
// "default_dock_anchor": "expanded"
|
||||
"default_dock_anchor": "bottom",
|
||||
// Whether or not to remove any trailing whitespace from lines of a buffer
|
||||
// before saving it.
|
||||
"remove_trailing_whitespace_on_save": true,
|
||||
@ -181,6 +184,12 @@
|
||||
// }
|
||||
// }
|
||||
"shell": "system",
|
||||
// Where to dock terminals panel. Can be 'left', 'right', 'bottom'.
|
||||
"dock": "bottom",
|
||||
// Default width when the terminal is docked to the left or right.
|
||||
"default_width": 640,
|
||||
// Default height when the terminal is docked to the bottom.
|
||||
"default_height": 320,
|
||||
// What working directory to use when launching the terminal.
|
||||
// May take 4 values:
|
||||
// 1. Use the current file's project directory. Will Fallback to the
|
||||
|
@ -21,3 +21,6 @@ workspace = { path = "../workspace" }
|
||||
|
||||
futures.workspace = true
|
||||
smallvec.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { path = "../editor", features = ["test-support"] }
|
||||
|
@ -339,7 +339,7 @@ pub struct TelemetrySettings {
|
||||
pub metrics: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Default, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct TelemetrySettingsContent {
|
||||
pub diagnostics: Option<bool>,
|
||||
pub metrics: Option<bool>,
|
||||
|
@ -10,6 +10,7 @@ use parking_lot::Mutex;
|
||||
use serde::Serialize;
|
||||
use serde_json::json;
|
||||
use std::{
|
||||
env,
|
||||
io::Write,
|
||||
mem,
|
||||
path::PathBuf,
|
||||
@ -33,8 +34,9 @@ struct TelemetryState {
|
||||
installation_id: Option<Arc<str>>, // Per app installation
|
||||
app_version: Option<Arc<str>>,
|
||||
release_channel: Option<&'static str>,
|
||||
os_version: Option<Arc<str>>,
|
||||
os_name: &'static str,
|
||||
os_version: Option<Arc<str>>,
|
||||
architecture: &'static str,
|
||||
mixpanel_events_queue: Vec<MixpanelEvent>,
|
||||
clickhouse_events_queue: Vec<ClickhouseEventWrapper>,
|
||||
next_mixpanel_event_id: usize,
|
||||
@ -63,6 +65,7 @@ struct ClickhouseEventRequestBody {
|
||||
app_version: Option<Arc<str>>,
|
||||
os_name: &'static str,
|
||||
os_version: Option<Arc<str>>,
|
||||
architecture: &'static str,
|
||||
release_channel: Option<&'static str>,
|
||||
events: Vec<ClickhouseEventWrapper>,
|
||||
}
|
||||
@ -153,12 +156,14 @@ impl Telemetry {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
// TODO: Replace all hardware stuff with nested SystemSpecs json
|
||||
let this = Arc::new(Self {
|
||||
http_client: client,
|
||||
executor: cx.background().clone(),
|
||||
state: Mutex::new(TelemetryState {
|
||||
os_version: platform.os_version().ok().map(|v| v.to_string().into()),
|
||||
os_name: platform.os_name().into(),
|
||||
os_version: platform.os_version().ok().map(|v| v.to_string().into()),
|
||||
architecture: env::consts::ARCH,
|
||||
app_version: platform.app_version().ok().map(|v| v.to_string().into()),
|
||||
release_channel,
|
||||
installation_id: None,
|
||||
@ -451,6 +456,8 @@ impl Telemetry {
|
||||
app_version: state.app_version.clone(),
|
||||
os_name: state.os_name,
|
||||
os_version: state.os_version.clone(),
|
||||
architecture: state.architecture,
|
||||
|
||||
release_channel: state.release_channel,
|
||||
events,
|
||||
},
|
||||
|
@ -192,8 +192,7 @@ impl TestServer {
|
||||
languages: Arc::new(LanguageRegistry::test()),
|
||||
fs: fs.clone(),
|
||||
build_window_options: |_, _, _| Default::default(),
|
||||
initialize_workspace: |_, _, _| unimplemented!(),
|
||||
dock_default_item_factory: |_, _| None,
|
||||
initialize_workspace: |_, _, _, _| unimplemented!(),
|
||||
background_actions: || &[],
|
||||
});
|
||||
|
||||
|
@ -2437,7 +2437,7 @@ async fn test_git_diff_base_change(
|
||||
buffer_local_a.read_with(cx_a, |buffer, _| {
|
||||
assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
|
||||
git::diff::assert_hunks(
|
||||
buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
|
||||
buffer.snapshot().git_diff_hunks_in_row_range(0..4),
|
||||
&buffer,
|
||||
&diff_base,
|
||||
&[(1..2, "", "two\n")],
|
||||
@ -2457,7 +2457,7 @@ async fn test_git_diff_base_change(
|
||||
buffer_remote_a.read_with(cx_b, |buffer, _| {
|
||||
assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
|
||||
git::diff::assert_hunks(
|
||||
buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
|
||||
buffer.snapshot().git_diff_hunks_in_row_range(0..4),
|
||||
&buffer,
|
||||
&diff_base,
|
||||
&[(1..2, "", "two\n")],
|
||||
@ -2481,7 +2481,7 @@ async fn test_git_diff_base_change(
|
||||
assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
|
||||
|
||||
git::diff::assert_hunks(
|
||||
buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
|
||||
buffer.snapshot().git_diff_hunks_in_row_range(0..4),
|
||||
&buffer,
|
||||
&diff_base,
|
||||
&[(2..3, "", "three\n")],
|
||||
@ -2492,7 +2492,7 @@ async fn test_git_diff_base_change(
|
||||
buffer_remote_a.read_with(cx_b, |buffer, _| {
|
||||
assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
|
||||
git::diff::assert_hunks(
|
||||
buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
|
||||
buffer.snapshot().git_diff_hunks_in_row_range(0..4),
|
||||
&buffer,
|
||||
&diff_base,
|
||||
&[(2..3, "", "three\n")],
|
||||
@ -2535,7 +2535,7 @@ async fn test_git_diff_base_change(
|
||||
buffer_local_b.read_with(cx_a, |buffer, _| {
|
||||
assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
|
||||
git::diff::assert_hunks(
|
||||
buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
|
||||
buffer.snapshot().git_diff_hunks_in_row_range(0..4),
|
||||
&buffer,
|
||||
&diff_base,
|
||||
&[(1..2, "", "two\n")],
|
||||
@ -2555,7 +2555,7 @@ async fn test_git_diff_base_change(
|
||||
buffer_remote_b.read_with(cx_b, |buffer, _| {
|
||||
assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
|
||||
git::diff::assert_hunks(
|
||||
buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
|
||||
buffer.snapshot().git_diff_hunks_in_row_range(0..4),
|
||||
&buffer,
|
||||
&diff_base,
|
||||
&[(1..2, "", "two\n")],
|
||||
@ -2583,12 +2583,12 @@ async fn test_git_diff_base_change(
|
||||
"{:?}",
|
||||
buffer
|
||||
.snapshot()
|
||||
.git_diff_hunks_in_row_range(0..4, false)
|
||||
.git_diff_hunks_in_row_range(0..4)
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
|
||||
git::diff::assert_hunks(
|
||||
buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
|
||||
buffer.snapshot().git_diff_hunks_in_row_range(0..4),
|
||||
&buffer,
|
||||
&diff_base,
|
||||
&[(2..3, "", "three\n")],
|
||||
@ -2599,7 +2599,7 @@ async fn test_git_diff_base_change(
|
||||
buffer_remote_b.read_with(cx_b, |buffer, _| {
|
||||
assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
|
||||
git::diff::assert_hunks(
|
||||
buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
|
||||
buffer.snapshot().git_diff_hunks_in_row_range(0..4),
|
||||
&buffer,
|
||||
&diff_base,
|
||||
&[(2..3, "", "three\n")],
|
||||
@ -2688,6 +2688,7 @@ async fn test_git_branch_name(
|
||||
});
|
||||
|
||||
let project_remote_c = client_c.build_remote_project(project_id, cx_c).await;
|
||||
deterministic.run_until_parked();
|
||||
project_remote_c.read_with(cx_c, |project, cx| {
|
||||
assert_branch(Some("branch-2"), project, cx)
|
||||
});
|
||||
|
@ -41,6 +41,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
titlebar: None,
|
||||
center: false,
|
||||
focus: false,
|
||||
show: true,
|
||||
kind: WindowKind::PopUp,
|
||||
is_movable: false,
|
||||
screen: Some(screen),
|
||||
|
@ -35,6 +35,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
titlebar: None,
|
||||
center: false,
|
||||
focus: false,
|
||||
show: true,
|
||||
kind: WindowKind::PopUp,
|
||||
is_movable: false,
|
||||
screen: Some(screen),
|
||||
|
@ -73,6 +73,7 @@ fn create_copilot_auth_window(
|
||||
titlebar: None,
|
||||
center: true,
|
||||
focus: true,
|
||||
show: true,
|
||||
kind: WindowKind::Normal,
|
||||
is_movable: true,
|
||||
screen: None,
|
||||
|
@ -23,3 +23,6 @@ workspace = { path = "../workspace" }
|
||||
anyhow.workspace = true
|
||||
smol.workspace = true
|
||||
futures.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { path = "../editor", features = ["test-support"] }
|
||||
|
@ -66,8 +66,8 @@ impl View for CopilotButton {
|
||||
let style = theme
|
||||
.workspace
|
||||
.status_bar
|
||||
.sidebar_buttons
|
||||
.item
|
||||
.panel_buttons
|
||||
.button
|
||||
.style_for(state, active);
|
||||
|
||||
Flex::row()
|
||||
@ -335,10 +335,9 @@ async fn configure_disabled_globs(
|
||||
.get::<AllLanguageSettings>(None)
|
||||
.copilot
|
||||
.disabled_globs
|
||||
.clone()
|
||||
.iter()
|
||||
.map(|glob| glob.as_str().to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.map(|glob| glob.glob().to_string())
|
||||
.collect()
|
||||
});
|
||||
|
||||
if let Some(path_to_disable) = &path_to_disable {
|
||||
|
@ -33,7 +33,7 @@ use theme::ThemeSettings;
|
||||
use util::TryFutureExt;
|
||||
use workspace::{
|
||||
item::{BreadcrumbText, Item, ItemEvent, ItemHandle},
|
||||
ItemNavHistory, Pane, ToolbarItemLocation, Workspace,
|
||||
ItemNavHistory, Pane, PaneBackdrop, ToolbarItemLocation, Workspace,
|
||||
};
|
||||
|
||||
actions!(diagnostics, [Deploy]);
|
||||
@ -90,10 +90,14 @@ impl View for ProjectDiagnosticsEditor {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
if self.path_states.is_empty() {
|
||||
let theme = &theme::current(cx).project_diagnostics;
|
||||
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,7 +165,12 @@ impl ProjectDiagnosticsEditor {
|
||||
editor.set_vertical_scroll_margin(5, cx);
|
||||
editor
|
||||
});
|
||||
cx.subscribe(&editor, |_, _, event, cx| cx.emit(event.clone()))
|
||||
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);
|
||||
|
@ -49,8 +49,7 @@ workspace = { path = "../workspace" }
|
||||
aho-corasick = "0.7"
|
||||
anyhow.workspace = true
|
||||
futures.workspace = true
|
||||
glob.workspace = true
|
||||
indoc.workspace = true
|
||||
indoc = "1.0.4"
|
||||
itertools = "0.10"
|
||||
lazy_static.workspace = true
|
||||
log.workspace = true
|
||||
@ -82,7 +81,6 @@ workspace = { path = "../workspace", features = ["test-support"] }
|
||||
|
||||
ctor.workspace = true
|
||||
env_logger.workspace = true
|
||||
glob.workspace = true
|
||||
rand.workspace = true
|
||||
unindent.workspace = true
|
||||
tree-sitter = "0.20"
|
||||
|
@ -20,6 +20,7 @@ mod editor_tests;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub mod test;
|
||||
|
||||
use ::git::diff::DiffHunk;
|
||||
use aho_corasick::AhoCorasick;
|
||||
use anyhow::{anyhow, Result};
|
||||
use blink_manager::BlinkManager;
|
||||
@ -215,6 +216,8 @@ actions!(
|
||||
MoveToNextSubwordEnd,
|
||||
MoveToBeginningOfLine,
|
||||
MoveToEndOfLine,
|
||||
MoveToStartOfParagraph,
|
||||
MoveToEndOfParagraph,
|
||||
MoveToBeginning,
|
||||
MoveToEnd,
|
||||
SelectUp,
|
||||
@ -225,6 +228,8 @@ actions!(
|
||||
SelectToPreviousSubwordStart,
|
||||
SelectToNextWordEnd,
|
||||
SelectToNextSubwordEnd,
|
||||
SelectToStartOfParagraph,
|
||||
SelectToEndOfParagraph,
|
||||
SelectToBeginning,
|
||||
SelectToEnd,
|
||||
SelectAll,
|
||||
@ -336,6 +341,8 @@ pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(Editor::move_to_next_subword_end);
|
||||
cx.add_action(Editor::move_to_beginning_of_line);
|
||||
cx.add_action(Editor::move_to_end_of_line);
|
||||
cx.add_action(Editor::move_to_start_of_paragraph);
|
||||
cx.add_action(Editor::move_to_end_of_paragraph);
|
||||
cx.add_action(Editor::move_to_beginning);
|
||||
cx.add_action(Editor::move_to_end);
|
||||
cx.add_action(Editor::select_up);
|
||||
@ -348,6 +355,8 @@ pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(Editor::select_to_next_subword_end);
|
||||
cx.add_action(Editor::select_to_beginning_of_line);
|
||||
cx.add_action(Editor::select_to_end_of_line);
|
||||
cx.add_action(Editor::select_to_start_of_paragraph);
|
||||
cx.add_action(Editor::select_to_end_of_paragraph);
|
||||
cx.add_action(Editor::select_to_beginning);
|
||||
cx.add_action(Editor::select_to_end);
|
||||
cx.add_action(Editor::select_all);
|
||||
@ -524,15 +533,6 @@ pub struct EditorSnapshot {
|
||||
ongoing_scroll: OngoingScroll,
|
||||
}
|
||||
|
||||
impl EditorSnapshot {
|
||||
fn has_scrollbar_info(&self) -> bool {
|
||||
self.buffer_snapshot
|
||||
.git_diff_hunks_in_range(0..self.max_point().row(), false)
|
||||
.next()
|
||||
.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct SelectionHistoryEntry {
|
||||
selections: Arc<[Selection<Anchor>]>,
|
||||
@ -4761,6 +4761,80 @@ impl Editor {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn move_to_start_of_paragraph(
|
||||
&mut self,
|
||||
_: &MoveToStartOfParagraph,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if matches!(self.mode, EditorMode::SingleLine) {
|
||||
cx.propagate_action();
|
||||
return;
|
||||
}
|
||||
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
selection.collapse_to(
|
||||
movement::start_of_paragraph(map, selection.head()),
|
||||
SelectionGoal::None,
|
||||
)
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
pub fn move_to_end_of_paragraph(
|
||||
&mut self,
|
||||
_: &MoveToEndOfParagraph,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if matches!(self.mode, EditorMode::SingleLine) {
|
||||
cx.propagate_action();
|
||||
return;
|
||||
}
|
||||
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
selection.collapse_to(
|
||||
movement::end_of_paragraph(map, selection.head()),
|
||||
SelectionGoal::None,
|
||||
)
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
pub fn select_to_start_of_paragraph(
|
||||
&mut self,
|
||||
_: &SelectToStartOfParagraph,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if matches!(self.mode, EditorMode::SingleLine) {
|
||||
cx.propagate_action();
|
||||
return;
|
||||
}
|
||||
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_heads_with(|map, head, _| {
|
||||
(movement::start_of_paragraph(map, head), SelectionGoal::None)
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
pub fn select_to_end_of_paragraph(
|
||||
&mut self,
|
||||
_: &SelectToEndOfParagraph,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if matches!(self.mode, EditorMode::SingleLine) {
|
||||
cx.propagate_action();
|
||||
return;
|
||||
}
|
||||
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_heads_with(|map, head, _| {
|
||||
(movement::end_of_paragraph(map, head), SelectionGoal::None)
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
pub fn move_to_beginning(&mut self, _: &MoveToBeginning, cx: &mut ViewContext<Self>) {
|
||||
if matches!(self.mode, EditorMode::SingleLine) {
|
||||
cx.propagate_action();
|
||||
@ -5569,37 +5643,69 @@ impl Editor {
|
||||
}
|
||||
|
||||
fn go_to_hunk(&mut self, _: &GoToHunk, cx: &mut ViewContext<Self>) {
|
||||
self.go_to_hunk_impl(Direction::Next, cx)
|
||||
}
|
||||
|
||||
fn go_to_prev_hunk(&mut self, _: &GoToPrevHunk, cx: &mut ViewContext<Self>) {
|
||||
self.go_to_hunk_impl(Direction::Prev, cx)
|
||||
}
|
||||
|
||||
pub fn go_to_hunk_impl(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
|
||||
let snapshot = self
|
||||
.display_map
|
||||
.update(cx, |display_map, cx| display_map.snapshot(cx));
|
||||
let selection = self.selections.newest::<Point>(cx);
|
||||
|
||||
if !self.seek_in_direction(
|
||||
&snapshot,
|
||||
selection.head(),
|
||||
false,
|
||||
snapshot
|
||||
.buffer_snapshot
|
||||
.git_diff_hunks_in_range((selection.head().row + 1)..u32::MAX),
|
||||
cx,
|
||||
) {
|
||||
let wrapped_point = Point::zero();
|
||||
self.seek_in_direction(
|
||||
&snapshot,
|
||||
wrapped_point,
|
||||
true,
|
||||
snapshot
|
||||
.buffer_snapshot
|
||||
.git_diff_hunks_in_range((wrapped_point.row + 1)..u32::MAX),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn go_to_prev_hunk(&mut self, _: &GoToPrevHunk, cx: &mut ViewContext<Self>) {
|
||||
let snapshot = self
|
||||
.display_map
|
||||
.update(cx, |display_map, cx| display_map.snapshot(cx));
|
||||
let selection = self.selections.newest::<Point>(cx);
|
||||
|
||||
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(
|
||||
this: &mut Editor,
|
||||
&mut self,
|
||||
snapshot: &DisplaySnapshot,
|
||||
initial_point: Point,
|
||||
is_wrapped: bool,
|
||||
direction: Direction,
|
||||
hunks: impl Iterator<Item = DiffHunk<u32>>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> bool {
|
||||
let hunks = if direction == Direction::Next {
|
||||
snapshot
|
||||
.buffer_snapshot
|
||||
.git_diff_hunks_in_range(initial_point.row..u32::MAX, false)
|
||||
} else {
|
||||
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))
|
||||
@ -5613,7 +5719,7 @@ impl Editor {
|
||||
.dedup();
|
||||
|
||||
if let Some(hunk) = hunks.next() {
|
||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
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]);
|
||||
@ -5625,15 +5731,6 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn go_to_definition(&mut self, _: &GoToDefinition, cx: &mut ViewContext<Self>) {
|
||||
self.go_to_definition_of_kind(GotoDefinitionKind::Symbol, cx);
|
||||
}
|
||||
@ -7104,6 +7201,7 @@ pub enum Event {
|
||||
BufferEdited,
|
||||
Edited,
|
||||
Reparsed,
|
||||
Focused,
|
||||
Blurred,
|
||||
DirtyChanged,
|
||||
Saved,
|
||||
@ -7157,6 +7255,7 @@ impl View for Editor {
|
||||
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
if cx.is_self_focused() {
|
||||
let focused_event = EditorFocused(cx.handle());
|
||||
cx.emit(Event::Focused);
|
||||
cx.emit_global(focused_event);
|
||||
}
|
||||
if let Some(rename) = self.pending_rename.as_ref() {
|
||||
|
@ -7,25 +7,36 @@ pub struct EditorSettings {
|
||||
pub cursor_blink: bool,
|
||||
pub hover_popover_enabled: bool,
|
||||
pub show_completions_on_input: bool,
|
||||
pub show_scrollbars: ShowScrollbars,
|
||||
pub scrollbar: Scrollbar,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
pub struct Scrollbar {
|
||||
pub show: ShowScrollbar,
|
||||
pub git_diff: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ShowScrollbars {
|
||||
#[default]
|
||||
pub enum ShowScrollbar {
|
||||
Auto,
|
||||
System,
|
||||
Always,
|
||||
Never,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct EditorSettingsContent {
|
||||
pub cursor_blink: Option<bool>,
|
||||
pub hover_popover_enabled: Option<bool>,
|
||||
pub show_completions_on_input: Option<bool>,
|
||||
pub show_scrollbars: Option<ShowScrollbars>,
|
||||
pub scrollbar: Option<ScrollbarContent>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
pub struct ScrollbarContent {
|
||||
pub show: Option<ShowScrollbar>,
|
||||
pub git_diff: Option<bool>,
|
||||
}
|
||||
|
||||
impl Setting for EditorSettings {
|
||||
|
@ -1243,6 +1243,118 @@ fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
let mut cx = EditorTestContext::new(cx);
|
||||
|
||||
let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache()));
|
||||
cx.simulate_window_resize(cx.window_id, vec2f(100., 4. * line_height));
|
||||
|
||||
cx.set_state(
|
||||
&r#"ˇone
|
||||
two
|
||||
|
||||
three
|
||||
fourˇ
|
||||
five
|
||||
|
||||
six"#
|
||||
.unindent(),
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx));
|
||||
cx.assert_editor_state(
|
||||
&r#"one
|
||||
two
|
||||
ˇ
|
||||
three
|
||||
four
|
||||
five
|
||||
ˇ
|
||||
six"#
|
||||
.unindent(),
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx));
|
||||
cx.assert_editor_state(
|
||||
&r#"one
|
||||
two
|
||||
|
||||
three
|
||||
four
|
||||
five
|
||||
ˇ
|
||||
sixˇ"#
|
||||
.unindent(),
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx));
|
||||
cx.assert_editor_state(
|
||||
&r#"ˇone
|
||||
two
|
||||
|
||||
three
|
||||
four
|
||||
five
|
||||
|
||||
sixˇ"#
|
||||
.unindent(),
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx));
|
||||
cx.assert_editor_state(
|
||||
&r#"ˇone
|
||||
two
|
||||
ˇ
|
||||
three
|
||||
four
|
||||
five
|
||||
|
||||
six"#
|
||||
.unindent(),
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
|
||||
cx.assert_editor_state(
|
||||
&r#"ˇone
|
||||
two
|
||||
|
||||
three
|
||||
four
|
||||
five
|
||||
|
||||
sixˇ"#
|
||||
.unindent(),
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
|
||||
cx.assert_editor_state(
|
||||
&r#"one
|
||||
two
|
||||
|
||||
three
|
||||
four
|
||||
five
|
||||
ˇ
|
||||
sixˇ"#
|
||||
.unindent(),
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
|
||||
cx.assert_editor_state(
|
||||
&r#"one
|
||||
two
|
||||
ˇ
|
||||
three
|
||||
four
|
||||
five
|
||||
ˇ
|
||||
six"#
|
||||
.unindent(),
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
@ -5,7 +5,7 @@ use super::{
|
||||
};
|
||||
use crate::{
|
||||
display_map::{BlockStyle, DisplaySnapshot, FoldStatus, TransformBlock},
|
||||
editor_settings::ShowScrollbars,
|
||||
editor_settings::ShowScrollbar,
|
||||
git::{diff_hunk_to_display, DisplayDiffHunk},
|
||||
hover_popover::{
|
||||
hide_hover, hover_at, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH,
|
||||
@ -50,6 +50,7 @@ use std::{
|
||||
ops::Range,
|
||||
sync::Arc,
|
||||
};
|
||||
use text::Point;
|
||||
use workspace::{item::Item, GitGutterSetting, WorkspaceSettings};
|
||||
|
||||
enum FoldMarkers {}
|
||||
@ -651,7 +652,7 @@ impl EditorElement {
|
||||
|
||||
//TODO: This rendering is entirely a horrible hack
|
||||
DiffHunkStatus::Removed => {
|
||||
let row = *display_row_range.start();
|
||||
let row = display_row_range.start;
|
||||
|
||||
let offset = line_height / 2.;
|
||||
let start_y = row as f32 * line_height - offset - scroll_top;
|
||||
@ -673,11 +674,11 @@ impl EditorElement {
|
||||
}
|
||||
};
|
||||
|
||||
let start_row = *display_row_range.start();
|
||||
let end_row = *display_row_range.end();
|
||||
let start_row = display_row_range.start;
|
||||
let end_row = display_row_range.end;
|
||||
|
||||
let start_y = start_row as f32 * line_height - scroll_top;
|
||||
let end_y = end_row as f32 * line_height - scroll_top + line_height;
|
||||
let end_y = end_row as f32 * line_height - scroll_top;
|
||||
|
||||
let width = diff_style.width_em * line_height;
|
||||
let highlight_origin = bounds.origin() + vec2f(-width, start_y);
|
||||
@ -1051,18 +1052,23 @@ impl EditorElement {
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let diff_style = theme::current(cx).editor.diff.clone();
|
||||
if layout.is_singleton && settings::get::<EditorSettings>(cx).scrollbar.git_diff {
|
||||
let diff_style = theme::current(cx).editor.scrollbar.git.clone();
|
||||
for hunk in layout
|
||||
.position_map
|
||||
.snapshot
|
||||
.buffer_snapshot
|
||||
.git_diff_hunks_in_range(0..(max_row.floor() as u32), false)
|
||||
.git_diff_hunks_in_range(0..(max_row.floor() as u32))
|
||||
{
|
||||
let start_y = y_for_row(hunk.buffer_range.start as f32);
|
||||
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((hunk.buffer_range.end + 1) as f32)
|
||||
y_for_row((end_display.row() + 1) as f32)
|
||||
} else {
|
||||
y_for_row((hunk.buffer_range.end) as f32)
|
||||
y_for_row((end_display.row()) as f32)
|
||||
};
|
||||
|
||||
if end_y - start_y < 1. {
|
||||
@ -1093,6 +1099,7 @@ impl EditorElement {
|
||||
corner_radius: style.thumb.corner_radius,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
scene.push_quad(Quad {
|
||||
bounds: thumb_bounds,
|
||||
@ -1269,7 +1276,7 @@ impl EditorElement {
|
||||
.row;
|
||||
|
||||
buffer_snapshot
|
||||
.git_diff_hunks_in_range(buffer_start_row..buffer_end_row, false)
|
||||
.git_diff_hunks_in_range(buffer_start_row..buffer_end_row)
|
||||
.map(|hunk| diff_hunk_to_display(hunk, snapshot))
|
||||
.dedup()
|
||||
.collect()
|
||||
@ -2060,13 +2067,17 @@ impl Element<Editor> for EditorElement {
|
||||
));
|
||||
}
|
||||
|
||||
let show_scrollbars = match settings::get::<EditorSettings>(cx).show_scrollbars {
|
||||
ShowScrollbars::Auto => {
|
||||
snapshot.has_scrollbar_info() || editor.scroll_manager.scrollbars_visible()
|
||||
let scrollbar_settings = &settings::get::<EditorSettings>(cx).scrollbar;
|
||||
let show_scrollbars = match scrollbar_settings.show {
|
||||
ShowScrollbar::Auto => {
|
||||
// Git
|
||||
(is_singleton && scrollbar_settings.git_diff && snapshot.buffer_snapshot.has_git_diffs())
|
||||
// Scrollmanager
|
||||
|| editor.scroll_manager.scrollbars_visible()
|
||||
}
|
||||
ShowScrollbars::System => editor.scroll_manager.scrollbars_visible(),
|
||||
ShowScrollbars::Always => true,
|
||||
ShowScrollbars::Never => false,
|
||||
ShowScrollbar::System => editor.scroll_manager.scrollbars_visible(),
|
||||
ShowScrollbar::Always => true,
|
||||
ShowScrollbar::Never => false,
|
||||
};
|
||||
|
||||
let include_root = editor
|
||||
@ -2285,6 +2296,7 @@ impl Element<Editor> for EditorElement {
|
||||
text_size,
|
||||
scrollbar_row_range,
|
||||
show_scrollbars,
|
||||
is_singleton,
|
||||
max_row,
|
||||
gutter_margin,
|
||||
active_rows,
|
||||
@ -2440,6 +2452,7 @@ pub struct LayoutState {
|
||||
selections: Vec<(ReplicaId, Vec<SelectionLayout>)>,
|
||||
scrollbar_row_range: Range<f32>,
|
||||
show_scrollbars: bool,
|
||||
is_singleton: bool,
|
||||
max_row: u32,
|
||||
context_menu: Option<(DisplayPoint, AnyElement<Editor>)>,
|
||||
code_actions_indicator: Option<(u32, AnyElement<Editor>)>,
|
||||
|
@ -1,4 +1,4 @@
|
||||
use std::ops::RangeInclusive;
|
||||
use std::ops::Range;
|
||||
|
||||
use git::diff::{DiffHunk, DiffHunkStatus};
|
||||
use language::Point;
|
||||
@ -15,7 +15,7 @@ pub enum DisplayDiffHunk {
|
||||
},
|
||||
|
||||
Unfolded {
|
||||
display_row_range: RangeInclusive<u32>,
|
||||
display_row_range: Range<u32>,
|
||||
status: DiffHunkStatus,
|
||||
},
|
||||
}
|
||||
@ -26,7 +26,7 @@ impl DisplayDiffHunk {
|
||||
&DisplayDiffHunk::Folded { display_row } => display_row,
|
||||
DisplayDiffHunk::Unfolded {
|
||||
display_row_range, ..
|
||||
} => *display_row_range.start(),
|
||||
} => display_row_range.start,
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,7 +36,7 @@ impl DisplayDiffHunk {
|
||||
|
||||
DisplayDiffHunk::Unfolded {
|
||||
display_row_range, ..
|
||||
} => display_row_range.clone(),
|
||||
} => display_row_range.start..=display_row_range.end - 1,
|
||||
};
|
||||
|
||||
range.contains(&display_row)
|
||||
@ -77,16 +77,12 @@ pub fn diff_hunk_to_display(hunk: DiffHunk<u32>, snapshot: &DisplaySnapshot) ->
|
||||
} else {
|
||||
let start = hunk_start_point.to_display_point(snapshot).row();
|
||||
|
||||
let hunk_end_row_inclusive = hunk
|
||||
.buffer_range
|
||||
.end
|
||||
.saturating_sub(1)
|
||||
.max(hunk.buffer_range.start);
|
||||
let hunk_end_row_inclusive = hunk.buffer_range.end.max(hunk.buffer_range.start);
|
||||
let hunk_end_point = Point::new(hunk_end_row_inclusive, 0);
|
||||
let end = hunk_end_point.to_display_point(snapshot).row();
|
||||
|
||||
DisplayDiffHunk::Unfolded {
|
||||
display_row_range: start..=end,
|
||||
display_row_range: start..end,
|
||||
status: hunk.status(),
|
||||
}
|
||||
}
|
||||
|
@ -1231,27 +1231,27 @@ mod tests {
|
||||
}
|
||||
|
||||
fn as_local(&self) -> Option<&dyn language::LocalFile> {
|
||||
todo!()
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn mtime(&self) -> SystemTime {
|
||||
todo!()
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn file_name<'a>(&'a self, _: &'a gpui::AppContext) -> &'a std::ffi::OsStr {
|
||||
todo!()
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn is_deleted(&self) -> bool {
|
||||
todo!()
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
todo!()
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn to_proto(&self) -> rpc::proto::File {
|
||||
todo!()
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -193,6 +193,44 @@ pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPo
|
||||
})
|
||||
}
|
||||
|
||||
pub fn start_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
|
||||
let point = display_point.to_point(map);
|
||||
if point.row == 0 {
|
||||
return map.max_point();
|
||||
}
|
||||
|
||||
let mut found_non_blank_line = false;
|
||||
for row in (0..point.row + 1).rev() {
|
||||
let blank = map.buffer_snapshot.is_line_blank(row);
|
||||
if found_non_blank_line && blank {
|
||||
return Point::new(row, 0).to_display_point(map);
|
||||
}
|
||||
|
||||
found_non_blank_line |= !blank;
|
||||
}
|
||||
|
||||
DisplayPoint::zero()
|
||||
}
|
||||
|
||||
pub fn end_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
|
||||
let point = display_point.to_point(map);
|
||||
if point.row == map.max_buffer_row() {
|
||||
return DisplayPoint::zero();
|
||||
}
|
||||
|
||||
let mut found_non_blank_line = false;
|
||||
for row in point.row..map.max_buffer_row() + 1 {
|
||||
let blank = map.buffer_snapshot.is_line_blank(row);
|
||||
if found_non_blank_line && blank {
|
||||
return Point::new(row, 0).to_display_point(map);
|
||||
}
|
||||
|
||||
found_non_blank_line |= !blank;
|
||||
}
|
||||
|
||||
map.max_point()
|
||||
}
|
||||
|
||||
/// Scans for a boundary preceding the given start point `from` until a boundary is found, indicated by the
|
||||
/// given predicate returning true. The predicate is called with the character to the left and right
|
||||
/// of the candidate boundary location, and will be called with `\n` characters indicating the start
|
||||
|
@ -1140,6 +1140,10 @@ impl MultiBuffer {
|
||||
let mut result = Vec::new();
|
||||
let mut cursor = snapshot.excerpts.cursor::<usize>();
|
||||
cursor.seek(&start, Bias::Right, &());
|
||||
if cursor.item().is_none() {
|
||||
cursor.prev(&());
|
||||
}
|
||||
|
||||
while let Some(excerpt) = cursor.item() {
|
||||
if *cursor.start() > end {
|
||||
break;
|
||||
@ -2841,21 +2845,25 @@ impl MultiBufferSnapshot {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn git_diff_hunks_in_range<'a>(
|
||||
pub fn has_git_diffs(&self) -> bool {
|
||||
for excerpt in self.excerpts.iter() {
|
||||
if !excerpt.buffer.git_diff.is_empty() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn git_diff_hunks_in_range_rev<'a>(
|
||||
&'a self,
|
||||
row_range: Range<u32>,
|
||||
reversed: bool,
|
||||
) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
|
||||
let mut cursor = self.excerpts.cursor::<Point>();
|
||||
|
||||
if reversed {
|
||||
cursor.seek(&Point::new(row_range.end, 0), Bias::Left, &());
|
||||
if cursor.item().is_none() {
|
||||
cursor.prev(&());
|
||||
}
|
||||
} else {
|
||||
cursor.seek(&Point::new(row_range.start, 0), Bias::Right, &());
|
||||
}
|
||||
|
||||
std::iter::from_fn(move || {
|
||||
let excerpt = cursor.item()?;
|
||||
@ -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(&());
|
||||
|
||||
Some(buffer_hunks)
|
||||
})
|
||||
.flatten()
|
||||
}
|
||||
|
||||
pub fn git_diff_hunks_in_range<'a>(
|
||||
&'a self,
|
||||
row_range: Range<u32>,
|
||||
) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
|
||||
let mut cursor = self.excerpts.cursor::<Point>();
|
||||
|
||||
cursor.seek(&Point::new(row_range.start, 0), Bias::Right, &());
|
||||
|
||||
std::iter::from_fn(move || {
|
||||
let excerpt = cursor.item()?;
|
||||
let multibuffer_start = *cursor.start();
|
||||
let multibuffer_end = multibuffer_start + excerpt.text_summary.lines;
|
||||
if multibuffer_start.row >= row_range.end {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut buffer_start = excerpt.range.context.start;
|
||||
let mut buffer_end = excerpt.range.context.end;
|
||||
let excerpt_start_point = buffer_start.to_point(&excerpt.buffer);
|
||||
let excerpt_end_point = excerpt_start_point + excerpt.text_summary.lines;
|
||||
|
||||
if row_range.start > multibuffer_start.row {
|
||||
let buffer_start_point =
|
||||
excerpt_start_point + Point::new(row_range.start - multibuffer_start.row, 0);
|
||||
buffer_start = excerpt.buffer.anchor_before(buffer_start_point);
|
||||
}
|
||||
|
||||
if row_range.end < multibuffer_end.row {
|
||||
let buffer_end_point =
|
||||
excerpt_start_point + Point::new(row_range.end - multibuffer_start.row, 0);
|
||||
buffer_end = excerpt.buffer.anchor_before(buffer_end_point);
|
||||
}
|
||||
|
||||
let buffer_hunks = excerpt
|
||||
.buffer
|
||||
.git_diff_hunks_intersecting_range(buffer_start..buffer_end)
|
||||
.filter_map(move |hunk| {
|
||||
let start = multibuffer_start.row
|
||||
+ hunk
|
||||
.buffer_range
|
||||
.start
|
||||
.saturating_sub(excerpt_start_point.row);
|
||||
let end = multibuffer_start.row
|
||||
+ hunk
|
||||
.buffer_range
|
||||
.end
|
||||
.min(excerpt_end_point.row + 1)
|
||||
.saturating_sub(excerpt_start_point.row);
|
||||
|
||||
Some(DiffHunk {
|
||||
buffer_range: start..end,
|
||||
diff_base_byte_range: hunk.diff_base_byte_range.clone(),
|
||||
})
|
||||
});
|
||||
|
||||
cursor.next(&());
|
||||
|
||||
Some(buffer_hunks)
|
||||
})
|
||||
.flatten()
|
||||
@ -4647,7 +4713,7 @@ mod tests {
|
||||
|
||||
assert_eq!(
|
||||
snapshot
|
||||
.git_diff_hunks_in_range(0..12, false)
|
||||
.git_diff_hunks_in_range(0..12)
|
||||
.map(|hunk| (hunk.status(), hunk.buffer_range))
|
||||
.collect::<Vec<_>>(),
|
||||
&expected,
|
||||
@ -4655,7 +4721,7 @@ mod tests {
|
||||
|
||||
assert_eq!(
|
||||
snapshot
|
||||
.git_diff_hunks_in_range(0..12, true)
|
||||
.git_diff_hunks_in_range_rev(0..12)
|
||||
.map(|hunk| (hunk.status(), hunk.buffer_range))
|
||||
.collect::<Vec<_>>(),
|
||||
expected
|
||||
@ -5010,16 +5076,19 @@ mod tests {
|
||||
.read(cx)
|
||||
.range_to_buffer_ranges(start_ix..end_ix, cx);
|
||||
let excerpted_buffers_text = excerpted_buffer_ranges
|
||||
.into_iter()
|
||||
.iter()
|
||||
.map(|(buffer, buffer_range)| {
|
||||
buffer
|
||||
.read(cx)
|
||||
.text_for_range(buffer_range)
|
||||
.text_for_range(buffer_range.clone())
|
||||
.collect::<String>()
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
assert_eq!(excerpted_buffers_text, text_for_range);
|
||||
if !expected_excerpts.is_empty() {
|
||||
assert!(!excerpted_buffer_ranges.is_empty());
|
||||
}
|
||||
|
||||
let expected_summary = TextSummary::from(&expected_text[start_ix..end_ix]);
|
||||
assert_eq!(
|
||||
|
@ -204,6 +204,7 @@ impl<'a> EditorTestContext<'a> {
|
||||
self.assert_selections(expected_selections, marked_text.to_string())
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn assert_editor_background_highlights<Tag: 'static>(&mut self, marked_text: &str) {
|
||||
let expected_ranges = self.ranges(marked_text);
|
||||
let actual_ranges: Vec<Range<usize>> = self.update_editor(|editor, cx| {
|
||||
@ -220,6 +221,7 @@ impl<'a> EditorTestContext<'a> {
|
||||
assert_set_eq!(actual_ranges, expected_ranges);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn assert_editor_text_highlights<Tag: ?Sized + 'static>(&mut self, marked_text: &str) {
|
||||
let expected_ranges = self.ranges(marked_text);
|
||||
let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
|
||||
@ -233,12 +235,14 @@ impl<'a> EditorTestContext<'a> {
|
||||
assert_set_eq!(actual_ranges, expected_ranges);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn assert_editor_selections(&mut self, expected_selections: Vec<Range<usize>>) {
|
||||
let expected_marked_text =
|
||||
generate_marked_text(&self.buffer_text(), &expected_selections, true);
|
||||
self.assert_selections(expected_selections, expected_marked_text)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_selections(
|
||||
&mut self,
|
||||
expected_selections: Vec<Range<usize>>,
|
||||
|
@ -35,3 +35,6 @@ serde_derive.workspace = true
|
||||
sysinfo = "0.27.1"
|
||||
tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" }
|
||||
urlencoding = "2.1.2"
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { path = "../editor", features = ["test-support"] }
|
||||
|
@ -39,8 +39,8 @@ impl View for DeployFeedbackButton {
|
||||
let style = &theme
|
||||
.workspace
|
||||
.status_bar
|
||||
.sidebar_buttons
|
||||
.item
|
||||
.panel_buttons
|
||||
.button
|
||||
.style_for(state, active);
|
||||
|
||||
Svg::new("icons/feedback_16.svg")
|
||||
|
@ -23,6 +23,7 @@ workspace = { path = "../workspace" }
|
||||
postage.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { path = "../editor", features = ["test-support"] }
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
language = { path = "../language", features = ["test-support"] }
|
||||
workspace = { path = "../workspace", features = ["test-support"] }
|
||||
|
@ -380,7 +380,7 @@ mod tests {
|
||||
use gpui::{TestAppContext, ViewHandle};
|
||||
use menu::{Confirm, SelectNext};
|
||||
use serde_json::json;
|
||||
use workspace::{AppState, Pane, Workspace};
|
||||
use workspace::{AppState, Workspace};
|
||||
|
||||
#[ctor::ctor]
|
||||
fn init_logger() {
|
||||
@ -1161,9 +1161,13 @@ mod tests {
|
||||
assert!(insertion_result.is_none(), "Pane id {pane_id} collision");
|
||||
}
|
||||
});
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
Pane::close_active_item(workspace, &workspace::CloseActiveItem, cx);
|
||||
});
|
||||
active_pane
|
||||
.update(cx, |pane, cx| {
|
||||
pane.close_active_item(&workspace::CloseActiveItem, cx)
|
||||
.unwrap()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
deterministic.run_until_parked();
|
||||
cx.read(|cx| {
|
||||
for pane in workspace.read(cx).panes() {
|
||||
|
@ -1,4 +1,4 @@
|
||||
use std::ops::Range;
|
||||
use std::{iter, ops::Range};
|
||||
use sum_tree::SumTree;
|
||||
use text::{Anchor, BufferSnapshot, OffsetRangeExt, Point};
|
||||
|
||||
@ -71,22 +71,66 @@ impl BufferDiff {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.tree.is_empty()
|
||||
}
|
||||
|
||||
pub fn hunks_in_row_range<'a>(
|
||||
&'a self,
|
||||
range: Range<u32>,
|
||||
buffer: &'a BufferSnapshot,
|
||||
reversed: bool,
|
||||
) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
|
||||
let start = buffer.anchor_before(Point::new(range.start, 0));
|
||||
let end = buffer.anchor_after(Point::new(range.end, 0));
|
||||
self.hunks_intersecting_range(start..end, buffer, reversed)
|
||||
|
||||
self.hunks_intersecting_range(start..end, buffer)
|
||||
}
|
||||
|
||||
pub fn hunks_intersecting_range<'a>(
|
||||
&'a self,
|
||||
range: Range<Anchor>,
|
||||
buffer: &'a BufferSnapshot,
|
||||
reversed: bool,
|
||||
) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
|
||||
let mut cursor = self.tree.filter::<_, DiffHunkSummary>(move |summary| {
|
||||
let before_start = summary.buffer_range.end.cmp(&range.start, buffer).is_lt();
|
||||
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::<Point, _, _>(anchor_iter);
|
||||
iter::from_fn(move || {
|
||||
let (start_point, start_base) = summaries.next()?;
|
||||
let (end_point, end_base) = summaries.next()?;
|
||||
|
||||
let end_row = if end_point.column > 0 {
|
||||
end_point.row + 1
|
||||
} else {
|
||||
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<Anchor>,
|
||||
buffer: &'a BufferSnapshot,
|
||||
) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
|
||||
let mut cursor = self.tree.filter::<_, DiffHunkSummary>(move |summary| {
|
||||
let before_start = summary.buffer_range.end.cmp(&range.start, buffer).is_lt();
|
||||
@ -95,14 +139,9 @@ impl BufferDiff {
|
||||
});
|
||||
|
||||
std::iter::from_fn(move || {
|
||||
if reversed {
|
||||
cursor.prev(buffer);
|
||||
} else {
|
||||
cursor.next(buffer);
|
||||
}
|
||||
|
||||
let hunk = cursor.item()?;
|
||||
|
||||
let range = hunk.buffer_range.to_point(buffer);
|
||||
let end_row = if range.end.column > 0 {
|
||||
range.end.row + 1
|
||||
@ -151,7 +190,7 @@ impl BufferDiff {
|
||||
fn hunks<'a>(&'a self, text: &'a BufferSnapshot) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
|
||||
let start = text.anchor_before(Point::new(0, 0));
|
||||
let end = text.anchor_after(Point::new(u32::MAX, u32::MAX));
|
||||
self.hunks_intersecting_range(start..end, text, false)
|
||||
self.hunks_intersecting_range(start..end, text)
|
||||
}
|
||||
|
||||
fn diff<'a>(head: &'a str, current: &'a str) -> Option<GitPatch<'a>> {
|
||||
@ -279,6 +318,8 @@ pub fn assert_hunks<Iter>(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::assert_eq;
|
||||
|
||||
use super::*;
|
||||
use text::Buffer;
|
||||
use unindent::Unindent as _;
|
||||
@ -365,7 +406,7 @@ mod tests {
|
||||
assert_eq!(diff.hunks(&buffer).count(), 8);
|
||||
|
||||
assert_hunks(
|
||||
diff.hunks_in_row_range(7..12, &buffer, false),
|
||||
diff.hunks_in_row_range(7..12, &buffer),
|
||||
&buffer,
|
||||
&diff_base,
|
||||
&[
|
||||
|
@ -18,3 +18,6 @@ workspace = { path = "../workspace" }
|
||||
postage.workspace = true
|
||||
theme = { path = "../theme" }
|
||||
util = { path = "../util" }
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { path = "../editor", features = ["test-support"] }
|
||||
|
@ -1460,27 +1460,13 @@ impl AppContext {
|
||||
self.views_metadata.remove(&(window_id, view_id));
|
||||
let mut view = self.views.remove(&(window_id, view_id)).unwrap();
|
||||
view.release(self);
|
||||
let change_focus_to = self.windows.get_mut(&window_id).and_then(|window| {
|
||||
if let Some(window) = self.windows.get_mut(&window_id) {
|
||||
window.parents.remove(&view_id);
|
||||
window
|
||||
.invalidation
|
||||
.get_or_insert_with(Default::default)
|
||||
.removed
|
||||
.push(view_id);
|
||||
if window.focused_view_id == Some(view_id) {
|
||||
Some(window.root_view().id())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(view_id) = change_focus_to {
|
||||
self.pending_effects
|
||||
.push_back(Effect::Focus(FocusEffect::View {
|
||||
window_id,
|
||||
view_id: Some(view_id),
|
||||
is_forced: false,
|
||||
}));
|
||||
}
|
||||
|
||||
self.pending_effects
|
||||
@ -1717,8 +1703,69 @@ impl AppContext {
|
||||
if let Some(invalidation) = invalidation {
|
||||
let appearance = cx.window.platform_window.appearance();
|
||||
cx.invalidate(invalidation, appearance);
|
||||
if cx.layout(refreshing).log_err().is_some() {
|
||||
if let Some(old_parents) = cx.layout(refreshing).log_err() {
|
||||
updated_windows.insert(window_id);
|
||||
|
||||
if let Some(focused_view_id) = cx.focused_view_id() {
|
||||
let old_ancestors = std::iter::successors(
|
||||
Some(focused_view_id),
|
||||
|&view_id| old_parents.get(&view_id).copied(),
|
||||
)
|
||||
.collect::<HashSet<_>>();
|
||||
let new_ancestors =
|
||||
cx.ancestors(focused_view_id).collect::<HashSet<_>>();
|
||||
|
||||
// Notify the old ancestors of the focused view when they don't contain it anymore.
|
||||
for old_ancestor in old_ancestors.iter().copied() {
|
||||
if !new_ancestors.contains(&old_ancestor) {
|
||||
if let Some(mut view) =
|
||||
cx.views.remove(&(window_id, old_ancestor))
|
||||
{
|
||||
view.focus_out(
|
||||
focused_view_id,
|
||||
cx,
|
||||
old_ancestor,
|
||||
);
|
||||
cx.views
|
||||
.insert((window_id, old_ancestor), view);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Notify the new ancestors of the focused view if they contain it now.
|
||||
for new_ancestor in new_ancestors.iter().copied() {
|
||||
if !old_ancestors.contains(&new_ancestor) {
|
||||
if let Some(mut view) =
|
||||
cx.views.remove(&(window_id, new_ancestor))
|
||||
{
|
||||
view.focus_in(
|
||||
focused_view_id,
|
||||
cx,
|
||||
new_ancestor,
|
||||
);
|
||||
cx.views
|
||||
.insert((window_id, new_ancestor), view);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// When the previously-focused view has been dropped and
|
||||
// there isn't any pending focus, focus the root view.
|
||||
let root_view_id = cx.window.root_view().id();
|
||||
if focused_view_id != root_view_id
|
||||
&& !cx.views.contains_key(&(window_id, focused_view_id))
|
||||
&& !focus_effects.contains_key(&window_id)
|
||||
{
|
||||
focus_effects.insert(
|
||||
window_id,
|
||||
FocusEffect::View {
|
||||
window_id,
|
||||
view_id: Some(root_view_id),
|
||||
is_forced: false,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -1895,9 +1942,27 @@ impl AppContext {
|
||||
fn handle_focus_effect(&mut self, effect: FocusEffect) {
|
||||
let window_id = effect.window_id();
|
||||
self.update_window(window_id, |cx| {
|
||||
// Ensure the newly-focused view still exists, otherwise focus
|
||||
// the root view instead.
|
||||
let focused_id = match effect {
|
||||
FocusEffect::View { view_id, .. } => view_id,
|
||||
FocusEffect::ViewParent { view_id, .. } => cx.ancestors(view_id).skip(1).next(),
|
||||
FocusEffect::View { view_id, .. } => {
|
||||
if let Some(view_id) = view_id {
|
||||
if cx.views.contains_key(&(window_id, view_id)) {
|
||||
Some(view_id)
|
||||
} else {
|
||||
Some(cx.root_view().id())
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
FocusEffect::ViewParent { view_id, .. } => Some(
|
||||
cx.window
|
||||
.parents
|
||||
.get(&view_id)
|
||||
.copied()
|
||||
.unwrap_or(cx.root_view().id()),
|
||||
),
|
||||
};
|
||||
|
||||
let focus_changed = cx.window.focused_view_id != focused_id;
|
||||
@ -3802,6 +3867,12 @@ impl<T> PartialEq for ViewHandle<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> PartialEq<AnyViewHandle> for ViewHandle<T> {
|
||||
fn eq(&self, other: &AnyViewHandle) -> bool {
|
||||
self.window_id == other.window_id && self.view_id == other.view_id
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> PartialEq<WeakViewHandle<T>> for ViewHandle<T> {
|
||||
fn eq(&self, other: &WeakViewHandle<T>) -> bool {
|
||||
self.window_id == other.window_id && self.view_id == other.view_id
|
||||
@ -3952,6 +4023,12 @@ impl Clone for AnyViewHandle {
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for AnyViewHandle {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.window_id == other.window_id && self.view_id == other.view_id
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> PartialEq<ViewHandle<T>> for AnyViewHandle {
|
||||
fn eq(&self, other: &ViewHandle<T>) -> bool {
|
||||
self.window_id == other.window_id && self.view_id == other.view_id
|
||||
@ -4198,7 +4275,7 @@ impl<T> Hash for WeakViewHandle<T> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub struct AnyWeakViewHandle {
|
||||
window_id: usize,
|
||||
view_id: usize,
|
||||
|
@ -270,7 +270,7 @@ impl TestAppContext {
|
||||
.borrow_mut()
|
||||
.pop_front()
|
||||
.expect("prompt was not called");
|
||||
let _ = done_tx.try_send(answer);
|
||||
done_tx.try_send(answer).ok();
|
||||
}
|
||||
|
||||
pub fn has_pending_prompt(&self, window_id: usize) -> bool {
|
||||
|
@ -29,6 +29,7 @@ use sqlez::{
|
||||
};
|
||||
use std::{
|
||||
any::TypeId,
|
||||
mem,
|
||||
ops::{Deref, DerefMut, Range},
|
||||
};
|
||||
use util::ResultExt;
|
||||
@ -890,7 +891,7 @@ impl<'a> WindowContext<'a> {
|
||||
Ok(element)
|
||||
}
|
||||
|
||||
pub(crate) fn layout(&mut self, refreshing: bool) -> Result<()> {
|
||||
pub(crate) fn layout(&mut self, refreshing: bool) -> Result<HashMap<usize, usize>> {
|
||||
let window_size = self.window.platform_window.content_size();
|
||||
let root_view_id = self.window.root_view().id();
|
||||
let mut rendered_root = self.window.rendered_views.remove(&root_view_id).unwrap();
|
||||
@ -923,11 +924,11 @@ impl<'a> WindowContext<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
self.window.parents = new_parents;
|
||||
let old_parents = mem::replace(&mut self.window.parents, new_parents);
|
||||
self.window
|
||||
.rendered_views
|
||||
.insert(root_view_id, rendered_root);
|
||||
Ok(())
|
||||
Ok(old_parents)
|
||||
}
|
||||
|
||||
pub(crate) fn paint(&mut self) -> Result<Scene> {
|
||||
|
@ -187,25 +187,23 @@ pub trait Element<V: View>: 'static {
|
||||
Tooltip::new::<Tag, V>(id, text, action, style, self.into_any(), cx)
|
||||
}
|
||||
|
||||
fn with_resize_handle<Tag: 'static>(
|
||||
fn resizable(
|
||||
self,
|
||||
element_id: usize,
|
||||
side: Side,
|
||||
handle_size: f32,
|
||||
initial_size: f32,
|
||||
cx: &mut ViewContext<V>,
|
||||
side: HandleSide,
|
||||
size: f32,
|
||||
on_resize: impl 'static + FnMut(&mut V, f32, &mut ViewContext<V>),
|
||||
) -> Resizable<V>
|
||||
where
|
||||
Self: 'static + Sized,
|
||||
{
|
||||
Resizable::new::<Tag, V>(
|
||||
self.into_any(),
|
||||
element_id,
|
||||
side,
|
||||
handle_size,
|
||||
initial_size,
|
||||
cx,
|
||||
)
|
||||
Resizable::new(self.into_any(), side, size, on_resize)
|
||||
}
|
||||
|
||||
fn mouse<Tag>(self, region_id: usize) -> MouseEventHandler<Tag, V>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
MouseEventHandler::for_child(self.into_any(), region_id)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -990,7 +990,7 @@ mod tests {
|
||||
_: &mut V,
|
||||
_: &mut ViewContext<V>,
|
||||
) {
|
||||
todo!()
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn rect_for_text_range(
|
||||
@ -1003,7 +1003,7 @@ mod tests {
|
||||
_: &V,
|
||||
_: &ViewContext<V>,
|
||||
) -> Option<RectF> {
|
||||
todo!()
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn debug(&self, _: RectF, _: &(), _: &(), _: &V, _: &ViewContext<V>) -> serde_json::Value {
|
||||
|
@ -32,10 +32,25 @@ pub struct MouseEventHandler<Tag: 'static, V: View> {
|
||||
/// Element which provides a render_child callback with a MouseState and paints a mouse
|
||||
/// region under (or above) it for easy mouse event handling.
|
||||
impl<Tag, V: View> MouseEventHandler<Tag, V> {
|
||||
pub fn new<D, F>(region_id: usize, cx: &mut ViewContext<V>, render_child: F) -> Self
|
||||
pub fn for_child(child: impl Element<V>, region_id: usize) -> Self {
|
||||
Self {
|
||||
child: child.into_any(),
|
||||
region_id,
|
||||
cursor_style: None,
|
||||
handlers: Default::default(),
|
||||
notify_on_hover: false,
|
||||
notify_on_click: false,
|
||||
hoverable: false,
|
||||
above: false,
|
||||
padding: Default::default(),
|
||||
_tag: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new<E, F>(region_id: usize, cx: &mut ViewContext<V>, render_child: F) -> Self
|
||||
where
|
||||
D: Element<V>,
|
||||
F: FnOnce(&mut MouseState, &mut ViewContext<V>) -> D,
|
||||
E: Element<V>,
|
||||
F: FnOnce(&mut MouseState, &mut ViewContext<V>) -> E,
|
||||
{
|
||||
let mut mouse_state = cx.mouse_state::<Tag>(region_id);
|
||||
let child = render_child(&mut mouse_state, cx).into_any();
|
||||
|
@ -1,4 +1,4 @@
|
||||
use std::{cell::Cell, rc::Rc};
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use pathfinder_geometry::vector::{vec2f, Vector2F};
|
||||
use serde_json::json;
|
||||
@ -7,25 +7,23 @@ use crate::{
|
||||
geometry::rect::RectF,
|
||||
platform::{CursorStyle, MouseButton},
|
||||
scene::MouseDrag,
|
||||
AnyElement, Axis, Element, ElementStateHandle, LayoutContext, MouseRegion, SceneBuilder, View,
|
||||
AnyElement, Axis, Element, LayoutContext, MouseRegion, SceneBuilder, SizeConstraint, View,
|
||||
ViewContext,
|
||||
};
|
||||
|
||||
use super::{ConstrainedBox, Hook};
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum Side {
|
||||
pub enum HandleSide {
|
||||
Top,
|
||||
Bottom,
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
impl Side {
|
||||
impl HandleSide {
|
||||
fn axis(&self) -> Axis {
|
||||
match self {
|
||||
Side::Left | Side::Right => Axis::Horizontal,
|
||||
Side::Top | Side::Bottom => Axis::Vertical,
|
||||
HandleSide::Left | HandleSide::Right => Axis::Horizontal,
|
||||
HandleSide::Top | HandleSide::Bottom => Axis::Vertical,
|
||||
}
|
||||
}
|
||||
|
||||
@ -33,8 +31,8 @@ impl Side {
|
||||
/// then top-to-bottom
|
||||
fn before_content(self) -> bool {
|
||||
match self {
|
||||
Side::Left | Side::Top => true,
|
||||
Side::Right | Side::Bottom => false,
|
||||
HandleSide::Left | HandleSide::Top => true,
|
||||
HandleSide::Right | HandleSide::Bottom => false,
|
||||
}
|
||||
}
|
||||
|
||||
@ -55,14 +53,14 @@ impl Side {
|
||||
|
||||
fn of_rect(&self, bounds: RectF, handle_size: f32) -> RectF {
|
||||
match self {
|
||||
Side::Top => RectF::new(bounds.origin(), vec2f(bounds.width(), handle_size)),
|
||||
Side::Left => RectF::new(bounds.origin(), vec2f(handle_size, bounds.height())),
|
||||
Side::Bottom => {
|
||||
HandleSide::Top => RectF::new(bounds.origin(), vec2f(bounds.width(), handle_size)),
|
||||
HandleSide::Left => RectF::new(bounds.origin(), vec2f(handle_size, bounds.height())),
|
||||
HandleSide::Bottom => {
|
||||
let mut origin = bounds.lower_left();
|
||||
origin.set_y(origin.y() - handle_size);
|
||||
RectF::new(origin, vec2f(bounds.width(), handle_size))
|
||||
}
|
||||
Side::Right => {
|
||||
HandleSide::Right => {
|
||||
let mut origin = bounds.upper_right();
|
||||
origin.set_x(origin.x() - handle_size);
|
||||
RectF::new(origin, vec2f(handle_size, bounds.height()))
|
||||
@ -71,69 +69,44 @@ impl Side {
|
||||
}
|
||||
}
|
||||
|
||||
struct ResizeHandleState {
|
||||
actual_dimension: Cell<f32>,
|
||||
custom_dimension: Cell<f32>,
|
||||
pub struct Resizable<V: View> {
|
||||
child: AnyElement<V>,
|
||||
handle_side: HandleSide,
|
||||
handle_size: f32,
|
||||
on_resize: Rc<RefCell<dyn FnMut(&mut V, f32, &mut ViewContext<V>)>>,
|
||||
}
|
||||
|
||||
pub struct Resizable<V: View> {
|
||||
side: Side,
|
||||
handle_size: f32,
|
||||
child: AnyElement<V>,
|
||||
state: Rc<ResizeHandleState>,
|
||||
_state_handle: ElementStateHandle<Rc<ResizeHandleState>>,
|
||||
}
|
||||
const DEFAULT_HANDLE_SIZE: f32 = 4.0;
|
||||
|
||||
impl<V: View> Resizable<V> {
|
||||
pub fn new<Tag: 'static, T: View>(
|
||||
pub fn new(
|
||||
child: AnyElement<V>,
|
||||
element_id: usize,
|
||||
side: Side,
|
||||
handle_size: f32,
|
||||
initial_size: f32,
|
||||
cx: &mut ViewContext<V>,
|
||||
handle_side: HandleSide,
|
||||
size: f32,
|
||||
on_resize: impl 'static + FnMut(&mut V, f32, &mut ViewContext<V>),
|
||||
) -> Self {
|
||||
let state_handle = cx.element_state::<Tag, Rc<ResizeHandleState>>(
|
||||
element_id,
|
||||
Rc::new(ResizeHandleState {
|
||||
actual_dimension: Cell::new(initial_size),
|
||||
custom_dimension: Cell::new(initial_size),
|
||||
}),
|
||||
);
|
||||
|
||||
let state = state_handle.read(cx).clone();
|
||||
|
||||
let child = Hook::new({
|
||||
let constrained = ConstrainedBox::new(child);
|
||||
match side.axis() {
|
||||
Axis::Horizontal => constrained.with_max_width(state.custom_dimension.get()),
|
||||
Axis::Vertical => constrained.with_max_height(state.custom_dimension.get()),
|
||||
let child = match handle_side.axis() {
|
||||
Axis::Horizontal => child.constrained().with_max_width(size),
|
||||
Axis::Vertical => child.constrained().with_max_height(size),
|
||||
}
|
||||
})
|
||||
.on_after_layout({
|
||||
let state = state.clone();
|
||||
move |size, _| {
|
||||
state.actual_dimension.set(side.relevant_component(size));
|
||||
}
|
||||
})
|
||||
.into_any();
|
||||
|
||||
Self {
|
||||
side,
|
||||
child,
|
||||
handle_size,
|
||||
state,
|
||||
_state_handle: state_handle,
|
||||
handle_side,
|
||||
handle_size: DEFAULT_HANDLE_SIZE,
|
||||
on_resize: Rc::new(RefCell::new(on_resize)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn current_size(&self) -> f32 {
|
||||
self.state.actual_dimension.get()
|
||||
pub fn with_handle_size(mut self, handle_size: f32) -> Self {
|
||||
self.handle_size = handle_size;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: View> Element<V> for Resizable<V> {
|
||||
type LayoutState = ();
|
||||
type LayoutState = SizeConstraint;
|
||||
type PaintState = ();
|
||||
|
||||
fn layout(
|
||||
@ -142,7 +115,7 @@ impl<V: View> Element<V> for Resizable<V> {
|
||||
view: &mut V,
|
||||
cx: &mut LayoutContext<V>,
|
||||
) -> (Vector2F, Self::LayoutState) {
|
||||
(self.child.layout(constraint, view, cx), ())
|
||||
(self.child.layout(constraint, view, cx), constraint)
|
||||
}
|
||||
|
||||
fn paint(
|
||||
@ -150,34 +123,44 @@ impl<V: View> Element<V> for Resizable<V> {
|
||||
scene: &mut SceneBuilder,
|
||||
bounds: pathfinder_geometry::rect::RectF,
|
||||
visible_bounds: pathfinder_geometry::rect::RectF,
|
||||
_child_size: &mut Self::LayoutState,
|
||||
constraint: &mut SizeConstraint,
|
||||
view: &mut V,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> Self::PaintState {
|
||||
scene.push_stacking_context(None, None);
|
||||
|
||||
let handle_region = self.side.of_rect(bounds, self.handle_size);
|
||||
let handle_region = self.handle_side.of_rect(bounds, self.handle_size);
|
||||
|
||||
enum ResizeHandle {}
|
||||
scene.push_mouse_region(
|
||||
MouseRegion::new::<ResizeHandle>(cx.view_id(), self.side as usize, handle_region)
|
||||
MouseRegion::new::<ResizeHandle>(
|
||||
cx.view_id(),
|
||||
self.handle_side as usize,
|
||||
handle_region,
|
||||
)
|
||||
.on_down(MouseButton::Left, |_, _: &mut V, _| {}) // This prevents the mouse down event from being propagated elsewhere
|
||||
.on_drag(MouseButton::Left, {
|
||||
let 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();
|
||||
let bounds = bounds.clone();
|
||||
let side = self.handle_side;
|
||||
let prev_size = side.relevant_component(bounds.size());
|
||||
let min_size = side.relevant_component(constraint.min);
|
||||
let max_size = side.relevant_component(constraint.max);
|
||||
let on_resize = self.on_resize.clone();
|
||||
move |event, view: &mut V, cx| {
|
||||
let new_size = min_size
|
||||
.max(prev_size + side.compute_delta(event))
|
||||
.min(max_size)
|
||||
.round();
|
||||
if new_size != prev_size {
|
||||
on_resize.borrow_mut()(view, new_size, cx);
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
scene.push_cursor_region(crate::CursorRegion {
|
||||
bounds: handle_region,
|
||||
style: match self.side.axis() {
|
||||
style: match self.handle_side.axis() {
|
||||
Axis::Horizontal => CursorStyle::ResizeLeftRight,
|
||||
Axis::Vertical => CursorStyle::ResizeUpDown,
|
||||
},
|
||||
|
@ -173,6 +173,7 @@ pub struct WindowOptions<'a> {
|
||||
pub titlebar: Option<TitlebarOptions<'a>>,
|
||||
pub center: bool,
|
||||
pub focus: bool,
|
||||
pub show: bool,
|
||||
pub kind: WindowKind,
|
||||
pub is_movable: bool,
|
||||
pub screen: Option<Rc<dyn Screen>>,
|
||||
@ -222,21 +223,21 @@ impl Bind for WindowBounds {
|
||||
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
|
||||
let (region, next_index) = match self {
|
||||
WindowBounds::Fullscreen => {
|
||||
let next_index = statement.bind("Fullscreen", start_index)?;
|
||||
let next_index = statement.bind(&"Fullscreen", start_index)?;
|
||||
(None, next_index)
|
||||
}
|
||||
WindowBounds::Maximized => {
|
||||
let next_index = statement.bind("Maximized", start_index)?;
|
||||
let next_index = statement.bind(&"Maximized", start_index)?;
|
||||
(None, next_index)
|
||||
}
|
||||
WindowBounds::Fixed(region) => {
|
||||
let next_index = statement.bind("Fixed", start_index)?;
|
||||
let next_index = statement.bind(&"Fixed", start_index)?;
|
||||
(Some(*region), next_index)
|
||||
}
|
||||
};
|
||||
|
||||
statement.bind(
|
||||
region.map(|region| {
|
||||
®ion.map(|region| {
|
||||
(
|
||||
region.min_x(),
|
||||
region.min_y(),
|
||||
@ -376,6 +377,7 @@ impl<'a> Default for WindowOptions<'a> {
|
||||
}),
|
||||
center: false,
|
||||
focus: true,
|
||||
show: true,
|
||||
kind: WindowKind::Normal,
|
||||
is_movable: true,
|
||||
screen: None,
|
||||
|
@ -614,7 +614,7 @@ impl Window {
|
||||
}
|
||||
if options.focus {
|
||||
native_window.makeKeyAndOrderFront_(nil);
|
||||
} else {
|
||||
} else if options.show {
|
||||
native_window.orderFront_(nil);
|
||||
}
|
||||
|
||||
|
@ -22,3 +22,6 @@ serde.workspace = true
|
||||
schemars.workspace = true
|
||||
log.workspace = true
|
||||
shellexpand = "2.1.0"
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { path = "../editor", features = ["test-support"] }
|
||||
|
@ -41,7 +41,7 @@ anyhow.workspace = true
|
||||
async-broadcast = "0.4"
|
||||
async-trait.workspace = true
|
||||
futures.workspace = true
|
||||
glob.workspace = true
|
||||
globset.workspace = true
|
||||
lazy_static.workspace = true
|
||||
log.workspace = true
|
||||
parking_lot.workspace = true
|
||||
|
@ -1644,10 +1644,17 @@ impl Buffer {
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
if lamport_timestamp > self.diagnostics_timestamp {
|
||||
match self.diagnostics.binary_search_by_key(&server_id, |e| e.0) {
|
||||
let ix = self.diagnostics.binary_search_by_key(&server_id, |e| e.0);
|
||||
if diagnostics.len() == 0 {
|
||||
if let Ok(ix) = ix {
|
||||
self.diagnostics.remove(ix);
|
||||
}
|
||||
} else {
|
||||
match ix {
|
||||
Err(ix) => self.diagnostics.insert(ix, (server_id, diagnostics)),
|
||||
Ok(ix) => self.diagnostics[ix].1 = diagnostics,
|
||||
};
|
||||
}
|
||||
self.diagnostics_timestamp = lamport_timestamp;
|
||||
self.diagnostics_update_count += 1;
|
||||
self.text.lamport_clock.observe(lamport_timestamp);
|
||||
@ -2509,18 +2516,22 @@ impl BufferSnapshot {
|
||||
pub fn git_diff_hunks_in_row_range<'a>(
|
||||
&'a self,
|
||||
range: Range<u32>,
|
||||
reversed: bool,
|
||||
) -> impl 'a + Iterator<Item = git::diff::DiffHunk<u32>> {
|
||||
self.git_diff.hunks_in_row_range(range, self, reversed)
|
||||
self.git_diff.hunks_in_row_range(range, self)
|
||||
}
|
||||
|
||||
pub fn git_diff_hunks_intersecting_range<'a>(
|
||||
&'a self,
|
||||
range: Range<Anchor>,
|
||||
reversed: bool,
|
||||
) -> impl 'a + Iterator<Item = git::diff::DiffHunk<u32>> {
|
||||
self.git_diff
|
||||
.hunks_intersecting_range(range, self, reversed)
|
||||
self.git_diff.hunks_intersecting_range(range, self)
|
||||
}
|
||||
|
||||
pub fn git_diff_hunks_intersecting_range_rev<'a>(
|
||||
&'a self,
|
||||
range: Range<Anchor>,
|
||||
) -> impl 'a + Iterator<Item = git::diff::DiffHunk<u32>> {
|
||||
self.git_diff.hunks_intersecting_range_rev(range, self)
|
||||
}
|
||||
|
||||
pub fn diagnostics_in_range<'a, T, O>(
|
||||
|
@ -80,6 +80,10 @@ impl DiagnosticSet {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.diagnostics.summary().count
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = &DiagnosticEntry<Anchor>> {
|
||||
self.diagnostics.iter()
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
use globset::GlobMatcher;
|
||||
use gpui::AppContext;
|
||||
use schemars::{
|
||||
schema::{InstanceType, ObjectValidation, Schema, SchemaObject},
|
||||
@ -45,10 +46,10 @@ pub struct LanguageSettings {
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct CopilotSettings {
|
||||
pub feature_enabled: bool,
|
||||
pub disabled_globs: Vec<glob::Pattern>,
|
||||
pub disabled_globs: Vec<GlobMatcher>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct AllLanguageSettingsContent {
|
||||
#[serde(default)]
|
||||
pub features: Option<FeaturesContent>,
|
||||
@ -151,7 +152,7 @@ impl AllLanguageSettings {
|
||||
.copilot
|
||||
.disabled_globs
|
||||
.iter()
|
||||
.any(|glob| glob.matches_path(path))
|
||||
.any(|glob| glob.is_match(path))
|
||||
}
|
||||
|
||||
pub fn copilot_enabled(&self, language_name: Option<&str>, path: Option<&Path>) -> bool {
|
||||
@ -236,7 +237,7 @@ impl settings::Setting for AllLanguageSettings {
|
||||
feature_enabled: copilot_enabled,
|
||||
disabled_globs: copilot_globs
|
||||
.iter()
|
||||
.filter_map(|pattern| glob::Pattern::new(pattern).ok())
|
||||
.filter_map(|g| Some(globset::Glob::new(g).ok()?.compile_matcher()))
|
||||
.collect(),
|
||||
},
|
||||
defaults,
|
||||
|
@ -20,3 +20,6 @@ settings = { path = "../settings" }
|
||||
util = { path = "../util" }
|
||||
workspace = { path = "../workspace" }
|
||||
anyhow.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { path = "../editor", features = ["test-support"] }
|
||||
|
@ -6,8 +6,8 @@
|
||||
"repositoryURL": "https://github.com/livekit/client-sdk-swift.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "f6ca534eb334e99acb8e82cc99b491717df28d8a",
|
||||
"version": null
|
||||
"revision": "7331b813a5ab8a95cfb81fb2b4ed10519428b9ff",
|
||||
"version": "1.0.12"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -15,8 +15,8 @@
|
||||
"repositoryURL": "https://github.com/google/promises.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "3e4e743631e86c8c70dbc6efdc7beaa6e90fd3bb",
|
||||
"version": "2.1.1"
|
||||
"revision": "ec957ccddbcc710ccc64c9dcbd4c7006fcf8b73a",
|
||||
"version": "2.2.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -24,8 +24,8 @@
|
||||
"repositoryURL": "https://github.com/webrtc-sdk/Specs.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "38ac06261e62f980652278c69b70284324c769e0",
|
||||
"version": "104.5112.5"
|
||||
"revision": "2f6bab30c8df0fe59ab3e58bc99097f757f85f65",
|
||||
"version": "104.5112.17"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -33,8 +33,8 @@
|
||||
"repositoryURL": "https://github.com/apple/swift-log.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "6fe203dc33195667ce1759bf0182975e4653ba1c",
|
||||
"version": "1.4.4"
|
||||
"revision": "32e8d724467f8fe623624570367e3d50c5638e46",
|
||||
"version": "1.5.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -42,8 +42,8 @@
|
||||
"repositoryURL": "https://github.com/apple/swift-protobuf.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "88c7d15e1242fdb6ecbafbc7926426a19be1e98a",
|
||||
"version": "1.20.2"
|
||||
"revision": "0af9125c4eae12a4973fb66574c53a54962a9e1e",
|
||||
"version": "1.21.0"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -15,7 +15,7 @@ let package = Package(
|
||||
targets: ["LiveKitBridge"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/livekit/client-sdk-swift.git", revision: "f6ca534eb334e99acb8e82cc99b491717df28d8a"),
|
||||
.package(url: "https://github.com/livekit/client-sdk-swift.git", .exact("1.0.12")),
|
||||
],
|
||||
targets: [
|
||||
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
||||
|
@ -24,6 +24,7 @@ serde.workspace = true
|
||||
anyhow.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { path = "../editor", features = ["test-support"] }
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
util = { path = "../util", features = ["test-support"] }
|
||||
unindent.workspace = true
|
||||
|
@ -22,3 +22,6 @@ workspace = { path = "../workspace" }
|
||||
ordered-float.workspace = true
|
||||
postage.workspace = true
|
||||
smol.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { path = "../editor", features = ["test-support"] }
|
||||
|
@ -20,6 +20,7 @@ workspace = { path = "../workspace" }
|
||||
parking_lot.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { path = "../editor", features = ["test-support"] }
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
serde_json.workspace = true
|
||||
workspace = { path = "../workspace", features = ["test-support"] }
|
||||
|
@ -42,7 +42,7 @@ anyhow.workspace = true
|
||||
async-trait.workspace = true
|
||||
backtrace = "0.3"
|
||||
futures.workspace = true
|
||||
glob.workspace = true
|
||||
globset.workspace = true
|
||||
ignore = "0.4"
|
||||
lazy_static.workspace = true
|
||||
log.workspace = true
|
||||
|
@ -1,121 +0,0 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct LspGlobSet {
|
||||
patterns: Vec<glob::Pattern>,
|
||||
}
|
||||
|
||||
impl LspGlobSet {
|
||||
pub fn clear(&mut self) {
|
||||
self.patterns.clear();
|
||||
}
|
||||
|
||||
/// Add a pattern to the glob set.
|
||||
///
|
||||
/// LSP's glob syntax supports bash-style brace expansion. For example,
|
||||
/// the pattern '*.{js,ts}' would match all JavaScript or TypeScript files.
|
||||
/// This is not a part of the standard libc glob syntax, and isn't supported
|
||||
/// by the `glob` crate. So we pre-process the glob patterns, producing a
|
||||
/// separate glob `Pattern` object for each part of a brace expansion.
|
||||
pub fn add_pattern(&mut self, pattern: &str) -> Result<()> {
|
||||
// Find all of the ranges of `pattern` that contain matched curly braces.
|
||||
let mut expansion_ranges = Vec::new();
|
||||
let mut expansion_start_ix = None;
|
||||
for (ix, c) in pattern.match_indices(|c| ['{', '}'].contains(&c)) {
|
||||
match c {
|
||||
"{" => {
|
||||
if expansion_start_ix.is_some() {
|
||||
return Err(anyhow!("nested braces in glob patterns aren't supported"));
|
||||
}
|
||||
expansion_start_ix = Some(ix);
|
||||
}
|
||||
"}" => {
|
||||
if let Some(start_ix) = expansion_start_ix {
|
||||
expansion_ranges.push(start_ix..ix + 1);
|
||||
}
|
||||
expansion_start_ix = None;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Starting with a single pattern, process each brace expansion by cloning
|
||||
// the pattern once per element of the expansion.
|
||||
let mut unexpanded_patterns = vec![];
|
||||
let mut expanded_patterns = vec![pattern.to_string()];
|
||||
|
||||
for outer_range in expansion_ranges.into_iter().rev() {
|
||||
let inner_range = (outer_range.start + 1)..(outer_range.end - 1);
|
||||
std::mem::swap(&mut unexpanded_patterns, &mut expanded_patterns);
|
||||
for unexpanded_pattern in unexpanded_patterns.drain(..) {
|
||||
for part in unexpanded_pattern[inner_range.clone()].split(',') {
|
||||
let mut expanded_pattern = unexpanded_pattern.clone();
|
||||
expanded_pattern.replace_range(outer_range.clone(), part);
|
||||
expanded_patterns.push(expanded_pattern);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the final glob patterns and add them to the set.
|
||||
for pattern in expanded_patterns {
|
||||
let pattern = glob::Pattern::new(&pattern)?;
|
||||
self.patterns.push(pattern);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn matches(&self, path: &Path) -> bool {
|
||||
self.patterns
|
||||
.iter()
|
||||
.any(|pattern| pattern.matches_path(path))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_glob_set() {
|
||||
let mut watch = LspGlobSet::default();
|
||||
watch.add_pattern("/a/**/*.rs").unwrap();
|
||||
watch.add_pattern("/a/**/Cargo.toml").unwrap();
|
||||
|
||||
assert!(watch.matches("/a/b.rs".as_ref()));
|
||||
assert!(watch.matches("/a/b/c.rs".as_ref()));
|
||||
|
||||
assert!(!watch.matches("/b/c.rs".as_ref()));
|
||||
assert!(!watch.matches("/a/b.ts".as_ref()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_brace_expansion() {
|
||||
let mut watch = LspGlobSet::default();
|
||||
watch.add_pattern("/a/*.{ts,js,tsx}").unwrap();
|
||||
|
||||
assert!(watch.matches("/a/one.js".as_ref()));
|
||||
assert!(watch.matches("/a/two.ts".as_ref()));
|
||||
assert!(watch.matches("/a/three.tsx".as_ref()));
|
||||
|
||||
assert!(!watch.matches("/a/one.j".as_ref()));
|
||||
assert!(!watch.matches("/a/two.s".as_ref()));
|
||||
assert!(!watch.matches("/a/three.t".as_ref()));
|
||||
assert!(!watch.matches("/a/four.t".as_ref()));
|
||||
assert!(!watch.matches("/a/five.xt".as_ref()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_brace_expansion() {
|
||||
let mut watch = LspGlobSet::default();
|
||||
watch.add_pattern("/a/{one,two,three}.{b*c,d*e}").unwrap();
|
||||
|
||||
assert!(watch.matches("/a/one.bic".as_ref()));
|
||||
assert!(watch.matches("/a/two.dole".as_ref()));
|
||||
assert!(watch.matches("/a/three.deeee".as_ref()));
|
||||
|
||||
assert!(!watch.matches("/a/four.bic".as_ref()));
|
||||
assert!(!watch.matches("/a/one.be".as_ref()));
|
||||
}
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
mod ignore;
|
||||
mod lsp_command;
|
||||
mod lsp_glob_set;
|
||||
mod project_settings;
|
||||
pub mod search;
|
||||
pub mod terminals;
|
||||
@ -17,8 +16,10 @@ use copilot::Copilot;
|
||||
use futures::{
|
||||
channel::mpsc::{self, UnboundedReceiver},
|
||||
future::{try_join_all, Shared},
|
||||
stream::FuturesUnordered,
|
||||
AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt,
|
||||
};
|
||||
use globset::{Glob, GlobSet, GlobSetBuilder};
|
||||
use gpui::{
|
||||
AnyModelHandle, AppContext, AsyncAppContext, BorrowAppContext, Entity, ModelContext,
|
||||
ModelHandle, Task, WeakModelHandle,
|
||||
@ -41,7 +42,6 @@ use lsp::{
|
||||
DocumentHighlightKind, LanguageServer, LanguageServerId,
|
||||
};
|
||||
use lsp_command::*;
|
||||
use lsp_glob_set::LspGlobSet;
|
||||
use postage::watch;
|
||||
use project_settings::ProjectSettings;
|
||||
use rand::prelude::*;
|
||||
@ -213,6 +213,7 @@ pub enum Event {
|
||||
RemoteIdChanged(Option<u64>),
|
||||
DisconnectedFromHost,
|
||||
Closed,
|
||||
DeletedEntry(ProjectEntryId),
|
||||
CollaboratorUpdated {
|
||||
old_peer_id: proto::PeerId,
|
||||
new_peer_id: proto::PeerId,
|
||||
@ -226,7 +227,7 @@ pub enum LanguageServerState {
|
||||
language: Arc<Language>,
|
||||
adapter: Arc<CachedLspAdapter>,
|
||||
server: Arc<LanguageServer>,
|
||||
watched_paths: LspGlobSet,
|
||||
watched_paths: HashMap<WorktreeId, GlobSet>,
|
||||
simulate_disk_based_diagnostics_completion: Option<Task<()>>,
|
||||
},
|
||||
}
|
||||
@ -977,6 +978,9 @@ impl Project {
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Option<Task<Result<()>>> {
|
||||
let worktree = self.worktree_for_entry(entry_id, cx)?;
|
||||
|
||||
cx.emit(Event::DeletedEntry(entry_id));
|
||||
|
||||
if self.is_local() {
|
||||
worktree.update(cx, |worktree, cx| {
|
||||
worktree.as_local_mut().unwrap().delete_entry(entry_id, cx)
|
||||
@ -1371,7 +1375,7 @@ impl Project {
|
||||
return Task::ready(Ok(existing_buffer));
|
||||
}
|
||||
|
||||
let mut loading_watch = match self.loading_buffers_by_path.entry(project_path.clone()) {
|
||||
let loading_watch = match self.loading_buffers_by_path.entry(project_path.clone()) {
|
||||
// If the given path is already being loaded, then wait for that existing
|
||||
// task to complete and return the same buffer.
|
||||
hash_map::Entry::Occupied(e) => e.get().clone(),
|
||||
@ -1402,15 +1406,9 @@ impl Project {
|
||||
};
|
||||
|
||||
cx.foreground().spawn(async move {
|
||||
loop {
|
||||
if let Some(result) = loading_watch.borrow().as_ref() {
|
||||
match result {
|
||||
Ok(buffer) => return Ok(buffer.clone()),
|
||||
Err(error) => return Err(anyhow!("{}", error)),
|
||||
}
|
||||
}
|
||||
loading_watch.next().await;
|
||||
}
|
||||
pump_loading_buffer_reciever(loading_watch)
|
||||
.await
|
||||
.map_err(|error| anyhow!("{}", error))
|
||||
})
|
||||
}
|
||||
|
||||
@ -2562,6 +2560,23 @@ impl Project {
|
||||
}
|
||||
}
|
||||
|
||||
for buffer in self.opened_buffers.values() {
|
||||
if let Some(buffer) = buffer.upgrade(cx) {
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.update_diagnostics(server_id, Default::default(), cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
for worktree in &self.worktrees {
|
||||
if let Some(worktree) = worktree.upgrade(cx) {
|
||||
worktree.update(cx, |worktree, cx| {
|
||||
if let Some(worktree) = worktree.as_local_mut() {
|
||||
worktree.clear_diagnostics_for_language_server(server_id, cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
self.language_server_statuses.remove(&server_id);
|
||||
cx.notify();
|
||||
|
||||
@ -2867,10 +2882,37 @@ impl Project {
|
||||
if let Some(LanguageServerState::Running { watched_paths, .. }) =
|
||||
self.language_servers.get_mut(&language_server_id)
|
||||
{
|
||||
watched_paths.clear();
|
||||
let mut builders = HashMap::default();
|
||||
for watcher in params.watchers {
|
||||
watched_paths.add_pattern(&watcher.glob_pattern).log_err();
|
||||
for worktree in &self.worktrees {
|
||||
if let Some(worktree) = worktree.upgrade(cx) {
|
||||
let worktree = worktree.read(cx);
|
||||
if let Some(abs_path) = worktree.abs_path().to_str() {
|
||||
if let Some(suffix) = watcher
|
||||
.glob_pattern
|
||||
.strip_prefix(abs_path)
|
||||
.and_then(|s| s.strip_prefix(std::path::MAIN_SEPARATOR))
|
||||
{
|
||||
if let Some(glob) = Glob::new(suffix).log_err() {
|
||||
builders
|
||||
.entry(worktree.id())
|
||||
.or_insert_with(|| GlobSetBuilder::new())
|
||||
.add(glob);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
watched_paths.clear();
|
||||
for (worktree_id, builder) in builders {
|
||||
if let Ok(globset) = builder.build() {
|
||||
watched_paths.insert(worktree_id, globset);
|
||||
}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
@ -4707,10 +4749,23 @@ impl Project {
|
||||
changes: &HashMap<(Arc<Path>, ProjectEntryId), PathChange>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
if changes.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let worktree_id = worktree_handle.read(cx).id();
|
||||
let mut language_server_ids = self
|
||||
.language_server_ids
|
||||
.iter()
|
||||
.filter_map(|((server_worktree_id, _), server_id)| {
|
||||
(*server_worktree_id == worktree_id).then_some(*server_id)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
language_server_ids.sort();
|
||||
language_server_ids.dedup();
|
||||
|
||||
let abs_path = worktree_handle.read(cx).abs_path();
|
||||
for ((server_worktree_id, _), server_id) in &self.language_server_ids {
|
||||
if *server_worktree_id == worktree_id {
|
||||
for server_id in &language_server_ids {
|
||||
if let Some(server) = self.language_servers.get(server_id) {
|
||||
if let LanguageServerState::Running {
|
||||
server,
|
||||
@ -4718,14 +4773,15 @@ impl Project {
|
||||
..
|
||||
} = server
|
||||
{
|
||||
if let Some(watched_paths) = watched_paths.get(&worktree_id) {
|
||||
let params = lsp::DidChangeWatchedFilesParams {
|
||||
changes: changes
|
||||
.iter()
|
||||
.filter_map(|((path, _), change)| {
|
||||
let path = abs_path.join(path);
|
||||
if watched_paths.matches(&path) {
|
||||
if watched_paths.is_match(&path) {
|
||||
Some(lsp::FileEvent {
|
||||
uri: lsp::Url::from_file_path(path).unwrap(),
|
||||
uri: lsp::Url::from_file_path(abs_path.join(path))
|
||||
.unwrap(),
|
||||
typ: match change {
|
||||
PathChange::Added => lsp::FileChangeType::CREATED,
|
||||
PathChange::Removed => lsp::FileChangeType::DELETED,
|
||||
@ -4761,6 +4817,51 @@ impl Project {
|
||||
) {
|
||||
debug_assert!(worktree_handle.read(cx).is_local());
|
||||
|
||||
// Setup the pending buffers
|
||||
let future_buffers = self
|
||||
.loading_buffers_by_path
|
||||
.iter()
|
||||
.filter_map(|(path, receiver)| {
|
||||
let path = &path.path;
|
||||
let (work_directory, repo) = repos
|
||||
.iter()
|
||||
.find(|(work_directory, _)| path.starts_with(work_directory))?;
|
||||
|
||||
let repo_relative_path = path.strip_prefix(work_directory).log_err()?;
|
||||
|
||||
let receiver = receiver.clone();
|
||||
let repo_ptr = repo.repo_ptr.clone();
|
||||
let repo_relative_path = repo_relative_path.to_owned();
|
||||
Some(async move {
|
||||
pump_loading_buffer_reciever(receiver)
|
||||
.await
|
||||
.ok()
|
||||
.map(|buffer| (buffer, repo_relative_path, repo_ptr))
|
||||
})
|
||||
})
|
||||
.collect::<FuturesUnordered<_>>()
|
||||
.filter_map(|result| async move {
|
||||
let (buffer_handle, repo_relative_path, repo_ptr) = result?;
|
||||
|
||||
let lock = repo_ptr.lock();
|
||||
lock.load_index_text(&repo_relative_path)
|
||||
.map(|diff_base| (diff_base, buffer_handle))
|
||||
});
|
||||
|
||||
let update_diff_base_fn = update_diff_base(self);
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
let diff_base_tasks = cx
|
||||
.background()
|
||||
.spawn(future_buffers.collect::<Vec<_>>())
|
||||
.await;
|
||||
|
||||
for (diff_base, buffer) in diff_base_tasks.into_iter() {
|
||||
update_diff_base_fn(Some(diff_base), buffer, &mut cx);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
// And the current buffers
|
||||
for (_, buffer) in &self.opened_buffers {
|
||||
if let Some(buffer) = buffer.upgrade(cx) {
|
||||
let file = match File::from_dyn(buffer.read(cx).file()) {
|
||||
@ -4780,18 +4881,17 @@ impl Project {
|
||||
.find(|(work_directory, _)| path.starts_with(work_directory))
|
||||
{
|
||||
Some(repo) => repo.clone(),
|
||||
None => return,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let relative_repo = match path.strip_prefix(work_directory).log_err() {
|
||||
Some(relative_repo) => relative_repo.to_owned(),
|
||||
None => return,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
drop(worktree);
|
||||
|
||||
let remote_id = self.remote_id();
|
||||
let client = self.client.clone();
|
||||
let update_diff_base_fn = update_diff_base(self);
|
||||
let git_ptr = repo.repo_ptr.clone();
|
||||
let diff_base_task = cx
|
||||
.background()
|
||||
@ -4799,21 +4899,7 @@ impl Project {
|
||||
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
let diff_base = diff_base_task.await;
|
||||
|
||||
let buffer_id = buffer.update(&mut cx, |buffer, cx| {
|
||||
buffer.set_diff_base(diff_base.clone(), cx);
|
||||
buffer.remote_id()
|
||||
});
|
||||
|
||||
if let Some(project_id) = remote_id {
|
||||
client
|
||||
.send(proto::UpdateDiffBase {
|
||||
project_id,
|
||||
buffer_id: buffer_id as u64,
|
||||
diff_base,
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
update_diff_base_fn(diff_base, buffer, &mut cx);
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
@ -5146,6 +5232,9 @@ impl Project {
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<proto::ProjectEntryResponse> {
|
||||
let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id);
|
||||
|
||||
this.update(&mut cx, |_, cx| cx.emit(Event::DeletedEntry(entry_id)));
|
||||
|
||||
let worktree = this.read_with(&cx, |this, cx| {
|
||||
this.worktree_for_entry(entry_id, cx)
|
||||
.ok_or_else(|| anyhow!("worktree not found"))
|
||||
@ -6700,3 +6789,40 @@ impl Item for Buffer {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn pump_loading_buffer_reciever(
|
||||
mut receiver: postage::watch::Receiver<Option<Result<ModelHandle<Buffer>, Arc<anyhow::Error>>>>,
|
||||
) -> Result<ModelHandle<Buffer>, Arc<anyhow::Error>> {
|
||||
loop {
|
||||
if let Some(result) = receiver.borrow().as_ref() {
|
||||
match result {
|
||||
Ok(buffer) => return Ok(buffer.to_owned()),
|
||||
Err(e) => return Err(e.to_owned()),
|
||||
}
|
||||
}
|
||||
receiver.next().await;
|
||||
}
|
||||
}
|
||||
|
||||
fn update_diff_base(
|
||||
project: &Project,
|
||||
) -> impl Fn(Option<String>, ModelHandle<Buffer>, &mut AsyncAppContext) {
|
||||
let remote_id = project.remote_id();
|
||||
let client = project.client().clone();
|
||||
move |diff_base, buffer, cx| {
|
||||
let buffer_id = buffer.update(cx, |buffer, cx| {
|
||||
buffer.set_diff_base(diff_base.clone(), cx);
|
||||
buffer.remote_id()
|
||||
});
|
||||
|
||||
if let Some(project_id) = remote_id {
|
||||
client
|
||||
.send(proto::UpdateDiffBase {
|
||||
project_id,
|
||||
buffer_id: buffer_id as u64,
|
||||
diff_base,
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};
|
||||
use settings::Setting;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ProjectSettings {
|
||||
#[serde(default)]
|
||||
pub lsp: HashMap<Arc<str>, LspSettings>,
|
||||
|
@ -1,6 +1,7 @@
|
||||
use crate::{worktree::WorktreeHandle, Event, *};
|
||||
use fs::{FakeFs, LineEnding, RealFs};
|
||||
use futures::{future, StreamExt};
|
||||
use globset::Glob;
|
||||
use gpui::{executor::Deterministic, test::subscribe, AppContext};
|
||||
use language::{
|
||||
language_settings::{AllLanguageSettings, LanguageSettingsContent},
|
||||
@ -505,7 +506,7 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
|
||||
register_options: serde_json::to_value(
|
||||
lsp::DidChangeWatchedFilesRegistrationOptions {
|
||||
watchers: vec![lsp::FileSystemWatcher {
|
||||
glob_pattern: "*.{rs,c}".to_string(),
|
||||
glob_pattern: "/the-root/*.{rs,c}".to_string(),
|
||||
kind: None,
|
||||
}],
|
||||
},
|
||||
@ -925,6 +926,95 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let mut language = Language::new(
|
||||
LanguageConfig {
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
None,
|
||||
);
|
||||
let mut fake_servers = language
|
||||
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
|
||||
..Default::default()
|
||||
}))
|
||||
.await;
|
||||
|
||||
let fs = FakeFs::new(cx.background());
|
||||
fs.insert_tree("/dir", json!({ "a.rs": "x" })).await;
|
||||
|
||||
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
|
||||
project.update(cx, |project, _| project.languages.add(Arc::new(language)));
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Publish diagnostics
|
||||
let fake_server = fake_servers.next().await.unwrap();
|
||||
fake_server.notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
|
||||
uri: Url::from_file_path("/dir/a.rs").unwrap(),
|
||||
version: None,
|
||||
diagnostics: vec![lsp::Diagnostic {
|
||||
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
|
||||
severity: Some(lsp::DiagnosticSeverity::ERROR),
|
||||
message: "the message".to_string(),
|
||||
..Default::default()
|
||||
}],
|
||||
});
|
||||
|
||||
cx.foreground().run_until_parked();
|
||||
buffer.read_with(cx, |buffer, _| {
|
||||
assert_eq!(
|
||||
buffer
|
||||
.snapshot()
|
||||
.diagnostics_in_range::<_, usize>(0..1, false)
|
||||
.map(|entry| entry.diagnostic.message.clone())
|
||||
.collect::<Vec<_>>(),
|
||||
["the message".to_string()]
|
||||
);
|
||||
});
|
||||
project.read_with(cx, |project, cx| {
|
||||
assert_eq!(
|
||||
project.diagnostic_summary(cx),
|
||||
DiagnosticSummary {
|
||||
error_count: 1,
|
||||
warning_count: 0,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
project.update(cx, |project, cx| {
|
||||
project.restart_language_servers_for_buffers([buffer.clone()], cx);
|
||||
});
|
||||
|
||||
// The diagnostics are cleared.
|
||||
cx.foreground().run_until_parked();
|
||||
buffer.read_with(cx, |buffer, _| {
|
||||
assert_eq!(
|
||||
buffer
|
||||
.snapshot()
|
||||
.diagnostics_in_range::<_, usize>(0..1, false)
|
||||
.map(|entry| entry.diagnostic.message.clone())
|
||||
.collect::<Vec<_>>(),
|
||||
Vec::<String>::new(),
|
||||
);
|
||||
});
|
||||
project.read_with(cx, |project, cx| {
|
||||
assert_eq!(
|
||||
project.diagnostic_summary(cx),
|
||||
DiagnosticSummary {
|
||||
error_count: 0,
|
||||
warning_count: 0,
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_restarted_server_reporting_invalid_buffer_version(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
@ -3393,7 +3483,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
|
||||
search_query,
|
||||
false,
|
||||
true,
|
||||
vec![glob::Pattern::new("*.odd").unwrap()],
|
||||
vec![Glob::new("*.odd").unwrap().compile_matcher()],
|
||||
Vec::new()
|
||||
),
|
||||
cx
|
||||
@ -3411,7 +3501,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
|
||||
search_query,
|
||||
false,
|
||||
true,
|
||||
vec![glob::Pattern::new("*.rs").unwrap()],
|
||||
vec![Glob::new("*.rs").unwrap().compile_matcher()],
|
||||
Vec::new()
|
||||
),
|
||||
cx
|
||||
@ -3433,8 +3523,8 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
|
||||
false,
|
||||
true,
|
||||
vec![
|
||||
glob::Pattern::new("*.ts").unwrap(),
|
||||
glob::Pattern::new("*.odd").unwrap(),
|
||||
Glob::new("*.ts").unwrap().compile_matcher(),
|
||||
Glob::new("*.odd").unwrap().compile_matcher(),
|
||||
],
|
||||
Vec::new()
|
||||
),
|
||||
@ -3457,9 +3547,9 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
|
||||
false,
|
||||
true,
|
||||
vec![
|
||||
glob::Pattern::new("*.rs").unwrap(),
|
||||
glob::Pattern::new("*.ts").unwrap(),
|
||||
glob::Pattern::new("*.odd").unwrap(),
|
||||
Glob::new("*.rs").unwrap().compile_matcher(),
|
||||
Glob::new("*.ts").unwrap().compile_matcher(),
|
||||
Glob::new("*.odd").unwrap().compile_matcher(),
|
||||
],
|
||||
Vec::new()
|
||||
),
|
||||
@ -3504,7 +3594,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
|
||||
false,
|
||||
true,
|
||||
Vec::new(),
|
||||
vec![glob::Pattern::new("*.odd").unwrap()],
|
||||
vec![Glob::new("*.odd").unwrap().compile_matcher()],
|
||||
),
|
||||
cx
|
||||
)
|
||||
@ -3527,7 +3617,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
|
||||
false,
|
||||
true,
|
||||
Vec::new(),
|
||||
vec![glob::Pattern::new("*.rs").unwrap()],
|
||||
vec![Glob::new("*.rs").unwrap().compile_matcher()],
|
||||
),
|
||||
cx
|
||||
)
|
||||
@ -3549,8 +3639,8 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
|
||||
true,
|
||||
Vec::new(),
|
||||
vec![
|
||||
glob::Pattern::new("*.ts").unwrap(),
|
||||
glob::Pattern::new("*.odd").unwrap(),
|
||||
Glob::new("*.ts").unwrap().compile_matcher(),
|
||||
Glob::new("*.odd").unwrap().compile_matcher(),
|
||||
],
|
||||
),
|
||||
cx
|
||||
@ -3573,9 +3663,9 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
|
||||
true,
|
||||
Vec::new(),
|
||||
vec![
|
||||
glob::Pattern::new("*.rs").unwrap(),
|
||||
glob::Pattern::new("*.ts").unwrap(),
|
||||
glob::Pattern::new("*.odd").unwrap(),
|
||||
Glob::new("*.rs").unwrap().compile_matcher(),
|
||||
Glob::new("*.ts").unwrap().compile_matcher(),
|
||||
Glob::new("*.odd").unwrap().compile_matcher(),
|
||||
],
|
||||
),
|
||||
cx
|
||||
@ -3612,8 +3702,8 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
|
||||
search_query,
|
||||
false,
|
||||
true,
|
||||
vec![glob::Pattern::new("*.odd").unwrap()],
|
||||
vec![glob::Pattern::new("*.odd").unwrap()],
|
||||
vec![Glob::new("*.odd").unwrap().compile_matcher()],
|
||||
vec![Glob::new("*.odd").unwrap().compile_matcher()],
|
||||
),
|
||||
cx
|
||||
)
|
||||
@ -3630,8 +3720,8 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
|
||||
search_query,
|
||||
false,
|
||||
true,
|
||||
vec![glob::Pattern::new("*.ts").unwrap()],
|
||||
vec![glob::Pattern::new("*.ts").unwrap()],
|
||||
vec![Glob::new("*.ts").unwrap().compile_matcher()],
|
||||
vec![Glob::new("*.ts").unwrap().compile_matcher()],
|
||||
),
|
||||
cx
|
||||
)
|
||||
@ -3649,12 +3739,12 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
|
||||
false,
|
||||
true,
|
||||
vec![
|
||||
glob::Pattern::new("*.ts").unwrap(),
|
||||
glob::Pattern::new("*.odd").unwrap()
|
||||
Glob::new("*.ts").unwrap().compile_matcher(),
|
||||
Glob::new("*.odd").unwrap().compile_matcher()
|
||||
],
|
||||
vec![
|
||||
glob::Pattern::new("*.ts").unwrap(),
|
||||
glob::Pattern::new("*.odd").unwrap()
|
||||
Glob::new("*.ts").unwrap().compile_matcher(),
|
||||
Glob::new("*.odd").unwrap().compile_matcher()
|
||||
],
|
||||
),
|
||||
cx
|
||||
@ -3673,12 +3763,12 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
|
||||
false,
|
||||
true,
|
||||
vec![
|
||||
glob::Pattern::new("*.ts").unwrap(),
|
||||
glob::Pattern::new("*.odd").unwrap()
|
||||
Glob::new("*.ts").unwrap().compile_matcher(),
|
||||
Glob::new("*.odd").unwrap().compile_matcher()
|
||||
],
|
||||
vec![
|
||||
glob::Pattern::new("*.rs").unwrap(),
|
||||
glob::Pattern::new("*.odd").unwrap()
|
||||
Glob::new("*.rs").unwrap().compile_matcher(),
|
||||
Glob::new("*.odd").unwrap().compile_matcher()
|
||||
],
|
||||
),
|
||||
cx
|
||||
|
@ -1,6 +1,7 @@
|
||||
use aho_corasick::{AhoCorasick, AhoCorasickBuilder};
|
||||
use anyhow::Result;
|
||||
use client::proto;
|
||||
use globset::{Glob, GlobMatcher};
|
||||
use itertools::Itertools;
|
||||
use language::{char_kind, Rope};
|
||||
use regex::{Regex, RegexBuilder};
|
||||
@ -19,8 +20,8 @@ pub enum SearchQuery {
|
||||
query: Arc<str>,
|
||||
whole_word: bool,
|
||||
case_sensitive: bool,
|
||||
files_to_include: Vec<glob::Pattern>,
|
||||
files_to_exclude: Vec<glob::Pattern>,
|
||||
files_to_include: Vec<GlobMatcher>,
|
||||
files_to_exclude: Vec<GlobMatcher>,
|
||||
},
|
||||
Regex {
|
||||
regex: Regex,
|
||||
@ -28,8 +29,8 @@ pub enum SearchQuery {
|
||||
multiline: bool,
|
||||
whole_word: bool,
|
||||
case_sensitive: bool,
|
||||
files_to_include: Vec<glob::Pattern>,
|
||||
files_to_exclude: Vec<glob::Pattern>,
|
||||
files_to_include: Vec<GlobMatcher>,
|
||||
files_to_exclude: Vec<GlobMatcher>,
|
||||
},
|
||||
}
|
||||
|
||||
@ -38,8 +39,8 @@ impl SearchQuery {
|
||||
query: impl ToString,
|
||||
whole_word: bool,
|
||||
case_sensitive: bool,
|
||||
files_to_include: Vec<glob::Pattern>,
|
||||
files_to_exclude: Vec<glob::Pattern>,
|
||||
files_to_include: Vec<GlobMatcher>,
|
||||
files_to_exclude: Vec<GlobMatcher>,
|
||||
) -> Self {
|
||||
let query = query.to_string();
|
||||
let search = AhoCorasickBuilder::new()
|
||||
@ -60,8 +61,8 @@ impl SearchQuery {
|
||||
query: impl ToString,
|
||||
whole_word: bool,
|
||||
case_sensitive: bool,
|
||||
files_to_include: Vec<glob::Pattern>,
|
||||
files_to_exclude: Vec<glob::Pattern>,
|
||||
files_to_include: Vec<GlobMatcher>,
|
||||
files_to_exclude: Vec<GlobMatcher>,
|
||||
) -> Result<Self> {
|
||||
let mut query = query.to_string();
|
||||
let initial_query = Arc::from(query.as_str());
|
||||
@ -95,40 +96,16 @@ impl SearchQuery {
|
||||
message.query,
|
||||
message.whole_word,
|
||||
message.case_sensitive,
|
||||
message
|
||||
.files_to_include
|
||||
.split(',')
|
||||
.map(str::trim)
|
||||
.filter(|glob_str| !glob_str.is_empty())
|
||||
.map(|glob_str| glob::Pattern::new(glob_str))
|
||||
.collect::<Result<_, _>>()?,
|
||||
message
|
||||
.files_to_exclude
|
||||
.split(',')
|
||||
.map(str::trim)
|
||||
.filter(|glob_str| !glob_str.is_empty())
|
||||
.map(|glob_str| glob::Pattern::new(glob_str))
|
||||
.collect::<Result<_, _>>()?,
|
||||
deserialize_globs(&message.files_to_include)?,
|
||||
deserialize_globs(&message.files_to_exclude)?,
|
||||
)
|
||||
} else {
|
||||
Ok(Self::text(
|
||||
message.query,
|
||||
message.whole_word,
|
||||
message.case_sensitive,
|
||||
message
|
||||
.files_to_include
|
||||
.split(',')
|
||||
.map(str::trim)
|
||||
.filter(|glob_str| !glob_str.is_empty())
|
||||
.map(|glob_str| glob::Pattern::new(glob_str))
|
||||
.collect::<Result<_, _>>()?,
|
||||
message
|
||||
.files_to_exclude
|
||||
.split(',')
|
||||
.map(str::trim)
|
||||
.filter(|glob_str| !glob_str.is_empty())
|
||||
.map(|glob_str| glob::Pattern::new(glob_str))
|
||||
.collect::<Result<_, _>>()?,
|
||||
deserialize_globs(&message.files_to_include)?,
|
||||
deserialize_globs(&message.files_to_exclude)?,
|
||||
))
|
||||
}
|
||||
}
|
||||
@ -143,12 +120,12 @@ impl SearchQuery {
|
||||
files_to_include: self
|
||||
.files_to_include()
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.map(|g| g.glob().to_string())
|
||||
.join(","),
|
||||
files_to_exclude: self
|
||||
.files_to_exclude()
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.map(|g| g.glob().to_string())
|
||||
.join(","),
|
||||
}
|
||||
}
|
||||
@ -289,7 +266,7 @@ impl SearchQuery {
|
||||
matches!(self, Self::Regex { .. })
|
||||
}
|
||||
|
||||
pub fn files_to_include(&self) -> &[glob::Pattern] {
|
||||
pub fn files_to_include(&self) -> &[GlobMatcher] {
|
||||
match self {
|
||||
Self::Text {
|
||||
files_to_include, ..
|
||||
@ -300,7 +277,7 @@ impl SearchQuery {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn files_to_exclude(&self) -> &[glob::Pattern] {
|
||||
pub fn files_to_exclude(&self) -> &[GlobMatcher] {
|
||||
match self {
|
||||
Self::Text {
|
||||
files_to_exclude, ..
|
||||
@ -317,14 +294,23 @@ impl SearchQuery {
|
||||
!self
|
||||
.files_to_exclude()
|
||||
.iter()
|
||||
.any(|exclude_glob| exclude_glob.matches_path(file_path))
|
||||
.any(|exclude_glob| exclude_glob.is_match(file_path))
|
||||
&& (self.files_to_include().is_empty()
|
||||
|| self
|
||||
.files_to_include()
|
||||
.iter()
|
||||
.any(|include_glob| include_glob.matches_path(file_path)))
|
||||
.any(|include_glob| include_glob.is_match(file_path)))
|
||||
}
|
||||
None => self.files_to_include().is_empty(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_globs(glob_set: &str) -> Result<Vec<GlobMatcher>> {
|
||||
glob_set
|
||||
.split(',')
|
||||
.map(str::trim)
|
||||
.filter(|glob_str| !glob_str.is_empty())
|
||||
.map(|glob_str| Ok(Glob::new(glob_str)?.compile_matcher()))
|
||||
.collect()
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -10,6 +10,7 @@ doctest = false
|
||||
|
||||
[dependencies]
|
||||
context_menu = { path = "../context_menu" }
|
||||
db = { path = "../db" }
|
||||
drag_and_drop = { path = "../drag_and_drop" }
|
||||
editor = { path = "../editor" }
|
||||
gpui = { path = "../gpui" }
|
||||
@ -21,6 +22,11 @@ util = { path = "../util" }
|
||||
workspace = { path = "../workspace" }
|
||||
postage.workspace = true
|
||||
futures.workspace = true
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
serde_json.workspace = true
|
||||
anyhow.workspace = true
|
||||
schemars.workspace = true
|
||||
unicase = "2.6"
|
||||
|
||||
[dev-dependencies]
|
||||
|
@ -1,25 +1,31 @@
|
||||
mod project_panel_settings;
|
||||
|
||||
use context_menu::{ContextMenu, ContextMenuItem};
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use drag_and_drop::{DragAndDrop, Draggable};
|
||||
use editor::{Cancel, Editor};
|
||||
use futures::stream::StreamExt;
|
||||
use gpui::{
|
||||
actions,
|
||||
anyhow::{anyhow, Result},
|
||||
anyhow::{self, anyhow, Result},
|
||||
elements::{
|
||||
AnchorCorner, ChildView, ComponentHost, ContainerStyle, Empty, Flex, MouseEventHandler,
|
||||
AnchorCorner, ChildView, ContainerStyle, Empty, Flex, Label, MouseEventHandler,
|
||||
ParentElement, ScrollTarget, Stack, Svg, UniformList, UniformListState,
|
||||
},
|
||||
geometry::vector::Vector2F,
|
||||
keymap_matcher::KeymapContext,
|
||||
platform::{CursorStyle, MouseButton, PromptLevel},
|
||||
AnyElement, AppContext, ClipboardItem, Element, Entity, ModelHandle, Task, View, ViewContext,
|
||||
ViewHandle, WeakViewHandle,
|
||||
Action, AnyElement, AppContext, AsyncAppContext, ClipboardItem, Element, Entity, ModelHandle,
|
||||
Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
|
||||
};
|
||||
use menu::{Confirm, SelectNext, SelectPrev};
|
||||
use project::{
|
||||
repository::GitFileStatus, Entry, EntryKind, Project, ProjectEntryId, ProjectPath, Worktree,
|
||||
WorktreeId,
|
||||
repository::GitFileStatus, Entry, EntryKind, Fs, Project, ProjectEntryId, ProjectPath,
|
||||
Worktree, WorktreeId,
|
||||
};
|
||||
use project_panel_settings::{ProjectPanelDockPosition, ProjectPanelSettings};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::SettingsStore;
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
collections::{hash_map, HashMap},
|
||||
@ -28,14 +34,20 @@ use std::{
|
||||
path::Path,
|
||||
sync::Arc,
|
||||
};
|
||||
use theme::{ui::FileName, ProjectPanelEntry};
|
||||
use theme::ProjectPanelEntry;
|
||||
use unicase::UniCase;
|
||||
use workspace::Workspace;
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
use workspace::{
|
||||
dock::{DockPosition, Panel},
|
||||
Workspace,
|
||||
};
|
||||
|
||||
const PROJECT_PANEL_KEY: &'static str = "ProjectPanel";
|
||||
const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX;
|
||||
|
||||
pub struct ProjectPanel {
|
||||
project: ModelHandle<Project>,
|
||||
fs: Arc<dyn Fs>,
|
||||
list: UniformListState,
|
||||
visible_entries: Vec<(WorktreeId, Vec<Entry>)>,
|
||||
last_worktree_root_id: Option<ProjectEntryId>,
|
||||
@ -47,6 +59,9 @@ pub struct ProjectPanel {
|
||||
context_menu: ViewHandle<ContextMenu>,
|
||||
dragged_entry_destination: Option<Arc<Path>>,
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
has_focus: bool,
|
||||
width: Option<f32>,
|
||||
pending_serialization: Task<Option<()>>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
@ -110,7 +125,12 @@ actions!(
|
||||
]
|
||||
);
|
||||
|
||||
pub fn init_settings(cx: &mut AppContext) {
|
||||
settings::register::<ProjectPanelSettings>(cx);
|
||||
}
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
init_settings(cx);
|
||||
cx.add_action(ProjectPanel::expand_selected_entry);
|
||||
cx.add_action(ProjectPanel::collapse_selected_entry);
|
||||
cx.add_action(ProjectPanel::select_prev);
|
||||
@ -138,10 +158,17 @@ pub enum Event {
|
||||
entry_id: ProjectEntryId,
|
||||
focus_opened_item: bool,
|
||||
},
|
||||
DockPositionChanged,
|
||||
Focus,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SerializedProjectPanel {
|
||||
width: Option<f32>,
|
||||
}
|
||||
|
||||
impl ProjectPanel {
|
||||
pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
|
||||
fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
|
||||
let project = workspace.project().clone();
|
||||
let project_panel = cx.add_view(|cx: &mut ViewContext<Self>| {
|
||||
cx.observe(&project, |this, _, cx| {
|
||||
@ -202,6 +229,7 @@ impl ProjectPanel {
|
||||
let view_id = cx.view_id();
|
||||
let mut this = Self {
|
||||
project: project.clone(),
|
||||
fs: workspace.app_state().fs.clone(),
|
||||
list: Default::default(),
|
||||
visible_entries: Default::default(),
|
||||
last_worktree_root_id: Default::default(),
|
||||
@ -213,8 +241,23 @@ impl ProjectPanel {
|
||||
context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)),
|
||||
dragged_entry_destination: None,
|
||||
workspace: workspace.weak_handle(),
|
||||
has_focus: false,
|
||||
width: None,
|
||||
pending_serialization: Task::ready(None),
|
||||
};
|
||||
this.update_visible_entries(None, cx);
|
||||
|
||||
// Update the dock position when the setting changes.
|
||||
let mut old_dock_position = this.position(cx);
|
||||
cx.observe_global::<SettingsStore, _>(move |this, cx| {
|
||||
let new_dock_position = this.position(cx);
|
||||
if new_dock_position != old_dock_position {
|
||||
old_dock_position = new_dock_position;
|
||||
cx.emit(Event::DockPositionChanged);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
this
|
||||
});
|
||||
|
||||
@ -246,6 +289,7 @@ impl ProjectPanel {
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
@ -253,6 +297,51 @@ impl ProjectPanel {
|
||||
project_panel
|
||||
}
|
||||
|
||||
pub fn load(
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
cx: AsyncAppContext,
|
||||
) -> Task<Result<ViewHandle<Self>>> {
|
||||
cx.spawn(|mut cx| async move {
|
||||
let serialized_panel = if let Some(panel) = cx
|
||||
.background()
|
||||
.spawn(async move { KEY_VALUE_STORE.read_kvp(PROJECT_PANEL_KEY) })
|
||||
.await
|
||||
.log_err()
|
||||
.flatten()
|
||||
{
|
||||
Some(serde_json::from_str::<SerializedProjectPanel>(&panel)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
let panel = ProjectPanel::new(workspace, cx);
|
||||
if let Some(serialized_panel) = serialized_panel {
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.width = serialized_panel.width;
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
panel
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn serialize(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let width = self.width;
|
||||
self.pending_serialization = cx.background().spawn(
|
||||
async move {
|
||||
KEY_VALUE_STORE
|
||||
.write_kvp(
|
||||
PROJECT_PANEL_KEY.into(),
|
||||
serde_json::to_string(&SerializedProjectPanel { width })?,
|
||||
)
|
||||
.await?;
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err(),
|
||||
);
|
||||
}
|
||||
|
||||
fn deploy_context_menu(
|
||||
&mut self,
|
||||
position: Vector2F,
|
||||
@ -1000,6 +1089,7 @@ impl ProjectPanel {
|
||||
}
|
||||
|
||||
let end_ix = range.end.min(ix + visible_worktree_entries.len());
|
||||
let git_status_setting = settings::get::<ProjectPanelSettings>(cx).git_status;
|
||||
if let Some(worktree) = self.project.read(cx).worktree_for_id(*worktree_id, cx) {
|
||||
let snapshot = worktree.read(cx).snapshot();
|
||||
let root_name = OsStr::new(snapshot.root_name());
|
||||
@ -1010,14 +1100,13 @@ impl ProjectPanel {
|
||||
.unwrap_or(&[]);
|
||||
|
||||
let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
|
||||
for entry in &visible_worktree_entries[entry_range] {
|
||||
let path = &entry.path;
|
||||
let status = (entry.path.parent().is_some() && !entry.is_ignored)
|
||||
.then(|| {
|
||||
snapshot
|
||||
.repo_for(path)
|
||||
.and_then(|entry| entry.status_for_path(&snapshot, path))
|
||||
})
|
||||
for (entry, repo) in
|
||||
snapshot.entries_with_repositories(visible_worktree_entries[entry_range].iter())
|
||||
{
|
||||
let status = (git_status_setting
|
||||
&& entry.path.parent().is_some()
|
||||
&& !entry.is_ignored)
|
||||
.then(|| repo.and_then(|repo| repo.status_for_path(&snapshot, &entry.path)))
|
||||
.flatten();
|
||||
|
||||
let mut details = EntryDetails {
|
||||
@ -1082,6 +1171,17 @@ impl ProjectPanel {
|
||||
let kind = details.kind;
|
||||
let show_editor = details.is_editing && !details.is_processing;
|
||||
|
||||
let mut filename_text_style = style.text.clone();
|
||||
filename_text_style.color = details
|
||||
.git_status
|
||||
.as_ref()
|
||||
.map(|status| match status {
|
||||
GitFileStatus::Added => style.status.git.inserted,
|
||||
GitFileStatus::Modified => style.status.git.modified,
|
||||
GitFileStatus::Conflict => style.status.git.conflict,
|
||||
})
|
||||
.unwrap_or(style.text.color);
|
||||
|
||||
Flex::row()
|
||||
.with_child(
|
||||
if kind == EntryKind::Dir {
|
||||
@ -1109,11 +1209,7 @@ 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)),
|
||||
))
|
||||
Label::new(details.filename.clone(), filename_text_style)
|
||||
.contained()
|
||||
.with_margin_left(style.icon_spacing)
|
||||
.aligned()
|
||||
@ -1337,16 +1433,103 @@ impl View for ProjectPanel {
|
||||
Self::reset_to_default_keymap_context(keymap);
|
||||
keymap.add_identifier("menu");
|
||||
}
|
||||
|
||||
fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
if !self.has_focus {
|
||||
self.has_focus = true;
|
||||
cx.emit(Event::Focus);
|
||||
}
|
||||
}
|
||||
|
||||
fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
|
||||
self.has_focus = false;
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for ProjectPanel {
|
||||
type Event = Event;
|
||||
}
|
||||
|
||||
impl workspace::sidebar::SidebarItem for ProjectPanel {
|
||||
fn should_show_badge(&self, _: &AppContext) -> bool {
|
||||
impl workspace::dock::Panel for ProjectPanel {
|
||||
fn position(&self, cx: &WindowContext) -> DockPosition {
|
||||
match settings::get::<ProjectPanelSettings>(cx).dock {
|
||||
ProjectPanelDockPosition::Left => DockPosition::Left,
|
||||
ProjectPanelDockPosition::Right => DockPosition::Right,
|
||||
}
|
||||
}
|
||||
|
||||
fn position_is_valid(&self, position: DockPosition) -> bool {
|
||||
matches!(position, DockPosition::Left | DockPosition::Right)
|
||||
}
|
||||
|
||||
fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
|
||||
settings::update_settings_file::<ProjectPanelSettings>(
|
||||
self.fs.clone(),
|
||||
cx,
|
||||
move |settings| {
|
||||
let dock = match position {
|
||||
DockPosition::Left | DockPosition::Bottom => ProjectPanelDockPosition::Left,
|
||||
DockPosition::Right => ProjectPanelDockPosition::Right,
|
||||
};
|
||||
settings.dock = Some(dock);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn size(&self, cx: &WindowContext) -> f32 {
|
||||
self.width
|
||||
.unwrap_or_else(|| settings::get::<ProjectPanelSettings>(cx).default_width)
|
||||
}
|
||||
|
||||
fn set_size(&mut self, size: f32, cx: &mut ViewContext<Self>) {
|
||||
self.width = Some(size);
|
||||
self.serialize(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn should_zoom_in_on_event(_: &Self::Event) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn should_zoom_out_on_event(_: &Self::Event) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn is_zoomed(&self, _: &WindowContext) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn set_zoomed(&mut self, _: bool, _: &mut ViewContext<Self>) {}
|
||||
|
||||
fn set_active(&mut self, _: bool, _: &mut ViewContext<Self>) {}
|
||||
|
||||
fn icon_path(&self) -> &'static str {
|
||||
"icons/folder_tree_16.svg"
|
||||
}
|
||||
|
||||
fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>) {
|
||||
("Project Panel".into(), Some(Box::new(ToggleFocus)))
|
||||
}
|
||||
|
||||
fn should_change_position_on_event(event: &Self::Event) -> bool {
|
||||
matches!(event, Event::DockPositionChanged)
|
||||
}
|
||||
|
||||
fn should_activate_on_event(_: &Self::Event) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn should_close_on_event(_: &Self::Event) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn has_focus(&self, _: &WindowContext) -> bool {
|
||||
self.has_focus
|
||||
}
|
||||
|
||||
fn is_focus_event(event: &Self::Event) -> bool {
|
||||
matches!(event, Event::Focus)
|
||||
}
|
||||
}
|
||||
|
||||
impl ClipboardEntry {
|
||||
@ -1378,6 +1561,7 @@ mod tests {
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use std::{collections::HashSet, path::Path};
|
||||
use workspace::{pane, AppState};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_visible_list(cx: &mut gpui::TestAppContext) {
|
||||
@ -1853,6 +2037,95 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_remove_opened_file(cx: &mut gpui::TestAppContext) {
|
||||
init_test_with_editor(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.background());
|
||||
fs.insert_tree(
|
||||
"/src",
|
||||
json!({
|
||||
"test": {
|
||||
"first.rs": "// First Rust file",
|
||||
"second.rs": "// Second Rust file",
|
||||
"third.rs": "// Third Rust file",
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
|
||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
|
||||
|
||||
toggle_expand_dir(&panel, "src/test", cx);
|
||||
select_path(&panel, "src/test/first.rs", cx);
|
||||
panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
|
||||
cx.foreground().run_until_parked();
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..10, cx),
|
||||
&[
|
||||
"v src",
|
||||
" v test",
|
||||
" first.rs <== selected",
|
||||
" second.rs",
|
||||
" third.rs"
|
||||
]
|
||||
);
|
||||
ensure_single_file_is_opened(window_id, &workspace, "test/first.rs", cx);
|
||||
|
||||
submit_deletion(window_id, &panel, cx);
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..10, cx),
|
||||
&[
|
||||
"v src",
|
||||
" v test",
|
||||
" second.rs",
|
||||
" third.rs"
|
||||
],
|
||||
"Project panel should have no deleted file, no other file is selected in it"
|
||||
);
|
||||
ensure_no_open_items_and_panes(window_id, &workspace, cx);
|
||||
|
||||
select_path(&panel, "src/test/second.rs", cx);
|
||||
panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
|
||||
cx.foreground().run_until_parked();
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..10, cx),
|
||||
&[
|
||||
"v src",
|
||||
" v test",
|
||||
" second.rs <== selected",
|
||||
" third.rs"
|
||||
]
|
||||
);
|
||||
ensure_single_file_is_opened(window_id, &workspace, "test/second.rs", cx);
|
||||
|
||||
cx.update_window(window_id, |cx| {
|
||||
let active_items = workspace
|
||||
.read(cx)
|
||||
.panes()
|
||||
.iter()
|
||||
.filter_map(|pane| pane.read(cx).active_item())
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(active_items.len(), 1);
|
||||
let open_editor = active_items
|
||||
.into_iter()
|
||||
.next()
|
||||
.unwrap()
|
||||
.downcast::<Editor>()
|
||||
.expect("Open item should be an editor");
|
||||
open_editor.update(cx, |editor, cx| editor.set_text("Another text!", cx));
|
||||
});
|
||||
submit_deletion(window_id, &panel, cx);
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..10, cx),
|
||||
&["v src", " v test", " third.rs"],
|
||||
"Project panel should have no deleted file, with one last file remaining"
|
||||
);
|
||||
ensure_no_open_items_and_panes(window_id, &workspace, cx);
|
||||
}
|
||||
|
||||
fn toggle_expand_dir(
|
||||
panel: &ViewHandle<ProjectPanel>,
|
||||
path: impl AsRef<Path>,
|
||||
@ -1950,10 +2223,104 @@ mod tests {
|
||||
cx.foreground().forbid_parking();
|
||||
cx.update(|cx| {
|
||||
cx.set_global(SettingsStore::test(cx));
|
||||
init_settings(cx);
|
||||
theme::init((), cx);
|
||||
language::init(cx);
|
||||
editor::init_settings(cx);
|
||||
crate::init(cx);
|
||||
workspace::init_settings(cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn init_test_with_editor(cx: &mut TestAppContext) {
|
||||
cx.foreground().forbid_parking();
|
||||
cx.update(|cx| {
|
||||
let app_state = AppState::test(cx);
|
||||
theme::init((), cx);
|
||||
init_settings(cx);
|
||||
language::init(cx);
|
||||
editor::init(cx);
|
||||
pane::init(cx);
|
||||
crate::init(cx);
|
||||
workspace::init(app_state.clone(), cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn ensure_single_file_is_opened(
|
||||
window_id: usize,
|
||||
workspace: &ViewHandle<Workspace>,
|
||||
expected_path: &str,
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
cx.read_window(window_id, |cx| {
|
||||
let workspace = workspace.read(cx);
|
||||
let worktrees = workspace.worktrees(cx).collect::<Vec<_>>();
|
||||
assert_eq!(worktrees.len(), 1);
|
||||
let worktree_id = WorktreeId::from_usize(worktrees[0].id());
|
||||
|
||||
let open_project_paths = workspace
|
||||
.panes()
|
||||
.iter()
|
||||
.filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
open_project_paths,
|
||||
vec![ProjectPath {
|
||||
worktree_id,
|
||||
path: Arc::from(Path::new(expected_path))
|
||||
}],
|
||||
"Should have opened file, selected in project panel"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn submit_deletion(
|
||||
window_id: usize,
|
||||
panel: &ViewHandle<ProjectPanel>,
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
assert!(
|
||||
!cx.has_pending_prompt(window_id),
|
||||
"Should have no prompts before the deletion"
|
||||
);
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel
|
||||
.delete(&Delete, cx)
|
||||
.expect("Deletion start")
|
||||
.detach_and_log_err(cx);
|
||||
});
|
||||
assert!(
|
||||
cx.has_pending_prompt(window_id),
|
||||
"Should have a prompt after the deletion"
|
||||
);
|
||||
cx.simulate_prompt_answer(window_id, 0);
|
||||
assert!(
|
||||
!cx.has_pending_prompt(window_id),
|
||||
"Should have no prompts after prompt was replied to"
|
||||
);
|
||||
cx.foreground().run_until_parked();
|
||||
}
|
||||
|
||||
fn ensure_no_open_items_and_panes(
|
||||
window_id: usize,
|
||||
workspace: &ViewHandle<Workspace>,
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
assert!(
|
||||
!cx.has_pending_prompt(window_id),
|
||||
"Should have no prompts after deletion operation closes the file"
|
||||
);
|
||||
cx.read_window(window_id, |cx| {
|
||||
let open_project_paths = workspace
|
||||
.read(cx)
|
||||
.panes()
|
||||
.iter()
|
||||
.filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
|
||||
.collect::<Vec<_>>();
|
||||
assert!(
|
||||
open_project_paths.is_empty(),
|
||||
"Deleted file's buffer should be closed, but got open files: {open_project_paths:?}"
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
39
crates/project_panel/src/project_panel_settings.rs
Normal file
39
crates/project_panel/src/project_panel_settings.rs
Normal file
@ -0,0 +1,39 @@
|
||||
use anyhow;
|
||||
use schemars::JsonSchema;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use settings::Setting;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ProjectPanelDockPosition {
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct ProjectPanelSettings {
|
||||
pub git_status: bool,
|
||||
pub dock: ProjectPanelDockPosition,
|
||||
pub default_width: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
pub struct ProjectPanelSettingsContent {
|
||||
pub git_status: Option<bool>,
|
||||
pub dock: Option<ProjectPanelDockPosition>,
|
||||
pub default_width: Option<f32>,
|
||||
}
|
||||
|
||||
impl Setting for ProjectPanelSettings {
|
||||
const KEY: Option<&'static str> = Some("project_panel");
|
||||
|
||||
type FileContent = ProjectPanelSettingsContent;
|
||||
|
||||
fn load(
|
||||
default_value: &Self::FileContent,
|
||||
user_values: &[&Self::FileContent],
|
||||
_: &gpui::AppContext,
|
||||
) -> anyhow::Result<Self> {
|
||||
Self::load_via_json_merge(default_value, user_values)
|
||||
}
|
||||
}
|
@ -27,6 +27,7 @@ smol.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
futures.workspace = true
|
||||
editor = { path = "../editor", features = ["test-support"] }
|
||||
settings = { path = "../settings", features = ["test-support"] }
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
language = { path = "../language", features = ["test-support"] }
|
||||
|
@ -24,3 +24,6 @@ workspace = { path = "../workspace" }
|
||||
ordered-float.workspace = true
|
||||
postage.workspace = true
|
||||
smol.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { path = "../editor", features = ["test-support"] }
|
||||
|
@ -27,7 +27,7 @@ serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
smallvec.workspace = true
|
||||
smol.workspace = true
|
||||
glob.workspace = true
|
||||
globset.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
client = { path = "../client", features = ["test-support"] }
|
||||
|
@ -2,12 +2,14 @@ use crate::{
|
||||
SearchOption, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleRegex,
|
||||
ToggleWholeWord,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
use editor::{
|
||||
items::active_match_index, scroll::autoscroll::Autoscroll, Anchor, Editor, MultiBuffer,
|
||||
SelectAll, MAX_TAB_TITLE_LEN,
|
||||
};
|
||||
use futures::StreamExt;
|
||||
use globset::{Glob, GlobMatcher};
|
||||
use gpui::{
|
||||
actions,
|
||||
elements::*,
|
||||
@ -46,7 +48,7 @@ pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(ProjectSearchBar::search_in_new);
|
||||
cx.add_action(ProjectSearchBar::select_next_match);
|
||||
cx.add_action(ProjectSearchBar::select_prev_match);
|
||||
cx.add_action(ProjectSearchBar::toggle_focus);
|
||||
cx.add_action(ProjectSearchBar::move_focus_to_results);
|
||||
cx.capture_action(ProjectSearchBar::tab);
|
||||
cx.capture_action(ProjectSearchBar::tab_previous);
|
||||
add_toggle_option_action::<ToggleCaseSensitive>(SearchOption::CaseSensitive, cx);
|
||||
@ -571,16 +573,8 @@ impl ProjectSearchView {
|
||||
|
||||
fn build_search_query(&mut self, cx: &mut ViewContext<Self>) -> Option<SearchQuery> {
|
||||
let text = self.query_editor.read(cx).text(cx);
|
||||
let included_files = match self
|
||||
.included_files_editor
|
||||
.read(cx)
|
||||
.text(cx)
|
||||
.split(',')
|
||||
.map(str::trim)
|
||||
.filter(|glob_str| !glob_str.is_empty())
|
||||
.map(|glob_str| glob::Pattern::new(glob_str))
|
||||
.collect::<Result<_, _>>()
|
||||
{
|
||||
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
|
||||
@ -591,16 +585,8 @@ impl ProjectSearchView {
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let excluded_files = match self
|
||||
.excluded_files_editor
|
||||
.read(cx)
|
||||
.text(cx)
|
||||
.split(',')
|
||||
.map(str::trim)
|
||||
.filter(|glob_str| !glob_str.is_empty())
|
||||
.map(|glob_str| glob::Pattern::new(glob_str))
|
||||
.collect::<Result<_, _>>()
|
||||
{
|
||||
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
|
||||
@ -640,6 +626,14 @@ impl ProjectSearchView {
|
||||
}
|
||||
}
|
||||
|
||||
fn load_glob_set(text: &str) -> Result<Vec<GlobMatcher>> {
|
||||
text.split(',')
|
||||
.map(str::trim)
|
||||
.filter(|glob_str| !glob_str.is_empty())
|
||||
.map(|glob_str| anyhow::Ok(Glob::new(glob_str)?.compile_matcher()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn select_match(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
|
||||
if let Some(index) = self.active_match_index {
|
||||
let match_ranges = self.model.read(cx).match_ranges.clone();
|
||||
@ -800,19 +794,17 @@ impl ProjectSearchBar {
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_focus(pane: &mut Pane, _: &ToggleFocus, cx: &mut ViewContext<Pane>) {
|
||||
fn move_focus_to_results(pane: &mut Pane, _: &ToggleFocus, cx: &mut ViewContext<Pane>) {
|
||||
if let Some(search_view) = pane
|
||||
.active_item()
|
||||
.and_then(|item| item.downcast::<ProjectSearchView>())
|
||||
{
|
||||
search_view.update(cx, |search_view, cx| {
|
||||
if search_view.query_editor.is_focused(cx) {
|
||||
if !search_view.model.read(cx).match_ranges.is_empty() {
|
||||
if search_view.query_editor.is_focused(cx)
|
||||
&& !search_view.model.read(cx).match_ranges.is_empty()
|
||||
{
|
||||
search_view.focus_results_editor(cx);
|
||||
}
|
||||
} else {
|
||||
search_view.focus_query_editor(cx);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
cx.propagate_action();
|
||||
|
@ -22,7 +22,6 @@ util = { path = "../util" }
|
||||
|
||||
anyhow.workspace = true
|
||||
futures.workspace = true
|
||||
glob.workspace = true
|
||||
json_comments = "0.2"
|
||||
lazy_static.workspace = true
|
||||
postage.workspace = true
|
||||
|
@ -25,7 +25,7 @@ pub trait Setting: 'static {
|
||||
const KEY: Option<&'static str>;
|
||||
|
||||
/// The type that is stored in an individual JSON file.
|
||||
type FileContent: Clone + Serialize + DeserializeOwned + JsonSchema;
|
||||
type FileContent: Clone + Default + Serialize + DeserializeOwned + JsonSchema;
|
||||
|
||||
/// The logic for combining together values from one or more JSON files into the
|
||||
/// final value for this setting.
|
||||
@ -460,11 +460,12 @@ impl SettingsStore {
|
||||
|
||||
// If the global settings file changed, reload the global value for the field.
|
||||
if changed_local_path.is_none() {
|
||||
setting_value.set_global_value(setting_value.load_setting(
|
||||
&default_settings,
|
||||
&user_settings_stack,
|
||||
cx,
|
||||
)?);
|
||||
if let Some(value) = setting_value
|
||||
.load_setting(&default_settings, &user_settings_stack, cx)
|
||||
.log_err()
|
||||
{
|
||||
setting_value.set_global_value(value);
|
||||
}
|
||||
}
|
||||
|
||||
// Reload the local values for the setting.
|
||||
@ -495,14 +496,12 @@ impl SettingsStore {
|
||||
continue;
|
||||
}
|
||||
|
||||
setting_value.set_local_value(
|
||||
path.clone(),
|
||||
setting_value.load_setting(
|
||||
&default_settings,
|
||||
&user_settings_stack,
|
||||
cx,
|
||||
)?,
|
||||
);
|
||||
if let Some(value) = setting_value
|
||||
.load_setting(&default_settings, &user_settings_stack, cx)
|
||||
.log_err()
|
||||
{
|
||||
setting_value.set_local_value(path.clone(), value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -536,7 +535,12 @@ impl<T: Setting> AnySettingValue for SettingValue<T> {
|
||||
|
||||
fn deserialize_setting(&self, mut json: &serde_json::Value) -> Result<DeserializedSetting> {
|
||||
if let Some(key) = T::KEY {
|
||||
json = json.get(key).unwrap_or(&serde_json::Value::Null);
|
||||
if let Some(value) = json.get(key) {
|
||||
json = value;
|
||||
} else {
|
||||
let value = T::FileContent::default();
|
||||
return Ok(DeserializedSetting(Box::new(value)));
|
||||
}
|
||||
}
|
||||
let value = T::FileContent::deserialize(json)?;
|
||||
Ok(DeserializedSetting(Box::new(value)))
|
||||
@ -826,37 +830,6 @@ mod tests {
|
||||
store.register_setting::<UserSettings>(cx);
|
||||
store.register_setting::<TurboSetting>(cx);
|
||||
store.register_setting::<MultiKeySettings>(cx);
|
||||
|
||||
// error - missing required field in default settings
|
||||
store
|
||||
.set_default_settings(
|
||||
r#"{
|
||||
"user": {
|
||||
"name": "John Doe",
|
||||
"age": 30,
|
||||
"staff": false
|
||||
}
|
||||
}"#,
|
||||
cx,
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
// error - type error in default settings
|
||||
store
|
||||
.set_default_settings(
|
||||
r#"{
|
||||
"turbo": "the-wrong-type",
|
||||
"user": {
|
||||
"name": "John Doe",
|
||||
"age": 30,
|
||||
"staff": false
|
||||
}
|
||||
}"#,
|
||||
cx,
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
// valid default settings.
|
||||
store
|
||||
.set_default_settings(
|
||||
r#"{
|
||||
@ -1126,7 +1099,7 @@ mod tests {
|
||||
staff: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Default, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
struct UserSettingsJson {
|
||||
name: Option<String>,
|
||||
age: Option<u32>,
|
||||
@ -1170,7 +1143,7 @@ mod tests {
|
||||
key2: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
|
||||
struct MultiKeySettingsJson {
|
||||
key1: Option<String>,
|
||||
key2: Option<String>,
|
||||
@ -1203,7 +1176,7 @@ mod tests {
|
||||
Hour24,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
struct JournalSettingsJson {
|
||||
pub path: Option<String>,
|
||||
pub hour_format: Option<HourFormat>,
|
||||
@ -1223,7 +1196,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
struct LanguageSettings {
|
||||
#[serde(default)]
|
||||
languages: HashMap<String, LanguageSettingEntry>,
|
||||
|
@ -27,7 +27,7 @@ impl StaticColumnCount for bool {}
|
||||
impl Bind for bool {
|
||||
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
|
||||
statement
|
||||
.bind(self.then_some(1).unwrap_or(0), start_index)
|
||||
.bind(&self.then_some(1).unwrap_or(0), start_index)
|
||||
.with_context(|| format!("Failed to bind bool at index {start_index}"))
|
||||
}
|
||||
}
|
||||
|
@ -236,7 +236,7 @@ impl<'a> Statement<'a> {
|
||||
Ok(str::from_utf8(slice)?)
|
||||
}
|
||||
|
||||
pub fn bind<T: Bind>(&self, value: T, index: i32) -> Result<i32> {
|
||||
pub fn bind<T: Bind>(&self, value: &T, index: i32) -> Result<i32> {
|
||||
debug_assert!(index > 0);
|
||||
Ok(value.bind(self, index)?)
|
||||
}
|
||||
@ -258,7 +258,7 @@ impl<'a> Statement<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_bindings(&mut self, bindings: impl Bind) -> Result<&mut Self> {
|
||||
pub fn with_bindings(&mut self, bindings: &impl Bind) -> Result<&mut Self> {
|
||||
self.bind(bindings, 1)?;
|
||||
Ok(self)
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ impl Connection {
|
||||
query: &str,
|
||||
) -> Result<impl 'a + FnMut(B) -> Result<()>> {
|
||||
let mut statement = Statement::prepare(self, query)?;
|
||||
Ok(move |bindings| statement.with_bindings(bindings)?.exec())
|
||||
Ok(move |bindings| statement.with_bindings(&bindings)?.exec())
|
||||
}
|
||||
|
||||
/// Prepare a statement which has no bindings and returns a `Vec<C>`.
|
||||
@ -55,7 +55,7 @@ impl Connection {
|
||||
query: &str,
|
||||
) -> Result<impl 'a + FnMut(B) -> Result<Vec<C>>> {
|
||||
let mut statement = Statement::prepare(self, query)?;
|
||||
Ok(move |bindings| statement.with_bindings(bindings)?.rows::<C>())
|
||||
Ok(move |bindings| statement.with_bindings(&bindings)?.rows::<C>())
|
||||
}
|
||||
|
||||
/// Prepare a statement that selects a single row from the database.
|
||||
@ -87,7 +87,7 @@ impl Connection {
|
||||
let mut statement = Statement::prepare(self, query)?;
|
||||
Ok(move |bindings| {
|
||||
statement
|
||||
.with_bindings(bindings)
|
||||
.with_bindings(&bindings)
|
||||
.context("Bindings failed")?
|
||||
.maybe_row::<C>()
|
||||
.context("Maybe row failed")
|
||||
|
@ -119,6 +119,14 @@ pub fn init(cx: &mut AppContext) {
|
||||
settings::register::<TerminalSettings>(cx);
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum TerminalDockPosition {
|
||||
Left,
|
||||
Bottom,
|
||||
Right,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct TerminalSettings {
|
||||
pub shell: Shell,
|
||||
@ -132,6 +140,9 @@ pub struct TerminalSettings {
|
||||
pub alternate_scroll: AlternateScroll,
|
||||
pub option_as_meta: bool,
|
||||
pub copy_on_select: bool,
|
||||
pub dock: TerminalDockPosition,
|
||||
pub default_width: f32,
|
||||
pub default_height: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
@ -147,6 +158,9 @@ pub struct TerminalSettingsContent {
|
||||
pub alternate_scroll: Option<AlternateScroll>,
|
||||
pub option_as_meta: Option<bool>,
|
||||
pub copy_on_select: Option<bool>,
|
||||
pub dock: Option<TerminalDockPosition>,
|
||||
pub default_width: Option<f32>,
|
||||
pub default_height: Option<f32>,
|
||||
}
|
||||
|
||||
impl TerminalSettings {
|
||||
|
@ -39,6 +39,7 @@ serde_derive.workspace = true
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { path = "../editor", features = ["test-support"] }
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
client = { path = "../client", features = ["test-support"]}
|
||||
project = { path = "../project", features = ["test-support"]}
|
||||
|
@ -1,173 +0,0 @@
|
||||
use crate::TerminalView;
|
||||
use context_menu::{ContextMenu, ContextMenuItem};
|
||||
use gpui::{
|
||||
elements::*,
|
||||
platform::{CursorStyle, MouseButton},
|
||||
AnyElement, Element, Entity, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use std::any::TypeId;
|
||||
use workspace::{
|
||||
dock::{Dock, FocusDock},
|
||||
item::ItemHandle,
|
||||
NewTerminal, StatusItemView, Workspace,
|
||||
};
|
||||
|
||||
pub struct TerminalButton {
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
popup_menu: ViewHandle<ContextMenu>,
|
||||
}
|
||||
|
||||
impl Entity for TerminalButton {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for TerminalButton {
|
||||
fn ui_name() -> &'static str {
|
||||
"TerminalButton"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
let workspace = self.workspace.upgrade(cx);
|
||||
let project = match workspace {
|
||||
Some(workspace) => workspace.read(cx).project().read(cx),
|
||||
None => return Empty::new().into_any(),
|
||||
};
|
||||
|
||||
let focused_view = cx.focused_view_id();
|
||||
let active = focused_view
|
||||
.map(|view_id| {
|
||||
cx.view_type_id(cx.window_id(), view_id) == Some(TypeId::of::<TerminalView>())
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
let has_terminals = !project.local_terminal_handles().is_empty();
|
||||
let terminal_count = project.local_terminal_handles().len() as i32;
|
||||
let theme = theme::current(cx).clone();
|
||||
|
||||
Stack::new()
|
||||
.with_child(
|
||||
MouseEventHandler::<Self, _>::new(0, cx, {
|
||||
let theme = theme.clone();
|
||||
move |state, _cx| {
|
||||
let style = theme
|
||||
.workspace
|
||||
.status_bar
|
||||
.sidebar_buttons
|
||||
.item
|
||||
.style_for(state, active);
|
||||
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Svg::new("icons/terminal_12.svg")
|
||||
.with_color(style.icon_color)
|
||||
.constrained()
|
||||
.with_width(style.icon_size)
|
||||
.aligned()
|
||||
.into_any_named("terminals-icon"),
|
||||
)
|
||||
.with_children(has_terminals.then(|| {
|
||||
Label::new(terminal_count.to_string(), style.label.text.clone())
|
||||
.contained()
|
||||
.with_style(style.label.container)
|
||||
.aligned()
|
||||
}))
|
||||
.constrained()
|
||||
.with_height(style.icon_size)
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
}
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
if has_terminals {
|
||||
this.deploy_terminal_menu(cx);
|
||||
} else {
|
||||
if !active {
|
||||
if let Some(workspace) = this.workspace.upgrade(cx) {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
Dock::focus_dock(workspace, &Default::default(), cx)
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
})
|
||||
.with_tooltip::<Self>(
|
||||
0,
|
||||
"Show Terminal".into(),
|
||||
Some(Box::new(FocusDock)),
|
||||
theme.tooltip.clone(),
|
||||
cx,
|
||||
),
|
||||
)
|
||||
.with_child(ChildView::new(&self.popup_menu, cx).aligned().top().right())
|
||||
.into_any_named("terminal button")
|
||||
}
|
||||
}
|
||||
|
||||
impl TerminalButton {
|
||||
pub fn new(workspace: ViewHandle<Workspace>, cx: &mut ViewContext<Self>) -> Self {
|
||||
let button_view_id = cx.view_id();
|
||||
cx.observe(&workspace, |_, _, cx| cx.notify()).detach();
|
||||
Self {
|
||||
workspace: workspace.downgrade(),
|
||||
popup_menu: cx.add_view(|cx| {
|
||||
let mut menu = ContextMenu::new(button_view_id, cx);
|
||||
menu.set_position_mode(OverlayPositionMode::Local);
|
||||
menu
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deploy_terminal_menu(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let mut menu_options = vec![ContextMenuItem::action("New Terminal", NewTerminal)];
|
||||
|
||||
if let Some(workspace) = self.workspace.upgrade(cx) {
|
||||
let project = workspace.read(cx).project().read(cx);
|
||||
let local_terminal_handles = project.local_terminal_handles();
|
||||
|
||||
if !local_terminal_handles.is_empty() {
|
||||
menu_options.push(ContextMenuItem::Separator)
|
||||
}
|
||||
|
||||
for local_terminal_handle in local_terminal_handles {
|
||||
if let Some(terminal) = local_terminal_handle.upgrade(cx) {
|
||||
let workspace = self.workspace.clone();
|
||||
let local_terminal_handle = local_terminal_handle.clone();
|
||||
menu_options.push(ContextMenuItem::handler(
|
||||
terminal.read(cx).title(),
|
||||
move |cx| {
|
||||
if let Some(workspace) = workspace.upgrade(cx) {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
let terminal = workspace
|
||||
.items_of_type::<TerminalView>(cx)
|
||||
.find(|terminal| {
|
||||
terminal.read(cx).model().downgrade()
|
||||
== local_terminal_handle
|
||||
});
|
||||
if let Some(terminal) = terminal {
|
||||
workspace.activate_item(&terminal, cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.popup_menu.update(cx, |menu, cx| {
|
||||
menu.show(
|
||||
Default::default(),
|
||||
AnchorCorner::BottomRight,
|
||||
menu_options,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl StatusItemView for TerminalButton {
|
||||
fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, cx: &mut ViewContext<Self>) {
|
||||
cx.notify();
|
||||
}
|
||||
}
|
408
crates/terminal_view/src/terminal_panel.rs
Normal file
408
crates/terminal_view/src/terminal_panel.rs
Normal file
@ -0,0 +1,408 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::TerminalView;
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use gpui::{
|
||||
actions, anyhow::Result, elements::*, serde_json, Action, AppContext, AsyncAppContext, Entity,
|
||||
Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
|
||||
};
|
||||
use project::Fs;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::SettingsStore;
|
||||
use terminal::{TerminalDockPosition, TerminalSettings};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
use workspace::{
|
||||
dock::{DockPosition, Panel},
|
||||
item::Item,
|
||||
pane, DraggedItem, Pane, Workspace,
|
||||
};
|
||||
|
||||
const TERMINAL_PANEL_KEY: &'static str = "TerminalPanel";
|
||||
|
||||
actions!(terminal_panel, [ToggleFocus]);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(TerminalPanel::add_terminal);
|
||||
}
|
||||
|
||||
pub enum Event {
|
||||
Close,
|
||||
DockPositionChanged,
|
||||
ZoomIn,
|
||||
ZoomOut,
|
||||
Focus,
|
||||
}
|
||||
|
||||
pub struct TerminalPanel {
|
||||
pane: ViewHandle<Pane>,
|
||||
fs: Arc<dyn Fs>,
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
width: Option<f32>,
|
||||
height: Option<f32>,
|
||||
pending_serialization: Task<Option<()>>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl TerminalPanel {
|
||||
fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
|
||||
let weak_self = cx.weak_handle();
|
||||
let pane = cx.add_view(|cx| {
|
||||
let window_id = cx.window_id();
|
||||
let mut pane = Pane::new(
|
||||
workspace.weak_handle(),
|
||||
workspace.app_state().background_actions,
|
||||
Default::default(),
|
||||
cx,
|
||||
);
|
||||
pane.set_can_split(false, cx);
|
||||
pane.on_can_drop(move |drag_and_drop, cx| {
|
||||
drag_and_drop
|
||||
.currently_dragged::<DraggedItem>(window_id)
|
||||
.map_or(false, |(_, item)| {
|
||||
item.handle.act_as::<TerminalView>(cx).is_some()
|
||||
})
|
||||
});
|
||||
pane.set_render_tab_bar_buttons(cx, move |pane, cx| {
|
||||
let this = weak_self.clone();
|
||||
Flex::row()
|
||||
.with_child(Pane::render_tab_bar_button(
|
||||
0,
|
||||
"icons/plus_12.svg",
|
||||
Some((
|
||||
"New Terminal".into(),
|
||||
Some(Box::new(workspace::NewTerminal)),
|
||||
)),
|
||||
cx,
|
||||
move |_, cx| {
|
||||
let this = this.clone();
|
||||
cx.window_context().defer(move |cx| {
|
||||
if let Some(this) = this.upgrade(cx) {
|
||||
this.update(cx, |this, cx| {
|
||||
this.add_terminal(&Default::default(), cx);
|
||||
});
|
||||
}
|
||||
})
|
||||
},
|
||||
None,
|
||||
))
|
||||
.with_child(Pane::render_tab_bar_button(
|
||||
1,
|
||||
if pane.is_zoomed() {
|
||||
"icons/minimize_8.svg"
|
||||
} else {
|
||||
"icons/maximize_8.svg"
|
||||
},
|
||||
Some(("Toggle Zoom".into(), Some(Box::new(workspace::ToggleZoom)))),
|
||||
cx,
|
||||
move |pane, cx| pane.toggle_zoom(&Default::default(), cx),
|
||||
None,
|
||||
))
|
||||
.into_any()
|
||||
});
|
||||
pane
|
||||
});
|
||||
let subscriptions = vec![
|
||||
cx.observe(&pane, |_, _, cx| cx.notify()),
|
||||
cx.subscribe(&pane, Self::handle_pane_event),
|
||||
];
|
||||
let this = Self {
|
||||
pane,
|
||||
fs: workspace.app_state().fs.clone(),
|
||||
workspace: workspace.weak_handle(),
|
||||
pending_serialization: Task::ready(None),
|
||||
width: None,
|
||||
height: None,
|
||||
_subscriptions: subscriptions,
|
||||
};
|
||||
let mut old_dock_position = this.position(cx);
|
||||
cx.observe_global::<SettingsStore, _>(move |this, cx| {
|
||||
let new_dock_position = this.position(cx);
|
||||
if new_dock_position != old_dock_position {
|
||||
old_dock_position = new_dock_position;
|
||||
cx.emit(Event::DockPositionChanged);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
this
|
||||
}
|
||||
|
||||
pub fn load(
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
cx: AsyncAppContext,
|
||||
) -> Task<Result<ViewHandle<Self>>> {
|
||||
cx.spawn(|mut cx| async move {
|
||||
let serialized_panel = if let Some(panel) = cx
|
||||
.background()
|
||||
.spawn(async move { KEY_VALUE_STORE.read_kvp(TERMINAL_PANEL_KEY) })
|
||||
.await
|
||||
.log_err()
|
||||
.flatten()
|
||||
{
|
||||
Some(serde_json::from_str::<SerializedTerminalPanel>(&panel)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let (panel, pane, items) = workspace.update(&mut cx, |workspace, cx| {
|
||||
let panel = cx.add_view(|cx| TerminalPanel::new(workspace, cx));
|
||||
let items = if let Some(serialized_panel) = serialized_panel.as_ref() {
|
||||
panel.update(cx, |panel, cx| {
|
||||
cx.notify();
|
||||
panel.height = serialized_panel.height;
|
||||
panel.width = serialized_panel.width;
|
||||
panel.pane.update(cx, |_, cx| {
|
||||
serialized_panel
|
||||
.items
|
||||
.iter()
|
||||
.map(|item_id| {
|
||||
TerminalView::deserialize(
|
||||
workspace.project().clone(),
|
||||
workspace.weak_handle(),
|
||||
workspace.database_id(),
|
||||
*item_id,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
})
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
let pane = panel.read(cx).pane.clone();
|
||||
(panel, pane, items)
|
||||
})?;
|
||||
|
||||
let items = futures::future::join_all(items).await;
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
let active_item_id = serialized_panel
|
||||
.as_ref()
|
||||
.and_then(|panel| panel.active_item_id);
|
||||
let mut active_ix = None;
|
||||
for item in items {
|
||||
if let Some(item) = item.log_err() {
|
||||
let item_id = item.id();
|
||||
Pane::add_item(workspace, &pane, Box::new(item), false, false, None, cx);
|
||||
if Some(item_id) == active_item_id {
|
||||
active_ix = Some(pane.read(cx).items_len() - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(active_ix) = active_ix {
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.activate_item(active_ix, false, false, cx)
|
||||
});
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(panel)
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_pane_event(
|
||||
&mut self,
|
||||
_pane: ViewHandle<Pane>,
|
||||
event: &pane::Event,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
pane::Event::ActivateItem { .. } => self.serialize(cx),
|
||||
pane::Event::RemoveItem { .. } => self.serialize(cx),
|
||||
pane::Event::Remove => cx.emit(Event::Close),
|
||||
pane::Event::ZoomIn => cx.emit(Event::ZoomIn),
|
||||
pane::Event::ZoomOut => cx.emit(Event::ZoomOut),
|
||||
pane::Event::Focus => cx.emit(Event::Focus),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn add_terminal(&mut self, _: &workspace::NewTerminal, cx: &mut ViewContext<Self>) {
|
||||
let workspace = self.workspace.clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let pane = this.read_with(&cx, |this, _| this.pane.clone())?;
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
let working_directory_strategy = settings::get::<TerminalSettings>(cx)
|
||||
.working_directory
|
||||
.clone();
|
||||
let working_directory =
|
||||
crate::get_working_directory(workspace, cx, working_directory_strategy);
|
||||
let window_id = cx.window_id();
|
||||
if let Some(terminal) = workspace.project().update(cx, |project, cx| {
|
||||
project
|
||||
.create_terminal(working_directory, window_id, cx)
|
||||
.log_err()
|
||||
}) {
|
||||
let terminal =
|
||||
Box::new(cx.add_view(|cx| {
|
||||
TerminalView::new(terminal, workspace.database_id(), cx)
|
||||
}));
|
||||
let focus = pane.read(cx).has_focus();
|
||||
Pane::add_item(workspace, &pane, terminal, true, focus, None, cx);
|
||||
}
|
||||
})?;
|
||||
this.update(&mut cx, |this, cx| this.serialize(cx))?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn serialize(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let items = self
|
||||
.pane
|
||||
.read(cx)
|
||||
.items()
|
||||
.map(|item| item.id())
|
||||
.collect::<Vec<_>>();
|
||||
let active_item_id = self.pane.read(cx).active_item().map(|item| item.id());
|
||||
let height = self.height;
|
||||
let width = self.width;
|
||||
self.pending_serialization = cx.background().spawn(
|
||||
async move {
|
||||
KEY_VALUE_STORE
|
||||
.write_kvp(
|
||||
TERMINAL_PANEL_KEY.into(),
|
||||
serde_json::to_string(&SerializedTerminalPanel {
|
||||
items,
|
||||
active_item_id,
|
||||
height,
|
||||
width,
|
||||
})?,
|
||||
)
|
||||
.await?;
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for TerminalPanel {
|
||||
type Event = Event;
|
||||
}
|
||||
|
||||
impl View for TerminalPanel {
|
||||
fn ui_name() -> &'static str {
|
||||
"TerminalPanel"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> gpui::AnyElement<Self> {
|
||||
ChildView::new(&self.pane, cx).into_any()
|
||||
}
|
||||
|
||||
fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
if cx.is_self_focused() {
|
||||
cx.focus(&self.pane);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Panel for TerminalPanel {
|
||||
fn position(&self, cx: &WindowContext) -> DockPosition {
|
||||
match settings::get::<TerminalSettings>(cx).dock {
|
||||
TerminalDockPosition::Left => DockPosition::Left,
|
||||
TerminalDockPosition::Bottom => DockPosition::Bottom,
|
||||
TerminalDockPosition::Right => DockPosition::Right,
|
||||
}
|
||||
}
|
||||
|
||||
fn position_is_valid(&self, _: DockPosition) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
|
||||
settings::update_settings_file::<TerminalSettings>(self.fs.clone(), cx, move |settings| {
|
||||
let dock = match position {
|
||||
DockPosition::Left => TerminalDockPosition::Left,
|
||||
DockPosition::Bottom => TerminalDockPosition::Bottom,
|
||||
DockPosition::Right => TerminalDockPosition::Right,
|
||||
};
|
||||
settings.dock = Some(dock);
|
||||
});
|
||||
}
|
||||
|
||||
fn size(&self, cx: &WindowContext) -> f32 {
|
||||
let settings = settings::get::<TerminalSettings>(cx);
|
||||
match self.position(cx) {
|
||||
DockPosition::Left | DockPosition::Right => {
|
||||
self.width.unwrap_or_else(|| settings.default_width)
|
||||
}
|
||||
DockPosition::Bottom => self.height.unwrap_or_else(|| settings.default_height),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_size(&mut self, size: f32, cx: &mut ViewContext<Self>) {
|
||||
match self.position(cx) {
|
||||
DockPosition::Left | DockPosition::Right => self.width = Some(size),
|
||||
DockPosition::Bottom => self.height = Some(size),
|
||||
}
|
||||
self.serialize(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn should_zoom_in_on_event(event: &Event) -> bool {
|
||||
matches!(event, Event::ZoomIn)
|
||||
}
|
||||
|
||||
fn should_zoom_out_on_event(event: &Event) -> bool {
|
||||
matches!(event, Event::ZoomOut)
|
||||
}
|
||||
|
||||
fn is_zoomed(&self, cx: &WindowContext) -> bool {
|
||||
self.pane.read(cx).is_zoomed()
|
||||
}
|
||||
|
||||
fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
|
||||
self.pane.update(cx, |pane, cx| pane.set_zoomed(zoomed, cx));
|
||||
}
|
||||
|
||||
fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
|
||||
if active && self.pane.read(cx).items_len() == 0 {
|
||||
self.add_terminal(&Default::default(), cx)
|
||||
}
|
||||
}
|
||||
|
||||
fn icon_path(&self) -> &'static str {
|
||||
"icons/terminal_12.svg"
|
||||
}
|
||||
|
||||
fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>) {
|
||||
("Terminal Panel".into(), Some(Box::new(ToggleFocus)))
|
||||
}
|
||||
|
||||
fn icon_label(&self, cx: &WindowContext) -> Option<String> {
|
||||
let count = self.pane.read(cx).items_len();
|
||||
if count == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(count.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn should_change_position_on_event(event: &Self::Event) -> bool {
|
||||
matches!(event, Event::DockPositionChanged)
|
||||
}
|
||||
|
||||
fn should_activate_on_event(_: &Self::Event) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn should_close_on_event(event: &Event) -> bool {
|
||||
matches!(event, Event::Close)
|
||||
}
|
||||
|
||||
fn has_focus(&self, cx: &WindowContext) -> bool {
|
||||
self.pane.read(cx).has_focus()
|
||||
}
|
||||
|
||||
fn is_focus_event(event: &Self::Event) -> bool {
|
||||
matches!(event, Event::Focus)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SerializedTerminalPanel {
|
||||
items: Vec<usize>,
|
||||
active_item_id: Option<usize>,
|
||||
width: Option<f32>,
|
||||
height: Option<f32>,
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
mod persistence;
|
||||
pub mod terminal_button;
|
||||
pub mod terminal_element;
|
||||
pub mod terminal_panel;
|
||||
|
||||
use crate::{persistence::TERMINAL_DB, terminal_element::TerminalElement};
|
||||
use context_menu::{ContextMenu, ContextMenuItem};
|
||||
@ -63,6 +63,7 @@ actions!(
|
||||
impl_actions!(terminal, [SendText, SendKeystroke]);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
terminal_panel::init(cx);
|
||||
terminal::init(cx);
|
||||
|
||||
cx.add_action(TerminalView::deploy);
|
||||
|
@ -1783,6 +1783,19 @@ impl BufferSnapshot {
|
||||
where
|
||||
D: 'a + TextDimension,
|
||||
A: 'a + IntoIterator<Item = &'a Anchor>,
|
||||
{
|
||||
let anchors = anchors.into_iter();
|
||||
self.summaries_for_anchors_with_payload::<D, _, ()>(anchors.map(|a| (a, ())))
|
||||
.map(|d| d.0)
|
||||
}
|
||||
|
||||
pub fn summaries_for_anchors_with_payload<'a, D, A, T>(
|
||||
&'a self,
|
||||
anchors: A,
|
||||
) -> impl 'a + Iterator<Item = (D, T)>
|
||||
where
|
||||
D: 'a + TextDimension,
|
||||
A: 'a + IntoIterator<Item = (&'a Anchor, T)>,
|
||||
{
|
||||
let anchors = anchors.into_iter();
|
||||
let mut insertion_cursor = self.insertions.cursor::<InsertionFragmentKey>();
|
||||
@ -1790,11 +1803,11 @@ impl BufferSnapshot {
|
||||
let mut text_cursor = self.visible_text.cursor(0);
|
||||
let mut position = D::default();
|
||||
|
||||
anchors.map(move |anchor| {
|
||||
anchors.map(move |(anchor, payload)| {
|
||||
if *anchor == Anchor::MIN {
|
||||
return D::default();
|
||||
return (D::default(), payload);
|
||||
} else if *anchor == Anchor::MAX {
|
||||
return D::from_text_summary(&self.visible_text.summary());
|
||||
return (D::from_text_summary(&self.visible_text.summary()), payload);
|
||||
}
|
||||
|
||||
let anchor_key = InsertionFragmentKey {
|
||||
@ -1825,7 +1838,7 @@ impl BufferSnapshot {
|
||||
}
|
||||
|
||||
position.add_assign(&text_cursor.summary(fragment_offset));
|
||||
position.clone()
|
||||
(position.clone(), payload)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -82,19 +82,20 @@ pub struct Workspace {
|
||||
pub pane_divider: Border,
|
||||
pub leader_border_opacity: f32,
|
||||
pub leader_border_width: f32,
|
||||
pub sidebar: Sidebar,
|
||||
pub dock: Dock,
|
||||
pub status_bar: StatusBar,
|
||||
pub toolbar: Toolbar,
|
||||
pub breadcrumb_height: f32,
|
||||
pub breadcrumbs: Interactive<ContainedText>,
|
||||
pub disconnected_overlay: ContainedText,
|
||||
pub modal: ContainerStyle,
|
||||
pub zoomed_foreground: ContainerStyle,
|
||||
pub zoomed_background: ContainerStyle,
|
||||
pub notification: ContainerStyle,
|
||||
pub notifications: Notifications,
|
||||
pub joining_project_avatar: ImageStyle,
|
||||
pub joining_project_message: ContainedText,
|
||||
pub external_location_message: ContainedText,
|
||||
pub dock: Dock,
|
||||
pub drop_target_overlay_color: Color,
|
||||
}
|
||||
|
||||
@ -317,15 +318,6 @@ pub struct Toolbar {
|
||||
pub nav_button: Interactive<IconButton>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
pub struct Dock {
|
||||
pub initial_size_right: f32,
|
||||
pub initial_size_bottom: f32,
|
||||
pub wash_color: Color,
|
||||
pub panel: ContainerStyle,
|
||||
pub maximized: ContainerStyle,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
pub struct Notifications {
|
||||
#[serde(flatten)]
|
||||
@ -369,17 +361,17 @@ pub struct StatusBar {
|
||||
pub auto_update_progress_message: TextStyle,
|
||||
pub auto_update_done_message: TextStyle,
|
||||
pub lsp_status: Interactive<StatusBarLspStatus>,
|
||||
pub sidebar_buttons: StatusBarSidebarButtons,
|
||||
pub panel_buttons: StatusBarPanelButtons,
|
||||
pub diagnostic_summary: Interactive<StatusBarDiagnosticSummary>,
|
||||
pub diagnostic_message: Interactive<ContainedText>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
pub struct StatusBarSidebarButtons {
|
||||
pub struct StatusBarPanelButtons {
|
||||
pub group_left: ContainerStyle,
|
||||
pub group_bottom: ContainerStyle,
|
||||
pub group_right: ContainerStyle,
|
||||
pub item: Interactive<SidebarItem>,
|
||||
pub badge: ContainerStyle,
|
||||
pub button: Interactive<PanelButton>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
@ -409,14 +401,14 @@ pub struct StatusBarLspStatus {
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
pub struct Sidebar {
|
||||
pub initial_size: f32,
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
pub struct Dock {
|
||||
pub left: ContainerStyle,
|
||||
pub bottom: ContainerStyle,
|
||||
pub right: ContainerStyle,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
pub struct SidebarItem {
|
||||
pub struct PanelButton {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
pub icon_color: Color,
|
||||
@ -446,6 +438,19 @@ pub struct ProjectPanelEntry {
|
||||
pub icon_color: Color,
|
||||
pub icon_size: f32,
|
||||
pub icon_spacing: f32,
|
||||
pub status: EntryStatus,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Default)]
|
||||
pub struct EntryStatus {
|
||||
pub git: GitProjectStatus,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Default)]
|
||||
pub struct GitProjectStatus {
|
||||
pub modified: Color,
|
||||
pub inserted: Color,
|
||||
pub conflict: Color,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Default)]
|
||||
@ -670,6 +675,14 @@ pub struct Scrollbar {
|
||||
pub thumb: ContainerStyle,
|
||||
pub width: f32,
|
||||
pub min_height_factor: f32,
|
||||
pub git: GitDiffColors,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
pub struct GitDiffColors {
|
||||
pub inserted: Color,
|
||||
pub modified: Color,
|
||||
pub deleted: Color,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
|
@ -1,10 +1,9 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use fs::repository::GitFileStatus;
|
||||
use gpui::{
|
||||
color::Color,
|
||||
elements::{
|
||||
ConstrainedBox, Container, ContainerStyle, Empty, Flex, KeystrokeLabel, Label, LabelStyle,
|
||||
ConstrainedBox, Container, ContainerStyle, Empty, Flex, KeystrokeLabel, Label,
|
||||
MouseEventHandler, ParentElement, Stack, Svg,
|
||||
},
|
||||
fonts::TextStyle,
|
||||
@ -12,11 +11,11 @@ use gpui::{
|
||||
platform,
|
||||
platform::MouseButton,
|
||||
scene::MouseClick,
|
||||
Action, AnyElement, Element, EventContext, MouseState, View, ViewContext,
|
||||
Action, Element, EventContext, MouseState, View, ViewContext,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{ContainedText, Interactive, Theme};
|
||||
use crate::{ContainedText, Interactive};
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
pub struct CheckboxStyle {
|
||||
@ -253,53 +252,3 @@ where
|
||||
.constrained()
|
||||
.with_height(style.dimensions().y())
|
||||
}
|
||||
|
||||
pub struct FileName {
|
||||
filename: String,
|
||||
git_status: Option<GitFileStatus>,
|
||||
style: FileNameStyle,
|
||||
}
|
||||
|
||||
pub struct FileNameStyle {
|
||||
template_style: LabelStyle,
|
||||
git_inserted: Color,
|
||||
git_modified: Color,
|
||||
git_deleted: Color,
|
||||
}
|
||||
|
||||
impl FileName {
|
||||
pub fn new(filename: String, git_status: Option<GitFileStatus>, style: FileNameStyle) -> Self {
|
||||
FileName {
|
||||
filename,
|
||||
git_status,
|
||||
style,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn style<I: Into<LabelStyle>>(style: I, theme: &Theme) -> FileNameStyle {
|
||||
FileNameStyle {
|
||||
template_style: style.into(),
|
||||
git_inserted: theme.editor.diff.inserted,
|
||||
git_modified: theme.editor.diff.modified,
|
||||
git_deleted: theme.editor.diff.deleted,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: View> gpui::elements::Component<V> for FileName {
|
||||
fn render(&self, _: &mut V, _: &mut ViewContext<V>) -> AnyElement<V> {
|
||||
// Prepare colors for git statuses
|
||||
let mut filename_text_style = self.style.template_style.text.clone();
|
||||
filename_text_style.color = self
|
||||
.git_status
|
||||
.as_ref()
|
||||
.map(|status| match status {
|
||||
GitFileStatus::Added => self.style.git_inserted,
|
||||
GitFileStatus::Modified => self.style.git_modified,
|
||||
GitFileStatus::Conflict => self.style.git_deleted,
|
||||
})
|
||||
.unwrap_or(self.style.template_style.text.color);
|
||||
|
||||
Label::new(self.filename.clone(), filename_text_style).into_any()
|
||||
}
|
||||
}
|
||||
|
@ -23,3 +23,6 @@ log.workspace = true
|
||||
parking_lot.workspace = true
|
||||
postage.workspace = true
|
||||
smol.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { path = "../editor", features = ["test-support"] }
|
||||
|
@ -3,15 +3,12 @@ use std::env;
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
lazy_static! {
|
||||
// TODO: Put this back!
|
||||
pub static ref RELEASE_CHANNEL_NAME: String = env::var("ZED_RELEASE_CHANNEL")
|
||||
.unwrap_or_else(|_| include_str!("../../zed/RELEASE_CHANNEL").to_string());
|
||||
// pub static ref RELEASE_CHANNEL_NAME: String = if cfg!(debug_assertions) {
|
||||
// env::var("ZED_RELEASE_CHANNEL")
|
||||
// .unwrap_or_else(|_| include_str!("../../zed/RELEASE_CHANNEL").to_string())
|
||||
// } else {
|
||||
// include_str!("../../zed/RELEASE_CHANNEL").to_string()
|
||||
// };
|
||||
pub static ref RELEASE_CHANNEL_NAME: String = if cfg!(debug_assertions) {
|
||||
env::var("ZED_RELEASE_CHANNEL")
|
||||
.unwrap_or_else(|_| include_str!("../../zed/RELEASE_CHANNEL").to_string())
|
||||
} else {
|
||||
include_str!("../../zed/RELEASE_CHANNEL").to_string()
|
||||
};
|
||||
pub static ref RELEASE_CHANNEL: ReleaseChannel = match RELEASE_CHANNEL_NAME.as_str() {
|
||||
"dev" => ReleaseChannel::Dev,
|
||||
"preview" => ReleaseChannel::Preview,
|
||||
|
@ -30,3 +30,6 @@ anyhow.workspace = true
|
||||
log.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { path = "../editor", features = ["test-support"] }
|
||||
|
@ -11,7 +11,7 @@ use gpui::{
|
||||
use settings::{update_settings_file, SettingsStore};
|
||||
use std::{borrow::Cow, sync::Arc};
|
||||
use workspace::{
|
||||
item::Item, open_new, sidebar::SidebarSide, AppState, PaneBackdrop, Welcome, Workspace,
|
||||
dock::DockPosition, item::Item, open_new, AppState, PaneBackdrop, Welcome, Workspace,
|
||||
WorkspaceId,
|
||||
};
|
||||
|
||||
@ -32,7 +32,7 @@ pub fn init(cx: &mut AppContext) {
|
||||
|
||||
pub fn show_welcome_experience(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
open_new(&app_state, cx, |workspace, cx| {
|
||||
workspace.toggle_sidebar(SidebarSide::Left, cx);
|
||||
workspace.toggle_dock(DockPosition::Left, false, cx);
|
||||
let welcome_page = cx.add_view(|cx| WelcomePage::new(workspace, cx));
|
||||
workspace.add_item_to_center(Box::new(welcome_page.clone()), cx);
|
||||
cx.focus(&welcome_page);
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,125 +0,0 @@
|
||||
use super::{icon_for_dock_anchor, Dock, FocusDock, HideDock};
|
||||
use crate::{handle_dropped_item, StatusItemView, Workspace};
|
||||
use gpui::{
|
||||
elements::{Empty, MouseEventHandler, Svg},
|
||||
platform::CursorStyle,
|
||||
platform::MouseButton,
|
||||
AnyElement, Element, Entity, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
|
||||
pub struct ToggleDockButton {
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
}
|
||||
|
||||
impl ToggleDockButton {
|
||||
pub fn new(workspace: ViewHandle<Workspace>, cx: &mut ViewContext<Self>) -> Self {
|
||||
// When dock moves, redraw so that the icon and toggle status matches.
|
||||
cx.subscribe(&workspace, |_, _, _, cx| cx.notify()).detach();
|
||||
|
||||
Self {
|
||||
workspace: workspace.downgrade(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for ToggleDockButton {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for ToggleDockButton {
|
||||
fn ui_name() -> &'static str {
|
||||
"Dock Toggle"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> AnyElement<Self> {
|
||||
let workspace = self.workspace.upgrade(cx);
|
||||
|
||||
if workspace.is_none() {
|
||||
return Empty::new().into_any();
|
||||
}
|
||||
|
||||
let workspace = workspace.unwrap();
|
||||
let dock_position = workspace.read(cx).dock.position;
|
||||
let dock_pane = workspace.read(cx).dock_pane().clone();
|
||||
|
||||
let theme = theme::current(cx).clone();
|
||||
|
||||
let button = MouseEventHandler::<Self, _>::new(0, cx, {
|
||||
let theme = theme.clone();
|
||||
move |state, _| {
|
||||
let style = theme
|
||||
.workspace
|
||||
.status_bar
|
||||
.sidebar_buttons
|
||||
.item
|
||||
.style_for(state, dock_position.is_visible());
|
||||
|
||||
Svg::new(icon_for_dock_anchor(dock_position.anchor()))
|
||||
.with_color(style.icon_color)
|
||||
.constrained()
|
||||
.with_width(style.icon_size)
|
||||
.with_height(style.icon_size)
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
}
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_up(MouseButton::Left, move |event, this, cx| {
|
||||
let drop_index = dock_pane.read(cx).items_len() + 1;
|
||||
handle_dropped_item(
|
||||
event,
|
||||
this.workspace.clone(),
|
||||
&dock_pane.downgrade(),
|
||||
drop_index,
|
||||
false,
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
if dock_position.is_visible() {
|
||||
button
|
||||
.on_click(MouseButton::Left, |_, this, cx| {
|
||||
if let Some(workspace) = this.workspace.upgrade(cx) {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
Dock::hide_dock(workspace, &Default::default(), cx)
|
||||
})
|
||||
}
|
||||
})
|
||||
.with_tooltip::<Self>(
|
||||
0,
|
||||
"Hide Dock".into(),
|
||||
Some(Box::new(HideDock)),
|
||||
theme.tooltip.clone(),
|
||||
cx,
|
||||
)
|
||||
} else {
|
||||
button
|
||||
.on_click(MouseButton::Left, |_, this, cx| {
|
||||
if let Some(workspace) = this.workspace.upgrade(cx) {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
Dock::focus_dock(workspace, &Default::default(), cx)
|
||||
})
|
||||
}
|
||||
})
|
||||
.with_tooltip::<Self>(
|
||||
0,
|
||||
"Focus Dock".into(),
|
||||
Some(Box::new(FocusDock)),
|
||||
theme.tooltip.clone(),
|
||||
cx,
|
||||
)
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
|
||||
impl StatusItemView for ToggleDockButton {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
_active_pane_item: Option<&dyn crate::ItemHandle>,
|
||||
_cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
//Not applicable
|
||||
}
|
||||
}
|
@ -437,7 +437,7 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
|
||||
for item_event in T::to_item_events(event).into_iter() {
|
||||
match item_event {
|
||||
ItemEvent::CloseItem => {
|
||||
Pane::close_item_by_id(workspace, pane, item.id(), cx)
|
||||
pane.update(cx, |pane, cx| pane.close_item_by_id(item.id(), cx))
|
||||
.detach_and_log_err(cx);
|
||||
return;
|
||||
}
|
||||
@ -769,7 +769,7 @@ impl<T: FollowableItem> FollowableItemHandle for ViewHandle<T> {
|
||||
#[cfg(test)]
|
||||
pub(crate) mod test {
|
||||
use super::{Item, ItemEvent};
|
||||
use crate::{sidebar::SidebarItem, ItemId, ItemNavHistory, Pane, Workspace, WorkspaceId};
|
||||
use crate::{ItemId, ItemNavHistory, Pane, Workspace, WorkspaceId};
|
||||
use gpui::{
|
||||
elements::Empty, AnyElement, AppContext, Element, Entity, ModelHandle, Task, View,
|
||||
ViewContext, ViewHandle, WeakViewHandle,
|
||||
@ -1062,6 +1062,4 @@ pub(crate) mod test {
|
||||
Task::Ready(Some(anyhow::Ok(view)))
|
||||
}
|
||||
}
|
||||
|
||||
impl SidebarItem for TestItem {}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -12,6 +12,7 @@ use gpui::{
|
||||
use project::ProjectEntryId;
|
||||
|
||||
pub fn dragged_item_receiver<Tag, D, F>(
|
||||
pane: &Pane,
|
||||
region_id: usize,
|
||||
drop_index: usize,
|
||||
allow_same_pane: bool,
|
||||
@ -24,15 +25,13 @@ where
|
||||
D: Element<Pane>,
|
||||
F: FnOnce(&mut MouseState, &mut ViewContext<Pane>) -> D,
|
||||
{
|
||||
MouseEventHandler::<Tag, _>::above(region_id, cx, |state, cx| {
|
||||
// Observing hovered will cause a render when the mouse enters regardless
|
||||
// of if mouse position was accessed before
|
||||
let drag_position = if state.hovered() {
|
||||
cx.global::<DragAndDrop<Workspace>>()
|
||||
let drag_and_drop = cx.global::<DragAndDrop<Workspace>>();
|
||||
let drag_position = if (pane.can_drop)(drag_and_drop, cx) {
|
||||
drag_and_drop
|
||||
.currently_dragged::<DraggedItem>(cx.window_id())
|
||||
.map(|(drag_position, _)| drag_position)
|
||||
.or_else(|| {
|
||||
cx.global::<DragAndDrop<Workspace>>()
|
||||
drag_and_drop
|
||||
.currently_dragged::<ProjectEntryId>(cx.window_id())
|
||||
.map(|(drag_position, _)| drag_position)
|
||||
})
|
||||
@ -40,6 +39,10 @@ where
|
||||
None
|
||||
};
|
||||
|
||||
let mut handler = MouseEventHandler::<Tag, _>::above(region_id, cx, |state, cx| {
|
||||
// Observing hovered will cause a render when the mouse enters regardless
|
||||
// of if mouse position was accessed before
|
||||
let drag_position = if state.hovered() { drag_position } else { None };
|
||||
Stack::new()
|
||||
.with_child(render_child(state, cx))
|
||||
.with_children(drag_position.map(|drag_position| {
|
||||
@ -64,7 +67,10 @@ where
|
||||
}
|
||||
})
|
||||
}))
|
||||
})
|
||||
});
|
||||
|
||||
if drag_position.is_some() {
|
||||
handler = handler
|
||||
.on_up(MouseButton::Left, {
|
||||
move |event, pane, cx| {
|
||||
let workspace = pane.workspace.clone();
|
||||
@ -98,6 +104,9 @@ where
|
||||
})
|
||||
}
|
||||
|
||||
handler
|
||||
}
|
||||
|
||||
pub fn handle_dropped_item<V: View>(
|
||||
event: MouseUp,
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
@ -115,7 +124,7 @@ pub fn handle_dropped_item<V: View>(
|
||||
let action = if let Some((_, dragged_item)) =
|
||||
drag_and_drop.currently_dragged::<DraggedItem>(cx.window_id())
|
||||
{
|
||||
Action::Move(dragged_item.pane.clone(), dragged_item.item.id())
|
||||
Action::Move(dragged_item.pane.clone(), dragged_item.handle.id())
|
||||
} else if let Some((_, project_entry)) =
|
||||
drag_and_drop.currently_dragged::<ProjectEntryId>(cx.window_id())
|
||||
{
|
||||
|
@ -7,7 +7,7 @@ use gpui::{
|
||||
elements::*,
|
||||
geometry::{rect::RectF, vector::Vector2F},
|
||||
platform::{CursorStyle, MouseButton},
|
||||
Axis, Border, ModelHandle, ViewContext, ViewHandle,
|
||||
AnyViewHandle, Axis, Border, ModelHandle, ViewContext, ViewHandle,
|
||||
};
|
||||
use project::Project;
|
||||
use serde::Deserialize;
|
||||
@ -71,6 +71,7 @@ impl PaneGroup {
|
||||
follower_states: &FollowerStatesByLeader,
|
||||
active_call: Option<&ModelHandle<ActiveCall>>,
|
||||
active_pane: &ViewHandle<Pane>,
|
||||
zoomed: Option<&AnyViewHandle>,
|
||||
app_state: &Arc<AppState>,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) -> AnyElement<Workspace> {
|
||||
@ -80,6 +81,7 @@ impl PaneGroup {
|
||||
follower_states,
|
||||
active_call,
|
||||
active_pane,
|
||||
zoomed,
|
||||
app_state,
|
||||
cx,
|
||||
)
|
||||
@ -134,6 +136,7 @@ impl Member {
|
||||
follower_states: &FollowerStatesByLeader,
|
||||
active_call: Option<&ModelHandle<ActiveCall>>,
|
||||
active_pane: &ViewHandle<Pane>,
|
||||
zoomed: Option<&AnyViewHandle>,
|
||||
app_state: &Arc<AppState>,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) -> AnyElement<Workspace> {
|
||||
@ -141,6 +144,12 @@ impl Member {
|
||||
|
||||
match self {
|
||||
Member::Pane(pane) => {
|
||||
let pane_element = if Some(&**pane) == zoomed {
|
||||
Empty::new().into_any()
|
||||
} else {
|
||||
ChildView::new(pane, cx).into_any()
|
||||
};
|
||||
|
||||
let leader = follower_states
|
||||
.iter()
|
||||
.find_map(|(leader_id, follower_states)| {
|
||||
@ -257,7 +266,7 @@ impl Member {
|
||||
};
|
||||
|
||||
Stack::new()
|
||||
.with_child(ChildView::new(pane, cx).contained().with_border(border))
|
||||
.with_child(pane_element.contained().with_border(border))
|
||||
.with_children(leader_status_box)
|
||||
.into_any()
|
||||
}
|
||||
@ -267,6 +276,7 @@ impl Member {
|
||||
follower_states,
|
||||
active_call,
|
||||
active_pane,
|
||||
zoomed,
|
||||
app_state,
|
||||
cx,
|
||||
),
|
||||
@ -371,6 +381,7 @@ impl PaneAxis {
|
||||
follower_state: &FollowerStatesByLeader,
|
||||
active_call: Option<&ModelHandle<ActiveCall>>,
|
||||
active_pane: &ViewHandle<Pane>,
|
||||
zoomed: Option<&AnyViewHandle>,
|
||||
app_state: &Arc<AppState>,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) -> AnyElement<Workspace> {
|
||||
@ -388,6 +399,7 @@ impl PaneAxis {
|
||||
follower_state,
|
||||
active_call,
|
||||
active_pane,
|
||||
zoomed,
|
||||
app_state,
|
||||
cx,
|
||||
);
|
||||
|
@ -11,7 +11,6 @@ use gpui::{platform::WindowBounds, Axis};
|
||||
use util::{unzip_option, ResultExt};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::dock::DockPosition;
|
||||
use crate::WorkspaceId;
|
||||
|
||||
use model::{
|
||||
@ -19,15 +18,17 @@ use model::{
|
||||
WorkspaceLocation,
|
||||
};
|
||||
|
||||
use self::model::DockStructure;
|
||||
|
||||
define_connection! {
|
||||
// Current schema shape using pseudo-rust syntax:
|
||||
//
|
||||
// workspaces(
|
||||
// workspace_id: usize, // Primary key for workspaces
|
||||
// workspace_location: Bincode<Vec<PathBuf>>,
|
||||
// dock_visible: bool,
|
||||
// dock_anchor: DockAnchor, // 'Bottom' / 'Right' / 'Expanded'
|
||||
// dock_pane: Option<usize>, // PaneId
|
||||
// dock_visible: bool, // Deprecated
|
||||
// dock_anchor: DockAnchor, // Deprecated
|
||||
// dock_pane: Option<usize>, // Deprecated
|
||||
// left_sidebar_open: boolean,
|
||||
// timestamp: String, // UTC YYYY-MM-DD HH:MM:SS
|
||||
// window_state: String, // WindowBounds Discriminant
|
||||
@ -71,9 +72,9 @@ 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
|
||||
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)
|
||||
@ -131,6 +132,36 @@ define_connection! {
|
||||
ALTER TABLE workspaces ADD COLUMN window_width REAL;
|
||||
ALTER TABLE workspaces ADD COLUMN window_height REAL;
|
||||
ALTER TABLE workspaces ADD COLUMN display BLOB;
|
||||
),
|
||||
// Drop foreign key constraint from workspaces.dock_pane to panes table.
|
||||
sql!(
|
||||
CREATE TABLE workspaces_2(
|
||||
workspace_id INTEGER PRIMARY KEY,
|
||||
workspace_location BLOB UNIQUE,
|
||||
dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed.
|
||||
dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed.
|
||||
dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed.
|
||||
left_sidebar_open INTEGER, // Boolean
|
||||
timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
window_state TEXT,
|
||||
window_x REAL,
|
||||
window_y REAL,
|
||||
window_width REAL,
|
||||
window_height REAL,
|
||||
display BLOB
|
||||
) STRICT;
|
||||
INSERT INTO workspaces_2 SELECT * FROM workspaces;
|
||||
DROP TABLE workspaces;
|
||||
ALTER TABLE workspaces_2 RENAME TO workspaces;
|
||||
),
|
||||
// Add panels related information
|
||||
sql!(
|
||||
ALTER TABLE workspaces ADD COLUMN left_dock_visible INTEGER; //bool
|
||||
ALTER TABLE workspaces ADD COLUMN left_dock_active_panel TEXT;
|
||||
ALTER TABLE workspaces ADD COLUMN right_dock_visible INTEGER; //bool
|
||||
ALTER TABLE workspaces ADD COLUMN right_dock_active_panel TEXT;
|
||||
ALTER TABLE workspaces ADD COLUMN bottom_dock_visible INTEGER; //bool
|
||||
ALTER TABLE workspaces ADD COLUMN bottom_dock_active_panel TEXT;
|
||||
)];
|
||||
}
|
||||
|
||||
@ -146,27 +177,29 @@ impl WorkspaceDb {
|
||||
|
||||
// Note that we re-assign the workspace_id here in case it's empty
|
||||
// and we've grabbed the most recent workspace
|
||||
let (workspace_id, workspace_location, left_sidebar_open, dock_position, bounds, display): (
|
||||
let (workspace_id, workspace_location, bounds, display, docks): (
|
||||
WorkspaceId,
|
||||
WorkspaceLocation,
|
||||
bool,
|
||||
DockPosition,
|
||||
Option<WindowBounds>,
|
||||
Option<Uuid>,
|
||||
DockStructure,
|
||||
) = self
|
||||
.select_row_bound(sql! {
|
||||
SELECT
|
||||
workspace_id,
|
||||
workspace_location,
|
||||
left_sidebar_open,
|
||||
dock_visible,
|
||||
dock_anchor,
|
||||
window_state,
|
||||
window_x,
|
||||
window_y,
|
||||
window_width,
|
||||
window_height,
|
||||
display
|
||||
display,
|
||||
left_dock_visible,
|
||||
left_dock_active_panel,
|
||||
right_dock_visible,
|
||||
right_dock_active_panel,
|
||||
bottom_dock_visible,
|
||||
bottom_dock_active_panel
|
||||
FROM workspaces
|
||||
WHERE workspace_location = ?
|
||||
})
|
||||
@ -178,18 +211,13 @@ impl WorkspaceDb {
|
||||
Some(SerializedWorkspace {
|
||||
id: workspace_id,
|
||||
location: workspace_location.clone(),
|
||||
dock_pane: self
|
||||
.get_dock_pane(workspace_id)
|
||||
.context("Getting dock pane")
|
||||
.log_err()?,
|
||||
center_group: self
|
||||
.get_center_pane_group(workspace_id)
|
||||
.context("Getting center group")
|
||||
.log_err()?,
|
||||
dock_position,
|
||||
left_sidebar_open,
|
||||
bounds,
|
||||
display,
|
||||
docks,
|
||||
})
|
||||
}
|
||||
|
||||
@ -200,7 +228,6 @@ impl WorkspaceDb {
|
||||
conn.with_savepoint("update_worktrees", || {
|
||||
// Clear out panes and pane_groups
|
||||
conn.exec_bound(sql!(
|
||||
UPDATE workspaces SET dock_pane = NULL WHERE workspace_id = ?1;
|
||||
DELETE FROM pane_groups WHERE workspace_id = ?1;
|
||||
DELETE FROM panes WHERE workspace_id = ?1;))?(workspace.id)
|
||||
.expect("Clearing old panes");
|
||||
@ -215,42 +242,32 @@ impl WorkspaceDb {
|
||||
INSERT INTO workspaces(
|
||||
workspace_id,
|
||||
workspace_location,
|
||||
left_sidebar_open,
|
||||
dock_visible,
|
||||
dock_anchor,
|
||||
left_dock_visible,
|
||||
left_dock_active_panel,
|
||||
right_dock_visible,
|
||||
right_dock_active_panel,
|
||||
bottom_dock_visible,
|
||||
bottom_dock_active_panel,
|
||||
timestamp
|
||||
)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, CURRENT_TIMESTAMP)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT DO
|
||||
UPDATE SET
|
||||
workspace_location = ?2,
|
||||
left_sidebar_open = ?3,
|
||||
dock_visible = ?4,
|
||||
dock_anchor = ?5,
|
||||
left_dock_visible = ?3,
|
||||
left_dock_active_panel = ?4,
|
||||
right_dock_visible = ?5,
|
||||
right_dock_active_panel = ?6,
|
||||
bottom_dock_visible = ?7,
|
||||
bottom_dock_active_panel = ?8,
|
||||
timestamp = CURRENT_TIMESTAMP
|
||||
))?((
|
||||
workspace.id,
|
||||
&workspace.location,
|
||||
workspace.left_sidebar_open,
|
||||
workspace.dock_position,
|
||||
))
|
||||
))?((workspace.id, &workspace.location, workspace.docks))
|
||||
.context("Updating workspace")?;
|
||||
|
||||
// Save center pane group and dock pane
|
||||
// Save center pane group
|
||||
Self::save_pane_group(conn, workspace.id, &workspace.center_group, None)
|
||||
.context("save pane group in save workspace")?;
|
||||
|
||||
let dock_id = Self::save_pane(conn, workspace.id, &workspace.dock_pane, None, true)
|
||||
.context("save pane in save workspace")?;
|
||||
|
||||
// Complete workspace initialization
|
||||
conn.exec_bound(sql!(
|
||||
UPDATE workspaces
|
||||
SET dock_pane = ?
|
||||
WHERE workspace_id = ?
|
||||
))?((dock_id, workspace.id))
|
||||
.context("Finishing initialization with dock pane")?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.log_err();
|
||||
@ -402,32 +419,17 @@ impl WorkspaceDb {
|
||||
Ok(())
|
||||
}
|
||||
SerializedPaneGroup::Pane(pane) => {
|
||||
Self::save_pane(conn, workspace_id, &pane, parent, false)?;
|
||||
Self::save_pane(conn, workspace_id, &pane, parent)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_dock_pane(&self, workspace_id: WorkspaceId) -> Result<SerializedPane> {
|
||||
let (pane_id, active) = self.select_row_bound(sql!(
|
||||
SELECT pane_id, active
|
||||
FROM panes
|
||||
WHERE pane_id = (SELECT dock_pane FROM workspaces WHERE workspace_id = ?)
|
||||
))?(workspace_id)?
|
||||
.context("No dock pane for workspace")?;
|
||||
|
||||
Ok(SerializedPane::new(
|
||||
self.get_items(pane_id).context("Reading items")?,
|
||||
active,
|
||||
))
|
||||
}
|
||||
|
||||
fn save_pane(
|
||||
conn: &Connection,
|
||||
workspace_id: WorkspaceId,
|
||||
pane: &SerializedPane,
|
||||
parent: Option<(GroupId, usize)>, // None indicates BOTH dock pane AND center_pane
|
||||
dock: bool,
|
||||
parent: Option<(GroupId, usize)>,
|
||||
) -> Result<PaneId> {
|
||||
let pane_id = conn.select_row_bound::<_, i64>(sql!(
|
||||
INSERT INTO panes(workspace_id, active)
|
||||
@ -436,13 +438,11 @@ impl WorkspaceDb {
|
||||
))?((workspace_id, pane.active))?
|
||||
.ok_or_else(|| anyhow!("Could not retrieve inserted pane_id"))?;
|
||||
|
||||
if !dock {
|
||||
let (parent_id, order) = unzip_option(parent);
|
||||
conn.exec_bound(sql!(
|
||||
INSERT INTO center_panes(pane_id, parent_group_id, position)
|
||||
VALUES (?, ?, ?)
|
||||
))?((pane_id, parent_id, order))?;
|
||||
}
|
||||
|
||||
Self::save_items(conn, workspace_id, pane_id, &pane.children).context("Saving items")?;
|
||||
|
||||
@ -498,9 +498,7 @@ impl WorkspaceDb {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::DockAnchor;
|
||||
use db::open_test_db;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_next_id_stability() {
|
||||
@ -575,23 +573,19 @@ mod tests {
|
||||
let mut workspace_1 = SerializedWorkspace {
|
||||
id: 1,
|
||||
location: (["/tmp", "/tmp2"]).into(),
|
||||
dock_position: crate::dock::DockPosition::Shown(DockAnchor::Bottom),
|
||||
center_group: Default::default(),
|
||||
dock_pane: Default::default(),
|
||||
left_sidebar_open: true,
|
||||
bounds: Default::default(),
|
||||
display: Default::default(),
|
||||
docks: Default::default(),
|
||||
};
|
||||
|
||||
let mut workspace_2 = SerializedWorkspace {
|
||||
let workspace_2 = SerializedWorkspace {
|
||||
id: 2,
|
||||
location: (["/tmp"]).into(),
|
||||
dock_position: crate::dock::DockPosition::Hidden(DockAnchor::Expanded),
|
||||
center_group: Default::default(),
|
||||
dock_pane: Default::default(),
|
||||
left_sidebar_open: false,
|
||||
bounds: Default::default(),
|
||||
display: Default::default(),
|
||||
docks: Default::default(),
|
||||
};
|
||||
|
||||
db.save_workspace(workspace_1.clone()).await;
|
||||
@ -615,12 +609,6 @@ mod tests {
|
||||
workspace_1.location = (["/tmp", "/tmp3"]).into();
|
||||
db.save_workspace(workspace_1.clone()).await;
|
||||
db.save_workspace(workspace_1).await;
|
||||
|
||||
workspace_2.dock_pane.children.push(SerializedItem {
|
||||
kind: Arc::from("Test"),
|
||||
item_id: 10,
|
||||
active: true,
|
||||
});
|
||||
db.save_workspace(workspace_2).await;
|
||||
|
||||
let test_text_2 = db
|
||||
@ -644,16 +632,6 @@ mod tests {
|
||||
|
||||
let db = WorkspaceDb(open_test_db("test_full_workspace_serialization").await);
|
||||
|
||||
let dock_pane = crate::persistence::model::SerializedPane {
|
||||
children: vec![
|
||||
SerializedItem::new("Terminal", 1, false),
|
||||
SerializedItem::new("Terminal", 2, false),
|
||||
SerializedItem::new("Terminal", 3, true),
|
||||
SerializedItem::new("Terminal", 4, false),
|
||||
],
|
||||
active: false,
|
||||
};
|
||||
|
||||
// -----------------
|
||||
// | 1,2 | 5,6 |
|
||||
// | - - - | |
|
||||
@ -694,12 +672,10 @@ mod tests {
|
||||
let workspace = SerializedWorkspace {
|
||||
id: 5,
|
||||
location: (["/tmp", "/tmp2"]).into(),
|
||||
dock_position: DockPosition::Shown(DockAnchor::Bottom),
|
||||
center_group,
|
||||
dock_pane,
|
||||
left_sidebar_open: true,
|
||||
bounds: Default::default(),
|
||||
display: Default::default(),
|
||||
docks: Default::default(),
|
||||
};
|
||||
|
||||
db.save_workspace(workspace.clone()).await;
|
||||
@ -724,23 +700,19 @@ mod tests {
|
||||
let workspace_1 = SerializedWorkspace {
|
||||
id: 1,
|
||||
location: (["/tmp", "/tmp2"]).into(),
|
||||
dock_position: crate::dock::DockPosition::Shown(DockAnchor::Bottom),
|
||||
center_group: Default::default(),
|
||||
dock_pane: Default::default(),
|
||||
left_sidebar_open: true,
|
||||
bounds: Default::default(),
|
||||
display: Default::default(),
|
||||
docks: Default::default(),
|
||||
};
|
||||
|
||||
let mut workspace_2 = SerializedWorkspace {
|
||||
id: 2,
|
||||
location: (["/tmp"]).into(),
|
||||
dock_position: crate::dock::DockPosition::Hidden(DockAnchor::Expanded),
|
||||
center_group: Default::default(),
|
||||
dock_pane: Default::default(),
|
||||
left_sidebar_open: false,
|
||||
bounds: Default::default(),
|
||||
display: Default::default(),
|
||||
docks: Default::default(),
|
||||
};
|
||||
|
||||
db.save_workspace(workspace_1.clone()).await;
|
||||
@ -773,12 +745,10 @@ mod tests {
|
||||
let mut workspace_3 = SerializedWorkspace {
|
||||
id: 3,
|
||||
location: (&["/tmp", "/tmp2"]).into(),
|
||||
dock_position: DockPosition::Shown(DockAnchor::Right),
|
||||
center_group: Default::default(),
|
||||
dock_pane: Default::default(),
|
||||
left_sidebar_open: false,
|
||||
bounds: Default::default(),
|
||||
display: Default::default(),
|
||||
docks: Default::default(),
|
||||
};
|
||||
|
||||
db.save_workspace(workspace_3.clone()).await;
|
||||
@ -798,52 +768,23 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
use crate::dock::DockPosition;
|
||||
use crate::persistence::model::SerializedWorkspace;
|
||||
use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup};
|
||||
|
||||
fn default_workspace<P: AsRef<Path>>(
|
||||
workspace_id: &[P],
|
||||
dock_pane: SerializedPane,
|
||||
center_group: &SerializedPaneGroup,
|
||||
) -> SerializedWorkspace {
|
||||
SerializedWorkspace {
|
||||
id: 4,
|
||||
location: workspace_id.into(),
|
||||
dock_position: crate::dock::DockPosition::Hidden(DockAnchor::Right),
|
||||
center_group: center_group.clone(),
|
||||
dock_pane,
|
||||
left_sidebar_open: true,
|
||||
bounds: Default::default(),
|
||||
display: Default::default(),
|
||||
docks: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_basic_dock_pane() {
|
||||
env_logger::try_init().ok();
|
||||
|
||||
let db = WorkspaceDb(open_test_db("basic_dock_pane").await);
|
||||
|
||||
let dock_pane = crate::persistence::model::SerializedPane::new(
|
||||
vec![
|
||||
SerializedItem::new("Terminal", 1, false),
|
||||
SerializedItem::new("Terminal", 4, false),
|
||||
SerializedItem::new("Terminal", 2, false),
|
||||
SerializedItem::new("Terminal", 3, true),
|
||||
],
|
||||
false,
|
||||
);
|
||||
|
||||
let workspace = default_workspace(&["/tmp"], dock_pane, &Default::default());
|
||||
|
||||
db.save_workspace(workspace.clone()).await;
|
||||
|
||||
let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
|
||||
|
||||
assert_eq!(workspace.dock_pane, new_workspace.dock_pane);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_simple_split() {
|
||||
env_logger::try_init().ok();
|
||||
@ -887,7 +828,7 @@ mod tests {
|
||||
],
|
||||
};
|
||||
|
||||
let workspace = default_workspace(&["/tmp"], Default::default(), ¢er_pane);
|
||||
let workspace = default_workspace(&["/tmp"], ¢er_pane);
|
||||
|
||||
db.save_workspace(workspace.clone()).await;
|
||||
|
||||
@ -936,7 +877,7 @@ mod tests {
|
||||
|
||||
let id = &["/tmp"];
|
||||
|
||||
let mut workspace = default_workspace(id, Default::default(), ¢er_pane);
|
||||
let mut workspace = default_workspace(id, ¢er_pane);
|
||||
|
||||
db.save_workspace(workspace.clone()).await;
|
||||
|
||||
|
@ -1,7 +1,4 @@
|
||||
use crate::{
|
||||
dock::DockPosition, item::ItemHandle, DockAnchor, ItemDeserializers, Member, Pane, PaneAxis,
|
||||
Workspace, WorkspaceId,
|
||||
};
|
||||
use crate::{item::ItemHandle, ItemDeserializers, Member, Pane, PaneAxis, Workspace, WorkspaceId};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use async_recursion::async_recursion;
|
||||
use db::sqlez::{
|
||||
@ -62,12 +59,68 @@ impl Column for WorkspaceLocation {
|
||||
pub struct SerializedWorkspace {
|
||||
pub id: WorkspaceId,
|
||||
pub location: WorkspaceLocation,
|
||||
pub dock_position: DockPosition,
|
||||
pub center_group: SerializedPaneGroup,
|
||||
pub dock_pane: SerializedPane,
|
||||
pub left_sidebar_open: bool,
|
||||
pub bounds: Option<WindowBounds>,
|
||||
pub display: Option<Uuid>,
|
||||
pub docks: DockStructure,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Default)]
|
||||
pub struct DockStructure {
|
||||
pub(crate) left: DockData,
|
||||
pub(crate) right: DockData,
|
||||
pub(crate) bottom: DockData,
|
||||
}
|
||||
|
||||
impl Column for DockStructure {
|
||||
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
|
||||
let (left, next_index) = DockData::column(statement, start_index)?;
|
||||
let (right, next_index) = DockData::column(statement, next_index)?;
|
||||
let (bottom, next_index) = DockData::column(statement, next_index)?;
|
||||
Ok((
|
||||
DockStructure {
|
||||
left,
|
||||
right,
|
||||
bottom,
|
||||
},
|
||||
next_index,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl Bind for DockStructure {
|
||||
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
|
||||
let next_index = statement.bind(&self.left, start_index)?;
|
||||
let next_index = statement.bind(&self.right, next_index)?;
|
||||
statement.bind(&self.bottom, next_index)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Default)]
|
||||
pub struct DockData {
|
||||
pub(crate) visible: bool,
|
||||
pub(crate) active_panel: Option<String>,
|
||||
}
|
||||
|
||||
impl Column for DockData {
|
||||
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
|
||||
let (visible, next_index) = Option::<bool>::column(statement, start_index)?;
|
||||
let (active_panel, next_index) = Option::<String>::column(statement, next_index)?;
|
||||
Ok((
|
||||
DockData {
|
||||
visible: visible.unwrap_or(false),
|
||||
active_panel,
|
||||
},
|
||||
next_index,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl Bind for DockData {
|
||||
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
|
||||
let next_index = statement.bind(&self.visible, start_index)?;
|
||||
statement.bind(&self.active_panel, next_index)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
@ -266,9 +319,9 @@ impl StaticColumnCount for SerializedItem {
|
||||
}
|
||||
impl Bind for &SerializedItem {
|
||||
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
|
||||
let next_index = statement.bind(self.kind.clone(), start_index)?;
|
||||
let next_index = statement.bind(self.item_id, next_index)?;
|
||||
statement.bind(self.active, next_index)
|
||||
let next_index = statement.bind(&self.kind, start_index)?;
|
||||
let next_index = statement.bind(&self.item_id, next_index)?;
|
||||
statement.bind(&self.active, next_index)
|
||||
}
|
||||
}
|
||||
|
||||
@ -287,64 +340,3 @@ impl Column for SerializedItem {
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl StaticColumnCount for DockPosition {
|
||||
fn column_count() -> usize {
|
||||
2
|
||||
}
|
||||
}
|
||||
impl Bind for DockPosition {
|
||||
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
|
||||
let next_index = statement.bind(self.is_visible(), start_index)?;
|
||||
statement.bind(self.anchor(), next_index)
|
||||
}
|
||||
}
|
||||
|
||||
impl Column for DockPosition {
|
||||
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
|
||||
let (visible, next_index) = bool::column(statement, start_index)?;
|
||||
let (dock_anchor, next_index) = DockAnchor::column(statement, next_index)?;
|
||||
let position = if visible {
|
||||
DockPosition::Shown(dock_anchor)
|
||||
} else {
|
||||
DockPosition::Hidden(dock_anchor)
|
||||
};
|
||||
Ok((position, next_index))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::WorkspaceLocation;
|
||||
use crate::DockAnchor;
|
||||
use db::sqlez::connection::Connection;
|
||||
|
||||
#[test]
|
||||
fn test_workspace_round_trips() {
|
||||
let db = Connection::open_memory(Some("workspace_id_round_trips"));
|
||||
|
||||
db.exec(indoc::indoc! {"
|
||||
CREATE TABLE workspace_id_test(
|
||||
workspace_id INTEGER,
|
||||
dock_anchor TEXT
|
||||
);"})
|
||||
.unwrap()()
|
||||
.unwrap();
|
||||
|
||||
let workspace_id: WorkspaceLocation = WorkspaceLocation::from(&["\test2", "\test1"]);
|
||||
|
||||
db.exec_bound("INSERT INTO workspace_id_test(workspace_id, dock_anchor) VALUES (?,?)")
|
||||
.unwrap()((&workspace_id, DockAnchor::Bottom))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
db.select_row("SELECT workspace_id, dock_anchor FROM workspace_id_test LIMIT 1")
|
||||
.unwrap()()
|
||||
.unwrap(),
|
||||
Some((
|
||||
WorkspaceLocation::from(&["\test1", "\test2"]),
|
||||
DockAnchor::Bottom
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,321 +0,0 @@
|
||||
use crate::{StatusItemView, Workspace};
|
||||
use gpui::{
|
||||
elements::*, impl_actions, platform::CursorStyle, platform::MouseButton, AnyViewHandle,
|
||||
AppContext, Entity, Subscription, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use std::rc::Rc;
|
||||
|
||||
pub trait SidebarItem: View {
|
||||
fn should_activate_item_on_event(&self, _: &Self::Event, _: &AppContext) -> bool {
|
||||
false
|
||||
}
|
||||
fn should_show_badge(&self, _: &AppContext) -> bool {
|
||||
false
|
||||
}
|
||||
fn contains_focused_view(&self, _: &AppContext) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub trait SidebarItemHandle {
|
||||
fn id(&self) -> usize;
|
||||
fn should_show_badge(&self, cx: &WindowContext) -> bool;
|
||||
fn is_focused(&self, cx: &WindowContext) -> bool;
|
||||
fn as_any(&self) -> &AnyViewHandle;
|
||||
}
|
||||
|
||||
impl<T> SidebarItemHandle for ViewHandle<T>
|
||||
where
|
||||
T: SidebarItem,
|
||||
{
|
||||
fn id(&self) -> usize {
|
||||
self.id()
|
||||
}
|
||||
|
||||
fn should_show_badge(&self, cx: &WindowContext) -> bool {
|
||||
self.read(cx).should_show_badge(cx)
|
||||
}
|
||||
|
||||
fn is_focused(&self, cx: &WindowContext) -> bool {
|
||||
ViewHandle::is_focused(self, cx) || self.read(cx).contains_focused_view(cx)
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &AnyViewHandle {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&dyn SidebarItemHandle> for AnyViewHandle {
|
||||
fn from(val: &dyn SidebarItemHandle) -> Self {
|
||||
val.as_any().clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Sidebar {
|
||||
sidebar_side: SidebarSide,
|
||||
items: Vec<Item>,
|
||||
is_open: bool,
|
||||
active_item_ix: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
|
||||
pub enum SidebarSide {
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
impl SidebarSide {
|
||||
fn to_resizable_side(self) -> Side {
|
||||
match self {
|
||||
Self::Left => Side::Right,
|
||||
Self::Right => Side::Left,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Item {
|
||||
icon_path: &'static str,
|
||||
tooltip: String,
|
||||
view: Rc<dyn SidebarItemHandle>,
|
||||
_subscriptions: [Subscription; 2],
|
||||
}
|
||||
|
||||
pub struct SidebarButtons {
|
||||
sidebar: ViewHandle<Sidebar>,
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq)]
|
||||
pub struct ToggleSidebarItem {
|
||||
pub sidebar_side: SidebarSide,
|
||||
pub item_index: usize,
|
||||
}
|
||||
|
||||
impl_actions!(workspace, [ToggleSidebarItem]);
|
||||
|
||||
impl Sidebar {
|
||||
pub fn new(sidebar_side: SidebarSide) -> Self {
|
||||
Self {
|
||||
sidebar_side,
|
||||
items: Default::default(),
|
||||
active_item_ix: 0,
|
||||
is_open: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_open(&self) -> bool {
|
||||
self.is_open
|
||||
}
|
||||
|
||||
pub fn active_item_ix(&self) -> usize {
|
||||
self.active_item_ix
|
||||
}
|
||||
|
||||
pub fn set_open(&mut self, open: bool, cx: &mut ViewContext<Self>) {
|
||||
if open != self.is_open {
|
||||
self.is_open = open;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_open(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if self.is_open {}
|
||||
self.is_open = !self.is_open;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn add_item<T: SidebarItem>(
|
||||
&mut self,
|
||||
icon_path: &'static str,
|
||||
tooltip: String,
|
||||
view: ViewHandle<T>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let subscriptions = [
|
||||
cx.observe(&view, |_, _, cx| cx.notify()),
|
||||
cx.subscribe(&view, |this, view, event, cx| {
|
||||
if view.read(cx).should_activate_item_on_event(event, cx) {
|
||||
if let Some(ix) = this
|
||||
.items
|
||||
.iter()
|
||||
.position(|item| item.view.id() == view.id())
|
||||
{
|
||||
this.activate_item(ix, cx);
|
||||
}
|
||||
}
|
||||
}),
|
||||
];
|
||||
|
||||
self.items.push(Item {
|
||||
icon_path,
|
||||
tooltip,
|
||||
view: Rc::new(view),
|
||||
_subscriptions: subscriptions,
|
||||
});
|
||||
cx.notify()
|
||||
}
|
||||
|
||||
pub fn activate_item(&mut self, item_ix: usize, cx: &mut ViewContext<Self>) {
|
||||
self.active_item_ix = item_ix;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn toggle_item(&mut self, item_ix: usize, cx: &mut ViewContext<Self>) {
|
||||
if self.active_item_ix == item_ix {
|
||||
self.is_open = false;
|
||||
} else {
|
||||
self.active_item_ix = item_ix;
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn active_item(&self) -> Option<&Rc<dyn SidebarItemHandle>> {
|
||||
if self.is_open {
|
||||
self.items.get(self.active_item_ix).map(|item| &item.view)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for Sidebar {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for Sidebar {
|
||||
fn ui_name() -> &'static str {
|
||||
"Sidebar"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
if let Some(active_item) = self.active_item() {
|
||||
enum ResizeHandleTag {}
|
||||
let style = &theme::current(cx).workspace.sidebar;
|
||||
ChildView::new(active_item.as_any(), cx)
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.with_resize_handle::<ResizeHandleTag>(
|
||||
self.sidebar_side as usize,
|
||||
self.sidebar_side.to_resizable_side(),
|
||||
4.,
|
||||
style.initial_size,
|
||||
cx,
|
||||
)
|
||||
.into_any()
|
||||
} else {
|
||||
Empty::new().into_any()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SidebarButtons {
|
||||
pub fn new(
|
||||
sidebar: ViewHandle<Sidebar>,
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
cx.observe(&sidebar, |_, _, cx| cx.notify()).detach();
|
||||
Self { sidebar, workspace }
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for SidebarButtons {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for SidebarButtons {
|
||||
fn ui_name() -> &'static str {
|
||||
"SidebarToggleButton"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
let theme = &theme::current(cx);
|
||||
let tooltip_style = theme.tooltip.clone();
|
||||
let theme = &theme.workspace.status_bar.sidebar_buttons;
|
||||
let sidebar = self.sidebar.read(cx);
|
||||
let item_style = theme.item.clone();
|
||||
let badge_style = theme.badge;
|
||||
let active_ix = sidebar.active_item_ix;
|
||||
let is_open = sidebar.is_open;
|
||||
let sidebar_side = sidebar.sidebar_side;
|
||||
let group_style = match sidebar_side {
|
||||
SidebarSide::Left => theme.group_left,
|
||||
SidebarSide::Right => theme.group_right,
|
||||
};
|
||||
|
||||
#[allow(clippy::needless_collect)]
|
||||
let items = sidebar
|
||||
.items
|
||||
.iter()
|
||||
.map(|item| (item.icon_path, item.tooltip.clone(), item.view.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Flex::row()
|
||||
.with_children(items.into_iter().enumerate().map(
|
||||
|(ix, (icon_path, tooltip, item_view))| {
|
||||
let action = ToggleSidebarItem {
|
||||
sidebar_side,
|
||||
item_index: ix,
|
||||
};
|
||||
MouseEventHandler::<Self, _>::new(ix, cx, |state, cx| {
|
||||
let is_active = is_open && ix == active_ix;
|
||||
let style = item_style.style_for(state, is_active);
|
||||
Stack::new()
|
||||
.with_child(Svg::new(icon_path).with_color(style.icon_color))
|
||||
.with_children(if !is_active && item_view.should_show_badge(cx) {
|
||||
Some(
|
||||
Empty::new()
|
||||
.collapsed()
|
||||
.contained()
|
||||
.with_style(badge_style)
|
||||
.aligned()
|
||||
.bottom()
|
||||
.right(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.constrained()
|
||||
.with_width(style.icon_size)
|
||||
.with_height(style.icon_size)
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, {
|
||||
let action = action.clone();
|
||||
move |_, this, cx| {
|
||||
if let Some(workspace) = this.workspace.upgrade(cx) {
|
||||
let action = action.clone();
|
||||
cx.window_context().defer(move |cx| {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.toggle_sidebar_item(&action, cx)
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.with_tooltip::<Self>(
|
||||
ix,
|
||||
tooltip,
|
||||
Some(Box::new(action)),
|
||||
tooltip_style.clone(),
|
||||
cx,
|
||||
)
|
||||
},
|
||||
))
|
||||
.contained()
|
||||
.with_style(group_style)
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
|
||||
impl StatusItemView for SidebarButtons {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
_: Option<&dyn crate::ItemHandle>,
|
||||
_: &mut ViewContext<Self>,
|
||||
) {
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,8 +1,3 @@
|
||||
use anyhow::bail;
|
||||
use db::sqlez::{
|
||||
bindable::{Bind, Column, StaticColumnCount},
|
||||
statement::Statement,
|
||||
};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Setting;
|
||||
@ -13,17 +8,15 @@ pub struct WorkspaceSettings {
|
||||
pub confirm_quit: bool,
|
||||
pub show_call_status_icon: bool,
|
||||
pub autosave: AutosaveSetting,
|
||||
pub default_dock_anchor: DockAnchor,
|
||||
pub git: GitSettings,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct WorkspaceSettingsContent {
|
||||
pub active_pane_magnification: Option<f32>,
|
||||
pub confirm_quit: Option<bool>,
|
||||
pub show_call_status_icon: Option<bool>,
|
||||
pub autosave: Option<AutosaveSetting>,
|
||||
pub default_dock_anchor: Option<DockAnchor>,
|
||||
pub git: Option<GitSettings>,
|
||||
}
|
||||
|
||||
@ -36,15 +29,6 @@ pub enum AutosaveSetting {
|
||||
OnWindowChange,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, Default, Copy, Clone, Hash, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum DockAnchor {
|
||||
#[default]
|
||||
Bottom,
|
||||
Right,
|
||||
Expanded,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct GitSettings {
|
||||
pub git_gutter: Option<GitGutterSetting>,
|
||||
@ -59,35 +43,6 @@ pub enum GitGutterSetting {
|
||||
Hide,
|
||||
}
|
||||
|
||||
impl StaticColumnCount for DockAnchor {}
|
||||
|
||||
impl Bind for DockAnchor {
|
||||
fn bind(&self, statement: &Statement, start_index: i32) -> anyhow::Result<i32> {
|
||||
match self {
|
||||
DockAnchor::Bottom => "Bottom",
|
||||
DockAnchor::Right => "Right",
|
||||
DockAnchor::Expanded => "Expanded",
|
||||
}
|
||||
.bind(statement, start_index)
|
||||
}
|
||||
}
|
||||
|
||||
impl Column for DockAnchor {
|
||||
fn column(statement: &mut Statement, start_index: i32) -> anyhow::Result<(Self, i32)> {
|
||||
String::column(statement, start_index).and_then(|(anchor_text, next_index)| {
|
||||
Ok((
|
||||
match anchor_text.as_ref() {
|
||||
"Bottom" => DockAnchor::Bottom,
|
||||
"Right" => DockAnchor::Right,
|
||||
"Expanded" => DockAnchor::Expanded,
|
||||
_ => bail!("Stored dock anchor is incorrect"),
|
||||
},
|
||||
next_index,
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Setting for WorkspaceSettings {
|
||||
const KEY: Option<&'static str> = None;
|
||||
|
||||
|
@ -56,8 +56,7 @@ use fs::RealFs;
|
||||
use staff_mode::StaffMode;
|
||||
use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt};
|
||||
use workspace::{
|
||||
dock::FocusDock, item::ItemHandle, notifications::NotifyResultExt, AppState, OpenSettings,
|
||||
Workspace,
|
||||
item::ItemHandle, notifications::NotifyResultExt, AppState, OpenSettings, Workspace,
|
||||
};
|
||||
use zed::{
|
||||
self, build_window_options, handle_keymap_file_changes, initialize_workspace, languages, menus,
|
||||
@ -187,7 +186,6 @@ fn main() {
|
||||
fs,
|
||||
build_window_options,
|
||||
initialize_workspace,
|
||||
dock_default_item_factory,
|
||||
background_actions,
|
||||
});
|
||||
cx.set_global(Arc::downgrade(&app_state));
|
||||
@ -817,7 +815,6 @@ pub fn background_actions() -> &'static [(&'static str, &'static dyn Action)] {
|
||||
&[
|
||||
("Go to file", &file_finder::Toggle),
|
||||
("Open command palette", &command_palette::Toggle),
|
||||
("Focus the dock", &FocusDock),
|
||||
("Open recent projects", &recent_projects::OpenRecent),
|
||||
("Change your settings", &OpenSettings),
|
||||
]
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user