Detailed changes
@@ -0,0 +1,15 @@
+name: 'Check formatting'
+description: 'Checks code formatting use cargo fmt'
+
+runs:
+ using: "composite"
+ steps:
+ - name: Install Rust
+ shell: bash -euxo pipefail {0}
+ run: |
+ rustup set profile minimal
+ rustup update stable
+
+ - name: cargo fmt
+ shell: bash -euxo pipefail {0}
+ run: cargo fmt --all -- --check
@@ -0,0 +1,34 @@
+name: "Run tests"
+description: "Runs the tests"
+
+runs:
+ using: "composite"
+ steps:
+ - name: Install Rust
+ shell: bash -euxo pipefail {0}
+ run: |
+ rustup set profile minimal
+ rustup update stable
+ rustup target add wasm32-wasi
+ cargo install cargo-nextest
+
+ - name: Install Node
+ uses: actions/setup-node@v3
+ with:
+ node-version: "18"
+
+ - name: Limit target directory size
+ shell: bash -euxo pipefail {0}
+ run: script/clear-target-dir-if-larger-than 70
+
+ - name: Run check
+ env:
+ RUSTFLAGS: -D warnings
+ shell: bash -euxo pipefail {0}
+ run: cargo check --tests --workspace
+
+ - name: Run tests
+ env:
+ RUSTFLAGS: -D warnings
+ shell: bash -euxo pipefail {0}
+ run: cargo nextest run --workspace --no-fail-fast
@@ -23,19 +23,14 @@ jobs:
- self-hosted
- test
steps:
- - name: Install Rust
- run: |
- rustup set profile minimal
- rustup update stable
-
- name: Checkout repo
uses: actions/checkout@v3
with:
clean: false
submodules: "recursive"
- - name: cargo fmt
- run: cargo fmt --all -- --check
+ - name: Run rustfmt
+ uses: ./.github/actions/check_formatting
tests:
name: Run tests
@@ -43,35 +38,15 @@ jobs:
- self-hosted
- test
needs: rustfmt
- env:
- RUSTFLAGS: -D warnings
steps:
- - name: Install Rust
- run: |
- rustup set profile minimal
- rustup update stable
- rustup target add wasm32-wasi
- cargo install cargo-nextest
-
- - name: Install Node
- uses: actions/setup-node@v3
- with:
- node-version: "18"
-
- name: Checkout repo
uses: actions/checkout@v3
with:
clean: false
submodules: "recursive"
- - name: Limit target directory size
- run: script/clear-target-dir-if-larger-than 70
-
- - name: Run check
- run: cargo check --workspace
-
- name: Run tests
- run: cargo nextest run --workspace --no-fail-fast
+ uses: ./.github/actions/run_tests
- name: Build collab
run: cargo build -p collab
@@ -130,6 +105,8 @@ jobs:
expected_tag_name="v${version}";;
preview)
expected_tag_name="v${version}-pre";;
+ nightly)
+ expected_tag_name="v${version}-nightly";;
*)
echo "can't publish a release on channel ${channel}"
exit 1;;
@@ -154,7 +131,9 @@ jobs:
- uses: softprops/action-gh-release@v1
name: Upload app bundle to release
- if: ${{ env.RELEASE_CHANNEL }}
+ # TODO kb seems that zed.dev relies on GitHub releases for release version tracking.
+ # Find alternatives for `nightly` or just go on with more releases?
+ if: ${{ env.RELEASE_CHANNEL == 'preview' || env.RELEASE_CHANNEL == 'stable' }}
with:
draft: true
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
@@ -0,0 +1,98 @@
+name: Release Nightly
+
+on:
+ schedule:
+ # Fire every night at 1:00am
+ - cron: "0 1 * * *"
+ push:
+ tags:
+ - "nightly"
+
+env:
+ CARGO_TERM_COLOR: always
+ CARGO_INCREMENTAL: 0
+ RUST_BACKTRACE: 1
+
+jobs:
+ rustfmt:
+ name: Check formatting
+ runs-on:
+ - self-hosted
+ - test
+ steps:
+ - name: Checkout repo
+ uses: actions/checkout@v3
+ with:
+ clean: false
+ submodules: "recursive"
+
+ - name: Run rustfmt
+ uses: ./.github/actions/check_formatting
+
+ tests:
+ name: Run tests
+ runs-on:
+ - self-hosted
+ - test
+ needs: rustfmt
+ steps:
+ - name: Checkout repo
+ uses: actions/checkout@v3
+ with:
+ clean: false
+ submodules: "recursive"
+
+ - name: Run tests
+ uses: ./.github/actions/run_tests
+
+ bundle:
+ name: Bundle app
+ runs-on:
+ - self-hosted
+ - bundle
+ needs: tests
+ env:
+ MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
+ MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
+ APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
+ APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
+ DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
+ DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
+ steps:
+ - name: Install Rust
+ run: |
+ rustup set profile minimal
+ rustup update stable
+ rustup target add aarch64-apple-darwin
+ rustup target add x86_64-apple-darwin
+ rustup target add wasm32-wasi
+
+ - name: Install Node
+ uses: actions/setup-node@v3
+ with:
+ node-version: "18"
+
+ - name: Checkout repo
+ uses: actions/checkout@v3
+ with:
+ clean: false
+ submodules: "recursive"
+
+ - name: Limit target directory size
+ run: script/clear-target-dir-if-larger-than 70
+
+ - name: Set release channel to nightly
+ run: |
+ set -eu
+ version=$(git rev-parse --short HEAD)
+ echo "Publishing version: ${version} on release channel nightly"
+ echo "nightly" > crates/zed/RELEASE_CHANNEL
+
+ - name: Generate license file
+ run: script/generate-licenses
+
+ - name: Create app bundle
+ run: script/bundle -2
+
+ - name: Upload Zed Nightly
+ run: script/upload-nightly
@@ -724,6 +724,30 @@ dependencies = [
"workspace",
]
+[[package]]
+name = "auto_update2"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "client2",
+ "db2",
+ "gpui2",
+ "isahc",
+ "lazy_static",
+ "log",
+ "menu2",
+ "project2",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "settings2",
+ "smol",
+ "tempdir",
+ "theme2",
+ "util",
+ "workspace2",
+]
+
[[package]]
name = "autocfg"
version = "1.1.0"
@@ -817,17 +841,6 @@ dependencies = [
"rustc-demangle",
]
-[[package]]
-name = "backtrace-on-stack-overflow"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7fd2d70527f3737a1ad17355e260706c1badebabd1fa06a7a053407380df841b"
-dependencies = [
- "backtrace",
- "libc",
- "nix 0.23.2",
-]
-
[[package]]
name = "base64"
version = "0.13.1"
@@ -1374,7 +1387,7 @@ dependencies = [
"smol",
"sum_tree",
"tempfile",
- "text",
+ "text2",
"thiserror",
"time",
"tiny_http",
@@ -1526,6 +1539,7 @@ dependencies = [
"anyhow",
"async-recursion 0.3.2",
"async-tungstenite",
+ "chrono",
"collections",
"db",
"feature_flags",
@@ -1562,6 +1576,7 @@ dependencies = [
"anyhow",
"async-recursion 0.3.2",
"async-tungstenite",
+ "chrono",
"collections",
"db2",
"feature_flags2",
@@ -1843,7 +1858,7 @@ dependencies = [
"editor2",
"feature_flags2",
"futures 0.3.28",
- "fuzzy",
+ "fuzzy2",
"gpui2",
"language2",
"lazy_static",
@@ -2614,6 +2629,34 @@ dependencies = [
"workspace",
]
+[[package]]
+name = "diagnostics2"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "client2",
+ "collections",
+ "editor2",
+ "futures 0.3.28",
+ "gpui2",
+ "language2",
+ "log",
+ "lsp2",
+ "postage",
+ "project2",
+ "schemars",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "settings2",
+ "smallvec",
+ "theme2",
+ "ui2",
+ "unindent",
+ "util",
+ "workspace2",
+]
+
[[package]]
name = "diff"
version = "0.1.13"
@@ -3759,7 +3802,7 @@ dependencies = [
"smol",
"sqlez",
"sum_tree",
- "taffy",
+ "taffy 0.3.11 (git+https://github.com/DioxusLabs/taffy?rev=4fb530bdd71609bb1d3f76c6a8bde1ba82805d5e)",
"thiserror",
"time",
"tiny-skia",
@@ -3824,7 +3867,7 @@ dependencies = [
"smol",
"sqlez",
"sum_tree",
- "taffy",
+ "taffy 0.3.11 (git+https://github.com/DioxusLabs/taffy?rev=1876f72bee5e376023eaa518aa7b8a34c769bd1b)",
"thiserror",
"time",
"tiny-skia",
@@ -3859,6 +3902,12 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eec1c01eb1de97451ee0d60de7d81cf1e72aabefb021616027f3d1c3ec1c723c"
+[[package]]
+name = "grid"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1df00eed8d1f0db937f6be10e46e8072b0671accb504cf0f959c5c52c679f5b9"
+
[[package]]
name = "h2"
version = "0.3.21"
@@ -4486,7 +4535,7 @@ dependencies = [
"anyhow",
"chrono",
"dirs 4.0.0",
- "editor",
+ "editor2",
"gpui2",
"log",
"schemars",
@@ -5510,19 +5559,6 @@ dependencies = [
"winapi 0.3.9",
]
-[[package]]
-name = "nix"
-version = "0.23.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c"
-dependencies = [
- "bitflags 1.3.2",
- "cc",
- "cfg-if 1.0.0",
- "libc",
- "memoffset 0.6.5",
-]
-
[[package]]
name = "nix"
version = "0.24.3"
@@ -6703,7 +6739,6 @@ dependencies = [
"anyhow",
"client2",
"collections",
- "context_menu",
"db2",
"editor2",
"futures 0.3.28",
@@ -7972,6 +8007,35 @@ dependencies = [
"workspace",
]
+[[package]]
+name = "search2"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "bitflags 1.3.2",
+ "client2",
+ "collections",
+ "editor2",
+ "futures 0.3.28",
+ "gpui2",
+ "language2",
+ "log",
+ "menu2",
+ "postage",
+ "project2",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "settings2",
+ "smallvec",
+ "smol",
+ "theme2",
+ "ui2",
+ "unindent",
+ "util",
+ "workspace2",
+]
+
[[package]]
name = "security-framework"
version = "2.9.2"
@@ -8795,45 +8859,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
-[[package]]
-name = "storybook2"
-version = "0.1.0"
-dependencies = [
- "anyhow",
- "backtrace-on-stack-overflow",
- "chrono",
- "clap 4.4.4",
- "editor2",
- "fuzzy2",
- "gpui2",
- "itertools 0.11.0",
- "language2",
- "log",
- "menu2",
- "picker2",
- "rust-embed",
- "serde",
- "settings2",
- "simplelog",
- "smallvec",
- "strum",
- "theme",
- "theme2",
- "ui2",
- "util",
-]
-
-[[package]]
-name = "storybook3"
-version = "0.1.0"
-dependencies = [
- "anyhow",
- "gpui2",
- "settings2",
- "theme2",
- "ui2",
-]
-
[[package]]
name = "stringprep"
version = "0.1.4"
@@ -9053,13 +9078,24 @@ dependencies = [
"winx",
]
+[[package]]
+name = "taffy"
+version = "0.3.11"
+source = "git+https://github.com/DioxusLabs/taffy?rev=1876f72bee5e376023eaa518aa7b8a34c769bd1b#1876f72bee5e376023eaa518aa7b8a34c769bd1b"
+dependencies = [
+ "arrayvec 0.7.4",
+ "grid 0.11.0",
+ "num-traits",
+ "slotmap",
+]
+
[[package]]
name = "taffy"
version = "0.3.11"
source = "git+https://github.com/DioxusLabs/taffy?rev=4fb530bdd71609bb1d3f76c6a8bde1ba82805d5e#4fb530bdd71609bb1d3f76c6a8bde1ba82805d5e"
dependencies = [
"arrayvec 0.7.4",
- "grid",
+ "grid 0.10.0",
"num-traits",
"slotmap",
]
@@ -11543,6 +11579,7 @@ dependencies = [
"async-recursion 0.3.2",
"async-tar",
"async-trait",
+ "auto_update2",
"backtrace",
"call2",
"chrono",
@@ -11554,6 +11591,7 @@ dependencies = [
"copilot2",
"ctor",
"db2",
+ "diagnostics2",
"editor2",
"env_logger 0.9.3",
"feature_flags2",
@@ -11561,7 +11599,6 @@ dependencies = [
"fs2",
"fsevent",
"futures 0.3.28",
- "fuzzy",
"go_to_line2",
"gpui2",
"ignore",
@@ -11571,7 +11608,6 @@ dependencies = [
"isahc",
"journal2",
"language2",
- "language_tools",
"lazy_static",
"libc",
"log",
@@ -11590,6 +11626,7 @@ dependencies = [
"rsa 0.4.0",
"rust-embed",
"schemars",
+ "search2",
"serde",
"serde_derive",
"serde_json",
@@ -6,6 +6,7 @@ members = [
"crates/audio",
"crates/audio2",
"crates/auto_update",
+ "crates/auto_update2",
"crates/breadcrumbs",
"crates/call",
"crates/call2",
@@ -32,6 +33,7 @@ members = [
"crates/refineable",
"crates/refineable/derive_refineable",
"crates/diagnostics",
+ "crates/diagnostics2",
"crates/drag_and_drop",
"crates/editor",
"crates/feature_flags",
@@ -88,14 +90,15 @@ members = [
"crates/rpc",
"crates/rpc2",
"crates/search",
+ "crates/search2",
"crates/settings",
"crates/settings2",
"crates/snippet",
"crates/sqlez",
"crates/sqlez_macros",
"crates/rich_text",
- "crates/storybook2",
- "crates/storybook3",
+ # "crates/storybook2",
+ # "crates/storybook3",
"crates/sum_tree",
"crates/terminal",
"crates/terminal2",
@@ -1,6 +1 @@
-<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M2.45563 12.3438H11.5444C11.9137 12.3438 12.1556 11.9571 11.994 11.625L10.2346 8.00952C9.77174 7.05841 8.89104 6.37821 7.85383 6.17077C7.29019 6.05804 6.70981 6.05804 6.14617 6.17077C5.10896 6.37821 4.22826 7.05841 3.76542 8.00952L2.00603 11.625C1.84442 11.9571 2.08628 12.3438 2.45563 12.3438Z" fill="#001A33" fill-opacity="0.157"/>
-<path d="M9.5 6.5L11.994 11.625C12.1556 11.9571 11.9137 12.3438 11.5444 12.3438H2.45563C2.08628 12.3438 1.84442 11.9571 2.00603 11.625L4.5 6.5" stroke="#11181C" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M7 7L7 2" stroke="#11181C" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
-<circle cx="7" cy="9.24219" r="0.75" fill="#11181C"/>
-</svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-alert-triangle"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>
@@ -268,6 +268,19 @@
// Whether to show warnings or not by default.
"include_warnings": true
},
+ // Add files or globs of files that will be excluded by Zed entirely:
+ // they will be skipped during FS scan(s), file tree and file search
+ // will lack the corresponding file entries.
+ "file_scan_exclusions": [
+ "**/.git",
+ "**/.svn",
+ "**/.hg",
+ "**/CVS",
+ "**/.DS_Store",
+ "**/Thumbs.db",
+ "**/.classpath",
+ "**/.settings"
+ ],
// Git gutter behavior configuration.
"git": {
// Control whether the git gutter is shown. May take 2 values:
@@ -15,7 +15,7 @@ use ai::{
use ai::prompts::repository_context::PromptCodeSnippet;
use anyhow::{anyhow, Result};
use chrono::{DateTime, Local};
-use client::{telemetry::AssistantKind, ClickhouseEvent, TelemetrySettings};
+use client::{telemetry::AssistantKind, TelemetrySettings};
use collections::{hash_map, HashMap, HashSet, VecDeque};
use editor::{
display_map::{
@@ -3803,12 +3803,12 @@ fn report_assistant_event(
.default_open_ai_model
.clone();
- let event = ClickhouseEvent::Assistant {
- conversation_id,
- kind: assistant_kind,
- model: model.full_name(),
- };
let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
- telemetry.report_clickhouse_event(event, telemetry_settings)
+ telemetry.report_assistant_event(
+ telemetry_settings,
+ conversation_id,
+ assistant_kind,
+ model.full_name(),
+ )
}
@@ -118,14 +118,18 @@ fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) {
let auto_updater = auto_updater.read(cx);
let server_url = &auto_updater.server_url;
let current_version = auto_updater.current_version;
- let latest_release_url = if cx.has_global::<ReleaseChannel>()
- && *cx.global::<ReleaseChannel>() == ReleaseChannel::Preview
- {
- format!("{server_url}/releases/preview/{current_version}")
- } else {
- format!("{server_url}/releases/stable/{current_version}")
- };
- cx.platform().open_url(&latest_release_url);
+ if cx.has_global::<ReleaseChannel>() {
+ match cx.global::<ReleaseChannel>() {
+ ReleaseChannel::Dev => {}
+ ReleaseChannel::Nightly => {}
+ ReleaseChannel::Preview => cx
+ .platform()
+ .open_url(&format!("{server_url}/releases/preview/{current_version}")),
+ ReleaseChannel::Stable => cx
+ .platform()
+ .open_url(&format!("{server_url}/releases/stable/{current_version}")),
+ }
+ }
}
}
@@ -224,22 +228,19 @@ impl AutoUpdater {
)
});
- let preview_param = cx.read(|cx| {
+ let mut url_string = format!(
+ "{server_url}/api/releases/latest?token={ZED_SECRET_CLIENT_TOKEN}&asset=Zed.dmg"
+ );
+ cx.read(|cx| {
if cx.has_global::<ReleaseChannel>() {
- if *cx.global::<ReleaseChannel>() == ReleaseChannel::Preview {
- return "&preview=1";
+ if let Some(param) = cx.global::<ReleaseChannel>().release_query_param() {
+ url_string += "&";
+ url_string += param;
}
}
- ""
});
- let mut response = client
- .get(
- &format!("{server_url}/api/releases/latest?token={ZED_SECRET_CLIENT_TOKEN}&asset=Zed.dmg{preview_param}"),
- Default::default(),
- true,
- )
- .await?;
+ let mut response = client.get(&url_string, Default::default(), true).await?;
let mut body = Vec::new();
response
@@ -0,0 +1,29 @@
+[package]
+name = "auto_update2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/auto_update.rs"
+doctest = false
+
+[dependencies]
+db = { package = "db2", path = "../db2" }
+client = { package = "client2", path = "../client2" }
+gpui = { package = "gpui2", path = "../gpui2" }
+menu = { package = "menu2", path = "../menu2" }
+project = { package = "project2", path = "../project2" }
+settings = { package = "settings2", path = "../settings2" }
+theme = { package = "theme2", path = "../theme2" }
+workspace = { package = "workspace2", path = "../workspace2" }
+util = { path = "../util" }
+anyhow.workspace = true
+isahc.workspace = true
+lazy_static.workspace = true
+log.workspace = true
+serde.workspace = true
+serde_derive.workspace = true
+serde_json.workspace = true
+smol.workspace = true
+tempdir.workspace = true
@@ -0,0 +1,406 @@
+mod update_notification;
+
+use anyhow::{anyhow, Context, Result};
+use client::{Client, TelemetrySettings, ZED_APP_PATH, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN};
+use db::kvp::KEY_VALUE_STORE;
+use db::RELEASE_CHANNEL;
+use gpui::{
+ actions, AppContext, AsyncAppContext, Context as _, Model, ModelContext, SemanticVersion, Task,
+ ViewContext, VisualContext,
+};
+use isahc::AsyncBody;
+use serde::Deserialize;
+use serde_derive::Serialize;
+use smol::io::AsyncReadExt;
+
+use settings::{Settings, SettingsStore};
+use smol::{fs::File, process::Command};
+use std::{ffi::OsString, sync::Arc, time::Duration};
+use update_notification::UpdateNotification;
+use util::channel::{AppCommitSha, ReleaseChannel};
+use util::http::HttpClient;
+use workspace::Workspace;
+
+const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &str = "auto-updater-should-show-updated-notification";
+const POLL_INTERVAL: Duration = Duration::from_secs(60 * 60);
+
+//todo!(remove CheckThatAutoUpdaterWorks)
+actions!(
+ Check,
+ DismissErrorMessage,
+ ViewReleaseNotes,
+ CheckThatAutoUpdaterWorks
+);
+
+#[derive(Serialize)]
+struct UpdateRequestBody {
+ installation_id: Option<Arc<str>>,
+ release_channel: Option<&'static str>,
+ telemetry: bool,
+}
+
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub enum AutoUpdateStatus {
+ Idle,
+ Checking,
+ Downloading,
+ Installing,
+ Updated,
+ Errored,
+}
+
+pub struct AutoUpdater {
+ status: AutoUpdateStatus,
+ current_version: SemanticVersion,
+ http_client: Arc<dyn HttpClient>,
+ pending_poll: Option<Task<Option<()>>>,
+ server_url: String,
+}
+
+#[derive(Deserialize)]
+struct JsonRelease {
+ version: String,
+ url: String,
+}
+
+struct AutoUpdateSetting(bool);
+
+impl Settings for AutoUpdateSetting {
+ const KEY: Option<&'static str> = Some("auto_update");
+
+ type FileContent = Option<bool>;
+
+ fn load(
+ default_value: &Option<bool>,
+ user_values: &[&Option<bool>],
+ _: &mut AppContext,
+ ) -> Result<Self> {
+ Ok(Self(
+ Self::json_merge(default_value, user_values)?.ok_or_else(Self::missing_default)?,
+ ))
+ }
+}
+
+pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut AppContext) {
+ AutoUpdateSetting::register(cx);
+
+ cx.observe_new_views(|wokrspace: &mut Workspace, _cx| {
+ wokrspace
+ .register_action(|_, action: &Check, cx| check(action, cx))
+ .register_action(|_, _action: &CheckThatAutoUpdaterWorks, cx| {
+ let prompt = cx.prompt(gpui::PromptLevel::Info, "It does!", &["Ok"]);
+ cx.spawn(|_, _cx| async move {
+ prompt.await.ok();
+ })
+ .detach();
+ });
+ })
+ .detach();
+
+ if let Some(version) = *ZED_APP_VERSION {
+ let auto_updater = cx.build_model(|cx| {
+ let updater = AutoUpdater::new(version, http_client, server_url);
+
+ let mut update_subscription = AutoUpdateSetting::get_global(cx)
+ .0
+ .then(|| updater.start_polling(cx));
+
+ cx.observe_global::<SettingsStore>(move |updater, cx| {
+ if AutoUpdateSetting::get_global(cx).0 {
+ if update_subscription.is_none() {
+ update_subscription = Some(updater.start_polling(cx))
+ }
+ } else {
+ update_subscription.take();
+ }
+ })
+ .detach();
+
+ updater
+ });
+ cx.set_global(Some(auto_updater));
+ //todo!(action)
+ // cx.add_global_action(view_release_notes);
+ // cx.add_action(UpdateNotification::dismiss);
+ }
+}
+
+pub fn check(_: &Check, cx: &mut AppContext) {
+ if let Some(updater) = AutoUpdater::get(cx) {
+ updater.update(cx, |updater, cx| updater.poll(cx));
+ }
+}
+
+fn _view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) {
+ if let Some(auto_updater) = AutoUpdater::get(cx) {
+ let auto_updater = auto_updater.read(cx);
+ let server_url = &auto_updater.server_url;
+ let current_version = auto_updater.current_version;
+ if cx.has_global::<ReleaseChannel>() {
+ match cx.global::<ReleaseChannel>() {
+ ReleaseChannel::Dev => {}
+ ReleaseChannel::Nightly => {}
+ ReleaseChannel::Preview => {
+ cx.open_url(&format!("{server_url}/releases/preview/{current_version}"))
+ }
+ ReleaseChannel::Stable => {
+ cx.open_url(&format!("{server_url}/releases/stable/{current_version}"))
+ }
+ }
+ }
+ }
+}
+
+pub fn notify_of_any_new_update(cx: &mut ViewContext<Workspace>) -> Option<()> {
+ let updater = AutoUpdater::get(cx)?;
+ let version = updater.read(cx).current_version;
+ let should_show_notification = updater.read(cx).should_show_update_notification(cx);
+
+ cx.spawn(|workspace, mut cx| async move {
+ let should_show_notification = should_show_notification.await?;
+ if should_show_notification {
+ workspace.update(&mut cx, |workspace, cx| {
+ workspace.show_notification(0, cx, |cx| {
+ cx.build_view(|_| UpdateNotification::new(version))
+ });
+ updater
+ .read(cx)
+ .set_should_show_update_notification(false, cx)
+ .detach_and_log_err(cx);
+ })?;
+ }
+ anyhow::Ok(())
+ })
+ .detach();
+
+ None
+}
+
+impl AutoUpdater {
+ pub fn get(cx: &mut AppContext) -> Option<Model<Self>> {
+ cx.default_global::<Option<Model<Self>>>().clone()
+ }
+
+ fn new(
+ current_version: SemanticVersion,
+ http_client: Arc<dyn HttpClient>,
+ server_url: String,
+ ) -> Self {
+ Self {
+ status: AutoUpdateStatus::Idle,
+ current_version,
+ http_client,
+ server_url,
+ pending_poll: None,
+ }
+ }
+
+ pub fn start_polling(&self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
+ cx.spawn(|this, mut cx| async move {
+ loop {
+ this.update(&mut cx, |this, cx| this.poll(cx))?;
+ cx.background_executor().timer(POLL_INTERVAL).await;
+ }
+ })
+ }
+
+ pub fn poll(&mut self, cx: &mut ModelContext<Self>) {
+ if self.pending_poll.is_some() || self.status == AutoUpdateStatus::Updated {
+ return;
+ }
+
+ self.status = AutoUpdateStatus::Checking;
+ cx.notify();
+
+ self.pending_poll = Some(cx.spawn(|this, mut cx| async move {
+ let result = Self::update(this.upgrade()?, cx.clone()).await;
+ this.update(&mut cx, |this, cx| {
+ this.pending_poll = None;
+ if let Err(error) = result {
+ log::error!("auto-update failed: error:{:?}", error);
+ this.status = AutoUpdateStatus::Errored;
+ cx.notify();
+ }
+ })
+ .ok()
+ }));
+ }
+
+ pub fn status(&self) -> AutoUpdateStatus {
+ self.status
+ }
+
+ pub fn dismiss_error(&mut self, cx: &mut ModelContext<Self>) {
+ self.status = AutoUpdateStatus::Idle;
+ cx.notify();
+ }
+
+ async fn update(this: Model<Self>, mut cx: AsyncAppContext) -> Result<()> {
+ let (client, server_url, current_version) = this.read_with(&cx, |this, _| {
+ (
+ this.http_client.clone(),
+ this.server_url.clone(),
+ this.current_version,
+ )
+ })?;
+
+ let mut url_string = format!(
+ "{server_url}/api/releases/latest?token={ZED_SECRET_CLIENT_TOKEN}&asset=Zed.dmg"
+ );
+ cx.update(|cx| {
+ if cx.has_global::<ReleaseChannel>() {
+ if let Some(param) = cx.global::<ReleaseChannel>().release_query_param() {
+ url_string += "&";
+ url_string += param;
+ }
+ }
+ })?;
+
+ let mut response = client.get(&url_string, Default::default(), true).await?;
+
+ let mut body = Vec::new();
+ response
+ .body_mut()
+ .read_to_end(&mut body)
+ .await
+ .context("error reading release")?;
+ let release: JsonRelease =
+ serde_json::from_slice(body.as_slice()).context("error deserializing release")?;
+
+ let should_download = match *RELEASE_CHANNEL {
+ ReleaseChannel::Nightly => cx
+ .try_read_global::<AppCommitSha, _>(|sha, _| release.version != sha.0)
+ .unwrap_or(true),
+ _ => release.version.parse::<SemanticVersion>()? <= current_version,
+ };
+
+ if !should_download {
+ this.update(&mut cx, |this, cx| {
+ this.status = AutoUpdateStatus::Idle;
+ cx.notify();
+ })?;
+ return Ok(());
+ }
+
+ this.update(&mut cx, |this, cx| {
+ this.status = AutoUpdateStatus::Downloading;
+ cx.notify();
+ })?;
+
+ let temp_dir = tempdir::TempDir::new("zed-auto-update")?;
+ let dmg_path = temp_dir.path().join("Zed.dmg");
+ let mount_path = temp_dir.path().join("Zed");
+ let running_app_path = ZED_APP_PATH
+ .clone()
+ .map_or_else(|| cx.update(|cx| cx.app_path())?, Ok)?;
+ let running_app_filename = running_app_path
+ .file_name()
+ .ok_or_else(|| anyhow!("invalid running app path"))?;
+ let mut mounted_app_path: OsString = mount_path.join(running_app_filename).into();
+ mounted_app_path.push("/");
+
+ let mut dmg_file = File::create(&dmg_path).await?;
+
+ let (installation_id, release_channel, telemetry) = cx.update(|cx| {
+ let installation_id = cx.global::<Arc<Client>>().telemetry().installation_id();
+ let release_channel = cx
+ .has_global::<ReleaseChannel>()
+ .then(|| cx.global::<ReleaseChannel>().display_name());
+ let telemetry = TelemetrySettings::get_global(cx).metrics;
+
+ (installation_id, release_channel, telemetry)
+ })?;
+
+ let request_body = AsyncBody::from(serde_json::to_string(&UpdateRequestBody {
+ installation_id,
+ release_channel,
+ telemetry,
+ })?);
+
+ let mut response = client.get(&release.url, request_body, true).await?;
+ smol::io::copy(response.body_mut(), &mut dmg_file).await?;
+ log::info!("downloaded update. path:{:?}", dmg_path);
+
+ this.update(&mut cx, |this, cx| {
+ this.status = AutoUpdateStatus::Installing;
+ cx.notify();
+ })?;
+
+ let output = Command::new("hdiutil")
+ .args(&["attach", "-nobrowse"])
+ .arg(&dmg_path)
+ .arg("-mountroot")
+ .arg(&temp_dir.path())
+ .output()
+ .await?;
+ if !output.status.success() {
+ Err(anyhow!(
+ "failed to mount: {:?}",
+ String::from_utf8_lossy(&output.stderr)
+ ))?;
+ }
+
+ let output = Command::new("rsync")
+ .args(&["-av", "--delete"])
+ .arg(&mounted_app_path)
+ .arg(&running_app_path)
+ .output()
+ .await?;
+ if !output.status.success() {
+ Err(anyhow!(
+ "failed to copy app: {:?}",
+ String::from_utf8_lossy(&output.stderr)
+ ))?;
+ }
+
+ let output = Command::new("hdiutil")
+ .args(&["detach"])
+ .arg(&mount_path)
+ .output()
+ .await?;
+ if !output.status.success() {
+ Err(anyhow!(
+ "failed to unmount: {:?}",
+ String::from_utf8_lossy(&output.stderr)
+ ))?;
+ }
+
+ this.update(&mut cx, |this, cx| {
+ this.set_should_show_update_notification(true, cx)
+ .detach_and_log_err(cx);
+ this.status = AutoUpdateStatus::Updated;
+ cx.notify();
+ })?;
+ Ok(())
+ }
+
+ fn set_should_show_update_notification(
+ &self,
+ should_show: bool,
+ cx: &AppContext,
+ ) -> Task<Result<()>> {
+ cx.background_executor().spawn(async move {
+ if should_show {
+ KEY_VALUE_STORE
+ .write_kvp(
+ SHOULD_SHOW_UPDATE_NOTIFICATION_KEY.to_string(),
+ "".to_string(),
+ )
+ .await?;
+ } else {
+ KEY_VALUE_STORE
+ .delete_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY.to_string())
+ .await?;
+ }
+ Ok(())
+ })
+ }
+
+ fn should_show_update_notification(&self, cx: &AppContext) -> Task<Result<bool>> {
+ cx.background_executor().spawn(async move {
+ Ok(KEY_VALUE_STORE
+ .read_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY)?
+ .is_some())
+ })
+ }
+}
@@ -0,0 +1,87 @@
+use gpui::{div, Div, EventEmitter, ParentElement, Render, SemanticVersion, ViewContext};
+use menu::Cancel;
+use workspace::notifications::NotificationEvent;
+
+pub struct UpdateNotification {
+ _version: SemanticVersion,
+}
+
+impl EventEmitter<NotificationEvent> for UpdateNotification {}
+
+impl Render for UpdateNotification {
+ type Element = Div;
+
+ fn render(&mut self, _cx: &mut gpui::ViewContext<Self>) -> Self::Element {
+ div().child("Updated zed!")
+ // let theme = theme::current(cx).clone();
+ // let theme = &theme.update_notification;
+
+ // let app_name = cx.global::<ReleaseChannel>().display_name();
+
+ // MouseEventHandler::new::<ViewReleaseNotes, _>(0, cx, |state, cx| {
+ // Flex::column()
+ // .with_child(
+ // Flex::row()
+ // .with_child(
+ // Text::new(
+ // format!("Updated to {app_name} {}", self.version),
+ // theme.message.text.clone(),
+ // )
+ // .contained()
+ // .with_style(theme.message.container)
+ // .aligned()
+ // .top()
+ // .left()
+ // .flex(1., true),
+ // )
+ // .with_child(
+ // MouseEventHandler::new::<Cancel, _>(0, cx, |state, _| {
+ // let style = theme.dismiss_button.style_for(state);
+ // Svg::new("icons/x.svg")
+ // .with_color(style.color)
+ // .constrained()
+ // .with_width(style.icon_width)
+ // .aligned()
+ // .contained()
+ // .with_style(style.container)
+ // .constrained()
+ // .with_width(style.button_width)
+ // .with_height(style.button_width)
+ // })
+ // .with_padding(Padding::uniform(5.))
+ // .on_click(MouseButton::Left, move |_, this, cx| {
+ // this.dismiss(&Default::default(), cx)
+ // })
+ // .aligned()
+ // .constrained()
+ // .with_height(cx.font_cache().line_height(theme.message.text.font_size))
+ // .aligned()
+ // .top()
+ // .flex_float(),
+ // ),
+ // )
+ // .with_child({
+ // let style = theme.action_message.style_for(state);
+ // Text::new("View the release notes", style.text.clone())
+ // .contained()
+ // .with_style(style.container)
+ // })
+ // .contained()
+ // })
+ // .with_cursor_style(CursorStyle::PointingHand)
+ // .on_click(MouseButton::Left, |_, _, cx| {
+ // crate::view_release_notes(&Default::default(), cx)
+ // })
+ // .into_any_named("update notification")
+ }
+}
+
+impl UpdateNotification {
+ pub fn new(version: SemanticVersion) -> Self {
+ Self { _version: version }
+ }
+
+ pub fn _dismiss(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
+ cx.emit(NotificationEvent::Dismiss);
+ }
+}
@@ -5,10 +5,7 @@ pub mod room;
use anyhow::{anyhow, Result};
use audio::Audio;
use call_settings::CallSettings;
-use client::{
- proto, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore,
- ZED_ALWAYS_ACTIVE,
-};
+use client::{proto, Client, TelemetrySettings, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE};
use collections::HashSet;
use futures::{channel::oneshot, future::Shared, Future, FutureExt};
use gpui::{
@@ -485,12 +482,8 @@ pub fn report_call_event_for_room(
) {
let telemetry = client.telemetry();
let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
- let event = ClickhouseEvent::Call {
- operation,
- room_id: Some(room_id),
- channel_id,
- };
- telemetry.report_clickhouse_event(event, telemetry_settings);
+
+ telemetry.report_call_event(telemetry_settings, operation, Some(room_id), channel_id)
}
pub fn report_call_event_for_channel(
@@ -504,12 +497,12 @@ pub fn report_call_event_for_channel(
let telemetry = client.telemetry();
let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
- let event = ClickhouseEvent::Call {
+ telemetry.report_call_event(
+ telemetry_settings,
operation,
- room_id: room.map(|r| r.read(cx).id()),
- channel_id: Some(channel_id),
- };
- telemetry.report_clickhouse_event(event, telemetry_settings);
+ room.map(|r| r.read(cx).id()),
+ Some(channel_id),
+ )
}
#[cfg(test)]
@@ -5,10 +5,7 @@ pub mod room;
use anyhow::{anyhow, Result};
use audio::Audio;
use call_settings::CallSettings;
-use client::{
- proto, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore,
- ZED_ALWAYS_ACTIVE,
-};
+use client::{proto, Client, TelemetrySettings, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE};
use collections::HashSet;
use futures::{channel::oneshot, future::Shared, Future, FutureExt};
use gpui::{
@@ -484,12 +481,8 @@ pub fn report_call_event_for_room(
) {
let telemetry = client.telemetry();
let telemetry_settings = *TelemetrySettings::get_global(cx);
- let event = ClickhouseEvent::Call {
- operation,
- room_id: Some(room_id),
- channel_id,
- };
- telemetry.report_clickhouse_event(event, telemetry_settings);
+
+ telemetry.report_call_event(telemetry_settings, operation, Some(room_id), channel_id)
}
pub fn report_call_event_for_channel(
@@ -504,12 +497,12 @@ pub fn report_call_event_for_channel(
let telemetry_settings = *TelemetrySettings::get_global(cx);
- let event = ClickhouseEvent::Call {
+ telemetry.report_call_event(
+ telemetry_settings,
operation,
- room_id: room.map(|r| r.read(cx).id()),
- channel_id: Some(channel_id),
- };
- telemetry.report_clickhouse_event(event, telemetry_settings);
+ room.map(|r| r.read(cx).id()),
+ Some(channel_id),
+ )
}
#[cfg(test)]
@@ -18,7 +18,7 @@ db = { package = "db2", path = "../db2" }
gpui = { package = "gpui2", path = "../gpui2" }
util = { path = "../util" }
rpc = { package = "rpc2", path = "../rpc2" }
-text = { path = "../text" }
+text = { package = "text2", path = "../text2" }
language = { package = "language2", path = "../language2" }
settings = { package = "settings2", path = "../settings2" }
feature_flags = { package = "feature_flags2", path = "../feature_flags2" }
@@ -12,6 +12,7 @@ doctest = false
test-support = ["collections/test-support", "gpui/test-support", "rpc/test-support"]
[dependencies]
+chrono = { version = "0.4", features = ["serde"] }
collections = { path = "../collections" }
db = { path = "../db" }
gpui = { path = "../gpui" }
@@ -987,9 +987,17 @@ impl Client {
self.establish_websocket_connection(credentials, cx)
}
- async fn get_rpc_url(http: Arc<dyn HttpClient>, is_preview: bool) -> Result<Url> {
- let preview_param = if is_preview { "?preview=1" } else { "" };
- let url = format!("{}/rpc{preview_param}", *ZED_SERVER_URL);
+ async fn get_rpc_url(
+ http: Arc<dyn HttpClient>,
+ release_channel: Option<ReleaseChannel>,
+ ) -> Result<Url> {
+ let mut url = format!("{}/rpc", *ZED_SERVER_URL);
+ if let Some(preview_param) =
+ release_channel.and_then(|channel| channel.release_query_param())
+ {
+ url += "?";
+ url += preview_param;
+ }
let response = http.get(&url, Default::default(), false).await?;
// Normally, ZED_SERVER_URL is set to the URL of zed.dev website.
@@ -1024,11 +1032,11 @@ impl Client {
credentials: &Credentials,
cx: &AsyncAppContext,
) -> Task<Result<Connection, EstablishConnectionError>> {
- let use_preview_server = cx.read(|cx| {
+ let release_channel = cx.read(|cx| {
if cx.has_global::<ReleaseChannel>() {
- *cx.global::<ReleaseChannel>() != ReleaseChannel::Stable
+ Some(*cx.global::<ReleaseChannel>())
} else {
- false
+ None
}
});
@@ -1041,7 +1049,7 @@ impl Client {
let http = self.http.clone();
cx.background().spawn(async move {
- let mut rpc_url = Self::get_rpc_url(http, use_preview_server).await?;
+ let mut rpc_url = Self::get_rpc_url(http, release_channel).await?;
let rpc_host = rpc_url
.host_str()
.zip(rpc_url.port_or_known_default())
@@ -1191,7 +1199,7 @@ impl Client {
// Use the collab server's admin API to retrieve the id
// of the impersonated user.
- let mut url = Self::get_rpc_url(http.clone(), false).await?;
+ let mut url = Self::get_rpc_url(http.clone(), None).await?;
url.set_path("/user");
url.set_query(Some(&format!("github_login={login}")));
let request = Request::get(url.as_str())
@@ -1,4 +1,5 @@
use crate::{TelemetrySettings, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
+use chrono::{DateTime, Utc};
use gpui::{executor::Background, serde_json, AppContext, Task};
use lazy_static::lazy_static;
use parking_lot::Mutex;
@@ -20,7 +21,7 @@ pub struct Telemetry {
#[derive(Default)]
struct TelemetryState {
metrics_id: Option<Arc<str>>, // Per logged-in user
- installation_id: Option<Arc<str>>, // Per app installation (different for dev, preview, and stable)
+ installation_id: Option<Arc<str>>, // Per app installation (different for dev, nightly, preview, and stable)
session_id: Option<Arc<str>>, // Per app launch
app_version: Option<Arc<str>>,
release_channel: Option<&'static str>,
@@ -31,6 +32,7 @@ struct TelemetryState {
flush_clickhouse_events_task: Option<Task<()>>,
log_file: Option<NamedTempFile>,
is_staff: Option<bool>,
+ first_event_datetime: Option<DateTime<Utc>>,
}
const CLICKHOUSE_EVENTS_URL_PATH: &'static str = "/api/events";
@@ -77,29 +79,35 @@ pub enum ClickhouseEvent {
vim_mode: bool,
copilot_enabled: bool,
copilot_enabled_for_language: bool,
+ milliseconds_since_first_event: i64,
},
Copilot {
suggestion_id: Option<String>,
suggestion_accepted: bool,
file_extension: Option<String>,
+ milliseconds_since_first_event: i64,
},
Call {
operation: &'static str,
room_id: Option<u64>,
channel_id: Option<u64>,
+ milliseconds_since_first_event: i64,
},
Assistant {
conversation_id: Option<String>,
kind: AssistantKind,
model: &'static str,
+ milliseconds_since_first_event: i64,
},
Cpu {
usage_as_percentage: f32,
core_count: u32,
+ milliseconds_since_first_event: i64,
},
Memory {
memory_in_bytes: u64,
virtual_memory_in_bytes: u64,
+ milliseconds_since_first_event: i64,
},
}
@@ -140,6 +148,7 @@ impl Telemetry {
flush_clickhouse_events_task: Default::default(),
log_file: None,
is_staff: None,
+ first_event_datetime: None,
}),
});
@@ -195,20 +204,18 @@ impl Telemetry {
return;
};
- let memory_event = ClickhouseEvent::Memory {
- memory_in_bytes: process.memory(),
- virtual_memory_in_bytes: process.virtual_memory(),
- };
-
- let cpu_event = ClickhouseEvent::Cpu {
- usage_as_percentage: process.cpu_usage(),
- core_count: system.cpus().len() as u32,
- };
-
let telemetry_settings = cx.update(|cx| *settings::get::<TelemetrySettings>(cx));
- this.report_clickhouse_event(memory_event, telemetry_settings);
- this.report_clickhouse_event(cpu_event, telemetry_settings);
+ this.report_memory_event(
+ telemetry_settings,
+ process.memory(),
+ process.virtual_memory(),
+ );
+ this.report_cpu_event(
+ telemetry_settings,
+ process.cpu_usage(),
+ system.cpus().len() as u32,
+ );
}
})
.detach();
@@ -231,7 +238,123 @@ impl Telemetry {
drop(state);
}
- pub fn report_clickhouse_event(
+ pub fn report_editor_event(
+ self: &Arc<Self>,
+ telemetry_settings: TelemetrySettings,
+ file_extension: Option<String>,
+ vim_mode: bool,
+ operation: &'static str,
+ copilot_enabled: bool,
+ copilot_enabled_for_language: bool,
+ ) {
+ let event = ClickhouseEvent::Editor {
+ file_extension,
+ vim_mode,
+ operation,
+ copilot_enabled,
+ copilot_enabled_for_language,
+ milliseconds_since_first_event: self.milliseconds_since_first_event(),
+ };
+
+ self.report_clickhouse_event(event, telemetry_settings)
+ }
+
+ pub fn report_copilot_event(
+ self: &Arc<Self>,
+ telemetry_settings: TelemetrySettings,
+ suggestion_id: Option<String>,
+ suggestion_accepted: bool,
+ file_extension: Option<String>,
+ ) {
+ let event = ClickhouseEvent::Copilot {
+ suggestion_id,
+ suggestion_accepted,
+ file_extension,
+ milliseconds_since_first_event: self.milliseconds_since_first_event(),
+ };
+
+ self.report_clickhouse_event(event, telemetry_settings)
+ }
+
+ pub fn report_assistant_event(
+ self: &Arc<Self>,
+ telemetry_settings: TelemetrySettings,
+ conversation_id: Option<String>,
+ kind: AssistantKind,
+ model: &'static str,
+ ) {
+ let event = ClickhouseEvent::Assistant {
+ conversation_id,
+ kind,
+ model,
+ milliseconds_since_first_event: self.milliseconds_since_first_event(),
+ };
+
+ self.report_clickhouse_event(event, telemetry_settings)
+ }
+
+ pub fn report_call_event(
+ self: &Arc<Self>,
+ telemetry_settings: TelemetrySettings,
+ operation: &'static str,
+ room_id: Option<u64>,
+ channel_id: Option<u64>,
+ ) {
+ let event = ClickhouseEvent::Call {
+ operation,
+ room_id,
+ channel_id,
+ milliseconds_since_first_event: self.milliseconds_since_first_event(),
+ };
+
+ self.report_clickhouse_event(event, telemetry_settings)
+ }
+
+ pub fn report_cpu_event(
+ self: &Arc<Self>,
+ telemetry_settings: TelemetrySettings,
+ usage_as_percentage: f32,
+ core_count: u32,
+ ) {
+ let event = ClickhouseEvent::Cpu {
+ usage_as_percentage,
+ core_count,
+ milliseconds_since_first_event: self.milliseconds_since_first_event(),
+ };
+
+ self.report_clickhouse_event(event, telemetry_settings)
+ }
+
+ pub fn report_memory_event(
+ self: &Arc<Self>,
+ telemetry_settings: TelemetrySettings,
+ memory_in_bytes: u64,
+ virtual_memory_in_bytes: u64,
+ ) {
+ let event = ClickhouseEvent::Memory {
+ memory_in_bytes,
+ virtual_memory_in_bytes,
+ milliseconds_since_first_event: self.milliseconds_since_first_event(),
+ };
+
+ self.report_clickhouse_event(event, telemetry_settings)
+ }
+
+ fn milliseconds_since_first_event(&self) -> i64 {
+ let mut state = self.state.lock();
+ match state.first_event_datetime {
+ Some(first_event_datetime) => {
+ let now: DateTime<Utc> = Utc::now();
+ now.timestamp_millis() - first_event_datetime.timestamp_millis()
+ }
+ None => {
+ state.first_event_datetime = Some(Utc::now());
+ 0
+ }
+ }
+ }
+
+ fn report_clickhouse_event(
self: &Arc<Self>,
event: ClickhouseEvent,
telemetry_settings: TelemetrySettings,
@@ -275,6 +398,7 @@ impl Telemetry {
fn flush_clickhouse_events(self: &Arc<Self>) {
let mut state = self.state.lock();
+ state.first_event_datetime = None;
let mut events = mem::take(&mut state.clickhouse_events_queue);
state.flush_clickhouse_events_task.take();
drop(state);
@@ -12,6 +12,7 @@ doctest = false
test-support = ["collections/test-support", "gpui/test-support", "rpc/test-support"]
[dependencies]
+chrono = { version = "0.4", features = ["serde"] }
collections = { path = "../collections" }
db = { package = "db2", path = "../db2" }
gpui = { package = "gpui2", path = "../gpui2" }
@@ -923,9 +923,17 @@ impl Client {
self.establish_websocket_connection(credentials, cx)
}
- async fn get_rpc_url(http: Arc<dyn HttpClient>, is_preview: bool) -> Result<Url> {
- let preview_param = if is_preview { "?preview=1" } else { "" };
- let url = format!("{}/rpc{preview_param}", *ZED_SERVER_URL);
+ async fn get_rpc_url(
+ http: Arc<dyn HttpClient>,
+ release_channel: Option<ReleaseChannel>,
+ ) -> Result<Url> {
+ let mut url = format!("{}/rpc", *ZED_SERVER_URL);
+ if let Some(preview_param) =
+ release_channel.and_then(|channel| channel.release_query_param())
+ {
+ url += "?";
+ url += preview_param;
+ }
let response = http.get(&url, Default::default(), false).await?;
// Normally, ZED_SERVER_URL is set to the URL of zed.dev website.
@@ -960,9 +968,7 @@ impl Client {
credentials: &Credentials,
cx: &AsyncAppContext,
) -> Task<Result<Connection, EstablishConnectionError>> {
- let use_preview_server = cx
- .try_read_global(|channel: &ReleaseChannel, _| *channel != ReleaseChannel::Stable)
- .unwrap_or(false);
+ let release_channel = cx.try_read_global(|channel: &ReleaseChannel, _| *channel);
let request = Request::builder()
.header(
@@ -973,7 +979,7 @@ impl Client {
let http = self.http.clone();
cx.background_executor().spawn(async move {
- let mut rpc_url = Self::get_rpc_url(http, use_preview_server).await?;
+ let mut rpc_url = Self::get_rpc_url(http, release_channel).await?;
let rpc_host = rpc_url
.host_str()
.zip(rpc_url.port_or_known_default())
@@ -1120,7 +1126,7 @@ impl Client {
// Use the collab server's admin API to retrieve the id
// of the impersonated user.
- let mut url = Self::get_rpc_url(http.clone(), false).await?;
+ let mut url = Self::get_rpc_url(http.clone(), None).await?;
url.set_path("/user");
url.set_query(Some(&format!("github_login={login}")));
let request = Request::get(url.as_str())
@@ -1,4 +1,5 @@
use crate::{TelemetrySettings, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
+use chrono::{DateTime, Utc};
use gpui::{serde_json, AppContext, AppMetadata, BackgroundExecutor, Task};
use lazy_static::lazy_static;
use parking_lot::Mutex;
@@ -20,7 +21,7 @@ pub struct Telemetry {
struct TelemetryState {
metrics_id: Option<Arc<str>>, // Per logged-in user
- installation_id: Option<Arc<str>>, // Per app installation (different for dev, preview, and stable)
+ installation_id: Option<Arc<str>>, // Per app installation (different for dev, nightly, preview, and stable)
session_id: Option<Arc<str>>, // Per app launch
release_channel: Option<&'static str>,
app_metadata: AppMetadata,
@@ -29,6 +30,7 @@ struct TelemetryState {
flush_clickhouse_events_task: Option<Task<()>>,
log_file: Option<NamedTempFile>,
is_staff: Option<bool>,
+ first_event_datetime: Option<DateTime<Utc>>,
}
const CLICKHOUSE_EVENTS_URL_PATH: &'static str = "/api/events";
@@ -75,29 +77,35 @@ pub enum ClickhouseEvent {
vim_mode: bool,
copilot_enabled: bool,
copilot_enabled_for_language: bool,
+ milliseconds_since_first_event: i64,
},
Copilot {
suggestion_id: Option<String>,
suggestion_accepted: bool,
file_extension: Option<String>,
+ milliseconds_since_first_event: i64,
},
Call {
operation: &'static str,
room_id: Option<u64>,
channel_id: Option<u64>,
+ milliseconds_since_first_event: i64,
},
Assistant {
conversation_id: Option<String>,
kind: AssistantKind,
model: &'static str,
+ milliseconds_since_first_event: i64,
},
Cpu {
usage_as_percentage: f32,
core_count: u32,
+ milliseconds_since_first_event: i64,
},
Memory {
memory_in_bytes: u64,
virtual_memory_in_bytes: u64,
+ milliseconds_since_first_event: i64,
},
}
@@ -135,6 +143,7 @@ impl Telemetry {
flush_clickhouse_events_task: Default::default(),
log_file: None,
is_staff: None,
+ first_event_datetime: None,
}),
});
@@ -190,16 +199,6 @@ impl Telemetry {
return;
};
- let memory_event = ClickhouseEvent::Memory {
- memory_in_bytes: process.memory(),
- virtual_memory_in_bytes: process.virtual_memory(),
- };
-
- let cpu_event = ClickhouseEvent::Cpu {
- usage_as_percentage: process.cpu_usage(),
- core_count: system.cpus().len() as u32,
- };
-
let telemetry_settings = if let Ok(telemetry_settings) =
cx.update(|cx| *TelemetrySettings::get_global(cx))
{
@@ -208,8 +207,16 @@ impl Telemetry {
break;
};
- this.report_clickhouse_event(memory_event, telemetry_settings);
- this.report_clickhouse_event(cpu_event, telemetry_settings);
+ this.report_memory_event(
+ telemetry_settings,
+ process.memory(),
+ process.virtual_memory(),
+ );
+ this.report_cpu_event(
+ telemetry_settings,
+ process.cpu_usage(),
+ system.cpus().len() as u32,
+ );
}
})
.detach();
@@ -232,7 +239,123 @@ impl Telemetry {
drop(state);
}
- pub fn report_clickhouse_event(
+ pub fn report_editor_event(
+ self: &Arc<Self>,
+ telemetry_settings: TelemetrySettings,
+ file_extension: Option<String>,
+ vim_mode: bool,
+ operation: &'static str,
+ copilot_enabled: bool,
+ copilot_enabled_for_language: bool,
+ ) {
+ let event = ClickhouseEvent::Editor {
+ file_extension,
+ vim_mode,
+ operation,
+ copilot_enabled,
+ copilot_enabled_for_language,
+ milliseconds_since_first_event: self.milliseconds_since_first_event(),
+ };
+
+ self.report_clickhouse_event(event, telemetry_settings)
+ }
+
+ pub fn report_copilot_event(
+ self: &Arc<Self>,
+ telemetry_settings: TelemetrySettings,
+ suggestion_id: Option<String>,
+ suggestion_accepted: bool,
+ file_extension: Option<String>,
+ ) {
+ let event = ClickhouseEvent::Copilot {
+ suggestion_id,
+ suggestion_accepted,
+ file_extension,
+ milliseconds_since_first_event: self.milliseconds_since_first_event(),
+ };
+
+ self.report_clickhouse_event(event, telemetry_settings)
+ }
+
+ pub fn report_assistant_event(
+ self: &Arc<Self>,
+ telemetry_settings: TelemetrySettings,
+ conversation_id: Option<String>,
+ kind: AssistantKind,
+ model: &'static str,
+ ) {
+ let event = ClickhouseEvent::Assistant {
+ conversation_id,
+ kind,
+ model,
+ milliseconds_since_first_event: self.milliseconds_since_first_event(),
+ };
+
+ self.report_clickhouse_event(event, telemetry_settings)
+ }
+
+ pub fn report_call_event(
+ self: &Arc<Self>,
+ telemetry_settings: TelemetrySettings,
+ operation: &'static str,
+ room_id: Option<u64>,
+ channel_id: Option<u64>,
+ ) {
+ let event = ClickhouseEvent::Call {
+ operation,
+ room_id,
+ channel_id,
+ milliseconds_since_first_event: self.milliseconds_since_first_event(),
+ };
+
+ self.report_clickhouse_event(event, telemetry_settings)
+ }
+
+ pub fn report_cpu_event(
+ self: &Arc<Self>,
+ telemetry_settings: TelemetrySettings,
+ usage_as_percentage: f32,
+ core_count: u32,
+ ) {
+ let event = ClickhouseEvent::Cpu {
+ usage_as_percentage,
+ core_count,
+ milliseconds_since_first_event: self.milliseconds_since_first_event(),
+ };
+
+ self.report_clickhouse_event(event, telemetry_settings)
+ }
+
+ pub fn report_memory_event(
+ self: &Arc<Self>,
+ telemetry_settings: TelemetrySettings,
+ memory_in_bytes: u64,
+ virtual_memory_in_bytes: u64,
+ ) {
+ let event = ClickhouseEvent::Memory {
+ memory_in_bytes,
+ virtual_memory_in_bytes,
+ milliseconds_since_first_event: self.milliseconds_since_first_event(),
+ };
+
+ self.report_clickhouse_event(event, telemetry_settings)
+ }
+
+ fn milliseconds_since_first_event(&self) -> i64 {
+ let mut state = self.state.lock();
+ match state.first_event_datetime {
+ Some(first_event_datetime) => {
+ let now: DateTime<Utc> = Utc::now();
+ now.timestamp_millis() - first_event_datetime.timestamp_millis()
+ }
+ None => {
+ state.first_event_datetime = Some(Utc::now());
+ 0
+ }
+ }
+ }
+
+ fn report_clickhouse_event(
self: &Arc<Self>,
event: ClickhouseEvent,
telemetry_settings: TelemetrySettings,
@@ -276,6 +399,7 @@ impl Telemetry {
fn flush_clickhouse_events(self: &Arc<Self>) {
let mut state = self.state.lock();
+ state.first_event_datetime = None;
let mut events = mem::take(&mut state.clickhouse_events_queue);
state.flush_clickhouse_events_task.take();
drop(state);
@@ -5052,7 +5052,7 @@ async fn test_project_search(
let mut results = HashMap::default();
let mut search_rx = project_b.update(cx_b, |project, cx| {
project.search(
- SearchQuery::text("world", false, false, Vec::new(), Vec::new()).unwrap(),
+ SearchQuery::text("world", false, false, false, Vec::new(), Vec::new()).unwrap(),
cx,
)
});
@@ -869,7 +869,8 @@ impl RandomizedTest for ProjectCollaborationTest {
let mut search = project.update(cx, |project, cx| {
project.search(
- SearchQuery::text(query, false, false, Vec::new(), Vec::new()).unwrap(),
+ SearchQuery::text(query, false, false, false, Vec::new(), Vec::new())
+ .unwrap(),
cx,
)
});
@@ -4599,7 +4599,7 @@ async fn test_project_search(
let mut results = HashMap::default();
let mut search_rx = project_b.update(cx_b, |project, cx| {
project.search(
- SearchQuery::text("world", false, false, Vec::new(), Vec::new()).unwrap(),
+ SearchQuery::text("world", false, false, false, Vec::new(), Vec::new()).unwrap(),
cx,
)
});
@@ -870,7 +870,8 @@ impl RandomizedTest for ProjectCollaborationTest {
let mut search = project.update(cx, |project, cx| {
project.search(
- SearchQuery::text(query, false, false, Vec::new(), Vec::new()).unwrap(),
+ SearchQuery::text(query, false, false, false, Vec::new(), Vec::new())
+ .unwrap(),
cx,
)
});
@@ -14,14 +14,8 @@ use std::{sync::Arc, time::Duration};
const MENTIONS_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(50);
lazy_static! {
- static ref MENTIONS_SEARCH: SearchQuery = SearchQuery::regex(
- "@[-_\\w]+",
- false,
- false,
- Default::default(),
- Default::default()
- )
- .unwrap();
+ static ref MENTIONS_SEARCH: SearchQuery =
+ SearchQuery::regex("@[-_\\w]+", false, false, false, Vec::new(), Vec::new()).unwrap();
}
pub struct MessageEditor {
@@ -33,7 +33,7 @@ collections = { path = "../collections" }
# drag_and_drop = { path = "../drag_and_drop" }
editor = { package="editor2", path = "../editor2" }
#feedback = { path = "../feedback" }
-fuzzy = { path = "../fuzzy" }
+fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
gpui = { package = "gpui2", path = "../gpui2" }
language = { package = "language2", path = "../language2" }
menu = { package = "menu2", path = "../menu2" }
@@ -14,14 +14,8 @@ use std::{sync::Arc, time::Duration};
const MENTIONS_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(50);
lazy_static! {
- static ref MENTIONS_SEARCH: SearchQuery = SearchQuery::regex(
- "@[-_\\w]+",
- false,
- false,
- Default::default(),
- Default::default()
- )
- .unwrap();
+ static ref MENTIONS_SEARCH: SearchQuery =
+ SearchQuery::regex("@[-_\\w]+", false, false, false, Vec::new(), Vec::new()).unwrap();
}
pub struct MessageEditor {
@@ -160,7 +160,7 @@ use std::sync::Arc;
use db::kvp::KEY_VALUE_STORE;
use gpui::{
actions, div, serde_json, AppContext, AsyncWindowContext, Div, EventEmitter, FocusHandle,
- Focusable, FocusableView, InteractiveComponent, ParentComponent, Render, View, ViewContext,
+ Focusable, FocusableView, InteractiveElement, ParentElement, Render, View, ViewContext,
VisualContext, WeakView,
};
use project::Fs;
@@ -3295,7 +3295,7 @@ impl CollabPanel {
// }
impl Render for CollabPanel {
- type Element = Focusable<Self, Div<Self>>;
+ type Element = Focusable<Div>;
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
div()
@@ -31,13 +31,13 @@ use std::sync::Arc;
use call::ActiveCall;
use client::{Client, UserStore};
use gpui::{
- div, px, rems, AppContext, Component, Div, InteractiveComponent, Model, ParentComponent,
- Render, Stateful, StatefulInteractiveComponent, Styled, Subscription, ViewContext,
- VisualContext, WeakView, WindowBounds,
+ div, px, rems, AppContext, Div, InteractiveElement, Model, ParentElement, Render, RenderOnce,
+ Stateful, StatefulInteractiveElement, Styled, Subscription, ViewContext, VisualContext,
+ WeakView, WindowBounds,
};
use project::Project;
use theme::ActiveTheme;
-use ui::{h_stack, Button, ButtonVariant, KeyBinding, Label, TextColor, Tooltip};
+use ui::{h_stack, Button, ButtonVariant, Color, KeyBinding, Label, Tooltip};
use workspace::Workspace;
// const MAX_PROJECT_NAME_LENGTH: usize = 40;
@@ -82,7 +82,7 @@ pub struct CollabTitlebarItem {
}
impl Render for CollabTitlebarItem {
- type Element = Stateful<Self, Div<Self>>;
+ type Element = Stateful<Div>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
h_stack()
@@ -100,7 +100,7 @@ impl Render for CollabTitlebarItem {
|s| s.pl(px(68.)),
)
.bg(cx.theme().colors().title_bar_background)
- .on_click(|_, event, cx| {
+ .on_click(|event, cx| {
if event.up.click_count == 2 {
cx.zoom_window();
}
@@ -115,16 +115,16 @@ impl Render for CollabTitlebarItem {
.child(
Button::new("player")
.variant(ButtonVariant::Ghost)
- .color(Some(TextColor::Player(0))),
+ .color(Some(Color::Player(0))),
)
- .tooltip(move |_, cx| Tooltip::text("Toggle following", cx)),
+ .tooltip(move |cx| Tooltip::text("Toggle following", cx)),
)
// TODO - Add project menu
.child(
div()
.id("titlebar_project_menu_button")
.child(Button::new("project_name").variant(ButtonVariant::Ghost))
- .tooltip(move |_, cx| Tooltip::text("Recent Projects", cx)),
+ .tooltip(move |cx| Tooltip::text("Recent Projects", cx)),
)
// TODO - Add git menu
.child(
@@ -133,9 +133,9 @@ impl Render for CollabTitlebarItem {
.child(
Button::new("branch_name")
.variant(ButtonVariant::Ghost)
- .color(Some(TextColor::Muted)),
+ .color(Some(Color::Muted)),
)
- .tooltip(move |_, cx| {
+ .tooltip(move |cx| {
cx.build_view(|_| {
Tooltip::new("Recent Branches")
.key_binding(KeyBinding::new(gpui::KeyBinding::new(
@@ -1,8 +1,8 @@
use collections::{CommandPaletteFilter, HashMap};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
- actions, div, prelude::*, Action, AppContext, Component, Dismiss, Div, FocusHandle, Keystroke,
- ManagedView, ParentComponent, Render, Styled, View, ViewContext, VisualContext, WeakView,
+ actions, div, prelude::*, Action, AppContext, Div, EventEmitter, FocusHandle, FocusableView,
+ Keystroke, Manager, ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView,
};
use picker::{Picker, PickerDelegate};
use std::{
@@ -68,14 +68,16 @@ impl CommandPalette {
}
}
-impl ManagedView for CommandPalette {
+impl EventEmitter<Manager> for CommandPalette {}
+
+impl FocusableView for CommandPalette {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.picker.focus_handle(cx)
}
}
impl Render for CommandPalette {
- type Element = Div<Self>;
+ type Element = Div;
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
v_stack().w_96().child(self.picker.clone())
@@ -114,6 +116,7 @@ impl Clone for Command {
}
}
}
+
/// Hit count for each command in the palette.
/// We only account for commands triggered directly via command palette and not by e.g. keystrokes because
/// if an user already knows a keystroke for a command, they are unlikely to use a command palette to look for it.
@@ -137,7 +140,7 @@ impl CommandPaletteDelegate {
}
impl PickerDelegate for CommandPaletteDelegate {
- type ListItem = Div<Picker<Self>>;
+ type ListItem = Div;
fn placeholder_text(&self) -> Arc<str> {
"Execute a command...".into()
@@ -265,7 +268,7 @@ impl PickerDelegate for CommandPaletteDelegate {
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
self.command_palette
- .update(cx, |_, cx| cx.emit(Dismiss))
+ .update(cx, |_, cx| cx.emit(Manager::Dismiss))
.log_err();
}
@@ -0,0 +1,43 @@
+[package]
+name = "diagnostics2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/diagnostics.rs"
+doctest = false
+
+[dependencies]
+collections = { path = "../collections" }
+editor = { package = "editor2", path = "../editor2" }
+gpui = { package = "gpui2", path = "../gpui2" }
+ui = { package = "ui2", path = "../ui2" }
+language = { package = "language2", path = "../language2" }
+lsp = { package = "lsp2", path = "../lsp2" }
+project = { package = "project2", path = "../project2" }
+settings = { package = "settings2", path = "../settings2" }
+theme = { package = "theme2", path = "../theme2" }
+util = { path = "../util" }
+workspace = { package = "workspace2", path = "../workspace2" }
+
+log.workspace = true
+anyhow.workspace = true
+futures.workspace = true
+schemars.workspace = true
+serde.workspace = true
+serde_derive.workspace = true
+smallvec.workspace = true
+postage.workspace = true
+
+[dev-dependencies]
+client = { package = "client2", path = "../client2", features = ["test-support"] }
+editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
+language = { package = "language2", path = "../language2", features = ["test-support"] }
+lsp = { package = "lsp2", path = "../lsp2", features = ["test-support"] }
+gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
+workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
+theme = { package = "theme2", path = "../theme2", features = ["test-support"] }
+
+serde_json.workspace = true
+unindent.workspace = true
@@ -0,0 +1,1573 @@
+pub mod items;
+mod project_diagnostics_settings;
+mod toolbar_controls;
+
+use anyhow::{Context as _, Result};
+use collections::{HashMap, HashSet};
+use editor::{
+ diagnostic_block_renderer,
+ display_map::{BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock},
+ highlight_diagnostic_message,
+ scroll::autoscroll::Autoscroll,
+ Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer, ToOffset,
+};
+use futures::future::try_join_all;
+use gpui::{
+ actions, div, AnyElement, AnyView, AppContext, Context, Div, EventEmitter, FocusEvent,
+ FocusHandle, Focusable, FocusableElement, FocusableView, InteractiveElement, Model,
+ ParentElement, Render, RenderOnce, SharedString, Styled, Subscription, Task, View, ViewContext,
+ VisualContext, WeakView, WindowContext,
+};
+use language::{
+ Anchor, Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Point, Selection,
+ SelectionGoal,
+};
+use lsp::LanguageServerId;
+use project::{DiagnosticSummary, Project, ProjectPath};
+use project_diagnostics_settings::ProjectDiagnosticsSettings;
+use settings::Settings;
+use std::{
+ any::{Any, TypeId},
+ cmp::Ordering,
+ mem,
+ ops::Range,
+ path::PathBuf,
+ sync::Arc,
+};
+use theme::ActiveTheme;
+pub use toolbar_controls::ToolbarControls;
+use ui::{h_stack, Color, HighlightedLabel, Icon, IconElement, Label};
+use util::TryFutureExt;
+use workspace::{
+ item::{BreadcrumbText, Item, ItemEvent, ItemHandle},
+ ItemNavHistory, Pane, ToolbarItemLocation, Workspace,
+};
+
+actions!(Deploy, ToggleWarnings);
+
+const CONTEXT_LINE_COUNT: u32 = 1;
+
+pub fn init(cx: &mut AppContext) {
+ ProjectDiagnosticsSettings::register(cx);
+ cx.observe_new_views(ProjectDiagnosticsEditor::register)
+ .detach();
+}
+
+struct ProjectDiagnosticsEditor {
+ project: Model<Project>,
+ workspace: WeakView<Workspace>,
+ focus_handle: FocusHandle,
+ editor: View<Editor>,
+ summary: DiagnosticSummary,
+ excerpts: Model<MultiBuffer>,
+ path_states: Vec<PathState>,
+ paths_to_update: HashMap<LanguageServerId, HashSet<ProjectPath>>,
+ current_diagnostics: HashMap<LanguageServerId, HashSet<ProjectPath>>,
+ include_warnings: bool,
+ _subscriptions: Vec<Subscription>,
+}
+
+struct PathState {
+ path: ProjectPath,
+ diagnostic_groups: Vec<DiagnosticGroupState>,
+}
+
+#[derive(Clone, Debug, PartialEq)]
+struct Jump {
+ path: ProjectPath,
+ position: Point,
+ anchor: Anchor,
+}
+
+struct DiagnosticGroupState {
+ language_server_id: LanguageServerId,
+ primary_diagnostic: DiagnosticEntry<language::Anchor>,
+ primary_excerpt_ix: usize,
+ excerpts: Vec<ExcerptId>,
+ blocks: HashSet<BlockId>,
+ block_count: usize,
+}
+
+impl EventEmitter<ItemEvent> for ProjectDiagnosticsEditor {}
+
+impl Render for ProjectDiagnosticsEditor {
+ type Element = Focusable<Div>;
+
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+ let child = if self.path_states.is_empty() {
+ div()
+ .bg(cx.theme().colors().editor_background)
+ .flex()
+ .items_center()
+ .justify_center()
+ .size_full()
+ .child(Label::new("No problems in workspace"))
+ } else {
+ div().size_full().child(self.editor.clone())
+ };
+
+ div()
+ .track_focus(&self.focus_handle)
+ .size_full()
+ .on_focus_in(cx.listener(Self::focus_in))
+ .on_action(cx.listener(Self::toggle_warnings))
+ .child(child)
+ }
+}
+
+impl ProjectDiagnosticsEditor {
+ fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
+ workspace.register_action(Self::deploy);
+ }
+
+ fn new(
+ project_handle: Model<Project>,
+ workspace: WeakView<Workspace>,
+ cx: &mut ViewContext<Self>,
+ ) -> Self {
+ let project_event_subscription =
+ cx.subscribe(&project_handle, |this, _, event, cx| match event {
+ project::Event::DiskBasedDiagnosticsFinished { language_server_id } => {
+ log::debug!("Disk based diagnostics finished for server {language_server_id}");
+ this.update_excerpts(Some(*language_server_id), cx);
+ }
+ project::Event::DiagnosticsUpdated {
+ language_server_id,
+ path,
+ } => {
+ log::debug!("Adding path {path:?} to update for server {language_server_id}");
+ this.paths_to_update
+ .entry(*language_server_id)
+ .or_default()
+ .insert(path.clone());
+ if this.editor.read(cx).selections.all::<usize>(cx).is_empty()
+ && !this.is_dirty(cx)
+ {
+ this.update_excerpts(Some(*language_server_id), cx);
+ }
+ }
+ _ => {}
+ });
+
+ let excerpts = cx.build_model(|cx| MultiBuffer::new(project_handle.read(cx).replica_id()));
+ let editor = cx.build_view(|cx| {
+ let mut editor =
+ Editor::for_multibuffer(excerpts.clone(), Some(project_handle.clone()), cx);
+ editor.set_vertical_scroll_margin(5, cx);
+ editor
+ });
+ let editor_event_subscription =
+ cx.subscribe(&editor, |this, _editor, event: &EditorEvent, cx| {
+ Self::emit_item_event_for_editor_event(event, cx);
+ if event == &EditorEvent::Focused && this.path_states.is_empty() {
+ cx.focus(&this.focus_handle);
+ }
+ });
+
+ let project = project_handle.read(cx);
+ let summary = project.diagnostic_summary(cx);
+ let mut this = Self {
+ project: project_handle,
+ summary,
+ workspace,
+ excerpts,
+ focus_handle: cx.focus_handle(),
+ editor,
+ path_states: Default::default(),
+ paths_to_update: HashMap::default(),
+ include_warnings: ProjectDiagnosticsSettings::get_global(cx).include_warnings,
+ current_diagnostics: HashMap::default(),
+ _subscriptions: vec![project_event_subscription, editor_event_subscription],
+ };
+ this.update_excerpts(None, cx);
+ this
+ }
+
+ fn emit_item_event_for_editor_event(event: &EditorEvent, cx: &mut ViewContext<Self>) {
+ match event {
+ EditorEvent::Closed => cx.emit(ItemEvent::CloseItem),
+
+ EditorEvent::Saved | EditorEvent::TitleChanged => {
+ cx.emit(ItemEvent::UpdateTab);
+ cx.emit(ItemEvent::UpdateBreadcrumbs);
+ }
+
+ EditorEvent::Reparsed => {
+ cx.emit(ItemEvent::UpdateBreadcrumbs);
+ }
+
+ EditorEvent::SelectionsChanged { local } if *local => {
+ cx.emit(ItemEvent::UpdateBreadcrumbs);
+ }
+
+ EditorEvent::DirtyChanged => {
+ cx.emit(ItemEvent::UpdateTab);
+ }
+
+ EditorEvent::BufferEdited => {
+ cx.emit(ItemEvent::Edit);
+ cx.emit(ItemEvent::UpdateBreadcrumbs);
+ }
+
+ EditorEvent::ExcerptsAdded { .. } | EditorEvent::ExcerptsRemoved { .. } => {
+ cx.emit(ItemEvent::Edit);
+ }
+
+ _ => {}
+ }
+ }
+
+ fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
+ if let Some(existing) = workspace.item_of_type::<ProjectDiagnosticsEditor>(cx) {
+ workspace.activate_item(&existing, cx);
+ } else {
+ let workspace_handle = cx.view().downgrade();
+ let diagnostics = cx.build_view(|cx| {
+ ProjectDiagnosticsEditor::new(workspace.project().clone(), workspace_handle, cx)
+ });
+ workspace.add_item(Box::new(diagnostics), cx);
+ }
+ }
+
+ fn toggle_warnings(&mut self, _: &ToggleWarnings, cx: &mut ViewContext<Self>) {
+ self.include_warnings = !self.include_warnings;
+ self.paths_to_update = self.current_diagnostics.clone();
+ self.update_excerpts(None, cx);
+ cx.notify();
+ }
+
+ fn focus_in(&mut self, _: &FocusEvent, cx: &mut ViewContext<Self>) {
+ if self.focus_handle.is_focused(cx) && !self.path_states.is_empty() {
+ self.editor.focus_handle(cx).focus(cx)
+ }
+ }
+
+ fn update_excerpts(
+ &mut self,
+ language_server_id: Option<LanguageServerId>,
+ cx: &mut ViewContext<Self>,
+ ) {
+ log::debug!("Updating excerpts for server {language_server_id:?}");
+ let mut paths_to_recheck = HashSet::default();
+ let mut new_summaries: HashMap<LanguageServerId, HashSet<ProjectPath>> = self
+ .project
+ .read(cx)
+ .diagnostic_summaries(cx)
+ .fold(HashMap::default(), |mut summaries, (path, server_id, _)| {
+ summaries.entry(server_id).or_default().insert(path);
+ summaries
+ });
+ let mut old_diagnostics = if let Some(language_server_id) = language_server_id {
+ new_summaries.retain(|server_id, _| server_id == &language_server_id);
+ self.paths_to_update.retain(|server_id, paths| {
+ if server_id == &language_server_id {
+ paths_to_recheck.extend(paths.drain());
+ false
+ } else {
+ true
+ }
+ });
+ let mut old_diagnostics = HashMap::default();
+ if let Some(new_paths) = new_summaries.get(&language_server_id) {
+ if let Some(old_paths) = self
+ .current_diagnostics
+ .insert(language_server_id, new_paths.clone())
+ {
+ old_diagnostics.insert(language_server_id, old_paths);
+ }
+ } else {
+ if let Some(old_paths) = self.current_diagnostics.remove(&language_server_id) {
+ old_diagnostics.insert(language_server_id, old_paths);
+ }
+ }
+ old_diagnostics
+ } else {
+ paths_to_recheck.extend(self.paths_to_update.drain().flat_map(|(_, paths)| paths));
+ mem::replace(&mut self.current_diagnostics, new_summaries.clone())
+ };
+ for (server_id, new_paths) in new_summaries {
+ match old_diagnostics.remove(&server_id) {
+ Some(mut old_paths) => {
+ paths_to_recheck.extend(
+ new_paths
+ .into_iter()
+ .filter(|new_path| !old_paths.remove(new_path)),
+ );
+ paths_to_recheck.extend(old_paths);
+ }
+ None => paths_to_recheck.extend(new_paths),
+ }
+ }
+ paths_to_recheck.extend(old_diagnostics.into_iter().flat_map(|(_, paths)| paths));
+
+ if paths_to_recheck.is_empty() {
+ log::debug!("No paths to recheck for language server {language_server_id:?}");
+ return;
+ }
+ log::debug!(
+ "Rechecking {} paths for language server {:?}",
+ paths_to_recheck.len(),
+ language_server_id
+ );
+ let project = self.project.clone();
+ cx.spawn(|this, mut cx| {
+ async move {
+ let _: Vec<()> = try_join_all(paths_to_recheck.into_iter().map(|path| {
+ let mut cx = cx.clone();
+ let project = project.clone();
+ let this = this.clone();
+ async move {
+ let buffer = project
+ .update(&mut cx, |project, cx| project.open_buffer(path.clone(), cx))?
+ .await
+ .with_context(|| format!("opening buffer for path {path:?}"))?;
+ this.update(&mut cx, |this, cx| {
+ this.populate_excerpts(path, language_server_id, buffer, cx);
+ })
+ .context("missing project")?;
+ anyhow::Ok(())
+ }
+ }))
+ .await
+ .context("rechecking diagnostics for paths")?;
+
+ this.update(&mut cx, |this, cx| {
+ this.summary = this.project.read(cx).diagnostic_summary(cx);
+ cx.emit(ItemEvent::UpdateTab);
+ cx.emit(ItemEvent::UpdateBreadcrumbs);
+ })?;
+ anyhow::Ok(())
+ }
+ .log_err()
+ })
+ .detach();
+ }
+
+ fn populate_excerpts(
+ &mut self,
+ path: ProjectPath,
+ language_server_id: Option<LanguageServerId>,
+ buffer: Model<Buffer>,
+ cx: &mut ViewContext<Self>,
+ ) {
+ let was_empty = self.path_states.is_empty();
+ let snapshot = buffer.read(cx).snapshot();
+ let path_ix = match self.path_states.binary_search_by_key(&&path, |e| &e.path) {
+ Ok(ix) => ix,
+ Err(ix) => {
+ self.path_states.insert(
+ ix,
+ PathState {
+ path: path.clone(),
+ diagnostic_groups: Default::default(),
+ },
+ );
+ ix
+ }
+ };
+
+ let mut prev_excerpt_id = if path_ix > 0 {
+ let prev_path_last_group = &self.path_states[path_ix - 1]
+ .diagnostic_groups
+ .last()
+ .unwrap();
+ prev_path_last_group.excerpts.last().unwrap().clone()
+ } else {
+ ExcerptId::min()
+ };
+
+ let path_state = &mut self.path_states[path_ix];
+ let mut groups_to_add = Vec::new();
+ let mut group_ixs_to_remove = Vec::new();
+ let mut blocks_to_add = Vec::new();
+ let mut blocks_to_remove = HashSet::default();
+ let mut first_excerpt_id = None;
+ let max_severity = if self.include_warnings {
+ DiagnosticSeverity::WARNING
+ } else {
+ DiagnosticSeverity::ERROR
+ };
+ let excerpts_snapshot = self.excerpts.update(cx, |excerpts, excerpts_cx| {
+ let mut old_groups = path_state.diagnostic_groups.iter().enumerate().peekable();
+ let mut new_groups = snapshot
+ .diagnostic_groups(language_server_id)
+ .into_iter()
+ .filter(|(_, group)| {
+ group.entries[group.primary_ix].diagnostic.severity <= max_severity
+ })
+ .peekable();
+ loop {
+ let mut to_insert = None;
+ let mut to_remove = None;
+ let mut to_keep = None;
+ match (old_groups.peek(), new_groups.peek()) {
+ (None, None) => break,
+ (None, Some(_)) => to_insert = new_groups.next(),
+ (Some((_, old_group)), None) => {
+ if language_server_id.map_or(true, |id| id == old_group.language_server_id)
+ {
+ to_remove = old_groups.next();
+ } else {
+ to_keep = old_groups.next();
+ }
+ }
+ (Some((_, old_group)), Some((_, new_group))) => {
+ let old_primary = &old_group.primary_diagnostic;
+ let new_primary = &new_group.entries[new_group.primary_ix];
+ match compare_diagnostics(old_primary, new_primary, &snapshot) {
+ Ordering::Less => {
+ if language_server_id
+ .map_or(true, |id| id == old_group.language_server_id)
+ {
+ to_remove = old_groups.next();
+ } else {
+ to_keep = old_groups.next();
+ }
+ }
+ Ordering::Equal => {
+ to_keep = old_groups.next();
+ new_groups.next();
+ }
+ Ordering::Greater => to_insert = new_groups.next(),
+ }
+ }
+ }
+
+ if let Some((language_server_id, group)) = to_insert {
+ let mut group_state = DiagnosticGroupState {
+ language_server_id,
+ primary_diagnostic: group.entries[group.primary_ix].clone(),
+ primary_excerpt_ix: 0,
+ excerpts: Default::default(),
+ blocks: Default::default(),
+ block_count: 0,
+ };
+ let mut pending_range: Option<(Range<Point>, usize)> = None;
+ let mut is_first_excerpt_for_group = true;
+ for (ix, entry) in group.entries.iter().map(Some).chain([None]).enumerate() {
+ let resolved_entry = entry.map(|e| e.resolve::<Point>(&snapshot));
+ if let Some((range, start_ix)) = &mut pending_range {
+ if let Some(entry) = resolved_entry.as_ref() {
+ if entry.range.start.row
+ <= range.end.row + 1 + CONTEXT_LINE_COUNT * 2
+ {
+ range.end = range.end.max(entry.range.end);
+ continue;
+ }
+ }
+
+ let excerpt_start =
+ Point::new(range.start.row.saturating_sub(CONTEXT_LINE_COUNT), 0);
+ let excerpt_end = snapshot.clip_point(
+ Point::new(range.end.row + CONTEXT_LINE_COUNT, u32::MAX),
+ Bias::Left,
+ );
+ let excerpt_id = excerpts
+ .insert_excerpts_after(
+ prev_excerpt_id,
+ buffer.clone(),
+ [ExcerptRange {
+ context: excerpt_start..excerpt_end,
+ primary: Some(range.clone()),
+ }],
+ excerpts_cx,
+ )
+ .pop()
+ .unwrap();
+
+ prev_excerpt_id = excerpt_id.clone();
+ first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone());
+ group_state.excerpts.push(excerpt_id.clone());
+ let header_position = (excerpt_id.clone(), language::Anchor::MIN);
+
+ if is_first_excerpt_for_group {
+ is_first_excerpt_for_group = false;
+ let mut primary =
+ group.entries[group.primary_ix].diagnostic.clone();
+ primary.message =
+ primary.message.split('\n').next().unwrap().to_string();
+ group_state.block_count += 1;
+ blocks_to_add.push(BlockProperties {
+ position: header_position,
+ height: 2,
+ style: BlockStyle::Sticky,
+ render: diagnostic_header_renderer(primary),
+ disposition: BlockDisposition::Above,
+ });
+ }
+
+ for entry in &group.entries[*start_ix..ix] {
+ let mut diagnostic = entry.diagnostic.clone();
+ if diagnostic.is_primary {
+ group_state.primary_excerpt_ix = group_state.excerpts.len() - 1;
+ diagnostic.message =
+ entry.diagnostic.message.split('\n').skip(1).collect();
+ }
+
+ if !diagnostic.message.is_empty() {
+ group_state.block_count += 1;
+ blocks_to_add.push(BlockProperties {
+ position: (excerpt_id.clone(), entry.range.start),
+ height: diagnostic.message.matches('\n').count() as u8 + 1,
+ style: BlockStyle::Fixed,
+ render: diagnostic_block_renderer(diagnostic, true),
+ disposition: BlockDisposition::Below,
+ });
+ }
+ }
+
+ pending_range.take();
+ }
+
+ if let Some(entry) = resolved_entry {
+ pending_range = Some((entry.range.clone(), ix));
+ }
+ }
+
+ groups_to_add.push(group_state);
+ } else if let Some((group_ix, group_state)) = to_remove {
+ excerpts.remove_excerpts(group_state.excerpts.iter().copied(), excerpts_cx);
+ group_ixs_to_remove.push(group_ix);
+ blocks_to_remove.extend(group_state.blocks.iter().copied());
+ } else if let Some((_, group)) = to_keep {
+ prev_excerpt_id = group.excerpts.last().unwrap().clone();
+ first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone());
+ }
+ }
+
+ excerpts.snapshot(excerpts_cx)
+ });
+
+ self.editor.update(cx, |editor, cx| {
+ editor.remove_blocks(blocks_to_remove, None, cx);
+ let block_ids = editor.insert_blocks(
+ blocks_to_add.into_iter().map(|block| {
+ let (excerpt_id, text_anchor) = block.position;
+ BlockProperties {
+ position: excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor),
+ height: block.height,
+ style: block.style,
+ render: block.render,
+ disposition: block.disposition,
+ }
+ }),
+ Some(Autoscroll::fit()),
+ cx,
+ );
+
+ let mut block_ids = block_ids.into_iter();
+ for group_state in &mut groups_to_add {
+ group_state.blocks = block_ids.by_ref().take(group_state.block_count).collect();
+ }
+ });
+
+ for ix in group_ixs_to_remove.into_iter().rev() {
+ path_state.diagnostic_groups.remove(ix);
+ }
+ path_state.diagnostic_groups.extend(groups_to_add);
+ path_state.diagnostic_groups.sort_unstable_by(|a, b| {
+ let range_a = &a.primary_diagnostic.range;
+ let range_b = &b.primary_diagnostic.range;
+ range_a
+ .start
+ .cmp(&range_b.start, &snapshot)
+ .then_with(|| range_a.end.cmp(&range_b.end, &snapshot))
+ });
+
+ if path_state.diagnostic_groups.is_empty() {
+ self.path_states.remove(path_ix);
+ }
+
+ self.editor.update(cx, |editor, cx| {
+ let groups;
+ let mut selections;
+ let new_excerpt_ids_by_selection_id;
+ if was_empty {
+ groups = self.path_states.first()?.diagnostic_groups.as_slice();
+ new_excerpt_ids_by_selection_id = [(0, ExcerptId::min())].into_iter().collect();
+ selections = vec![Selection {
+ id: 0,
+ start: 0,
+ end: 0,
+ reversed: false,
+ goal: SelectionGoal::None,
+ }];
+ } else {
+ groups = self.path_states.get(path_ix)?.diagnostic_groups.as_slice();
+ new_excerpt_ids_by_selection_id =
+ editor.change_selections(Some(Autoscroll::fit()), cx, |s| s.refresh());
+ selections = editor.selections.all::<usize>(cx);
+ }
+
+ // If any selection has lost its position, move it to start of the next primary diagnostic.
+ let snapshot = editor.snapshot(cx);
+ for selection in &mut selections {
+ if let Some(new_excerpt_id) = new_excerpt_ids_by_selection_id.get(&selection.id) {
+ let group_ix = match groups.binary_search_by(|probe| {
+ probe
+ .excerpts
+ .last()
+ .unwrap()
+ .cmp(new_excerpt_id, &snapshot.buffer_snapshot)
+ }) {
+ Ok(ix) | Err(ix) => ix,
+ };
+ if let Some(group) = groups.get(group_ix) {
+ let offset = excerpts_snapshot
+ .anchor_in_excerpt(
+ group.excerpts[group.primary_excerpt_ix].clone(),
+ group.primary_diagnostic.range.start,
+ )
+ .to_offset(&excerpts_snapshot);
+ selection.start = offset;
+ selection.end = offset;
+ }
+ }
+ }
+ editor.change_selections(None, cx, |s| {
+ s.select(selections);
+ });
+ Some(())
+ });
+
+ if self.path_states.is_empty() {
+ if self.editor.focus_handle(cx).is_focused(cx) {
+ cx.focus(&self.focus_handle);
+ }
+ } else if self.focus_handle.is_focused(cx) {
+ let focus_handle = self.editor.focus_handle(cx);
+ cx.focus(&focus_handle);
+ }
+ cx.notify();
+ }
+}
+
+impl FocusableView for ProjectDiagnosticsEditor {
+ fn focus_handle(&self, _: &AppContext) -> FocusHandle {
+ self.focus_handle.clone()
+ }
+}
+
+impl Item for ProjectDiagnosticsEditor {
+ fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
+ self.editor.update(cx, |editor, cx| editor.deactivated(cx));
+ }
+
+ fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
+ self.editor
+ .update(cx, |editor, cx| editor.navigate(data, cx))
+ }
+
+ fn tab_tooltip_text(&self, _: &AppContext) -> Option<SharedString> {
+ Some("Project Diagnostics".into())
+ }
+
+ fn tab_content(&self, _detail: Option<usize>, _: &WindowContext) -> AnyElement {
+ render_summary(&self.summary)
+ }
+
+ fn for_each_project_item(
+ &self,
+ cx: &AppContext,
+ f: &mut dyn FnMut(gpui::EntityId, &dyn project::Item),
+ ) {
+ self.editor.for_each_project_item(cx, f)
+ }
+
+ fn is_singleton(&self, _: &AppContext) -> bool {
+ false
+ }
+
+ fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext<Self>) {
+ self.editor.update(cx, |editor, _| {
+ editor.set_nav_history(Some(nav_history));
+ });
+ }
+
+ fn clone_on_split(
+ &self,
+ _workspace_id: workspace::WorkspaceId,
+ cx: &mut ViewContext<Self>,
+ ) -> Option<View<Self>>
+ where
+ Self: Sized,
+ {
+ Some(cx.build_view(|cx| {
+ ProjectDiagnosticsEditor::new(self.project.clone(), self.workspace.clone(), cx)
+ }))
+ }
+
+ fn is_dirty(&self, cx: &AppContext) -> bool {
+ self.excerpts.read(cx).is_dirty(cx)
+ }
+
+ fn has_conflict(&self, cx: &AppContext) -> bool {
+ self.excerpts.read(cx).has_conflict(cx)
+ }
+
+ fn can_save(&self, _: &AppContext) -> bool {
+ true
+ }
+
+ fn save(&mut self, project: Model<Project>, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
+ self.editor.save(project, cx)
+ }
+
+ fn save_as(
+ &mut self,
+ _: Model<Project>,
+ _: PathBuf,
+ _: &mut ViewContext<Self>,
+ ) -> Task<Result<()>> {
+ unreachable!()
+ }
+
+ fn reload(&mut self, project: Model<Project>, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
+ self.editor.reload(project, cx)
+ }
+
+ fn act_as_type<'a>(
+ &'a self,
+ type_id: TypeId,
+ self_handle: &'a View<Self>,
+ _: &'a AppContext,
+ ) -> Option<AnyView> {
+ if type_id == TypeId::of::<Self>() {
+ Some(self_handle.to_any())
+ } else if type_id == TypeId::of::<Editor>() {
+ Some(self.editor.to_any())
+ } else {
+ None
+ }
+ }
+
+ fn breadcrumb_location(&self) -> ToolbarItemLocation {
+ ToolbarItemLocation::PrimaryLeft
+ }
+
+ fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
+ self.editor.breadcrumbs(theme, cx)
+ }
+
+ fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
+ self.editor
+ .update(cx, |editor, cx| editor.added_to_workspace(workspace, cx));
+ }
+
+ fn serialized_item_kind() -> Option<&'static str> {
+ Some("diagnostics")
+ }
+
+ fn deserialize(
+ project: Model<Project>,
+ workspace: WeakView<Workspace>,
+ _workspace_id: workspace::WorkspaceId,
+ _item_id: workspace::ItemId,
+ cx: &mut ViewContext<Pane>,
+ ) -> Task<Result<View<Self>>> {
+ Task::ready(Ok(cx.build_view(|cx| Self::new(project, workspace, cx))))
+ }
+}
+
+fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
+ let (message, highlights) = highlight_diagnostic_message(Vec::new(), &diagnostic.message);
+ Arc::new(move |_| {
+ h_stack()
+ .id("diagnostic header")
+ .gap_3()
+ .bg(gpui::red())
+ .map(|stack| {
+ let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
+ IconElement::new(Icon::XCircle).color(Color::Error)
+ } else {
+ IconElement::new(Icon::ExclamationTriangle).color(Color::Warning)
+ };
+
+ stack.child(div().pl_8().child(icon))
+ })
+ .when_some(diagnostic.source.as_ref(), |stack, source| {
+ stack.child(Label::new(format!("{source}:")).color(Color::Accent))
+ })
+ .child(HighlightedLabel::new(message.clone(), highlights.clone()))
+ .when_some(diagnostic.code.as_ref(), |stack, code| {
+ stack.child(Label::new(code.clone()))
+ })
+ .render_into_any()
+ })
+}
+
+pub(crate) fn render_summary(summary: &DiagnosticSummary) -> AnyElement {
+ if summary.error_count == 0 && summary.warning_count == 0 {
+ let label = Label::new("No problems");
+ label.render_into_any()
+ } else {
+ h_stack()
+ .bg(gpui::red())
+ .child(IconElement::new(Icon::XCircle))
+ .child(Label::new(summary.error_count.to_string()))
+ .child(IconElement::new(Icon::ExclamationTriangle))
+ .child(Label::new(summary.warning_count.to_string()))
+ .render_into_any()
+ }
+}
+
+fn compare_diagnostics<L: language::ToOffset, R: language::ToOffset>(
+ lhs: &DiagnosticEntry<L>,
+ rhs: &DiagnosticEntry<R>,
+ snapshot: &language::BufferSnapshot,
+) -> Ordering {
+ lhs.range
+ .start
+ .to_offset(snapshot)
+ .cmp(&rhs.range.start.to_offset(snapshot))
+ .then_with(|| {
+ lhs.range
+ .end
+ .to_offset(snapshot)
+ .cmp(&rhs.range.end.to_offset(snapshot))
+ })
+ .then_with(|| lhs.diagnostic.message.cmp(&rhs.diagnostic.message))
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use editor::{
+ display_map::{BlockContext, TransformBlock},
+ DisplayPoint,
+ };
+ use gpui::{px, TestAppContext, VisualTestContext, WindowContext};
+ use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16, Unclipped};
+ use project::FakeFs;
+ use serde_json::json;
+ use settings::SettingsStore;
+ use unindent::Unindent as _;
+
+ #[gpui::test]
+ async fn test_diagnostics(cx: &mut TestAppContext) {
+ init_test(cx);
+
+ let fs = FakeFs::new(cx.executor());
+ fs.insert_tree(
+ "/test",
+ json!({
+ "consts.rs": "
+ const a: i32 = 'a';
+ const b: i32 = c;
+ "
+ .unindent(),
+
+ "main.rs": "
+ fn main() {
+ let x = vec![];
+ let y = vec![];
+ a(x);
+ b(y);
+ // comment 1
+ // comment 2
+ c(y);
+ d(x);
+ }
+ "
+ .unindent(),
+ }),
+ )
+ .await;
+
+ let language_server_id = LanguageServerId(0);
+ let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
+ let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+ let cx = &mut VisualTestContext::from_window(*window, cx);
+ let workspace = window.root(cx).unwrap();
+
+ // Create some diagnostics
+ project.update(cx, |project, cx| {
+ project
+ .update_diagnostic_entries(
+ language_server_id,
+ PathBuf::from("/test/main.rs"),
+ None,
+ vec![
+ DiagnosticEntry {
+ range: Unclipped(PointUtf16::new(1, 8))..Unclipped(PointUtf16::new(1, 9)),
+ diagnostic: Diagnostic {
+ message:
+ "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
+ .to_string(),
+ severity: DiagnosticSeverity::INFORMATION,
+ is_primary: false,
+ is_disk_based: true,
+ group_id: 1,
+ ..Default::default()
+ },
+ },
+ DiagnosticEntry {
+ range: Unclipped(PointUtf16::new(2, 8))..Unclipped(PointUtf16::new(2, 9)),
+ diagnostic: Diagnostic {
+ message:
+ "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
+ .to_string(),
+ severity: DiagnosticSeverity::INFORMATION,
+ is_primary: false,
+ is_disk_based: true,
+ group_id: 0,
+ ..Default::default()
+ },
+ },
+ DiagnosticEntry {
+ range: Unclipped(PointUtf16::new(3, 6))..Unclipped(PointUtf16::new(3, 7)),
+ diagnostic: Diagnostic {
+ message: "value moved here".to_string(),
+ severity: DiagnosticSeverity::INFORMATION,
+ is_primary: false,
+ is_disk_based: true,
+ group_id: 1,
+ ..Default::default()
+ },
+ },
+ DiagnosticEntry {
+ range: Unclipped(PointUtf16::new(4, 6))..Unclipped(PointUtf16::new(4, 7)),
+ diagnostic: Diagnostic {
+ message: "value moved here".to_string(),
+ severity: DiagnosticSeverity::INFORMATION,
+ is_primary: false,
+ is_disk_based: true,
+ group_id: 0,
+ ..Default::default()
+ },
+ },
+ DiagnosticEntry {
+ range: Unclipped(PointUtf16::new(7, 6))..Unclipped(PointUtf16::new(7, 7)),
+ diagnostic: Diagnostic {
+ message: "use of moved value\nvalue used here after move".to_string(),
+ severity: DiagnosticSeverity::ERROR,
+ is_primary: true,
+ is_disk_based: true,
+ group_id: 0,
+ ..Default::default()
+ },
+ },
+ DiagnosticEntry {
+ range: Unclipped(PointUtf16::new(8, 6))..Unclipped(PointUtf16::new(8, 7)),
+ diagnostic: Diagnostic {
+ message: "use of moved value\nvalue used here after move".to_string(),
+ severity: DiagnosticSeverity::ERROR,
+ is_primary: true,
+ is_disk_based: true,
+ group_id: 1,
+ ..Default::default()
+ },
+ },
+ ],
+ cx,
+ )
+ .unwrap();
+ });
+
+ // Open the project diagnostics view while there are already diagnostics.
+ let view = window.build_view(cx, |cx| {
+ ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
+ });
+
+ view.next_notification(cx).await;
+ view.update(cx, |view, cx| {
+ assert_eq!(
+ editor_blocks(&view.editor, cx),
+ [
+ (0, "path header block".into()),
+ (2, "diagnostic header".into()),
+ (15, "collapsed context".into()),
+ (16, "diagnostic header".into()),
+ (25, "collapsed context".into()),
+ ]
+ );
+ assert_eq!(
+ view.editor.update(cx, |editor, cx| editor.display_text(cx)),
+ concat!(
+ //
+ // main.rs
+ //
+ "\n", // filename
+ "\n", // padding
+ // diagnostic group 1
+ "\n", // primary message
+ "\n", // padding
+ " let x = vec![];\n",
+ " let y = vec![];\n",
+ "\n", // supporting diagnostic
+ " a(x);\n",
+ " b(y);\n",
+ "\n", // supporting diagnostic
+ " // comment 1\n",
+ " // comment 2\n",
+ " c(y);\n",
+ "\n", // supporting diagnostic
+ " d(x);\n",
+ "\n", // context ellipsis
+ // diagnostic group 2
+ "\n", // primary message
+ "\n", // padding
+ "fn main() {\n",
+ " let x = vec![];\n",
+ "\n", // supporting diagnostic
+ " let y = vec![];\n",
+ " a(x);\n",
+ "\n", // supporting diagnostic
+ " b(y);\n",
+ "\n", // context ellipsis
+ " c(y);\n",
+ " d(x);\n",
+ "\n", // supporting diagnostic
+ "}"
+ )
+ );
+
+ // Cursor is at the first diagnostic
+ view.editor.update(cx, |editor, cx| {
+ assert_eq!(
+ editor.selections.display_ranges(cx),
+ [DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)]
+ );
+ });
+ });
+
+ // Diagnostics are added for another earlier path.
+ project.update(cx, |project, cx| {
+ project.disk_based_diagnostics_started(language_server_id, cx);
+ project
+ .update_diagnostic_entries(
+ language_server_id,
+ PathBuf::from("/test/consts.rs"),
+ None,
+ vec![DiagnosticEntry {
+ range: Unclipped(PointUtf16::new(0, 15))..Unclipped(PointUtf16::new(0, 15)),
+ diagnostic: Diagnostic {
+ message: "mismatched types\nexpected `usize`, found `char`".to_string(),
+ severity: DiagnosticSeverity::ERROR,
+ is_primary: true,
+ is_disk_based: true,
+ group_id: 0,
+ ..Default::default()
+ },
+ }],
+ cx,
+ )
+ .unwrap();
+ project.disk_based_diagnostics_finished(language_server_id, cx);
+ });
+
+ view.next_notification(cx).await;
+ view.update(cx, |view, cx| {
+ assert_eq!(
+ editor_blocks(&view.editor, cx),
+ [
+ (0, "path header block".into()),
+ (2, "diagnostic header".into()),
+ (7, "path header block".into()),
+ (9, "diagnostic header".into()),
+ (22, "collapsed context".into()),
+ (23, "diagnostic header".into()),
+ (32, "collapsed context".into()),
+ ]
+ );
+ assert_eq!(
+ view.editor.update(cx, |editor, cx| editor.display_text(cx)),
+ concat!(
+ //
+ // consts.rs
+ //
+ "\n", // filename
+ "\n", // padding
+ // diagnostic group 1
+ "\n", // primary message
+ "\n", // padding
+ "const a: i32 = 'a';\n",
+ "\n", // supporting diagnostic
+ "const b: i32 = c;\n",
+ //
+ // main.rs
+ //
+ "\n", // filename
+ "\n", // padding
+ // diagnostic group 1
+ "\n", // primary message
+ "\n", // padding
+ " let x = vec![];\n",
+ " let y = vec![];\n",
+ "\n", // supporting diagnostic
+ " a(x);\n",
+ " b(y);\n",
+ "\n", // supporting diagnostic
+ " // comment 1\n",
+ " // comment 2\n",
+ " c(y);\n",
+ "\n", // supporting diagnostic
+ " d(x);\n",
+ "\n", // collapsed context
+ // diagnostic group 2
+ "\n", // primary message
+ "\n", // filename
+ "fn main() {\n",
+ " let x = vec![];\n",
+ "\n", // supporting diagnostic
+ " let y = vec![];\n",
+ " a(x);\n",
+ "\n", // supporting diagnostic
+ " b(y);\n",
+ "\n", // context ellipsis
+ " c(y);\n",
+ " d(x);\n",
+ "\n", // supporting diagnostic
+ "}"
+ )
+ );
+
+ // Cursor keeps its position.
+ view.editor.update(cx, |editor, cx| {
+ assert_eq!(
+ editor.selections.display_ranges(cx),
+ [DisplayPoint::new(19, 6)..DisplayPoint::new(19, 6)]
+ );
+ });
+ });
+
+ // Diagnostics are added to the first path
+ project.update(cx, |project, cx| {
+ project.disk_based_diagnostics_started(language_server_id, cx);
+ project
+ .update_diagnostic_entries(
+ language_server_id,
+ PathBuf::from("/test/consts.rs"),
+ None,
+ vec![
+ DiagnosticEntry {
+ range: Unclipped(PointUtf16::new(0, 15))
+ ..Unclipped(PointUtf16::new(0, 15)),
+ diagnostic: Diagnostic {
+ message: "mismatched types\nexpected `usize`, found `char`"
+ .to_string(),
+ severity: DiagnosticSeverity::ERROR,
+ is_primary: true,
+ is_disk_based: true,
+ group_id: 0,
+ ..Default::default()
+ },
+ },
+ DiagnosticEntry {
+ range: Unclipped(PointUtf16::new(1, 15))
+ ..Unclipped(PointUtf16::new(1, 15)),
+ diagnostic: Diagnostic {
+ message: "unresolved name `c`".to_string(),
+ severity: DiagnosticSeverity::ERROR,
+ is_primary: true,
+ is_disk_based: true,
+ group_id: 1,
+ ..Default::default()
+ },
+ },
+ ],
+ cx,
+ )
+ .unwrap();
+ project.disk_based_diagnostics_finished(language_server_id, cx);
+ });
+
+ view.next_notification(cx).await;
+ view.update(cx, |view, cx| {
+ assert_eq!(
+ editor_blocks(&view.editor, cx),
+ [
+ (0, "path header block".into()),
+ (2, "diagnostic header".into()),
+ (7, "collapsed context".into()),
+ (8, "diagnostic header".into()),
+ (13, "path header block".into()),
+ (15, "diagnostic header".into()),
+ (28, "collapsed context".into()),
+ (29, "diagnostic header".into()),
+ (38, "collapsed context".into()),
+ ]
+ );
+ assert_eq!(
+ view.editor.update(cx, |editor, cx| editor.display_text(cx)),
+ concat!(
+ //
+ // consts.rs
+ //
+ "\n", // filename
+ "\n", // padding
+ // diagnostic group 1
+ "\n", // primary message
+ "\n", // padding
+ "const a: i32 = 'a';\n",
+ "\n", // supporting diagnostic
+ "const b: i32 = c;\n",
+ "\n", // context ellipsis
+ // diagnostic group 2
+ "\n", // primary message
+ "\n", // padding
+ "const a: i32 = 'a';\n",
+ "const b: i32 = c;\n",
+ "\n", // supporting diagnostic
+ //
+ // main.rs
+ //
+ "\n", // filename
+ "\n", // padding
+ // diagnostic group 1
+ "\n", // primary message
+ "\n", // padding
+ " let x = vec![];\n",
+ " let y = vec![];\n",
+ "\n", // supporting diagnostic
+ " a(x);\n",
+ " b(y);\n",
+ "\n", // supporting diagnostic
+ " // comment 1\n",
+ " // comment 2\n",
+ " c(y);\n",
+ "\n", // supporting diagnostic
+ " d(x);\n",
+ "\n", // context ellipsis
+ // diagnostic group 2
+ "\n", // primary message
+ "\n", // filename
+ "fn main() {\n",
+ " let x = vec![];\n",
+ "\n", // supporting diagnostic
+ " let y = vec![];\n",
+ " a(x);\n",
+ "\n", // supporting diagnostic
+ " b(y);\n",
+ "\n", // context ellipsis
+ " c(y);\n",
+ " d(x);\n",
+ "\n", // supporting diagnostic
+ "}"
+ )
+ );
+ });
+ }
+
+ #[gpui::test]
+ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
+ init_test(cx);
+
+ let fs = FakeFs::new(cx.executor());
+ fs.insert_tree(
+ "/test",
+ json!({
+ "main.js": "
+ a();
+ b();
+ c();
+ d();
+ e();
+ ".unindent()
+ }),
+ )
+ .await;
+
+ let server_id_1 = LanguageServerId(100);
+ let server_id_2 = LanguageServerId(101);
+ let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
+ let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+ let cx = &mut VisualTestContext::from_window(*window, cx);
+ let workspace = window.root(cx).unwrap();
+
+ let view = window.build_view(cx, |cx| {
+ ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
+ });
+
+ // Two language servers start updating diagnostics
+ project.update(cx, |project, cx| {
+ project.disk_based_diagnostics_started(server_id_1, cx);
+ project.disk_based_diagnostics_started(server_id_2, cx);
+ project
+ .update_diagnostic_entries(
+ server_id_1,
+ PathBuf::from("/test/main.js"),
+ None,
+ vec![DiagnosticEntry {
+ range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 1)),
+ diagnostic: Diagnostic {
+ message: "error 1".to_string(),
+ severity: DiagnosticSeverity::WARNING,
+ is_primary: true,
+ is_disk_based: true,
+ group_id: 1,
+ ..Default::default()
+ },
+ }],
+ cx,
+ )
+ .unwrap();
+ });
+
+ // The first language server finishes
+ project.update(cx, |project, cx| {
+ project.disk_based_diagnostics_finished(server_id_1, cx);
+ });
+
+ // Only the first language server's diagnostics are shown.
+ cx.executor().run_until_parked();
+ view.update(cx, |view, cx| {
+ assert_eq!(
+ editor_blocks(&view.editor, cx),
+ [
+ (0, "path header block".into()),
+ (2, "diagnostic header".into()),
+ ]
+ );
+ assert_eq!(
+ view.editor.update(cx, |editor, cx| editor.display_text(cx)),
+ concat!(
+ "\n", // filename
+ "\n", // padding
+ // diagnostic group 1
+ "\n", // primary message
+ "\n", // padding
+ "a();\n", //
+ "b();",
+ )
+ );
+ });
+
+ // The second language server finishes
+ project.update(cx, |project, cx| {
+ project
+ .update_diagnostic_entries(
+ server_id_2,
+ PathBuf::from("/test/main.js"),
+ None,
+ vec![DiagnosticEntry {
+ range: Unclipped(PointUtf16::new(1, 0))..Unclipped(PointUtf16::new(1, 1)),
+ diagnostic: Diagnostic {
+ message: "warning 1".to_string(),
+ severity: DiagnosticSeverity::ERROR,
+ is_primary: true,
+ is_disk_based: true,
+ group_id: 2,
+ ..Default::default()
+ },
+ }],
+ cx,
+ )
+ .unwrap();
+ project.disk_based_diagnostics_finished(server_id_2, cx);
+ });
+
+ // Both language server's diagnostics are shown.
+ cx.executor().run_until_parked();
+ view.update(cx, |view, cx| {
+ assert_eq!(
+ editor_blocks(&view.editor, cx),
+ [
+ (0, "path header block".into()),
+ (2, "diagnostic header".into()),
+ (6, "collapsed context".into()),
+ (7, "diagnostic header".into()),
+ ]
+ );
+ assert_eq!(
+ view.editor.update(cx, |editor, cx| editor.display_text(cx)),
+ concat!(
+ "\n", // filename
+ "\n", // padding
+ // diagnostic group 1
+ "\n", // primary message
+ "\n", // padding
+ "a();\n", // location
+ "b();\n", //
+ "\n", // collapsed context
+ // diagnostic group 2
+ "\n", // primary message
+ "\n", // padding
+ "a();\n", // context
+ "b();\n", //
+ "c();", // context
+ )
+ );
+ });
+
+ // Both language servers start updating diagnostics, and the first server finishes.
+ project.update(cx, |project, cx| {
+ project.disk_based_diagnostics_started(server_id_1, cx);
+ project.disk_based_diagnostics_started(server_id_2, cx);
+ project
+ .update_diagnostic_entries(
+ server_id_1,
+ PathBuf::from("/test/main.js"),
+ None,
+ vec![DiagnosticEntry {
+ range: Unclipped(PointUtf16::new(2, 0))..Unclipped(PointUtf16::new(2, 1)),
+ diagnostic: Diagnostic {
+ message: "warning 2".to_string(),
+ severity: DiagnosticSeverity::WARNING,
+ is_primary: true,
+ is_disk_based: true,
+ group_id: 1,
+ ..Default::default()
+ },
+ }],
+ cx,
+ )
+ .unwrap();
+ project
+ .update_diagnostic_entries(
+ server_id_2,
+ PathBuf::from("/test/main.rs"),
+ None,
+ vec![],
+ cx,
+ )
+ .unwrap();
+ project.disk_based_diagnostics_finished(server_id_1, cx);
+ });
+
+ // Only the first language server's diagnostics are updated.
+ cx.executor().run_until_parked();
+ view.update(cx, |view, cx| {
+ assert_eq!(
+ editor_blocks(&view.editor, cx),
+ [
+ (0, "path header block".into()),
+ (2, "diagnostic header".into()),
+ (7, "collapsed context".into()),
+ (8, "diagnostic header".into()),
+ ]
+ );
+ assert_eq!(
+ view.editor.update(cx, |editor, cx| editor.display_text(cx)),
+ concat!(
+ "\n", // filename
+ "\n", // padding
+ // diagnostic group 1
+ "\n", // primary message
+ "\n", // padding
+ "a();\n", // location
+ "b();\n", //
+ "c();\n", // context
+ "\n", // collapsed context
+ // diagnostic group 2
+ "\n", // primary message
+ "\n", // padding
+ "b();\n", // context
+ "c();\n", //
+ "d();", // context
+ )
+ );
+ });
+
+ // The second language server finishes.
+ project.update(cx, |project, cx| {
+ project
+ .update_diagnostic_entries(
+ server_id_2,
+ PathBuf::from("/test/main.js"),
+ None,
+ vec![DiagnosticEntry {
+ range: Unclipped(PointUtf16::new(3, 0))..Unclipped(PointUtf16::new(3, 1)),
+ diagnostic: Diagnostic {
+ message: "warning 2".to_string(),
+ severity: DiagnosticSeverity::WARNING,
+ is_primary: true,
+ is_disk_based: true,
+ group_id: 1,
+ ..Default::default()
+ },
+ }],
+ cx,
+ )
+ .unwrap();
+ project.disk_based_diagnostics_finished(server_id_2, cx);
+ });
+
+ // Both language servers' diagnostics are updated.
+ cx.executor().run_until_parked();
+ view.update(cx, |view, cx| {
+ assert_eq!(
+ editor_blocks(&view.editor, cx),
+ [
+ (0, "path header block".into()),
+ (2, "diagnostic header".into()),
+ (7, "collapsed context".into()),
+ (8, "diagnostic header".into()),
+ ]
+ );
+ assert_eq!(
+ view.editor.update(cx, |editor, cx| editor.display_text(cx)),
+ concat!(
+ "\n", // filename
+ "\n", // padding
+ // diagnostic group 1
+ "\n", // primary message
+ "\n", // padding
+ "b();\n", // location
+ "c();\n", //
+ "d();\n", // context
+ "\n", // collapsed context
+ // diagnostic group 2
+ "\n", // primary message
+ "\n", // padding
+ "c();\n", // context
+ "d();\n", //
+ "e();", // context
+ )
+ );
+ });
+ }
+
+ fn init_test(cx: &mut TestAppContext) {
+ cx.update(|cx| {
+ let settings = SettingsStore::test(cx);
+ cx.set_global(settings);
+ theme::init(theme::LoadThemes::JustBase, cx);
+ language::init(cx);
+ client::init_settings(cx);
+ workspace::init_settings(cx);
+ Project::init_settings(cx);
+ crate::init(cx);
+ });
+ }
+
+ fn editor_blocks(editor: &View<Editor>, cx: &mut WindowContext) -> Vec<(u32, SharedString)> {
+ editor.update(cx, |editor, cx| {
+ let snapshot = editor.snapshot(cx);
+ snapshot
+ .blocks_in_range(0..snapshot.max_point().row())
+ .enumerate()
+ .filter_map(|(ix, (row, block))| {
+ let name = match block {
+ TransformBlock::Custom(block) => block
+ .render(&mut BlockContext {
+ view_context: cx,
+ anchor_x: px(0.),
+ gutter_padding: px(0.),
+ gutter_width: px(0.),
+ line_height: px(0.),
+ em_width: px(0.),
+ block_id: ix,
+ editor_style: &editor::EditorStyle::default(),
+ })
+ .element_id()?
+ .try_into()
+ .ok()?,
+
+ TransformBlock::ExcerptHeader {
+ starts_new_buffer, ..
+ } => {
+ if *starts_new_buffer {
+ "path header block".into()
+ } else {
+ "collapsed context".into()
+ }
+ }
+ };
+
+ Some((row, name))
+ })
+ .collect()
+ })
+ }
+}
@@ -0,0 +1,151 @@
+use collections::HashSet;
+use editor::{Editor, GoToDiagnostic};
+use gpui::{
+ rems, Div, EventEmitter, InteractiveElement, ParentElement, Render, Stateful,
+ StatefulInteractiveElement, Styled, Subscription, View, ViewContext, WeakView,
+};
+use language::Diagnostic;
+use lsp::LanguageServerId;
+use theme::ActiveTheme;
+use ui::{h_stack, Color, Icon, IconElement, Label, Tooltip};
+use workspace::{item::ItemHandle, StatusItemView, ToolbarItemEvent, Workspace};
+
+use crate::ProjectDiagnosticsEditor;
+
+pub struct DiagnosticIndicator {
+ summary: project::DiagnosticSummary,
+ active_editor: Option<WeakView<Editor>>,
+ workspace: WeakView<Workspace>,
+ current_diagnostic: Option<Diagnostic>,
+ in_progress_checks: HashSet<LanguageServerId>,
+ _observe_active_editor: Option<Subscription>,
+}
+
+impl Render for DiagnosticIndicator {
+ type Element = Stateful<Div>;
+
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+ let diagnostic_indicator = match (self.summary.error_count, self.summary.warning_count) {
+ (0, 0) => h_stack().child(IconElement::new(Icon::Check).color(Color::Success)),
+ (0, warning_count) => h_stack()
+ .gap_1()
+ .child(IconElement::new(Icon::ExclamationTriangle).color(Color::Warning))
+ .child(Label::new(warning_count.to_string())),
+ (error_count, 0) => h_stack()
+ .gap_1()
+ .child(IconElement::new(Icon::XCircle).color(Color::Error))
+ .child(Label::new(error_count.to_string())),
+ (error_count, warning_count) => h_stack()
+ .gap_1()
+ .child(IconElement::new(Icon::XCircle).color(Color::Error))
+ .child(Label::new(error_count.to_string()))
+ .child(IconElement::new(Icon::ExclamationTriangle).color(Color::Warning))
+ .child(Label::new(warning_count.to_string())),
+ };
+
+ h_stack()
+ .id(cx.entity_id())
+ .on_action(cx.listener(Self::go_to_next_diagnostic))
+ .rounded_md()
+ .flex_none()
+ .h(rems(1.375))
+ .px_1()
+ .cursor_pointer()
+ .bg(cx.theme().colors().ghost_element_background)
+ .hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
+ .active(|style| style.bg(cx.theme().colors().ghost_element_active))
+ .tooltip(|cx| Tooltip::text("Project Diagnostics", cx))
+ .on_click(cx.listener(|this, _, cx| {
+ if let Some(workspace) = this.workspace.upgrade() {
+ workspace.update(cx, |workspace, cx| {
+ ProjectDiagnosticsEditor::deploy(workspace, &Default::default(), cx)
+ })
+ }
+ }))
+ .child(diagnostic_indicator)
+ }
+}
+
+impl DiagnosticIndicator {
+ pub fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
+ let project = workspace.project();
+ cx.subscribe(project, |this, project, event, cx| match event {
+ project::Event::DiskBasedDiagnosticsStarted { language_server_id } => {
+ this.in_progress_checks.insert(*language_server_id);
+ cx.notify();
+ }
+
+ project::Event::DiskBasedDiagnosticsFinished { language_server_id }
+ | project::Event::LanguageServerRemoved(language_server_id) => {
+ this.summary = project.read(cx).diagnostic_summary(cx);
+ this.in_progress_checks.remove(language_server_id);
+ cx.notify();
+ }
+
+ project::Event::DiagnosticsUpdated { .. } => {
+ this.summary = project.read(cx).diagnostic_summary(cx);
+ cx.notify();
+ }
+
+ _ => {}
+ })
+ .detach();
+
+ Self {
+ summary: project.read(cx).diagnostic_summary(cx),
+ in_progress_checks: project
+ .read(cx)
+ .language_servers_running_disk_based_diagnostics()
+ .collect(),
+ active_editor: None,
+ workspace: workspace.weak_handle(),
+ current_diagnostic: None,
+ _observe_active_editor: None,
+ }
+ }
+
+ fn go_to_next_diagnostic(&mut self, _: &GoToDiagnostic, cx: &mut ViewContext<Self>) {
+ if let Some(editor) = self.active_editor.as_ref().and_then(|e| e.upgrade()) {
+ editor.update(cx, |editor, cx| {
+ editor.go_to_diagnostic_impl(editor::Direction::Next, cx);
+ })
+ }
+ }
+
+ fn update(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
+ let editor = editor.read(cx);
+ let buffer = editor.buffer().read(cx);
+ let cursor_position = editor.selections.newest::<usize>(cx).head();
+ let new_diagnostic = buffer
+ .snapshot(cx)
+ .diagnostics_in_range::<_, usize>(cursor_position..cursor_position, false)
+ .filter(|entry| !entry.range.is_empty())
+ .min_by_key(|entry| (entry.diagnostic.severity, entry.range.len()))
+ .map(|entry| entry.diagnostic);
+ if new_diagnostic != self.current_diagnostic {
+ self.current_diagnostic = new_diagnostic;
+ cx.notify();
+ }
+ }
+}
+
+impl EventEmitter<ToolbarItemEvent> for DiagnosticIndicator {}
+
+impl StatusItemView for DiagnosticIndicator {
+ fn set_active_pane_item(
+ &mut self,
+ active_pane_item: Option<&dyn ItemHandle>,
+ cx: &mut ViewContext<Self>,
+ ) {
+ if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
+ self.active_editor = Some(editor.downgrade());
+ self._observe_active_editor = Some(cx.observe(&editor, Self::update));
+ self.update(editor, cx);
+ } else {
+ self.active_editor = None;
+ self.current_diagnostic = None;
+ self._observe_active_editor = None;
+ }
+ cx.notify();
+ }
+}
@@ -0,0 +1,28 @@
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+
+#[derive(Deserialize, Debug)]
+pub struct ProjectDiagnosticsSettings {
+ pub include_warnings: bool,
+}
+
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
+pub struct ProjectDiagnosticsSettingsContent {
+ include_warnings: Option<bool>,
+}
+
+impl settings::Settings for ProjectDiagnosticsSettings {
+ const KEY: Option<&'static str> = Some("diagnostics");
+ type FileContent = ProjectDiagnosticsSettingsContent;
+
+ fn load(
+ default_value: &Self::FileContent,
+ user_values: &[&Self::FileContent],
+ _cx: &mut gpui::AppContext,
+ ) -> anyhow::Result<Self>
+ where
+ Self: Sized,
+ {
+ Self::load_via_json_merge(default_value, user_values)
+ }
+}
@@ -0,0 +1,66 @@
+use crate::ProjectDiagnosticsEditor;
+use gpui::{div, Div, EventEmitter, ParentElement, Render, ViewContext, WeakView};
+use ui::{Icon, IconButton, Tooltip};
+use workspace::{item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
+
+pub struct ToolbarControls {
+ editor: Option<WeakView<ProjectDiagnosticsEditor>>,
+}
+
+impl Render for ToolbarControls {
+ type Element = Div;
+
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+ let include_warnings = self
+ .editor
+ .as_ref()
+ .and_then(|editor| editor.upgrade())
+ .map(|editor| editor.read(cx).include_warnings)
+ .unwrap_or(false);
+
+ let tooltip = if include_warnings {
+ "Exclude Warnings"
+ } else {
+ "Include Warnings"
+ };
+
+ div().child(
+ IconButton::new("toggle-warnings", Icon::ExclamationTriangle)
+ .tooltip(move |cx| Tooltip::text(tooltip, cx))
+ .on_click(cx.listener(|this, _, cx| {
+ if let Some(editor) = this.editor.as_ref().and_then(|editor| editor.upgrade()) {
+ editor.update(cx, |editor, cx| {
+ editor.toggle_warnings(&Default::default(), cx);
+ });
+ }
+ })),
+ )
+ }
+}
+
+impl EventEmitter<ToolbarItemEvent> for ToolbarControls {}
+
+impl ToolbarItemView for ToolbarControls {
+ fn set_active_pane_item(
+ &mut self,
+ active_pane_item: Option<&dyn ItemHandle>,
+ _: &mut ViewContext<Self>,
+ ) -> ToolbarItemLocation {
+ if let Some(pane_item) = active_pane_item.as_ref() {
+ if let Some(editor) = pane_item.downcast::<ProjectDiagnosticsEditor>() {
+ self.editor = Some(editor.downgrade());
+ ToolbarItemLocation::PrimaryRight
+ } else {
+ ToolbarItemLocation::Hidden
+ }
+ } else {
+ ToolbarItemLocation::Hidden
+ }
+ }
+}
+
+impl ToolbarControls {
+ pub fn new() -> Self {
+ ToolbarControls { editor: None }
+ }
+}
@@ -24,7 +24,7 @@ use ::git::diff::DiffHunk;
use aho_corasick::AhoCorasick;
use anyhow::{anyhow, Context, Result};
use blink_manager::BlinkManager;
-use client::{ClickhouseEvent, Client, Collaborator, ParticipantIndex, TelemetrySettings};
+use client::{Client, Collaborator, ParticipantIndex, TelemetrySettings};
use clock::{Global, ReplicaId};
use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
use convert_case::{Case, Casing};
@@ -8946,12 +8946,12 @@ impl Editor {
let telemetry = project.read(cx).client().telemetry().clone();
let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
- let event = ClickhouseEvent::Copilot {
+ telemetry.report_copilot_event(
+ telemetry_settings,
suggestion_id,
suggestion_accepted,
file_extension,
- };
- telemetry.report_clickhouse_event(event, telemetry_settings);
+ )
}
#[cfg(any(test, feature = "test-support"))]
@@ -8998,14 +8998,14 @@ impl Editor {
.show_copilot_suggestions;
let telemetry = project.read(cx).client().telemetry().clone();
- let event = ClickhouseEvent::Editor {
+ telemetry.report_editor_event(
+ telemetry_settings,
file_extension,
vim_mode,
operation,
copilot_enabled,
copilot_enabled_for_language,
- };
- telemetry.report_clickhouse_event(event, telemetry_settings)
+ )
}
/// Copy the highlighted chunks to the clipboard as JSON. The format is an array of lines,
@@ -50,7 +50,7 @@ struct BlockRow(u32);
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
struct WrapRow(u32);
-pub type RenderBlock = Arc<dyn Fn(&mut BlockContext) -> AnyElement<Editor>>;
+pub type RenderBlock = Arc<dyn Fn(&mut BlockContext) -> AnyElement>;
pub struct Block {
id: BlockId,
@@ -69,7 +69,7 @@ where
pub position: P,
pub height: u8,
pub style: BlockStyle,
- pub render: Arc<dyn Fn(&mut BlockContext) -> AnyElement<Editor>>,
+ pub render: Arc<dyn Fn(&mut BlockContext) -> AnyElement>,
pub disposition: BlockDisposition,
}
@@ -947,7 +947,7 @@ impl DerefMut for BlockContext<'_, '_> {
}
impl Block {
- pub fn render(&self, cx: &mut BlockContext) -> AnyElement<Editor> {
+ pub fn render(&self, cx: &mut BlockContext) -> AnyElement {
self.render.lock()(cx)
}
@@ -24,7 +24,7 @@ use ::git::diff::DiffHunk;
use aho_corasick::AhoCorasick;
use anyhow::{anyhow, Context as _, Result};
use blink_manager::BlinkManager;
-use client::{ClickhouseEvent, Client, Collaborator, ParticipantIndex, TelemetrySettings};
+use client::{Client, Collaborator, ParticipantIndex, TelemetrySettings};
use clock::ReplicaId;
use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
use convert_case::{Case, Casing};
@@ -42,9 +42,9 @@ use gpui::{
actions, div, point, prelude::*, px, relative, rems, size, uniform_list, Action, AnyElement,
AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Component, Context,
EventEmitter, FocusHandle, FocusableView, FontFeatures, FontStyle, FontWeight, HighlightStyle,
- Hsla, InputHandler, KeyContext, Model, MouseButton, ParentComponent, Pixels, Render, Styled,
- Subscription, Task, TextStyle, UniformListScrollHandle, View, ViewContext, VisualContext,
- WeakView, WindowContext,
+ Hsla, InputHandler, KeyContext, Model, MouseButton, ParentElement, Pixels, Render,
+ SharedString, Styled, Subscription, Task, TextStyle, UniformListScrollHandle, View,
+ ViewContext, VisualContext, WeakView, WindowContext,
};
use highlight_matching_bracket::refresh_matching_bracket_highlights;
use hover_popover::{hide_hover, HoverState};
@@ -585,7 +585,7 @@ pub enum SoftWrap {
Column(u32),
}
-#[derive(Clone)]
+#[derive(Clone, Default)]
pub struct EditorStyle {
pub background: Hsla,
pub local_player: PlayerColor,
@@ -907,7 +907,7 @@ impl ContextMenu {
style: &EditorStyle,
workspace: Option<WeakView<Workspace>>,
cx: &mut ViewContext<Editor>,
- ) -> (DisplayPoint, AnyElement<Editor>) {
+ ) -> (DisplayPoint, AnyElement) {
match self {
ContextMenu::Completions(menu) => (cursor_position, menu.render(style, workspace, cx)),
ContextMenu::CodeActions(menu) => menu.render(cursor_position, style, cx),
@@ -1223,9 +1223,7 @@ impl CompletionsMenu {
style: &EditorStyle,
workspace: Option<WeakView<Workspace>>,
cx: &mut ViewContext<Editor>,
- ) -> AnyElement<Editor> {
- // enum CompletionTag {}
-
+ ) -> AnyElement {
let settings = EditorSettings::get_global(cx);
let show_completion_documentation = settings.show_completion_documentation;
@@ -1253,121 +1251,126 @@ impl CompletionsMenu {
let matches = self.matches.clone();
let selected_item = self.selected_item;
- let list = uniform_list("completions", matches.len(), move |editor, range, cx| {
- let start_ix = range.start;
- let completions_guard = completions.read();
+ let list = uniform_list(
+ cx.view().clone(),
+ "completions",
+ matches.len(),
+ move |editor, range, cx| {
+ let start_ix = range.start;
+ let completions_guard = completions.read();
- matches[range]
- .iter()
- .enumerate()
- .map(|(ix, mat)| {
- let item_ix = start_ix + ix;
- let candidate_id = mat.candidate_id;
- let completion = &completions_guard[candidate_id];
+ matches[range]
+ .iter()
+ .enumerate()
+ .map(|(ix, mat)| {
+ let item_ix = start_ix + ix;
+ let candidate_id = mat.candidate_id;
+ let completion = &completions_guard[candidate_id];
- let documentation = if show_completion_documentation {
- &completion.documentation
- } else {
- &None
- };
+ let documentation = if show_completion_documentation {
+ &completion.documentation
+ } else {
+ &None
+ };
- // todo!("highlights")
- // let highlights = combine_syntax_and_fuzzy_match_highlights(
- // &completion.label.text,
- // style.text.color.into(),
- // styled_runs_for_code_label(&completion.label, &style.syntax),
- // &mat.positions,
- // )
-
- // todo!("documentation")
- // MouseEventHandler::new::<CompletionTag, _>(mat.candidate_id, cx, |state, _| {
- // let completion_label = HighlightedLabel::new(
- // completion.label.text.clone(),
- // combine_syntax_and_fuzzy_match_highlights(
- // &completion.label.text,
- // style.text.color.into(),
- // styled_runs_for_code_label(&completion.label, &style.syntax),
- // &mat.positions,
- // ),
- // );
- // Text::new(completion.label.text.clone(), style.text.clone())
- // .with_soft_wrap(false)
- // .with_highlights();
-
- // if let Some(Documentation::SingleLine(text)) = documentation {
- // h_stack()
- // .child(completion_label)
- // .with_children((|| {
- // let text_style = TextStyle {
- // color: style.autocomplete.inline_docs_color,
- // font_size: style.text.font_size
- // * style.autocomplete.inline_docs_size_percent,
- // ..style.text.clone()
- // };
-
- // let label = Text::new(text.clone(), text_style)
- // .aligned()
- // .constrained()
- // .dynamically(move |constraint, _, _| gpui::SizeConstraint {
- // min: constraint.min,
- // max: vec2f(constraint.max.x(), constraint.min.y()),
- // });
-
- // if Some(item_ix) == widest_completion_ix {
- // Some(
- // label
- // .contained()
- // .with_style(style.autocomplete.inline_docs_container)
- // .into_any(),
- // )
- // } else {
- // Some(label.flex_float().into_any())
- // }
- // })())
- // .into_any()
- // } else {
- // completion_label.into_any()
- // }
- // .contained()
- // .with_style(item_style)
- // .constrained()
- // .dynamically(move |constraint, _, _| {
- // if Some(item_ix) == widest_completion_ix {
- // constraint
- // } else {
- // gpui::SizeConstraint {
- // min: constraint.min,
- // max: constraint.min,
- // }
- // }
- // })
- // })
- // .with_cursor_style(CursorStyle::PointingHand)
- // .on_down(MouseButton::Left, move |_, this, cx| {
- // this.confirm_completion(
- // &ConfirmCompletion {
- // item_ix: Some(item_ix),
- // },
- // cx,
- // )
- // .map(|task| task.detach());
- // })
- // .constrained()
- //
- div()
- .id(mat.candidate_id)
- .bg(gpui::green())
- .hover(|style| style.bg(gpui::blue()))
- .when(item_ix == selected_item, |div| div.bg(gpui::blue()))
- .child(completion.label.text.clone())
- .min_w(px(300.))
- .max_w(px(700.))
- })
- .collect()
- })
+ // todo!("highlights")
+ // let highlights = combine_syntax_and_fuzzy_match_highlights(
+ // &completion.label.text,
+ // style.text.color.into(),
+ // styled_runs_for_code_label(&completion.label, &style.syntax),
+ // &mat.positions,
+ // )
+
+ // todo!("documentation")
+ // MouseEventHandler::new::<CompletionTag, _>(mat.candidate_id, cx, |state, _| {
+ // let completion_label = HighlightedLabel::new(
+ // completion.label.text.clone(),
+ // combine_syntax_and_fuzzy_match_highlights(
+ // &completion.label.text,
+ // style.text.color.into(),
+ // styled_runs_for_code_label(&completion.label, &style.syntax),
+ // &mat.positions,
+ // ),
+ // );
+ // Text::new(completion.label.text.clone(), style.text.clone())
+ // .with_soft_wrap(false)
+ // .with_highlights();
+
+ // if let Some(Documentation::SingleLine(text)) = documentation {
+ // h_stack()
+ // .child(completion_label)
+ // .with_children((|| {
+ // let text_style = TextStyle {
+ // color: style.autocomplete.inline_docs_color,
+ // font_size: style.text.font_size
+ // * style.autocomplete.inline_docs_size_percent,
+ // ..style.text.clone()
+ // };
+
+ // let label = Text::new(text.clone(), text_style)
+ // .aligned()
+ // .constrained()
+ // .dynamically(move |constraint, _, _| gpui::SizeConstraint {
+ // min: constraint.min,
+ // max: vec2f(constraint.max.x(), constraint.min.y()),
+ // });
+
+ // if Some(item_ix) == widest_completion_ix {
+ // Some(
+ // label
+ // .contained()
+ // .with_style(style.autocomplete.inline_docs_container)
+ // .into_any(),
+ // )
+ // } else {
+ // Some(label.flex_float().into_any())
+ // }
+ // })())
+ // .into_any()
+ // } else {
+ // completion_label.into_any()
+ // }
+ // .contained()
+ // .with_style(item_style)
+ // .constrained()
+ // .dynamically(move |constraint, _, _| {
+ // if Some(item_ix) == widest_completion_ix {
+ // constraint
+ // } else {
+ // gpui::SizeConstraint {
+ // min: constraint.min,
+ // max: constraint.min,
+ // }
+ // }
+ // })
+ // })
+ // .with_cursor_style(CursorStyle::PointingHand)
+ // .on_down(MouseButton::Left, move |_, this, cx| {
+ // this.confirm_completion(
+ // &ConfirmCompletion {
+ // item_ix: Some(item_ix),
+ // },
+ // cx,
+ // )
+ // .map(|task| task.detach());
+ // })
+ // .constrained()
+ //
+ div()
+ .id(mat.candidate_id)
+ .bg(gpui::green())
+ .hover(|style| style.bg(gpui::blue()))
+ .when(item_ix == selected_item, |div| div.bg(gpui::blue()))
+ .child(SharedString::from(completion.label.text.clone()))
+ .min_w(px(300.))
+ .max_w(px(700.))
+ })
+ .collect()
+ },
+ )
.with_width_from_item(widest_completion_ix);
- list.render()
+ list.render_into_any()
// todo!("multiline documentation")
// enum MultiLineDocumentation {}
@@ -1529,13 +1532,15 @@ impl CodeActionsMenu {
mut cursor_position: DisplayPoint,
style: &EditorStyle,
cx: &mut ViewContext<Editor>,
- ) -> (DisplayPoint, AnyElement<Editor>) {
+ ) -> (DisplayPoint, AnyElement) {
let actions = self.actions.clone();
let selected_item = self.selected_item;
+
let element = uniform_list(
+ cx.view().clone(),
"code_actions_menu",
self.actions.len(),
- move |editor, range, cx| {
+ move |this, range, cx| {
actions[range.clone()]
.iter()
.enumerate()
@@ -1557,18 +1562,22 @@ impl CodeActionsMenu {
.bg(colors.element_hover)
.text_color(colors.text_accent)
})
- .on_mouse_down(MouseButton::Left, move |editor: &mut Editor, _, cx| {
- cx.stop_propagation();
- editor
- .confirm_code_action(
- &ConfirmCodeAction {
- item_ix: Some(item_ix),
- },
- cx,
- )
- .map(|task| task.detach_and_log_err(cx));
- })
- .child(action.lsp_action.title.clone())
+ .on_mouse_down(
+ MouseButton::Left,
+ cx.listener(move |editor, _, cx| {
+ cx.stop_propagation();
+ editor
+ .confirm_code_action(
+ &ConfirmCodeAction {
+ item_ix: Some(item_ix),
+ },
+ cx,
+ )
+ .map(|task| task.detach_and_log_err(cx));
+ }),
+ )
+ // TASK: It would be good to make lsp_action.title a SharedString to avoid allocating here.
+ .child(SharedString::from(action.lsp_action.title.clone()))
})
.collect()
},
@@ -1583,7 +1592,7 @@ impl CodeActionsMenu {
.max_by_key(|(_, action)| action.lsp_action.title.chars().count())
.map(|(ix, _)| ix),
)
- .render();
+ .render_into_any();
if self.deployed_from_indicator {
*cursor_position.column_mut() = 0;
@@ -2306,7 +2315,8 @@ impl Editor {
}
self.blink_manager.update(cx, BlinkManager::pause_blinking);
- cx.emit(Event::SelectionsChanged { local });
+ cx.emit(EditorEvent::SelectionsChanged { local });
+ cx.emit(SearchEvent::MatchesInvalidated);
if self.selections.disjoint_anchors().len() == 1 {
cx.emit(SearchEvent::ActiveMatchChanged)
@@ -4230,7 +4240,7 @@ impl Editor {
self.report_copilot_event(Some(completion.uuid.clone()), true, cx)
}
- cx.emit(Event::InputHandled {
+ cx.emit(EditorEvent::InputHandled {
utf16_range_to_replace: None,
text: suggestion.text.to_string().into(),
});
@@ -4341,19 +4351,19 @@ impl Editor {
style: &EditorStyle,
is_active: bool,
cx: &mut ViewContext<Self>,
- ) -> Option<AnyElement<Self>> {
+ ) -> Option<IconButton> {
if self.available_code_actions.is_some() {
Some(
- IconButton::new("code_actions_indicator", ui::Icon::Bolt)
- .on_click(|editor: &mut Editor, cx| {
+ IconButton::new("code_actions_indicator", ui::Icon::Bolt).on_click(cx.listener(
+ |editor, e, cx| {
editor.toggle_code_actions(
&ToggleCodeActions {
deployed_from_indicator: true,
},
cx,
);
- })
- .render(),
+ },
+ )),
)
} else {
None
@@ -4368,7 +4378,7 @@ impl Editor {
line_height: Pixels,
gutter_margin: Pixels,
cx: &mut ViewContext<Self>,
- ) -> Vec<Option<AnyElement<Self>>> {
+ ) -> Vec<Option<IconButton>> {
fold_data
.iter()
.enumerate()
@@ -4381,15 +4391,15 @@ impl Editor {
FoldStatus::Foldable => ui::Icon::ChevronDown,
};
IconButton::new(ix as usize, icon)
- .on_click(move |editor: &mut Editor, cx| match fold_status {
+ .on_click(cx.listener(move |editor, e, cx| match fold_status {
FoldStatus::Folded => {
editor.unfold_at(&UnfoldAt { buffer_row }, cx);
}
FoldStatus::Foldable => {
editor.fold_at(&FoldAt { buffer_row }, cx);
}
- })
- .render()
+ }))
+ .color(ui::Color::Muted)
})
})
.flatten()
@@ -4409,7 +4419,7 @@ impl Editor {
cursor_position: DisplayPoint,
style: &EditorStyle,
cx: &mut ViewContext<Editor>,
- ) -> Option<(DisplayPoint, AnyElement<Editor>)> {
+ ) -> Option<(DisplayPoint, AnyElement)> {
self.context_menu.read().as_ref().map(|menu| {
menu.render(
cursor_position,
@@ -5627,7 +5637,7 @@ impl Editor {
self.request_autoscroll(Autoscroll::fit(), cx);
self.unmark_text(cx);
self.refresh_copilot_suggestions(true, cx);
- cx.emit(Event::Edited);
+ cx.emit(EditorEvent::Edited);
}
}
@@ -5642,7 +5652,7 @@ impl Editor {
self.request_autoscroll(Autoscroll::fit(), cx);
self.unmark_text(cx);
self.refresh_copilot_suggestions(true, cx);
- cx.emit(Event::Edited);
+ cx.emit(EditorEvent::Edited);
}
}
@@ -7768,7 +7778,7 @@ impl Editor {
}
div()
.pl(cx.anchor_x)
- .child(rename_editor.render_with(EditorElement::new(
+ .child(EditorElement::new(
&rename_editor,
EditorStyle {
background: cx.theme().system().transparent,
@@ -7776,11 +7786,13 @@ impl Editor {
text: text_style,
scrollbar_width: cx.editor_style.scrollbar_width,
syntax: cx.editor_style.syntax.clone(),
- diagnostic_style:
- cx.editor_style.diagnostic_style.clone(),
+ diagnostic_style: cx
+ .editor_style
+ .diagnostic_style
+ .clone(),
},
- )))
- .render()
+ ))
+ .render_into_any()
}
}),
disposition: BlockDisposition::Below,
@@ -8111,7 +8123,7 @@ impl Editor {
log::error!("unexpectedly ended a transaction that wasn't started by this editor");
}
- cx.emit(Event::Edited);
+ cx.emit(EditorEvent::Edited);
Some(tx_id)
} else {
None
@@ -8699,7 +8711,7 @@ impl Editor {
if self.has_active_copilot_suggestion(cx) {
self.update_visible_copilot_suggestion(cx);
}
- cx.emit(Event::BufferEdited);
+ cx.emit(EditorEvent::BufferEdited);
cx.emit(ItemEvent::Edit);
cx.emit(ItemEvent::UpdateBreadcrumbs);
cx.emit(SearchEvent::MatchesInvalidated);
@@ -8738,7 +8750,7 @@ impl Editor {
predecessor,
excerpts,
} => {
- cx.emit(Event::ExcerptsAdded {
+ cx.emit(EditorEvent::ExcerptsAdded {
buffer: buffer.clone(),
predecessor: *predecessor,
excerpts: excerpts.clone(),
@@ -8747,7 +8759,7 @@ impl Editor {
}
multi_buffer::Event::ExcerptsRemoved { ids } => {
self.refresh_inlay_hints(InlayHintRefreshReason::ExcerptsRemoved(ids.clone()), cx);
- cx.emit(Event::ExcerptsRemoved { ids: ids.clone() })
+ cx.emit(EditorEvent::ExcerptsRemoved { ids: ids.clone() })
}
multi_buffer::Event::Reparsed => {
cx.emit(ItemEvent::UpdateBreadcrumbs);
@@ -8761,7 +8773,7 @@ impl Editor {
cx.emit(ItemEvent::UpdateTab);
cx.emit(ItemEvent::UpdateBreadcrumbs);
}
- multi_buffer::Event::DiffBaseChanged => cx.emit(Event::DiffBaseChanged),
+ multi_buffer::Event::DiffBaseChanged => cx.emit(EditorEvent::DiffBaseChanged),
multi_buffer::Event::Closed => cx.emit(ItemEvent::CloseItem),
multi_buffer::Event::DiagnosticsUpdated => {
self.refresh_active_diagnostics(cx);
@@ -8955,12 +8967,12 @@ impl Editor {
let telemetry = project.read(cx).client().telemetry().clone();
let telemetry_settings = *TelemetrySettings::get_global(cx);
- let event = ClickhouseEvent::Copilot {
+ telemetry.report_copilot_event(
+ telemetry_settings,
suggestion_id,
suggestion_accepted,
file_extension,
- };
- telemetry.report_clickhouse_event(event, telemetry_settings);
+ )
}
#[cfg(any(test, feature = "test-support"))]
@@ -9007,14 +9019,14 @@ impl Editor {
.show_copilot_suggestions;
let telemetry = project.read(cx).client().telemetry().clone();
- let event = ClickhouseEvent::Editor {
+ telemetry.report_editor_event(
+ telemetry_settings,
file_extension,
vim_mode,
operation,
copilot_enabled,
copilot_enabled_for_language,
- };
- telemetry.report_clickhouse_event(event, telemetry_settings)
+ )
}
/// Copy the highlighted chunks to the clipboard as JSON. The format is an array of lines,
@@ -9101,7 +9113,7 @@ impl Editor {
cx: &mut ViewContext<Self>,
) {
if !self.input_enabled {
- cx.emit(Event::InputIgnored { text: text.into() });
+ cx.emit(EditorEvent::InputIgnored { text: text.into() });
return;
}
if let Some(relative_utf16_range) = relative_utf16_range {
@@ -9161,7 +9173,7 @@ impl Editor {
}
fn handle_focus(&mut self, cx: &mut ViewContext<Self>) {
- cx.emit(Event::Focused);
+ cx.emit(EditorEvent::Focused);
if let Some(rename) = self.pending_rename.as_ref() {
let rename_editor_focus_handle = rename.editor.read(cx).focus_handle.clone();
@@ -9191,7 +9203,7 @@ impl Editor {
.update(cx, |buffer, cx| buffer.remove_active_selections(cx));
self.hide_context_menu(cx);
hide_hover(self, cx);
- cx.emit(Event::Blurred);
+ cx.emit(EditorEvent::Blurred);
cx.notify();
}
}
@@ -9314,7 +9326,7 @@ impl Deref for EditorSnapshot {
}
#[derive(Clone, Debug, PartialEq, Eq)]
-pub enum Event {
+pub enum EditorEvent {
InputIgnored {
text: Arc<str>,
},
@@ -9332,8 +9344,12 @@ pub enum Event {
},
BufferEdited,
Edited,
+ Reparsed,
Focused,
Blurred,
+ DirtyChanged,
+ Saved,
+ TitleChanged,
DiffBaseChanged,
SelectionsChanged {
local: bool,
@@ -9342,6 +9358,7 @@ pub enum Event {
local: bool,
autoscroll: bool,
},
+ Closed,
}
pub struct EditorFocused(pub View<Editor>);
@@ -9356,7 +9373,7 @@ pub struct EditorReleased(pub WeakView<Editor>);
// }
// }
//
-impl EventEmitter<Event> for Editor {}
+impl EventEmitter<EditorEvent> for Editor {}
impl FocusableView for Editor {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
@@ -9559,7 +9576,7 @@ impl InputHandler for Editor {
cx: &mut ViewContext<Self>,
) {
if !self.input_enabled {
- cx.emit(Event::InputIgnored { text: text.into() });
+ cx.emit(EditorEvent::InputIgnored { text: text.into() });
return;
}
@@ -9589,7 +9606,7 @@ impl InputHandler for Editor {
})
});
- cx.emit(Event::InputHandled {
+ cx.emit(EditorEvent::InputHandled {
utf16_range_to_replace: range_to_replace,
text: text.into(),
});
@@ -9620,7 +9637,7 @@ impl InputHandler for Editor {
cx: &mut ViewContext<Self>,
) {
if !self.input_enabled {
- cx.emit(Event::InputIgnored { text: text.into() });
+ cx.emit(EditorEvent::InputIgnored { text: text.into() });
return;
}
@@ -9663,7 +9680,7 @@ impl InputHandler for Editor {
})
});
- cx.emit(Event::InputHandled {
+ cx.emit(EditorEvent::InputHandled {
utf16_range_to_replace: range_to_replace,
text: text.into(),
});
@@ -9978,11 +9995,11 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend
.ml(cx.anchor_x)
}))
.cursor_pointer()
- .on_click(move |_, _, cx| {
+ .on_click(cx.listener(move |_, _, cx| {
cx.write_to_clipboard(ClipboardItem::new(message.clone()));
- })
- .tooltip(|_, cx| Tooltip::text("Copy diagnostic message", cx))
- .render()
+ }))
+ .tooltip(|cx| Tooltip::text("Copy diagnostic message", cx))
+ .render_into_any()
})
}
@@ -3048,7 +3048,7 @@ fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
position: snapshot.anchor_after(Point::new(2, 0)),
disposition: BlockDisposition::Below,
height: 1,
- render: Arc::new(|_| div().render()),
+ render: Arc::new(|_| div().into_any()),
}],
Some(Autoscroll::fit()),
cx,
@@ -3853,7 +3853,7 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
let (view, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
- view.condition::<crate::Event>(&cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
+ view.condition::<crate::EditorEvent>(&cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
.await;
view.update(cx, |view, cx| {
@@ -4019,7 +4019,7 @@ async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) {
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
editor
- .condition::<crate::Event>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
+ .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
.await;
editor.update(cx, |editor, cx| {
@@ -4583,7 +4583,7 @@ async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) {
});
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
let (view, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
- view.condition::<crate::Event>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
+ view.condition::<crate::EditorEvent>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
.await;
view.update(cx, |view, cx| {
@@ -4734,7 +4734,7 @@ async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) {
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
editor
- .condition::<crate::Event>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
+ .condition::<crate::EditorEvent>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
.await;
editor.update(cx, |editor, cx| {
@@ -6295,7 +6295,7 @@ async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) {
});
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
let (view, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
- view.condition::<crate::Event>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
+ view.condition::<crate::EditorEvent>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
.await;
view.update(cx, |view, cx| {
@@ -20,10 +20,10 @@ use collections::{BTreeMap, HashMap};
use gpui::{
div, point, px, relative, size, transparent_black, Action, AnyElement, AvailableSpace,
BorrowWindow, Bounds, Component, ContentMask, Corners, DispatchPhase, Edges, Element,
- ElementId, ElementInputHandler, Entity, EntityId, Hsla, InteractiveComponent, LineLayout,
- MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentComponent, Pixels,
- ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveComponent, Style, Styled,
- TextRun, TextStyle, View, ViewContext, WindowContext, WrappedLine,
+ ElementId, ElementInputHandler, Entity, EntityId, Hsla, InteractiveElement, LineLayout,
+ MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, RenderOnce,
+ ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled,
+ TextRun, TextStyle, View, ViewContext, WeakView, WindowContext, WrappedLine,
};
use itertools::Itertools;
use language::language_settings::ShowWhitespaceSetting;
@@ -112,14 +112,14 @@ impl SelectionLayout {
}
pub struct EditorElement {
- editor_id: EntityId,
+ editor: View<Editor>,
style: EditorStyle,
}
impl EditorElement {
pub fn new(editor: &View<Editor>, style: EditorStyle) -> Self {
Self {
- editor_id: editor.entity_id(),
+ editor: editor.clone(),
style,
}
}
@@ -349,7 +349,7 @@ impl EditorElement {
gutter_bounds: Bounds<Pixels>,
text_bounds: Bounds<Pixels>,
layout: &LayoutState,
- cx: &mut ViewContext<Editor>,
+ cx: &mut WindowContext,
) {
let bounds = gutter_bounds.union(&text_bounds);
let scroll_top =
@@ -460,7 +460,7 @@ impl EditorElement {
bounds: Bounds<Pixels>,
layout: &mut LayoutState,
editor: &mut Editor,
- cx: &mut ViewContext<Editor>,
+ cx: &mut WindowContext,
) {
let line_height = layout.position_map.line_height;
@@ -488,13 +488,14 @@ impl EditorElement {
}
}
- for (ix, fold_indicator) in layout.fold_indicators.iter_mut().enumerate() {
- if let Some(fold_indicator) = fold_indicator.as_mut() {
+ for (ix, fold_indicator) in layout.fold_indicators.drain(..).enumerate() {
+ if let Some(mut fold_indicator) = fold_indicator {
+ let mut fold_indicator = fold_indicator.render_into_any();
let available_space = size(
AvailableSpace::MinContent,
AvailableSpace::Definite(line_height * 0.55),
);
- let fold_indicator_size = fold_indicator.measure(available_space, editor, cx);
+ let fold_indicator_size = fold_indicator.measure(available_space, cx);
let position = point(
bounds.size.width - layout.gutter_padding,
@@ -505,32 +506,29 @@ impl EditorElement {
(line_height - fold_indicator_size.height) / 2.,
);
let origin = bounds.origin + position + centering_offset;
- fold_indicator.draw(origin, available_space, editor, cx);
+ fold_indicator.draw(origin, available_space, cx);
}
}
- if let Some(indicator) = layout.code_actions_indicator.as_mut() {
+ if let Some(indicator) = layout.code_actions_indicator.take() {
+ let mut button = indicator.button.render_into_any();
let available_space = size(
AvailableSpace::MinContent,
AvailableSpace::Definite(line_height),
);
- let indicator_size = indicator.element.measure(available_space, editor, cx);
+ let indicator_size = button.measure(available_space, cx);
+
let mut x = Pixels::ZERO;
let mut y = indicator.row as f32 * line_height - scroll_top;
// Center indicator.
x += ((layout.gutter_padding + layout.gutter_margin) - indicator_size.width) / 2.;
y += (line_height - indicator_size.height) / 2.;
- indicator
- .element
- .draw(bounds.origin + point(x, y), available_space, editor, cx);
+
+ button.draw(bounds.origin + point(x, y), available_space, cx);
}
}
- fn paint_diff_hunks(
- bounds: Bounds<Pixels>,
- layout: &LayoutState,
- cx: &mut ViewContext<Editor>,
- ) {
+ fn paint_diff_hunks(bounds: Bounds<Pixels>, layout: &LayoutState, cx: &mut WindowContext) {
// todo!()
// let diff_style = &theme::current(cx).editor.diff.clone();
// let line_height = layout.position_map.line_height;
@@ -619,7 +617,7 @@ impl EditorElement {
text_bounds: Bounds<Pixels>,
layout: &mut LayoutState,
editor: &mut Editor,
- cx: &mut ViewContext<Editor>,
+ cx: &mut WindowContext,
) {
let scroll_position = layout.position_map.snapshot.scroll_position();
let start_row = layout.visible_display_row_range.start;
@@ -674,20 +672,22 @@ impl EditorElement {
div()
.id(fold.id)
.size_full()
- .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
- .on_click(move |editor: &mut Editor, _, cx| {
- editor.unfold_ranges(
- [fold_range.start..fold_range.end],
- true,
- false,
- cx,
- );
- cx.stop_propagation();
- })
+ .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
+ .on_click(cx.listener_for(
+ &self.editor,
+ move |editor: &mut Editor, _, cx| {
+ editor.unfold_ranges(
+ [fold_range.start..fold_range.end],
+ true,
+ false,
+ cx,
+ );
+ cx.stop_propagation();
+ },
+ ))
.draw(
fold_bounds.origin,
fold_bounds.size,
- editor,
cx,
|fold_element_state, cx| {
if fold_element_state.is_active() {
@@ -840,7 +840,7 @@ impl EditorElement {
}
});
- if let Some((position, context_menu)) = layout.context_menu.as_mut() {
+ if let Some((position, mut context_menu)) = layout.context_menu.take() {
cx.with_z_index(1, |cx| {
let line_height = self.style.text.line_height_in_pixels(cx.rem_size());
let available_space = size(
@@ -850,7 +850,7 @@ impl EditorElement {
.min((text_bounds.size.height - line_height) / 2.),
),
);
- let context_menu_size = context_menu.measure(available_space, editor, cx);
+ let context_menu_size = context_menu.measure(available_space, cx);
let cursor_row_layout = &layout.position_map.line_layouts
[(position.row() - start_row) as usize]
@@ -874,7 +874,7 @@ impl EditorElement {
list_origin.y -= layout.position_map.line_height - list_height;
}
- context_menu.draw(list_origin, available_space, editor, cx);
+ context_menu.draw(list_origin, available_space, cx);
})
}
@@ -1165,7 +1165,7 @@ impl EditorElement {
layout: &LayoutState,
content_origin: gpui::Point<Pixels>,
bounds: Bounds<Pixels>,
- cx: &mut ViewContext<Editor>,
+ cx: &mut WindowContext,
) {
let start_row = layout.visible_display_row_range.start;
let end_row = layout.visible_display_row_range.end;
@@ -1218,13 +1218,13 @@ impl EditorElement {
bounds: Bounds<Pixels>,
layout: &mut LayoutState,
editor: &mut Editor,
- cx: &mut ViewContext<Editor>,
+ cx: &mut WindowContext,
) {
let scroll_position = layout.position_map.snapshot.scroll_position();
let scroll_left = scroll_position.x * layout.position_map.em_width;
let scroll_top = scroll_position.y * layout.position_map.line_height;
- for block in &mut layout.blocks {
+ for block in layout.blocks.drain(..) {
let mut origin = bounds.origin
+ point(
Pixels::ZERO,
@@ -1233,9 +1233,7 @@ impl EditorElement {
if !matches!(block.style, BlockStyle::Sticky) {
origin += point(-scroll_left, Pixels::ZERO);
}
- block
- .element
- .draw(origin, block.available_space, editor, cx);
+ block.element.draw(origin, block.available_space, cx);
}
}
@@ -1810,7 +1808,7 @@ impl EditorElement {
.render_code_actions_indicator(&style, active, cx)
.map(|element| CodeActionsIndicator {
row: newest_selection_head.row(),
- element,
+ button: element,
});
}
}
@@ -1970,6 +1968,7 @@ impl EditorElement {
TransformBlock::ExcerptHeader { .. } => false,
TransformBlock::Custom(block) => block.style() == BlockStyle::Fixed,
});
+
let mut render_block = |block: &TransformBlock,
available_space: Size<AvailableSpace>,
block_id: usize,
@@ -2003,6 +2002,7 @@ impl EditorElement {
editor_style: &self.style,
})
}
+
TransformBlock::ExcerptHeader {
buffer,
range,
@@ -2026,12 +2026,10 @@ impl EditorElement {
let jump_position = language::ToPoint::to_point(&jump_anchor, buffer);
IconButton::new(block_id, ui::Icon::ArrowUpRight)
- .on_click(move |editor: &mut Editor, cx| {
+ .on_click(cx.listener_for(&self.editor, move |editor, e, cx| {
editor.jump(jump_path.clone(), jump_position, jump_anchor, cx);
- })
- .tooltip(move |_, cx| {
- Tooltip::for_action("Jump to Buffer", &OpenExcerpts, cx)
- })
+ }))
+ .tooltip(|cx| Tooltip::for_action("Jump to Buffer", &OpenExcerpts, cx))
});
let element = if *starts_new_buffer {
@@ -2041,29 +2039,36 @@ impl EditorElement {
// Can't use .and_then() because `.file_name()` and `.parent()` return references :(
if let Some(path) = path {
filename = path.file_name().map(|f| f.to_string_lossy().to_string());
- parent_path =
- path.parent().map(|p| p.to_string_lossy().to_string() + "/");
+ parent_path = path
+ .parent()
+ .map(|p| SharedString::from(p.to_string_lossy().to_string() + "/"));
}
h_stack()
+ .id("path header block")
.size_full()
.bg(gpui::red())
- .child(filename.unwrap_or_else(|| "untitled".to_string()))
+ .child(
+ filename
+ .map(SharedString::from)
+ .unwrap_or_else(|| "untitled".into()),
+ )
.children(parent_path)
.children(jump_icon) // .p_x(gutter_padding)
} else {
let text_style = style.text.clone();
h_stack()
+ .id("collapsed context")
.size_full()
.bg(gpui::red())
.child("⋯")
.children(jump_icon) // .p_x(gutter_padding)
};
- element.render()
+ element.into_any()
}
};
- let size = element.measure(available_space, editor, cx);
+ let size = element.measure(available_space, cx);
(element, size)
};
@@ -2122,47 +2127,61 @@ impl EditorElement {
gutter_bounds: Bounds<Pixels>,
text_bounds: Bounds<Pixels>,
layout: &LayoutState,
- cx: &mut ViewContext<Editor>,
+ cx: &mut WindowContext,
) {
let content_origin = text_bounds.origin + point(layout.gutter_margin, Pixels::ZERO);
cx.on_mouse_event({
let position_map = layout.position_map.clone();
- move |editor, event: &ScrollWheelEvent, phase, cx| {
+ let editor = self.editor.clone();
+
+ move |event: &ScrollWheelEvent, phase, cx| {
if phase != DispatchPhase::Bubble {
return;
}
- if Self::scroll(editor, event, &position_map, bounds, cx) {
+ let should_cancel = editor.update(cx, |editor, cx| {
+ Self::scroll(editor, event, &position_map, bounds, cx)
+ });
+ if should_cancel {
cx.stop_propagation();
}
}
});
+
cx.on_mouse_event({
let position_map = layout.position_map.clone();
- move |editor, event: &MouseDownEvent, phase, cx| {
+ let editor = self.editor.clone();
+
+ move |event: &MouseDownEvent, phase, cx| {
if phase != DispatchPhase::Bubble {
return;
}
- if Self::mouse_down(editor, event, &position_map, text_bounds, gutter_bounds, cx) {
+ let should_cancel = editor.update(cx, |editor, cx| {
+ Self::mouse_down(editor, event, &position_map, text_bounds, gutter_bounds, cx)
+ });
+
+ if should_cancel {
cx.stop_propagation()
}
}
});
+
cx.on_mouse_event({
let position_map = layout.position_map.clone();
- move |editor, event: &MouseUpEvent, phase, cx| {
- if phase != DispatchPhase::Bubble {
- return;
- }
+ let editor = self.editor.clone();
+ move |event: &MouseUpEvent, phase, cx| {
+ let should_cancel = editor.update(cx, |editor, cx| {
+ Self::mouse_up(editor, event, &position_map, text_bounds, cx)
+ });
- if Self::mouse_up(editor, event, &position_map, text_bounds, cx) {
+ if should_cancel {
cx.stop_propagation()
}
}
});
- // todo!()
+ //todo!()
// on_down(MouseButton::Right, {
// let position_map = layout.position_map.clone();
// move |event, editor, cx| {
@@ -2179,12 +2198,17 @@ impl EditorElement {
// });
cx.on_mouse_event({
let position_map = layout.position_map.clone();
- move |editor, event: &MouseMoveEvent, phase, cx| {
+ let editor = self.editor.clone();
+ move |event: &MouseMoveEvent, phase, cx| {
if phase != DispatchPhase::Bubble {
return;
}
- if Self::mouse_moved(editor, event, &position_map, text_bounds, gutter_bounds, cx) {
+ let stop_propogating = editor.update(cx, |editor, cx| {
+ Self::mouse_moved(editor, event, &position_map, text_bounds, gutter_bounds, cx)
+ });
+
+ if stop_propogating {
cx.stop_propagation()
}
}
@@ -2313,7 +2337,7 @@ impl LineWithInvisibles {
content_origin: gpui::Point<Pixels>,
whitespace_setting: ShowWhitespaceSetting,
selection_ranges: &[Range<DisplayPoint>],
- cx: &mut ViewContext<Editor>,
+ cx: &mut WindowContext,
) {
let line_height = layout.position_map.line_height;
let line_y = line_height * row as f32 - layout.position_map.scroll_position.y;
@@ -2345,7 +2369,7 @@ impl LineWithInvisibles {
row: u32,
line_height: Pixels,
whitespace_setting: ShowWhitespaceSetting,
- cx: &mut ViewContext<Editor>,
+ cx: &mut WindowContext,
) {
let allowed_invisibles_regions = match whitespace_setting {
ShowWhitespaceSetting::None => return,
@@ -2388,87 +2412,102 @@ enum Invisible {
Whitespace { line_offset: usize },
}
-impl Element<Editor> for EditorElement {
- type ElementState = ();
-
- fn element_id(&self) -> Option<gpui::ElementId> {
- Some(self.editor_id.into())
- }
+impl Element for EditorElement {
+ type State = ();
fn layout(
&mut self,
- editor: &mut Editor,
- element_state: Option<Self::ElementState>,
- cx: &mut gpui::ViewContext<Editor>,
- ) -> (gpui::LayoutId, Self::ElementState) {
- editor.style = Some(self.style.clone()); // Long-term, we'd like to eliminate this.
-
- let rem_size = cx.rem_size();
- let mut style = Style::default();
- style.size.width = relative(1.).into();
- style.size.height = match editor.mode {
- EditorMode::SingleLine => self.style.text.line_height_in_pixels(cx.rem_size()).into(),
- EditorMode::AutoHeight { .. } => todo!(),
- EditorMode::Full => relative(1.).into(),
- };
- let layout_id = cx.request_layout(&style, None);
- (layout_id, ())
+ element_state: Option<Self::State>,
+ cx: &mut gpui::WindowContext,
+ ) -> (gpui::LayoutId, Self::State) {
+ self.editor.update(cx, |editor, cx| {
+ editor.style = Some(self.style.clone()); // Long-term, we'd like to eliminate this.
+
+ let rem_size = cx.rem_size();
+ let mut style = Style::default();
+ style.size.width = relative(1.).into();
+ style.size.height = match editor.mode {
+ EditorMode::SingleLine => {
+ self.style.text.line_height_in_pixels(cx.rem_size()).into()
+ }
+ EditorMode::AutoHeight { .. } => todo!(),
+ EditorMode::Full => relative(1.).into(),
+ };
+ let layout_id = cx.request_layout(&style, None);
+
+ (layout_id, ())
+ })
}
fn paint(
- &mut self,
+ mut self,
bounds: Bounds<gpui::Pixels>,
- editor: &mut Editor,
- element_state: &mut Self::ElementState,
- cx: &mut gpui::ViewContext<Editor>,
+ element_state: &mut Self::State,
+ cx: &mut gpui::WindowContext,
) {
- let mut layout = self.compute_layout(editor, cx, bounds);
- let gutter_bounds = Bounds {
- origin: bounds.origin,
- size: layout.gutter_size,
- };
- let text_bounds = Bounds {
- origin: gutter_bounds.upper_right(),
- size: layout.text_size,
- };
-
- let dispatch_context = editor.dispatch_context(cx);
- cx.with_key_dispatch(
- dispatch_context,
- Some(editor.focus_handle.clone()),
- |_, cx| {
- register_actions(cx);
+ let editor = self.editor.clone();
+ editor.update(cx, |editor, cx| {
+ let mut layout = self.compute_layout(editor, cx, bounds);
+ let gutter_bounds = Bounds {
+ origin: bounds.origin,
+ size: layout.gutter_size,
+ };
+ let text_bounds = Bounds {
+ origin: gutter_bounds.upper_right(),
+ size: layout.text_size,
+ };
- // We call with_z_index to establish a new stacking context.
- cx.with_z_index(0, |cx| {
- cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
- // Paint mouse listeners first, so any elements we paint on top of the editor
- // take precedence.
- self.paint_mouse_listeners(bounds, gutter_bounds, text_bounds, &layout, cx);
- let input_handler = ElementInputHandler::new(bounds, cx);
- cx.handle_input(&editor.focus_handle, input_handler);
-
- self.paint_background(gutter_bounds, text_bounds, &layout, cx);
- if layout.gutter_size.width > Pixels::ZERO {
- self.paint_gutter(gutter_bounds, &mut layout, editor, cx);
- }
- self.paint_text(text_bounds, &mut layout, editor, cx);
+ let dispatch_context = editor.dispatch_context(cx);
+ let editor_handle = cx.view().clone();
+ cx.with_key_dispatch(
+ dispatch_context,
+ Some(editor.focus_handle.clone()),
+ |_, cx| {
+ register_actions(&editor_handle, cx);
+
+ // We call with_z_index to establish a new stacking context.
+ cx.with_z_index(0, |cx| {
+ cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
+ // Paint mouse listeners first, so any elements we paint on top of the editor
+ // take precedence.
+ self.paint_mouse_listeners(
+ bounds,
+ gutter_bounds,
+ text_bounds,
+ &layout,
+ cx,
+ );
+ let input_handler = ElementInputHandler::new(bounds, editor_handle, cx);
+ cx.handle_input(&editor.focus_handle, input_handler);
+
+ self.paint_background(gutter_bounds, text_bounds, &layout, cx);
+ if layout.gutter_size.width > Pixels::ZERO {
+ self.paint_gutter(gutter_bounds, &mut layout, editor, cx);
+ }
+ self.paint_text(text_bounds, &mut layout, editor, cx);
- if !layout.blocks.is_empty() {
- cx.with_element_id(Some("editor_blocks"), |cx| {
- self.paint_blocks(bounds, &mut layout, editor, cx);
- })
- }
+ if !layout.blocks.is_empty() {
+ cx.with_element_id(Some("editor_blocks"), |cx| {
+ self.paint_blocks(bounds, &mut layout, editor, cx);
+ })
+ }
+ });
});
- });
- },
- )
+ },
+ )
+ })
}
}
-impl Component<Editor> for EditorElement {
- fn render(self) -> AnyElement<Editor> {
- AnyElement::new(self)
+impl RenderOnce for EditorElement {
+ type Element = Self;
+
+ fn element_id(&self) -> Option<gpui::ElementId> {
+ self.editor.element_id()
+ }
+
+ fn render_once(self) -> Self::Element {
+ self
}
}
@@ -3093,17 +3132,17 @@ pub struct LayoutState {
show_scrollbars: bool,
is_singleton: bool,
max_row: u32,
- context_menu: Option<(DisplayPoint, AnyElement<Editor>)>,
+ context_menu: Option<(DisplayPoint, AnyElement)>,
code_actions_indicator: Option<CodeActionsIndicator>,
- // hover_popovers: Option<(DisplayPoint, Vec<AnyElement<Editor>>)>,
- fold_indicators: Vec<Option<AnyElement<Editor>>>,
+ // hover_popovers: Option<(DisplayPoint, Vec<AnyElement>)>,
+ fold_indicators: Vec<Option<IconButton>>,
tab_invisible: ShapedLine,
space_invisible: ShapedLine,
}
struct CodeActionsIndicator {
row: u32,
- element: AnyElement<Editor>,
+ button: IconButton,
}
struct PositionMap {
@@ -3188,7 +3227,7 @@ impl PositionMap {
struct BlockLayout {
row: u32,
- element: AnyElement<Editor>,
+ element: AnyElement,
available_space: Size<AvailableSpace>,
style: BlockStyle,
}
@@ -3893,187 +3932,191 @@ fn scale_horizontal_mouse_autoscroll_delta(delta: Pixels) -> f32 {
// }
// }
-fn register_actions(cx: &mut ViewContext<Editor>) {
- register_action(cx, Editor::move_left);
- register_action(cx, Editor::move_right);
- register_action(cx, Editor::move_down);
- register_action(cx, Editor::move_up);
+fn register_actions(view: &View<Editor>, cx: &mut WindowContext) {
+ register_action(view, cx, Editor::move_left);
+ register_action(view, cx, Editor::move_right);
+ register_action(view, cx, Editor::move_down);
+ register_action(view, cx, Editor::move_up);
// on_action(cx, Editor::new_file); todo!()
// on_action(cx, Editor::new_file_in_direction); todo!()
- register_action(cx, Editor::cancel);
- register_action(cx, Editor::newline);
- register_action(cx, Editor::newline_above);
- register_action(cx, Editor::newline_below);
- register_action(cx, Editor::backspace);
- register_action(cx, Editor::delete);
- register_action(cx, Editor::tab);
- register_action(cx, Editor::tab_prev);
- register_action(cx, Editor::indent);
- register_action(cx, Editor::outdent);
- register_action(cx, Editor::delete_line);
- register_action(cx, Editor::join_lines);
- register_action(cx, Editor::sort_lines_case_sensitive);
- register_action(cx, Editor::sort_lines_case_insensitive);
- register_action(cx, Editor::reverse_lines);
- register_action(cx, Editor::shuffle_lines);
- register_action(cx, Editor::convert_to_upper_case);
- register_action(cx, Editor::convert_to_lower_case);
- register_action(cx, Editor::convert_to_title_case);
- register_action(cx, Editor::convert_to_snake_case);
- register_action(cx, Editor::convert_to_kebab_case);
- register_action(cx, Editor::convert_to_upper_camel_case);
- register_action(cx, Editor::convert_to_lower_camel_case);
- register_action(cx, Editor::delete_to_previous_word_start);
- register_action(cx, Editor::delete_to_previous_subword_start);
- register_action(cx, Editor::delete_to_next_word_end);
- register_action(cx, Editor::delete_to_next_subword_end);
- register_action(cx, Editor::delete_to_beginning_of_line);
- register_action(cx, Editor::delete_to_end_of_line);
- register_action(cx, Editor::cut_to_end_of_line);
- register_action(cx, Editor::duplicate_line);
- register_action(cx, Editor::move_line_up);
- register_action(cx, Editor::move_line_down);
- register_action(cx, Editor::transpose);
- register_action(cx, Editor::cut);
- register_action(cx, Editor::copy);
- register_action(cx, Editor::paste);
- register_action(cx, Editor::undo);
- register_action(cx, Editor::redo);
- register_action(cx, Editor::move_page_up);
- register_action(cx, Editor::move_page_down);
- register_action(cx, Editor::next_screen);
- register_action(cx, Editor::scroll_cursor_top);
- register_action(cx, Editor::scroll_cursor_center);
- register_action(cx, Editor::scroll_cursor_bottom);
- register_action(cx, |editor, _: &LineDown, cx| {
+ register_action(view, cx, Editor::cancel);
+ register_action(view, cx, Editor::newline);
+ register_action(view, cx, Editor::newline_above);
+ register_action(view, cx, Editor::newline_below);
+ register_action(view, cx, Editor::backspace);
+ register_action(view, cx, Editor::delete);
+ register_action(view, cx, Editor::tab);
+ register_action(view, cx, Editor::tab_prev);
+ register_action(view, cx, Editor::indent);
+ register_action(view, cx, Editor::outdent);
+ register_action(view, cx, Editor::delete_line);
+ register_action(view, cx, Editor::join_lines);
+ register_action(view, cx, Editor::sort_lines_case_sensitive);
+ register_action(view, cx, Editor::sort_lines_case_insensitive);
+ register_action(view, cx, Editor::reverse_lines);
+ register_action(view, cx, Editor::shuffle_lines);
+ register_action(view, cx, Editor::convert_to_upper_case);
+ register_action(view, cx, Editor::convert_to_lower_case);
+ register_action(view, cx, Editor::convert_to_title_case);
+ register_action(view, cx, Editor::convert_to_snake_case);
+ register_action(view, cx, Editor::convert_to_kebab_case);
+ register_action(view, cx, Editor::convert_to_upper_camel_case);
+ register_action(view, cx, Editor::convert_to_lower_camel_case);
+ register_action(view, cx, Editor::delete_to_previous_word_start);
+ register_action(view, cx, Editor::delete_to_previous_subword_start);
+ register_action(view, cx, Editor::delete_to_next_word_end);
+ register_action(view, cx, Editor::delete_to_next_subword_end);
+ register_action(view, cx, Editor::delete_to_beginning_of_line);
+ register_action(view, cx, Editor::delete_to_end_of_line);
+ register_action(view, cx, Editor::cut_to_end_of_line);
+ register_action(view, cx, Editor::duplicate_line);
+ register_action(view, cx, Editor::move_line_up);
+ register_action(view, cx, Editor::move_line_down);
+ register_action(view, cx, Editor::transpose);
+ register_action(view, cx, Editor::cut);
+ register_action(view, cx, Editor::copy);
+ register_action(view, cx, Editor::paste);
+ register_action(view, cx, Editor::undo);
+ register_action(view, cx, Editor::redo);
+ register_action(view, cx, Editor::move_page_up);
+ register_action(view, cx, Editor::move_page_down);
+ register_action(view, cx, Editor::next_screen);
+ register_action(view, cx, Editor::scroll_cursor_top);
+ register_action(view, cx, Editor::scroll_cursor_center);
+ register_action(view, cx, Editor::scroll_cursor_bottom);
+ register_action(view, cx, |editor, _: &LineDown, cx| {
editor.scroll_screen(&ScrollAmount::Line(1.), cx)
});
- register_action(cx, |editor, _: &LineUp, cx| {
+ register_action(view, cx, |editor, _: &LineUp, cx| {
editor.scroll_screen(&ScrollAmount::Line(-1.), cx)
});
- register_action(cx, |editor, _: &HalfPageDown, cx| {
+ register_action(view, cx, |editor, _: &HalfPageDown, cx| {
editor.scroll_screen(&ScrollAmount::Page(0.5), cx)
});
- register_action(cx, |editor, _: &HalfPageUp, cx| {
+ register_action(view, cx, |editor, _: &HalfPageUp, cx| {
editor.scroll_screen(&ScrollAmount::Page(-0.5), cx)
});
- register_action(cx, |editor, _: &PageDown, cx| {
+ register_action(view, cx, |editor, _: &PageDown, cx| {
editor.scroll_screen(&ScrollAmount::Page(1.), cx)
});
- register_action(cx, |editor, _: &PageUp, cx| {
+ register_action(view, cx, |editor, _: &PageUp, cx| {
editor.scroll_screen(&ScrollAmount::Page(-1.), cx)
});
- register_action(cx, Editor::move_to_previous_word_start);
- register_action(cx, Editor::move_to_previous_subword_start);
- register_action(cx, Editor::move_to_next_word_end);
- register_action(cx, Editor::move_to_next_subword_end);
- register_action(cx, Editor::move_to_beginning_of_line);
- register_action(cx, Editor::move_to_end_of_line);
- register_action(cx, Editor::move_to_start_of_paragraph);
- register_action(cx, Editor::move_to_end_of_paragraph);
- register_action(cx, Editor::move_to_beginning);
- register_action(cx, Editor::move_to_end);
- register_action(cx, Editor::select_up);
- register_action(cx, Editor::select_down);
- register_action(cx, Editor::select_left);
- register_action(cx, Editor::select_right);
- register_action(cx, Editor::select_to_previous_word_start);
- register_action(cx, Editor::select_to_previous_subword_start);
- register_action(cx, Editor::select_to_next_word_end);
- register_action(cx, Editor::select_to_next_subword_end);
- register_action(cx, Editor::select_to_beginning_of_line);
- register_action(cx, Editor::select_to_end_of_line);
- register_action(cx, Editor::select_to_start_of_paragraph);
- register_action(cx, Editor::select_to_end_of_paragraph);
- register_action(cx, Editor::select_to_beginning);
- register_action(cx, Editor::select_to_end);
- register_action(cx, Editor::select_all);
- register_action(cx, |editor, action, cx| {
+ register_action(view, cx, Editor::move_to_previous_word_start);
+ register_action(view, cx, Editor::move_to_previous_subword_start);
+ register_action(view, cx, Editor::move_to_next_word_end);
+ register_action(view, cx, Editor::move_to_next_subword_end);
+ register_action(view, cx, Editor::move_to_beginning_of_line);
+ register_action(view, cx, Editor::move_to_end_of_line);
+ register_action(view, cx, Editor::move_to_start_of_paragraph);
+ register_action(view, cx, Editor::move_to_end_of_paragraph);
+ register_action(view, cx, Editor::move_to_beginning);
+ register_action(view, cx, Editor::move_to_end);
+ register_action(view, cx, Editor::select_up);
+ register_action(view, cx, Editor::select_down);
+ register_action(view, cx, Editor::select_left);
+ register_action(view, cx, Editor::select_right);
+ register_action(view, cx, Editor::select_to_previous_word_start);
+ register_action(view, cx, Editor::select_to_previous_subword_start);
+ register_action(view, cx, Editor::select_to_next_word_end);
+ register_action(view, cx, Editor::select_to_next_subword_end);
+ register_action(view, cx, Editor::select_to_beginning_of_line);
+ register_action(view, cx, Editor::select_to_end_of_line);
+ register_action(view, cx, Editor::select_to_start_of_paragraph);
+ register_action(view, cx, Editor::select_to_end_of_paragraph);
+ register_action(view, cx, Editor::select_to_beginning);
+ register_action(view, cx, Editor::select_to_end);
+ register_action(view, cx, Editor::select_all);
+ register_action(view, cx, |editor, action, cx| {
editor.select_all_matches(action, cx).log_err();
});
- register_action(cx, Editor::select_line);
- register_action(cx, Editor::split_selection_into_lines);
- register_action(cx, Editor::add_selection_above);
- register_action(cx, Editor::add_selection_below);
- register_action(cx, |editor, action, cx| {
+ register_action(view, cx, Editor::select_line);
+ register_action(view, cx, Editor::split_selection_into_lines);
+ register_action(view, cx, Editor::add_selection_above);
+ register_action(view, cx, Editor::add_selection_below);
+ register_action(view, cx, |editor, action, cx| {
editor.select_next(action, cx).log_err();
});
- register_action(cx, |editor, action, cx| {
+ register_action(view, cx, |editor, action, cx| {
editor.select_previous(action, cx).log_err();
});
- register_action(cx, Editor::toggle_comments);
- register_action(cx, Editor::select_larger_syntax_node);
- register_action(cx, Editor::select_smaller_syntax_node);
- register_action(cx, Editor::move_to_enclosing_bracket);
- register_action(cx, Editor::undo_selection);
- register_action(cx, Editor::redo_selection);
- register_action(cx, Editor::go_to_diagnostic);
- register_action(cx, Editor::go_to_prev_diagnostic);
- register_action(cx, Editor::go_to_hunk);
- register_action(cx, Editor::go_to_prev_hunk);
- register_action(cx, Editor::go_to_definition);
- register_action(cx, Editor::go_to_definition_split);
- register_action(cx, Editor::go_to_type_definition);
- register_action(cx, Editor::go_to_type_definition_split);
- register_action(cx, Editor::fold);
- register_action(cx, Editor::fold_at);
- register_action(cx, Editor::unfold_lines);
- register_action(cx, Editor::unfold_at);
- register_action(cx, Editor::fold_selected_ranges);
- register_action(cx, Editor::show_completions);
- register_action(cx, Editor::toggle_code_actions);
+ register_action(view, cx, Editor::toggle_comments);
+ register_action(view, cx, Editor::select_larger_syntax_node);
+ register_action(view, cx, Editor::select_smaller_syntax_node);
+ register_action(view, cx, Editor::move_to_enclosing_bracket);
+ register_action(view, cx, Editor::undo_selection);
+ register_action(view, cx, Editor::redo_selection);
+ register_action(view, cx, Editor::go_to_diagnostic);
+ register_action(view, cx, Editor::go_to_prev_diagnostic);
+ register_action(view, cx, Editor::go_to_hunk);
+ register_action(view, cx, Editor::go_to_prev_hunk);
+ register_action(view, cx, Editor::go_to_definition);
+ register_action(view, cx, Editor::go_to_definition_split);
+ register_action(view, cx, Editor::go_to_type_definition);
+ register_action(view, cx, Editor::go_to_type_definition_split);
+ register_action(view, cx, Editor::fold);
+ register_action(view, cx, Editor::fold_at);
+ register_action(view, cx, Editor::unfold_lines);
+ register_action(view, cx, Editor::unfold_at);
+ register_action(view, cx, Editor::fold_selected_ranges);
+ register_action(view, cx, Editor::show_completions);
+ register_action(view, cx, Editor::toggle_code_actions);
// on_action(cx, Editor::open_excerpts); todo!()
- register_action(cx, Editor::toggle_soft_wrap);
- register_action(cx, Editor::toggle_inlay_hints);
- register_action(cx, Editor::reveal_in_finder);
- register_action(cx, Editor::copy_path);
- register_action(cx, Editor::copy_relative_path);
- register_action(cx, Editor::copy_highlight_json);
- register_action(cx, |editor, action, cx| {
+ register_action(view, cx, Editor::toggle_soft_wrap);
+ register_action(view, cx, Editor::toggle_inlay_hints);
+ register_action(view, cx, Editor::reveal_in_finder);
+ register_action(view, cx, Editor::copy_path);
+ register_action(view, cx, Editor::copy_relative_path);
+ register_action(view, cx, Editor::copy_highlight_json);
+ register_action(view, cx, |editor, action, cx| {
editor
.format(action, cx)
.map(|task| task.detach_and_log_err(cx));
});
- register_action(cx, Editor::restart_language_server);
- register_action(cx, Editor::show_character_palette);
+ register_action(view, cx, Editor::restart_language_server);
+ register_action(view, cx, Editor::show_character_palette);
// on_action(cx, Editor::confirm_completion); todo!()
- register_action(cx, |editor, action, cx| {
+ register_action(view, cx, |editor, action, cx| {
editor
.confirm_code_action(action, cx)
.map(|task| task.detach_and_log_err(cx));
});
- register_action(cx, |editor, action, cx| {
+ register_action(view, cx, |editor, action, cx| {
editor
.rename(action, cx)
.map(|task| task.detach_and_log_err(cx));
});
- register_action(cx, |editor, action, cx| {
+ register_action(view, cx, |editor, action, cx| {
editor
.confirm_rename(action, cx)
.map(|task| task.detach_and_log_err(cx));
});
- register_action(cx, |editor, action, cx| {
+ register_action(view, cx, |editor, action, cx| {
editor
.find_all_references(action, cx)
.map(|task| task.detach_and_log_err(cx));
});
- register_action(cx, Editor::next_copilot_suggestion);
- register_action(cx, Editor::previous_copilot_suggestion);
- register_action(cx, Editor::copilot_suggest);
- register_action(cx, Editor::context_menu_first);
- register_action(cx, Editor::context_menu_prev);
- register_action(cx, Editor::context_menu_next);
- register_action(cx, Editor::context_menu_last);
+ register_action(view, cx, Editor::next_copilot_suggestion);
+ register_action(view, cx, Editor::previous_copilot_suggestion);
+ register_action(view, cx, Editor::copilot_suggest);
+ register_action(view, cx, Editor::context_menu_first);
+ register_action(view, cx, Editor::context_menu_prev);
+ register_action(view, cx, Editor::context_menu_next);
+ register_action(view, cx, Editor::context_menu_last);
}
fn register_action<T: Action>(
- cx: &mut ViewContext<Editor>,
+ view: &View<Editor>,
+ cx: &mut WindowContext,
listener: impl Fn(&mut Editor, &T, &mut ViewContext<Editor>) + 'static,
) {
- cx.on_action(TypeId::of::<T>(), move |editor, action, phase, cx| {
+ let view = view.clone();
+ cx.on_action(TypeId::of::<T>(), move |action, phase, cx| {
let action = action.downcast_ref().unwrap();
if phase == DispatchPhase::Bubble {
- listener(editor, action, cx);
+ view.update(cx, |editor, cx| {
+ listener(editor, action, cx);
+ })
}
})
}
@@ -422,7 +422,7 @@ impl HoverState {
visible_rows: Range<u32>,
workspace: Option<WeakView<Workspace>>,
cx: &mut ViewContext<Editor>,
- ) -> Option<(DisplayPoint, Vec<AnyElement<Editor>>)> {
+ ) -> Option<(DisplayPoint, Vec<AnyElement>)> {
todo!("old version below")
}
// // If there is a diagnostic, position the popovers based on that.
@@ -504,7 +504,7 @@ pub struct DiagnosticPopover {
}
impl DiagnosticPopover {
- pub fn render(&self, style: &EditorStyle, cx: &mut ViewContext<Editor>) -> AnyElement<Editor> {
+ pub fn render(&self, style: &EditorStyle, cx: &mut ViewContext<Editor>) -> AnyElement {
todo!()
// enum PrimaryDiagnostic {}
@@ -1,7 +1,7 @@
use crate::{
editor_settings::SeedQuerySetting, link_go_to_definition::hide_link_definition,
movement::surrounding_word, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor,
- EditorSettings, Event, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot,
+ EditorEvent, EditorSettings, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot,
NavigationData, ToPoint as _,
};
use anyhow::{anyhow, Context, Result};
@@ -9,8 +9,8 @@ use collections::HashSet;
use futures::future::try_join_all;
use gpui::{
div, point, AnyElement, AppContext, AsyncAppContext, Entity, EntityId, EventEmitter,
- FocusHandle, Model, ParentComponent, Pixels, SharedString, Styled, Subscription, Task, View,
- ViewContext, VisualContext, WeakView,
+ FocusHandle, Model, ParentElement, Pixels, SharedString, Styled, Subscription, Task, View,
+ ViewContext, VisualContext, WeakView, WindowContext,
};
use language::{
proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, CharKind, OffsetRangeExt,
@@ -30,7 +30,7 @@ use std::{
};
use text::Selection;
use theme::{ActiveTheme, Theme};
-use ui::{Label, TextColor};
+use ui::{Color, Label};
use util::{paths::PathExt, ResultExt, TryFutureExt};
use workspace::item::{BreadcrumbText, FollowEvent, FollowableEvents, FollowableItemHandle};
use workspace::{
@@ -41,11 +41,12 @@ use workspace::{
pub const MAX_TAB_TITLE_LEN: usize = 24;
-impl FollowableEvents for Event {
+impl FollowableEvents for EditorEvent {
fn to_follow_event(&self) -> Option<workspace::item::FollowEvent> {
match self {
- Event::Edited => Some(FollowEvent::Unfollow),
- Event::SelectionsChanged { local } | Event::ScrollPositionChanged { local, .. } => {
+ EditorEvent::Edited => Some(FollowEvent::Unfollow),
+ EditorEvent::SelectionsChanged { local }
+ | EditorEvent::ScrollPositionChanged { local, .. } => {
if *local {
Some(FollowEvent::Unfollow)
} else {
@@ -60,7 +61,7 @@ impl FollowableEvents for Event {
impl EventEmitter<ItemEvent> for Editor {}
impl FollowableItem for Editor {
- type FollowableEvent = Event;
+ type FollowableEvent = EditorEvent;
fn remote_id(&self) -> Option<ViewId> {
self.remote_id
}
@@ -248,7 +249,7 @@ impl FollowableItem for Editor {
match update {
proto::update_view::Variant::Editor(update) => match event {
- Event::ExcerptsAdded {
+ EditorEvent::ExcerptsAdded {
buffer,
predecessor,
excerpts,
@@ -269,20 +270,20 @@ impl FollowableItem for Editor {
}
true
}
- Event::ExcerptsRemoved { ids } => {
+ EditorEvent::ExcerptsRemoved { ids } => {
update
.deleted_excerpts
.extend(ids.iter().map(ExcerptId::to_proto));
true
}
- Event::ScrollPositionChanged { .. } => {
+ EditorEvent::ScrollPositionChanged { .. } => {
let scroll_anchor = self.scroll_manager.anchor();
update.scroll_top_anchor = Some(serialize_anchor(&scroll_anchor.anchor));
update.scroll_x = scroll_anchor.offset.x;
update.scroll_y = scroll_anchor.offset.y;
true
}
- Event::SelectionsChanged { .. } => {
+ EditorEvent::SelectionsChanged { .. } => {
update.selections = self
.selections
.disjoint_anchors()
@@ -583,7 +584,7 @@ impl Item for Editor {
Some(path.to_string_lossy().to_string().into())
}
- fn tab_content<T: 'static>(&self, detail: Option<usize>, cx: &AppContext) -> AnyElement<T> {
+ fn tab_content(&self, detail: Option<usize>, cx: &WindowContext) -> AnyElement {
let theme = cx.theme();
AnyElement::new(
@@ -603,7 +604,7 @@ impl Item for Editor {
&description,
MAX_TAB_TITLE_LEN,
))
- .color(TextColor::Muted),
+ .color(Color::Muted),
),
)
})),
@@ -760,7 +761,7 @@ impl Item for Editor {
}
fn breadcrumb_location(&self) -> ToolbarItemLocation {
- ToolbarItemLocation::PrimaryLeft { flex: None }
+ ToolbarItemLocation::PrimaryLeft
}
fn breadcrumbs(&self, variant: &Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
@@ -906,17 +907,15 @@ impl SearchableItem for Editor {
type Match = Range<Anchor>;
fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
- todo!()
- // self.clear_background_highlights::<BufferSearchHighlights>(cx);
+ self.clear_background_highlights::<BufferSearchHighlights>(cx);
}
fn update_matches(&mut self, matches: Vec<Range<Anchor>>, cx: &mut ViewContext<Self>) {
- todo!()
- // self.highlight_background::<BufferSearchHighlights>(
- // matches,
- // |theme| theme.search.match_background,
- // cx,
- // );
+ self.highlight_background::<BufferSearchHighlights>(
+ matches,
+ |theme| theme.title_bar_background, // todo: update theme
+ cx,
+ );
}
fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
@@ -951,22 +950,20 @@ impl SearchableItem for Editor {
matches: Vec<Range<Anchor>>,
cx: &mut ViewContext<Self>,
) {
- todo!()
- // self.unfold_ranges([matches[index].clone()], false, true, cx);
- // let range = self.range_for_match(&matches[index]);
- // self.change_selections(Some(Autoscroll::fit()), cx, |s| {
- // s.select_ranges([range]);
- // })
+ self.unfold_ranges([matches[index].clone()], false, true, cx);
+ let range = self.range_for_match(&matches[index]);
+ self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.select_ranges([range]);
+ })
}
fn select_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>) {
- todo!()
- // self.unfold_ranges(matches.clone(), false, false, cx);
- // let mut ranges = Vec::new();
- // for m in &matches {
- // ranges.push(self.range_for_match(&m))
- // }
- // self.change_selections(None, cx, |s| s.select_ranges(ranges));
+ self.unfold_ranges(matches.clone(), false, false, cx);
+ let mut ranges = Vec::new();
+ for m in &matches {
+ ranges.push(self.range_for_match(&m))
+ }
+ self.change_selections(None, cx, |s| s.select_ranges(ranges));
}
fn replace(
&mut self,
@@ -6,8 +6,8 @@ use crate::{
display_map::{DisplaySnapshot, ToDisplayPoint},
hover_popover::hide_hover,
persistence::DB,
- Anchor, DisplayPoint, Editor, EditorMode, Event, InlayHintRefreshReason, MultiBufferSnapshot,
- ToPoint,
+ Anchor, DisplayPoint, Editor, EditorEvent, EditorMode, InlayHintRefreshReason,
+ MultiBufferSnapshot, ToPoint,
};
use gpui::{point, px, AppContext, Entity, Pixels, Styled, Task, ViewContext};
use language::{Bias, Point};
@@ -224,7 +224,7 @@ impl ScrollManager {
cx: &mut ViewContext<Editor>,
) {
self.anchor = anchor;
- cx.emit(Event::ScrollPositionChanged { local, autoscroll });
+ cx.emit(EditorEvent::ScrollPositionChanged { local, autoscroll });
self.show_scrollbar(cx);
self.autoscroll_request.take();
if let Some(workspace_id) = workspace_id {
@@ -71,7 +71,8 @@ impl<'a> EditorTestContext<'a> {
&self,
predicate: impl FnMut(&Editor, &AppContext) -> bool,
) -> impl Future<Output = ()> {
- self.editor.condition::<crate::Event>(&self.cx, predicate)
+ self.editor
+ .condition::<crate::EditorEvent>(&self.cx, predicate)
}
#[track_caller]
@@ -2,9 +2,9 @@ use collections::HashMap;
use editor::{scroll::autoscroll::Autoscroll, Bias, Editor};
use fuzzy::{CharBag, PathMatch, PathMatchCandidate};
use gpui::{
- actions, div, AppContext, Component, Dismiss, Div, FocusHandle, InteractiveComponent,
- ManagedView, Model, ParentComponent, Render, Styled, Task, View, ViewContext, VisualContext,
- WeakView,
+ actions, div, AppContext, Div, EventEmitter, FocusHandle, FocusableView, InteractiveElement,
+ Manager, Model, ParentElement, Render, RenderOnce, Styled, Task, View, ViewContext,
+ VisualContext, WeakView,
};
use picker::{Picker, PickerDelegate};
use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
@@ -111,13 +111,14 @@ impl FileFinder {
}
}
-impl ManagedView for FileFinder {
+impl EventEmitter<Manager> for FileFinder {}
+impl FocusableView for FileFinder {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.picker.focus_handle(cx)
}
}
impl Render for FileFinder {
- type Element = Div<Self>;
+ type Element = Div;
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
v_stack().w_96().child(self.picker.clone())
@@ -529,7 +530,7 @@ impl FileFinderDelegate {
}
impl PickerDelegate for FileFinderDelegate {
- type ListItem = Div<Picker<Self>>;
+ type ListItem = Div;
fn placeholder_text(&self) -> Arc<str> {
"Search project files...".into()
@@ -688,7 +689,9 @@ impl PickerDelegate for FileFinderDelegate {
.log_err();
}
}
- finder.update(&mut cx, |_, cx| cx.emit(Dismiss)).ok()?;
+ finder
+ .update(&mut cx, |_, cx| cx.emit(Manager::Dismiss))
+ .ok()?;
Some(())
})
@@ -699,7 +702,7 @@ impl PickerDelegate for FileFinderDelegate {
fn dismissed(&mut self, cx: &mut ViewContext<Picker<FileFinderDelegate>>) {
self.file_finder
- .update(cx, |_, cx| cx.emit(Dismiss))
+ .update(cx, |_, cx| cx.emit(Manager::Dismiss))
.log_err();
}
@@ -1,11 +1,11 @@
use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Editor};
use gpui::{
- actions, div, prelude::*, AppContext, Dismiss, Div, FocusHandle, ManagedView, ParentComponent,
+ actions, div, prelude::*, AppContext, Div, EventEmitter, FocusHandle, FocusableView, Manager,
Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext, WindowContext,
};
use text::{Bias, Point};
use theme::ActiveTheme;
-use ui::{h_stack, v_stack, Label, StyledExt, TextColor};
+use ui::{h_stack, v_stack, Color, Label, StyledExt};
use util::paths::FILE_ROW_COLUMN_DELIMITER;
use workspace::Workspace;
@@ -23,11 +23,12 @@ pub struct GoToLine {
_subscriptions: Vec<Subscription>,
}
-impl ManagedView for GoToLine {
+impl FocusableView for GoToLine {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
- self.line_editor.focus_handle(cx)
+ self.active_editor.focus_handle(cx)
}
}
+impl EventEmitter<Manager> for GoToLine {}
impl GoToLine {
fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
@@ -82,13 +83,13 @@ impl GoToLine {
fn on_line_editor_event(
&mut self,
_: View<Editor>,
- event: &editor::Event,
+ event: &editor::EditorEvent,
cx: &mut ViewContext<Self>,
) {
match event {
// todo!() this isn't working...
- editor::Event::Blurred => cx.emit(Dismiss),
- editor::Event::BufferEdited { .. } => self.highlight_current_line(cx),
+ editor::EditorEvent::Blurred => cx.emit(Manager::Dismiss),
+ editor::EditorEvent::BufferEdited { .. } => self.highlight_current_line(cx),
_ => {}
}
}
@@ -122,7 +123,7 @@ impl GoToLine {
}
fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
- cx.emit(Dismiss);
+ cx.emit(Manager::Dismiss);
}
fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
@@ -139,19 +140,19 @@ impl GoToLine {
self.prev_scroll_position.take();
}
- cx.emit(Dismiss);
+ cx.emit(Manager::Dismiss);
}
}
impl Render for GoToLine {
- type Element = Div<Self>;
+ type Element = Div;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
div()
.elevation_2(cx)
.key_context("GoToLine")
- .on_action(Self::cancel)
- .on_action(Self::confirm)
+ .on_action(cx.listener(Self::cancel))
+ .on_action(cx.listener(Self::confirm))
.w_96()
.child(
v_stack()
@@ -175,7 +176,7 @@ impl Render for GoToLine {
.justify_between()
.px_2()
.py_1()
- .child(Label::new(self.current_text.clone()).color(TextColor::Muted)),
+ .child(Label::new(self.current_text.clone()).color(Color::Muted)),
),
)
}
@@ -47,7 +47,7 @@ serde_derive.workspace = true
serde_json.workspace = true
smallvec.workspace = true
smol.workspace = true
-taffy = { git = "https://github.com/DioxusLabs/taffy", rev = "4fb530bdd71609bb1d3f76c6a8bde1ba82805d5e" }
+taffy = { git = "https://github.com/DioxusLabs/taffy", rev = "1876f72bee5e376023eaa518aa7b8a34c769bd1b" }
thiserror.workspace = true
time.workspace = true
tiny-skia = "0.5"
@@ -14,7 +14,7 @@ use smallvec::SmallVec;
pub use test_context::*;
use crate::{
- current_platform, image_cache::ImageCache, Action, ActionRegistry, AnyBox, AnyView,
+ current_platform, image_cache::ImageCache, Action, ActionRegistry, Any, AnyView,
AnyWindowHandle, AppMetadata, AssetSource, BackgroundExecutor, ClipboardItem, Context,
DispatchPhase, DisplayId, Entity, EventEmitter, FocusEvent, FocusHandle, FocusId,
ForegroundExecutor, KeyBinding, Keymap, LayoutId, PathPromptOptions, Pixels, Platform,
@@ -28,7 +28,7 @@ use futures::{channel::oneshot, future::LocalBoxFuture, Future};
use parking_lot::Mutex;
use slotmap::SlotMap;
use std::{
- any::{type_name, Any, TypeId},
+ any::{type_name, TypeId},
cell::{Ref, RefCell, RefMut},
marker::PhantomData,
mem,
@@ -194,7 +194,7 @@ pub struct AppContext {
asset_source: Arc<dyn AssetSource>,
pub(crate) image_cache: ImageCache,
pub(crate) text_style_stack: Vec<TextStyleRefinement>,
- pub(crate) globals_by_type: HashMap<TypeId, AnyBox>,
+ pub(crate) globals_by_type: HashMap<TypeId, Box<dyn Any>>,
pub(crate) entities: EntityMap,
pub(crate) new_view_observers: SubscriberSet<TypeId, NewViewListener>,
pub(crate) windows: SlotMap<WindowId, Option<Window>>,
@@ -424,7 +424,7 @@ impl AppContext {
/// Opens a new window with the given option and the root view returned by the given function.
/// The function is invoked with a `WindowContext`, which can be used to interact with window-specific
/// functionality.
- pub fn open_window<V: Render>(
+ pub fn open_window<V: 'static + Render>(
&mut self,
options: crate::WindowOptions,
build_root_view: impl FnOnce(&mut WindowContext) -> View<V>,
@@ -492,6 +492,10 @@ impl AppContext {
self.platform.open_url(url);
}
+ pub fn app_path(&self) -> Result<PathBuf> {
+ self.platform.app_path()
+ }
+
pub fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf> {
self.platform.path_for_auxiliary_executable(name)
}
@@ -1100,12 +1104,12 @@ pub(crate) enum Effect {
/// Wraps a global variable value during `update_global` while the value has been moved to the stack.
pub(crate) struct GlobalLease<G: 'static> {
- global: AnyBox,
+ global: Box<dyn Any>,
global_type: PhantomData<G>,
}
impl<G: 'static> GlobalLease<G> {
- fn new(global: AnyBox) -> Self {
+ fn new(global: Box<dyn Any>) -> Self {
GlobalLease {
global,
global_type: PhantomData,
@@ -1,6 +1,6 @@
use crate::{
AnyView, AnyWindowHandle, AppCell, AppContext, BackgroundExecutor, Context, FocusableView,
- ForegroundExecutor, Model, ModelContext, Render, Result, Task, View, ViewContext,
+ ForegroundExecutor, Manager, Model, ModelContext, Render, Result, Task, View, ViewContext,
VisualContext, WindowContext, WindowHandle,
};
use anyhow::{anyhow, Context as _};
@@ -115,7 +115,7 @@ impl AsyncAppContext {
build_root_view: impl FnOnce(&mut WindowContext) -> View<V>,
) -> Result<WindowHandle<V>>
where
- V: Render,
+ V: 'static + Render,
{
let app = self
.app
@@ -306,7 +306,7 @@ impl VisualContext for AsyncWindowContext {
build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V,
) -> Self::Result<View<V>>
where
- V: Render,
+ V: 'static + Render,
{
self.window
.update(self, |_, cx| cx.replace_root_view(build_view))
@@ -320,4 +320,13 @@ impl VisualContext for AsyncWindowContext {
view.read(cx).focus_handle(cx).clone().focus(cx);
})
}
+
+ fn dismiss_view<V>(&mut self, view: &View<V>) -> Self::Result<()>
+ where
+ V: crate::ManagedView,
+ {
+ self.window.update(self, |_, cx| {
+ view.update(cx, |_, cx| cx.emit(Manager::Dismiss))
+ })
+ }
}
@@ -1,10 +1,10 @@
-use crate::{private::Sealed, AnyBox, AppContext, Context, Entity, ModelContext};
+use crate::{private::Sealed, AppContext, Context, Entity, ModelContext};
use anyhow::{anyhow, Result};
use derive_more::{Deref, DerefMut};
use parking_lot::{RwLock, RwLockUpgradableReadGuard};
use slotmap::{SecondaryMap, SlotMap};
use std::{
- any::{type_name, TypeId},
+ any::{type_name, Any, TypeId},
fmt::{self, Display},
hash::{Hash, Hasher},
marker::PhantomData,
@@ -31,7 +31,7 @@ impl Display for EntityId {
}
pub(crate) struct EntityMap {
- entities: SecondaryMap<EntityId, AnyBox>,
+ entities: SecondaryMap<EntityId, Box<dyn Any>>,
ref_counts: Arc<RwLock<EntityRefCounts>>,
}
@@ -71,11 +71,12 @@ impl EntityMap {
#[track_caller]
pub fn lease<'a, T>(&mut self, model: &'a Model<T>) -> Lease<'a, T> {
self.assert_valid_context(model);
- let entity = Some(
- self.entities
- .remove(model.entity_id)
- .expect("Circular entity lease. Is the entity already being updated?"),
- );
+ let entity = Some(self.entities.remove(model.entity_id).unwrap_or_else(|| {
+ panic!(
+ "Circular entity lease of {}. Is it already being updated?",
+ std::any::type_name::<T>()
+ )
+ }));
Lease {
model,
entity,
@@ -101,7 +102,7 @@ impl EntityMap {
);
}
- pub fn take_dropped(&mut self) -> Vec<(EntityId, AnyBox)> {
+ pub fn take_dropped(&mut self) -> Vec<(EntityId, Box<dyn Any>)> {
let mut ref_counts = self.ref_counts.write();
let dropped_entity_ids = mem::take(&mut ref_counts.dropped_entity_ids);
@@ -121,7 +122,7 @@ impl EntityMap {
}
pub struct Lease<'a, T> {
- entity: Option<AnyBox>,
+ entity: Option<Box<dyn Any>>,
pub model: &'a Model<T>,
entity_type: PhantomData<T>,
}
@@ -1,8 +1,9 @@
use crate::{
div, Action, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext,
- BackgroundExecutor, Context, Div, EventEmitter, ForegroundExecutor, InputEvent, KeyDownEvent,
- Keystroke, Model, ModelContext, Render, Result, Task, TestDispatcher, TestPlatform, TestWindow,
- View, ViewContext, VisualContext, WindowContext, WindowHandle, WindowOptions,
+ BackgroundExecutor, Context, Div, Entity, EventEmitter, ForegroundExecutor, InputEvent,
+ KeyDownEvent, Keystroke, Model, ModelContext, Render, Result, Task, TestDispatcher,
+ TestPlatform, TestWindow, View, ViewContext, VisualContext, WindowContext, WindowHandle,
+ WindowOptions,
};
use anyhow::{anyhow, bail};
use futures::{Stream, StreamExt};
@@ -126,7 +127,7 @@ impl TestAppContext {
pub fn add_window<F, V>(&mut self, build_window: F) -> WindowHandle<V>
where
F: FnOnce(&mut ViewContext<V>) -> V,
- V: Render,
+ V: 'static + Render,
{
let mut cx = self.app.borrow_mut();
cx.open_window(WindowOptions::default(), |cx| cx.build_view(build_window))
@@ -143,7 +144,7 @@ impl TestAppContext {
pub fn add_window_view<F, V>(&mut self, build_window: F) -> (View<V>, &mut VisualTestContext)
where
F: FnOnce(&mut ViewContext<V>) -> V,
- V: Render,
+ V: 'static + Render,
{
let mut cx = self.app.borrow_mut();
let window = cx.open_window(WindowOptions::default(), |cx| cx.build_view(build_window));
@@ -296,21 +297,19 @@ impl TestAppContext {
.unwrap()
}
- pub fn notifications<T: 'static>(&mut self, entity: &Model<T>) -> impl Stream<Item = ()> {
+ pub fn notifications<T: 'static>(&mut self, entity: &impl Entity<T>) -> impl Stream<Item = ()> {
let (tx, rx) = futures::channel::mpsc::unbounded();
-
- entity.update(self, move |_, cx: &mut ModelContext<T>| {
+ self.update(|cx| {
cx.observe(entity, {
let tx = tx.clone();
- move |_, _, _| {
+ move |_, _| {
let _ = tx.unbounded_send(());
}
})
.detach();
-
- cx.on_release(move |_, _| tx.close_channel()).detach();
+ cx.observe_release(entity, move |_, _| tx.close_channel())
+ .detach()
});
-
rx
}
@@ -386,6 +385,32 @@ impl<T: Send> Model<T> {
}
}
+impl<V: 'static> View<V> {
+ pub fn next_notification(&self, cx: &TestAppContext) -> impl Future<Output = ()> {
+ use postage::prelude::{Sink as _, Stream as _};
+
+ let (mut tx, mut rx) = postage::mpsc::channel(1);
+ let mut cx = cx.app.app.borrow_mut();
+ let subscription = cx.observe(self, move |_, _| {
+ tx.try_send(()).ok();
+ });
+
+ let duration = if std::env::var("CI").is_ok() {
+ Duration::from_secs(5)
+ } else {
+ Duration::from_secs(1)
+ };
+
+ async move {
+ let notification = crate::util::timeout(duration, rx.recv())
+ .await
+ .expect("next notification timed out");
+ drop(subscription);
+ notification.expect("model dropped while test was waiting for its next notification")
+ }
+ }
+}
+
impl<V> View<V> {
pub fn condition<Evt>(
&self,
@@ -565,7 +590,7 @@ impl<'a> VisualContext for VisualTestContext<'a> {
build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V,
) -> Self::Result<View<V>>
where
- V: Render,
+ V: 'static + Render,
{
self.window
.update(self.cx, |_, cx| cx.replace_root_view(build_view))
@@ -579,6 +604,17 @@ impl<'a> VisualContext for VisualTestContext<'a> {
})
.unwrap()
}
+
+ fn dismiss_view<V>(&mut self, view: &View<V>) -> Self::Result<()>
+ where
+ V: crate::ManagedView,
+ {
+ self.window
+ .update(self.cx, |_, cx| {
+ view.update(cx, |_, cx| cx.emit(crate::Manager::Dismiss))
+ })
+ .unwrap()
+ }
}
impl AnyWindowHandle {
@@ -594,7 +630,7 @@ impl AnyWindowHandle {
pub struct EmptyView {}
impl Render for EmptyView {
- type Element = Div<Self>;
+ type Element = Div;
fn render(&mut self, _cx: &mut crate::ViewContext<Self>) -> Self::Element {
div()
@@ -1,231 +1,331 @@
use crate::{
AvailableSpace, BorrowWindow, Bounds, ElementId, LayoutId, Pixels, Point, Size, ViewContext,
+ WindowContext,
};
use derive_more::{Deref, DerefMut};
pub(crate) use smallvec::SmallVec;
-use std::{any::Any, fmt::Debug, mem};
+use std::{any::Any, fmt::Debug};
-pub trait Element<V: 'static> {
- type ElementState: 'static;
+pub trait Render: 'static + Sized {
+ type Element: Element + 'static;
+
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element;
+}
+
+pub trait RenderOnce: Sized {
+ type Element: Element + 'static;
fn element_id(&self) -> Option<ElementId>;
- fn layout(
- &mut self,
- view_state: &mut V,
- element_state: Option<Self::ElementState>,
- cx: &mut ViewContext<V>,
- ) -> (LayoutId, Self::ElementState);
+ fn render_once(self) -> Self::Element;
- fn paint(
- &mut self,
- bounds: Bounds<Pixels>,
- view_state: &mut V,
- element_state: &mut Self::ElementState,
- cx: &mut ViewContext<V>,
- );
+ fn render_into_any(self) -> AnyElement {
+ self.render_once().into_any()
+ }
fn draw<T, R>(
self,
origin: Point<Pixels>,
available_space: Size<T>,
- view_state: &mut V,
- cx: &mut ViewContext<V>,
- f: impl FnOnce(&Self::ElementState, &mut ViewContext<V>) -> R,
+ cx: &mut WindowContext,
+ f: impl FnOnce(&mut <Self::Element as Element>::State, &mut WindowContext) -> R,
) -> R
where
- Self: Sized,
T: Clone + Default + Debug + Into<AvailableSpace>,
{
- let mut element = RenderedElement {
- element: self,
- phase: ElementRenderPhase::Start,
+ let element = self.render_once();
+ let element_id = element.element_id();
+ let element = DrawableElement {
+ element: Some(element),
+ phase: ElementDrawPhase::Start,
};
- element.draw(origin, available_space.map(Into::into), view_state, cx);
- if let ElementRenderPhase::Painted { frame_state } = &element.phase {
- if let Some(frame_state) = frame_state.as_ref() {
- f(&frame_state, cx)
+
+ let frame_state =
+ DrawableElement::draw(element, origin, available_space.map(Into::into), cx);
+
+ if let Some(mut frame_state) = frame_state {
+ f(&mut frame_state, cx)
+ } else {
+ cx.with_element_state(element_id.unwrap(), |element_state, cx| {
+ let mut element_state = element_state.unwrap();
+ let result = f(&mut element_state, cx);
+ (result, element_state)
+ })
+ }
+ }
+
+ fn map<U>(self, f: impl FnOnce(Self) -> U) -> U
+ where
+ Self: Sized,
+ U: RenderOnce,
+ {
+ f(self)
+ }
+
+ fn when(self, condition: bool, then: impl FnOnce(Self) -> Self) -> Self
+ where
+ Self: Sized,
+ {
+ self.map(|this| if condition { then(this) } else { this })
+ }
+
+ fn when_some<T>(self, option: Option<T>, then: impl FnOnce(Self, T) -> Self) -> Self
+ where
+ Self: Sized,
+ {
+ self.map(|this| {
+ if let Some(value) = option {
+ then(this, value)
} else {
- let element_id = element
- .element
- .element_id()
- .expect("we either have some frame_state or some element_id");
- cx.with_element_state(element_id, |element_state, cx| {
- let element_state = element_state.unwrap();
- let result = f(&element_state, cx);
- (result, element_state)
- })
+ this
}
- } else {
- unreachable!()
+ })
+ }
+}
+
+pub trait Element: 'static + RenderOnce {
+ type State: 'static;
+
+ fn layout(
+ &mut self,
+ state: Option<Self::State>,
+ cx: &mut WindowContext,
+ ) -> (LayoutId, Self::State);
+
+ fn paint(self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext);
+
+ fn into_any(self) -> AnyElement {
+ AnyElement::new(self)
+ }
+}
+
+pub trait Component: 'static {
+ type Rendered: RenderOnce;
+
+ fn render(self, cx: &mut WindowContext) -> Self::Rendered;
+}
+
+pub struct CompositeElement<C> {
+ component: Option<C>,
+}
+
+pub struct CompositeElementState<C: Component> {
+ rendered_element: Option<<C::Rendered as RenderOnce>::Element>,
+ rendered_element_state: <<C::Rendered as RenderOnce>::Element as Element>::State,
+}
+
+impl<C> CompositeElement<C> {
+ pub fn new(component: C) -> Self {
+ CompositeElement {
+ component: Some(component),
}
}
}
+impl<C: Component> Element for CompositeElement<C> {
+ type State = CompositeElementState<C>;
+
+ fn layout(
+ &mut self,
+ state: Option<Self::State>,
+ cx: &mut WindowContext,
+ ) -> (LayoutId, Self::State) {
+ let mut element = self.component.take().unwrap().render(cx).render_once();
+ let (layout_id, state) = element.layout(state.map(|s| s.rendered_element_state), cx);
+ let state = CompositeElementState {
+ rendered_element: Some(element),
+ rendered_element_state: state,
+ };
+ (layout_id, state)
+ }
+
+ fn paint(self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
+ state
+ .rendered_element
+ .take()
+ .unwrap()
+ .paint(bounds, &mut state.rendered_element_state, cx);
+ }
+}
+
+impl<C: Component> RenderOnce for CompositeElement<C> {
+ type Element = Self;
+
+ fn element_id(&self) -> Option<ElementId> {
+ None
+ }
+
+ fn render_once(self) -> Self::Element {
+ self
+ }
+}
+
#[derive(Deref, DerefMut, Default, Clone, Debug, Eq, PartialEq, Hash)]
pub struct GlobalElementId(SmallVec<[ElementId; 32]>);
-pub trait ParentComponent<V: 'static> {
- fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]>;
+pub trait ParentElement {
+ fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]>;
- fn child(mut self, child: impl Component<V>) -> Self
+ fn child(mut self, child: impl RenderOnce) -> Self
where
Self: Sized,
{
- self.children_mut().push(child.render());
+ self.children_mut().push(child.render_once().into_any());
self
}
- fn children(mut self, iter: impl IntoIterator<Item = impl Component<V>>) -> Self
+ fn children(mut self, children: impl IntoIterator<Item = impl RenderOnce>) -> Self
where
Self: Sized,
{
- self.children_mut()
- .extend(iter.into_iter().map(|item| item.render()));
+ self.children_mut().extend(
+ children
+ .into_iter()
+ .map(|child| child.render_once().into_any()),
+ );
self
}
}
-trait ElementObject<V> {
- fn layout(&mut self, view_state: &mut V, cx: &mut ViewContext<V>) -> LayoutId;
- fn paint(&mut self, view_state: &mut V, cx: &mut ViewContext<V>);
+trait ElementObject {
+ fn element_id(&self) -> Option<ElementId>;
+
+ fn layout(&mut self, cx: &mut WindowContext) -> LayoutId;
+
+ fn paint(&mut self, cx: &mut WindowContext);
+
fn measure(
&mut self,
available_space: Size<AvailableSpace>,
- view_state: &mut V,
- cx: &mut ViewContext<V>,
+ cx: &mut WindowContext,
) -> Size<Pixels>;
+
fn draw(
&mut self,
origin: Point<Pixels>,
available_space: Size<AvailableSpace>,
- view_state: &mut V,
- cx: &mut ViewContext<V>,
+ cx: &mut WindowContext,
);
}
-struct RenderedElement<V: 'static, E: Element<V>> {
- element: E,
- phase: ElementRenderPhase<E::ElementState>,
+pub struct DrawableElement<E: Element> {
+ element: Option<E>,
+ phase: ElementDrawPhase<E::State>,
}
#[derive(Default)]
-enum ElementRenderPhase<V> {
+enum ElementDrawPhase<S> {
#[default]
Start,
LayoutRequested {
layout_id: LayoutId,
- frame_state: Option<V>,
+ frame_state: Option<S>,
},
LayoutComputed {
layout_id: LayoutId,
available_space: Size<AvailableSpace>,
- frame_state: Option<V>,
- },
- Painted {
- frame_state: Option<V>,
+ frame_state: Option<S>,
},
}
-/// Internal struct that wraps an element to store Layout and ElementState after the element is rendered.
-/// It's allocated as a trait object to erase the element type and wrapped in AnyElement<E::State> for
-/// improved usability.
-impl<V, E: Element<V>> RenderedElement<V, E> {
+/// A wrapper around an implementer of [Element] that allows it to be drawn in a window.
+impl<E: Element> DrawableElement<E> {
fn new(element: E) -> Self {
- RenderedElement {
- element,
- phase: ElementRenderPhase::Start,
+ DrawableElement {
+ element: Some(element),
+ phase: ElementDrawPhase::Start,
}
}
-}
-impl<V, E> ElementObject<V> for RenderedElement<V, E>
-where
- E: Element<V>,
- E::ElementState: 'static,
-{
- fn layout(&mut self, state: &mut V, cx: &mut ViewContext<V>) -> LayoutId {
- let (layout_id, frame_state) = match mem::take(&mut self.phase) {
- ElementRenderPhase::Start => {
- if let Some(id) = self.element.element_id() {
- let layout_id = cx.with_element_state(id, |element_state, cx| {
- self.element.layout(state, element_state, cx)
- });
- (layout_id, None)
- } else {
- let (layout_id, frame_state) = self.element.layout(state, None, cx);
- (layout_id, Some(frame_state))
- }
- }
- ElementRenderPhase::LayoutRequested { .. }
- | ElementRenderPhase::LayoutComputed { .. }
- | ElementRenderPhase::Painted { .. } => {
- panic!("element rendered twice")
- }
+ fn element_id(&self) -> Option<ElementId> {
+ self.element.as_ref()?.element_id()
+ }
+
+ fn layout(&mut self, cx: &mut WindowContext) -> LayoutId {
+ let (layout_id, frame_state) = if let Some(id) = self.element.as_ref().unwrap().element_id()
+ {
+ let layout_id = cx.with_element_state(id, |element_state, cx| {
+ self.element.as_mut().unwrap().layout(element_state, cx)
+ });
+ (layout_id, None)
+ } else {
+ let (layout_id, frame_state) = self.element.as_mut().unwrap().layout(None, cx);
+ (layout_id, Some(frame_state))
};
- self.phase = ElementRenderPhase::LayoutRequested {
+ self.phase = ElementDrawPhase::LayoutRequested {
layout_id,
frame_state,
};
layout_id
}
- fn paint(&mut self, view_state: &mut V, cx: &mut ViewContext<V>) {
- self.phase = match mem::take(&mut self.phase) {
- ElementRenderPhase::LayoutRequested {
+ fn paint(mut self, cx: &mut WindowContext) -> Option<E::State> {
+ match self.phase {
+ ElementDrawPhase::LayoutRequested {
layout_id,
- mut frame_state,
+ frame_state,
}
- | ElementRenderPhase::LayoutComputed {
+ | ElementDrawPhase::LayoutComputed {
layout_id,
- mut frame_state,
+ frame_state,
..
} => {
let bounds = cx.layout_bounds(layout_id);
- if let Some(id) = self.element.element_id() {
- cx.with_element_state(id, |element_state, cx| {
+
+ if let Some(mut frame_state) = frame_state {
+ self.element
+ .take()
+ .unwrap()
+ .paint(bounds, &mut frame_state, cx);
+ Some(frame_state)
+ } else {
+ let element_id = self
+ .element
+ .as_ref()
+ .unwrap()
+ .element_id()
+ .expect("if we don't have frame state, we should have element state");
+ cx.with_element_state(element_id, |element_state, cx| {
let mut element_state = element_state.unwrap();
self.element
- .paint(bounds, view_state, &mut element_state, cx);
+ .take()
+ .unwrap()
+ .paint(bounds, &mut element_state, cx);
((), element_state)
});
- } else {
- self.element
- .paint(bounds, view_state, frame_state.as_mut().unwrap(), cx);
+ None
}
- ElementRenderPhase::Painted { frame_state }
}
_ => panic!("must call layout before paint"),
- };
+ }
}
fn measure(
&mut self,
available_space: Size<AvailableSpace>,
- view_state: &mut V,
- cx: &mut ViewContext<V>,
+ cx: &mut WindowContext,
) -> Size<Pixels> {
- if matches!(&self.phase, ElementRenderPhase::Start) {
- self.layout(view_state, cx);
+ if matches!(&self.phase, ElementDrawPhase::Start) {
+ self.layout(cx);
}
let layout_id = match &mut self.phase {
- ElementRenderPhase::LayoutRequested {
+ ElementDrawPhase::LayoutRequested {
layout_id,
frame_state,
} => {
cx.compute_layout(*layout_id, available_space);
let layout_id = *layout_id;
- self.phase = ElementRenderPhase::LayoutComputed {
+ self.phase = ElementDrawPhase::LayoutComputed {
layout_id,
available_space,
frame_state: frame_state.take(),
};
layout_id
}
- ElementRenderPhase::LayoutComputed {
+ ElementDrawPhase::LayoutComputed {
layout_id,
available_space: prev_available_space,
..
@@ -243,150 +343,203 @@ where
}
fn draw(
- &mut self,
+ mut self,
origin: Point<Pixels>,
available_space: Size<AvailableSpace>,
- view_state: &mut V,
- cx: &mut ViewContext<V>,
- ) {
- self.measure(available_space, view_state, cx);
- cx.with_absolute_element_offset(origin, |cx| self.paint(view_state, cx))
+ cx: &mut WindowContext,
+ ) -> Option<E::State> {
+ self.measure(available_space, cx);
+ cx.with_absolute_element_offset(origin, |cx| self.paint(cx))
}
}
-pub struct AnyElement<V>(Box<dyn ElementObject<V>>);
+// impl<V: 'static, E: Element> Element for DrawableElement<V, E> {
+// type State = <E::Element as Element>::State;
-impl<V> AnyElement<V> {
- pub fn new<E>(element: E) -> Self
- where
- V: 'static,
- E: 'static + Element<V>,
- E::ElementState: Any,
- {
- AnyElement(Box::new(RenderedElement::new(element)))
+// fn layout(
+// &mut self,
+// element_state: Option<Self::State>,
+// cx: &mut WindowContext,
+// ) -> (LayoutId, Self::State) {
+
+// }
+
+// fn paint(
+// self,
+// bounds: Bounds<Pixels>,
+// element_state: &mut Self::State,
+// cx: &mut WindowContext,
+// ) {
+// todo!()
+// }
+// }
+
+// impl<V: 'static, E: 'static + Element> RenderOnce for DrawableElement<V, E> {
+// type Element = Self;
+
+// fn element_id(&self) -> Option<ElementId> {
+// self.element.as_ref()?.element_id()
+// }
+
+// fn render_once(self) -> Self::Element {
+// self
+// }
+// }
+
+impl<E> ElementObject for Option<DrawableElement<E>>
+where
+ E: Element,
+ E::State: 'static,
+{
+ fn element_id(&self) -> Option<ElementId> {
+ self.as_ref().unwrap().element_id()
}
- pub fn layout(&mut self, view_state: &mut V, cx: &mut ViewContext<V>) -> LayoutId {
- self.0.layout(view_state, cx)
+ fn layout(&mut self, cx: &mut WindowContext) -> LayoutId {
+ DrawableElement::layout(self.as_mut().unwrap(), cx)
}
- pub fn paint(&mut self, view_state: &mut V, cx: &mut ViewContext<V>) {
- self.0.paint(view_state, cx)
+ fn paint(&mut self, cx: &mut WindowContext) {
+ DrawableElement::paint(self.take().unwrap(), cx);
}
- /// Initializes this element and performs layout within the given available space to determine its size.
- pub fn measure(
+ fn measure(
&mut self,
available_space: Size<AvailableSpace>,
- view_state: &mut V,
- cx: &mut ViewContext<V>,
+ cx: &mut WindowContext,
) -> Size<Pixels> {
- self.0.measure(available_space, view_state, cx)
+ DrawableElement::measure(self.as_mut().unwrap(), available_space, cx)
}
- /// Initializes this element and performs layout in the available space, then paints it at the given origin.
- pub fn draw(
+ fn draw(
&mut self,
origin: Point<Pixels>,
available_space: Size<AvailableSpace>,
- view_state: &mut V,
- cx: &mut ViewContext<V>,
+ cx: &mut WindowContext,
) {
- self.0.draw(origin, available_space, view_state, cx)
+ DrawableElement::draw(self.take().unwrap(), origin, available_space, cx);
}
}
-pub trait Component<V> {
- fn render(self) -> AnyElement<V>;
+pub struct AnyElement(Box<dyn ElementObject>);
- fn map<U>(self, f: impl FnOnce(Self) -> U) -> U
+impl AnyElement {
+ pub fn new<E>(element: E) -> Self
where
- Self: Sized,
- U: Component<V>,
+ E: 'static + Element,
+ E::State: Any,
{
- f(self)
+ AnyElement(Box::new(Some(DrawableElement::new(element))) as Box<dyn ElementObject>)
}
- fn when(self, condition: bool, then: impl FnOnce(Self) -> Self) -> Self
- where
- Self: Sized,
- {
- self.map(|this| if condition { then(this) } else { this })
+ pub fn element_id(&self) -> Option<ElementId> {
+ self.0.element_id()
}
- fn when_some<T>(self, option: Option<T>, then: impl FnOnce(Self, T) -> Self) -> Self
- where
- Self: Sized,
- {
- self.map(|this| {
- if let Some(value) = option {
- then(this, value)
- } else {
- this
- }
- })
+ pub fn layout(&mut self, cx: &mut WindowContext) -> LayoutId {
+ self.0.layout(cx)
}
-}
-impl<V> Component<V> for AnyElement<V> {
- fn render(self) -> AnyElement<V> {
- self
+ pub fn paint(mut self, cx: &mut WindowContext) {
+ self.0.paint(cx)
}
-}
-impl<V, E, F> Element<V> for Option<F>
-where
- V: 'static,
- E: 'static + Component<V>,
- F: FnOnce(&mut V, &mut ViewContext<'_, V>) -> E + 'static,
-{
- type ElementState = AnyElement<V>;
+ /// Initializes this element and performs layout within the given available space to determine its size.
+ pub fn measure(
+ &mut self,
+ available_space: Size<AvailableSpace>,
+ cx: &mut WindowContext,
+ ) -> Size<Pixels> {
+ self.0.measure(available_space, cx)
+ }
- fn element_id(&self) -> Option<ElementId> {
- None
+ /// Initializes this element and performs layout in the available space, then paints it at the given origin.
+ pub fn draw(
+ mut self,
+ origin: Point<Pixels>,
+ available_space: Size<AvailableSpace>,
+ cx: &mut WindowContext,
+ ) {
+ self.0.draw(origin, available_space, cx)
}
+ /// Converts this `AnyElement` into a trait object that can be stored and manipulated.
+ pub fn into_any(self) -> AnyElement {
+ AnyElement::new(self)
+ }
+}
+
+impl Element for AnyElement {
+ type State = ();
+
fn layout(
&mut self,
- view_state: &mut V,
- _: Option<Self::ElementState>,
- cx: &mut ViewContext<V>,
- ) -> (LayoutId, Self::ElementState) {
- let render = self.take().unwrap();
- let mut rendered_element = (render)(view_state, cx).render();
- let layout_id = rendered_element.layout(view_state, cx);
- (layout_id, rendered_element)
+ _: Option<Self::State>,
+ cx: &mut WindowContext,
+ ) -> (LayoutId, Self::State) {
+ let layout_id = self.layout(cx);
+ (layout_id, ())
}
- fn paint(
- &mut self,
- _bounds: Bounds<Pixels>,
- view_state: &mut V,
- rendered_element: &mut Self::ElementState,
- cx: &mut ViewContext<V>,
- ) {
- rendered_element.paint(view_state, cx)
+ fn paint(self, _: Bounds<Pixels>, _: &mut Self::State, cx: &mut WindowContext) {
+ self.paint(cx);
}
}
-impl<V, E, F> Component<V> for Option<F>
-where
- V: 'static,
- E: 'static + Component<V>,
- F: FnOnce(&mut V, &mut ViewContext<'_, V>) -> E + 'static,
-{
- fn render(self) -> AnyElement<V> {
- AnyElement::new(self)
+impl RenderOnce for AnyElement {
+ type Element = Self;
+
+ fn element_id(&self) -> Option<ElementId> {
+ AnyElement::element_id(self)
}
-}
-impl<V, E, F> Component<V> for F
-where
- V: 'static,
- E: 'static + Component<V>,
- F: FnOnce(&mut V, &mut ViewContext<'_, V>) -> E + 'static,
-{
- fn render(self) -> AnyElement<V> {
- AnyElement::new(Some(self))
+ fn render_once(self) -> Self::Element {
+ self
}
}
+
+// impl<V, E, F> Element for Option<F>
+// where
+// V: 'static,
+// E: Element,
+// F: FnOnce(&mut V, &mut WindowContext<'_, V>) -> E + 'static,
+// {
+// type State = Option<AnyElement>;
+
+// fn element_id(&self) -> Option<ElementId> {
+// None
+// }
+
+// fn layout(
+// &mut self,
+// _: Option<Self::State>,
+// cx: &mut WindowContext,
+// ) -> (LayoutId, Self::State) {
+// let render = self.take().unwrap();
+// let mut element = (render)(view_state, cx).into_any();
+// let layout_id = element.layout(view_state, cx);
+// (layout_id, Some(element))
+// }
+
+// fn paint(
+// self,
+// _bounds: Bounds<Pixels>,
+// rendered_element: &mut Self::State,
+// cx: &mut WindowContext,
+// ) {
+// rendered_element.take().unwrap().paint(view_state, cx);
+// }
+// }
+
+// impl<V, E, F> RenderOnce for Option<F>
+// where
+// V: 'static,
+// E: Element,
+// F: FnOnce(&mut V, &mut WindowContext) -> E + 'static,
+// {
+// type Element = Self;
+
+// fn render(self) -> Self::Element {
+// self
+// }
+// }
@@ -1,9 +1,9 @@
use crate::{
point, px, Action, AnyDrag, AnyElement, AnyTooltip, AnyView, AppContext, BorrowAppContext,
- BorrowWindow, Bounds, ClickEvent, Component, DispatchPhase, Element, ElementId, FocusEvent,
- FocusHandle, KeyContext, KeyDownEvent, KeyUpEvent, LayoutId, MouseButton, MouseDownEvent,
- MouseMoveEvent, MouseUpEvent, ParentComponent, Pixels, Point, Render, ScrollWheelEvent,
- SharedString, Size, Style, StyleRefinement, Styled, Task, View, ViewContext, Visibility,
+ BorrowWindow, Bounds, ClickEvent, DispatchPhase, Element, ElementId, FocusEvent, FocusHandle,
+ KeyContext, KeyDownEvent, KeyUpEvent, LayoutId, MouseButton, MouseDownEvent, MouseMoveEvent,
+ MouseUpEvent, ParentElement, Pixels, Point, Render, RenderOnce, ScrollWheelEvent, SharedString,
+ Size, Style, StyleRefinement, Styled, Task, View, Visibility, WindowContext,
};
use collections::HashMap;
use refineable::Refineable;
@@ -12,7 +12,6 @@ use std::{
any::{Any, TypeId},
cell::RefCell,
fmt::Debug,
- marker::PhantomData,
mem,
rc::Rc,
time::Duration,
@@ -28,30 +27,24 @@ pub struct GroupStyle {
pub style: StyleRefinement,
}
-pub trait InteractiveComponent<V: 'static>: Sized + Element<V> {
- fn interactivity(&mut self) -> &mut Interactivity<V>;
+pub trait InteractiveElement: Sized + Element {
+ fn interactivity(&mut self) -> &mut Interactivity;
fn group(mut self, group: impl Into<SharedString>) -> Self {
self.interactivity().group = Some(group.into());
self
}
- fn id(mut self, id: impl Into<ElementId>) -> Stateful<V, Self> {
+ fn id(mut self, id: impl Into<ElementId>) -> Stateful<Self> {
self.interactivity().element_id = Some(id.into());
- Stateful {
- element: self,
- view_type: PhantomData,
- }
+ Stateful { element: self }
}
- fn track_focus(mut self, focus_handle: &FocusHandle) -> Focusable<V, Self> {
+ fn track_focus(mut self, focus_handle: &FocusHandle) -> Focusable<Self> {
self.interactivity().focusable = true;
self.interactivity().tracked_focus_handle = Some(focus_handle.clone());
- Focusable {
- element: self,
- view_type: PhantomData,
- }
+ Focusable { element: self }
}
fn key_context<C, E>(mut self, key_context: C) -> Self
@@ -85,15 +78,15 @@ pub trait InteractiveComponent<V: 'static>: Sized + Element<V> {
fn on_mouse_down(
mut self,
button: MouseButton,
- handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext<V>) + 'static,
+ listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
) -> Self {
self.interactivity().mouse_down_listeners.push(Box::new(
- move |view, event, bounds, phase, cx| {
+ move |event, bounds, phase, cx| {
if phase == DispatchPhase::Bubble
&& event.button == button
&& bounds.contains_point(&event.position)
{
- handler(view, event, cx)
+ (listener)(event, cx)
}
},
));
@@ -102,12 +95,12 @@ pub trait InteractiveComponent<V: 'static>: Sized + Element<V> {
fn on_any_mouse_down(
mut self,
- handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext<V>) + 'static,
+ listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
) -> Self {
self.interactivity().mouse_down_listeners.push(Box::new(
- move |view, event, bounds, phase, cx| {
+ move |event, bounds, phase, cx| {
if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
- handler(view, event, cx)
+ (listener)(event, cx)
}
},
));
@@ -117,43 +110,43 @@ pub trait InteractiveComponent<V: 'static>: Sized + Element<V> {
fn on_mouse_up(
mut self,
button: MouseButton,
- handler: impl Fn(&mut V, &MouseUpEvent, &mut ViewContext<V>) + 'static,
+ listener: impl Fn(&MouseUpEvent, &mut WindowContext) + 'static,
) -> Self {
- self.interactivity().mouse_up_listeners.push(Box::new(
- move |view, event, bounds, phase, cx| {
+ self.interactivity()
+ .mouse_up_listeners
+ .push(Box::new(move |event, bounds, phase, cx| {
if phase == DispatchPhase::Bubble
&& event.button == button
&& bounds.contains_point(&event.position)
{
- handler(view, event, cx)
+ (listener)(event, cx)
}
- },
- ));
+ }));
self
}
fn on_any_mouse_up(
mut self,
- handler: impl Fn(&mut V, &MouseUpEvent, &mut ViewContext<V>) + 'static,
+ listener: impl Fn(&MouseUpEvent, &mut WindowContext) + 'static,
) -> Self {
- self.interactivity().mouse_up_listeners.push(Box::new(
- move |view, event, bounds, phase, cx| {
+ self.interactivity()
+ .mouse_up_listeners
+ .push(Box::new(move |event, bounds, phase, cx| {
if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
- handler(view, event, cx)
+ (listener)(event, cx)
}
- },
- ));
+ }));
self
}
fn on_mouse_down_out(
mut self,
- handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext<V>) + 'static,
+ listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
) -> Self {
self.interactivity().mouse_down_listeners.push(Box::new(
- move |view, event, bounds, phase, cx| {
+ move |event, bounds, phase, cx| {
if phase == DispatchPhase::Capture && !bounds.contains_point(&event.position) {
- handler(view, event, cx)
+ (listener)(event, cx)
}
},
));
@@ -163,29 +156,29 @@ pub trait InteractiveComponent<V: 'static>: Sized + Element<V> {
fn on_mouse_up_out(
mut self,
button: MouseButton,
- handler: impl Fn(&mut V, &MouseUpEvent, &mut ViewContext<V>) + 'static,
+ listener: impl Fn(&MouseUpEvent, &mut WindowContext) + 'static,
) -> Self {
- self.interactivity().mouse_up_listeners.push(Box::new(
- move |view, event, bounds, phase, cx| {
+ self.interactivity()
+ .mouse_up_listeners
+ .push(Box::new(move |event, bounds, phase, cx| {
if phase == DispatchPhase::Capture
&& event.button == button
&& !bounds.contains_point(&event.position)
{
- handler(view, event, cx);
+ (listener)(event, cx);
}
- },
- ));
+ }));
self
}
fn on_mouse_move(
mut self,
- handler: impl Fn(&mut V, &MouseMoveEvent, &mut ViewContext<V>) + 'static,
+ listener: impl Fn(&MouseMoveEvent, &mut WindowContext) + 'static,
) -> Self {
self.interactivity().mouse_move_listeners.push(Box::new(
- move |view, event, bounds, phase, cx| {
+ move |event, bounds, phase, cx| {
if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
- handler(view, event, cx);
+ (listener)(event, cx);
}
},
));
@@ -194,29 +187,29 @@ pub trait InteractiveComponent<V: 'static>: Sized + Element<V> {
fn on_scroll_wheel(
mut self,
- handler: impl Fn(&mut V, &ScrollWheelEvent, &mut ViewContext<V>) + 'static,
+ listener: impl Fn(&ScrollWheelEvent, &mut WindowContext) + 'static,
) -> Self {
self.interactivity().scroll_wheel_listeners.push(Box::new(
- move |view, event, bounds, phase, cx| {
+ move |event, bounds, phase, cx| {
if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
- handler(view, event, cx);
+ (listener)(event, cx);
}
},
));
self
}
- /// Capture the given action, fires during the capture phase
+ /// Capture the given action, before normal action dispatch can fire
fn capture_action<A: Action>(
mut self,
- listener: impl Fn(&mut V, &A, &mut ViewContext<V>) + 'static,
+ listener: impl Fn(&A, &mut WindowContext) + 'static,
) -> Self {
self.interactivity().action_listeners.push((
TypeId::of::<A>(),
- Box::new(move |view, action, phase, cx| {
+ Box::new(move |action, phase, cx| {
let action = action.downcast_ref().unwrap();
if phase == DispatchPhase::Capture {
- listener(view, action, cx)
+ (listener)(action, cx)
}
}),
));
@@ -224,10 +217,7 @@ pub trait InteractiveComponent<V: 'static>: Sized + Element<V> {
}
/// Add a listener for the given action, fires during the bubble event phase
- fn on_action<A: Action>(
- mut self,
- listener: impl Fn(&mut V, &A, &mut ViewContext<V>) + 'static,
- ) -> Self {
+ fn on_action<A: Action>(mut self, listener: impl Fn(&A, &mut WindowContext) + 'static) -> Self {
// NOTE: this debug assert has the side-effect of working around
// a bug where a crate consisting only of action definitions does
// not register the actions in debug builds:
@@ -244,10 +234,10 @@ pub trait InteractiveComponent<V: 'static>: Sized + Element<V> {
// );
self.interactivity().action_listeners.push((
TypeId::of::<A>(),
- Box::new(move |view, action, phase, cx| {
+ Box::new(move |action, phase, cx| {
let action = action.downcast_ref().unwrap();
if phase == DispatchPhase::Bubble {
- listener(view, action, cx)
+ (listener)(action, cx)
}
}),
));
@@ -256,24 +246,53 @@ pub trait InteractiveComponent<V: 'static>: Sized + Element<V> {
fn on_key_down(
mut self,
- listener: impl Fn(&mut V, &KeyDownEvent, DispatchPhase, &mut ViewContext<V>) + 'static,
+ listener: impl Fn(&KeyDownEvent, &mut WindowContext) + 'static,
) -> Self {
self.interactivity()
.key_down_listeners
- .push(Box::new(move |view, event, phase, cx| {
- listener(view, event, phase, cx)
+ .push(Box::new(move |event, phase, cx| {
+ if phase == DispatchPhase::Bubble {
+ (listener)(event, cx)
+ }
}));
self
}
- fn on_key_up(
+ fn capture_key_down(
mut self,
- listener: impl Fn(&mut V, &KeyUpEvent, DispatchPhase, &mut ViewContext<V>) + 'static,
+ listener: impl Fn(&KeyDownEvent, &mut WindowContext) + 'static,
) -> Self {
+ self.interactivity()
+ .key_down_listeners
+ .push(Box::new(move |event, phase, cx| {
+ if phase == DispatchPhase::Capture {
+ listener(event, cx)
+ }
+ }));
+ self
+ }
+
+ fn on_key_up(mut self, listener: impl Fn(&KeyUpEvent, &mut WindowContext) + 'static) -> Self {
self.interactivity()
.key_up_listeners
- .push(Box::new(move |view, event, phase, cx| {
- listener(view, event, phase, cx)
+ .push(Box::new(move |event, phase, cx| {
+ if phase == DispatchPhase::Bubble {
+ listener(event, cx)
+ }
+ }));
+ self
+ }
+
+ fn capture_key_up(
+ mut self,
+ listener: impl Fn(&KeyUpEvent, &mut WindowContext) + 'static,
+ ) -> Self {
+ self.interactivity()
+ .key_up_listeners
+ .push(Box::new(move |event, phase, cx| {
+ if phase == DispatchPhase::Capture {
+ listener(event, cx)
+ }
}));
self
}
@@ -302,25 +321,22 @@ pub trait InteractiveComponent<V: 'static>: Sized + Element<V> {
fn on_drop<W: 'static>(
mut self,
- listener: impl Fn(&mut V, View<W>, &mut ViewContext<V>) + 'static,
+ listener: impl Fn(&View<W>, &mut WindowContext) + 'static,
) -> Self {
self.interactivity().drop_listeners.push((
TypeId::of::<W>(),
- Box::new(move |view, dragged_view, cx| {
- listener(view, dragged_view.downcast().unwrap(), cx);
+ Box::new(move |dragged_view, cx| {
+ listener(&dragged_view.downcast().unwrap(), cx);
}),
));
self
}
}
-pub trait StatefulInteractiveComponent<V: 'static, E: Element<V>>: InteractiveComponent<V> {
- fn focusable(mut self) -> Focusable<V, Self> {
+pub trait StatefulInteractiveElement: InteractiveElement {
+ fn focusable(mut self) -> Focusable<Self> {
self.interactivity().focusable = true;
- Focusable {
- element: self,
- view_type: PhantomData,
- }
+ Focusable { element: self }
}
fn overflow_scroll(mut self) -> Self {
@@ -362,23 +378,17 @@ pub trait StatefulInteractiveComponent<V: 'static, E: Element<V>>: InteractiveCo
self
}
- fn on_click(
- mut self,
- listener: impl Fn(&mut V, &ClickEvent, &mut ViewContext<V>) + 'static,
- ) -> Self
+ fn on_click(mut self, listener: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self
where
Self: Sized,
{
self.interactivity()
.click_listeners
- .push(Box::new(move |view, event, cx| listener(view, event, cx)));
+ .push(Box::new(move |event, cx| listener(event, cx)));
self
}
- fn on_drag<W>(
- mut self,
- listener: impl Fn(&mut V, &mut ViewContext<V>) -> View<W> + 'static,
- ) -> Self
+ fn on_drag<W>(mut self, listener: impl Fn(&mut WindowContext) -> View<W> + 'static) -> Self
where
Self: Sized,
W: 'static + Render,
@@ -387,15 +397,14 @@ pub trait StatefulInteractiveComponent<V: 'static, E: Element<V>>: InteractiveCo
self.interactivity().drag_listener.is_none(),
"calling on_drag more than once on the same element is not supported"
);
- self.interactivity().drag_listener =
- Some(Box::new(move |view_state, cursor_offset, cx| AnyDrag {
- view: listener(view_state, cx).into(),
- cursor_offset,
- }));
+ self.interactivity().drag_listener = Some(Box::new(move |cursor_offset, cx| AnyDrag {
+ view: listener(cx).into(),
+ cursor_offset,
+ }));
self
}
- fn on_hover(mut self, listener: impl 'static + Fn(&mut V, bool, &mut ViewContext<V>)) -> Self
+ fn on_hover(mut self, listener: impl Fn(&bool, &mut WindowContext) + 'static) -> Self
where
Self: Sized,
{
@@ -407,10 +416,7 @@ pub trait StatefulInteractiveComponent<V: 'static, E: Element<V>>: InteractiveCo
self
}
- fn tooltip(
- mut self,
- build_tooltip: impl Fn(&mut V, &mut ViewContext<V>) -> AnyView + 'static,
- ) -> Self
+ fn tooltip(mut self, build_tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self
where
Self: Sized,
{
@@ -418,14 +424,13 @@ pub trait StatefulInteractiveComponent<V: 'static, E: Element<V>>: InteractiveCo
self.interactivity().tooltip_builder.is_none(),
"calling tooltip more than once on the same element is not supported"
);
- self.interactivity().tooltip_builder =
- Some(Rc::new(move |view_state, cx| build_tooltip(view_state, cx)));
+ self.interactivity().tooltip_builder = Some(Rc::new(build_tooltip));
self
}
}
-pub trait FocusableComponent<V: 'static>: InteractiveComponent<V> {
+pub trait FocusableElement: InteractiveElement {
fn focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
where
Self: Sized,
@@ -442,49 +447,41 @@ pub trait FocusableComponent<V: 'static>: InteractiveComponent<V> {
self
}
- fn on_focus(
- mut self,
- listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + 'static,
- ) -> Self
+ fn on_focus(mut self, listener: impl Fn(&FocusEvent, &mut WindowContext) + 'static) -> Self
where
Self: Sized,
{
- self.interactivity().focus_listeners.push(Box::new(
- move |view, focus_handle, event, cx| {
+ self.interactivity()
+ .focus_listeners
+ .push(Box::new(move |focus_handle, event, cx| {
if event.focused.as_ref() == Some(focus_handle) {
- listener(view, event, cx)
+ listener(event, cx)
}
- },
- ));
+ }));
self
}
- fn on_blur(
- mut self,
- listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + 'static,
- ) -> Self
+ fn on_blur(mut self, listener: impl Fn(&FocusEvent, &mut WindowContext) + 'static) -> Self
where
Self: Sized,
{
- self.interactivity().focus_listeners.push(Box::new(
- move |view, focus_handle, event, cx| {
+ self.interactivity()
+ .focus_listeners
+ .push(Box::new(move |focus_handle, event, cx| {
if event.blurred.as_ref() == Some(focus_handle) {
- listener(view, event, cx)
+ listener(event, cx)
}
- },
- ));
+ }));
self
}
- fn on_focus_in(
- mut self,
- listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + 'static,
- ) -> Self
+ fn on_focus_in(mut self, listener: impl Fn(&FocusEvent, &mut WindowContext) + 'static) -> Self
where
Self: Sized,
{
- self.interactivity().focus_listeners.push(Box::new(
- move |view, focus_handle, event, cx| {
+ self.interactivity()
+ .focus_listeners
+ .push(Box::new(move |focus_handle, event, cx| {
let descendant_blurred = event
.blurred
.as_ref()
@@ -495,22 +492,19 @@ pub trait FocusableComponent<V: 'static>: InteractiveComponent<V> {
.map_or(false, |focused| focus_handle.contains(focused, cx));
if !descendant_blurred && descendant_focused {
- listener(view, event, cx)
+ listener(event, cx)
}
- },
- ));
+ }));
self
}
- fn on_focus_out(
- mut self,
- listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + 'static,
- ) -> Self
+ fn on_focus_out(mut self, listener: impl Fn(&FocusEvent, &mut WindowContext) + 'static) -> Self
where
Self: Sized,
{
- self.interactivity().focus_listeners.push(Box::new(
- move |view, focus_handle, event, cx| {
+ self.interactivity()
+ .focus_listeners
+ .push(Box::new(move |focus_handle, event, cx| {
let descendant_blurred = event
.blurred
.as_ref()
@@ -520,98 +514,80 @@ pub trait FocusableComponent<V: 'static>: InteractiveComponent<V> {
.as_ref()
.map_or(false, |focused| focus_handle.contains(focused, cx));
if descendant_blurred && !descendant_focused {
- listener(view, event, cx)
+ listener(event, cx)
}
- },
- ));
+ }));
self
}
}
-pub type FocusListeners<V> = SmallVec<[FocusListener<V>; 2]>;
-
-pub type FocusListener<V> =
- Box<dyn Fn(&mut V, &FocusHandle, &FocusEvent, &mut ViewContext<V>) + 'static>;
+pub type FocusListeners = SmallVec<[FocusListener; 2]>;
-pub type MouseDownListener<V> = Box<
- dyn Fn(&mut V, &MouseDownEvent, &Bounds<Pixels>, DispatchPhase, &mut ViewContext<V>) + 'static,
->;
-pub type MouseUpListener<V> = Box<
- dyn Fn(&mut V, &MouseUpEvent, &Bounds<Pixels>, DispatchPhase, &mut ViewContext<V>) + 'static,
->;
+pub type FocusListener = Box<dyn Fn(&FocusHandle, &FocusEvent, &mut WindowContext) + 'static>;
-pub type MouseMoveListener<V> = Box<
- dyn Fn(&mut V, &MouseMoveEvent, &Bounds<Pixels>, DispatchPhase, &mut ViewContext<V>) + 'static,
->;
+pub type MouseDownListener =
+ Box<dyn Fn(&MouseDownEvent, &Bounds<Pixels>, DispatchPhase, &mut WindowContext) + 'static>;
+pub type MouseUpListener =
+ Box<dyn Fn(&MouseUpEvent, &Bounds<Pixels>, DispatchPhase, &mut WindowContext) + 'static>;
-pub type ScrollWheelListener<V> = Box<
- dyn Fn(&mut V, &ScrollWheelEvent, &Bounds<Pixels>, DispatchPhase, &mut ViewContext<V>)
- + 'static,
->;
+pub type MouseMoveListener =
+ Box<dyn Fn(&MouseMoveEvent, &Bounds<Pixels>, DispatchPhase, &mut WindowContext) + 'static>;
-pub type ClickListener<V> = Box<dyn Fn(&mut V, &ClickEvent, &mut ViewContext<V>) + 'static>;
+pub type ScrollWheelListener =
+ Box<dyn Fn(&ScrollWheelEvent, &Bounds<Pixels>, DispatchPhase, &mut WindowContext) + 'static>;
-pub type DragListener<V> =
- Box<dyn Fn(&mut V, Point<Pixels>, &mut ViewContext<V>) -> AnyDrag + 'static>;
+pub type ClickListener = Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>;
-type DropListener<V> = dyn Fn(&mut V, AnyView, &mut ViewContext<V>) + 'static;
+pub type DragListener = Box<dyn Fn(Point<Pixels>, &mut WindowContext) -> AnyDrag + 'static>;
-pub type HoverListener<V> = Box<dyn Fn(&mut V, bool, &mut ViewContext<V>) + 'static>;
+type DropListener = dyn Fn(AnyView, &mut WindowContext) + 'static;
-pub type TooltipBuilder<V> = Rc<dyn Fn(&mut V, &mut ViewContext<V>) -> AnyView + 'static>;
+pub type TooltipBuilder = Rc<dyn Fn(&mut WindowContext) -> AnyView + 'static>;
-pub type KeyDownListener<V> =
- Box<dyn Fn(&mut V, &KeyDownEvent, DispatchPhase, &mut ViewContext<V>) + 'static>;
+pub type KeyDownListener = Box<dyn Fn(&KeyDownEvent, DispatchPhase, &mut WindowContext) + 'static>;
-pub type KeyUpListener<V> =
- Box<dyn Fn(&mut V, &KeyUpEvent, DispatchPhase, &mut ViewContext<V>) + 'static>;
+pub type KeyUpListener = Box<dyn Fn(&KeyUpEvent, DispatchPhase, &mut WindowContext) + 'static>;
-pub type ActionListener<V> =
- Box<dyn Fn(&mut V, &dyn Any, DispatchPhase, &mut ViewContext<V>) + 'static>;
+pub type ActionListener = Box<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext) + 'static>;
-pub fn div<V: 'static>() -> Div<V> {
+pub fn div() -> Div {
Div {
interactivity: Interactivity::default(),
children: SmallVec::default(),
}
}
-pub struct Div<V> {
- interactivity: Interactivity<V>,
- children: SmallVec<[AnyElement<V>; 2]>,
+pub struct Div {
+ interactivity: Interactivity,
+ children: SmallVec<[AnyElement; 2]>,
}
-impl<V> Styled for Div<V> {
+impl Styled for Div {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.interactivity.base_style
}
}
-impl<V: 'static> InteractiveComponent<V> for Div<V> {
- fn interactivity(&mut self) -> &mut Interactivity<V> {
+impl InteractiveElement for Div {
+ fn interactivity(&mut self) -> &mut Interactivity {
&mut self.interactivity
}
}
-impl<V: 'static> ParentComponent<V> for Div<V> {
- fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
+impl ParentElement for Div {
+ fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
&mut self.children
}
}
-impl<V: 'static> Element<V> for Div<V> {
- type ElementState = DivState;
-
- fn element_id(&self) -> Option<ElementId> {
- self.interactivity.element_id.clone()
- }
+impl Element for Div {
+ type State = DivState;
fn layout(
&mut self,
- view_state: &mut V,
- element_state: Option<Self::ElementState>,
- cx: &mut ViewContext<V>,
- ) -> (LayoutId, Self::ElementState) {
+ element_state: Option<Self::State>,
+ cx: &mut WindowContext,
+ ) -> (LayoutId, Self::State) {
let mut child_layout_ids = SmallVec::new();
let mut interactivity = mem::take(&mut self.interactivity);
let (layout_id, interactive_state) = interactivity.layout(
@@ -622,7 +598,7 @@ impl<V: 'static> Element<V> for Div<V> {
child_layout_ids = self
.children
.iter_mut()
- .map(|child| child.layout(view_state, cx))
+ .map(|child| child.layout(cx))
.collect::<SmallVec<_>>();
cx.request_layout(&style, child_layout_ids.iter().copied())
})
@@ -639,11 +615,10 @@ impl<V: 'static> Element<V> for Div<V> {
}
fn paint(
- &mut self,
+ self,
bounds: Bounds<Pixels>,
- view_state: &mut V,
- element_state: &mut Self::ElementState,
- cx: &mut ViewContext<V>,
+ element_state: &mut Self::State,
+ cx: &mut WindowContext,
) {
let mut child_min = point(Pixels::MAX, Pixels::MAX);
let mut child_max = Point::default();
@@ -658,8 +633,7 @@ impl<V: 'static> Element<V> for Div<V> {
(child_max - child_min).into()
};
- let mut interactivity = mem::take(&mut self.interactivity);
- interactivity.paint(
+ self.interactivity.paint(
bounds,
content_size,
&mut element_state.interactive_state,
@@ -679,8 +653,8 @@ impl<V: 'static> Element<V> for Div<V> {
cx.with_text_style(style.text_style().cloned(), |cx| {
cx.with_content_mask(style.overflow_mask(bounds), |cx| {
cx.with_element_offset(scroll_offset, |cx| {
- for child in &mut self.children {
- child.paint(view_state, cx);
+ for child in self.children {
+ child.paint(cx);
}
})
})
@@ -689,13 +663,18 @@ impl<V: 'static> Element<V> for Div<V> {
})
},
);
- self.interactivity = interactivity;
}
}
-impl<V: 'static> Component<V> for Div<V> {
- fn render(self) -> AnyElement<V> {
- AnyElement::new(self)
+impl RenderOnce for Div {
+ type Element = Self;
+
+ fn element_id(&self) -> Option<ElementId> {
+ self.interactivity.element_id.clone()
+ }
+
+ fn render_once(self) -> Self::Element {
+ self
}
}
@@ -710,12 +689,12 @@ impl DivState {
}
}
-pub struct Interactivity<V> {
+pub struct Interactivity {
pub element_id: Option<ElementId>,
pub key_context: KeyContext,
pub focusable: bool,
pub tracked_focus_handle: Option<FocusHandle>,
- pub focus_listeners: FocusListeners<V>,
+ pub focus_listeners: FocusListeners,
pub group: Option<SharedString>,
pub base_style: StyleRefinement,
pub focus_style: StyleRefinement,
@@ -726,29 +705,26 @@ pub struct Interactivity<V> {
pub group_active_style: Option<GroupStyle>,
pub drag_over_styles: SmallVec<[(TypeId, StyleRefinement); 2]>,
pub group_drag_over_styles: SmallVec<[(TypeId, GroupStyle); 2]>,
- pub mouse_down_listeners: SmallVec<[MouseDownListener<V>; 2]>,
- pub mouse_up_listeners: SmallVec<[MouseUpListener<V>; 2]>,
- pub mouse_move_listeners: SmallVec<[MouseMoveListener<V>; 2]>,
- pub scroll_wheel_listeners: SmallVec<[ScrollWheelListener<V>; 2]>,
- pub key_down_listeners: SmallVec<[KeyDownListener<V>; 2]>,
- pub key_up_listeners: SmallVec<[KeyUpListener<V>; 2]>,
- pub action_listeners: SmallVec<[(TypeId, ActionListener<V>); 8]>,
- pub drop_listeners: SmallVec<[(TypeId, Box<DropListener<V>>); 2]>,
- pub click_listeners: SmallVec<[ClickListener<V>; 2]>,
- pub drag_listener: Option<DragListener<V>>,
- pub hover_listener: Option<HoverListener<V>>,
- pub tooltip_builder: Option<TooltipBuilder<V>>,
+ pub mouse_down_listeners: SmallVec<[MouseDownListener; 2]>,
+ pub mouse_up_listeners: SmallVec<[MouseUpListener; 2]>,
+ pub mouse_move_listeners: SmallVec<[MouseMoveListener; 2]>,
+ pub scroll_wheel_listeners: SmallVec<[ScrollWheelListener; 2]>,
+ pub key_down_listeners: SmallVec<[KeyDownListener; 2]>,
+ pub key_up_listeners: SmallVec<[KeyUpListener; 2]>,
+ pub action_listeners: SmallVec<[(TypeId, ActionListener); 8]>,
+ pub drop_listeners: SmallVec<[(TypeId, Box<DropListener>); 2]>,
+ pub click_listeners: SmallVec<[ClickListener; 2]>,
+ pub drag_listener: Option<DragListener>,
+ pub hover_listener: Option<Box<dyn Fn(&bool, &mut WindowContext)>>,
+ pub tooltip_builder: Option<TooltipBuilder>,
}
-impl<V> Interactivity<V>
-where
- V: 'static,
-{
+impl Interactivity {
pub fn layout(
&mut self,
element_state: Option<InteractiveElementState>,
- cx: &mut ViewContext<V>,
- f: impl FnOnce(Style, &mut ViewContext<V>) -> LayoutId,
+ cx: &mut WindowContext,
+ f: impl FnOnce(Style, &mut WindowContext) -> LayoutId,
) -> (LayoutId, InteractiveElementState) {
let mut element_state = element_state.unwrap_or_default();
@@ -770,12 +746,12 @@ where
}
pub fn paint(
- &mut self,
+ mut self,
bounds: Bounds<Pixels>,
content_size: Size<Pixels>,
element_state: &mut InteractiveElementState,
- cx: &mut ViewContext<V>,
- f: impl FnOnce(Style, Point<Pixels>, &mut ViewContext<V>),
+ cx: &mut WindowContext,
+ f: impl FnOnce(Style, Point<Pixels>, &mut WindowContext),
) {
let style = self.compute_style(Some(bounds), element_state, cx);
@@ -787,26 +763,26 @@ where
}
for listener in self.mouse_down_listeners.drain(..) {
- cx.on_mouse_event(move |state, event: &MouseDownEvent, phase, cx| {
- listener(state, event, &bounds, phase, cx);
+ cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| {
+ listener(event, &bounds, phase, cx);
})
}
for listener in self.mouse_up_listeners.drain(..) {
- cx.on_mouse_event(move |state, event: &MouseUpEvent, phase, cx| {
- listener(state, event, &bounds, phase, cx);
+ cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| {
+ listener(event, &bounds, phase, cx);
})
}
for listener in self.mouse_move_listeners.drain(..) {
- cx.on_mouse_event(move |state, event: &MouseMoveEvent, phase, cx| {
- listener(state, event, &bounds, phase, cx);
+ cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {
+ listener(event, &bounds, phase, cx);
})
}
for listener in self.scroll_wheel_listeners.drain(..) {
- cx.on_mouse_event(move |state, event: &ScrollWheelEvent, phase, cx| {
- listener(state, event, &bounds, phase, cx);
+ cx.on_mouse_event(move |event: &ScrollWheelEvent, phase, cx| {
+ listener(event, &bounds, phase, cx);
})
}
@@ -817,7 +793,7 @@ where
if let Some(group_bounds) = hover_group_bounds {
let hovered = group_bounds.contains_point(&cx.mouse_position());
- cx.on_mouse_event(move |_, event: &MouseMoveEvent, phase, cx| {
+ cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {
if phase == DispatchPhase::Capture {
if group_bounds.contains_point(&event.position) != hovered {
cx.notify();
@@ -830,7 +806,7 @@ where
|| (cx.active_drag.is_some() && !self.drag_over_styles.is_empty())
{
let hovered = bounds.contains_point(&cx.mouse_position());
- cx.on_mouse_event(move |_, event: &MouseMoveEvent, phase, cx| {
+ cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {
if phase == DispatchPhase::Capture {
if bounds.contains_point(&event.position) != hovered {
cx.notify();
@@ -841,7 +817,7 @@ where
if cx.active_drag.is_some() {
let drop_listeners = mem::take(&mut self.drop_listeners);
- cx.on_mouse_event(move |view, event: &MouseUpEvent, phase, cx| {
+ cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| {
if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
if let Some(drag_state_type) =
cx.active_drag.as_ref().map(|drag| drag.view.entity_type())
@@ -852,7 +828,7 @@ where
.active_drag
.take()
.expect("checked for type drag state type above");
- listener(view, drag.view.clone(), cx);
+ listener(drag.view.clone(), cx);
cx.notify();
cx.stop_propagation();
}
@@ -872,7 +848,7 @@ where
if let Some(drag_listener) = drag_listener {
let active_state = element_state.clicked_state.clone();
- cx.on_mouse_event(move |view_state, event: &MouseMoveEvent, phase, cx| {
+ cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {
if cx.active_drag.is_some() {
if phase == DispatchPhase::Capture {
cx.notify();
@@ -883,7 +859,7 @@ where
{
*active_state.borrow_mut() = ElementClickedState::default();
let cursor_offset = event.position - bounds.origin;
- let drag = drag_listener(view_state, cursor_offset, cx);
+ let drag = drag_listener(cursor_offset, cx);
cx.active_drag = Some(drag);
cx.notify();
cx.stop_propagation();
@@ -891,21 +867,21 @@ where
});
}
- cx.on_mouse_event(move |view_state, event: &MouseUpEvent, phase, cx| {
+ cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| {
if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
let mouse_click = ClickEvent {
down: mouse_down.clone(),
up: event.clone(),
};
for listener in &click_listeners {
- listener(view_state, &mouse_click, cx);
+ listener(&mouse_click, cx);
}
}
*pending_mouse_down.borrow_mut() = None;
cx.notify();
});
} else {
- cx.on_mouse_event(move |_view_state, event: &MouseDownEvent, phase, cx| {
+ cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| {
if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
*pending_mouse_down.borrow_mut() = Some(event.clone());
cx.notify();
@@ -918,7 +894,7 @@ where
let was_hovered = element_state.hover_state.clone();
let has_mouse_down = element_state.pending_mouse_down.clone();
- cx.on_mouse_event(move |view_state, event: &MouseMoveEvent, phase, cx| {
+ cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {
if phase != DispatchPhase::Bubble {
return;
}
@@ -930,7 +906,7 @@ where
*was_hovered = is_hovered;
drop(was_hovered);
- hover_listener(view_state, is_hovered, cx);
+ hover_listener(&is_hovered, cx);
}
});
}
@@ -939,7 +915,7 @@ where
let active_tooltip = element_state.active_tooltip.clone();
let pending_mouse_down = element_state.pending_mouse_down.clone();
- cx.on_mouse_event(move |_, event: &MouseMoveEvent, phase, cx| {
+ cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {
if phase != DispatchPhase::Bubble {
return;
}
@@ -956,12 +932,12 @@ where
let active_tooltip = active_tooltip.clone();
let tooltip_builder = tooltip_builder.clone();
- move |view, mut cx| async move {
+ move |mut cx| async move {
cx.background_executor().timer(TOOLTIP_DELAY).await;
- view.update(&mut cx, move |view_state, cx| {
+ cx.update(|_, cx| {
active_tooltip.borrow_mut().replace(ActiveTooltip {
tooltip: Some(AnyTooltip {
- view: tooltip_builder(view_state, cx),
+ view: tooltip_builder(cx),
cursor_offset: cx.mouse_position(),
}),
_task: None,
@@ -979,7 +955,7 @@ where
});
let active_tooltip = element_state.active_tooltip.clone();
- cx.on_mouse_event(move |_, _: &MouseDownEvent, _, _| {
+ cx.on_mouse_event(move |_: &MouseDownEvent, _, _| {
active_tooltip.borrow_mut().take();
});
@@ -992,7 +968,7 @@ where
let active_state = element_state.clicked_state.clone();
if !active_state.borrow().is_clicked() {
- cx.on_mouse_event(move |_, _: &MouseUpEvent, phase, cx| {
+ cx.on_mouse_event(move |_: &MouseUpEvent, phase, cx| {
if phase == DispatchPhase::Capture {
*active_state.borrow_mut() = ElementClickedState::default();
cx.notify();
@@ -1003,7 +979,7 @@ where
.group_active_style
.as_ref()
.and_then(|group_active| GroupBounds::get(&group_active.group, cx));
- cx.on_mouse_event(move |_view, down: &MouseDownEvent, phase, cx| {
+ cx.on_mouse_event(move |down: &MouseDownEvent, phase, cx| {
if phase == DispatchPhase::Bubble {
let group = active_group_bounds
.map_or(false, |bounds| bounds.contains_point(&down.position));
@@ -1025,7 +1001,7 @@ where
let line_height = cx.line_height();
let scroll_max = (content_size - bounds.size).max(&Size::default());
- cx.on_mouse_event(move |_, event: &ScrollWheelEvent, phase, cx| {
+ cx.on_mouse_event(move |event: &ScrollWheelEvent, phase, cx| {
if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
let mut scroll_offset = scroll_offset.borrow_mut();
let old_scroll_offset = *scroll_offset;
@@ -1063,27 +1039,25 @@ where
element_state.focus_handle.clone(),
|_, cx| {
for listener in self.key_down_listeners.drain(..) {
- cx.on_key_event(move |state, event: &KeyDownEvent, phase, cx| {
- listener(state, event, phase, cx);
+ cx.on_key_event(move |event: &KeyDownEvent, phase, cx| {
+ listener(event, phase, cx);
})
}
for listener in self.key_up_listeners.drain(..) {
- cx.on_key_event(move |state, event: &KeyUpEvent, phase, cx| {
- listener(state, event, phase, cx);
+ cx.on_key_event(move |event: &KeyUpEvent, phase, cx| {
+ listener(event, phase, cx);
})
}
- for (action_type, listener) in self.action_listeners.drain(..) {
+ for (action_type, listener) in self.action_listeners {
cx.on_action(action_type, listener)
}
if let Some(focus_handle) = element_state.focus_handle.as_ref() {
- for listener in self.focus_listeners.drain(..) {
+ for listener in self.focus_listeners {
let focus_handle = focus_handle.clone();
- cx.on_focus_changed(move |view, event, cx| {
- listener(view, &focus_handle, event, cx)
- });
+ cx.on_focus_changed(move |event, cx| listener(&focus_handle, event, cx));
}
}
@@ -1,18 +1,17 @@
use crate::{
- AnyElement, BorrowWindow, Bounds, Component, Element, InteractiveComponent,
- InteractiveElementState, Interactivity, LayoutId, Pixels, SharedString, StyleRefinement,
- Styled, ViewContext,
+ Bounds, Element, InteractiveElement, InteractiveElementState, Interactivity, LayoutId, Pixels,
+ RenderOnce, SharedString, StyleRefinement, Styled, WindowContext,
};
use futures::FutureExt;
use util::ResultExt;
-pub struct Img<V: 'static> {
- interactivity: Interactivity<V>,
+pub struct Img {
+ interactivity: Interactivity,
uri: Option<SharedString>,
grayscale: bool,
}
-pub fn img<V: 'static>() -> Img<V> {
+pub fn img() -> Img {
Img {
interactivity: Interactivity::default(),
uri: None,
@@ -20,10 +19,7 @@ pub fn img<V: 'static>() -> Img<V> {
}
}
-impl<V> Img<V>
-where
- V: 'static,
-{
+impl Img {
pub fn uri(mut self, uri: impl Into<SharedString>) -> Self {
self.uri = Some(uri.into());
self
@@ -35,36 +31,24 @@ where
}
}
-impl<V> Component<V> for Img<V> {
- fn render(self) -> AnyElement<V> {
- AnyElement::new(self)
- }
-}
-
-impl<V> Element<V> for Img<V> {
- type ElementState = InteractiveElementState;
-
- fn element_id(&self) -> Option<crate::ElementId> {
- self.interactivity.element_id.clone()
- }
+impl Element for Img {
+ type State = InteractiveElementState;
fn layout(
&mut self,
- _view_state: &mut V,
- element_state: Option<Self::ElementState>,
- cx: &mut ViewContext<V>,
- ) -> (LayoutId, Self::ElementState) {
+ element_state: Option<Self::State>,
+ cx: &mut WindowContext,
+ ) -> (LayoutId, Self::State) {
self.interactivity.layout(element_state, cx, |style, cx| {
cx.request_layout(&style, None)
})
}
fn paint(
- &mut self,
+ self,
bounds: Bounds<Pixels>,
- _view_state: &mut V,
- element_state: &mut Self::ElementState,
- cx: &mut ViewContext<V>,
+ element_state: &mut Self::State,
+ cx: &mut WindowContext,
) {
self.interactivity.paint(
bounds,
@@ -81,7 +65,7 @@ impl<V> Element<V> for Img<V> {
if let Some(data) = image_future
.clone()
.now_or_never()
- .and_then(ResultExt::log_err)
+ .and_then(|result| result.ok())
{
let corner_radii = corner_radii.to_pixels(bounds.size, cx.rem_size());
cx.with_z_index(1, |cx| {
@@ -89,8 +73,8 @@ impl<V> Element<V> for Img<V> {
.log_err()
});
} else {
- cx.spawn(|_, mut cx| async move {
- if image_future.await.log_err().is_some() {
+ cx.spawn(|mut cx| async move {
+ if image_future.await.ok().is_some() {
cx.on_next_frame(|cx| cx.notify());
}
})
@@ -102,14 +86,26 @@ impl<V> Element<V> for Img<V> {
}
}
-impl<V> Styled for Img<V> {
+impl RenderOnce for Img {
+ type Element = Self;
+
+ fn element_id(&self) -> Option<crate::ElementId> {
+ self.interactivity.element_id.clone()
+ }
+
+ fn render_once(self) -> Self::Element {
+ self
+ }
+}
+
+impl Styled for Img {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.interactivity.base_style
}
}
-impl<V> InteractiveComponent<V> for Img<V> {
- fn interactivity(&mut self) -> &mut Interactivity<V> {
+impl InteractiveElement for Img {
+ fn interactivity(&mut self) -> &mut Interactivity {
&mut self.interactivity
}
}
@@ -2,16 +2,16 @@ use smallvec::SmallVec;
use taffy::style::{Display, Position};
use crate::{
- point, AnyElement, BorrowWindow, Bounds, Component, Element, LayoutId, ParentComponent, Pixels,
- Point, Size, Style,
+ point, AnyElement, BorrowWindow, Bounds, Element, LayoutId, ParentElement, Pixels, Point,
+ RenderOnce, Size, Style, WindowContext,
};
pub struct OverlayState {
child_layout_ids: SmallVec<[LayoutId; 4]>,
}
-pub struct Overlay<V> {
- children: SmallVec<[AnyElement<V>; 2]>,
+pub struct Overlay {
+ children: SmallVec<[AnyElement; 2]>,
anchor_corner: AnchorCorner,
fit_mode: OverlayFitMode,
// todo!();
@@ -21,7 +21,7 @@ pub struct Overlay<V> {
/// overlay gives you a floating element that will avoid overflowing the window bounds.
/// Its children should have no margin to avoid measurement issues.
-pub fn overlay<V: 'static>() -> Overlay<V> {
+pub fn overlay() -> Overlay {
Overlay {
children: SmallVec::new(),
anchor_corner: AnchorCorner::TopLeft,
@@ -30,7 +30,7 @@ pub fn overlay<V: 'static>() -> Overlay<V> {
}
}
-impl<V> Overlay<V> {
+impl Overlay {
/// Sets which corner of the overlay should be anchored to the current position.
pub fn anchor(mut self, anchor: AnchorCorner) -> Self {
self.anchor_corner = anchor;
@@ -51,35 +51,24 @@ impl<V> Overlay<V> {
}
}
-impl<V: 'static> ParentComponent<V> for Overlay<V> {
- fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
+impl ParentElement for Overlay {
+ fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
&mut self.children
}
}
-impl<V: 'static> Component<V> for Overlay<V> {
- fn render(self) -> AnyElement<V> {
- AnyElement::new(self)
- }
-}
-
-impl<V: 'static> Element<V> for Overlay<V> {
- type ElementState = OverlayState;
-
- fn element_id(&self) -> Option<crate::ElementId> {
- None
- }
+impl Element for Overlay {
+ type State = OverlayState;
fn layout(
&mut self,
- view_state: &mut V,
- _: Option<Self::ElementState>,
- cx: &mut crate::ViewContext<V>,
- ) -> (crate::LayoutId, Self::ElementState) {
+ _: Option<Self::State>,
+ cx: &mut WindowContext,
+ ) -> (crate::LayoutId, Self::State) {
let child_layout_ids = self
.children
.iter_mut()
- .map(|child| child.layout(view_state, cx))
+ .map(|child| child.layout(cx))
.collect::<SmallVec<_>>();
let mut overlay_style = Style::default();
@@ -92,11 +81,10 @@ impl<V: 'static> Element<V> for Overlay<V> {
}
fn paint(
- &mut self,
+ self,
bounds: crate::Bounds<crate::Pixels>,
- view_state: &mut V,
- element_state: &mut Self::ElementState,
- cx: &mut crate::ViewContext<V>,
+ element_state: &mut Self::State,
+ cx: &mut WindowContext,
) {
if element_state.child_layout_ids.is_empty() {
return;
@@ -156,13 +144,25 @@ impl<V: 'static> Element<V> for Overlay<V> {
}
cx.with_element_offset(desired.origin - bounds.origin, |cx| {
- for child in &mut self.children {
- child.paint(view_state, cx);
+ for child in self.children {
+ child.paint(cx);
}
})
}
}
+impl RenderOnce for Overlay {
+ type Element = Self;
+
+ fn element_id(&self) -> Option<crate::ElementId> {
+ None
+ }
+
+ fn render_once(self) -> Self::Element {
+ self
+ }
+}
+
enum Axis {
Horizontal,
Vertical,
@@ -1,60 +1,43 @@
use crate::{
- AnyElement, Bounds, Component, Element, ElementId, InteractiveComponent,
- InteractiveElementState, Interactivity, LayoutId, Pixels, SharedString, StyleRefinement,
- Styled, ViewContext,
+ Bounds, Element, ElementId, InteractiveElement, InteractiveElementState, Interactivity,
+ LayoutId, Pixels, RenderOnce, SharedString, StyleRefinement, Styled, WindowContext,
};
use util::ResultExt;
-pub struct Svg<V: 'static> {
- interactivity: Interactivity<V>,
+pub struct Svg {
+ interactivity: Interactivity,
path: Option<SharedString>,
}
-pub fn svg<V: 'static>() -> Svg<V> {
+pub fn svg() -> Svg {
Svg {
interactivity: Interactivity::default(),
path: None,
}
}
-impl<V> Svg<V> {
+impl Svg {
pub fn path(mut self, path: impl Into<SharedString>) -> Self {
self.path = Some(path.into());
self
}
}
-impl<V> Component<V> for Svg<V> {
- fn render(self) -> AnyElement<V> {
- AnyElement::new(self)
- }
-}
-
-impl<V> Element<V> for Svg<V> {
- type ElementState = InteractiveElementState;
-
- fn element_id(&self) -> Option<ElementId> {
- self.interactivity.element_id.clone()
- }
+impl Element for Svg {
+ type State = InteractiveElementState;
fn layout(
&mut self,
- _view_state: &mut V,
- element_state: Option<Self::ElementState>,
- cx: &mut ViewContext<V>,
- ) -> (LayoutId, Self::ElementState) {
+ element_state: Option<Self::State>,
+ cx: &mut WindowContext,
+ ) -> (LayoutId, Self::State) {
self.interactivity.layout(element_state, cx, |style, cx| {
cx.request_layout(&style, None)
})
}
- fn paint(
- &mut self,
- bounds: Bounds<Pixels>,
- _view_state: &mut V,
- element_state: &mut Self::ElementState,
- cx: &mut ViewContext<V>,
- ) where
+ fn paint(self, bounds: Bounds<Pixels>, element_state: &mut Self::State, cx: &mut WindowContext)
+ where
Self: Sized,
{
self.interactivity
@@ -66,14 +49,26 @@ impl<V> Element<V> for Svg<V> {
}
}
-impl<V> Styled for Svg<V> {
+impl RenderOnce for Svg {
+ type Element = Self;
+
+ fn element_id(&self) -> Option<ElementId> {
+ self.interactivity.element_id.clone()
+ }
+
+ fn render_once(self) -> Self::Element {
+ self
+ }
+}
+
+impl Styled for Svg {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.interactivity.base_style
}
}
-impl<V> InteractiveComponent<V> for Svg<V> {
- fn interactivity(&mut self) -> &mut Interactivity<V> {
+impl InteractiveElement for Svg {
+ fn interactivity(&mut self) -> &mut Interactivity {
&mut self.interactivity
}
}
@@ -1,82 +1,191 @@
use crate::{
- AnyElement, BorrowWindow, Bounds, Component, Element, ElementId, LayoutId, Pixels,
- SharedString, Size, TextRun, ViewContext, WrappedLine,
+ Bounds, Element, ElementId, LayoutId, Pixels, RenderOnce, SharedString, Size, TextRun,
+ WindowContext, WrappedLine,
};
+use anyhow::anyhow;
use parking_lot::{Mutex, MutexGuard};
use smallvec::SmallVec;
use std::{cell::Cell, rc::Rc, sync::Arc};
use util::ResultExt;
-pub struct Text {
+impl Element for &'static str {
+ type State = TextState;
+
+ fn layout(
+ &mut self,
+ _: Option<Self::State>,
+ cx: &mut WindowContext,
+ ) -> (LayoutId, Self::State) {
+ let mut state = TextState::default();
+ let layout_id = state.layout(SharedString::from(*self), None, cx);
+ (layout_id, state)
+ }
+
+ fn paint(self, bounds: Bounds<Pixels>, state: &mut TextState, cx: &mut WindowContext) {
+ state.paint(bounds, self, cx)
+ }
+}
+
+impl RenderOnce for &'static str {
+ type Element = Self;
+
+ fn element_id(&self) -> Option<ElementId> {
+ None
+ }
+
+ fn render_once(self) -> Self::Element {
+ self
+ }
+}
+
+impl Element for SharedString {
+ type State = TextState;
+
+ fn layout(
+ &mut self,
+ _: Option<Self::State>,
+ cx: &mut WindowContext,
+ ) -> (LayoutId, Self::State) {
+ let mut state = TextState::default();
+ let layout_id = state.layout(self.clone(), None, cx);
+ (layout_id, state)
+ }
+
+ fn paint(self, bounds: Bounds<Pixels>, state: &mut TextState, cx: &mut WindowContext) {
+ let text_str: &str = self.as_ref();
+ state.paint(bounds, text_str, cx)
+ }
+}
+
+impl RenderOnce for SharedString {
+ type Element = Self;
+
+ fn element_id(&self) -> Option<ElementId> {
+ None
+ }
+
+ fn render_once(self) -> Self::Element {
+ self
+ }
+}
+
+pub struct StyledText {
text: SharedString,
runs: Option<Vec<TextRun>>,
}
-impl Text {
+impl StyledText {
/// Renders text with runs of different styles.
///
/// Callers are responsible for setting the correct style for each run.
/// For text with a uniform style, you can usually avoid calling this constructor
/// and just pass text directly.
- pub fn styled(text: SharedString, runs: Vec<TextRun>) -> Self {
- Text {
+ pub fn new(text: SharedString, runs: Vec<TextRun>) -> Self {
+ StyledText {
text,
runs: Some(runs),
}
}
}
-impl<V: 'static> Component<V> for Text {
- fn render(self) -> AnyElement<V> {
- AnyElement::new(self)
+impl Element for StyledText {
+ type State = TextState;
+
+ fn layout(
+ &mut self,
+ _: Option<Self::State>,
+ cx: &mut WindowContext,
+ ) -> (LayoutId, Self::State) {
+ let mut state = TextState::default();
+ let layout_id = state.layout(self.text.clone(), self.runs.take(), cx);
+ (layout_id, state)
+ }
+
+ fn paint(self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
+ state.paint(bounds, &self.text, cx)
}
}
-impl<V: 'static> Element<V> for Text {
- type ElementState = TextState;
+impl RenderOnce for StyledText {
+ type Element = Self;
fn element_id(&self) -> Option<crate::ElementId> {
None
}
+ fn render_once(self) -> Self::Element {
+ self
+ }
+}
+
+#[derive(Default, Clone)]
+pub struct TextState(Arc<Mutex<Option<TextStateInner>>>);
+
+struct TextStateInner {
+ lines: SmallVec<[WrappedLine; 1]>,
+ line_height: Pixels,
+ wrap_width: Option<Pixels>,
+ size: Option<Size<Pixels>>,
+}
+
+impl TextState {
+ fn lock(&self) -> MutexGuard<Option<TextStateInner>> {
+ self.0.lock()
+ }
+
fn layout(
&mut self,
- _view: &mut V,
- element_state: Option<Self::ElementState>,
- cx: &mut ViewContext<V>,
- ) -> (LayoutId, Self::ElementState) {
- let element_state = element_state.unwrap_or_default();
+ text: SharedString,
+ runs: Option<Vec<TextRun>>,
+ cx: &mut WindowContext,
+ ) -> LayoutId {
let text_system = cx.text_system().clone();
let text_style = cx.text_style();
let font_size = text_style.font_size.to_pixels(cx.rem_size());
let line_height = text_style
.line_height
.to_pixels(font_size.into(), cx.rem_size());
- let text = self.text.clone();
+ let text = SharedString::from(text);
let rem_size = cx.rem_size();
- let runs = if let Some(runs) = self.runs.take() {
+ let runs = if let Some(runs) = runs {
runs
} else {
vec![text_style.to_run(text.len())]
};
let layout_id = cx.request_measured_layout(Default::default(), rem_size, {
- let element_state = element_state.clone();
- move |known_dimensions, _| {
+ let element_state = self.clone();
+
+ move |known_dimensions, available_space| {
+ let wrap_width = known_dimensions.width.or(match available_space.width {
+ crate::AvailableSpace::Definite(x) => Some(x),
+ _ => None,
+ });
+
+ if let Some(text_state) = element_state.0.lock().as_ref() {
+ if text_state.size.is_some()
+ && (wrap_width.is_none() || wrap_width == text_state.wrap_width)
+ {
+ return text_state.size.unwrap();
+ }
+ }
+
let Some(lines) = text_system
.shape_text(
&text,
font_size,
&runs[..],
- known_dimensions.width, // Wrap if we know the width.
+ wrap_width, // Wrap if we know the width.
)
.log_err()
else {
element_state.lock().replace(TextStateInner {
lines: Default::default(),
line_height,
+ wrap_width,
+ size: Some(Size::default()),
});
return Size::default();
};
@@ -88,28 +197,25 @@ impl<V: 'static> Element<V> for Text {
size.width = size.width.max(line_size.width);
}
- element_state
- .lock()
- .replace(TextStateInner { lines, line_height });
+ element_state.lock().replace(TextStateInner {
+ lines,
+ line_height,
+ wrap_width,
+ size: Some(size),
+ });
size
}
});
- (layout_id, element_state)
+ layout_id
}
- fn paint(
- &mut self,
- bounds: Bounds<Pixels>,
- _: &mut V,
- element_state: &mut Self::ElementState,
- cx: &mut ViewContext<V>,
- ) {
- let element_state = element_state.lock();
+ fn paint(&mut self, bounds: Bounds<Pixels>, text: &str, cx: &mut WindowContext) {
+ let element_state = self.lock();
let element_state = element_state
.as_ref()
- .ok_or_else(|| anyhow::anyhow!("measurement has not been performed on {}", &self.text))
+ .ok_or_else(|| anyhow!("measurement has not been performed on {}", text))
.unwrap();
let line_height = element_state.line_height;
@@ -121,23 +227,9 @@ impl<V: 'static> Element<V> for Text {
}
}
-#[derive(Default, Clone)]
-pub struct TextState(Arc<Mutex<Option<TextStateInner>>>);
-
-impl TextState {
- fn lock(&self) -> MutexGuard<Option<TextStateInner>> {
- self.0.lock()
- }
-}
-
-struct TextStateInner {
- lines: SmallVec<[WrappedLine; 1]>,
- line_height: Pixels,
-}
-
struct InteractiveText {
- id: ElementId,
- text: Text,
+ element_id: ElementId,
+ text: StyledText,
}
struct InteractiveTextState {
@@ -145,32 +237,27 @@ struct InteractiveTextState {
clicked_range_ixs: Rc<Cell<SmallVec<[usize; 1]>>>,
}
-impl<V: 'static> Element<V> for InteractiveText {
- type ElementState = InteractiveTextState;
-
- fn element_id(&self) -> Option<ElementId> {
- Some(self.id.clone())
- }
+impl Element for InteractiveText {
+ type State = InteractiveTextState;
fn layout(
&mut self,
- view_state: &mut V,
- element_state: Option<Self::ElementState>,
- cx: &mut ViewContext<V>,
- ) -> (LayoutId, Self::ElementState) {
+ state: Option<Self::State>,
+ cx: &mut WindowContext,
+ ) -> (LayoutId, Self::State) {
if let Some(InteractiveTextState {
text_state,
clicked_range_ixs,
- }) = element_state
+ }) = state
{
- let (layout_id, text_state) = self.text.layout(view_state, Some(text_state), cx);
+ let (layout_id, text_state) = self.text.layout(Some(text_state), cx);
let element_state = InteractiveTextState {
text_state,
clicked_range_ixs,
};
(layout_id, element_state)
} else {
- let (layout_id, text_state) = self.text.layout(view_state, None, cx);
+ let (layout_id, text_state) = self.text.layout(None, cx);
let element_state = InteractiveTextState {
text_state,
clicked_range_ixs: Rc::default(),
@@ -179,46 +266,19 @@ impl<V: 'static> Element<V> for InteractiveText {
}
}
- fn paint(
- &mut self,
- bounds: Bounds<Pixels>,
- view_state: &mut V,
- element_state: &mut Self::ElementState,
- cx: &mut ViewContext<V>,
- ) {
- self.text
- .paint(bounds, view_state, &mut element_state.text_state, cx)
+ fn paint(self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
+ self.text.paint(bounds, &mut state.text_state, cx)
}
}
-impl<V: 'static> Component<V> for SharedString {
- fn render(self) -> AnyElement<V> {
- Text {
- text: self,
- runs: None,
- }
- .render()
- }
-}
+impl RenderOnce for InteractiveText {
+ type Element = Self;
-impl<V: 'static> Component<V> for &'static str {
- fn render(self) -> AnyElement<V> {
- Text {
- text: self.into(),
- runs: None,
- }
- .render()
+ fn element_id(&self) -> Option<ElementId> {
+ Some(self.element_id.clone())
}
-}
-// TODO: Figure out how to pass `String` to `child` without this.
-// This impl doesn't exist in the `gpui2` crate.
-impl<V: 'static> Component<V> for String {
- fn render(self) -> AnyElement<V> {
- Text {
- text: self.into(),
- runs: None,
- }
- .render()
+ fn render_once(self) -> Self::Element {
+ self
}
}
@@ -1,40 +1,45 @@
use crate::{
- point, px, size, AnyElement, AvailableSpace, BorrowWindow, Bounds, Component, Element,
- ElementId, InteractiveComponent, InteractiveElementState, Interactivity, LayoutId, Pixels,
- Point, Size, StyleRefinement, Styled, ViewContext,
+ point, px, size, AnyElement, AvailableSpace, Bounds, Element, ElementId, InteractiveElement,
+ InteractiveElementState, Interactivity, LayoutId, Pixels, Point, Render, RenderOnce, Size,
+ StyleRefinement, Styled, View, ViewContext, WindowContext,
};
use smallvec::SmallVec;
-use std::{cell::RefCell, cmp, mem, ops::Range, rc::Rc};
+use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
use taffy::style::Overflow;
/// uniform_list provides lazy rendering for a set of items that are of uniform height.
/// When rendered into a container with overflow-y: hidden and a fixed (or max) height,
/// uniform_list will only render the visibile subset of items.
-pub fn uniform_list<I, V, C>(
+pub fn uniform_list<I, R, V>(
+ view: View<V>,
id: I,
item_count: usize,
- f: impl 'static + Fn(&mut V, Range<usize>, &mut ViewContext<V>) -> Vec<C>,
-) -> UniformList<V>
+ f: impl 'static + Fn(&mut V, Range<usize>, &mut ViewContext<V>) -> Vec<R>,
+) -> UniformList
where
I: Into<ElementId>,
- V: 'static,
- C: Component<V>,
+ R: RenderOnce,
+ V: Render,
{
let id = id.into();
let mut style = StyleRefinement::default();
style.overflow.y = Some(Overflow::Hidden);
+ let render_range = move |range, cx: &mut WindowContext| {
+ view.update(cx, |this, cx| {
+ f(this, range, cx)
+ .into_iter()
+ .map(|component| component.render_into_any())
+ .collect()
+ })
+ };
+
UniformList {
id: id.clone(),
style,
item_count,
item_to_measure_index: 0,
- render_items: Box::new(move |view, visible_range, cx| {
- f(view, visible_range, cx)
- .into_iter()
- .map(|component| component.render())
- .collect()
- }),
+ render_items: Box::new(render_range),
interactivity: Interactivity {
element_id: Some(id.into()),
..Default::default()
@@ -43,19 +48,14 @@ where
}
}
-pub struct UniformList<V: 'static> {
+pub struct UniformList {
id: ElementId,
style: StyleRefinement,
item_count: usize,
item_to_measure_index: usize,
- render_items: Box<
- dyn for<'a> Fn(
- &'a mut V,
- Range<usize>,
- &'a mut ViewContext<V>,
- ) -> SmallVec<[AnyElement<V>; 64]>,
- >,
- interactivity: Interactivity<V>,
+ render_items:
+ Box<dyn for<'a> Fn(Range<usize>, &'a mut WindowContext) -> SmallVec<[AnyElement; 64]>>,
+ interactivity: Interactivity,
scroll_handle: Option<UniformListScrollHandle>,
}
@@ -89,7 +89,7 @@ impl UniformListScrollHandle {
}
}
-impl<V: 'static> Styled for UniformList<V> {
+impl Styled for UniformList {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.style
}
@@ -101,29 +101,24 @@ pub struct UniformListState {
item_size: Size<Pixels>,
}
-impl<V: 'static> Element<V> for UniformList<V> {
- type ElementState = UniformListState;
-
- fn element_id(&self) -> Option<crate::ElementId> {
- Some(self.id.clone())
- }
+impl Element for UniformList {
+ type State = UniformListState;
fn layout(
&mut self,
- view_state: &mut V,
- element_state: Option<Self::ElementState>,
- cx: &mut ViewContext<V>,
- ) -> (LayoutId, Self::ElementState) {
+ state: Option<Self::State>,
+ cx: &mut WindowContext,
+ ) -> (LayoutId, Self::State) {
let max_items = self.item_count;
let rem_size = cx.rem_size();
- let item_size = element_state
+ let item_size = state
.as_ref()
.map(|s| s.item_size)
- .unwrap_or_else(|| self.measure_item(view_state, None, cx));
+ .unwrap_or_else(|| self.measure_item(None, cx));
let (layout_id, interactive) =
self.interactivity
- .layout(element_state.map(|s| s.interactive), cx, |style, cx| {
+ .layout(state.map(|s| s.interactive), cx, |style, cx| {
cx.request_measured_layout(
style,
rem_size,
@@ -159,11 +154,10 @@ impl<V: 'static> Element<V> for UniformList<V> {
}
fn paint(
- &mut self,
+ self,
bounds: Bounds<crate::Pixels>,
- view_state: &mut V,
- element_state: &mut Self::ElementState,
- cx: &mut ViewContext<V>,
+ element_state: &mut Self::State,
+ cx: &mut WindowContext,
) {
let style =
self.interactivity
@@ -183,14 +177,15 @@ impl<V: 'static> Element<V> for UniformList<V> {
height: item_size.height * self.item_count,
};
- let mut interactivity = mem::take(&mut self.interactivity);
let shared_scroll_offset = element_state
.interactive
.scroll_offset
.get_or_insert_with(Rc::default)
.clone();
- interactivity.paint(
+ let item_height = self.measure_item(Some(padded_bounds.size.width), cx).height;
+
+ self.interactivity.paint(
bounds,
content_size,
&mut element_state.interactive,
@@ -209,9 +204,6 @@ impl<V: 'static> Element<V> for UniformList<V> {
style.paint(bounds, cx);
if self.item_count > 0 {
- let item_height = self
- .measure_item(view_state, Some(padded_bounds.size.width), cx)
- .height;
if let Some(scroll_handle) = self.scroll_handle.clone() {
scroll_handle.0.borrow_mut().replace(ScrollHandleState {
item_height,
@@ -233,44 +225,50 @@ impl<V: 'static> Element<V> for UniformList<V> {
self.item_count,
);
- let mut items = (self.render_items)(view_state, visible_range.clone(), cx);
+ let items = (self.render_items)(visible_range.clone(), cx);
cx.with_z_index(1, |cx| {
- for (item, ix) in items.iter_mut().zip(visible_range) {
+ for (item, ix) in items.into_iter().zip(visible_range) {
let item_origin = padded_bounds.origin
+ point(px(0.), item_height * ix + scroll_offset.y);
let available_space = size(
AvailableSpace::Definite(padded_bounds.size.width),
AvailableSpace::Definite(item_height),
);
- item.draw(item_origin, available_space, view_state, cx);
+ item.draw(item_origin, available_space, cx);
}
});
}
})
},
);
- self.interactivity = interactivity;
}
}
-impl<V> UniformList<V> {
+impl RenderOnce for UniformList {
+ type Element = Self;
+
+ fn element_id(&self) -> Option<crate::ElementId> {
+ Some(self.id.clone())
+ }
+
+ fn render_once(self) -> Self::Element {
+ self
+ }
+}
+
+impl UniformList {
pub fn with_width_from_item(mut self, item_index: Option<usize>) -> Self {
self.item_to_measure_index = item_index.unwrap_or(0);
self
}
- fn measure_item(
- &self,
- view_state: &mut V,
- list_width: Option<Pixels>,
- cx: &mut ViewContext<V>,
- ) -> Size<Pixels> {
+ fn measure_item(&self, list_width: Option<Pixels>, cx: &mut WindowContext) -> Size<Pixels> {
if self.item_count == 0 {
return Size::default();
}
let item_ix = cmp::min(self.item_to_measure_index, self.item_count - 1);
- let mut items = (self.render_items)(view_state, item_ix..item_ix + 1, cx);
+ let mut items = (self.render_items)(item_ix..item_ix + 1, cx);
let mut item_to_measure = items.pop().unwrap();
let available_space = size(
list_width.map_or(AvailableSpace::MinContent, |width| {
@@ -278,7 +276,7 @@ impl<V> UniformList<V> {
}),
AvailableSpace::MinContent,
);
- item_to_measure.measure(available_space, view_state, cx)
+ item_to_measure.measure(available_space, cx)
}
pub fn track_scroll(mut self, handle: UniformListScrollHandle) -> Self {
@@ -287,14 +285,8 @@ impl<V> UniformList<V> {
}
}
-impl<V> InteractiveComponent<V> for UniformList<V> {
- fn interactivity(&mut self) -> &mut crate::Interactivity<V> {
+impl InteractiveElement for UniformList {
+ fn interactivity(&mut self) -> &mut crate::Interactivity {
&mut self.interactivity
}
}
-
-impl<V: 'static> Component<V> for UniformList<V> {
- fn render(self) -> AnyElement<V> {
- AnyElement::new(self)
- }
-}
@@ -78,8 +78,6 @@ use std::{
};
use taffy::TaffyLayoutEngine;
-type AnyBox = Box<dyn Any>;
-
pub trait Context {
type Result<T>;
@@ -136,11 +134,15 @@ pub trait VisualContext: Context {
build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V,
) -> Self::Result<View<V>>
where
- V: Render;
+ V: 'static + Render;
fn focus_view<V>(&mut self, view: &View<V>) -> Self::Result<()>
where
V: FocusableView;
+
+ fn dismiss_view<V>(&mut self, view: &View<V>) -> Self::Result<()>
+ where
+ V: ManagedView;
}
pub trait Entity<T>: Sealed {
@@ -2,7 +2,7 @@ use crate::{ImageData, ImageId, SharedString};
use collections::HashMap;
use futures::{
future::{BoxFuture, Shared},
- AsyncReadExt, FutureExt,
+ AsyncReadExt, FutureExt, TryFutureExt,
};
use image::ImageError;
use parking_lot::Mutex;
@@ -88,6 +88,14 @@ impl ImageCache {
Ok(Arc::new(ImageData::new(image)))
}
}
+ .map_err({
+ let uri = uri.clone();
+
+ move |error| {
+ log::log!(log::Level::Error, "{:?} {:?}", &uri, &error);
+ error
+ }
+ })
.boxed()
.shared();
@@ -1,4 +1,6 @@
-use crate::{AsyncWindowContext, Bounds, Pixels, PlatformInputHandler, View, ViewContext};
+use crate::{
+ AsyncWindowContext, Bounds, Pixels, PlatformInputHandler, View, ViewContext, WindowContext,
+};
use std::ops::Range;
/// Implement this trait to allow views to handle textual input when implementing an editor, field, etc.
@@ -43,9 +45,9 @@ pub struct ElementInputHandler<V> {
impl<V: 'static> ElementInputHandler<V> {
/// Used in [Element::paint] with the element's bounds and a view context for its
/// containing view.
- pub fn new(element_bounds: Bounds<Pixels>, cx: &mut ViewContext<V>) -> Self {
+ pub fn new(element_bounds: Bounds<Pixels>, view: View<V>, cx: &mut WindowContext) -> Self {
ElementInputHandler {
- view: cx.view().clone(),
+ view,
element_bounds,
cx: cx.to_async(),
}
@@ -1,5 +1,5 @@
use crate::{
- div, point, Component, Div, FocusHandle, Keystroke, Modifiers, Pixels, Point, Render,
+ div, point, Div, Element, FocusHandle, Keystroke, Modifiers, Pixels, Point, Render, RenderOnce,
ViewContext,
};
use smallvec::SmallVec;
@@ -64,24 +64,24 @@ pub struct Drag<S, R, V, E>
where
R: Fn(&mut V, &mut ViewContext<V>) -> E,
V: 'static,
- E: Component<()>,
+ E: RenderOnce,
{
pub state: S,
pub render_drag_handle: R,
- view_type: PhantomData<V>,
+ view_element_types: PhantomData<(V, E)>,
}
impl<S, R, V, E> Drag<S, R, V, E>
where
R: Fn(&mut V, &mut ViewContext<V>) -> E,
V: 'static,
- E: Component<()>,
+ E: Element,
{
pub fn new(state: S, render_drag_handle: R) -> Self {
Drag {
state,
render_drag_handle,
- view_type: PhantomData,
+ view_element_types: Default::default(),
}
}
}
@@ -194,7 +194,7 @@ impl Deref for MouseExitEvent {
pub struct ExternalPaths(pub(crate) SmallVec<[PathBuf; 2]>);
impl Render for ExternalPaths {
- type Element = Div<Self>;
+ type Element = Div;
fn render(&mut self, _: &mut ViewContext<Self>) -> Self::Element {
div() // Intentionally left empty because the platform will render icons for the dragged files
@@ -286,8 +286,8 @@ pub struct FocusEvent {
#[cfg(test)]
mod test {
use crate::{
- self as gpui, div, Component, Div, FocusHandle, InteractiveComponent, KeyBinding,
- Keystroke, ParentComponent, Render, Stateful, TestAppContext, ViewContext, VisualContext,
+ self as gpui, div, Div, FocusHandle, InteractiveElement, KeyBinding, Keystroke,
+ ParentElement, Render, RenderOnce, Stateful, TestAppContext, VisualContext,
};
struct TestView {
@@ -299,20 +299,24 @@ mod test {
actions!(TestAction);
impl Render for TestView {
- type Element = Stateful<Self, Div<Self>>;
+ type Element = Stateful<Div>;
- fn render(&mut self, _: &mut gpui::ViewContext<Self>) -> Self::Element {
+ fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element {
div().id("testview").child(
div()
.key_context("parent")
- .on_key_down(|this: &mut TestView, _, _, _| this.saw_key_down = true)
- .on_action(|this: &mut TestView, _: &TestAction, _| this.saw_action = true)
- .child(|this: &mut Self, _cx: &mut ViewContext<Self>| {
+ .on_key_down(cx.listener(|this, _, _| this.saw_key_down = true))
+ .on_action(
+ cx.listener(|this: &mut TestView, _: &TestAction, _| {
+ this.saw_action = true
+ }),
+ )
+ .child(
div()
.key_context("nested")
- .track_focus(&this.focus_handle)
- .render()
- }),
+ .track_focus(&self.focus_handle)
+ .render_once(),
+ ),
)
}
}
@@ -1205,10 +1205,7 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
InputEvent::MouseMove(_) if !(is_active || lock.kind == WindowKind::PopUp) => return,
- InputEvent::MouseUp(MouseUpEvent {
- button: MouseButton::Left,
- ..
- }) => {
+ InputEvent::MouseUp(MouseUpEvent { .. }) => {
lock.synthetic_drag_counter += 1;
}
@@ -1,4 +1,5 @@
pub use crate::{
- BorrowAppContext, BorrowWindow, Component, Context, FocusableComponent, InteractiveComponent,
- ParentComponent, Refineable, Render, StatefulInteractiveComponent, Styled, VisualContext,
+ BorrowAppContext, BorrowWindow, Component, Context, Element, FocusableElement,
+ InteractiveElement, ParentElement, Refineable, Render, RenderOnce, StatefulInteractiveElement,
+ Styled, VisualContext,
};
@@ -2,7 +2,7 @@ use crate::{
black, phi, point, rems, AbsoluteLength, BorrowAppContext, BorrowWindow, Bounds, ContentMask,
Corners, CornersRefinement, CursorStyle, DefiniteLength, Edges, EdgesRefinement, Font,
FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Rgba,
- SharedString, Size, SizeRefinement, Styled, TextRun, ViewContext,
+ SharedString, Size, SizeRefinement, Styled, TextRun, WindowContext,
};
use refineable::{Cascade, Refineable};
use smallvec::SmallVec;
@@ -313,7 +313,7 @@ impl Style {
}
/// Paints the background of an element styled with this style.
- pub fn paint<V: 'static>(&self, bounds: Bounds<Pixels>, cx: &mut ViewContext<V>) {
+ pub fn paint(&self, bounds: Bounds<Pixels>, cx: &mut WindowContext) {
let rem_size = cx.rem_size();
cx.with_z_index(0, |cx| {
@@ -5,12 +5,14 @@ use std::fmt::Debug;
use taffy::{
geometry::{Point as TaffyPoint, Rect as TaffyRect, Size as TaffySize},
style::AvailableSpace as TaffyAvailableSpace,
- tree::{Measurable, MeasureFunc, NodeId},
+ tree::NodeId,
Taffy,
};
+type Measureable = dyn Fn(Size<Option<Pixels>>, Size<AvailableSpace>) -> Size<Pixels> + Send + Sync;
+
pub struct TaffyLayoutEngine {
- taffy: Taffy,
+ taffy: Taffy<Box<Measureable>>,
children_to_parents: HashMap<LayoutId, LayoutId>,
absolute_layout_bounds: HashMap<LayoutId, Bounds<Pixels>>,
computed_layouts: HashSet<LayoutId>,
@@ -70,9 +72,9 @@ impl TaffyLayoutEngine {
) -> LayoutId {
let style = style.to_taffy(rem_size);
- let measurable = Box::new(Measureable(measure)) as Box<dyn Measurable>;
+ let measurable = Box::new(measure);
self.taffy
- .new_leaf_with_measure(style, MeasureFunc::Boxed(measurable))
+ .new_leaf_with_context(style, measurable)
.expect(EXPECT_MESSAGE)
.into()
}
@@ -154,7 +156,22 @@ impl TaffyLayoutEngine {
// let started_at = std::time::Instant::now();
self.taffy
- .compute_layout(id.into(), available_space.into())
+ .compute_layout_with_measure(
+ id.into(),
+ available_space.into(),
+ |known_dimensions, available_space, _node_id, context| {
+ let Some(measure) = context else {
+ return taffy::geometry::Size::default();
+ };
+
+ let known_dimensions = Size {
+ width: known_dimensions.width.map(Pixels),
+ height: known_dimensions.height.map(Pixels),
+ };
+
+ measure(known_dimensions, available_space.into()).into()
+ },
+ )
.expect(EXPECT_MESSAGE);
// println!("compute_layout took {:?}", started_at.elapsed());
}
@@ -202,25 +219,6 @@ impl From<LayoutId> for NodeId {
}
}
-struct Measureable<F>(F);
-
-impl<F> taffy::tree::Measurable for Measureable<F>
-where
- F: Fn(Size<Option<Pixels>>, Size<AvailableSpace>) -> Size<Pixels> + Send + Sync,
-{
- fn measure(
- &self,
- known_dimensions: TaffySize<Option<f32>>,
- available_space: TaffySize<TaffyAvailableSpace>,
- ) -> TaffySize<f32> {
- let known_dimensions: Size<Option<f32>> = known_dimensions.into();
- let known_dimensions: Size<Option<Pixels>> = known_dimensions.map(|d| d.map(Into::into));
- let available_space = available_space.into();
- let size = (self.0)(known_dimensions, available_space);
- size.into()
- }
-}
-
trait ToTaffy<Output> {
fn to_taffy(&self, rem_size: Pixels) -> Output;
}
@@ -1,23 +1,17 @@
use crate::{
- private::Sealed, AnyBox, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace,
- BorrowWindow, Bounds, Component, Element, ElementId, Entity, EntityId, Flatten, FocusHandle,
- FocusableView, LayoutId, Model, Pixels, Point, Size, ViewContext, VisualContext, WeakModel,
+ private::Sealed, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace, BorrowWindow,
+ Bounds, Element, ElementId, Entity, EntityId, Flatten, FocusHandle, FocusableView, LayoutId,
+ Model, Pixels, Point, Render, RenderOnce, Size, ViewContext, VisualContext, WeakModel,
WindowContext,
};
use anyhow::{Context, Result};
use std::{
- any::{Any, TypeId},
+ any::TypeId,
hash::{Hash, Hasher},
};
-pub trait Render: 'static + Sized {
- type Element: Element<Self> + 'static;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element;
-}
-
pub struct View<V> {
- pub(crate) model: Model<V>,
+ pub model: Model<V>,
}
impl<V> Sealed for View<V> {}
@@ -65,15 +59,15 @@ impl<V: 'static> View<V> {
self.model.read(cx)
}
- pub fn render_with<C>(&self, component: C) -> RenderViewWith<C, V>
- where
- C: 'static + Component<V>,
- {
- RenderViewWith {
- view: self.clone(),
- component: Some(component),
- }
- }
+ // pub fn render_with<E>(&self, component: E) -> RenderViewWith<E, V>
+ // where
+ // E: 'static + Element,
+ // {
+ // RenderViewWith {
+ // view: self.clone(),
+ // element: Some(component),
+ // }
+ // }
pub fn focus_handle(&self, cx: &AppContext) -> FocusHandle
where
@@ -83,6 +77,24 @@ impl<V: 'static> View<V> {
}
}
+impl<V: Render> Element for View<V> {
+ type State = Option<AnyElement>;
+
+ fn layout(
+ &mut self,
+ _state: Option<Self::State>,
+ cx: &mut WindowContext,
+ ) -> (LayoutId, Self::State) {
+ let mut element = self.update(cx, |view, cx| view.render(cx).into_any());
+ let layout_id = element.layout(cx);
+ (layout_id, Some(element))
+ }
+
+ fn paint(self, _: Bounds<Pixels>, element: &mut Self::State, cx: &mut WindowContext) {
+ element.take().unwrap().paint(cx);
+ }
+}
+
impl<V> Clone for View<V> {
fn clone(&self) -> Self {
Self {
@@ -105,12 +117,6 @@ impl<V> PartialEq for View<V> {
impl<V> Eq for View<V> {}
-impl<V: Render, ParentViewState: 'static> Component<ParentViewState> for View<V> {
- fn render(self) -> AnyElement<ParentViewState> {
- AnyElement::new(AnyView::from(self))
- }
-}
-
pub struct WeakView<V> {
pub(crate) model: WeakModel<V>,
}
@@ -163,8 +169,8 @@ impl<V> Eq for WeakView<V> {}
#[derive(Clone, Debug)]
pub struct AnyView {
model: AnyModel,
- layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, Box<dyn Any>),
- paint: fn(&AnyView, &mut AnyBox, &mut WindowContext),
+ layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, AnyElement),
+ paint: fn(&AnyView, AnyElement, &mut WindowContext),
}
impl AnyView {
@@ -191,6 +197,10 @@ impl AnyView {
self.model.entity_type
}
+ pub fn entity_id(&self) -> EntityId {
+ self.model.entity_id()
+ }
+
pub(crate) fn draw(
&self,
origin: Point<Pixels>,
@@ -198,21 +208,15 @@ impl AnyView {
cx: &mut WindowContext,
) {
cx.with_absolute_element_offset(origin, |cx| {
- let (layout_id, mut rendered_element) = (self.layout)(self, cx);
+ let (layout_id, rendered_element) = (self.layout)(self, cx);
cx.window
.layout_engine
.compute_layout(layout_id, available_space);
- (self.paint)(self, &mut rendered_element, cx);
+ (self.paint)(self, rendered_element, cx);
})
}
}
-impl<V: 'static> Component<V> for AnyView {
- fn render(self) -> AnyElement<V> {
- AnyElement::new(self)
- }
-}
-
impl<V: Render> From<View<V>> for AnyView {
fn from(value: View<V>) -> Self {
AnyView {
@@ -223,37 +227,51 @@ impl<V: Render> From<View<V>> for AnyView {
}
}
-impl<ParentViewState: 'static> Element<ParentViewState> for AnyView {
- type ElementState = Box<dyn Any>;
+impl Element for AnyView {
+ type State = Option<AnyElement>;
+
+ fn layout(
+ &mut self,
+ _state: Option<Self::State>,
+ cx: &mut WindowContext,
+ ) -> (LayoutId, Self::State) {
+ let (layout_id, state) = (self.layout)(self, cx);
+ (layout_id, Some(state))
+ }
+
+ fn paint(self, _: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
+ (self.paint)(&self, state.take().unwrap(), cx)
+ }
+}
+
+impl<V: 'static + Render> RenderOnce for View<V> {
+ type Element = View<V>;
fn element_id(&self) -> Option<ElementId> {
Some(self.model.entity_id.into())
}
- fn layout(
- &mut self,
- _view_state: &mut ParentViewState,
- _element_state: Option<Self::ElementState>,
- cx: &mut ViewContext<ParentViewState>,
- ) -> (LayoutId, Self::ElementState) {
- (self.layout)(self, cx)
+ fn render_once(self) -> Self::Element {
+ self
}
+}
- fn paint(
- &mut self,
- _bounds: Bounds<Pixels>,
- _view_state: &mut ParentViewState,
- rendered_element: &mut Self::ElementState,
- cx: &mut ViewContext<ParentViewState>,
- ) {
- (self.paint)(self, rendered_element, cx)
+impl RenderOnce for AnyView {
+ type Element = Self;
+
+ fn element_id(&self) -> Option<ElementId> {
+ Some(self.model.entity_id.into())
+ }
+
+ fn render_once(self) -> Self::Element {
+ self
}
}
pub struct AnyWeakView {
model: AnyWeakModel,
- layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, Box<dyn Any>),
- paint: fn(&AnyView, &mut AnyBox, &mut WindowContext),
+ layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, AnyElement),
+ paint: fn(&AnyView, AnyElement, &mut WindowContext),
}
impl AnyWeakView {
@@ -267,7 +285,7 @@ impl AnyWeakView {
}
}
-impl<V: Render> From<WeakView<V>> for AnyWeakView {
+impl<V: 'static + Render> From<WeakView<V>> for AnyWeakView {
fn from(view: WeakView<V>) -> Self {
Self {
model: view.model.into(),
@@ -280,7 +298,7 @@ impl<V: Render> From<WeakView<V>> for AnyWeakView {
impl<T, E> Render for T
where
T: 'static + FnMut(&mut WindowContext) -> E,
- E: 'static + Send + Element<T>,
+ E: 'static + Send + Element,
{
type Element = E;
@@ -289,85 +307,28 @@ where
}
}
-pub struct RenderViewWith<C, V> {
- view: View<V>,
- component: Option<C>,
-}
-
-impl<C, ParentViewState, ViewState> Component<ParentViewState> for RenderViewWith<C, ViewState>
-where
- C: 'static + Component<ViewState>,
- ParentViewState: 'static,
- ViewState: 'static,
-{
- fn render(self) -> AnyElement<ParentViewState> {
- AnyElement::new(self)
- }
-}
-
-impl<C, ParentViewState, ViewState> Element<ParentViewState> for RenderViewWith<C, ViewState>
-where
- C: 'static + Component<ViewState>,
- ParentViewState: 'static,
- ViewState: 'static,
-{
- type ElementState = AnyElement<ViewState>;
-
- fn element_id(&self) -> Option<ElementId> {
- Some(self.view.entity_id().into())
- }
-
- fn layout(
- &mut self,
- _: &mut ParentViewState,
- _: Option<Self::ElementState>,
- cx: &mut ViewContext<ParentViewState>,
- ) -> (LayoutId, Self::ElementState) {
- self.view.update(cx, |view, cx| {
- let mut element = self.component.take().unwrap().render();
- let layout_id = element.layout(view, cx);
- (layout_id, element)
- })
- }
-
- fn paint(
- &mut self,
- _: Bounds<Pixels>,
- _: &mut ParentViewState,
- element: &mut Self::ElementState,
- cx: &mut ViewContext<ParentViewState>,
- ) {
- self.view.update(cx, |view, cx| element.paint(view, cx))
- }
-}
-
mod any_view {
- use crate::{AnyElement, AnyView, BorrowWindow, LayoutId, Render, WindowContext};
- use std::any::Any;
+ use crate::{AnyElement, AnyView, BorrowWindow, Element, LayoutId, Render, WindowContext};
- pub(crate) fn layout<V: Render>(
+ pub(crate) fn layout<V: 'static + Render>(
view: &AnyView,
cx: &mut WindowContext,
- ) -> (LayoutId, Box<dyn Any>) {
+ ) -> (LayoutId, AnyElement) {
cx.with_element_id(Some(view.model.entity_id), |cx| {
let view = view.clone().downcast::<V>().unwrap();
- view.update(cx, |view, cx| {
- let mut element = AnyElement::new(view.render(cx));
- let layout_id = element.layout(view, cx);
- (layout_id, Box::new(element) as Box<dyn Any>)
- })
+ let mut element = view.update(cx, |view, cx| view.render(cx).into_any());
+ let layout_id = element.layout(cx);
+ (layout_id, element)
})
}
- pub(crate) fn paint<V: Render>(
+ pub(crate) fn paint<V: 'static + Render>(
view: &AnyView,
- element: &mut Box<dyn Any>,
+ element: AnyElement,
cx: &mut WindowContext,
) {
cx.with_element_id(Some(view.model.entity_id), |cx| {
- let view = view.clone().downcast::<V>().unwrap();
- let element = element.downcast_mut::<AnyElement<V>>().unwrap();
- view.update(cx, |view, cx| element.paint(view, cx))
+ element.paint(cx);
})
}
}
@@ -1,15 +1,15 @@
use crate::{
- key_dispatch::DispatchActionListener, px, size, Action, AnyBox, AnyDrag, AnyView, AppContext,
+ key_dispatch::DispatchActionListener, px, size, Action, AnyDrag, AnyView, AppContext,
AsyncWindowContext, AvailableSpace, Bounds, BoxShadow, Context, Corners, CursorStyle,
DevicePixels, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId,
- EventEmitter, FileDropEvent, FocusEvent, FontId, GlobalElementId, GlyphId, Hsla, ImageData,
- InputEvent, IsZero, KeyBinding, KeyContext, KeyDownEvent, LayoutId, Model, ModelContext,
- Modifiers, MonochromeSprite, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Path,
- Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point,
- PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams,
- RenderSvgParams, ScaledPixels, SceneBuilder, Shadow, SharedString, Size, Style, SubscriberSet,
- Subscription, TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, VisualContext,
- WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS,
+ EventEmitter, FileDropEvent, Flatten, FocusEvent, FontId, GlobalElementId, GlyphId, Hsla,
+ ImageData, InputEvent, IsZero, KeyBinding, KeyContext, KeyDownEvent, LayoutId, Model,
+ ModelContext, Modifiers, MonochromeSprite, MouseButton, MouseDownEvent, MouseMoveEvent,
+ MouseUpEvent, Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler,
+ PlatformWindow, Point, PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams,
+ RenderImageParams, RenderSvgParams, ScaledPixels, SceneBuilder, Shadow, SharedString, Size,
+ Style, SubscriberSet, Subscription, TaffyLayoutEngine, Task, Underline, UnderlineStyle, View,
+ VisualContext, WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS,
};
use anyhow::{anyhow, Context as _, Result};
use collections::HashMap;
@@ -187,23 +187,18 @@ impl Drop for FocusHandle {
/// FocusableView allows users of your view to easily
/// focus it (using cx.focus_view(view))
-pub trait FocusableView: Render {
+pub trait FocusableView: 'static + Render {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle;
}
/// ManagedView is a view (like a Modal, Popover, Menu, etc.)
/// where the lifecycle of the view is handled by another view.
-pub trait ManagedView: Render {
- fn focus_handle(&self, cx: &AppContext) -> FocusHandle;
-}
+pub trait ManagedView: FocusableView + EventEmitter<Manager> {}
-pub struct Dismiss;
-impl<T: ManagedView> EventEmitter<Dismiss> for T {}
+impl<M: FocusableView + EventEmitter<Manager>> ManagedView for M {}
-impl<T: ManagedView> FocusableView for T {
- fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
- self.focus_handle(cx)
- }
+pub enum Manager {
+ Dismiss,
}
// Holds the state for a specific window.
@@ -237,7 +232,7 @@ pub struct Window {
// #[derive(Default)]
pub(crate) struct Frame {
- pub(crate) element_states: HashMap<GlobalElementId, AnyBox>,
+ pub(crate) element_states: HashMap<GlobalElementId, Box<dyn Any>>,
mouse_listeners: HashMap<TypeId, Vec<(StackingOrder, AnyMouseListener)>>,
pub(crate) dispatch_tree: DispatchTree,
pub(crate) focus_listeners: Vec<AnyFocusListener>,
@@ -1441,6 +1436,82 @@ impl<'a> WindowContext<'a> {
.dispatch_tree
.bindings_for_action(action)
}
+
+ pub fn listener_for<V: Render, E>(
+ &self,
+ view: &View<V>,
+ f: impl Fn(&mut V, &E, &mut ViewContext<V>) + 'static,
+ ) -> impl Fn(&E, &mut WindowContext) + 'static {
+ let view = view.downgrade();
+ move |e: &E, cx: &mut WindowContext| {
+ view.update(cx, |view, cx| f(view, e, cx)).ok();
+ }
+ }
+
+ pub fn constructor_for<V: Render, R>(
+ &self,
+ view: &View<V>,
+ f: impl Fn(&mut V, &mut ViewContext<V>) -> R + 'static,
+ ) -> impl Fn(&mut WindowContext) -> R + 'static {
+ let view = view.clone();
+ move |cx: &mut WindowContext| view.update(cx, |view, cx| f(view, cx))
+ }
+
+ //========== ELEMENT RELATED FUNCTIONS ===========
+ pub fn with_key_dispatch<R>(
+ &mut self,
+ context: KeyContext,
+ focus_handle: Option<FocusHandle>,
+ f: impl FnOnce(Option<FocusHandle>, &mut Self) -> R,
+ ) -> R {
+ let window = &mut self.window;
+ window
+ .current_frame
+ .dispatch_tree
+ .push_node(context.clone());
+ if let Some(focus_handle) = focus_handle.as_ref() {
+ window
+ .current_frame
+ .dispatch_tree
+ .make_focusable(focus_handle.id);
+ }
+ let result = f(focus_handle, self);
+
+ self.window.current_frame.dispatch_tree.pop_node();
+
+ result
+ }
+
+ /// Register a focus listener for the current frame only. It will be cleared
+ /// on the next frame render. You should use this method only from within elements,
+ /// and we may want to enforce that better via a different context type.
+ // todo!() Move this to `FrameContext` to emphasize its individuality?
+ pub fn on_focus_changed(
+ &mut self,
+ listener: impl Fn(&FocusEvent, &mut WindowContext) + 'static,
+ ) {
+ self.window
+ .current_frame
+ .focus_listeners
+ .push(Box::new(move |event, cx| {
+ listener(event, cx);
+ }));
+ }
+
+ /// Set an input handler, such as [ElementInputHandler], which interfaces with the
+ /// platform to receive textual input with proper integration with concerns such
+ /// as IME interactions.
+ pub fn handle_input(
+ &mut self,
+ focus_handle: &FocusHandle,
+ input_handler: impl PlatformInputHandler,
+ ) {
+ if focus_handle.is_focused(self) {
+ self.window
+ .platform_window
+ .set_input_handler(Box::new(input_handler));
+ }
+ }
}
impl Context for WindowContext<'_> {
@@ -1564,7 +1635,7 @@ impl VisualContext for WindowContext<'_> {
build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V,
) -> Self::Result<View<V>>
where
- V: Render,
+ V: 'static + Render,
{
let slot = self.app.entities.reserve();
let view = View {
@@ -1582,6 +1653,13 @@ impl VisualContext for WindowContext<'_> {
view.focus_handle(cx).clone().focus(cx);
})
}
+
+ fn dismiss_view<V>(&mut self, view: &View<V>) -> Self::Result<()>
+ where
+ V: ManagedView,
+ {
+ self.update_view(view, |_, cx| cx.emit(Manager::Dismiss))
+ }
}
impl<'a> std::ops::Deref for WindowContext<'a> {
@@ -1615,6 +1693,10 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
self.borrow_mut()
}
+ fn app(&self) -> &AppContext {
+ self.borrow()
+ }
+
fn window(&self) -> &Window {
self.borrow()
}
@@ -2122,49 +2204,6 @@ impl<'a, V: 'static> ViewContext<'a, V> {
)
}
- /// Register a focus listener for the current frame only. It will be cleared
- /// on the next frame render. You should use this method only from within elements,
- /// and we may want to enforce that better via a different context type.
- // todo!() Move this to `FrameContext` to emphasize its individuality?
- pub fn on_focus_changed(
- &mut self,
- listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + 'static,
- ) {
- let handle = self.view().downgrade();
- self.window
- .current_frame
- .focus_listeners
- .push(Box::new(move |event, cx| {
- handle
- .update(cx, |view, cx| listener(view, event, cx))
- .log_err();
- }));
- }
-
- pub fn with_key_dispatch<R>(
- &mut self,
- context: KeyContext,
- focus_handle: Option<FocusHandle>,
- f: impl FnOnce(Option<FocusHandle>, &mut Self) -> R,
- ) -> R {
- let window = &mut self.window;
- window
- .current_frame
- .dispatch_tree
- .push_node(context.clone());
- if let Some(focus_handle) = focus_handle.as_ref() {
- window
- .current_frame
- .dispatch_tree
- .make_focusable(focus_handle.id);
- }
- let result = f(focus_handle, self);
-
- self.window.current_frame.dispatch_tree.pop_node();
-
- result
- }
-
pub fn spawn<Fut, R>(
&mut self,
f: impl FnOnce(WeakView<V>, AsyncWindowContext) -> Fut,
@@ -2241,21 +2280,6 @@ impl<'a, V: 'static> ViewContext<'a, V> {
});
}
- /// Set an input handler, such as [ElementInputHandler], which interfaces with the
- /// platform to receive textual input with proper integration with concerns such
- /// as IME interactions.
- pub fn handle_input(
- &mut self,
- focus_handle: &FocusHandle,
- input_handler: impl PlatformInputHandler,
- ) {
- if focus_handle.is_focused(self) {
- self.window
- .platform_window
- .set_input_handler(Box::new(input_handler));
- }
- }
-
pub fn emit<Evt>(&mut self, event: Evt)
where
Evt: 'static,
@@ -2275,6 +2299,23 @@ impl<'a, V: 'static> ViewContext<'a, V> {
{
self.defer(|view, cx| view.focus_handle(cx).focus(cx))
}
+
+ pub fn dismiss_self(&mut self)
+ where
+ V: ManagedView,
+ {
+ self.defer(|_, cx| cx.emit(Manager::Dismiss))
+ }
+
+ pub fn listener<E>(
+ &self,
+ f: impl Fn(&mut V, &E, &mut ViewContext<V>) + 'static,
+ ) -> impl Fn(&E, &mut WindowContext) + 'static {
+ let view = self.view().downgrade();
+ move |e: &E, cx: &mut WindowContext| {
+ view.update(cx, |view, cx| f(view, e, cx)).ok();
+ }
+ }
}
impl<V> Context for ViewContext<'_, V> {
@@ -2346,7 +2387,7 @@ impl<V: 'static> VisualContext for ViewContext<'_, V> {
build_view: impl FnOnce(&mut ViewContext<'_, W>) -> W,
) -> Self::Result<View<W>>
where
- W: Render,
+ W: 'static + Render,
{
self.window_cx.replace_root_view(build_view)
}
@@ -2354,6 +2395,10 @@ impl<V: 'static> VisualContext for ViewContext<'_, V> {
fn focus_view<W: FocusableView>(&mut self, view: &View<W>) -> Self::Result<()> {
self.window_cx.focus_view(view)
}
+
+ fn dismiss_view<W: ManagedView>(&mut self, view: &View<W>) -> Self::Result<()> {
+ self.window_cx.dismiss_view(view)
+ }
}
impl<'a, V> std::ops::Deref for ViewContext<'a, V> {
@@ -2398,6 +2443,17 @@ impl<V: 'static + Render> WindowHandle<V> {
}
}
+ pub fn root<C>(&self, cx: &mut C) -> Result<View<V>>
+ where
+ C: Context,
+ {
+ Flatten::flatten(cx.update_window(self.any_handle, |root_view, _| {
+ root_view
+ .downcast::<V>()
+ .map_err(|_| anyhow!("the type of the window's root view has changed"))
+ }))
+ }
+
pub fn update<C, R>(
&self,
cx: &mut C,
@@ -2543,6 +2599,18 @@ pub enum ElementId {
FocusHandle(FocusId),
}
+impl TryInto<SharedString> for ElementId {
+ type Error = anyhow::Error;
+
+ fn try_into(self) -> anyhow::Result<SharedString> {
+ if let ElementId::Name(name) = self {
+ Ok(name)
+ } else {
+ Err(anyhow!("element id is not string"))
+ }
+ }
+}
+
impl From<EntityId> for ElementId {
fn from(id: EntityId) -> Self {
ElementId::View(id)
@@ -0,0 +1,27 @@
+use proc_macro::TokenStream;
+use quote::quote;
+use syn::{parse_macro_input, DeriveInput};
+
+pub fn derive_render_once(input: TokenStream) -> TokenStream {
+ let ast = parse_macro_input!(input as DeriveInput);
+ let type_name = &ast.ident;
+ let (impl_generics, type_generics, where_clause) = ast.generics.split_for_impl();
+
+ let gen = quote! {
+ impl #impl_generics gpui::RenderOnce for #type_name #type_generics
+ #where_clause
+ {
+ type Element = gpui::CompositeElement<Self>;
+
+ fn element_id(&self) -> Option<ElementId> {
+ None
+ }
+
+ fn render_once(self) -> Self::Element {
+ gpui::CompositeElement::new(self)
+ }
+ }
+ };
+
+ gen.into()
+}
@@ -1,16 +1,12 @@
mod action;
mod derive_component;
+mod derive_render_once;
mod register_action;
mod style_helpers;
mod test;
use proc_macro::TokenStream;
-#[proc_macro]
-pub fn style_helpers(args: TokenStream) -> TokenStream {
- style_helpers::style_helpers(args)
-}
-
#[proc_macro_derive(Action)]
pub fn action(input: TokenStream) -> TokenStream {
action::action(input)
@@ -26,6 +22,16 @@ pub fn derive_component(input: TokenStream) -> TokenStream {
derive_component::derive_component(input)
}
+#[proc_macro_derive(RenderOnce, attributes(view))]
+pub fn derive_render_once(input: TokenStream) -> TokenStream {
+ derive_render_once::derive_render_once(input)
+}
+
+#[proc_macro]
+pub fn style_helpers(input: TokenStream) -> TokenStream {
+ style_helpers::style_helpers(input)
+}
+
#[proc_macro_attribute]
pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
test::test(args, function)
@@ -9,7 +9,7 @@ path = "src/journal2.rs"
doctest = false
[dependencies]
-editor = { path = "../editor" }
+editor = { package = "editor2", path = "../editor2" }
gpui = { package = "gpui2", path = "../gpui2" }
util = { path = "../util" }
workspace2 = { path = "../workspace2" }
@@ -24,4 +24,4 @@ log.workspace = true
shellexpand = "2.1.0"
[dev-dependencies]
-editor = { path = "../editor", features = ["test-support"] }
+editor = { package="editor2", path = "../editor2", features = ["test-support"] }
@@ -61,12 +61,14 @@ fn build_bridge(swift_target: &SwiftTarget) {
let swift_package_root = swift_package_root();
let swift_target_folder = swift_target_folder();
+ let swift_cache_folder = swift_cache_folder();
if !Command::new("swift")
.arg("build")
.arg("--disable-automatic-resolution")
.args(["--configuration", &env::var("PROFILE").unwrap()])
.args(["--triple", &swift_target.target.triple])
.args(["--build-path".into(), swift_target_folder])
+ .args(["--cache-path".into(), swift_cache_folder])
.current_dir(&swift_package_root)
.status()
.unwrap()
@@ -133,9 +135,17 @@ fn swift_package_root() -> PathBuf {
}
fn swift_target_folder() -> PathBuf {
+ let target = env::var("TARGET").unwrap();
env::current_dir()
.unwrap()
- .join(format!("../../target/{SWIFT_PACKAGE_NAME}"))
+ .join(format!("../../target/{target}/{SWIFT_PACKAGE_NAME}_target"))
+}
+
+fn swift_cache_folder() -> PathBuf {
+ let target = env::var("TARGET").unwrap();
+ env::current_dir()
+ .unwrap()
+ .join(format!("../../target/{target}/{SWIFT_PACKAGE_NAME}_cache"))
}
fn copy_dir(source: &Path, destination: &Path) {
@@ -1,10 +1,10 @@
use editor::Editor;
use gpui::{
- div, prelude::*, uniform_list, AppContext, Component, Div, FocusHandle, FocusableView,
- MouseButton, Render, Task, UniformListScrollHandle, View, ViewContext, WindowContext,
+ div, prelude::*, uniform_list, AppContext, Div, FocusHandle, FocusableView, MouseButton,
+ MouseDownEvent, Render, Task, UniformListScrollHandle, View, ViewContext, WindowContext,
};
use std::{cmp, sync::Arc};
-use ui::{prelude::*, v_stack, Divider, Label, TextColor};
+use ui::{prelude::*, v_stack, Color, Divider, Label};
pub struct Picker<D: PickerDelegate> {
pub delegate: D,
@@ -15,7 +15,7 @@ pub struct Picker<D: PickerDelegate> {
}
pub trait PickerDelegate: Sized + 'static {
- type ListItem: Component<Picker<Self>>;
+ type ListItem: RenderOnce;
fn match_count(&self) -> usize;
fn selected_index(&self) -> usize;
@@ -143,10 +143,10 @@ impl<D: PickerDelegate> Picker<D> {
fn on_input_editor_event(
&mut self,
_: View<Editor>,
- event: &editor::Event,
+ event: &editor::EditorEvent,
cx: &mut ViewContext<Self>,
) {
- if let editor::Event::BufferEdited = event {
+ if let editor::EditorEvent::BufferEdited = event {
let query = self.editor.read(cx).text(cx);
self.update_matches(query, cx);
}
@@ -181,20 +181,20 @@ impl<D: PickerDelegate> Picker<D> {
}
impl<D: PickerDelegate> Render for Picker<D> {
- type Element = Div<Self>;
+ type Element = Div;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
div()
.key_context("picker")
.size_full()
.elevation_2(cx)
- .on_action(Self::select_next)
- .on_action(Self::select_prev)
- .on_action(Self::select_first)
- .on_action(Self::select_last)
- .on_action(Self::cancel)
- .on_action(Self::confirm)
- .on_action(Self::secondary_confirm)
+ .on_action(cx.listener(Self::select_next))
+ .on_action(cx.listener(Self::select_prev))
+ .on_action(cx.listener(Self::select_first))
+ .on_action(cx.listener(Self::select_last))
+ .on_action(cx.listener(Self::cancel))
+ .on_action(cx.listener(Self::confirm))
+ .on_action(cx.listener(Self::secondary_confirm))
.child(
v_stack()
.py_0p5()
@@ -208,31 +208,37 @@ impl<D: PickerDelegate> Render for Picker<D> {
.p_1()
.grow()
.child(
- uniform_list("candidates", self.delegate.match_count(), {
- move |this: &mut Self, visible_range, cx| {
- let selected_ix = this.delegate.selected_index();
- visible_range
- .map(|ix| {
- div()
- .on_mouse_down(
- MouseButton::Left,
- move |this: &mut Self, event, cx| {
- this.handle_click(
- ix,
- event.modifiers.command,
- cx,
- )
- },
- )
- .child(this.delegate.render_match(
- ix,
- ix == selected_ix,
- cx,
- ))
- })
- .collect()
- }
- })
+ uniform_list(
+ cx.view().clone(),
+ "candidates",
+ self.delegate.match_count(),
+ {
+ let selected_index = self.delegate.selected_index();
+
+ move |picker, visible_range, cx| {
+ visible_range
+ .map(|ix| {
+ div()
+ .on_mouse_down(
+ MouseButton::Left,
+ cx.listener(move |this, event: &MouseDownEvent, cx| {
+ this.handle_click(
+ ix,
+ event.modifiers.command,
+ cx,
+ )
+ }),
+ )
+ .child(picker.delegate.render_match(
+ ix,
+ ix == selected_index,
+ cx,
+ ))
+ })
+ .collect()
+ }
+ },
+ )
.track_scroll(self.scroll_handle.clone()),
)
.max_h_72()
@@ -244,7 +250,7 @@ impl<D: PickerDelegate> Render for Picker<D> {
v_stack().p_1().grow().child(
div()
.px_1()
- .child(Label::new("No matches").color(TextColor::Muted)),
+ .child(Label::new("No matches").color(Color::Muted)),
),
)
})
@@ -20,10 +20,6 @@ impl IgnoreStack {
Arc::new(Self::All)
}
- pub fn is_all(&self) -> bool {
- matches!(self, IgnoreStack::All)
- }
-
pub fn append(self: Arc<Self>, abs_base_path: Arc<Path>, ignore: Arc<Gitignore>) -> Arc<Self> {
match self.as_ref() {
IgnoreStack::All => self,
@@ -5548,7 +5548,16 @@ impl Project {
.collect::<Vec<_>>();
let background = cx.background().clone();
- let path_count: usize = snapshots.iter().map(|s| s.visible_file_count()).sum();
+ let path_count: usize = snapshots
+ .iter()
+ .map(|s| {
+ if query.include_ignored() {
+ s.file_count()
+ } else {
+ s.visible_file_count()
+ }
+ })
+ .sum();
if path_count == 0 {
let (_, rx) = smol::channel::bounded(1024);
return rx;
@@ -5561,8 +5570,16 @@ impl Project {
.iter()
.filter_map(|(_, b)| {
let buffer = b.upgrade(cx)?;
- let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
- if let Some(path) = snapshot.file().map(|file| file.path()) {
+ let (is_ignored, snapshot) = buffer.update(cx, |buffer, cx| {
+ let is_ignored = buffer
+ .project_path(cx)
+ .and_then(|path| self.entry_for_path(&path, cx))
+ .map_or(false, |entry| entry.is_ignored);
+ (is_ignored, buffer.snapshot())
+ });
+ if is_ignored && !query.include_ignored() {
+ return None;
+ } else if let Some(path) = snapshot.file().map(|file| file.path()) {
Some((path.clone(), (buffer, snapshot)))
} else {
unnamed_files.push(buffer);
@@ -5735,7 +5752,12 @@ impl Project {
let mut snapshot_start_ix = 0;
let mut abs_path = PathBuf::new();
for snapshot in snapshots {
- let snapshot_end_ix = snapshot_start_ix + snapshot.visible_file_count();
+ let snapshot_end_ix = snapshot_start_ix
+ + if query.include_ignored() {
+ snapshot.file_count()
+ } else {
+ snapshot.visible_file_count()
+ };
if worker_end_ix <= snapshot_start_ix {
break;
} else if worker_start_ix > snapshot_end_ix {
@@ -5748,7 +5770,7 @@ impl Project {
cmp::min(worker_end_ix, snapshot_end_ix) - snapshot_start_ix;
for entry in snapshot
- .files(false, start_in_snapshot)
+ .files(query.include_ignored(), start_in_snapshot)
.take(end_in_snapshot - start_in_snapshot)
{
if matching_paths_tx.is_closed() {
@@ -10,6 +10,8 @@ pub struct ProjectSettings {
pub lsp: HashMap<Arc<str>, LspSettings>,
#[serde(default)]
pub git: GitSettings,
+ #[serde(default)]
+ pub file_scan_exclusions: Option<Vec<String>>,
}
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
@@ -3598,7 +3598,7 @@ async fn test_search(cx: &mut gpui::TestAppContext) {
assert_eq!(
search(
&project,
- SearchQuery::text("TWO", false, true, Vec::new(), Vec::new()).unwrap(),
+ SearchQuery::text("TWO", false, true, false, Vec::new(), Vec::new()).unwrap(),
cx
)
.await
@@ -3623,7 +3623,7 @@ async fn test_search(cx: &mut gpui::TestAppContext) {
assert_eq!(
search(
&project,
- SearchQuery::text("TWO", false, true, Vec::new(), Vec::new()).unwrap(),
+ SearchQuery::text("TWO", false, true, false, Vec::new(), Vec::new()).unwrap(),
cx
)
.await
@@ -3662,6 +3662,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
search_query,
false,
true,
+ false,
vec![PathMatcher::new("*.odd").unwrap()],
Vec::new()
)
@@ -3681,6 +3682,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
search_query,
false,
true,
+ false,
vec![PathMatcher::new("*.rs").unwrap()],
Vec::new()
)
@@ -3703,6 +3705,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
search_query,
false,
true,
+ false,
vec![
PathMatcher::new("*.ts").unwrap(),
PathMatcher::new("*.odd").unwrap(),
@@ -3727,6 +3730,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
search_query,
false,
true,
+ false,
vec![
PathMatcher::new("*.rs").unwrap(),
PathMatcher::new("*.ts").unwrap(),
@@ -3774,6 +3778,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
search_query,
false,
true,
+ false,
Vec::new(),
vec![PathMatcher::new("*.odd").unwrap()],
)
@@ -3798,6 +3803,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
search_query,
false,
true,
+ false,
Vec::new(),
vec![PathMatcher::new("*.rs").unwrap()],
)
@@ -3820,6 +3826,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
search_query,
false,
true,
+ false,
Vec::new(),
vec![
PathMatcher::new("*.ts").unwrap(),
@@ -3844,6 +3851,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
search_query,
false,
true,
+ false,
Vec::new(),
vec![
PathMatcher::new("*.rs").unwrap(),
@@ -3885,6 +3893,7 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
search_query,
false,
true,
+ false,
vec![PathMatcher::new("*.odd").unwrap()],
vec![PathMatcher::new("*.odd").unwrap()],
)
@@ -3904,6 +3913,7 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
search_query,
false,
true,
+ false,
vec![PathMatcher::new("*.ts").unwrap()],
vec![PathMatcher::new("*.ts").unwrap()],
).unwrap(),
@@ -3922,6 +3932,7 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
search_query,
false,
true,
+ false,
vec![
PathMatcher::new("*.ts").unwrap(),
PathMatcher::new("*.odd").unwrap()
@@ -3947,6 +3958,7 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
search_query,
false,
true,
+ false,
vec![
PathMatcher::new("*.ts").unwrap(),
PathMatcher::new("*.odd").unwrap()
@@ -39,6 +39,7 @@ pub enum SearchQuery {
replacement: Option<String>,
whole_word: bool,
case_sensitive: bool,
+ include_ignored: bool,
inner: SearchInputs,
},
@@ -48,6 +49,7 @@ pub enum SearchQuery {
multiline: bool,
whole_word: bool,
case_sensitive: bool,
+ include_ignored: bool,
inner: SearchInputs,
},
}
@@ -57,6 +59,7 @@ impl SearchQuery {
query: impl ToString,
whole_word: bool,
case_sensitive: bool,
+ include_ignored: bool,
files_to_include: Vec<PathMatcher>,
files_to_exclude: Vec<PathMatcher>,
) -> Result<Self> {
@@ -74,6 +77,7 @@ impl SearchQuery {
replacement: None,
whole_word,
case_sensitive,
+ include_ignored,
inner,
})
}
@@ -82,6 +86,7 @@ impl SearchQuery {
query: impl ToString,
whole_word: bool,
case_sensitive: bool,
+ include_ignored: bool,
files_to_include: Vec<PathMatcher>,
files_to_exclude: Vec<PathMatcher>,
) -> Result<Self> {
@@ -111,6 +116,7 @@ impl SearchQuery {
multiline,
whole_word,
case_sensitive,
+ include_ignored,
inner,
})
}
@@ -121,6 +127,7 @@ impl SearchQuery {
message.query,
message.whole_word,
message.case_sensitive,
+ message.include_ignored,
deserialize_path_matches(&message.files_to_include)?,
deserialize_path_matches(&message.files_to_exclude)?,
)
@@ -129,6 +136,7 @@ impl SearchQuery {
message.query,
message.whole_word,
message.case_sensitive,
+ message.include_ignored,
deserialize_path_matches(&message.files_to_include)?,
deserialize_path_matches(&message.files_to_exclude)?,
)
@@ -156,6 +164,7 @@ impl SearchQuery {
regex: self.is_regex(),
whole_word: self.whole_word(),
case_sensitive: self.case_sensitive(),
+ include_ignored: self.include_ignored(),
files_to_include: self
.files_to_include()
.iter()
@@ -336,6 +345,17 @@ impl SearchQuery {
}
}
+ pub fn include_ignored(&self) -> bool {
+ match self {
+ Self::Text {
+ include_ignored, ..
+ } => *include_ignored,
+ Self::Regex {
+ include_ignored, ..
+ } => *include_ignored,
+ }
+ }
+
pub fn is_regex(&self) -> bool {
matches!(self, Self::Regex { .. })
}
@@ -1,5 +1,6 @@
use crate::{
- copy_recursive, ignore::IgnoreStack, DiagnosticSummary, ProjectEntryId, RemoveOptions,
+ copy_recursive, ignore::IgnoreStack, project_settings::ProjectSettings, DiagnosticSummary,
+ ProjectEntryId, RemoveOptions,
};
use ::ignore::gitignore::{Gitignore, GitignoreBuilder};
use anyhow::{anyhow, Context, Result};
@@ -21,7 +22,10 @@ use futures::{
};
use fuzzy::CharBag;
use git::{DOT_GIT, GITIGNORE};
-use gpui::{executor, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task};
+use gpui::{
+ executor, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Subscription, Task,
+};
+use itertools::Itertools;
use language::{
proto::{
deserialize_fingerprint, deserialize_version, serialize_fingerprint, serialize_line_ending,
@@ -36,6 +40,7 @@ use postage::{
prelude::{Sink as _, Stream as _},
watch,
};
+use settings::SettingsStore;
use smol::channel::{self, Sender};
use std::{
any::Any,
@@ -55,7 +60,10 @@ use std::{
time::{Duration, SystemTime},
};
use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet};
-use util::{paths::HOME, ResultExt};
+use util::{
+ paths::{PathMatcher, HOME},
+ ResultExt,
+};
#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)]
pub struct WorktreeId(usize);
@@ -70,7 +78,8 @@ pub struct LocalWorktree {
scan_requests_tx: channel::Sender<ScanRequest>,
path_prefixes_to_scan_tx: channel::Sender<Arc<Path>>,
is_scanning: (watch::Sender<bool>, watch::Receiver<bool>),
- _background_scanner_task: Task<()>,
+ _settings_subscription: Subscription,
+ _background_scanner_tasks: Vec<Task<()>>,
share: Option<ShareState>,
diagnostics: HashMap<
Arc<Path>,
@@ -216,6 +225,7 @@ pub struct LocalSnapshot {
/// All of the git repositories in the worktree, indexed by the project entry
/// id of their parent directory.
git_repositories: TreeMap<ProjectEntryId, LocalRepositoryEntry>,
+ file_scan_exclusions: Vec<PathMatcher>,
}
struct BackgroundScannerState {
@@ -299,17 +309,54 @@ impl Worktree {
.await
.context("failed to stat worktree path")?;
+ let closure_fs = Arc::clone(&fs);
+ let closure_next_entry_id = Arc::clone(&next_entry_id);
+ let closure_abs_path = abs_path.to_path_buf();
Ok(cx.add_model(move |cx: &mut ModelContext<Worktree>| {
+ let settings_subscription = cx.observe_global::<SettingsStore, _>(move |this, cx| {
+ if let Self::Local(this) = this {
+ let new_file_scan_exclusions =
+ file_scan_exclusions(settings::get::<ProjectSettings>(cx));
+ if new_file_scan_exclusions != this.snapshot.file_scan_exclusions {
+ this.snapshot.file_scan_exclusions = new_file_scan_exclusions;
+ log::info!(
+ "Re-scanning directories, new scan exclude files: {:?}",
+ this.snapshot
+ .file_scan_exclusions
+ .iter()
+ .map(ToString::to_string)
+ .collect::<Vec<_>>()
+ );
+
+ let (scan_requests_tx, scan_requests_rx) = channel::unbounded();
+ let (path_prefixes_to_scan_tx, path_prefixes_to_scan_rx) =
+ channel::unbounded();
+ this.scan_requests_tx = scan_requests_tx;
+ this.path_prefixes_to_scan_tx = path_prefixes_to_scan_tx;
+ this._background_scanner_tasks = start_background_scan_tasks(
+ &closure_abs_path,
+ this.snapshot(),
+ scan_requests_rx,
+ path_prefixes_to_scan_rx,
+ Arc::clone(&closure_next_entry_id),
+ Arc::clone(&closure_fs),
+ cx,
+ );
+ this.is_scanning = watch::channel_with(true);
+ }
+ }
+ });
+
let root_name = abs_path
.file_name()
.map_or(String::new(), |f| f.to_string_lossy().to_string());
-
let mut snapshot = LocalSnapshot {
+ file_scan_exclusions: file_scan_exclusions(settings::get::<ProjectSettings>(cx)),
ignores_by_parent_abs_path: Default::default(),
git_repositories: Default::default(),
snapshot: Snapshot {
id: WorktreeId::from_usize(cx.model_id()),
- abs_path: abs_path.clone(),
+ abs_path: abs_path.to_path_buf().into(),
root_name: root_name.clone(),
root_char_bag: root_name.chars().map(|c| c.to_ascii_lowercase()).collect(),
entries_by_path: Default::default(),
@@ -334,60 +381,23 @@ impl Worktree {
let (scan_requests_tx, scan_requests_rx) = channel::unbounded();
let (path_prefixes_to_scan_tx, path_prefixes_to_scan_rx) = channel::unbounded();
- let (scan_states_tx, mut scan_states_rx) = mpsc::unbounded();
-
- cx.spawn_weak(|this, mut cx| async move {
- while let Some((state, this)) = scan_states_rx.next().await.zip(this.upgrade(&cx)) {
- this.update(&mut cx, |this, cx| {
- let this = this.as_local_mut().unwrap();
- match state {
- ScanState::Started => {
- *this.is_scanning.0.borrow_mut() = true;
- }
- ScanState::Updated {
- snapshot,
- changes,
- barrier,
- scanning,
- } => {
- *this.is_scanning.0.borrow_mut() = scanning;
- this.set_snapshot(snapshot, changes, cx);
- drop(barrier);
- }
- }
- cx.notify();
- });
- }
- })
- .detach();
-
- let background_scanner_task = cx.background().spawn({
- let fs = fs.clone();
- let snapshot = snapshot.clone();
- let background = cx.background().clone();
- async move {
- let events = fs.watch(&abs_path, Duration::from_millis(100)).await;
- BackgroundScanner::new(
- snapshot,
- next_entry_id,
- fs,
- scan_states_tx,
- background,
- scan_requests_rx,
- path_prefixes_to_scan_rx,
- )
- .run(events)
- .await;
- }
- });
-
+ let task_snapshot = snapshot.clone();
Worktree::Local(LocalWorktree {
snapshot,
is_scanning: watch::channel_with(true),
share: None,
scan_requests_tx,
path_prefixes_to_scan_tx,
- _background_scanner_task: background_scanner_task,
+ _settings_subscription: settings_subscription,
+ _background_scanner_tasks: start_background_scan_tasks(
+ &abs_path,
+ task_snapshot,
+ scan_requests_rx,
+ path_prefixes_to_scan_rx,
+ Arc::clone(&next_entry_id),
+ Arc::clone(&fs),
+ cx,
+ ),
diagnostics: Default::default(),
diagnostic_summaries: Default::default(),
client,
@@ -584,6 +594,76 @@ impl Worktree {
}
}
+fn start_background_scan_tasks(
+ abs_path: &Path,
+ snapshot: LocalSnapshot,
+ scan_requests_rx: channel::Receiver<ScanRequest>,
+ path_prefixes_to_scan_rx: channel::Receiver<Arc<Path>>,
+ next_entry_id: Arc<AtomicUsize>,
+ fs: Arc<dyn Fs>,
+ cx: &mut ModelContext<'_, Worktree>,
+) -> Vec<Task<()>> {
+ let (scan_states_tx, mut scan_states_rx) = mpsc::unbounded();
+ let background_scanner = cx.background().spawn({
+ let abs_path = abs_path.to_path_buf();
+ let background = cx.background().clone();
+ async move {
+ let events = fs.watch(&abs_path, Duration::from_millis(100)).await;
+ BackgroundScanner::new(
+ snapshot,
+ next_entry_id,
+ fs,
+ scan_states_tx,
+ background,
+ scan_requests_rx,
+ path_prefixes_to_scan_rx,
+ )
+ .run(events)
+ .await;
+ }
+ });
+ let scan_state_updater = cx.spawn_weak(|this, mut cx| async move {
+ while let Some((state, this)) = scan_states_rx.next().await.zip(this.upgrade(&cx)) {
+ this.update(&mut cx, |this, cx| {
+ let this = this.as_local_mut().unwrap();
+ match state {
+ ScanState::Started => {
+ *this.is_scanning.0.borrow_mut() = true;
+ }
+ ScanState::Updated {
+ snapshot,
+ changes,
+ barrier,
+ scanning,
+ } => {
+ *this.is_scanning.0.borrow_mut() = scanning;
+ this.set_snapshot(snapshot, changes, cx);
+ drop(barrier);
+ }
+ }
+ cx.notify();
+ });
+ }
+ });
+ vec![background_scanner, scan_state_updater]
+}
+
+fn file_scan_exclusions(project_settings: &ProjectSettings) -> Vec<PathMatcher> {
+ project_settings.file_scan_exclusions.as_deref().unwrap_or(&[]).iter()
+ .sorted()
+ .filter_map(|pattern| {
+ PathMatcher::new(pattern)
+ .map(Some)
+ .unwrap_or_else(|e| {
+ log::error!(
+ "Skipping pattern {pattern} in `file_scan_exclusions` project settings due to parsing error: {e:#}"
+ );
+ None
+ })
+ })
+ .collect()
+}
+
impl LocalWorktree {
pub fn contains_abs_path(&self, path: &Path) -> bool {
path.starts_with(&self.abs_path)
@@ -1481,7 +1561,7 @@ impl Snapshot {
self.entries_by_id.get(&entry_id, &()).is_some()
}
- pub(crate) fn insert_entry(&mut self, entry: proto::Entry) -> Result<Entry> {
+ fn insert_entry(&mut self, entry: proto::Entry) -> Result<Entry> {
let entry = Entry::try_from((&self.root_char_bag, entry))?;
let old_entry = self.entries_by_id.insert_or_replace(
PathEntry {
@@ -2145,6 +2225,12 @@ impl LocalSnapshot {
paths.sort_by(|a, b| a.0.cmp(b.0));
paths
}
+
+ fn is_abs_path_excluded(&self, abs_path: &Path) -> bool {
+ self.file_scan_exclusions
+ .iter()
+ .any(|exclude_matcher| exclude_matcher.is_match(abs_path))
+ }
}
impl BackgroundScannerState {
@@ -2167,7 +2253,7 @@ impl BackgroundScannerState {
let ignore_stack = self.snapshot.ignore_stack_for_abs_path(&abs_path, true);
let mut ancestor_inodes = self.snapshot.ancestor_inodes_for_path(&path);
let mut containing_repository = None;
- if !ignore_stack.is_all() {
+ if !ignore_stack.is_abs_path_ignored(&abs_path, true) {
if let Some((workdir_path, repo)) = self.snapshot.local_repo_for_path(&path) {
if let Ok(repo_path) = path.strip_prefix(&workdir_path.0) {
containing_repository = Some((
@@ -2378,18 +2464,30 @@ impl BackgroundScannerState {
// Remove any git repositories whose .git entry no longer exists.
let snapshot = &mut self.snapshot;
- let mut repositories = mem::take(&mut snapshot.git_repositories);
- let mut repository_entries = mem::take(&mut snapshot.repository_entries);
- repositories.retain(|work_directory_id, _| {
- snapshot
- .entry_for_id(*work_directory_id)
+ let mut ids_to_preserve = HashSet::default();
+ for (&work_directory_id, entry) in snapshot.git_repositories.iter() {
+ let exists_in_snapshot = snapshot
+ .entry_for_id(work_directory_id)
.map_or(false, |entry| {
snapshot.entry_for_path(entry.path.join(*DOT_GIT)).is_some()
- })
- });
- repository_entries.retain(|_, entry| repositories.get(&entry.work_directory.0).is_some());
- snapshot.git_repositories = repositories;
- snapshot.repository_entries = repository_entries;
+ });
+ if exists_in_snapshot {
+ ids_to_preserve.insert(work_directory_id);
+ } else {
+ let git_dir_abs_path = snapshot.abs_path().join(&entry.git_dir_path);
+ if snapshot.is_abs_path_excluded(&git_dir_abs_path)
+ && !matches!(smol::block_on(fs.metadata(&git_dir_abs_path)), Ok(None))
+ {
+ ids_to_preserve.insert(work_directory_id);
+ }
+ }
+ }
+ snapshot
+ .git_repositories
+ .retain(|work_directory_id, _| ids_to_preserve.contains(work_directory_id));
+ snapshot
+ .repository_entries
+ .retain(|_, entry| ids_to_preserve.contains(&entry.work_directory.0));
}
fn build_git_repository(
@@ -3094,7 +3192,7 @@ impl BackgroundScanner {
let ignore_stack = state
.snapshot
.ignore_stack_for_abs_path(&root_abs_path, true);
- if ignore_stack.is_all() {
+ if ignore_stack.is_abs_path_ignored(&root_abs_path, true) {
root_entry.is_ignored = true;
state.insert_entry(root_entry.clone(), self.fs.as_ref());
}
@@ -3231,14 +3329,22 @@ impl BackgroundScanner {
return false;
};
- let parent_dir_is_loaded = relative_path.parent().map_or(true, |parent| {
- snapshot
- .entry_for_path(parent)
- .map_or(false, |entry| entry.kind == EntryKind::Dir)
- });
- if !parent_dir_is_loaded {
- log::debug!("ignoring event {relative_path:?} within unloaded directory");
- return false;
+ if !is_git_related(&abs_path) {
+ let parent_dir_is_loaded = relative_path.parent().map_or(true, |parent| {
+ snapshot
+ .entry_for_path(parent)
+ .map_or(false, |entry| entry.kind == EntryKind::Dir)
+ });
+ if !parent_dir_is_loaded {
+ log::debug!("ignoring event {relative_path:?} within unloaded directory");
+ return false;
+ }
+ if snapshot.is_abs_path_excluded(abs_path) {
+ log::debug!(
+ "ignoring FS event for path {relative_path:?} within excluded directory"
+ );
+ return false;
+ }
}
relative_paths.push(relative_path);
@@ -3401,18 +3507,26 @@ impl BackgroundScanner {
}
async fn scan_dir(&self, job: &ScanJob) -> Result<()> {
- log::debug!("scan directory {:?}", job.path);
-
- let mut ignore_stack = job.ignore_stack.clone();
- let mut new_ignore = None;
- let (root_abs_path, root_char_bag, next_entry_id) = {
- let snapshot = &self.state.lock().snapshot;
- (
- snapshot.abs_path().clone(),
- snapshot.root_char_bag,
- self.next_entry_id.clone(),
- )
- };
+ let root_abs_path;
+ let mut ignore_stack;
+ let mut new_ignore;
+ let root_char_bag;
+ let next_entry_id;
+ {
+ let state = self.state.lock();
+ let snapshot = &state.snapshot;
+ root_abs_path = snapshot.abs_path().clone();
+ if snapshot.is_abs_path_excluded(&job.abs_path) {
+ log::error!("skipping excluded directory {:?}", job.path);
+ return Ok(());
+ }
+ log::debug!("scanning directory {:?}", job.path);
+ ignore_stack = job.ignore_stack.clone();
+ new_ignore = None;
+ root_char_bag = snapshot.root_char_bag;
+ next_entry_id = self.next_entry_id.clone();
+ drop(state);
+ }
let mut dotgit_path = None;
let mut root_canonical_path = None;
@@ -3427,18 +3541,8 @@ impl BackgroundScanner {
continue;
}
};
-
let child_name = child_abs_path.file_name().unwrap();
let child_path: Arc<Path> = job.path.join(child_name).into();
- let child_metadata = match self.fs.metadata(&child_abs_path).await {
- Ok(Some(metadata)) => metadata,
- Ok(None) => continue,
- Err(err) => {
- log::error!("error processing {:?}: {:?}", child_abs_path, err);
- continue;
- }
- };
-
// If we find a .gitignore, add it to the stack of ignores used to determine which paths are ignored
if child_name == *GITIGNORE {
match build_gitignore(&child_abs_path, self.fs.as_ref()).await {
@@ -3482,6 +3586,26 @@ impl BackgroundScanner {
dotgit_path = Some(child_path.clone());
}
+ {
+ let mut state = self.state.lock();
+ if state.snapshot.is_abs_path_excluded(&child_abs_path) {
+ let relative_path = job.path.join(child_name);
+ log::debug!("skipping excluded child entry {relative_path:?}");
+ state.remove_path(&relative_path);
+ continue;
+ }
+ drop(state);
+ }
+
+ let child_metadata = match self.fs.metadata(&child_abs_path).await {
+ Ok(Some(metadata)) => metadata,
+ Ok(None) => continue,
+ Err(err) => {
+ log::error!("error processing {child_abs_path:?}: {err:?}");
+ continue;
+ }
+ };
+
let mut child_entry = Entry::new(
child_path.clone(),
&child_metadata,
@@ -3662,19 +3786,16 @@ impl BackgroundScanner {
self.next_entry_id.as_ref(),
state.snapshot.root_char_bag,
);
- fs_entry.is_ignored = ignore_stack.is_all();
+ let is_dir = fs_entry.is_dir();
+ fs_entry.is_ignored = ignore_stack.is_abs_path_ignored(&abs_path, is_dir);
fs_entry.is_external = !canonical_path.starts_with(&root_canonical_path);
- if !fs_entry.is_ignored {
- if !fs_entry.is_dir() {
- if let Some((work_dir, repo)) =
- state.snapshot.local_repo_for_path(&path)
- {
- if let Ok(repo_path) = path.strip_prefix(work_dir.0) {
- let repo_path = RepoPath(repo_path.into());
- let repo = repo.repo_ptr.lock();
- fs_entry.git_status = repo.status(&repo_path, fs_entry.mtime);
- }
+ if !is_dir && !fs_entry.is_ignored {
+ if let Some((work_dir, repo)) = state.snapshot.local_repo_for_path(&path) {
+ if let Ok(repo_path) = path.strip_prefix(work_dir.0) {
+ let repo_path = RepoPath(repo_path.into());
+ let repo = repo.repo_ptr.lock();
+ fs_entry.git_status = repo.status(&repo_path, fs_entry.mtime);
}
}
}
@@ -3833,8 +3954,7 @@ impl BackgroundScanner {
ignore_stack.clone()
};
- // Scan any directories that were previously ignored and weren't
- // previously scanned.
+ // Scan any directories that were previously ignored and weren't previously scanned.
if was_ignored && !entry.is_ignored && entry.kind.is_unloaded() {
let state = self.state.lock();
if state.should_scan_directory(&entry) {
@@ -4010,6 +4130,12 @@ impl BackgroundScanner {
}
}
+fn is_git_related(abs_path: &Path) -> bool {
+ abs_path
+ .components()
+ .any(|c| c.as_os_str() == *DOT_GIT || c.as_os_str() == *GITIGNORE)
+}
+
fn char_bag_for_path(root_char_bag: CharBag, path: &Path) -> CharBag {
let mut result = root_char_bag;
result.extend(
@@ -1,6 +1,7 @@
use crate::{
+ project_settings::ProjectSettings,
worktree::{Event, Snapshot, WorktreeModelHandle},
- Entry, EntryKind, PathChange, Worktree,
+ Entry, EntryKind, PathChange, Project, Worktree,
};
use anyhow::Result;
use client::Client;
@@ -12,6 +13,7 @@ use postage::stream::Stream;
use pretty_assertions::assert_eq;
use rand::prelude::*;
use serde_json::json;
+use settings::SettingsStore;
use std::{
env,
fmt::Write,
@@ -23,6 +25,7 @@ use util::{http::FakeHttpClient, test::temp_tree, ResultExt};
#[gpui::test]
async fn test_traversal(cx: &mut TestAppContext) {
+ init_test(cx);
let fs = FakeFs::new(cx.background());
fs.insert_tree(
"/root",
@@ -78,6 +81,7 @@ async fn test_traversal(cx: &mut TestAppContext) {
#[gpui::test]
async fn test_descendent_entries(cx: &mut TestAppContext) {
+ init_test(cx);
let fs = FakeFs::new(cx.background());
fs.insert_tree(
"/root",
@@ -185,6 +189,7 @@ async fn test_descendent_entries(cx: &mut TestAppContext) {
#[gpui::test(iterations = 10)]
async fn test_circular_symlinks(executor: Arc<Deterministic>, cx: &mut TestAppContext) {
+ init_test(cx);
let fs = FakeFs::new(cx.background());
fs.insert_tree(
"/root",
@@ -264,6 +269,7 @@ async fn test_circular_symlinks(executor: Arc<Deterministic>, cx: &mut TestAppCo
#[gpui::test]
async fn test_symlinks_pointing_outside(cx: &mut TestAppContext) {
+ init_test(cx);
let fs = FakeFs::new(cx.background());
fs.insert_tree(
"/root",
@@ -439,6 +445,7 @@ async fn test_symlinks_pointing_outside(cx: &mut TestAppContext) {
#[gpui::test]
async fn test_open_gitignored_files(cx: &mut TestAppContext) {
+ init_test(cx);
let fs = FakeFs::new(cx.background());
fs.insert_tree(
"/root",
@@ -599,6 +606,7 @@ async fn test_open_gitignored_files(cx: &mut TestAppContext) {
#[gpui::test]
async fn test_dirs_no_longer_ignored(cx: &mut TestAppContext) {
+ init_test(cx);
let fs = FakeFs::new(cx.background());
fs.insert_tree(
"/root",
@@ -722,6 +730,14 @@ async fn test_dirs_no_longer_ignored(cx: &mut TestAppContext) {
#[gpui::test(iterations = 10)]
async fn test_rescan_with_gitignore(cx: &mut TestAppContext) {
+ init_test(cx);
+ cx.update(|cx| {
+ cx.update_global::<SettingsStore, _, _>(|store, cx| {
+ store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
+ project_settings.file_scan_exclusions = Some(Vec::new());
+ });
+ });
+ });
let fs = FakeFs::new(cx.background());
fs.insert_tree(
"/root",
@@ -827,6 +843,7 @@ async fn test_rescan_with_gitignore(cx: &mut TestAppContext) {
#[gpui::test]
async fn test_write_file(cx: &mut TestAppContext) {
+ init_test(cx);
let dir = temp_tree(json!({
".git": {},
".gitignore": "ignored-dir\n",
@@ -877,8 +894,105 @@ async fn test_write_file(cx: &mut TestAppContext) {
});
}
+#[gpui::test]
+async fn test_file_scan_exclusions(cx: &mut TestAppContext) {
+ init_test(cx);
+ let dir = temp_tree(json!({
+ ".gitignore": "**/target\n/node_modules\n",
+ "target": {
+ "index": "blah2"
+ },
+ "node_modules": {
+ ".DS_Store": "",
+ "prettier": {
+ "package.json": "{}",
+ },
+ },
+ "src": {
+ ".DS_Store": "",
+ "foo": {
+ "foo.rs": "mod another;\n",
+ "another.rs": "// another",
+ },
+ "bar": {
+ "bar.rs": "// bar",
+ },
+ "lib.rs": "mod foo;\nmod bar;\n",
+ },
+ ".DS_Store": "",
+ }));
+ cx.update(|cx| {
+ cx.update_global::<SettingsStore, _, _>(|store, cx| {
+ store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
+ project_settings.file_scan_exclusions =
+ Some(vec!["**/foo/**".to_string(), "**/.DS_Store".to_string()]);
+ });
+ });
+ });
+
+ let tree = Worktree::local(
+ build_client(cx),
+ dir.path(),
+ true,
+ Arc::new(RealFs),
+ Default::default(),
+ &mut cx.to_async(),
+ )
+ .await
+ .unwrap();
+ cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+ .await;
+ tree.flush_fs_events(cx).await;
+ tree.read_with(cx, |tree, _| {
+ check_worktree_entries(
+ tree,
+ &[
+ "src/foo/foo.rs",
+ "src/foo/another.rs",
+ "node_modules/.DS_Store",
+ "src/.DS_Store",
+ ".DS_Store",
+ ],
+ &["target", "node_modules"],
+ &["src/lib.rs", "src/bar/bar.rs", ".gitignore"],
+ )
+ });
+
+ cx.update(|cx| {
+ cx.update_global::<SettingsStore, _, _>(|store, cx| {
+ store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
+ project_settings.file_scan_exclusions =
+ Some(vec!["**/node_modules/**".to_string()]);
+ });
+ });
+ });
+ tree.flush_fs_events(cx).await;
+ cx.foreground().run_until_parked();
+ tree.read_with(cx, |tree, _| {
+ check_worktree_entries(
+ tree,
+ &[
+ "node_modules/prettier/package.json",
+ "node_modules/.DS_Store",
+ "node_modules",
+ ],
+ &["target"],
+ &[
+ ".gitignore",
+ "src/lib.rs",
+ "src/bar/bar.rs",
+ "src/foo/foo.rs",
+ "src/foo/another.rs",
+ "src/.DS_Store",
+ ".DS_Store",
+ ],
+ )
+ });
+}
+
#[gpui::test(iterations = 30)]
async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
+ init_test(cx);
let fs = FakeFs::new(cx.background());
fs.insert_tree(
"/root",
@@ -938,6 +1052,7 @@ async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
#[gpui::test]
async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
+ init_test(cx);
let client_fake = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
let fs_fake = FakeFs::new(cx.background());
@@ -1054,6 +1169,7 @@ async fn test_random_worktree_operations_during_initial_scan(
cx: &mut TestAppContext,
mut rng: StdRng,
) {
+ init_test(cx);
let operations = env::var("OPERATIONS")
.map(|o| o.parse().unwrap())
.unwrap_or(5);
@@ -1143,6 +1259,7 @@ async fn test_random_worktree_operations_during_initial_scan(
#[gpui::test(iterations = 100)]
async fn test_random_worktree_changes(cx: &mut TestAppContext, mut rng: StdRng) {
+ init_test(cx);
let operations = env::var("OPERATIONS")
.map(|o| o.parse().unwrap())
.unwrap_or(40);
@@ -1557,6 +1674,7 @@ fn random_filename(rng: &mut impl Rng) -> String {
#[gpui::test]
async fn test_rename_work_directory(cx: &mut TestAppContext) {
+ init_test(cx);
let root = temp_tree(json!({
"projects": {
"project1": {
@@ -1627,6 +1745,7 @@ async fn test_rename_work_directory(cx: &mut TestAppContext) {
#[gpui::test]
async fn test_git_repository_for_path(cx: &mut TestAppContext) {
+ init_test(cx);
let root = temp_tree(json!({
"c.txt": "",
"dir1": {
@@ -1747,6 +1866,15 @@ async fn test_git_repository_for_path(cx: &mut TestAppContext) {
#[gpui::test]
async fn test_git_status(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
+ init_test(cx);
+ cx.update(|cx| {
+ cx.update_global::<SettingsStore, _, _>(|store, cx| {
+ store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
+ project_settings.file_scan_exclusions =
+ Some(vec!["**/.git".to_string(), "**/.gitignore".to_string()]);
+ });
+ });
+ });
const IGNORE_RULE: &'static str = "**/target";
let root = temp_tree(json!({
@@ -1935,6 +2063,7 @@ async fn test_git_status(deterministic: Arc<Deterministic>, cx: &mut TestAppCont
#[gpui::test]
async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
+ init_test(cx);
let fs = FakeFs::new(cx.background());
fs.insert_tree(
"/root",
@@ -2139,3 +2268,44 @@ fn git_status(repo: &git2::Repository) -> collections::HashMap<String, git2::Sta
.map(|status| (status.path().unwrap().to_string(), status.status()))
.collect()
}
+
+#[track_caller]
+fn check_worktree_entries(
+ tree: &Worktree,
+ expected_excluded_paths: &[&str],
+ expected_ignored_paths: &[&str],
+ expected_tracked_paths: &[&str],
+) {
+ for path in expected_excluded_paths {
+ let entry = tree.entry_for_path(path);
+ assert!(
+ entry.is_none(),
+ "expected path '{path}' to be excluded, but got entry: {entry:?}",
+ );
+ }
+ for path in expected_ignored_paths {
+ let entry = tree
+ .entry_for_path(path)
+ .unwrap_or_else(|| panic!("Missing entry for expected ignored path '{path}'"));
+ assert!(
+ entry.is_ignored,
+ "expected path '{path}' to be ignored, but got entry: {entry:?}",
+ );
+ }
+ for path in expected_tracked_paths {
+ let entry = tree
+ .entry_for_path(path)
+ .unwrap_or_else(|| panic!("Missing entry for expected tracked path '{path}'"));
+ assert!(
+ !entry.is_ignored,
+ "expected path '{path}' to be tracked, but got entry: {entry:?}",
+ );
+ }
+}
+
+fn init_test(cx: &mut gpui::TestAppContext) {
+ cx.update(|cx| {
+ cx.set_global(SettingsStore::test(cx));
+ Project::init_settings(cx);
+ });
+}
@@ -20,10 +20,6 @@ impl IgnoreStack {
Arc::new(Self::All)
}
- pub fn is_all(&self) -> bool {
- matches!(self, IgnoreStack::All)
- }
-
pub fn append(self: Arc<Self>, abs_base_path: Arc<Path>, ignore: Arc<Gitignore>) -> Arc<Self> {
match self.as_ref() {
IgnoreStack::All => self,
@@ -5618,7 +5618,16 @@ impl Project {
.collect::<Vec<_>>();
let background = cx.background_executor().clone();
- let path_count: usize = snapshots.iter().map(|s| s.visible_file_count()).sum();
+ let path_count: usize = snapshots
+ .iter()
+ .map(|s| {
+ if query.include_ignored() {
+ s.file_count()
+ } else {
+ s.visible_file_count()
+ }
+ })
+ .sum();
if path_count == 0 {
let (_, rx) = smol::channel::bounded(1024);
return rx;
@@ -5631,8 +5640,16 @@ impl Project {
.iter()
.filter_map(|(_, b)| {
let buffer = b.upgrade()?;
- let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
- if let Some(path) = snapshot.file().map(|file| file.path()) {
+ let (is_ignored, snapshot) = buffer.update(cx, |buffer, cx| {
+ let is_ignored = buffer
+ .project_path(cx)
+ .and_then(|path| self.entry_for_path(&path, cx))
+ .map_or(false, |entry| entry.is_ignored);
+ (is_ignored, buffer.snapshot())
+ });
+ if is_ignored && !query.include_ignored() {
+ return None;
+ } else if let Some(path) = snapshot.file().map(|file| file.path()) {
Some((path.clone(), (buffer, snapshot)))
} else {
unnamed_files.push(buffer);
@@ -5806,7 +5823,12 @@ impl Project {
let mut snapshot_start_ix = 0;
let mut abs_path = PathBuf::new();
for snapshot in snapshots {
- let snapshot_end_ix = snapshot_start_ix + snapshot.visible_file_count();
+ let snapshot_end_ix = snapshot_start_ix
+ + if query.include_ignored() {
+ snapshot.file_count()
+ } else {
+ snapshot.visible_file_count()
+ };
if worker_end_ix <= snapshot_start_ix {
break;
} else if worker_start_ix > snapshot_end_ix {
@@ -5819,7 +5841,7 @@ impl Project {
cmp::min(worker_end_ix, snapshot_end_ix) - snapshot_start_ix;
for entry in snapshot
- .files(false, start_in_snapshot)
+ .files(query.include_ignored(), start_in_snapshot)
.take(end_in_snapshot - start_in_snapshot)
{
if matching_paths_tx.is_closed() {
@@ -11,6 +11,8 @@ pub struct ProjectSettings {
pub lsp: HashMap<Arc<str>, LspSettings>,
#[serde(default)]
pub git: GitSettings,
+ #[serde(default)]
+ pub file_scan_exclusions: Option<Vec<String>>,
}
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
@@ -2633,6 +2633,60 @@ async fn test_file_changes_multiple_times_on_disk(cx: &mut gpui::TestAppContext)
.unwrap();
worktree.next_event(cx);
+ cx.executor().run_until_parked();
+ let on_disk_text = fs.load(Path::new("/dir/file1")).await.unwrap();
+ buffer.read_with(cx, |buffer, _| {
+ assert_eq!(buffer.text(), on_disk_text);
+ assert!(!buffer.is_dirty(), "buffer should not be dirty");
+ assert!(!buffer.has_conflict(), "buffer should not be dirty");
+ });
+}
+
+#[gpui::test(iterations = 30)]
+async fn test_edit_buffer_while_it_reloads(cx: &mut gpui::TestAppContext) {
+ init_test(cx);
+
+ let fs = FakeFs::new(cx.executor().clone());
+ fs.insert_tree(
+ "/dir",
+ json!({
+ "file1": "the original contents",
+ }),
+ )
+ .await;
+
+ let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
+ let worktree = project.read_with(cx, |project, _| project.worktrees().next().unwrap());
+ let buffer = project
+ .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
+ .await
+ .unwrap();
+
+ // Simulate buffer diffs being slow, so that they don't complete before
+ // the next file change occurs.
+ cx.executor().deprioritize(*language::BUFFER_DIFF_TASK);
+
+ // Change the buffer's file on disk, and then wait for the file change
+ // to be detected by the worktree, so that the buffer starts reloading.
+ fs.save(
+ "/dir/file1".as_ref(),
+ &"the first contents".into(),
+ Default::default(),
+ )
+ .await
+ .unwrap();
+ worktree.next_event(cx);
+
+ cx.executor()
+ .spawn(cx.executor().simulate_random_delay())
+ .await;
+
+ // Perform a noop edit, causing the buffer's version to increase.
+ buffer.update(cx, |buffer, cx| {
+ buffer.edit([(0..0, " ")], None, cx);
+ buffer.undo(cx);
+ });
+
cx.executor().run_until_parked();
let on_disk_text = fs.load(Path::new("/dir/file1")).await.unwrap();
buffer.read_with(cx, |buffer, _| {
@@ -2646,10 +2700,8 @@ async fn test_file_changes_multiple_times_on_disk(cx: &mut gpui::TestAppContext)
// If the file change occurred while the buffer was processing the first
// change, the buffer will be in a conflicting state.
else {
- assert!(
- buffer.is_dirty() && buffer.has_conflict(),
- "buffer should report that it has a conflict. text: {buffer_text:?}, disk text: {on_disk_text:?}"
- );
+ assert!(buffer.is_dirty(), "buffer should report that it is dirty. text: {buffer_text:?}, disk text: {on_disk_text:?}");
+ assert!(buffer.has_conflict(), "buffer should report that it is dirty. text: {buffer_text:?}, disk text: {on_disk_text:?}");
}
});
}
@@ -3678,7 +3730,7 @@ async fn test_search(cx: &mut gpui::TestAppContext) {
assert_eq!(
search(
&project,
- SearchQuery::text("TWO", false, true, Vec::new(), Vec::new()).unwrap(),
+ SearchQuery::text("TWO", false, true, false, Vec::new(), Vec::new()).unwrap(),
cx
)
.await
@@ -3703,7 +3755,7 @@ async fn test_search(cx: &mut gpui::TestAppContext) {
assert_eq!(
search(
&project,
- SearchQuery::text("TWO", false, true, Vec::new(), Vec::new()).unwrap(),
+ SearchQuery::text("TWO", false, true, false, Vec::new(), Vec::new()).unwrap(),
cx
)
.await
@@ -3742,6 +3794,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
search_query,
false,
true,
+ false,
vec![PathMatcher::new("*.odd").unwrap()],
Vec::new()
)
@@ -3761,6 +3814,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
search_query,
false,
true,
+ false,
vec![PathMatcher::new("*.rs").unwrap()],
Vec::new()
)
@@ -3783,6 +3837,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
search_query,
false,
true,
+ false,
vec![
PathMatcher::new("*.ts").unwrap(),
PathMatcher::new("*.odd").unwrap(),
@@ -3807,6 +3862,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
search_query,
false,
true,
+ false,
vec![
PathMatcher::new("*.rs").unwrap(),
PathMatcher::new("*.ts").unwrap(),
@@ -3854,6 +3910,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
search_query,
false,
true,
+ false,
Vec::new(),
vec![PathMatcher::new("*.odd").unwrap()],
)
@@ -3878,6 +3935,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
search_query,
false,
true,
+ false,
Vec::new(),
vec![PathMatcher::new("*.rs").unwrap()],
)
@@ -3900,6 +3958,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
search_query,
false,
true,
+ false,
Vec::new(),
vec![
PathMatcher::new("*.ts").unwrap(),
@@ -3924,6 +3983,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
search_query,
false,
true,
+ false,
Vec::new(),
vec![
PathMatcher::new("*.rs").unwrap(),
@@ -3965,6 +4025,7 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
search_query,
false,
true,
+ false,
vec![PathMatcher::new("*.odd").unwrap()],
vec![PathMatcher::new("*.odd").unwrap()],
)
@@ -3984,6 +4045,7 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
search_query,
false,
true,
+ false,
vec![PathMatcher::new("*.ts").unwrap()],
vec![PathMatcher::new("*.ts").unwrap()],
).unwrap(),
@@ -4002,6 +4064,7 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
search_query,
false,
true,
+ false,
vec![
PathMatcher::new("*.ts").unwrap(),
PathMatcher::new("*.odd").unwrap()
@@ -4027,6 +4090,7 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
search_query,
false,
true,
+ false,
vec![
PathMatcher::new("*.ts").unwrap(),
PathMatcher::new("*.odd").unwrap()
@@ -4084,7 +4148,7 @@ async fn search(
fn init_test(cx: &mut gpui::TestAppContext) {
if std::env::var("RUST_LOG").is_ok() {
- env_logger::init();
+ env_logger::try_init().ok();
}
cx.update(|cx| {
@@ -39,6 +39,7 @@ pub enum SearchQuery {
replacement: Option<String>,
whole_word: bool,
case_sensitive: bool,
+ include_ignored: bool,
inner: SearchInputs,
},
@@ -48,6 +49,7 @@ pub enum SearchQuery {
multiline: bool,
whole_word: bool,
case_sensitive: bool,
+ include_ignored: bool,
inner: SearchInputs,
},
}
@@ -57,6 +59,7 @@ impl SearchQuery {
query: impl ToString,
whole_word: bool,
case_sensitive: bool,
+ include_ignored: bool,
files_to_include: Vec<PathMatcher>,
files_to_exclude: Vec<PathMatcher>,
) -> Result<Self> {
@@ -74,6 +77,7 @@ impl SearchQuery {
replacement: None,
whole_word,
case_sensitive,
+ include_ignored,
inner,
})
}
@@ -82,6 +86,7 @@ impl SearchQuery {
query: impl ToString,
whole_word: bool,
case_sensitive: bool,
+ include_ignored: bool,
files_to_include: Vec<PathMatcher>,
files_to_exclude: Vec<PathMatcher>,
) -> Result<Self> {
@@ -111,6 +116,7 @@ impl SearchQuery {
multiline,
whole_word,
case_sensitive,
+ include_ignored,
inner,
})
}
@@ -121,6 +127,7 @@ impl SearchQuery {
message.query,
message.whole_word,
message.case_sensitive,
+ message.include_ignored,
deserialize_path_matches(&message.files_to_include)?,
deserialize_path_matches(&message.files_to_exclude)?,
)
@@ -129,6 +136,7 @@ impl SearchQuery {
message.query,
message.whole_word,
message.case_sensitive,
+ message.include_ignored,
deserialize_path_matches(&message.files_to_include)?,
deserialize_path_matches(&message.files_to_exclude)?,
)
@@ -156,6 +164,7 @@ impl SearchQuery {
regex: self.is_regex(),
whole_word: self.whole_word(),
case_sensitive: self.case_sensitive(),
+ include_ignored: self.include_ignored(),
files_to_include: self
.files_to_include()
.iter()
@@ -336,6 +345,17 @@ impl SearchQuery {
}
}
+ pub fn include_ignored(&self) -> bool {
+ match self {
+ Self::Text {
+ include_ignored, ..
+ } => *include_ignored,
+ Self::Regex {
+ include_ignored, ..
+ } => *include_ignored,
+ }
+ }
+
pub fn is_regex(&self) -> bool {
matches!(self, Self::Regex { .. })
}
@@ -1,5 +1,6 @@
use crate::{
- copy_recursive, ignore::IgnoreStack, DiagnosticSummary, ProjectEntryId, RemoveOptions,
+ copy_recursive, ignore::IgnoreStack, project_settings::ProjectSettings, DiagnosticSummary,
+ ProjectEntryId, RemoveOptions,
};
use ::ignore::gitignore::{Gitignore, GitignoreBuilder};
use anyhow::{anyhow, Context as _, Result};
@@ -25,6 +26,7 @@ use gpui::{
AppContext, AsyncAppContext, BackgroundExecutor, Context, EventEmitter, Model, ModelContext,
Task,
};
+use itertools::Itertools;
use language::{
proto::{
deserialize_fingerprint, deserialize_version, serialize_fingerprint, serialize_line_ending,
@@ -39,6 +41,7 @@ use postage::{
prelude::{Sink as _, Stream as _},
watch,
};
+use settings::{Settings, SettingsStore};
use smol::channel::{self, Sender};
use std::{
any::Any,
@@ -58,7 +61,10 @@ use std::{
time::{Duration, SystemTime},
};
use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet};
-use util::{paths::HOME, ResultExt};
+use util::{
+ paths::{PathMatcher, HOME},
+ ResultExt,
+};
#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)]
pub struct WorktreeId(usize);
@@ -73,7 +79,7 @@ pub struct LocalWorktree {
scan_requests_tx: channel::Sender<ScanRequest>,
path_prefixes_to_scan_tx: channel::Sender<Arc<Path>>,
is_scanning: (watch::Sender<bool>, watch::Receiver<bool>),
- _background_scanner_task: Task<()>,
+ _background_scanner_tasks: Vec<Task<()>>,
share: Option<ShareState>,
diagnostics: HashMap<
Arc<Path>,
@@ -219,6 +225,7 @@ pub struct LocalSnapshot {
/// All of the git repositories in the worktree, indexed by the project entry
/// id of their parent directory.
git_repositories: TreeMap<ProjectEntryId, LocalRepositoryEntry>,
+ file_scan_exclusions: Vec<PathMatcher>,
}
struct BackgroundScannerState {
@@ -302,17 +309,56 @@ impl Worktree {
.await
.context("failed to stat worktree path")?;
+ let closure_fs = Arc::clone(&fs);
+ let closure_next_entry_id = Arc::clone(&next_entry_id);
+ let closure_abs_path = abs_path.to_path_buf();
cx.build_model(move |cx: &mut ModelContext<Worktree>| {
+ cx.observe_global::<SettingsStore>(move |this, cx| {
+ if let Self::Local(this) = this {
+ let new_file_scan_exclusions =
+ file_scan_exclusions(ProjectSettings::get_global(cx));
+ if new_file_scan_exclusions != this.snapshot.file_scan_exclusions {
+ this.snapshot.file_scan_exclusions = new_file_scan_exclusions;
+ log::info!(
+ "Re-scanning directories, new scan exclude files: {:?}",
+ this.snapshot
+ .file_scan_exclusions
+ .iter()
+ .map(ToString::to_string)
+ .collect::<Vec<_>>()
+ );
+
+ let (scan_requests_tx, scan_requests_rx) = channel::unbounded();
+ let (path_prefixes_to_scan_tx, path_prefixes_to_scan_rx) =
+ channel::unbounded();
+ this.scan_requests_tx = scan_requests_tx;
+ this.path_prefixes_to_scan_tx = path_prefixes_to_scan_tx;
+ this._background_scanner_tasks = start_background_scan_tasks(
+ &closure_abs_path,
+ this.snapshot(),
+ scan_requests_rx,
+ path_prefixes_to_scan_rx,
+ Arc::clone(&closure_next_entry_id),
+ Arc::clone(&closure_fs),
+ cx,
+ );
+ this.is_scanning = watch::channel_with(true);
+ }
+ }
+ })
+ .detach();
+
let root_name = abs_path
.file_name()
.map_or(String::new(), |f| f.to_string_lossy().to_string());
let mut snapshot = LocalSnapshot {
+ file_scan_exclusions: file_scan_exclusions(ProjectSettings::get_global(cx)),
ignores_by_parent_abs_path: Default::default(),
git_repositories: Default::default(),
snapshot: Snapshot {
id: WorktreeId::from_usize(cx.entity_id().as_u64() as usize),
- abs_path: abs_path.clone(),
+ abs_path: abs_path.to_path_buf().into(),
root_name: root_name.clone(),
root_char_bag: root_name.chars().map(|c| c.to_ascii_lowercase()).collect(),
entries_by_path: Default::default(),
@@ -337,61 +383,22 @@ impl Worktree {
let (scan_requests_tx, scan_requests_rx) = channel::unbounded();
let (path_prefixes_to_scan_tx, path_prefixes_to_scan_rx) = channel::unbounded();
- let (scan_states_tx, mut scan_states_rx) = mpsc::unbounded();
-
- cx.spawn(|this, mut cx| async move {
- while let Some((state, this)) = scan_states_rx.next().await.zip(this.upgrade()) {
- this.update(&mut cx, |this, cx| {
- let this = this.as_local_mut().unwrap();
- match state {
- ScanState::Started => {
- *this.is_scanning.0.borrow_mut() = true;
- }
- ScanState::Updated {
- snapshot,
- changes,
- barrier,
- scanning,
- } => {
- *this.is_scanning.0.borrow_mut() = scanning;
- this.set_snapshot(snapshot, changes, cx);
- drop(barrier);
- }
- }
- cx.notify();
- })
- .ok();
- }
- })
- .detach();
-
- let background_scanner_task = cx.background_executor().spawn({
- let fs = fs.clone();
- let snapshot = snapshot.clone();
- let background = cx.background_executor().clone();
- async move {
- let events = fs.watch(&abs_path, Duration::from_millis(100)).await;
- BackgroundScanner::new(
- snapshot,
- next_entry_id,
- fs,
- scan_states_tx,
- background,
- scan_requests_rx,
- path_prefixes_to_scan_rx,
- )
- .run(events)
- .await;
- }
- });
-
+ let task_snapshot = snapshot.clone();
Worktree::Local(LocalWorktree {
snapshot,
is_scanning: watch::channel_with(true),
share: None,
scan_requests_tx,
path_prefixes_to_scan_tx,
- _background_scanner_task: background_scanner_task,
+ _background_scanner_tasks: start_background_scan_tasks(
+ &abs_path,
+ task_snapshot,
+ scan_requests_rx,
+ path_prefixes_to_scan_rx,
+ Arc::clone(&next_entry_id),
+ Arc::clone(&fs),
+ cx,
+ ),
diagnostics: Default::default(),
diagnostic_summaries: Default::default(),
client,
@@ -584,6 +591,77 @@ impl Worktree {
}
}
+fn start_background_scan_tasks(
+ abs_path: &Path,
+ snapshot: LocalSnapshot,
+ scan_requests_rx: channel::Receiver<ScanRequest>,
+ path_prefixes_to_scan_rx: channel::Receiver<Arc<Path>>,
+ next_entry_id: Arc<AtomicUsize>,
+ fs: Arc<dyn Fs>,
+ cx: &mut ModelContext<'_, Worktree>,
+) -> Vec<Task<()>> {
+ let (scan_states_tx, mut scan_states_rx) = mpsc::unbounded();
+ let background_scanner = cx.background_executor().spawn({
+ let abs_path = abs_path.to_path_buf();
+ let background = cx.background_executor().clone();
+ async move {
+ let events = fs.watch(&abs_path, Duration::from_millis(100)).await;
+ BackgroundScanner::new(
+ snapshot,
+ next_entry_id,
+ fs,
+ scan_states_tx,
+ background,
+ scan_requests_rx,
+ path_prefixes_to_scan_rx,
+ )
+ .run(events)
+ .await;
+ }
+ });
+ let scan_state_updater = cx.spawn(|this, mut cx| async move {
+ while let Some((state, this)) = scan_states_rx.next().await.zip(this.upgrade()) {
+ this.update(&mut cx, |this, cx| {
+ let this = this.as_local_mut().unwrap();
+ match state {
+ ScanState::Started => {
+ *this.is_scanning.0.borrow_mut() = true;
+ }
+ ScanState::Updated {
+ snapshot,
+ changes,
+ barrier,
+ scanning,
+ } => {
+ *this.is_scanning.0.borrow_mut() = scanning;
+ this.set_snapshot(snapshot, changes, cx);
+ drop(barrier);
+ }
+ }
+ cx.notify();
+ })
+ .ok();
+ }
+ });
+ vec![background_scanner, scan_state_updater]
+}
+
+fn file_scan_exclusions(project_settings: &ProjectSettings) -> Vec<PathMatcher> {
+ project_settings.file_scan_exclusions.as_deref().unwrap_or(&[]).iter()
+ .sorted()
+ .filter_map(|pattern| {
+ PathMatcher::new(pattern)
+ .map(Some)
+ .unwrap_or_else(|e| {
+ log::error!(
+ "Skipping pattern {pattern} in `file_scan_exclusions` project settings due to parsing error: {e:#}"
+ );
+ None
+ })
+ })
+ .collect()
+}
+
impl LocalWorktree {
pub fn contains_abs_path(&self, path: &Path) -> bool {
path.starts_with(&self.abs_path)
@@ -1482,7 +1560,7 @@ impl Snapshot {
self.entries_by_id.get(&entry_id, &()).is_some()
}
- pub(crate) fn insert_entry(&mut self, entry: proto::Entry) -> Result<Entry> {
+ fn insert_entry(&mut self, entry: proto::Entry) -> Result<Entry> {
let entry = Entry::try_from((&self.root_char_bag, entry))?;
let old_entry = self.entries_by_id.insert_or_replace(
PathEntry {
@@ -2143,6 +2221,12 @@ impl LocalSnapshot {
paths.sort_by(|a, b| a.0.cmp(b.0));
paths
}
+
+ fn is_abs_path_excluded(&self, abs_path: &Path) -> bool {
+ self.file_scan_exclusions
+ .iter()
+ .any(|exclude_matcher| exclude_matcher.is_match(abs_path))
+ }
}
impl BackgroundScannerState {
@@ -2165,7 +2249,7 @@ impl BackgroundScannerState {
let ignore_stack = self.snapshot.ignore_stack_for_abs_path(&abs_path, true);
let mut ancestor_inodes = self.snapshot.ancestor_inodes_for_path(&path);
let mut containing_repository = None;
- if !ignore_stack.is_all() {
+ if !ignore_stack.is_abs_path_ignored(&abs_path, true) {
if let Some((workdir_path, repo)) = self.snapshot.local_repo_for_path(&path) {
if let Ok(repo_path) = path.strip_prefix(&workdir_path.0) {
containing_repository = Some((
@@ -2376,18 +2460,30 @@ impl BackgroundScannerState {
// Remove any git repositories whose .git entry no longer exists.
let snapshot = &mut self.snapshot;
- let mut repositories = mem::take(&mut snapshot.git_repositories);
- let mut repository_entries = mem::take(&mut snapshot.repository_entries);
- repositories.retain(|work_directory_id, _| {
- snapshot
- .entry_for_id(*work_directory_id)
+ let mut ids_to_preserve = HashSet::default();
+ for (&work_directory_id, entry) in snapshot.git_repositories.iter() {
+ let exists_in_snapshot = snapshot
+ .entry_for_id(work_directory_id)
.map_or(false, |entry| {
snapshot.entry_for_path(entry.path.join(*DOT_GIT)).is_some()
- })
- });
- repository_entries.retain(|_, entry| repositories.get(&entry.work_directory.0).is_some());
- snapshot.git_repositories = repositories;
- snapshot.repository_entries = repository_entries;
+ });
+ if exists_in_snapshot {
+ ids_to_preserve.insert(work_directory_id);
+ } else {
+ let git_dir_abs_path = snapshot.abs_path().join(&entry.git_dir_path);
+ if snapshot.is_abs_path_excluded(&git_dir_abs_path)
+ && !matches!(smol::block_on(fs.metadata(&git_dir_abs_path)), Ok(None))
+ {
+ ids_to_preserve.insert(work_directory_id);
+ }
+ }
+ }
+ snapshot
+ .git_repositories
+ .retain(|work_directory_id, _| ids_to_preserve.contains(work_directory_id));
+ snapshot
+ .repository_entries
+ .retain(|_, entry| ids_to_preserve.contains(&entry.work_directory.0));
}
fn build_git_repository(
@@ -3085,7 +3181,7 @@ impl BackgroundScanner {
let ignore_stack = state
.snapshot
.ignore_stack_for_abs_path(&root_abs_path, true);
- if ignore_stack.is_all() {
+ if ignore_stack.is_abs_path_ignored(&root_abs_path, true) {
root_entry.is_ignored = true;
state.insert_entry(root_entry.clone(), self.fs.as_ref());
}
@@ -3222,14 +3318,22 @@ impl BackgroundScanner {
return false;
};
- let parent_dir_is_loaded = relative_path.parent().map_or(true, |parent| {
- snapshot
- .entry_for_path(parent)
- .map_or(false, |entry| entry.kind == EntryKind::Dir)
- });
- if !parent_dir_is_loaded {
- log::debug!("ignoring event {relative_path:?} within unloaded directory");
- return false;
+ if !is_git_related(&abs_path) {
+ let parent_dir_is_loaded = relative_path.parent().map_or(true, |parent| {
+ snapshot
+ .entry_for_path(parent)
+ .map_or(false, |entry| entry.kind == EntryKind::Dir)
+ });
+ if !parent_dir_is_loaded {
+ log::debug!("ignoring event {relative_path:?} within unloaded directory");
+ return false;
+ }
+ if snapshot.is_abs_path_excluded(abs_path) {
+ log::debug!(
+ "ignoring FS event for path {relative_path:?} within excluded directory"
+ );
+ return false;
+ }
}
relative_paths.push(relative_path);
@@ -3392,18 +3496,26 @@ impl BackgroundScanner {
}
async fn scan_dir(&self, job: &ScanJob) -> Result<()> {
- log::debug!("scan directory {:?}", job.path);
-
- let mut ignore_stack = job.ignore_stack.clone();
- let mut new_ignore = None;
- let (root_abs_path, root_char_bag, next_entry_id) = {
- let snapshot = &self.state.lock().snapshot;
- (
- snapshot.abs_path().clone(),
- snapshot.root_char_bag,
- self.next_entry_id.clone(),
- )
- };
+ let root_abs_path;
+ let mut ignore_stack;
+ let mut new_ignore;
+ let root_char_bag;
+ let next_entry_id;
+ {
+ let state = self.state.lock();
+ let snapshot = &state.snapshot;
+ root_abs_path = snapshot.abs_path().clone();
+ if snapshot.is_abs_path_excluded(&job.abs_path) {
+ log::error!("skipping excluded directory {:?}", job.path);
+ return Ok(());
+ }
+ log::debug!("scanning directory {:?}", job.path);
+ ignore_stack = job.ignore_stack.clone();
+ new_ignore = None;
+ root_char_bag = snapshot.root_char_bag;
+ next_entry_id = self.next_entry_id.clone();
+ drop(state);
+ }
let mut dotgit_path = None;
let mut root_canonical_path = None;
@@ -3418,18 +3530,8 @@ impl BackgroundScanner {
continue;
}
};
-
let child_name = child_abs_path.file_name().unwrap();
let child_path: Arc<Path> = job.path.join(child_name).into();
- let child_metadata = match self.fs.metadata(&child_abs_path).await {
- Ok(Some(metadata)) => metadata,
- Ok(None) => continue,
- Err(err) => {
- log::error!("error processing {:?}: {:?}", child_abs_path, err);
- continue;
- }
- };
-
// If we find a .gitignore, add it to the stack of ignores used to determine which paths are ignored
if child_name == *GITIGNORE {
match build_gitignore(&child_abs_path, self.fs.as_ref()).await {
@@ -3473,6 +3575,26 @@ impl BackgroundScanner {
dotgit_path = Some(child_path.clone());
}
+ {
+ let mut state = self.state.lock();
+ if state.snapshot.is_abs_path_excluded(&child_abs_path) {
+ let relative_path = job.path.join(child_name);
+ log::debug!("skipping excluded child entry {relative_path:?}");
+ state.remove_path(&relative_path);
+ continue;
+ }
+ drop(state);
+ }
+
+ let child_metadata = match self.fs.metadata(&child_abs_path).await {
+ Ok(Some(metadata)) => metadata,
+ Ok(None) => continue,
+ Err(err) => {
+ log::error!("error processing {child_abs_path:?}: {err:?}");
+ continue;
+ }
+ };
+
let mut child_entry = Entry::new(
child_path.clone(),
&child_metadata,
@@ -3653,19 +3775,16 @@ impl BackgroundScanner {
self.next_entry_id.as_ref(),
state.snapshot.root_char_bag,
);
- fs_entry.is_ignored = ignore_stack.is_all();
+ let is_dir = fs_entry.is_dir();
+ fs_entry.is_ignored = ignore_stack.is_abs_path_ignored(&abs_path, is_dir);
fs_entry.is_external = !canonical_path.starts_with(&root_canonical_path);
- if !fs_entry.is_ignored {
- if !fs_entry.is_dir() {
- if let Some((work_dir, repo)) =
- state.snapshot.local_repo_for_path(&path)
- {
- if let Ok(repo_path) = path.strip_prefix(work_dir.0) {
- let repo_path = RepoPath(repo_path.into());
- let repo = repo.repo_ptr.lock();
- fs_entry.git_status = repo.status(&repo_path, fs_entry.mtime);
- }
+ if !is_dir && !fs_entry.is_ignored {
+ if let Some((work_dir, repo)) = state.snapshot.local_repo_for_path(&path) {
+ if let Ok(repo_path) = path.strip_prefix(work_dir.0) {
+ let repo_path = RepoPath(repo_path.into());
+ let repo = repo.repo_ptr.lock();
+ fs_entry.git_status = repo.status(&repo_path, fs_entry.mtime);
}
}
}
@@ -3824,8 +3943,7 @@ impl BackgroundScanner {
ignore_stack.clone()
};
- // Scan any directories that were previously ignored and weren't
- // previously scanned.
+ // Scan any directories that were previously ignored and weren't previously scanned.
if was_ignored && !entry.is_ignored && entry.kind.is_unloaded() {
let state = self.state.lock();
if state.should_scan_directory(&entry) {
@@ -4001,6 +4119,12 @@ impl BackgroundScanner {
}
}
+fn is_git_related(abs_path: &Path) -> bool {
+ abs_path
+ .components()
+ .any(|c| c.as_os_str() == *DOT_GIT || c.as_os_str() == *GITIGNORE)
+}
+
fn char_bag_for_path(root_char_bag: CharBag, path: &Path) -> CharBag {
let mut result = root_char_bag;
result.extend(
@@ -1,2141 +1,2310 @@
-// use crate::{
-// worktree::{Event, Snapshot, WorktreeModelHandle},
-// Entry, EntryKind, PathChange, Worktree,
-// };
-// use anyhow::Result;
-// use client2::Client;
-// use fs::{repository::GitFileStatus, FakeFs, Fs, RealFs, RemoveOptions};
-// use git::GITIGNORE;
-// use gpui::{executor::Deterministic, ModelContext, Task, TestAppContext};
-// use parking_lot::Mutex;
-// use postage::stream::Stream;
-// use pretty_assertions::assert_eq;
-// use rand::prelude::*;
-// use serde_json::json;
-// use std::{
-// env,
-// fmt::Write,
-// mem,
-// path::{Path, PathBuf},
-// sync::Arc,
-// };
-// use util::{http::FakeHttpClient, test::temp_tree, ResultExt};
-
-// #[gpui::test]
-// async fn test_traversal(cx: &mut TestAppContext) {
-// let fs = FakeFs::new(cx.background());
-// fs.insert_tree(
-// "/root",
-// json!({
-// ".gitignore": "a/b\n",
-// "a": {
-// "b": "",
-// "c": "",
-// }
-// }),
-// )
-// .await;
-
-// let tree = Worktree::local(
-// build_client(cx),
-// Path::new("/root"),
-// true,
-// fs,
-// Default::default(),
-// &mut cx.to_async(),
-// )
-// .await
-// .unwrap();
-// cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
-// .await;
-
-// tree.read_with(cx, |tree, _| {
-// assert_eq!(
-// tree.entries(false)
-// .map(|entry| entry.path.as_ref())
-// .collect::<Vec<_>>(),
-// vec![
-// Path::new(""),
-// Path::new(".gitignore"),
-// Path::new("a"),
-// Path::new("a/c"),
-// ]
-// );
-// assert_eq!(
-// tree.entries(true)
-// .map(|entry| entry.path.as_ref())
-// .collect::<Vec<_>>(),
-// vec![
-// Path::new(""),
-// Path::new(".gitignore"),
-// Path::new("a"),
-// Path::new("a/b"),
-// Path::new("a/c"),
-// ]
-// );
-// })
-// }
-
-// #[gpui::test]
-// async fn test_descendent_entries(cx: &mut TestAppContext) {
-// let fs = FakeFs::new(cx.background());
-// fs.insert_tree(
-// "/root",
-// json!({
-// "a": "",
-// "b": {
-// "c": {
-// "d": ""
-// },
-// "e": {}
-// },
-// "f": "",
-// "g": {
-// "h": {}
-// },
-// "i": {
-// "j": {
-// "k": ""
-// },
-// "l": {
-
-// }
-// },
-// ".gitignore": "i/j\n",
-// }),
-// )
-// .await;
-
-// let tree = Worktree::local(
-// build_client(cx),
-// Path::new("/root"),
-// true,
-// fs,
-// Default::default(),
-// &mut cx.to_async(),
-// )
-// .await
-// .unwrap();
-// cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
-// .await;
-
-// tree.read_with(cx, |tree, _| {
-// assert_eq!(
-// tree.descendent_entries(false, false, Path::new("b"))
-// .map(|entry| entry.path.as_ref())
-// .collect::<Vec<_>>(),
-// vec![Path::new("b/c/d"),]
-// );
-// assert_eq!(
-// tree.descendent_entries(true, false, Path::new("b"))
-// .map(|entry| entry.path.as_ref())
-// .collect::<Vec<_>>(),
-// vec![
-// Path::new("b"),
-// Path::new("b/c"),
-// Path::new("b/c/d"),
-// Path::new("b/e"),
-// ]
-// );
-
-// assert_eq!(
-// tree.descendent_entries(false, false, Path::new("g"))
-// .map(|entry| entry.path.as_ref())
-// .collect::<Vec<_>>(),
-// Vec::<PathBuf>::new()
-// );
-// assert_eq!(
-// tree.descendent_entries(true, false, Path::new("g"))
-// .map(|entry| entry.path.as_ref())
-// .collect::<Vec<_>>(),
-// vec![Path::new("g"), Path::new("g/h"),]
-// );
-// });
-
-// // Expand gitignored directory.
-// tree.read_with(cx, |tree, _| {
-// tree.as_local()
-// .unwrap()
-// .refresh_entries_for_paths(vec![Path::new("i/j").into()])
-// })
-// .recv()
-// .await;
-
-// tree.read_with(cx, |tree, _| {
-// assert_eq!(
-// tree.descendent_entries(false, false, Path::new("i"))
-// .map(|entry| entry.path.as_ref())
-// .collect::<Vec<_>>(),
-// Vec::<PathBuf>::new()
-// );
-// assert_eq!(
-// tree.descendent_entries(false, true, Path::new("i"))
-// .map(|entry| entry.path.as_ref())
-// .collect::<Vec<_>>(),
-// vec![Path::new("i/j/k")]
-// );
-// assert_eq!(
-// tree.descendent_entries(true, false, Path::new("i"))
-// .map(|entry| entry.path.as_ref())
-// .collect::<Vec<_>>(),
-// vec![Path::new("i"), Path::new("i/l"),]
-// );
-// })
-// }
-
-// #[gpui::test(iterations = 10)]
-// async fn test_circular_symlinks(executor: Arc<Deterministic>, cx: &mut TestAppContext) {
-// let fs = FakeFs::new(cx.background());
-// fs.insert_tree(
-// "/root",
-// json!({
-// "lib": {
-// "a": {
-// "a.txt": ""
-// },
-// "b": {
-// "b.txt": ""
-// }
-// }
-// }),
-// )
-// .await;
-// fs.insert_symlink("/root/lib/a/lib", "..".into()).await;
-// fs.insert_symlink("/root/lib/b/lib", "..".into()).await;
-
-// let tree = Worktree::local(
-// build_client(cx),
-// Path::new("/root"),
-// true,
-// fs.clone(),
-// Default::default(),
-// &mut cx.to_async(),
-// )
-// .await
-// .unwrap();
-
-// cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
-// .await;
-
-// tree.read_with(cx, |tree, _| {
-// assert_eq!(
-// tree.entries(false)
-// .map(|entry| entry.path.as_ref())
-// .collect::<Vec<_>>(),
-// vec![
-// Path::new(""),
-// Path::new("lib"),
-// Path::new("lib/a"),
-// Path::new("lib/a/a.txt"),
-// Path::new("lib/a/lib"),
-// Path::new("lib/b"),
-// Path::new("lib/b/b.txt"),
-// Path::new("lib/b/lib"),
-// ]
-// );
-// });
-
-// fs.rename(
-// Path::new("/root/lib/a/lib"),
-// Path::new("/root/lib/a/lib-2"),
-// Default::default(),
-// )
-// .await
-// .unwrap();
-// executor.run_until_parked();
-// tree.read_with(cx, |tree, _| {
-// assert_eq!(
-// tree.entries(false)
-// .map(|entry| entry.path.as_ref())
-// .collect::<Vec<_>>(),
-// vec![
-// Path::new(""),
-// Path::new("lib"),
-// Path::new("lib/a"),
-// Path::new("lib/a/a.txt"),
-// Path::new("lib/a/lib-2"),
-// Path::new("lib/b"),
-// Path::new("lib/b/b.txt"),
-// Path::new("lib/b/lib"),
-// ]
-// );
-// });
-// }
-
-// #[gpui::test]
-// async fn test_symlinks_pointing_outside(cx: &mut TestAppContext) {
-// let fs = FakeFs::new(cx.background());
-// fs.insert_tree(
-// "/root",
-// json!({
-// "dir1": {
-// "deps": {
-// // symlinks here
-// },
-// "src": {
-// "a.rs": "",
-// "b.rs": "",
-// },
-// },
-// "dir2": {
-// "src": {
-// "c.rs": "",
-// "d.rs": "",
-// }
-// },
-// "dir3": {
-// "deps": {},
-// "src": {
-// "e.rs": "",
-// "f.rs": "",
-// },
-// }
-// }),
-// )
-// .await;
-
-// // These symlinks point to directories outside of the worktree's root, dir1.
-// fs.insert_symlink("/root/dir1/deps/dep-dir2", "../../dir2".into())
-// .await;
-// fs.insert_symlink("/root/dir1/deps/dep-dir3", "../../dir3".into())
-// .await;
-
-// let tree = Worktree::local(
-// build_client(cx),
-// Path::new("/root/dir1"),
-// true,
-// fs.clone(),
-// Default::default(),
-// &mut cx.to_async(),
-// )
-// .await
-// .unwrap();
-
-// cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
-// .await;
-
-// let tree_updates = Arc::new(Mutex::new(Vec::new()));
-// tree.update(cx, |_, cx| {
-// let tree_updates = tree_updates.clone();
-// cx.subscribe(&tree, move |_, _, event, _| {
-// if let Event::UpdatedEntries(update) = event {
-// tree_updates.lock().extend(
-// update
-// .iter()
-// .map(|(path, _, change)| (path.clone(), *change)),
-// );
-// }
-// })
-// .detach();
-// });
-
-// // The symlinked directories are not scanned by default.
-// tree.read_with(cx, |tree, _| {
-// assert_eq!(
-// tree.entries(true)
-// .map(|entry| (entry.path.as_ref(), entry.is_external))
-// .collect::<Vec<_>>(),
-// vec![
-// (Path::new(""), false),
-// (Path::new("deps"), false),
-// (Path::new("deps/dep-dir2"), true),
-// (Path::new("deps/dep-dir3"), true),
-// (Path::new("src"), false),
-// (Path::new("src/a.rs"), false),
-// (Path::new("src/b.rs"), false),
-// ]
-// );
-
-// assert_eq!(
-// tree.entry_for_path("deps/dep-dir2").unwrap().kind,
-// EntryKind::UnloadedDir
-// );
-// });
-
-// // Expand one of the symlinked directories.
-// tree.read_with(cx, |tree, _| {
-// tree.as_local()
-// .unwrap()
-// .refresh_entries_for_paths(vec![Path::new("deps/dep-dir3").into()])
-// })
-// .recv()
-// .await;
-
-// // The expanded directory's contents are loaded. Subdirectories are
-// // not scanned yet.
-// tree.read_with(cx, |tree, _| {
-// assert_eq!(
-// tree.entries(true)
-// .map(|entry| (entry.path.as_ref(), entry.is_external))
-// .collect::<Vec<_>>(),
-// vec![
-// (Path::new(""), false),
-// (Path::new("deps"), false),
-// (Path::new("deps/dep-dir2"), true),
-// (Path::new("deps/dep-dir3"), true),
-// (Path::new("deps/dep-dir3/deps"), true),
-// (Path::new("deps/dep-dir3/src"), true),
-// (Path::new("src"), false),
-// (Path::new("src/a.rs"), false),
-// (Path::new("src/b.rs"), false),
-// ]
-// );
-// });
-// assert_eq!(
-// mem::take(&mut *tree_updates.lock()),
-// &[
-// (Path::new("deps/dep-dir3").into(), PathChange::Loaded),
-// (Path::new("deps/dep-dir3/deps").into(), PathChange::Loaded),
-// (Path::new("deps/dep-dir3/src").into(), PathChange::Loaded)
-// ]
-// );
-
-// // Expand a subdirectory of one of the symlinked directories.
-// tree.read_with(cx, |tree, _| {
-// tree.as_local()
-// .unwrap()
-// .refresh_entries_for_paths(vec![Path::new("deps/dep-dir3/src").into()])
-// })
-// .recv()
-// .await;
-
-// // The expanded subdirectory's contents are loaded.
-// tree.read_with(cx, |tree, _| {
-// assert_eq!(
-// tree.entries(true)
-// .map(|entry| (entry.path.as_ref(), entry.is_external))
-// .collect::<Vec<_>>(),
-// vec![
-// (Path::new(""), false),
-// (Path::new("deps"), false),
-// (Path::new("deps/dep-dir2"), true),
-// (Path::new("deps/dep-dir3"), true),
-// (Path::new("deps/dep-dir3/deps"), true),
-// (Path::new("deps/dep-dir3/src"), true),
-// (Path::new("deps/dep-dir3/src/e.rs"), true),
-// (Path::new("deps/dep-dir3/src/f.rs"), true),
-// (Path::new("src"), false),
-// (Path::new("src/a.rs"), false),
-// (Path::new("src/b.rs"), false),
-// ]
-// );
-// });
-
-// assert_eq!(
-// mem::take(&mut *tree_updates.lock()),
-// &[
-// (Path::new("deps/dep-dir3/src").into(), PathChange::Loaded),
-// (
-// Path::new("deps/dep-dir3/src/e.rs").into(),
-// PathChange::Loaded
-// ),
-// (
-// Path::new("deps/dep-dir3/src/f.rs").into(),
-// PathChange::Loaded
-// )
-// ]
-// );
-// }
-
-// #[gpui::test]
-// async fn test_open_gitignored_files(cx: &mut TestAppContext) {
-// let fs = FakeFs::new(cx.background());
-// fs.insert_tree(
-// "/root",
-// json!({
-// ".gitignore": "node_modules\n",
-// "one": {
-// "node_modules": {
-// "a": {
-// "a1.js": "a1",
-// "a2.js": "a2",
-// },
-// "b": {
-// "b1.js": "b1",
-// "b2.js": "b2",
-// },
-// "c": {
-// "c1.js": "c1",
-// "c2.js": "c2",
-// }
-// },
-// },
-// "two": {
-// "x.js": "",
-// "y.js": "",
-// },
-// }),
-// )
-// .await;
-
-// let tree = Worktree::local(
-// build_client(cx),
-// Path::new("/root"),
-// true,
-// fs.clone(),
-// Default::default(),
-// &mut cx.to_async(),
-// )
-// .await
-// .unwrap();
-
-// cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
-// .await;
-
-// tree.read_with(cx, |tree, _| {
-// assert_eq!(
-// tree.entries(true)
-// .map(|entry| (entry.path.as_ref(), entry.is_ignored))
-// .collect::<Vec<_>>(),
-// vec![
-// (Path::new(""), false),
-// (Path::new(".gitignore"), false),
-// (Path::new("one"), false),
-// (Path::new("one/node_modules"), true),
-// (Path::new("two"), false),
-// (Path::new("two/x.js"), false),
-// (Path::new("two/y.js"), false),
-// ]
-// );
-// });
-
-// // Open a file that is nested inside of a gitignored directory that
-// // has not yet been expanded.
-// let prev_read_dir_count = fs.read_dir_call_count();
-// let buffer = tree
-// .update(cx, |tree, cx| {
-// tree.as_local_mut()
-// .unwrap()
-// .load_buffer(0, "one/node_modules/b/b1.js".as_ref(), cx)
-// })
-// .await
-// .unwrap();
-
-// tree.read_with(cx, |tree, cx| {
-// assert_eq!(
-// tree.entries(true)
-// .map(|entry| (entry.path.as_ref(), entry.is_ignored))
-// .collect::<Vec<_>>(),
-// vec![
-// (Path::new(""), false),
-// (Path::new(".gitignore"), false),
-// (Path::new("one"), false),
-// (Path::new("one/node_modules"), true),
-// (Path::new("one/node_modules/a"), true),
-// (Path::new("one/node_modules/b"), true),
-// (Path::new("one/node_modules/b/b1.js"), true),
-// (Path::new("one/node_modules/b/b2.js"), true),
-// (Path::new("one/node_modules/c"), true),
-// (Path::new("two"), false),
-// (Path::new("two/x.js"), false),
-// (Path::new("two/y.js"), false),
-// ]
-// );
-
-// assert_eq!(
-// buffer.read(cx).file().unwrap().path().as_ref(),
-// Path::new("one/node_modules/b/b1.js")
-// );
-
-// // Only the newly-expanded directories are scanned.
-// assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 2);
-// });
-
-// // Open another file in a different subdirectory of the same
-// // gitignored directory.
-// let prev_read_dir_count = fs.read_dir_call_count();
-// let buffer = tree
-// .update(cx, |tree, cx| {
-// tree.as_local_mut()
-// .unwrap()
-// .load_buffer(0, "one/node_modules/a/a2.js".as_ref(), cx)
-// })
-// .await
-// .unwrap();
-
-// tree.read_with(cx, |tree, cx| {
-// assert_eq!(
-// tree.entries(true)
-// .map(|entry| (entry.path.as_ref(), entry.is_ignored))
-// .collect::<Vec<_>>(),
-// vec![
-// (Path::new(""), false),
-// (Path::new(".gitignore"), false),
-// (Path::new("one"), false),
-// (Path::new("one/node_modules"), true),
-// (Path::new("one/node_modules/a"), true),
-// (Path::new("one/node_modules/a/a1.js"), true),
-// (Path::new("one/node_modules/a/a2.js"), true),
-// (Path::new("one/node_modules/b"), true),
-// (Path::new("one/node_modules/b/b1.js"), true),
-// (Path::new("one/node_modules/b/b2.js"), true),
-// (Path::new("one/node_modules/c"), true),
-// (Path::new("two"), false),
-// (Path::new("two/x.js"), false),
-// (Path::new("two/y.js"), false),
-// ]
-// );
-
-// assert_eq!(
-// buffer.read(cx).file().unwrap().path().as_ref(),
-// Path::new("one/node_modules/a/a2.js")
-// );
-
-// // Only the newly-expanded directory is scanned.
-// assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 1);
-// });
-
-// // No work happens when files and directories change within an unloaded directory.
-// let prev_fs_call_count = fs.read_dir_call_count() + fs.metadata_call_count();
-// fs.create_dir("/root/one/node_modules/c/lib".as_ref())
-// .await
-// .unwrap();
-// cx.foreground().run_until_parked();
-// assert_eq!(
-// fs.read_dir_call_count() + fs.metadata_call_count() - prev_fs_call_count,
-// 0
-// );
-// }
-
-// #[gpui::test]
-// async fn test_dirs_no_longer_ignored(cx: &mut TestAppContext) {
-// let fs = FakeFs::new(cx.background());
-// fs.insert_tree(
-// "/root",
-// json!({
-// ".gitignore": "node_modules\n",
-// "a": {
-// "a.js": "",
-// },
-// "b": {
-// "b.js": "",
-// },
-// "node_modules": {
-// "c": {
-// "c.js": "",
-// },
-// "d": {
-// "d.js": "",
-// "e": {
-// "e1.js": "",
-// "e2.js": "",
-// },
-// "f": {
-// "f1.js": "",
-// "f2.js": "",
-// }
-// },
-// },
-// }),
-// )
-// .await;
-
-// let tree = Worktree::local(
-// build_client(cx),
-// Path::new("/root"),
-// true,
-// fs.clone(),
-// Default::default(),
-// &mut cx.to_async(),
-// )
-// .await
-// .unwrap();
-
-// cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
-// .await;
-
-// // Open a file within the gitignored directory, forcing some of its
-// // subdirectories to be read, but not all.
-// let read_dir_count_1 = fs.read_dir_call_count();
-// tree.read_with(cx, |tree, _| {
-// tree.as_local()
-// .unwrap()
-// .refresh_entries_for_paths(vec![Path::new("node_modules/d/d.js").into()])
-// })
-// .recv()
-// .await;
-
-// // Those subdirectories are now loaded.
-// tree.read_with(cx, |tree, _| {
-// assert_eq!(
-// tree.entries(true)
-// .map(|e| (e.path.as_ref(), e.is_ignored))
-// .collect::<Vec<_>>(),
-// &[
-// (Path::new(""), false),
-// (Path::new(".gitignore"), false),
-// (Path::new("a"), false),
-// (Path::new("a/a.js"), false),
-// (Path::new("b"), false),
-// (Path::new("b/b.js"), false),
-// (Path::new("node_modules"), true),
-// (Path::new("node_modules/c"), true),
-// (Path::new("node_modules/d"), true),
-// (Path::new("node_modules/d/d.js"), true),
-// (Path::new("node_modules/d/e"), true),
-// (Path::new("node_modules/d/f"), true),
-// ]
-// );
-// });
-// let read_dir_count_2 = fs.read_dir_call_count();
-// assert_eq!(read_dir_count_2 - read_dir_count_1, 2);
-
-// // Update the gitignore so that node_modules is no longer ignored,
-// // but a subdirectory is ignored
-// fs.save("/root/.gitignore".as_ref(), &"e".into(), Default::default())
-// .await
-// .unwrap();
-// cx.foreground().run_until_parked();
-
-// // All of the directories that are no longer ignored are now loaded.
-// tree.read_with(cx, |tree, _| {
-// assert_eq!(
-// tree.entries(true)
-// .map(|e| (e.path.as_ref(), e.is_ignored))
-// .collect::<Vec<_>>(),
-// &[
-// (Path::new(""), false),
-// (Path::new(".gitignore"), false),
-// (Path::new("a"), false),
-// (Path::new("a/a.js"), false),
-// (Path::new("b"), false),
-// (Path::new("b/b.js"), false),
-// // This directory is no longer ignored
-// (Path::new("node_modules"), false),
-// (Path::new("node_modules/c"), false),
-// (Path::new("node_modules/c/c.js"), false),
-// (Path::new("node_modules/d"), false),
-// (Path::new("node_modules/d/d.js"), false),
-// // This subdirectory is now ignored
-// (Path::new("node_modules/d/e"), true),
-// (Path::new("node_modules/d/f"), false),
-// (Path::new("node_modules/d/f/f1.js"), false),
-// (Path::new("node_modules/d/f/f2.js"), false),
-// ]
-// );
-// });
-
-// // Each of the newly-loaded directories is scanned only once.
-// let read_dir_count_3 = fs.read_dir_call_count();
-// assert_eq!(read_dir_count_3 - read_dir_count_2, 2);
-// }
-
-// #[gpui::test(iterations = 10)]
-// async fn test_rescan_with_gitignore(cx: &mut TestAppContext) {
-// let fs = FakeFs::new(cx.background());
-// fs.insert_tree(
-// "/root",
-// json!({
-// ".gitignore": "ancestor-ignored-file1\nancestor-ignored-file2\n",
-// "tree": {
-// ".git": {},
-// ".gitignore": "ignored-dir\n",
-// "tracked-dir": {
-// "tracked-file1": "",
-// "ancestor-ignored-file1": "",
-// },
-// "ignored-dir": {
-// "ignored-file1": ""
-// }
-// }
-// }),
-// )
-// .await;
-
-// let tree = Worktree::local(
-// build_client(cx),
-// "/root/tree".as_ref(),
-// true,
-// fs.clone(),
-// Default::default(),
-// &mut cx.to_async(),
-// )
-// .await
-// .unwrap();
-// cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
-// .await;
-
-// tree.read_with(cx, |tree, _| {
-// tree.as_local()
-// .unwrap()
-// .refresh_entries_for_paths(vec![Path::new("ignored-dir").into()])
-// })
-// .recv()
-// .await;
-
-// cx.read(|cx| {
-// let tree = tree.read(cx);
-// assert!(
-// !tree
-// .entry_for_path("tracked-dir/tracked-file1")
-// .unwrap()
-// .is_ignored
-// );
-// assert!(
-// tree.entry_for_path("tracked-dir/ancestor-ignored-file1")
-// .unwrap()
-// .is_ignored
-// );
-// assert!(
-// tree.entry_for_path("ignored-dir/ignored-file1")
-// .unwrap()
-// .is_ignored
-// );
-// });
-
-// fs.create_file(
-// "/root/tree/tracked-dir/tracked-file2".as_ref(),
-// Default::default(),
-// )
-// .await
-// .unwrap();
-// fs.create_file(
-// "/root/tree/tracked-dir/ancestor-ignored-file2".as_ref(),
-// Default::default(),
-// )
-// .await
-// .unwrap();
-// fs.create_file(
-// "/root/tree/ignored-dir/ignored-file2".as_ref(),
-// Default::default(),
-// )
-// .await
-// .unwrap();
-
-// cx.foreground().run_until_parked();
-// cx.read(|cx| {
-// let tree = tree.read(cx);
-// assert!(
-// !tree
-// .entry_for_path("tracked-dir/tracked-file2")
-// .unwrap()
-// .is_ignored
-// );
-// assert!(
-// tree.entry_for_path("tracked-dir/ancestor-ignored-file2")
-// .unwrap()
-// .is_ignored
-// );
-// assert!(
-// tree.entry_for_path("ignored-dir/ignored-file2")
-// .unwrap()
-// .is_ignored
-// );
-// assert!(tree.entry_for_path(".git").unwrap().is_ignored);
-// });
-// }
-
-// #[gpui::test]
-// async fn test_write_file(cx: &mut TestAppContext) {
-// let dir = temp_tree(json!({
-// ".git": {},
-// ".gitignore": "ignored-dir\n",
-// "tracked-dir": {},
-// "ignored-dir": {}
-// }));
-
-// let tree = Worktree::local(
-// build_client(cx),
-// dir.path(),
-// true,
-// Arc::new(RealFs),
-// Default::default(),
-// &mut cx.to_async(),
-// )
-// .await
-// .unwrap();
-// cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
-// .await;
-// tree.flush_fs_events(cx).await;
-
-// tree.update(cx, |tree, cx| {
-// tree.as_local().unwrap().write_file(
-// Path::new("tracked-dir/file.txt"),
-// "hello".into(),
-// Default::default(),
-// cx,
-// )
-// })
-// .await
-// .unwrap();
-// tree.update(cx, |tree, cx| {
-// tree.as_local().unwrap().write_file(
-// Path::new("ignored-dir/file.txt"),
-// "world".into(),
-// Default::default(),
-// cx,
-// )
-// })
-// .await
-// .unwrap();
-
-// tree.read_with(cx, |tree, _| {
-// let tracked = tree.entry_for_path("tracked-dir/file.txt").unwrap();
-// let ignored = tree.entry_for_path("ignored-dir/file.txt").unwrap();
-// assert!(!tracked.is_ignored);
-// assert!(ignored.is_ignored);
-// });
-// }
-
-// #[gpui::test(iterations = 30)]
-// async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
-// let fs = FakeFs::new(cx.background());
-// fs.insert_tree(
-// "/root",
-// json!({
-// "b": {},
-// "c": {},
-// "d": {},
-// }),
-// )
-// .await;
-
-// let tree = Worktree::local(
-// build_client(cx),
-// "/root".as_ref(),
-// true,
-// fs,
-// Default::default(),
-// &mut cx.to_async(),
-// )
-// .await
-// .unwrap();
-
-// let snapshot1 = tree.update(cx, |tree, cx| {
-// let tree = tree.as_local_mut().unwrap();
-// let snapshot = Arc::new(Mutex::new(tree.snapshot()));
-// let _ = tree.observe_updates(0, cx, {
-// let snapshot = snapshot.clone();
-// move |update| {
-// snapshot.lock().apply_remote_update(update).unwrap();
-// async { true }
-// }
-// });
-// snapshot
-// });
-
-// let entry = tree
-// .update(cx, |tree, cx| {
-// tree.as_local_mut()
-// .unwrap()
-// .create_entry("a/e".as_ref(), true, cx)
-// })
-// .await
-// .unwrap();
-// assert!(entry.is_dir());
-
-// cx.foreground().run_until_parked();
-// tree.read_with(cx, |tree, _| {
-// assert_eq!(tree.entry_for_path("a/e").unwrap().kind, EntryKind::Dir);
-// });
-
-// let snapshot2 = tree.update(cx, |tree, _| tree.as_local().unwrap().snapshot());
-// assert_eq!(
-// snapshot1.lock().entries(true).collect::<Vec<_>>(),
-// snapshot2.entries(true).collect::<Vec<_>>()
-// );
-// }
-
-// #[gpui::test]
-// async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
-// let client_fake = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
-
-// let fs_fake = FakeFs::new(cx.background());
-// fs_fake
-// .insert_tree(
-// "/root",
-// json!({
-// "a": {},
-// }),
-// )
-// .await;
-
-// let tree_fake = Worktree::local(
-// client_fake,
-// "/root".as_ref(),
-// true,
-// fs_fake,
-// Default::default(),
-// &mut cx.to_async(),
-// )
-// .await
-// .unwrap();
-
-// let entry = tree_fake
-// .update(cx, |tree, cx| {
-// tree.as_local_mut()
-// .unwrap()
-// .create_entry("a/b/c/d.txt".as_ref(), false, cx)
-// })
-// .await
-// .unwrap();
-// assert!(entry.is_file());
-
-// cx.foreground().run_until_parked();
-// tree_fake.read_with(cx, |tree, _| {
-// assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file());
-// assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir());
-// assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
-// });
-
-// let client_real = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
-
-// let fs_real = Arc::new(RealFs);
-// let temp_root = temp_tree(json!({
-// "a": {}
-// }));
-
-// let tree_real = Worktree::local(
-// client_real,
-// temp_root.path(),
-// true,
-// fs_real,
-// Default::default(),
-// &mut cx.to_async(),
-// )
-// .await
-// .unwrap();
-
-// let entry = tree_real
-// .update(cx, |tree, cx| {
-// tree.as_local_mut()
-// .unwrap()
-// .create_entry("a/b/c/d.txt".as_ref(), false, cx)
-// })
-// .await
-// .unwrap();
-// assert!(entry.is_file());
-
-// cx.foreground().run_until_parked();
-// tree_real.read_with(cx, |tree, _| {
-// assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file());
-// assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir());
-// assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
-// });
-
-// // Test smallest change
-// let entry = tree_real
-// .update(cx, |tree, cx| {
-// tree.as_local_mut()
-// .unwrap()
-// .create_entry("a/b/c/e.txt".as_ref(), false, cx)
-// })
-// .await
-// .unwrap();
-// assert!(entry.is_file());
-
-// cx.foreground().run_until_parked();
-// tree_real.read_with(cx, |tree, _| {
-// assert!(tree.entry_for_path("a/b/c/e.txt").unwrap().is_file());
-// });
-
-// // Test largest change
-// let entry = tree_real
-// .update(cx, |tree, cx| {
-// tree.as_local_mut()
-// .unwrap()
-// .create_entry("d/e/f/g.txt".as_ref(), false, cx)
-// })
-// .await
-// .unwrap();
-// assert!(entry.is_file());
-
-// cx.foreground().run_until_parked();
-// tree_real.read_with(cx, |tree, _| {
-// assert!(tree.entry_for_path("d/e/f/g.txt").unwrap().is_file());
-// assert!(tree.entry_for_path("d/e/f").unwrap().is_dir());
-// assert!(tree.entry_for_path("d/e/").unwrap().is_dir());
-// assert!(tree.entry_for_path("d/").unwrap().is_dir());
-// });
-// }
-
-// #[gpui::test(iterations = 100)]
-// async fn test_random_worktree_operations_during_initial_scan(
-// cx: &mut TestAppContext,
-// mut rng: StdRng,
-// ) {
-// let operations = env::var("OPERATIONS")
-// .map(|o| o.parse().unwrap())
-// .unwrap_or(5);
-// let initial_entries = env::var("INITIAL_ENTRIES")
-// .map(|o| o.parse().unwrap())
-// .unwrap_or(20);
-
-// let root_dir = Path::new("/test");
-// let fs = FakeFs::new(cx.background()) as Arc<dyn Fs>;
-// fs.as_fake().insert_tree(root_dir, json!({})).await;
-// for _ in 0..initial_entries {
-// randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
-// }
-// log::info!("generated initial tree");
-
-// let worktree = Worktree::local(
-// build_client(cx),
-// root_dir,
-// true,
-// fs.clone(),
-// Default::default(),
-// &mut cx.to_async(),
-// )
-// .await
-// .unwrap();
-
-// let mut snapshots = vec![worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot())];
-// let updates = Arc::new(Mutex::new(Vec::new()));
-// worktree.update(cx, |tree, cx| {
-// check_worktree_change_events(tree, cx);
-
-// let _ = tree.as_local_mut().unwrap().observe_updates(0, cx, {
-// let updates = updates.clone();
-// move |update| {
-// updates.lock().push(update);
-// async { true }
-// }
-// });
-// });
-
-// for _ in 0..operations {
-// worktree
-// .update(cx, |worktree, cx| {
-// randomly_mutate_worktree(worktree, &mut rng, cx)
-// })
-// .await
-// .log_err();
-// worktree.read_with(cx, |tree, _| {
-// tree.as_local().unwrap().snapshot().check_invariants(true)
-// });
-
-// if rng.gen_bool(0.6) {
-// snapshots.push(worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot()));
-// }
-// }
-
-// worktree
-// .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
-// .await;
-
-// cx.foreground().run_until_parked();
-
-// let final_snapshot = worktree.read_with(cx, |tree, _| {
-// let tree = tree.as_local().unwrap();
-// let snapshot = tree.snapshot();
-// snapshot.check_invariants(true);
-// snapshot
-// });
-
-// for (i, snapshot) in snapshots.into_iter().enumerate().rev() {
-// let mut updated_snapshot = snapshot.clone();
-// for update in updates.lock().iter() {
-// if update.scan_id >= updated_snapshot.scan_id() as u64 {
-// updated_snapshot
-// .apply_remote_update(update.clone())
-// .unwrap();
-// }
-// }
-
-// assert_eq!(
-// updated_snapshot.entries(true).collect::<Vec<_>>(),
-// final_snapshot.entries(true).collect::<Vec<_>>(),
-// "wrong updates after snapshot {i}: {snapshot:#?} {updates:#?}",
-// );
-// }
-// }
-
-// #[gpui::test(iterations = 100)]
-// async fn test_random_worktree_changes(cx: &mut TestAppContext, mut rng: StdRng) {
-// let operations = env::var("OPERATIONS")
-// .map(|o| o.parse().unwrap())
-// .unwrap_or(40);
-// let initial_entries = env::var("INITIAL_ENTRIES")
-// .map(|o| o.parse().unwrap())
-// .unwrap_or(20);
-
-// let root_dir = Path::new("/test");
-// let fs = FakeFs::new(cx.background()) as Arc<dyn Fs>;
-// fs.as_fake().insert_tree(root_dir, json!({})).await;
-// for _ in 0..initial_entries {
-// randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
-// }
-// log::info!("generated initial tree");
-
-// let worktree = Worktree::local(
-// build_client(cx),
-// root_dir,
-// true,
-// fs.clone(),
-// Default::default(),
-// &mut cx.to_async(),
-// )
-// .await
-// .unwrap();
-
-// let updates = Arc::new(Mutex::new(Vec::new()));
-// worktree.update(cx, |tree, cx| {
-// check_worktree_change_events(tree, cx);
-
-// let _ = tree.as_local_mut().unwrap().observe_updates(0, cx, {
-// let updates = updates.clone();
-// move |update| {
-// updates.lock().push(update);
-// async { true }
-// }
-// });
-// });
-
-// worktree
-// .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
-// .await;
-
-// fs.as_fake().pause_events();
-// let mut snapshots = Vec::new();
-// let mut mutations_len = operations;
-// while mutations_len > 1 {
-// if rng.gen_bool(0.2) {
-// worktree
-// .update(cx, |worktree, cx| {
-// randomly_mutate_worktree(worktree, &mut rng, cx)
-// })
-// .await
-// .log_err();
-// } else {
-// randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
-// }
-
-// let buffered_event_count = fs.as_fake().buffered_event_count();
-// if buffered_event_count > 0 && rng.gen_bool(0.3) {
-// let len = rng.gen_range(0..=buffered_event_count);
-// log::info!("flushing {} events", len);
-// fs.as_fake().flush_events(len);
-// } else {
-// randomly_mutate_fs(&fs, root_dir, 0.6, &mut rng).await;
-// mutations_len -= 1;
-// }
-
-// cx.foreground().run_until_parked();
-// if rng.gen_bool(0.2) {
-// log::info!("storing snapshot {}", snapshots.len());
-// let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
-// snapshots.push(snapshot);
-// }
-// }
-
-// log::info!("quiescing");
-// fs.as_fake().flush_events(usize::MAX);
-// cx.foreground().run_until_parked();
-
-// let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
-// snapshot.check_invariants(true);
-// let expanded_paths = snapshot
-// .expanded_entries()
-// .map(|e| e.path.clone())
-// .collect::<Vec<_>>();
-
-// {
-// let new_worktree = Worktree::local(
-// build_client(cx),
-// root_dir,
-// true,
-// fs.clone(),
-// Default::default(),
-// &mut cx.to_async(),
-// )
-// .await
-// .unwrap();
-// new_worktree
-// .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
-// .await;
-// new_worktree
-// .update(cx, |tree, _| {
-// tree.as_local_mut()
-// .unwrap()
-// .refresh_entries_for_paths(expanded_paths)
-// })
-// .recv()
-// .await;
-// let new_snapshot =
-// new_worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
-// assert_eq!(
-// snapshot.entries_without_ids(true),
-// new_snapshot.entries_without_ids(true)
-// );
-// }
-
-// for (i, mut prev_snapshot) in snapshots.into_iter().enumerate().rev() {
-// for update in updates.lock().iter() {
-// if update.scan_id >= prev_snapshot.scan_id() as u64 {
-// prev_snapshot.apply_remote_update(update.clone()).unwrap();
-// }
-// }
-
-// assert_eq!(
-// prev_snapshot
-// .entries(true)
-// .map(ignore_pending_dir)
-// .collect::<Vec<_>>(),
-// snapshot
-// .entries(true)
-// .map(ignore_pending_dir)
-// .collect::<Vec<_>>(),
-// "wrong updates after snapshot {i}: {updates:#?}",
-// );
-// }
-
-// fn ignore_pending_dir(entry: &Entry) -> Entry {
-// let mut entry = entry.clone();
-// if entry.kind.is_dir() {
-// entry.kind = EntryKind::Dir
-// }
-// entry
-// }
-// }
-
-// // The worktree's `UpdatedEntries` event can be used to follow along with
-// // all changes to the worktree's snapshot.
-// fn check_worktree_change_events(tree: &mut Worktree, cx: &mut ModelContext<Worktree>) {
-// let mut entries = tree.entries(true).cloned().collect::<Vec<_>>();
-// cx.subscribe(&cx.handle(), move |tree, _, event, _| {
-// if let Event::UpdatedEntries(changes) = event {
-// for (path, _, change_type) in changes.iter() {
-// let entry = tree.entry_for_path(&path).cloned();
-// let ix = match entries.binary_search_by_key(&path, |e| &e.path) {
-// Ok(ix) | Err(ix) => ix,
-// };
-// match change_type {
-// PathChange::Added => entries.insert(ix, entry.unwrap()),
-// PathChange::Removed => drop(entries.remove(ix)),
-// PathChange::Updated => {
-// let entry = entry.unwrap();
-// let existing_entry = entries.get_mut(ix).unwrap();
-// assert_eq!(existing_entry.path, entry.path);
-// *existing_entry = entry;
-// }
-// PathChange::AddedOrUpdated | PathChange::Loaded => {
-// let entry = entry.unwrap();
-// if entries.get(ix).map(|e| &e.path) == Some(&entry.path) {
-// *entries.get_mut(ix).unwrap() = entry;
-// } else {
-// entries.insert(ix, entry);
-// }
-// }
-// }
-// }
-
-// let new_entries = tree.entries(true).cloned().collect::<Vec<_>>();
-// assert_eq!(entries, new_entries, "incorrect changes: {:?}", changes);
-// }
-// })
-// .detach();
-// }
-
-// fn randomly_mutate_worktree(
-// worktree: &mut Worktree,
-// rng: &mut impl Rng,
-// cx: &mut ModelContext<Worktree>,
-// ) -> Task<Result<()>> {
-// log::info!("mutating worktree");
-// let worktree = worktree.as_local_mut().unwrap();
-// let snapshot = worktree.snapshot();
-// let entry = snapshot.entries(false).choose(rng).unwrap();
-
-// match rng.gen_range(0_u32..100) {
-// 0..=33 if entry.path.as_ref() != Path::new("") => {
-// log::info!("deleting entry {:?} ({})", entry.path, entry.id.0);
-// worktree.delete_entry(entry.id, cx).unwrap()
-// }
-// ..=66 if entry.path.as_ref() != Path::new("") => {
-// let other_entry = snapshot.entries(false).choose(rng).unwrap();
-// let new_parent_path = if other_entry.is_dir() {
-// other_entry.path.clone()
-// } else {
-// other_entry.path.parent().unwrap().into()
-// };
-// let mut new_path = new_parent_path.join(random_filename(rng));
-// if new_path.starts_with(&entry.path) {
-// new_path = random_filename(rng).into();
-// }
-
-// log::info!(
-// "renaming entry {:?} ({}) to {:?}",
-// entry.path,
-// entry.id.0,
-// new_path
-// );
-// let task = worktree.rename_entry(entry.id, new_path, cx).unwrap();
-// cx.foreground().spawn(async move {
-// task.await?;
-// Ok(())
-// })
-// }
-// _ => {
-// let task = if entry.is_dir() {
-// let child_path = entry.path.join(random_filename(rng));
-// let is_dir = rng.gen_bool(0.3);
-// log::info!(
-// "creating {} at {:?}",
-// if is_dir { "dir" } else { "file" },
-// child_path,
-// );
-// worktree.create_entry(child_path, is_dir, cx)
-// } else {
-// log::info!("overwriting file {:?} ({})", entry.path, entry.id.0);
-// worktree.write_file(entry.path.clone(), "".into(), Default::default(), cx)
-// };
-// cx.foreground().spawn(async move {
-// task.await?;
-// Ok(())
-// })
-// }
-// }
-// }
-
-// async fn randomly_mutate_fs(
-// fs: &Arc<dyn Fs>,
-// root_path: &Path,
-// insertion_probability: f64,
-// rng: &mut impl Rng,
-// ) {
-// log::info!("mutating fs");
-// let mut files = Vec::new();
-// let mut dirs = Vec::new();
-// for path in fs.as_fake().paths(false) {
-// if path.starts_with(root_path) {
-// if fs.is_file(&path).await {
-// files.push(path);
-// } else {
-// dirs.push(path);
-// }
-// }
-// }
-
-// if (files.is_empty() && dirs.len() == 1) || rng.gen_bool(insertion_probability) {
-// let path = dirs.choose(rng).unwrap();
-// let new_path = path.join(random_filename(rng));
-
-// if rng.gen() {
-// log::info!(
-// "creating dir {:?}",
-// new_path.strip_prefix(root_path).unwrap()
-// );
-// fs.create_dir(&new_path).await.unwrap();
-// } else {
-// log::info!(
-// "creating file {:?}",
-// new_path.strip_prefix(root_path).unwrap()
-// );
-// fs.create_file(&new_path, Default::default()).await.unwrap();
-// }
-// } else if rng.gen_bool(0.05) {
-// let ignore_dir_path = dirs.choose(rng).unwrap();
-// let ignore_path = ignore_dir_path.join(&*GITIGNORE);
-
-// let subdirs = dirs
-// .iter()
-// .filter(|d| d.starts_with(&ignore_dir_path))
-// .cloned()
-// .collect::<Vec<_>>();
-// let subfiles = files
-// .iter()
-// .filter(|d| d.starts_with(&ignore_dir_path))
-// .cloned()
-// .collect::<Vec<_>>();
-// let files_to_ignore = {
-// let len = rng.gen_range(0..=subfiles.len());
-// subfiles.choose_multiple(rng, len)
-// };
-// let dirs_to_ignore = {
-// let len = rng.gen_range(0..subdirs.len());
-// subdirs.choose_multiple(rng, len)
-// };
-
-// let mut ignore_contents = String::new();
-// for path_to_ignore in files_to_ignore.chain(dirs_to_ignore) {
-// writeln!(
-// ignore_contents,
-// "{}",
-// path_to_ignore
-// .strip_prefix(&ignore_dir_path)
-// .unwrap()
-// .to_str()
-// .unwrap()
-// )
-// .unwrap();
-// }
-// log::info!(
-// "creating gitignore {:?} with contents:\n{}",
-// ignore_path.strip_prefix(&root_path).unwrap(),
-// ignore_contents
-// );
-// fs.save(
-// &ignore_path,
-// &ignore_contents.as_str().into(),
-// Default::default(),
-// )
-// .await
-// .unwrap();
-// } else {
-// let old_path = {
-// let file_path = files.choose(rng);
-// let dir_path = dirs[1..].choose(rng);
-// file_path.into_iter().chain(dir_path).choose(rng).unwrap()
-// };
-
-// let is_rename = rng.gen();
-// if is_rename {
-// let new_path_parent = dirs
-// .iter()
-// .filter(|d| !d.starts_with(old_path))
-// .choose(rng)
-// .unwrap();
-
-// let overwrite_existing_dir =
-// !old_path.starts_with(&new_path_parent) && rng.gen_bool(0.3);
-// let new_path = if overwrite_existing_dir {
-// fs.remove_dir(
-// &new_path_parent,
-// RemoveOptions {
-// recursive: true,
-// ignore_if_not_exists: true,
-// },
-// )
-// .await
-// .unwrap();
-// new_path_parent.to_path_buf()
-// } else {
-// new_path_parent.join(random_filename(rng))
-// };
-
-// log::info!(
-// "renaming {:?} to {}{:?}",
-// old_path.strip_prefix(&root_path).unwrap(),
-// if overwrite_existing_dir {
-// "overwrite "
-// } else {
-// ""
-// },
-// new_path.strip_prefix(&root_path).unwrap()
-// );
-// fs.rename(
-// &old_path,
-// &new_path,
-// fs::RenameOptions {
-// overwrite: true,
-// ignore_if_exists: true,
-// },
-// )
-// .await
-// .unwrap();
-// } else if fs.is_file(&old_path).await {
-// log::info!(
-// "deleting file {:?}",
-// old_path.strip_prefix(&root_path).unwrap()
-// );
-// fs.remove_file(old_path, Default::default()).await.unwrap();
-// } else {
-// log::info!(
-// "deleting dir {:?}",
-// old_path.strip_prefix(&root_path).unwrap()
-// );
-// fs.remove_dir(
-// &old_path,
-// RemoveOptions {
-// recursive: true,
-// ignore_if_not_exists: true,
-// },
-// )
-// .await
-// .unwrap();
-// }
-// }
-// }
-
-// fn random_filename(rng: &mut impl Rng) -> String {
-// (0..6)
-// .map(|_| rng.sample(rand::distributions::Alphanumeric))
-// .map(char::from)
-// .collect()
-// }
-
-// #[gpui::test]
-// async fn test_rename_work_directory(cx: &mut TestAppContext) {
-// let root = temp_tree(json!({
-// "projects": {
-// "project1": {
-// "a": "",
-// "b": "",
-// }
-// },
-
-// }));
-// let root_path = root.path();
-
-// let tree = Worktree::local(
-// build_client(cx),
-// root_path,
-// true,
-// Arc::new(RealFs),
-// Default::default(),
-// &mut cx.to_async(),
-// )
-// .await
-// .unwrap();
-
-// let repo = git_init(&root_path.join("projects/project1"));
-// git_add("a", &repo);
-// git_commit("init", &repo);
-// std::fs::write(root_path.join("projects/project1/a"), "aa").ok();
-
-// cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
-// .await;
-
-// tree.flush_fs_events(cx).await;
-
-// cx.read(|cx| {
-// let tree = tree.read(cx);
-// let (work_dir, _) = tree.repositories().next().unwrap();
-// assert_eq!(work_dir.as_ref(), Path::new("projects/project1"));
-// assert_eq!(
-// tree.status_for_file(Path::new("projects/project1/a")),
-// Some(GitFileStatus::Modified)
-// );
-// assert_eq!(
-// tree.status_for_file(Path::new("projects/project1/b")),
-// Some(GitFileStatus::Added)
-// );
-// });
-
-// std::fs::rename(
-// root_path.join("projects/project1"),
-// root_path.join("projects/project2"),
-// )
-// .ok();
-// tree.flush_fs_events(cx).await;
-
-// cx.read(|cx| {
-// let tree = tree.read(cx);
-// let (work_dir, _) = tree.repositories().next().unwrap();
-// assert_eq!(work_dir.as_ref(), Path::new("projects/project2"));
-// assert_eq!(
-// tree.status_for_file(Path::new("projects/project2/a")),
-// Some(GitFileStatus::Modified)
-// );
-// assert_eq!(
-// tree.status_for_file(Path::new("projects/project2/b")),
-// Some(GitFileStatus::Added)
-// );
-// });
-// }
-
-// #[gpui::test]
-// async fn test_git_repository_for_path(cx: &mut TestAppContext) {
-// let root = temp_tree(json!({
-// "c.txt": "",
-// "dir1": {
-// ".git": {},
-// "deps": {
-// "dep1": {
-// ".git": {},
-// "src": {
-// "a.txt": ""
-// }
-// }
-// },
-// "src": {
-// "b.txt": ""
-// }
-// },
-// }));
-
-// let tree = Worktree::local(
-// build_client(cx),
-// root.path(),
-// true,
-// Arc::new(RealFs),
-// Default::default(),
-// &mut cx.to_async(),
-// )
-// .await
-// .unwrap();
-
-// cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
-// .await;
-// tree.flush_fs_events(cx).await;
-
-// tree.read_with(cx, |tree, _cx| {
-// let tree = tree.as_local().unwrap();
-
-// assert!(tree.repository_for_path("c.txt".as_ref()).is_none());
-
-// let entry = tree.repository_for_path("dir1/src/b.txt".as_ref()).unwrap();
-// assert_eq!(
-// entry
-// .work_directory(tree)
-// .map(|directory| directory.as_ref().to_owned()),
-// Some(Path::new("dir1").to_owned())
-// );
-
-// let entry = tree
-// .repository_for_path("dir1/deps/dep1/src/a.txt".as_ref())
-// .unwrap();
-// assert_eq!(
-// entry
-// .work_directory(tree)
-// .map(|directory| directory.as_ref().to_owned()),
-// Some(Path::new("dir1/deps/dep1").to_owned())
-// );
-
-// let entries = tree.files(false, 0);
-
-// let paths_with_repos = tree
-// .entries_with_repositories(entries)
-// .map(|(entry, repo)| {
-// (
-// entry.path.as_ref(),
-// repo.and_then(|repo| {
-// repo.work_directory(&tree)
-// .map(|work_directory| work_directory.0.to_path_buf())
-// }),
-// )
-// })
-// .collect::<Vec<_>>();
-
-// assert_eq!(
-// paths_with_repos,
-// &[
-// (Path::new("c.txt"), None),
-// (
-// Path::new("dir1/deps/dep1/src/a.txt"),
-// Some(Path::new("dir1/deps/dep1").into())
-// ),
-// (Path::new("dir1/src/b.txt"), Some(Path::new("dir1").into())),
-// ]
-// );
-// });
-
-// let repo_update_events = Arc::new(Mutex::new(vec![]));
-// tree.update(cx, |_, cx| {
-// let repo_update_events = repo_update_events.clone();
-// cx.subscribe(&tree, move |_, _, event, _| {
-// if let Event::UpdatedGitRepositories(update) = event {
-// repo_update_events.lock().push(update.clone());
-// }
-// })
-// .detach();
-// });
-
-// std::fs::write(root.path().join("dir1/.git/random_new_file"), "hello").unwrap();
-// tree.flush_fs_events(cx).await;
-
-// assert_eq!(
-// repo_update_events.lock()[0]
-// .iter()
-// .map(|e| e.0.clone())
-// .collect::<Vec<Arc<Path>>>(),
-// vec![Path::new("dir1").into()]
-// );
-
-// std::fs::remove_dir_all(root.path().join("dir1/.git")).unwrap();
-// tree.flush_fs_events(cx).await;
-
-// tree.read_with(cx, |tree, _cx| {
-// let tree = tree.as_local().unwrap();
-
-// assert!(tree
-// .repository_for_path("dir1/src/b.txt".as_ref())
-// .is_none());
-// });
-// }
-
-// #[gpui::test]
-// async fn test_git_status(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
-// const IGNORE_RULE: &'static str = "**/target";
-
-// let root = temp_tree(json!({
-// "project": {
-// "a.txt": "a",
-// "b.txt": "bb",
-// "c": {
-// "d": {
-// "e.txt": "eee"
-// }
-// },
-// "f.txt": "ffff",
-// "target": {
-// "build_file": "???"
-// },
-// ".gitignore": IGNORE_RULE
-// },
-
-// }));
-
-// const A_TXT: &'static str = "a.txt";
-// const B_TXT: &'static str = "b.txt";
-// const E_TXT: &'static str = "c/d/e.txt";
-// const F_TXT: &'static str = "f.txt";
-// const DOTGITIGNORE: &'static str = ".gitignore";
-// const BUILD_FILE: &'static str = "target/build_file";
-// let project_path = Path::new("project");
-
-// // Set up git repository before creating the worktree.
-// let work_dir = root.path().join("project");
-// let mut repo = git_init(work_dir.as_path());
-// repo.add_ignore_rule(IGNORE_RULE).unwrap();
-// git_add(A_TXT, &repo);
-// git_add(E_TXT, &repo);
-// git_add(DOTGITIGNORE, &repo);
-// git_commit("Initial commit", &repo);
-
-// let tree = Worktree::local(
-// build_client(cx),
-// root.path(),
-// true,
-// Arc::new(RealFs),
-// Default::default(),
-// &mut cx.to_async(),
-// )
-// .await
-// .unwrap();
-
-// tree.flush_fs_events(cx).await;
-// cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
-// .await;
-// deterministic.run_until_parked();
-
-// // Check that the right git state is observed on startup
-// tree.read_with(cx, |tree, _cx| {
-// let snapshot = tree.snapshot();
-// assert_eq!(snapshot.repositories().count(), 1);
-// let (dir, _) = snapshot.repositories().next().unwrap();
-// assert_eq!(dir.as_ref(), Path::new("project"));
-
-// assert_eq!(
-// snapshot.status_for_file(project_path.join(B_TXT)),
-// Some(GitFileStatus::Added)
-// );
-// assert_eq!(
-// snapshot.status_for_file(project_path.join(F_TXT)),
-// Some(GitFileStatus::Added)
-// );
-// });
-
-// // Modify a file in the working copy.
-// std::fs::write(work_dir.join(A_TXT), "aa").unwrap();
-// tree.flush_fs_events(cx).await;
-// deterministic.run_until_parked();
-
-// // The worktree detects that the file's git status has changed.
-// tree.read_with(cx, |tree, _cx| {
-// let snapshot = tree.snapshot();
-// assert_eq!(
-// snapshot.status_for_file(project_path.join(A_TXT)),
-// Some(GitFileStatus::Modified)
-// );
-// });
-
-// // Create a commit in the git repository.
-// git_add(A_TXT, &repo);
-// git_add(B_TXT, &repo);
-// git_commit("Committing modified and added", &repo);
-// tree.flush_fs_events(cx).await;
-// deterministic.run_until_parked();
-
-// // The worktree detects that the files' git status have changed.
-// tree.read_with(cx, |tree, _cx| {
-// let snapshot = tree.snapshot();
-// assert_eq!(
-// snapshot.status_for_file(project_path.join(F_TXT)),
-// Some(GitFileStatus::Added)
-// );
-// assert_eq!(snapshot.status_for_file(project_path.join(B_TXT)), None);
-// assert_eq!(snapshot.status_for_file(project_path.join(A_TXT)), None);
-// });
-
-// // Modify files in the working copy and perform git operations on other files.
-// git_reset(0, &repo);
-// git_remove_index(Path::new(B_TXT), &repo);
-// git_stash(&mut repo);
-// std::fs::write(work_dir.join(E_TXT), "eeee").unwrap();
-// std::fs::write(work_dir.join(BUILD_FILE), "this should be ignored").unwrap();
-// tree.flush_fs_events(cx).await;
-// deterministic.run_until_parked();
-
-// // Check that more complex repo changes are tracked
-// tree.read_with(cx, |tree, _cx| {
-// let snapshot = tree.snapshot();
-
-// assert_eq!(snapshot.status_for_file(project_path.join(A_TXT)), None);
-// assert_eq!(
-// snapshot.status_for_file(project_path.join(B_TXT)),
-// Some(GitFileStatus::Added)
-// );
-// assert_eq!(
-// snapshot.status_for_file(project_path.join(E_TXT)),
-// Some(GitFileStatus::Modified)
-// );
-// });
-
-// std::fs::remove_file(work_dir.join(B_TXT)).unwrap();
-// std::fs::remove_dir_all(work_dir.join("c")).unwrap();
-// std::fs::write(
-// work_dir.join(DOTGITIGNORE),
-// [IGNORE_RULE, "f.txt"].join("\n"),
-// )
-// .unwrap();
-
-// git_add(Path::new(DOTGITIGNORE), &repo);
-// git_commit("Committing modified git ignore", &repo);
-
-// tree.flush_fs_events(cx).await;
-// deterministic.run_until_parked();
-
-// let mut renamed_dir_name = "first_directory/second_directory";
-// const RENAMED_FILE: &'static str = "rf.txt";
-
-// std::fs::create_dir_all(work_dir.join(renamed_dir_name)).unwrap();
-// std::fs::write(
-// work_dir.join(renamed_dir_name).join(RENAMED_FILE),
-// "new-contents",
-// )
-// .unwrap();
-
-// tree.flush_fs_events(cx).await;
-// deterministic.run_until_parked();
-
-// tree.read_with(cx, |tree, _cx| {
-// let snapshot = tree.snapshot();
-// assert_eq!(
-// snapshot.status_for_file(&project_path.join(renamed_dir_name).join(RENAMED_FILE)),
-// Some(GitFileStatus::Added)
-// );
-// });
-
-// renamed_dir_name = "new_first_directory/second_directory";
-
-// std::fs::rename(
-// work_dir.join("first_directory"),
-// work_dir.join("new_first_directory"),
-// )
-// .unwrap();
-
-// tree.flush_fs_events(cx).await;
-// deterministic.run_until_parked();
-
-// tree.read_with(cx, |tree, _cx| {
-// let snapshot = tree.snapshot();
-
-// assert_eq!(
-// snapshot.status_for_file(
-// project_path
-// .join(Path::new(renamed_dir_name))
-// .join(RENAMED_FILE)
-// ),
-// Some(GitFileStatus::Added)
-// );
-// });
-// }
-
-// #[gpui::test]
-// async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
-// let fs = FakeFs::new(cx.background());
-// fs.insert_tree(
-// "/root",
-// json!({
-// ".git": {},
-// "a": {
-// "b": {
-// "c1.txt": "",
-// "c2.txt": "",
-// },
-// "d": {
-// "e1.txt": "",
-// "e2.txt": "",
-// "e3.txt": "",
-// }
-// },
-// "f": {
-// "no-status.txt": ""
-// },
-// "g": {
-// "h1.txt": "",
-// "h2.txt": ""
-// },
-
-// }),
-// )
-// .await;
-
-// fs.set_status_for_repo_via_git_operation(
-// &Path::new("/root/.git"),
-// &[
-// (Path::new("a/b/c1.txt"), GitFileStatus::Added),
-// (Path::new("a/d/e2.txt"), GitFileStatus::Modified),
-// (Path::new("g/h2.txt"), GitFileStatus::Conflict),
-// ],
-// );
-
-// let tree = Worktree::local(
-// build_client(cx),
-// Path::new("/root"),
-// true,
-// fs.clone(),
-// Default::default(),
-// &mut cx.to_async(),
-// )
-// .await
-// .unwrap();
-
-// cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
-// .await;
-
-// cx.foreground().run_until_parked();
-// let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
-
-// check_propagated_statuses(
-// &snapshot,
-// &[
-// (Path::new(""), Some(GitFileStatus::Conflict)),
-// (Path::new("a"), Some(GitFileStatus::Modified)),
-// (Path::new("a/b"), Some(GitFileStatus::Added)),
-// (Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)),
-// (Path::new("a/b/c2.txt"), None),
-// (Path::new("a/d"), Some(GitFileStatus::Modified)),
-// (Path::new("a/d/e2.txt"), Some(GitFileStatus::Modified)),
-// (Path::new("f"), None),
-// (Path::new("f/no-status.txt"), None),
-// (Path::new("g"), Some(GitFileStatus::Conflict)),
-// (Path::new("g/h2.txt"), Some(GitFileStatus::Conflict)),
-// ],
-// );
-
-// check_propagated_statuses(
-// &snapshot,
-// &[
-// (Path::new("a/b"), Some(GitFileStatus::Added)),
-// (Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)),
-// (Path::new("a/b/c2.txt"), None),
-// (Path::new("a/d"), Some(GitFileStatus::Modified)),
-// (Path::new("a/d/e1.txt"), None),
-// (Path::new("a/d/e2.txt"), Some(GitFileStatus::Modified)),
-// (Path::new("f"), None),
-// (Path::new("f/no-status.txt"), None),
-// (Path::new("g"), Some(GitFileStatus::Conflict)),
-// ],
-// );
-
-// check_propagated_statuses(
-// &snapshot,
-// &[
-// (Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)),
-// (Path::new("a/b/c2.txt"), None),
-// (Path::new("a/d/e1.txt"), None),
-// (Path::new("a/d/e2.txt"), Some(GitFileStatus::Modified)),
-// (Path::new("f/no-status.txt"), None),
-// ],
-// );
-
-// #[track_caller]
-// fn check_propagated_statuses(
-// snapshot: &Snapshot,
-// expected_statuses: &[(&Path, Option<GitFileStatus>)],
-// ) {
-// let mut entries = expected_statuses
-// .iter()
-// .map(|(path, _)| snapshot.entry_for_path(path).unwrap().clone())
-// .collect::<Vec<_>>();
-// snapshot.propagate_git_statuses(&mut entries);
-// assert_eq!(
-// entries
-// .iter()
-// .map(|e| (e.path.as_ref(), e.git_status))
-// .collect::<Vec<_>>(),
-// expected_statuses
-// );
-// }
-// }
-
-// fn build_client(cx: &mut TestAppContext) -> Arc<Client> {
-// let http_client = FakeHttpClient::with_404_response();
-// cx.read(|cx| Client::new(http_client, cx))
-// }
-
-// #[track_caller]
-// fn git_init(path: &Path) -> git2::Repository {
-// git2::Repository::init(path).expect("Failed to initialize git repository")
-// }
-
-// #[track_caller]
-// fn git_add<P: AsRef<Path>>(path: P, repo: &git2::Repository) {
-// let path = path.as_ref();
-// let mut index = repo.index().expect("Failed to get index");
-// index.add_path(path).expect("Failed to add a.txt");
-// index.write().expect("Failed to write index");
-// }
-
-// #[track_caller]
-// fn git_remove_index(path: &Path, repo: &git2::Repository) {
-// let mut index = repo.index().expect("Failed to get index");
-// index.remove_path(path).expect("Failed to add a.txt");
-// index.write().expect("Failed to write index");
-// }
-
-// #[track_caller]
-// fn git_commit(msg: &'static str, repo: &git2::Repository) {
-// use git2::Signature;
-
-// let signature = Signature::now("test", "test@zed.dev").unwrap();
-// let oid = repo.index().unwrap().write_tree().unwrap();
-// let tree = repo.find_tree(oid).unwrap();
-// if let Some(head) = repo.head().ok() {
-// let parent_obj = head.peel(git2::ObjectType::Commit).unwrap();
-
-// let parent_commit = parent_obj.as_commit().unwrap();
-
-// repo.commit(
-// Some("HEAD"),
-// &signature,
-// &signature,
-// msg,
-// &tree,
-// &[parent_commit],
-// )
-// .expect("Failed to commit with parent");
-// } else {
-// repo.commit(Some("HEAD"), &signature, &signature, msg, &tree, &[])
-// .expect("Failed to commit");
-// }
-// }
-
-// #[track_caller]
-// fn git_stash(repo: &mut git2::Repository) {
-// use git2::Signature;
-
-// let signature = Signature::now("test", "test@zed.dev").unwrap();
-// repo.stash_save(&signature, "N/A", None)
-// .expect("Failed to stash");
-// }
-
-// #[track_caller]
-// fn git_reset(offset: usize, repo: &git2::Repository) {
-// let head = repo.head().expect("Couldn't get repo head");
-// let object = head.peel(git2::ObjectType::Commit).unwrap();
-// let commit = object.as_commit().unwrap();
-// let new_head = commit
-// .parents()
-// .inspect(|parnet| {
-// parnet.message();
-// })
-// .skip(offset)
-// .next()
-// .expect("Not enough history");
-// repo.reset(&new_head.as_object(), git2::ResetType::Soft, None)
-// .expect("Could not reset");
-// }
-
-// #[allow(dead_code)]
-// #[track_caller]
-// fn git_status(repo: &git2::Repository) -> collections::HashMap<String, git2::Status> {
-// repo.statuses(None)
-// .unwrap()
-// .iter()
-// .map(|status| (status.path().unwrap().to_string(), status.status()))
-// .collect()
-// }
+use crate::{
+ project_settings::ProjectSettings,
+ worktree::{Event, Snapshot, WorktreeModelHandle},
+ Entry, EntryKind, PathChange, Project, Worktree,
+};
+use anyhow::Result;
+use client::Client;
+use fs::{repository::GitFileStatus, FakeFs, Fs, RealFs, RemoveOptions};
+use git::GITIGNORE;
+use gpui::{ModelContext, Task, TestAppContext};
+use parking_lot::Mutex;
+use postage::stream::Stream;
+use pretty_assertions::assert_eq;
+use rand::prelude::*;
+use serde_json::json;
+use settings::SettingsStore;
+use std::{
+ env,
+ fmt::Write,
+ mem,
+ path::{Path, PathBuf},
+ sync::Arc,
+};
+use util::{http::FakeHttpClient, test::temp_tree, ResultExt};
+
+#[gpui::test]
+async fn test_traversal(cx: &mut TestAppContext) {
+ init_test(cx);
+ let fs = FakeFs::new(cx.background_executor.clone());
+ fs.insert_tree(
+ "/root",
+ json!({
+ ".gitignore": "a/b\n",
+ "a": {
+ "b": "",
+ "c": "",
+ }
+ }),
+ )
+ .await;
+
+ let tree = Worktree::local(
+ build_client(cx),
+ Path::new("/root"),
+ true,
+ fs,
+ Default::default(),
+ &mut cx.to_async(),
+ )
+ .await
+ .unwrap();
+ cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+ .await;
+
+ tree.read_with(cx, |tree, _| {
+ assert_eq!(
+ tree.entries(false)
+ .map(|entry| entry.path.as_ref())
+ .collect::<Vec<_>>(),
+ vec![
+ Path::new(""),
+ Path::new(".gitignore"),
+ Path::new("a"),
+ Path::new("a/c"),
+ ]
+ );
+ assert_eq!(
+ tree.entries(true)
+ .map(|entry| entry.path.as_ref())
+ .collect::<Vec<_>>(),
+ vec![
+ Path::new(""),
+ Path::new(".gitignore"),
+ Path::new("a"),
+ Path::new("a/b"),
+ Path::new("a/c"),
+ ]
+ );
+ })
+}
+
+#[gpui::test]
+async fn test_descendent_entries(cx: &mut TestAppContext) {
+ init_test(cx);
+ let fs = FakeFs::new(cx.background_executor.clone());
+ fs.insert_tree(
+ "/root",
+ json!({
+ "a": "",
+ "b": {
+ "c": {
+ "d": ""
+ },
+ "e": {}
+ },
+ "f": "",
+ "g": {
+ "h": {}
+ },
+ "i": {
+ "j": {
+ "k": ""
+ },
+ "l": {
+
+ }
+ },
+ ".gitignore": "i/j\n",
+ }),
+ )
+ .await;
+
+ let tree = Worktree::local(
+ build_client(cx),
+ Path::new("/root"),
+ true,
+ fs,
+ Default::default(),
+ &mut cx.to_async(),
+ )
+ .await
+ .unwrap();
+ cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+ .await;
+
+ tree.read_with(cx, |tree, _| {
+ assert_eq!(
+ tree.descendent_entries(false, false, Path::new("b"))
+ .map(|entry| entry.path.as_ref())
+ .collect::<Vec<_>>(),
+ vec![Path::new("b/c/d"),]
+ );
+ assert_eq!(
+ tree.descendent_entries(true, false, Path::new("b"))
+ .map(|entry| entry.path.as_ref())
+ .collect::<Vec<_>>(),
+ vec![
+ Path::new("b"),
+ Path::new("b/c"),
+ Path::new("b/c/d"),
+ Path::new("b/e"),
+ ]
+ );
+
+ assert_eq!(
+ tree.descendent_entries(false, false, Path::new("g"))
+ .map(|entry| entry.path.as_ref())
+ .collect::<Vec<_>>(),
+ Vec::<PathBuf>::new()
+ );
+ assert_eq!(
+ tree.descendent_entries(true, false, Path::new("g"))
+ .map(|entry| entry.path.as_ref())
+ .collect::<Vec<_>>(),
+ vec![Path::new("g"), Path::new("g/h"),]
+ );
+ });
+
+ // Expand gitignored directory.
+ tree.read_with(cx, |tree, _| {
+ tree.as_local()
+ .unwrap()
+ .refresh_entries_for_paths(vec![Path::new("i/j").into()])
+ })
+ .recv()
+ .await;
+
+ tree.read_with(cx, |tree, _| {
+ assert_eq!(
+ tree.descendent_entries(false, false, Path::new("i"))
+ .map(|entry| entry.path.as_ref())
+ .collect::<Vec<_>>(),
+ Vec::<PathBuf>::new()
+ );
+ assert_eq!(
+ tree.descendent_entries(false, true, Path::new("i"))
+ .map(|entry| entry.path.as_ref())
+ .collect::<Vec<_>>(),
+ vec![Path::new("i/j/k")]
+ );
+ assert_eq!(
+ tree.descendent_entries(true, false, Path::new("i"))
+ .map(|entry| entry.path.as_ref())
+ .collect::<Vec<_>>(),
+ vec![Path::new("i"), Path::new("i/l"),]
+ );
+ })
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_circular_symlinks(cx: &mut TestAppContext) {
+ init_test(cx);
+ let fs = FakeFs::new(cx.background_executor.clone());
+ fs.insert_tree(
+ "/root",
+ json!({
+ "lib": {
+ "a": {
+ "a.txt": ""
+ },
+ "b": {
+ "b.txt": ""
+ }
+ }
+ }),
+ )
+ .await;
+ fs.insert_symlink("/root/lib/a/lib", "..".into()).await;
+ fs.insert_symlink("/root/lib/b/lib", "..".into()).await;
+
+ let tree = Worktree::local(
+ build_client(cx),
+ Path::new("/root"),
+ true,
+ fs.clone(),
+ Default::default(),
+ &mut cx.to_async(),
+ )
+ .await
+ .unwrap();
+
+ cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+ .await;
+
+ tree.read_with(cx, |tree, _| {
+ assert_eq!(
+ tree.entries(false)
+ .map(|entry| entry.path.as_ref())
+ .collect::<Vec<_>>(),
+ vec![
+ Path::new(""),
+ Path::new("lib"),
+ Path::new("lib/a"),
+ Path::new("lib/a/a.txt"),
+ Path::new("lib/a/lib"),
+ Path::new("lib/b"),
+ Path::new("lib/b/b.txt"),
+ Path::new("lib/b/lib"),
+ ]
+ );
+ });
+
+ fs.rename(
+ Path::new("/root/lib/a/lib"),
+ Path::new("/root/lib/a/lib-2"),
+ Default::default(),
+ )
+ .await
+ .unwrap();
+ cx.executor().run_until_parked();
+ tree.read_with(cx, |tree, _| {
+ assert_eq!(
+ tree.entries(false)
+ .map(|entry| entry.path.as_ref())
+ .collect::<Vec<_>>(),
+ vec![
+ Path::new(""),
+ Path::new("lib"),
+ Path::new("lib/a"),
+ Path::new("lib/a/a.txt"),
+ Path::new("lib/a/lib-2"),
+ Path::new("lib/b"),
+ Path::new("lib/b/b.txt"),
+ Path::new("lib/b/lib"),
+ ]
+ );
+ });
+}
+
+#[gpui::test]
+async fn test_symlinks_pointing_outside(cx: &mut TestAppContext) {
+ init_test(cx);
+ let fs = FakeFs::new(cx.background_executor.clone());
+ fs.insert_tree(
+ "/root",
+ json!({
+ "dir1": {
+ "deps": {
+ // symlinks here
+ },
+ "src": {
+ "a.rs": "",
+ "b.rs": "",
+ },
+ },
+ "dir2": {
+ "src": {
+ "c.rs": "",
+ "d.rs": "",
+ }
+ },
+ "dir3": {
+ "deps": {},
+ "src": {
+ "e.rs": "",
+ "f.rs": "",
+ },
+ }
+ }),
+ )
+ .await;
+
+ // These symlinks point to directories outside of the worktree's root, dir1.
+ fs.insert_symlink("/root/dir1/deps/dep-dir2", "../../dir2".into())
+ .await;
+ fs.insert_symlink("/root/dir1/deps/dep-dir3", "../../dir3".into())
+ .await;
+
+ let tree = Worktree::local(
+ build_client(cx),
+ Path::new("/root/dir1"),
+ true,
+ fs.clone(),
+ Default::default(),
+ &mut cx.to_async(),
+ )
+ .await
+ .unwrap();
+
+ cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+ .await;
+
+ let tree_updates = Arc::new(Mutex::new(Vec::new()));
+ tree.update(cx, |_, cx| {
+ let tree_updates = tree_updates.clone();
+ cx.subscribe(&tree, move |_, _, event, _| {
+ if let Event::UpdatedEntries(update) = event {
+ tree_updates.lock().extend(
+ update
+ .iter()
+ .map(|(path, _, change)| (path.clone(), *change)),
+ );
+ }
+ })
+ .detach();
+ });
+
+ // The symlinked directories are not scanned by default.
+ tree.read_with(cx, |tree, _| {
+ assert_eq!(
+ tree.entries(true)
+ .map(|entry| (entry.path.as_ref(), entry.is_external))
+ .collect::<Vec<_>>(),
+ vec![
+ (Path::new(""), false),
+ (Path::new("deps"), false),
+ (Path::new("deps/dep-dir2"), true),
+ (Path::new("deps/dep-dir3"), true),
+ (Path::new("src"), false),
+ (Path::new("src/a.rs"), false),
+ (Path::new("src/b.rs"), false),
+ ]
+ );
+
+ assert_eq!(
+ tree.entry_for_path("deps/dep-dir2").unwrap().kind,
+ EntryKind::UnloadedDir
+ );
+ });
+
+ // Expand one of the symlinked directories.
+ tree.read_with(cx, |tree, _| {
+ tree.as_local()
+ .unwrap()
+ .refresh_entries_for_paths(vec![Path::new("deps/dep-dir3").into()])
+ })
+ .recv()
+ .await;
+
+ // The expanded directory's contents are loaded. Subdirectories are
+ // not scanned yet.
+ tree.read_with(cx, |tree, _| {
+ assert_eq!(
+ tree.entries(true)
+ .map(|entry| (entry.path.as_ref(), entry.is_external))
+ .collect::<Vec<_>>(),
+ vec![
+ (Path::new(""), false),
+ (Path::new("deps"), false),
+ (Path::new("deps/dep-dir2"), true),
+ (Path::new("deps/dep-dir3"), true),
+ (Path::new("deps/dep-dir3/deps"), true),
+ (Path::new("deps/dep-dir3/src"), true),
+ (Path::new("src"), false),
+ (Path::new("src/a.rs"), false),
+ (Path::new("src/b.rs"), false),
+ ]
+ );
+ });
+ assert_eq!(
+ mem::take(&mut *tree_updates.lock()),
+ &[
+ (Path::new("deps/dep-dir3").into(), PathChange::Loaded),
+ (Path::new("deps/dep-dir3/deps").into(), PathChange::Loaded),
+ (Path::new("deps/dep-dir3/src").into(), PathChange::Loaded)
+ ]
+ );
+
+ // Expand a subdirectory of one of the symlinked directories.
+ tree.read_with(cx, |tree, _| {
+ tree.as_local()
+ .unwrap()
+ .refresh_entries_for_paths(vec![Path::new("deps/dep-dir3/src").into()])
+ })
+ .recv()
+ .await;
+
+ // The expanded subdirectory's contents are loaded.
+ tree.read_with(cx, |tree, _| {
+ assert_eq!(
+ tree.entries(true)
+ .map(|entry| (entry.path.as_ref(), entry.is_external))
+ .collect::<Vec<_>>(),
+ vec![
+ (Path::new(""), false),
+ (Path::new("deps"), false),
+ (Path::new("deps/dep-dir2"), true),
+ (Path::new("deps/dep-dir3"), true),
+ (Path::new("deps/dep-dir3/deps"), true),
+ (Path::new("deps/dep-dir3/src"), true),
+ (Path::new("deps/dep-dir3/src/e.rs"), true),
+ (Path::new("deps/dep-dir3/src/f.rs"), true),
+ (Path::new("src"), false),
+ (Path::new("src/a.rs"), false),
+ (Path::new("src/b.rs"), false),
+ ]
+ );
+ });
+
+ assert_eq!(
+ mem::take(&mut *tree_updates.lock()),
+ &[
+ (Path::new("deps/dep-dir3/src").into(), PathChange::Loaded),
+ (
+ Path::new("deps/dep-dir3/src/e.rs").into(),
+ PathChange::Loaded
+ ),
+ (
+ Path::new("deps/dep-dir3/src/f.rs").into(),
+ PathChange::Loaded
+ )
+ ]
+ );
+}
+
+#[gpui::test]
+async fn test_open_gitignored_files(cx: &mut TestAppContext) {
+ init_test(cx);
+ let fs = FakeFs::new(cx.background_executor.clone());
+ fs.insert_tree(
+ "/root",
+ json!({
+ ".gitignore": "node_modules\n",
+ "one": {
+ "node_modules": {
+ "a": {
+ "a1.js": "a1",
+ "a2.js": "a2",
+ },
+ "b": {
+ "b1.js": "b1",
+ "b2.js": "b2",
+ },
+ "c": {
+ "c1.js": "c1",
+ "c2.js": "c2",
+ }
+ },
+ },
+ "two": {
+ "x.js": "",
+ "y.js": "",
+ },
+ }),
+ )
+ .await;
+
+ let tree = Worktree::local(
+ build_client(cx),
+ Path::new("/root"),
+ true,
+ fs.clone(),
+ Default::default(),
+ &mut cx.to_async(),
+ )
+ .await
+ .unwrap();
+
+ cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+ .await;
+
+ tree.read_with(cx, |tree, _| {
+ assert_eq!(
+ tree.entries(true)
+ .map(|entry| (entry.path.as_ref(), entry.is_ignored))
+ .collect::<Vec<_>>(),
+ vec![
+ (Path::new(""), false),
+ (Path::new(".gitignore"), false),
+ (Path::new("one"), false),
+ (Path::new("one/node_modules"), true),
+ (Path::new("two"), false),
+ (Path::new("two/x.js"), false),
+ (Path::new("two/y.js"), false),
+ ]
+ );
+ });
+
+ // Open a file that is nested inside of a gitignored directory that
+ // has not yet been expanded.
+ let prev_read_dir_count = fs.read_dir_call_count();
+ let buffer = tree
+ .update(cx, |tree, cx| {
+ tree.as_local_mut()
+ .unwrap()
+ .load_buffer(0, "one/node_modules/b/b1.js".as_ref(), cx)
+ })
+ .await
+ .unwrap();
+
+ tree.read_with(cx, |tree, cx| {
+ assert_eq!(
+ tree.entries(true)
+ .map(|entry| (entry.path.as_ref(), entry.is_ignored))
+ .collect::<Vec<_>>(),
+ vec![
+ (Path::new(""), false),
+ (Path::new(".gitignore"), false),
+ (Path::new("one"), false),
+ (Path::new("one/node_modules"), true),
+ (Path::new("one/node_modules/a"), true),
+ (Path::new("one/node_modules/b"), true),
+ (Path::new("one/node_modules/b/b1.js"), true),
+ (Path::new("one/node_modules/b/b2.js"), true),
+ (Path::new("one/node_modules/c"), true),
+ (Path::new("two"), false),
+ (Path::new("two/x.js"), false),
+ (Path::new("two/y.js"), false),
+ ]
+ );
+
+ assert_eq!(
+ buffer.read(cx).file().unwrap().path().as_ref(),
+ Path::new("one/node_modules/b/b1.js")
+ );
+
+ // Only the newly-expanded directories are scanned.
+ assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 2);
+ });
+
+ // Open another file in a different subdirectory of the same
+ // gitignored directory.
+ let prev_read_dir_count = fs.read_dir_call_count();
+ let buffer = tree
+ .update(cx, |tree, cx| {
+ tree.as_local_mut()
+ .unwrap()
+ .load_buffer(0, "one/node_modules/a/a2.js".as_ref(), cx)
+ })
+ .await
+ .unwrap();
+
+ tree.read_with(cx, |tree, cx| {
+ assert_eq!(
+ tree.entries(true)
+ .map(|entry| (entry.path.as_ref(), entry.is_ignored))
+ .collect::<Vec<_>>(),
+ vec![
+ (Path::new(""), false),
+ (Path::new(".gitignore"), false),
+ (Path::new("one"), false),
+ (Path::new("one/node_modules"), true),
+ (Path::new("one/node_modules/a"), true),
+ (Path::new("one/node_modules/a/a1.js"), true),
+ (Path::new("one/node_modules/a/a2.js"), true),
+ (Path::new("one/node_modules/b"), true),
+ (Path::new("one/node_modules/b/b1.js"), true),
+ (Path::new("one/node_modules/b/b2.js"), true),
+ (Path::new("one/node_modules/c"), true),
+ (Path::new("two"), false),
+ (Path::new("two/x.js"), false),
+ (Path::new("two/y.js"), false),
+ ]
+ );
+
+ assert_eq!(
+ buffer.read(cx).file().unwrap().path().as_ref(),
+ Path::new("one/node_modules/a/a2.js")
+ );
+
+ // Only the newly-expanded directory is scanned.
+ assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 1);
+ });
+
+ // No work happens when files and directories change within an unloaded directory.
+ let prev_fs_call_count = fs.read_dir_call_count() + fs.metadata_call_count();
+ fs.create_dir("/root/one/node_modules/c/lib".as_ref())
+ .await
+ .unwrap();
+ cx.executor().run_until_parked();
+ assert_eq!(
+ fs.read_dir_call_count() + fs.metadata_call_count() - prev_fs_call_count,
+ 0
+ );
+}
+
+#[gpui::test]
+async fn test_dirs_no_longer_ignored(cx: &mut TestAppContext) {
+ init_test(cx);
+ let fs = FakeFs::new(cx.background_executor.clone());
+ fs.insert_tree(
+ "/root",
+ json!({
+ ".gitignore": "node_modules\n",
+ "a": {
+ "a.js": "",
+ },
+ "b": {
+ "b.js": "",
+ },
+ "node_modules": {
+ "c": {
+ "c.js": "",
+ },
+ "d": {
+ "d.js": "",
+ "e": {
+ "e1.js": "",
+ "e2.js": "",
+ },
+ "f": {
+ "f1.js": "",
+ "f2.js": "",
+ }
+ },
+ },
+ }),
+ )
+ .await;
+
+ let tree = Worktree::local(
+ build_client(cx),
+ Path::new("/root"),
+ true,
+ fs.clone(),
+ Default::default(),
+ &mut cx.to_async(),
+ )
+ .await
+ .unwrap();
+
+ cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+ .await;
+
+ // Open a file within the gitignored directory, forcing some of its
+ // subdirectories to be read, but not all.
+ let read_dir_count_1 = fs.read_dir_call_count();
+ tree.read_with(cx, |tree, _| {
+ tree.as_local()
+ .unwrap()
+ .refresh_entries_for_paths(vec![Path::new("node_modules/d/d.js").into()])
+ })
+ .recv()
+ .await;
+
+ // Those subdirectories are now loaded.
+ tree.read_with(cx, |tree, _| {
+ assert_eq!(
+ tree.entries(true)
+ .map(|e| (e.path.as_ref(), e.is_ignored))
+ .collect::<Vec<_>>(),
+ &[
+ (Path::new(""), false),
+ (Path::new(".gitignore"), false),
+ (Path::new("a"), false),
+ (Path::new("a/a.js"), false),
+ (Path::new("b"), false),
+ (Path::new("b/b.js"), false),
+ (Path::new("node_modules"), true),
+ (Path::new("node_modules/c"), true),
+ (Path::new("node_modules/d"), true),
+ (Path::new("node_modules/d/d.js"), true),
+ (Path::new("node_modules/d/e"), true),
+ (Path::new("node_modules/d/f"), true),
+ ]
+ );
+ });
+ let read_dir_count_2 = fs.read_dir_call_count();
+ assert_eq!(read_dir_count_2 - read_dir_count_1, 2);
+
+ // Update the gitignore so that node_modules is no longer ignored,
+ // but a subdirectory is ignored
+ fs.save("/root/.gitignore".as_ref(), &"e".into(), Default::default())
+ .await
+ .unwrap();
+ cx.executor().run_until_parked();
+
+ // All of the directories that are no longer ignored are now loaded.
+ tree.read_with(cx, |tree, _| {
+ assert_eq!(
+ tree.entries(true)
+ .map(|e| (e.path.as_ref(), e.is_ignored))
+ .collect::<Vec<_>>(),
+ &[
+ (Path::new(""), false),
+ (Path::new(".gitignore"), false),
+ (Path::new("a"), false),
+ (Path::new("a/a.js"), false),
+ (Path::new("b"), false),
+ (Path::new("b/b.js"), false),
+ // This directory is no longer ignored
+ (Path::new("node_modules"), false),
+ (Path::new("node_modules/c"), false),
+ (Path::new("node_modules/c/c.js"), false),
+ (Path::new("node_modules/d"), false),
+ (Path::new("node_modules/d/d.js"), false),
+ // This subdirectory is now ignored
+ (Path::new("node_modules/d/e"), true),
+ (Path::new("node_modules/d/f"), false),
+ (Path::new("node_modules/d/f/f1.js"), false),
+ (Path::new("node_modules/d/f/f2.js"), false),
+ ]
+ );
+ });
+
+ // Each of the newly-loaded directories is scanned only once.
+ let read_dir_count_3 = fs.read_dir_call_count();
+ assert_eq!(read_dir_count_3 - read_dir_count_2, 2);
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_rescan_with_gitignore(cx: &mut TestAppContext) {
+ init_test(cx);
+ cx.update(|cx| {
+ cx.update_global::<SettingsStore, _>(|store, cx| {
+ store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
+ project_settings.file_scan_exclusions = Some(Vec::new());
+ });
+ });
+ });
+ let fs = FakeFs::new(cx.background_executor.clone());
+ fs.insert_tree(
+ "/root",
+ json!({
+ ".gitignore": "ancestor-ignored-file1\nancestor-ignored-file2\n",
+ "tree": {
+ ".git": {},
+ ".gitignore": "ignored-dir\n",
+ "tracked-dir": {
+ "tracked-file1": "",
+ "ancestor-ignored-file1": "",
+ },
+ "ignored-dir": {
+ "ignored-file1": ""
+ }
+ }
+ }),
+ )
+ .await;
+
+ let tree = Worktree::local(
+ build_client(cx),
+ "/root/tree".as_ref(),
+ true,
+ fs.clone(),
+ Default::default(),
+ &mut cx.to_async(),
+ )
+ .await
+ .unwrap();
+ cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+ .await;
+
+ tree.read_with(cx, |tree, _| {
+ tree.as_local()
+ .unwrap()
+ .refresh_entries_for_paths(vec![Path::new("ignored-dir").into()])
+ })
+ .recv()
+ .await;
+
+ cx.read(|cx| {
+ let tree = tree.read(cx);
+ assert!(
+ !tree
+ .entry_for_path("tracked-dir/tracked-file1")
+ .unwrap()
+ .is_ignored
+ );
+ assert!(
+ tree.entry_for_path("tracked-dir/ancestor-ignored-file1")
+ .unwrap()
+ .is_ignored
+ );
+ assert!(
+ tree.entry_for_path("ignored-dir/ignored-file1")
+ .unwrap()
+ .is_ignored
+ );
+ });
+
+ fs.create_file(
+ "/root/tree/tracked-dir/tracked-file2".as_ref(),
+ Default::default(),
+ )
+ .await
+ .unwrap();
+ fs.create_file(
+ "/root/tree/tracked-dir/ancestor-ignored-file2".as_ref(),
+ Default::default(),
+ )
+ .await
+ .unwrap();
+ fs.create_file(
+ "/root/tree/ignored-dir/ignored-file2".as_ref(),
+ Default::default(),
+ )
+ .await
+ .unwrap();
+
+ cx.executor().run_until_parked();
+ cx.read(|cx| {
+ let tree = tree.read(cx);
+ assert!(
+ !tree
+ .entry_for_path("tracked-dir/tracked-file2")
+ .unwrap()
+ .is_ignored
+ );
+ assert!(
+ tree.entry_for_path("tracked-dir/ancestor-ignored-file2")
+ .unwrap()
+ .is_ignored
+ );
+ assert!(
+ tree.entry_for_path("ignored-dir/ignored-file2")
+ .unwrap()
+ .is_ignored
+ );
+ assert!(tree.entry_for_path(".git").unwrap().is_ignored);
+ });
+}
+
+#[gpui::test]
+async fn test_write_file(cx: &mut TestAppContext) {
+ init_test(cx);
+ cx.executor().allow_parking();
+ let dir = temp_tree(json!({
+ ".git": {},
+ ".gitignore": "ignored-dir\n",
+ "tracked-dir": {},
+ "ignored-dir": {}
+ }));
+
+ let tree = Worktree::local(
+ build_client(cx),
+ dir.path(),
+ true,
+ Arc::new(RealFs),
+ Default::default(),
+ &mut cx.to_async(),
+ )
+ .await
+ .unwrap();
+ cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+ .await;
+ tree.flush_fs_events(cx).await;
+
+ tree.update(cx, |tree, cx| {
+ tree.as_local().unwrap().write_file(
+ Path::new("tracked-dir/file.txt"),
+ "hello".into(),
+ Default::default(),
+ cx,
+ )
+ })
+ .await
+ .unwrap();
+ tree.update(cx, |tree, cx| {
+ tree.as_local().unwrap().write_file(
+ Path::new("ignored-dir/file.txt"),
+ "world".into(),
+ Default::default(),
+ cx,
+ )
+ })
+ .await
+ .unwrap();
+
+ tree.read_with(cx, |tree, _| {
+ let tracked = tree.entry_for_path("tracked-dir/file.txt").unwrap();
+ let ignored = tree.entry_for_path("ignored-dir/file.txt").unwrap();
+ assert!(!tracked.is_ignored);
+ assert!(ignored.is_ignored);
+ });
+}
+
+#[gpui::test]
+async fn test_file_scan_exclusions(cx: &mut TestAppContext) {
+ init_test(cx);
+ cx.executor().allow_parking();
+ let dir = temp_tree(json!({
+ ".gitignore": "**/target\n/node_modules\n",
+ "target": {
+ "index": "blah2"
+ },
+ "node_modules": {
+ ".DS_Store": "",
+ "prettier": {
+ "package.json": "{}",
+ },
+ },
+ "src": {
+ ".DS_Store": "",
+ "foo": {
+ "foo.rs": "mod another;\n",
+ "another.rs": "// another",
+ },
+ "bar": {
+ "bar.rs": "// bar",
+ },
+ "lib.rs": "mod foo;\nmod bar;\n",
+ },
+ ".DS_Store": "",
+ }));
+ cx.update(|cx| {
+ cx.update_global::<SettingsStore, _>(|store, cx| {
+ store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
+ project_settings.file_scan_exclusions =
+ Some(vec!["**/foo/**".to_string(), "**/.DS_Store".to_string()]);
+ });
+ });
+ });
+
+ let tree = Worktree::local(
+ build_client(cx),
+ dir.path(),
+ true,
+ Arc::new(RealFs),
+ Default::default(),
+ &mut cx.to_async(),
+ )
+ .await
+ .unwrap();
+ cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+ .await;
+ tree.flush_fs_events(cx).await;
+ tree.read_with(cx, |tree, _| {
+ check_worktree_entries(
+ tree,
+ &[
+ "src/foo/foo.rs",
+ "src/foo/another.rs",
+ "node_modules/.DS_Store",
+ "src/.DS_Store",
+ ".DS_Store",
+ ],
+ &["target", "node_modules"],
+ &["src/lib.rs", "src/bar/bar.rs", ".gitignore"],
+ )
+ });
+
+ cx.update(|cx| {
+ cx.update_global::<SettingsStore, _>(|store, cx| {
+ store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
+ project_settings.file_scan_exclusions =
+ Some(vec!["**/node_modules/**".to_string()]);
+ });
+ });
+ });
+ tree.flush_fs_events(cx).await;
+ cx.executor().run_until_parked();
+ tree.read_with(cx, |tree, _| {
+ check_worktree_entries(
+ tree,
+ &[
+ "node_modules/prettier/package.json",
+ "node_modules/.DS_Store",
+ "node_modules",
+ ],
+ &["target"],
+ &[
+ ".gitignore",
+ "src/lib.rs",
+ "src/bar/bar.rs",
+ "src/foo/foo.rs",
+ "src/foo/another.rs",
+ "src/.DS_Store",
+ ".DS_Store",
+ ],
+ )
+ });
+}
+
+#[gpui::test(iterations = 30)]
+async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
+ init_test(cx);
+ let fs = FakeFs::new(cx.background_executor.clone());
+ fs.insert_tree(
+ "/root",
+ json!({
+ "b": {},
+ "c": {},
+ "d": {},
+ }),
+ )
+ .await;
+
+ let tree = Worktree::local(
+ build_client(cx),
+ "/root".as_ref(),
+ true,
+ fs,
+ Default::default(),
+ &mut cx.to_async(),
+ )
+ .await
+ .unwrap();
+
+ let snapshot1 = tree.update(cx, |tree, cx| {
+ let tree = tree.as_local_mut().unwrap();
+ let snapshot = Arc::new(Mutex::new(tree.snapshot()));
+ let _ = tree.observe_updates(0, cx, {
+ let snapshot = snapshot.clone();
+ move |update| {
+ snapshot.lock().apply_remote_update(update).unwrap();
+ async { true }
+ }
+ });
+ snapshot
+ });
+
+ let entry = tree
+ .update(cx, |tree, cx| {
+ tree.as_local_mut()
+ .unwrap()
+ .create_entry("a/e".as_ref(), true, cx)
+ })
+ .await
+ .unwrap();
+ assert!(entry.is_dir());
+
+ cx.executor().run_until_parked();
+ tree.read_with(cx, |tree, _| {
+ assert_eq!(tree.entry_for_path("a/e").unwrap().kind, EntryKind::Dir);
+ });
+
+ let snapshot2 = tree.update(cx, |tree, _| tree.as_local().unwrap().snapshot());
+ assert_eq!(
+ snapshot1.lock().entries(true).collect::<Vec<_>>(),
+ snapshot2.entries(true).collect::<Vec<_>>()
+ );
+}
+
+#[gpui::test]
+async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
+ init_test(cx);
+ cx.executor().allow_parking();
+ let client_fake = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
+
+ let fs_fake = FakeFs::new(cx.background_executor.clone());
+ fs_fake
+ .insert_tree(
+ "/root",
+ json!({
+ "a": {},
+ }),
+ )
+ .await;
+
+ let tree_fake = Worktree::local(
+ client_fake,
+ "/root".as_ref(),
+ true,
+ fs_fake,
+ Default::default(),
+ &mut cx.to_async(),
+ )
+ .await
+ .unwrap();
+
+ let entry = tree_fake
+ .update(cx, |tree, cx| {
+ tree.as_local_mut()
+ .unwrap()
+ .create_entry("a/b/c/d.txt".as_ref(), false, cx)
+ })
+ .await
+ .unwrap();
+ assert!(entry.is_file());
+
+ cx.executor().run_until_parked();
+ tree_fake.read_with(cx, |tree, _| {
+ assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file());
+ assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir());
+ assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
+ });
+
+ let client_real = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
+
+ let fs_real = Arc::new(RealFs);
+ let temp_root = temp_tree(json!({
+ "a": {}
+ }));
+
+ let tree_real = Worktree::local(
+ client_real,
+ temp_root.path(),
+ true,
+ fs_real,
+ Default::default(),
+ &mut cx.to_async(),
+ )
+ .await
+ .unwrap();
+
+ let entry = tree_real
+ .update(cx, |tree, cx| {
+ tree.as_local_mut()
+ .unwrap()
+ .create_entry("a/b/c/d.txt".as_ref(), false, cx)
+ })
+ .await
+ .unwrap();
+ assert!(entry.is_file());
+
+ cx.executor().run_until_parked();
+ tree_real.read_with(cx, |tree, _| {
+ assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file());
+ assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir());
+ assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
+ });
+
+ // Test smallest change
+ let entry = tree_real
+ .update(cx, |tree, cx| {
+ tree.as_local_mut()
+ .unwrap()
+ .create_entry("a/b/c/e.txt".as_ref(), false, cx)
+ })
+ .await
+ .unwrap();
+ assert!(entry.is_file());
+
+ cx.executor().run_until_parked();
+ tree_real.read_with(cx, |tree, _| {
+ assert!(tree.entry_for_path("a/b/c/e.txt").unwrap().is_file());
+ });
+
+ // Test largest change
+ let entry = tree_real
+ .update(cx, |tree, cx| {
+ tree.as_local_mut()
+ .unwrap()
+ .create_entry("d/e/f/g.txt".as_ref(), false, cx)
+ })
+ .await
+ .unwrap();
+ assert!(entry.is_file());
+
+ cx.executor().run_until_parked();
+ tree_real.read_with(cx, |tree, _| {
+ assert!(tree.entry_for_path("d/e/f/g.txt").unwrap().is_file());
+ assert!(tree.entry_for_path("d/e/f").unwrap().is_dir());
+ assert!(tree.entry_for_path("d/e/").unwrap().is_dir());
+ assert!(tree.entry_for_path("d/").unwrap().is_dir());
+ });
+}
+
+#[gpui::test(iterations = 100)]
+async fn test_random_worktree_operations_during_initial_scan(
+ cx: &mut TestAppContext,
+ mut rng: StdRng,
+) {
+ init_test(cx);
+ let operations = env::var("OPERATIONS")
+ .map(|o| o.parse().unwrap())
+ .unwrap_or(5);
+ let initial_entries = env::var("INITIAL_ENTRIES")
+ .map(|o| o.parse().unwrap())
+ .unwrap_or(20);
+
+ let root_dir = Path::new("/test");
+ let fs = FakeFs::new(cx.background_executor.clone()) as Arc<dyn Fs>;
+ fs.as_fake().insert_tree(root_dir, json!({})).await;
+ for _ in 0..initial_entries {
+ randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
+ }
+ log::info!("generated initial tree");
+
+ let worktree = Worktree::local(
+ build_client(cx),
+ root_dir,
+ true,
+ fs.clone(),
+ Default::default(),
+ &mut cx.to_async(),
+ )
+ .await
+ .unwrap();
+
+ let mut snapshots = vec![worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot())];
+ let updates = Arc::new(Mutex::new(Vec::new()));
+ worktree.update(cx, |tree, cx| {
+ check_worktree_change_events(tree, cx);
+
+ let _ = tree.as_local_mut().unwrap().observe_updates(0, cx, {
+ let updates = updates.clone();
+ move |update| {
+ updates.lock().push(update);
+ async { true }
+ }
+ });
+ });
+
+ for _ in 0..operations {
+ worktree
+ .update(cx, |worktree, cx| {
+ randomly_mutate_worktree(worktree, &mut rng, cx)
+ })
+ .await
+ .log_err();
+ worktree.read_with(cx, |tree, _| {
+ tree.as_local().unwrap().snapshot().check_invariants(true)
+ });
+
+ if rng.gen_bool(0.6) {
+ snapshots.push(worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot()));
+ }
+ }
+
+ worktree
+ .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
+ .await;
+
+ cx.executor().run_until_parked();
+
+ let final_snapshot = worktree.read_with(cx, |tree, _| {
+ let tree = tree.as_local().unwrap();
+ let snapshot = tree.snapshot();
+ snapshot.check_invariants(true);
+ snapshot
+ });
+
+ for (i, snapshot) in snapshots.into_iter().enumerate().rev() {
+ let mut updated_snapshot = snapshot.clone();
+ for update in updates.lock().iter() {
+ if update.scan_id >= updated_snapshot.scan_id() as u64 {
+ updated_snapshot
+ .apply_remote_update(update.clone())
+ .unwrap();
+ }
+ }
+
+ assert_eq!(
+ updated_snapshot.entries(true).collect::<Vec<_>>(),
+ final_snapshot.entries(true).collect::<Vec<_>>(),
+ "wrong updates after snapshot {i}: {snapshot:#?} {updates:#?}",
+ );
+ }
+}
+
+#[gpui::test(iterations = 100)]
+async fn test_random_worktree_changes(cx: &mut TestAppContext, mut rng: StdRng) {
+ init_test(cx);
+ let operations = env::var("OPERATIONS")
+ .map(|o| o.parse().unwrap())
+ .unwrap_or(40);
+ let initial_entries = env::var("INITIAL_ENTRIES")
+ .map(|o| o.parse().unwrap())
+ .unwrap_or(20);
+
+ let root_dir = Path::new("/test");
+ let fs = FakeFs::new(cx.background_executor.clone()) as Arc<dyn Fs>;
+ fs.as_fake().insert_tree(root_dir, json!({})).await;
+ for _ in 0..initial_entries {
+ randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
+ }
+ log::info!("generated initial tree");
+
+ let worktree = Worktree::local(
+ build_client(cx),
+ root_dir,
+ true,
+ fs.clone(),
+ Default::default(),
+ &mut cx.to_async(),
+ )
+ .await
+ .unwrap();
+
+ let updates = Arc::new(Mutex::new(Vec::new()));
+ worktree.update(cx, |tree, cx| {
+ check_worktree_change_events(tree, cx);
+
+ let _ = tree.as_local_mut().unwrap().observe_updates(0, cx, {
+ let updates = updates.clone();
+ move |update| {
+ updates.lock().push(update);
+ async { true }
+ }
+ });
+ });
+
+ worktree
+ .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
+ .await;
+
+ fs.as_fake().pause_events();
+ let mut snapshots = Vec::new();
+ let mut mutations_len = operations;
+ while mutations_len > 1 {
+ if rng.gen_bool(0.2) {
+ worktree
+ .update(cx, |worktree, cx| {
+ randomly_mutate_worktree(worktree, &mut rng, cx)
+ })
+ .await
+ .log_err();
+ } else {
+ randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
+ }
+
+ let buffered_event_count = fs.as_fake().buffered_event_count();
+ if buffered_event_count > 0 && rng.gen_bool(0.3) {
+ let len = rng.gen_range(0..=buffered_event_count);
+ log::info!("flushing {} events", len);
+ fs.as_fake().flush_events(len);
+ } else {
+ randomly_mutate_fs(&fs, root_dir, 0.6, &mut rng).await;
+ mutations_len -= 1;
+ }
+
+ cx.executor().run_until_parked();
+ if rng.gen_bool(0.2) {
+ log::info!("storing snapshot {}", snapshots.len());
+ let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
+ snapshots.push(snapshot);
+ }
+ }
+
+ log::info!("quiescing");
+ fs.as_fake().flush_events(usize::MAX);
+ cx.executor().run_until_parked();
+
+ let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
+ snapshot.check_invariants(true);
+ let expanded_paths = snapshot
+ .expanded_entries()
+ .map(|e| e.path.clone())
+ .collect::<Vec<_>>();
+
+ {
+ let new_worktree = Worktree::local(
+ build_client(cx),
+ root_dir,
+ true,
+ fs.clone(),
+ Default::default(),
+ &mut cx.to_async(),
+ )
+ .await
+ .unwrap();
+ new_worktree
+ .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
+ .await;
+ new_worktree
+ .update(cx, |tree, _| {
+ tree.as_local_mut()
+ .unwrap()
+ .refresh_entries_for_paths(expanded_paths)
+ })
+ .recv()
+ .await;
+ let new_snapshot =
+ new_worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
+ assert_eq!(
+ snapshot.entries_without_ids(true),
+ new_snapshot.entries_without_ids(true)
+ );
+ }
+
+ for (i, mut prev_snapshot) in snapshots.into_iter().enumerate().rev() {
+ for update in updates.lock().iter() {
+ if update.scan_id >= prev_snapshot.scan_id() as u64 {
+ prev_snapshot.apply_remote_update(update.clone()).unwrap();
+ }
+ }
+
+ assert_eq!(
+ prev_snapshot
+ .entries(true)
+ .map(ignore_pending_dir)
+ .collect::<Vec<_>>(),
+ snapshot
+ .entries(true)
+ .map(ignore_pending_dir)
+ .collect::<Vec<_>>(),
+ "wrong updates after snapshot {i}: {updates:#?}",
+ );
+ }
+
+ fn ignore_pending_dir(entry: &Entry) -> Entry {
+ let mut entry = entry.clone();
+ if entry.kind.is_dir() {
+ entry.kind = EntryKind::Dir
+ }
+ entry
+ }
+}
+
+// The worktree's `UpdatedEntries` event can be used to follow along with
+// all changes to the worktree's snapshot.
+fn check_worktree_change_events(tree: &mut Worktree, cx: &mut ModelContext<Worktree>) {
+ let mut entries = tree.entries(true).cloned().collect::<Vec<_>>();
+ cx.subscribe(&cx.handle(), move |tree, _, event, _| {
+ if let Event::UpdatedEntries(changes) = event {
+ for (path, _, change_type) in changes.iter() {
+ let entry = tree.entry_for_path(&path).cloned();
+ let ix = match entries.binary_search_by_key(&path, |e| &e.path) {
+ Ok(ix) | Err(ix) => ix,
+ };
+ match change_type {
+ PathChange::Added => entries.insert(ix, entry.unwrap()),
+ PathChange::Removed => drop(entries.remove(ix)),
+ PathChange::Updated => {
+ let entry = entry.unwrap();
+ let existing_entry = entries.get_mut(ix).unwrap();
+ assert_eq!(existing_entry.path, entry.path);
+ *existing_entry = entry;
+ }
+ PathChange::AddedOrUpdated | PathChange::Loaded => {
+ let entry = entry.unwrap();
+ if entries.get(ix).map(|e| &e.path) == Some(&entry.path) {
+ *entries.get_mut(ix).unwrap() = entry;
+ } else {
+ entries.insert(ix, entry);
+ }
+ }
+ }
+ }
+
+ let new_entries = tree.entries(true).cloned().collect::<Vec<_>>();
+ assert_eq!(entries, new_entries, "incorrect changes: {:?}", changes);
+ }
+ })
+ .detach();
+}
+
+fn randomly_mutate_worktree(
+ worktree: &mut Worktree,
+ rng: &mut impl Rng,
+ cx: &mut ModelContext<Worktree>,
+) -> Task<Result<()>> {
+ log::info!("mutating worktree");
+ let worktree = worktree.as_local_mut().unwrap();
+ let snapshot = worktree.snapshot();
+ let entry = snapshot.entries(false).choose(rng).unwrap();
+
+ match rng.gen_range(0_u32..100) {
+ 0..=33 if entry.path.as_ref() != Path::new("") => {
+ log::info!("deleting entry {:?} ({})", entry.path, entry.id.0);
+ worktree.delete_entry(entry.id, cx).unwrap()
+ }
+ ..=66 if entry.path.as_ref() != Path::new("") => {
+ let other_entry = snapshot.entries(false).choose(rng).unwrap();
+ let new_parent_path = if other_entry.is_dir() {
+ other_entry.path.clone()
+ } else {
+ other_entry.path.parent().unwrap().into()
+ };
+ let mut new_path = new_parent_path.join(random_filename(rng));
+ if new_path.starts_with(&entry.path) {
+ new_path = random_filename(rng).into();
+ }
+
+ log::info!(
+ "renaming entry {:?} ({}) to {:?}",
+ entry.path,
+ entry.id.0,
+ new_path
+ );
+ let task = worktree.rename_entry(entry.id, new_path, cx).unwrap();
+ cx.background_executor().spawn(async move {
+ task.await?;
+ Ok(())
+ })
+ }
+ _ => {
+ let task = if entry.is_dir() {
+ let child_path = entry.path.join(random_filename(rng));
+ let is_dir = rng.gen_bool(0.3);
+ log::info!(
+ "creating {} at {:?}",
+ if is_dir { "dir" } else { "file" },
+ child_path,
+ );
+ worktree.create_entry(child_path, is_dir, cx)
+ } else {
+ log::info!("overwriting file {:?} ({})", entry.path, entry.id.0);
+ worktree.write_file(entry.path.clone(), "".into(), Default::default(), cx)
+ };
+ cx.background_executor().spawn(async move {
+ task.await?;
+ Ok(())
+ })
+ }
+ }
+}
+
+async fn randomly_mutate_fs(
+ fs: &Arc<dyn Fs>,
+ root_path: &Path,
+ insertion_probability: f64,
+ rng: &mut impl Rng,
+) {
+ log::info!("mutating fs");
+ let mut files = Vec::new();
+ let mut dirs = Vec::new();
+ for path in fs.as_fake().paths(false) {
+ if path.starts_with(root_path) {
+ if fs.is_file(&path).await {
+ files.push(path);
+ } else {
+ dirs.push(path);
+ }
+ }
+ }
+
+ if (files.is_empty() && dirs.len() == 1) || rng.gen_bool(insertion_probability) {
+ let path = dirs.choose(rng).unwrap();
+ let new_path = path.join(random_filename(rng));
+
+ if rng.gen() {
+ log::info!(
+ "creating dir {:?}",
+ new_path.strip_prefix(root_path).unwrap()
+ );
+ fs.create_dir(&new_path).await.unwrap();
+ } else {
+ log::info!(
+ "creating file {:?}",
+ new_path.strip_prefix(root_path).unwrap()
+ );
+ fs.create_file(&new_path, Default::default()).await.unwrap();
+ }
+ } else if rng.gen_bool(0.05) {
+ let ignore_dir_path = dirs.choose(rng).unwrap();
+ let ignore_path = ignore_dir_path.join(&*GITIGNORE);
+
+ let subdirs = dirs
+ .iter()
+ .filter(|d| d.starts_with(&ignore_dir_path))
+ .cloned()
+ .collect::<Vec<_>>();
+ let subfiles = files
+ .iter()
+ .filter(|d| d.starts_with(&ignore_dir_path))
+ .cloned()
+ .collect::<Vec<_>>();
+ let files_to_ignore = {
+ let len = rng.gen_range(0..=subfiles.len());
+ subfiles.choose_multiple(rng, len)
+ };
+ let dirs_to_ignore = {
+ let len = rng.gen_range(0..subdirs.len());
+ subdirs.choose_multiple(rng, len)
+ };
+
+ let mut ignore_contents = String::new();
+ for path_to_ignore in files_to_ignore.chain(dirs_to_ignore) {
+ writeln!(
+ ignore_contents,
+ "{}",
+ path_to_ignore
+ .strip_prefix(&ignore_dir_path)
+ .unwrap()
+ .to_str()
+ .unwrap()
+ )
+ .unwrap();
+ }
+ log::info!(
+ "creating gitignore {:?} with contents:\n{}",
+ ignore_path.strip_prefix(&root_path).unwrap(),
+ ignore_contents
+ );
+ fs.save(
+ &ignore_path,
+ &ignore_contents.as_str().into(),
+ Default::default(),
+ )
+ .await
+ .unwrap();
+ } else {
+ let old_path = {
+ let file_path = files.choose(rng);
+ let dir_path = dirs[1..].choose(rng);
+ file_path.into_iter().chain(dir_path).choose(rng).unwrap()
+ };
+
+ let is_rename = rng.gen();
+ if is_rename {
+ let new_path_parent = dirs
+ .iter()
+ .filter(|d| !d.starts_with(old_path))
+ .choose(rng)
+ .unwrap();
+
+ let overwrite_existing_dir =
+ !old_path.starts_with(&new_path_parent) && rng.gen_bool(0.3);
+ let new_path = if overwrite_existing_dir {
+ fs.remove_dir(
+ &new_path_parent,
+ RemoveOptions {
+ recursive: true,
+ ignore_if_not_exists: true,
+ },
+ )
+ .await
+ .unwrap();
+ new_path_parent.to_path_buf()
+ } else {
+ new_path_parent.join(random_filename(rng))
+ };
+
+ log::info!(
+ "renaming {:?} to {}{:?}",
+ old_path.strip_prefix(&root_path).unwrap(),
+ if overwrite_existing_dir {
+ "overwrite "
+ } else {
+ ""
+ },
+ new_path.strip_prefix(&root_path).unwrap()
+ );
+ fs.rename(
+ &old_path,
+ &new_path,
+ fs::RenameOptions {
+ overwrite: true,
+ ignore_if_exists: true,
+ },
+ )
+ .await
+ .unwrap();
+ } else if fs.is_file(&old_path).await {
+ log::info!(
+ "deleting file {:?}",
+ old_path.strip_prefix(&root_path).unwrap()
+ );
+ fs.remove_file(old_path, Default::default()).await.unwrap();
+ } else {
+ log::info!(
+ "deleting dir {:?}",
+ old_path.strip_prefix(&root_path).unwrap()
+ );
+ fs.remove_dir(
+ &old_path,
+ RemoveOptions {
+ recursive: true,
+ ignore_if_not_exists: true,
+ },
+ )
+ .await
+ .unwrap();
+ }
+ }
+}
+
+fn random_filename(rng: &mut impl Rng) -> String {
+ (0..6)
+ .map(|_| rng.sample(rand::distributions::Alphanumeric))
+ .map(char::from)
+ .collect()
+}
+
+#[gpui::test]
+async fn test_rename_work_directory(cx: &mut TestAppContext) {
+ init_test(cx);
+ cx.executor().allow_parking();
+ let root = temp_tree(json!({
+ "projects": {
+ "project1": {
+ "a": "",
+ "b": "",
+ }
+ },
+
+ }));
+ let root_path = root.path();
+
+ let tree = Worktree::local(
+ build_client(cx),
+ root_path,
+ true,
+ Arc::new(RealFs),
+ Default::default(),
+ &mut cx.to_async(),
+ )
+ .await
+ .unwrap();
+
+ let repo = git_init(&root_path.join("projects/project1"));
+ git_add("a", &repo);
+ git_commit("init", &repo);
+ std::fs::write(root_path.join("projects/project1/a"), "aa").ok();
+
+ cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+ .await;
+
+ tree.flush_fs_events(cx).await;
+
+ cx.read(|cx| {
+ let tree = tree.read(cx);
+ let (work_dir, _) = tree.repositories().next().unwrap();
+ assert_eq!(work_dir.as_ref(), Path::new("projects/project1"));
+ assert_eq!(
+ tree.status_for_file(Path::new("projects/project1/a")),
+ Some(GitFileStatus::Modified)
+ );
+ assert_eq!(
+ tree.status_for_file(Path::new("projects/project1/b")),
+ Some(GitFileStatus::Added)
+ );
+ });
+
+ std::fs::rename(
+ root_path.join("projects/project1"),
+ root_path.join("projects/project2"),
+ )
+ .ok();
+ tree.flush_fs_events(cx).await;
+
+ cx.read(|cx| {
+ let tree = tree.read(cx);
+ let (work_dir, _) = tree.repositories().next().unwrap();
+ assert_eq!(work_dir.as_ref(), Path::new("projects/project2"));
+ assert_eq!(
+ tree.status_for_file(Path::new("projects/project2/a")),
+ Some(GitFileStatus::Modified)
+ );
+ assert_eq!(
+ tree.status_for_file(Path::new("projects/project2/b")),
+ Some(GitFileStatus::Added)
+ );
+ });
+}
+
+#[gpui::test]
+async fn test_git_repository_for_path(cx: &mut TestAppContext) {
+ init_test(cx);
+ cx.executor().allow_parking();
+ let root = temp_tree(json!({
+ "c.txt": "",
+ "dir1": {
+ ".git": {},
+ "deps": {
+ "dep1": {
+ ".git": {},
+ "src": {
+ "a.txt": ""
+ }
+ }
+ },
+ "src": {
+ "b.txt": ""
+ }
+ },
+ }));
+
+ let tree = Worktree::local(
+ build_client(cx),
+ root.path(),
+ true,
+ Arc::new(RealFs),
+ Default::default(),
+ &mut cx.to_async(),
+ )
+ .await
+ .unwrap();
+
+ cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+ .await;
+ tree.flush_fs_events(cx).await;
+
+ tree.read_with(cx, |tree, _cx| {
+ let tree = tree.as_local().unwrap();
+
+ assert!(tree.repository_for_path("c.txt".as_ref()).is_none());
+
+ let entry = tree.repository_for_path("dir1/src/b.txt".as_ref()).unwrap();
+ assert_eq!(
+ entry
+ .work_directory(tree)
+ .map(|directory| directory.as_ref().to_owned()),
+ Some(Path::new("dir1").to_owned())
+ );
+
+ let entry = tree
+ .repository_for_path("dir1/deps/dep1/src/a.txt".as_ref())
+ .unwrap();
+ assert_eq!(
+ entry
+ .work_directory(tree)
+ .map(|directory| directory.as_ref().to_owned()),
+ Some(Path::new("dir1/deps/dep1").to_owned())
+ );
+
+ let entries = tree.files(false, 0);
+
+ let paths_with_repos = tree
+ .entries_with_repositories(entries)
+ .map(|(entry, repo)| {
+ (
+ entry.path.as_ref(),
+ repo.and_then(|repo| {
+ repo.work_directory(&tree)
+ .map(|work_directory| work_directory.0.to_path_buf())
+ }),
+ )
+ })
+ .collect::<Vec<_>>();
+
+ assert_eq!(
+ paths_with_repos,
+ &[
+ (Path::new("c.txt"), None),
+ (
+ Path::new("dir1/deps/dep1/src/a.txt"),
+ Some(Path::new("dir1/deps/dep1").into())
+ ),
+ (Path::new("dir1/src/b.txt"), Some(Path::new("dir1").into())),
+ ]
+ );
+ });
+
+ let repo_update_events = Arc::new(Mutex::new(vec![]));
+ tree.update(cx, |_, cx| {
+ let repo_update_events = repo_update_events.clone();
+ cx.subscribe(&tree, move |_, _, event, _| {
+ if let Event::UpdatedGitRepositories(update) = event {
+ repo_update_events.lock().push(update.clone());
+ }
+ })
+ .detach();
+ });
+
+ std::fs::write(root.path().join("dir1/.git/random_new_file"), "hello").unwrap();
+ tree.flush_fs_events(cx).await;
+
+ assert_eq!(
+ repo_update_events.lock()[0]
+ .iter()
+ .map(|e| e.0.clone())
+ .collect::<Vec<Arc<Path>>>(),
+ vec![Path::new("dir1").into()]
+ );
+
+ std::fs::remove_dir_all(root.path().join("dir1/.git")).unwrap();
+ tree.flush_fs_events(cx).await;
+
+ tree.read_with(cx, |tree, _cx| {
+ let tree = tree.as_local().unwrap();
+
+ assert!(tree
+ .repository_for_path("dir1/src/b.txt".as_ref())
+ .is_none());
+ });
+}
+
+#[gpui::test]
+async fn test_git_status(cx: &mut TestAppContext) {
+ init_test(cx);
+ cx.executor().allow_parking();
+ const IGNORE_RULE: &'static str = "**/target";
+
+ let root = temp_tree(json!({
+ "project": {
+ "a.txt": "a",
+ "b.txt": "bb",
+ "c": {
+ "d": {
+ "e.txt": "eee"
+ }
+ },
+ "f.txt": "ffff",
+ "target": {
+ "build_file": "???"
+ },
+ ".gitignore": IGNORE_RULE
+ },
+
+ }));
+
+ const A_TXT: &'static str = "a.txt";
+ const B_TXT: &'static str = "b.txt";
+ const E_TXT: &'static str = "c/d/e.txt";
+ const F_TXT: &'static str = "f.txt";
+ const DOTGITIGNORE: &'static str = ".gitignore";
+ const BUILD_FILE: &'static str = "target/build_file";
+ let project_path = Path::new("project");
+
+ // Set up git repository before creating the worktree.
+ let work_dir = root.path().join("project");
+ let mut repo = git_init(work_dir.as_path());
+ repo.add_ignore_rule(IGNORE_RULE).unwrap();
+ git_add(A_TXT, &repo);
+ git_add(E_TXT, &repo);
+ git_add(DOTGITIGNORE, &repo);
+ git_commit("Initial commit", &repo);
+
+ let tree = Worktree::local(
+ build_client(cx),
+ root.path(),
+ true,
+ Arc::new(RealFs),
+ Default::default(),
+ &mut cx.to_async(),
+ )
+ .await
+ .unwrap();
+
+ tree.flush_fs_events(cx).await;
+ cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+ .await;
+ cx.executor().run_until_parked();
+
+ // Check that the right git state is observed on startup
+ tree.read_with(cx, |tree, _cx| {
+ let snapshot = tree.snapshot();
+ assert_eq!(snapshot.repositories().count(), 1);
+ let (dir, _) = snapshot.repositories().next().unwrap();
+ assert_eq!(dir.as_ref(), Path::new("project"));
+
+ assert_eq!(
+ snapshot.status_for_file(project_path.join(B_TXT)),
+ Some(GitFileStatus::Added)
+ );
+ assert_eq!(
+ snapshot.status_for_file(project_path.join(F_TXT)),
+ Some(GitFileStatus::Added)
+ );
+ });
+
+ // Modify a file in the working copy.
+ std::fs::write(work_dir.join(A_TXT), "aa").unwrap();
+ tree.flush_fs_events(cx).await;
+ cx.executor().run_until_parked();
+
+ // The worktree detects that the file's git status has changed.
+ tree.read_with(cx, |tree, _cx| {
+ let snapshot = tree.snapshot();
+ assert_eq!(
+ snapshot.status_for_file(project_path.join(A_TXT)),
+ Some(GitFileStatus::Modified)
+ );
+ });
+
+ // Create a commit in the git repository.
+ git_add(A_TXT, &repo);
+ git_add(B_TXT, &repo);
+ git_commit("Committing modified and added", &repo);
+ tree.flush_fs_events(cx).await;
+ cx.executor().run_until_parked();
+
+ // The worktree detects that the files' git status have changed.
+ tree.read_with(cx, |tree, _cx| {
+ let snapshot = tree.snapshot();
+ assert_eq!(
+ snapshot.status_for_file(project_path.join(F_TXT)),
+ Some(GitFileStatus::Added)
+ );
+ assert_eq!(snapshot.status_for_file(project_path.join(B_TXT)), None);
+ assert_eq!(snapshot.status_for_file(project_path.join(A_TXT)), None);
+ });
+
+ // Modify files in the working copy and perform git operations on other files.
+ git_reset(0, &repo);
+ git_remove_index(Path::new(B_TXT), &repo);
+ git_stash(&mut repo);
+ std::fs::write(work_dir.join(E_TXT), "eeee").unwrap();
+ std::fs::write(work_dir.join(BUILD_FILE), "this should be ignored").unwrap();
+ tree.flush_fs_events(cx).await;
+ cx.executor().run_until_parked();
+
+ // Check that more complex repo changes are tracked
+ tree.read_with(cx, |tree, _cx| {
+ let snapshot = tree.snapshot();
+
+ assert_eq!(snapshot.status_for_file(project_path.join(A_TXT)), None);
+ assert_eq!(
+ snapshot.status_for_file(project_path.join(B_TXT)),
+ Some(GitFileStatus::Added)
+ );
+ assert_eq!(
+ snapshot.status_for_file(project_path.join(E_TXT)),
+ Some(GitFileStatus::Modified)
+ );
+ });
+
+ std::fs::remove_file(work_dir.join(B_TXT)).unwrap();
+ std::fs::remove_dir_all(work_dir.join("c")).unwrap();
+ std::fs::write(
+ work_dir.join(DOTGITIGNORE),
+ [IGNORE_RULE, "f.txt"].join("\n"),
+ )
+ .unwrap();
+
+ git_add(Path::new(DOTGITIGNORE), &repo);
+ git_commit("Committing modified git ignore", &repo);
+
+ tree.flush_fs_events(cx).await;
+ cx.executor().run_until_parked();
+
+ let mut renamed_dir_name = "first_directory/second_directory";
+ const RENAMED_FILE: &'static str = "rf.txt";
+
+ std::fs::create_dir_all(work_dir.join(renamed_dir_name)).unwrap();
+ std::fs::write(
+ work_dir.join(renamed_dir_name).join(RENAMED_FILE),
+ "new-contents",
+ )
+ .unwrap();
+
+ tree.flush_fs_events(cx).await;
+ cx.executor().run_until_parked();
+
+ tree.read_with(cx, |tree, _cx| {
+ let snapshot = tree.snapshot();
+ assert_eq!(
+ snapshot.status_for_file(&project_path.join(renamed_dir_name).join(RENAMED_FILE)),
+ Some(GitFileStatus::Added)
+ );
+ });
+
+ renamed_dir_name = "new_first_directory/second_directory";
+
+ std::fs::rename(
+ work_dir.join("first_directory"),
+ work_dir.join("new_first_directory"),
+ )
+ .unwrap();
+
+ tree.flush_fs_events(cx).await;
+ cx.executor().run_until_parked();
+
+ tree.read_with(cx, |tree, _cx| {
+ let snapshot = tree.snapshot();
+
+ assert_eq!(
+ snapshot.status_for_file(
+ project_path
+ .join(Path::new(renamed_dir_name))
+ .join(RENAMED_FILE)
+ ),
+ Some(GitFileStatus::Added)
+ );
+ });
+}
+
+#[gpui::test]
+async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
+ init_test(cx);
+ let fs = FakeFs::new(cx.background_executor.clone());
+ fs.insert_tree(
+ "/root",
+ json!({
+ ".git": {},
+ "a": {
+ "b": {
+ "c1.txt": "",
+ "c2.txt": "",
+ },
+ "d": {
+ "e1.txt": "",
+ "e2.txt": "",
+ "e3.txt": "",
+ }
+ },
+ "f": {
+ "no-status.txt": ""
+ },
+ "g": {
+ "h1.txt": "",
+ "h2.txt": ""
+ },
+
+ }),
+ )
+ .await;
+
+ fs.set_status_for_repo_via_git_operation(
+ &Path::new("/root/.git"),
+ &[
+ (Path::new("a/b/c1.txt"), GitFileStatus::Added),
+ (Path::new("a/d/e2.txt"), GitFileStatus::Modified),
+ (Path::new("g/h2.txt"), GitFileStatus::Conflict),
+ ],
+ );
+
+ let tree = Worktree::local(
+ build_client(cx),
+ Path::new("/root"),
+ true,
+ fs.clone(),
+ Default::default(),
+ &mut cx.to_async(),
+ )
+ .await
+ .unwrap();
+
+ cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+ .await;
+
+ cx.executor().run_until_parked();
+ let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
+
+ check_propagated_statuses(
+ &snapshot,
+ &[
+ (Path::new(""), Some(GitFileStatus::Conflict)),
+ (Path::new("a"), Some(GitFileStatus::Modified)),
+ (Path::new("a/b"), Some(GitFileStatus::Added)),
+ (Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)),
+ (Path::new("a/b/c2.txt"), None),
+ (Path::new("a/d"), Some(GitFileStatus::Modified)),
+ (Path::new("a/d/e2.txt"), Some(GitFileStatus::Modified)),
+ (Path::new("f"), None),
+ (Path::new("f/no-status.txt"), None),
+ (Path::new("g"), Some(GitFileStatus::Conflict)),
+ (Path::new("g/h2.txt"), Some(GitFileStatus::Conflict)),
+ ],
+ );
+
+ check_propagated_statuses(
+ &snapshot,
+ &[
+ (Path::new("a/b"), Some(GitFileStatus::Added)),
+ (Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)),
+ (Path::new("a/b/c2.txt"), None),
+ (Path::new("a/d"), Some(GitFileStatus::Modified)),
+ (Path::new("a/d/e1.txt"), None),
+ (Path::new("a/d/e2.txt"), Some(GitFileStatus::Modified)),
+ (Path::new("f"), None),
+ (Path::new("f/no-status.txt"), None),
+ (Path::new("g"), Some(GitFileStatus::Conflict)),
+ ],
+ );
+
+ check_propagated_statuses(
+ &snapshot,
+ &[
+ (Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)),
+ (Path::new("a/b/c2.txt"), None),
+ (Path::new("a/d/e1.txt"), None),
+ (Path::new("a/d/e2.txt"), Some(GitFileStatus::Modified)),
+ (Path::new("f/no-status.txt"), None),
+ ],
+ );
+
+ #[track_caller]
+ fn check_propagated_statuses(
+ snapshot: &Snapshot,
+ expected_statuses: &[(&Path, Option<GitFileStatus>)],
+ ) {
+ let mut entries = expected_statuses
+ .iter()
+ .map(|(path, _)| snapshot.entry_for_path(path).unwrap().clone())
+ .collect::<Vec<_>>();
+ snapshot.propagate_git_statuses(&mut entries);
+ assert_eq!(
+ entries
+ .iter()
+ .map(|e| (e.path.as_ref(), e.git_status))
+ .collect::<Vec<_>>(),
+ expected_statuses
+ );
+ }
+}
+
+fn build_client(cx: &mut TestAppContext) -> Arc<Client> {
+ let http_client = FakeHttpClient::with_404_response();
+ cx.read(|cx| Client::new(http_client, cx))
+}
+
+#[track_caller]
+fn git_init(path: &Path) -> git2::Repository {
+ git2::Repository::init(path).expect("Failed to initialize git repository")
+}
+
+#[track_caller]
+fn git_add<P: AsRef<Path>>(path: P, repo: &git2::Repository) {
+ let path = path.as_ref();
+ let mut index = repo.index().expect("Failed to get index");
+ index.add_path(path).expect("Failed to add a.txt");
+ index.write().expect("Failed to write index");
+}
+
+#[track_caller]
+fn git_remove_index(path: &Path, repo: &git2::Repository) {
+ let mut index = repo.index().expect("Failed to get index");
+ index.remove_path(path).expect("Failed to add a.txt");
+ index.write().expect("Failed to write index");
+}
+
+#[track_caller]
+fn git_commit(msg: &'static str, repo: &git2::Repository) {
+ use git2::Signature;
+
+ let signature = Signature::now("test", "test@zed.dev").unwrap();
+ let oid = repo.index().unwrap().write_tree().unwrap();
+ let tree = repo.find_tree(oid).unwrap();
+ if let Some(head) = repo.head().ok() {
+ let parent_obj = head.peel(git2::ObjectType::Commit).unwrap();
+
+ let parent_commit = parent_obj.as_commit().unwrap();
+
+ repo.commit(
+ Some("HEAD"),
+ &signature,
+ &signature,
+ msg,
+ &tree,
+ &[parent_commit],
+ )
+ .expect("Failed to commit with parent");
+ } else {
+ repo.commit(Some("HEAD"), &signature, &signature, msg, &tree, &[])
+ .expect("Failed to commit");
+ }
+}
+
+#[track_caller]
+fn git_stash(repo: &mut git2::Repository) {
+ use git2::Signature;
+
+ let signature = Signature::now("test", "test@zed.dev").unwrap();
+ repo.stash_save(&signature, "N/A", None)
+ .expect("Failed to stash");
+}
+
+#[track_caller]
+fn git_reset(offset: usize, repo: &git2::Repository) {
+ let head = repo.head().expect("Couldn't get repo head");
+ let object = head.peel(git2::ObjectType::Commit).unwrap();
+ let commit = object.as_commit().unwrap();
+ let new_head = commit
+ .parents()
+ .inspect(|parnet| {
+ parnet.message();
+ })
+ .skip(offset)
+ .next()
+ .expect("Not enough history");
+ repo.reset(&new_head.as_object(), git2::ResetType::Soft, None)
+ .expect("Could not reset");
+}
+
+#[allow(dead_code)]
+#[track_caller]
+fn git_status(repo: &git2::Repository) -> collections::HashMap<String, git2::Status> {
+ repo.statuses(None)
+ .unwrap()
+ .iter()
+ .map(|status| (status.path().unwrap().to_string(), status.status()))
+ .collect()
+}
+
+#[track_caller]
+fn check_worktree_entries(
+ tree: &Worktree,
+ expected_excluded_paths: &[&str],
+ expected_ignored_paths: &[&str],
+ expected_tracked_paths: &[&str],
+) {
+ for path in expected_excluded_paths {
+ let entry = tree.entry_for_path(path);
+ assert!(
+ entry.is_none(),
+ "expected path '{path}' to be excluded, but got entry: {entry:?}",
+ );
+ }
+ for path in expected_ignored_paths {
+ let entry = tree
+ .entry_for_path(path)
+ .unwrap_or_else(|| panic!("Missing entry for expected ignored path '{path}'"));
+ assert!(
+ entry.is_ignored,
+ "expected path '{path}' to be ignored, but got entry: {entry:?}",
+ );
+ }
+ for path in expected_tracked_paths {
+ let entry = tree
+ .entry_for_path(path)
+ .unwrap_or_else(|| panic!("Missing entry for expected tracked path '{path}'"));
+ assert!(
+ !entry.is_ignored,
+ "expected path '{path}' to be tracked, but got entry: {entry:?}",
+ );
+ }
+}
+
+fn init_test(cx: &mut gpui::TestAppContext) {
+ cx.update(|cx| {
+ let settings_store = SettingsStore::test(cx);
+ cx.set_global(settings_store);
+ Project::init_settings(cx);
+ });
+}
@@ -1732,7 +1732,7 @@ mod tests {
use super::*;
use gpui::{AnyWindowHandle, TestAppContext, ViewHandle, WindowHandle};
use pretty_assertions::assert_eq;
- use project::FakeFs;
+ use project::{project_settings::ProjectSettings, FakeFs};
use serde_json::json;
use settings::SettingsStore;
use std::{
@@ -1832,6 +1832,123 @@ mod tests {
);
}
+ #[gpui::test]
+ async fn test_exclusions_in_visible_list(cx: &mut gpui::TestAppContext) {
+ init_test(cx);
+ cx.update(|cx| {
+ cx.update_global::<SettingsStore, _, _>(|store, cx| {
+ store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
+ project_settings.file_scan_exclusions =
+ Some(vec!["**/.git".to_string(), "**/4/**".to_string()]);
+ });
+ });
+ });
+
+ let fs = FakeFs::new(cx.background());
+ fs.insert_tree(
+ "/root1",
+ json!({
+ ".dockerignore": "",
+ ".git": {
+ "HEAD": "",
+ },
+ "a": {
+ "0": { "q": "", "r": "", "s": "" },
+ "1": { "t": "", "u": "" },
+ "2": { "v": "", "w": "", "x": "", "y": "" },
+ },
+ "b": {
+ "3": { "Q": "" },
+ "4": { "R": "", "S": "", "T": "", "U": "" },
+ },
+ "C": {
+ "5": {},
+ "6": { "V": "", "W": "" },
+ "7": { "X": "" },
+ "8": { "Y": {}, "Z": "" }
+ }
+ }),
+ )
+ .await;
+ fs.insert_tree(
+ "/root2",
+ json!({
+ "d": {
+ "4": ""
+ },
+ "e": {}
+ }),
+ )
+ .await;
+
+ let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
+ let workspace = cx
+ .add_window(|cx| Workspace::test_new(project.clone(), cx))
+ .root(cx);
+ let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..50, cx),
+ &[
+ "v root1",
+ " > a",
+ " > b",
+ " > C",
+ " .dockerignore",
+ "v root2",
+ " > d",
+ " > e",
+ ]
+ );
+
+ toggle_expand_dir(&panel, "root1/b", cx);
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..50, cx),
+ &[
+ "v root1",
+ " > a",
+ " v b <== selected",
+ " > 3",
+ " > C",
+ " .dockerignore",
+ "v root2",
+ " > d",
+ " > e",
+ ]
+ );
+
+ toggle_expand_dir(&panel, "root2/d", cx);
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..50, cx),
+ &[
+ "v root1",
+ " > a",
+ " v b",
+ " > 3",
+ " > C",
+ " .dockerignore",
+ "v root2",
+ " v d <== selected",
+ " > e",
+ ]
+ );
+
+ toggle_expand_dir(&panel, "root2/e", cx);
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..50, cx),
+ &[
+ "v root1",
+ " > a",
+ " v b",
+ " > 3",
+ " > C",
+ " .dockerignore",
+ "v root2",
+ " v d",
+ " v e <== selected",
+ ]
+ );
+ }
+
#[gpui::test(iterations = 30)]
async fn test_editing_files(cx: &mut gpui::TestAppContext) {
init_test(cx);
@@ -2929,6 +3046,12 @@ mod tests {
workspace::init_settings(cx);
client::init_settings(cx);
Project::init_settings(cx);
+
+ cx.update_global::<SettingsStore, _, _>(|store, cx| {
+ store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
+ project_settings.file_scan_exclusions = Some(Vec::new());
+ });
+ });
});
}
@@ -9,7 +9,6 @@ path = "src/project_panel.rs"
doctest = false
[dependencies]
-context_menu = { path = "../context_menu" }
collections = { path = "../collections" }
db = { path = "../db2", package = "db2" }
editor = { path = "../editor2", package = "editor2" }
@@ -1,6 +1,6 @@
pub mod file_associations;
mod project_panel_settings;
-use settings::Settings;
+use settings::{Settings, SettingsStore};
use db::kvp::KEY_VALUE_STORE;
use editor::{scroll::autoscroll::Autoscroll, Cancel, Editor};
@@ -9,9 +9,9 @@ use file_associations::FileAssociations;
use anyhow::{anyhow, Result};
use gpui::{
actions, div, px, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext,
- ClipboardItem, Component, Div, EventEmitter, FocusHandle, Focusable, FocusableView,
- InteractiveComponent, Model, MouseButton, ParentComponent, Pixels, Point, PromptLevel, Render,
- Stateful, StatefulInteractiveComponent, Styled, Task, UniformListScrollHandle, View,
+ ClipboardItem, Div, EventEmitter, FocusHandle, Focusable, FocusableView, InteractiveElement,
+ Model, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render,
+ RenderOnce, Stateful, StatefulInteractiveElement, Styled, Task, UniformListScrollHandle, View,
ViewContext, VisualContext as _, WeakView, WindowContext,
};
use menu::{Confirm, SelectNext, SelectPrev};
@@ -34,7 +34,7 @@ use ui::{h_stack, v_stack, IconElement, Label};
use unicase::UniCase;
use util::{maybe, ResultExt, TryFutureExt};
use workspace::{
- dock::{DockPosition, PanelEvent},
+ dock::{DockPosition, Panel, PanelEvent},
Workspace,
};
@@ -148,7 +148,6 @@ pub enum Event {
SplitEntry {
entry_id: ProjectEntryId,
},
- DockPositionChanged,
Focus,
NewSearchInDirectory {
dir_entry: Entry,
@@ -200,10 +199,11 @@ impl ProjectPanel {
let filename_editor = cx.build_view(|cx| Editor::single_line(cx));
cx.subscribe(&filename_editor, |this, _, event, cx| match event {
- editor::Event::BufferEdited | editor::Event::SelectionsChanged { .. } => {
+ editor::EditorEvent::BufferEdited
+ | editor::EditorEvent::SelectionsChanged { .. } => {
this.autoscroll(cx);
}
- editor::Event::Blurred => {
+ editor::EditorEvent::Blurred => {
if this
.edit_state
.as_ref()
@@ -244,16 +244,16 @@ impl ProjectPanel {
this.update_visible_entries(None, cx);
// Update the dock position when the setting changes.
- // todo!()
- // 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();
+ let mut old_dock_position = this.position(cx);
+ ProjectPanelSettings::register(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(PanelEvent::ChangePosition);
+ }
+ })
+ .detach();
this
});
@@ -1339,7 +1339,7 @@ impl ProjectPanel {
editor: Option<&View<Editor>>,
padding: Pixels,
cx: &mut ViewContext<Self>,
- ) -> Div<Self> {
+ ) -> Div {
let show_editor = details.is_editing && !details.is_processing;
let theme = cx.theme();
@@ -1378,7 +1378,7 @@ impl ProjectPanel {
details: EntryDetails,
// dragged_entry_destination: &mut Option<Arc<Path>>,
cx: &mut ViewContext<Self>,
- ) -> Stateful<Self, Div<Self>> {
+ ) -> Stateful<Div> {
let kind = details.kind;
let settings = ProjectPanelSettings::get_global(cx);
const INDENT_SIZE: Pixels = px(16.0);
@@ -1396,7 +1396,7 @@ impl ProjectPanel {
this.bg(cx.theme().colors().element_selected)
})
.hover(|style| style.bg(cx.theme().colors().element_hover))
- .on_click(move |this, event, cx| {
+ .on_click(cx.listener(move |this, event: &gpui::ClickEvent, cx| {
if !show_editor {
if kind.is_dir() {
this.toggle_expanded(entry_id, cx);
@@ -1408,10 +1408,13 @@ impl ProjectPanel {
}
}
}
- })
- .on_mouse_down(MouseButton::Right, move |this, event, cx| {
- this.deploy_context_menu(event.position, entry_id, cx);
- })
+ }))
+ .on_mouse_down(
+ MouseButton::Right,
+ cx.listener(move |this, event: &MouseDownEvent, cx| {
+ this.deploy_context_menu(event.position, entry_id, cx);
+ }),
+ )
// .on_drop::<ProjectEntryId>(|this, event, cx| {
// this.move_entry(
// *dragged_entry,
@@ -1424,9 +1427,9 @@ impl ProjectPanel {
}
impl Render for ProjectPanel {
- type Element = Focusable<Self, Stateful<Self, Div<Self>>>;
+ type Element = Focusable<Stateful<Div>>;
- fn render(&mut self, _cx: &mut gpui::ViewContext<Self>) -> Self::Element {
+ fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element {
let has_worktree = self.visible_entries.len() != 0;
if has_worktree {
@@ -1434,40 +1437,43 @@ impl Render for ProjectPanel {
.id("project-panel")
.size_full()
.key_context("ProjectPanel")
- .on_action(Self::select_next)
- .on_action(Self::select_prev)
- .on_action(Self::expand_selected_entry)
- .on_action(Self::collapse_selected_entry)
- .on_action(Self::collapse_all_entries)
- .on_action(Self::new_file)
- .on_action(Self::new_directory)
- .on_action(Self::rename)
- .on_action(Self::delete)
- .on_action(Self::confirm)
- .on_action(Self::open_file)
- .on_action(Self::cancel)
- .on_action(Self::cut)
- .on_action(Self::copy)
- .on_action(Self::copy_path)
- .on_action(Self::copy_relative_path)
- .on_action(Self::paste)
- .on_action(Self::reveal_in_finder)
- .on_action(Self::open_in_terminal)
- .on_action(Self::new_search_in_directory)
+ .on_action(cx.listener(Self::select_next))
+ .on_action(cx.listener(Self::select_prev))
+ .on_action(cx.listener(Self::expand_selected_entry))
+ .on_action(cx.listener(Self::collapse_selected_entry))
+ .on_action(cx.listener(Self::collapse_all_entries))
+ .on_action(cx.listener(Self::new_file))
+ .on_action(cx.listener(Self::new_directory))
+ .on_action(cx.listener(Self::rename))
+ .on_action(cx.listener(Self::delete))
+ .on_action(cx.listener(Self::confirm))
+ .on_action(cx.listener(Self::open_file))
+ .on_action(cx.listener(Self::cancel))
+ .on_action(cx.listener(Self::cut))
+ .on_action(cx.listener(Self::copy))
+ .on_action(cx.listener(Self::copy_path))
+ .on_action(cx.listener(Self::copy_relative_path))
+ .on_action(cx.listener(Self::paste))
+ .on_action(cx.listener(Self::reveal_in_finder))
+ .on_action(cx.listener(Self::open_in_terminal))
+ .on_action(cx.listener(Self::new_search_in_directory))
.track_focus(&self.focus_handle)
.child(
uniform_list(
+ cx.view().clone(),
"entries",
self.visible_entries
.iter()
.map(|(_, worktree_entries)| worktree_entries.len())
.sum(),
- |this: &mut Self, range, cx| {
- let mut items = Vec::new();
- this.for_each_visible_entry(range, cx, |id, details, cx| {
- items.push(this.render_entry(id, details, cx));
- });
- items
+ {
+ |this, range, cx| {
+ let mut items = Vec::new();
+ this.for_each_visible_entry(range, cx, |id, details, cx| {
+ items.push(this.render_entry(id, details, cx));
+ });
+ items
+ }
},
)
.size_full()
@@ -1485,7 +1491,7 @@ impl EventEmitter<Event> for ProjectPanel {}
impl EventEmitter<PanelEvent> for ProjectPanel {}
-impl workspace::dock::Panel for ProjectPanel {
+impl Panel for ProjectPanel {
fn position(&self, cx: &WindowContext) -> DockPosition {
match ProjectPanelSettings::get_global(cx).dock {
ProjectPanelDockPosition::Left => DockPosition::Left,
@@ -1571,7 +1577,7 @@ mod tests {
use super::*;
use gpui::{TestAppContext, View, VisualTestContext, WindowHandle};
use pretty_assertions::assert_eq;
- use project::FakeFs;
+ use project::{project_settings::ProjectSettings, FakeFs};
use serde_json::json;
use settings::SettingsStore;
use std::{
@@ -1672,6 +1678,124 @@ mod tests {
);
}
+ #[gpui::test]
+ async fn test_exclusions_in_visible_list(cx: &mut gpui::TestAppContext) {
+ init_test(cx);
+ cx.update(|cx| {
+ cx.update_global::<SettingsStore, _>(|store, cx| {
+ store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
+ project_settings.file_scan_exclusions =
+ Some(vec!["**/.git".to_string(), "**/4/**".to_string()]);
+ });
+ });
+ });
+
+ let fs = FakeFs::new(cx.background_executor.clone());
+ fs.insert_tree(
+ "/root1",
+ json!({
+ ".dockerignore": "",
+ ".git": {
+ "HEAD": "",
+ },
+ "a": {
+ "0": { "q": "", "r": "", "s": "" },
+ "1": { "t": "", "u": "" },
+ "2": { "v": "", "w": "", "x": "", "y": "" },
+ },
+ "b": {
+ "3": { "Q": "" },
+ "4": { "R": "", "S": "", "T": "", "U": "" },
+ },
+ "C": {
+ "5": {},
+ "6": { "V": "", "W": "" },
+ "7": { "X": "" },
+ "8": { "Y": {}, "Z": "" }
+ }
+ }),
+ )
+ .await;
+ fs.insert_tree(
+ "/root2",
+ json!({
+ "d": {
+ "4": ""
+ },
+ "e": {}
+ }),
+ )
+ .await;
+
+ let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
+ let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+ let cx = &mut VisualTestContext::from_window(*workspace, cx);
+ let panel = workspace
+ .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx))
+ .unwrap();
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..50, cx),
+ &[
+ "v root1",
+ " > a",
+ " > b",
+ " > C",
+ " .dockerignore",
+ "v root2",
+ " > d",
+ " > e",
+ ]
+ );
+
+ toggle_expand_dir(&panel, "root1/b", cx);
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..50, cx),
+ &[
+ "v root1",
+ " > a",
+ " v b <== selected",
+ " > 3",
+ " > C",
+ " .dockerignore",
+ "v root2",
+ " > d",
+ " > e",
+ ]
+ );
+
+ toggle_expand_dir(&panel, "root2/d", cx);
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..50, cx),
+ &[
+ "v root1",
+ " > a",
+ " v b",
+ " > 3",
+ " > C",
+ " .dockerignore",
+ "v root2",
+ " v d <== selected",
+ " > e",
+ ]
+ );
+
+ toggle_expand_dir(&panel, "root2/e", cx);
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..50, cx),
+ &[
+ "v root1",
+ " > a",
+ " v b",
+ " > 3",
+ " > C",
+ " .dockerignore",
+ "v root2",
+ " v d",
+ " v e <== selected",
+ ]
+ );
+ }
+
#[gpui::test(iterations = 30)]
async fn test_editing_files(cx: &mut gpui::TestAppContext) {
init_test(cx);
@@ -2792,6 +2916,12 @@ mod tests {
workspace::init_settings(cx);
client::init_settings(cx);
Project::init_settings(cx);
+
+ cx.update_global::<SettingsStore, _>(|store, cx| {
+ store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
+ project_settings.file_scan_exclusions = Some(Vec::new());
+ });
+ });
});
}
@@ -56,12 +56,12 @@ pub struct Mention {
}
impl RichText {
- pub fn element<V: 'static>(
+ pub fn element(
&self,
// syntax: Arc<SyntaxTheme>,
// style: RichTextStyle,
// cx: &mut ViewContext<V>,
- ) -> AnyElement<V> {
+ ) -> AnyElement {
todo!();
// let mut region_id = 0;
@@ -884,6 +884,7 @@ message SearchProject {
bool case_sensitive = 5;
string files_to_include = 6;
string files_to_exclude = 7;
+ bool include_ignored = 8;
}
message SearchProjectResponse {
@@ -884,6 +884,7 @@ message SearchProject {
bool case_sensitive = 5;
string files_to_include = 6;
string files_to_exclude = 7;
+ bool include_ignored = 8;
}
message SearchProjectResponse {
@@ -805,6 +805,7 @@ impl BufferSearchBar {
query,
self.search_options.contains(SearchOptions::WHOLE_WORD),
self.search_options.contains(SearchOptions::CASE_SENSITIVE),
+ false,
Vec::new(),
Vec::new(),
) {
@@ -820,6 +821,7 @@ impl BufferSearchBar {
query,
self.search_options.contains(SearchOptions::WHOLE_WORD),
self.search_options.contains(SearchOptions::CASE_SENSITIVE),
+ false,
Vec::new(),
Vec::new(),
) {
@@ -4,7 +4,7 @@ use crate::{
search_bar::{render_nav_button, render_option_button_icon, render_search_mode_button},
ActivateRegexMode, ActivateSemanticMode, ActivateTextMode, CycleMode, NextHistoryQuery,
PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions, SelectNextMatch, SelectPrevMatch,
- ToggleCaseSensitive, ToggleReplace, ToggleWholeWord,
+ ToggleCaseSensitive, ToggleIncludeIgnored, ToggleReplace, ToggleWholeWord,
};
use anyhow::{Context, Result};
use collections::HashMap;
@@ -85,6 +85,7 @@ pub fn init(cx: &mut AppContext) {
cx.capture_action(ProjectSearchView::replace_next);
add_toggle_option_action::<ToggleCaseSensitive>(SearchOptions::CASE_SENSITIVE, cx);
add_toggle_option_action::<ToggleWholeWord>(SearchOptions::WHOLE_WORD, cx);
+ add_toggle_option_action::<ToggleIncludeIgnored>(SearchOptions::INCLUDE_IGNORED, cx);
add_toggle_filters_action::<ToggleFilters>(cx);
}
@@ -1192,6 +1193,7 @@ impl ProjectSearchView {
text,
self.search_options.contains(SearchOptions::WHOLE_WORD),
self.search_options.contains(SearchOptions::CASE_SENSITIVE),
+ self.search_options.contains(SearchOptions::INCLUDE_IGNORED),
included_files,
excluded_files,
) {
@@ -1210,6 +1212,7 @@ impl ProjectSearchView {
text,
self.search_options.contains(SearchOptions::WHOLE_WORD),
self.search_options.contains(SearchOptions::CASE_SENSITIVE),
+ self.search_options.contains(SearchOptions::INCLUDE_IGNORED),
included_files,
excluded_files,
) {
@@ -1764,6 +1767,17 @@ impl View for ProjectSearchBar {
render_option_button_icon("icons/word_search.svg", SearchOptions::WHOLE_WORD, cx)
});
+ let mut include_ignored = is_semantic_disabled.then(|| {
+ render_option_button_icon(
+ // TODO proper icon
+ "icons/case_insensitive.svg",
+ SearchOptions::INCLUDE_IGNORED,
+ cx,
+ )
+ });
+ // TODO not implemented yet
+ let _ = include_ignored.take();
+
let search_button_for_mode = |mode, side, cx: &mut ViewContext<ProjectSearchBar>| {
let is_active = if let Some(search) = self.active_project_search.as_ref() {
let search = search.read(cx);
@@ -1879,7 +1893,15 @@ impl View for ProjectSearchBar {
.with_children(search.filters_enabled.then(|| {
Flex::row()
.with_child(
- ChildView::new(&search.included_files_editor, cx)
+ Flex::row()
+ .with_child(
+ ChildView::new(&search.included_files_editor, cx)
+ .contained()
+ .constrained()
+ .with_height(theme.search.search_bar_row_height)
+ .flex(1., true),
+ )
+ .with_children(include_ignored)
.contained()
.with_style(include_container_style)
.constrained()
@@ -29,6 +29,7 @@ actions!(
CycleMode,
ToggleWholeWord,
ToggleCaseSensitive,
+ ToggleIncludeIgnored,
ToggleReplace,
SelectNextMatch,
SelectPrevMatch,
@@ -49,31 +50,35 @@ bitflags! {
const NONE = 0b000;
const WHOLE_WORD = 0b001;
const CASE_SENSITIVE = 0b010;
+ const INCLUDE_IGNORED = 0b100;
}
}
impl SearchOptions {
pub fn label(&self) -> &'static str {
match *self {
- SearchOptions::WHOLE_WORD => "Match Whole Word",
- SearchOptions::CASE_SENSITIVE => "Match Case",
- _ => panic!("{:?} is not a named SearchOption", self),
+ Self::WHOLE_WORD => "Match Whole Word",
+ Self::CASE_SENSITIVE => "Match Case",
+ Self::INCLUDE_IGNORED => "Include Ignored",
+ _ => panic!("{self:?} is not a named SearchOption"),
}
}
pub fn icon(&self) -> &'static str {
match *self {
- SearchOptions::WHOLE_WORD => "icons/word_search.svg",
- SearchOptions::CASE_SENSITIVE => "icons/case_insensitive.svg",
- _ => panic!("{:?} is not a named SearchOption", self),
+ Self::WHOLE_WORD => "icons/word_search.svg",
+ Self::CASE_SENSITIVE => "icons/case_insensitive.svg",
+ Self::INCLUDE_IGNORED => "icons/case_insensitive.svg",
+ _ => panic!("{self:?} is not a named SearchOption"),
}
}
pub fn to_toggle_action(&self) -> Box<dyn Action> {
match *self {
- SearchOptions::WHOLE_WORD => Box::new(ToggleWholeWord),
- SearchOptions::CASE_SENSITIVE => Box::new(ToggleCaseSensitive),
- _ => panic!("{:?} is not a named SearchOption", self),
+ Self::WHOLE_WORD => Box::new(ToggleWholeWord),
+ Self::CASE_SENSITIVE => Box::new(ToggleCaseSensitive),
+ Self::INCLUDE_IGNORED => Box::new(ToggleIncludeIgnored),
+ _ => panic!("{self:?} is not a named SearchOption"),
}
}
@@ -85,6 +90,7 @@ impl SearchOptions {
let mut options = SearchOptions::NONE;
options.set(SearchOptions::WHOLE_WORD, query.whole_word());
options.set(SearchOptions::CASE_SENSITIVE, query.case_sensitive());
+ options.set(SearchOptions::INCLUDE_IGNORED, query.include_ignored());
options
}
@@ -0,0 +1,40 @@
+[package]
+name = "search2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/search.rs"
+doctest = false
+
+[dependencies]
+bitflags = "1"
+collections = { path = "../collections" }
+editor = { package = "editor2", path = "../editor2" }
+gpui = { package = "gpui2", path = "../gpui2" }
+language = { package = "language2", path = "../language2" }
+menu = { package = "menu2", path = "../menu2" }
+project = { package = "project2", path = "../project2" }
+settings = { package = "settings2", path = "../settings2" }
+theme = { package = "theme2", path = "../theme2" }
+util = { path = "../util" }
+ui = {package = "ui2", path = "../ui2"}
+workspace = { package = "workspace2", path = "../workspace2" }
+#semantic_index = { path = "../semantic_index" }
+anyhow.workspace = true
+futures.workspace = true
+log.workspace = true
+postage.workspace = true
+serde.workspace = true
+serde_derive.workspace = true
+smallvec.workspace = true
+smol.workspace = true
+serde_json.workspace = true
+[dev-dependencies]
+client = { package = "client2", path = "../client2", features = ["test-support"] }
+editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
+gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
+
+workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
+unindent.workspace = true
@@ -0,0 +1,1704 @@
+use crate::{
+ history::SearchHistory,
+ mode::{next_mode, SearchMode},
+ search_bar::{render_nav_button, render_search_mode_button},
+ ActivateRegexMode, ActivateTextMode, CycleMode, NextHistoryQuery, PreviousHistoryQuery,
+ ReplaceAll, ReplaceNext, SearchOptions, SelectAllMatches, SelectNextMatch, SelectPrevMatch,
+ ToggleCaseSensitive, ToggleReplace, ToggleWholeWord,
+};
+use collections::HashMap;
+use editor::Editor;
+use futures::channel::oneshot;
+use gpui::{
+ actions, div, red, Action, AppContext, Div, EventEmitter, InteractiveElement as _,
+ ParentElement as _, Render, RenderOnce, Styled, Subscription, Task, View, ViewContext,
+ VisualContext as _, WindowContext,
+};
+use project::search::SearchQuery;
+use serde::Deserialize;
+use std::{any::Any, sync::Arc};
+
+use ui::{h_stack, ButtonGroup, Icon, IconButton, IconElement};
+use util::ResultExt;
+use workspace::{
+ item::ItemHandle,
+ searchable::{Direction, SearchEvent, SearchableItemHandle, WeakSearchableItemHandle},
+ ToolbarItemLocation, ToolbarItemView, Workspace,
+};
+
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
+pub struct Deploy {
+ pub focus: bool,
+}
+
+actions!(Dismiss, FocusEditor);
+
+pub enum Event {
+ UpdateLocation,
+}
+
+pub fn init(cx: &mut AppContext) {
+ cx.observe_new_views(|workspace: &mut Workspace, _| BufferSearchBar::register(workspace))
+ .detach();
+}
+
+pub struct BufferSearchBar {
+ query_editor: View<Editor>,
+ replacement_editor: View<Editor>,
+ active_searchable_item: Option<Box<dyn SearchableItemHandle>>,
+ active_match_index: Option<usize>,
+ active_searchable_item_subscription: Option<Subscription>,
+ active_search: Option<Arc<SearchQuery>>,
+ searchable_items_with_matches:
+ HashMap<Box<dyn WeakSearchableItemHandle>, Vec<Box<dyn Any + Send>>>,
+ pending_search: Option<Task<()>>,
+ search_options: SearchOptions,
+ default_options: SearchOptions,
+ query_contains_error: bool,
+ dismissed: bool,
+ search_history: SearchHistory,
+ current_mode: SearchMode,
+ replace_enabled: bool,
+}
+
+impl EventEmitter<Event> for BufferSearchBar {}
+impl EventEmitter<workspace::ToolbarItemEvent> for BufferSearchBar {}
+impl Render for BufferSearchBar {
+ type Element = Div;
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+ // let query_container_style = if self.query_contains_error {
+ // theme.search.invalid_editor
+ // } else {
+ // theme.search.editor.input.container
+ // };
+ if self.dismissed {
+ return div();
+ }
+ let supported_options = self.supported_options();
+
+ let previous_query_keystrokes = cx
+ .bindings_for_action(&PreviousHistoryQuery {})
+ .into_iter()
+ .next()
+ .map(|binding| {
+ binding
+ .keystrokes()
+ .iter()
+ .map(|k| k.to_string())
+ .collect::<Vec<_>>()
+ });
+ let next_query_keystrokes = cx
+ .bindings_for_action(&NextHistoryQuery {})
+ .into_iter()
+ .next()
+ .map(|binding| {
+ binding
+ .keystrokes()
+ .iter()
+ .map(|k| k.to_string())
+ .collect::<Vec<_>>()
+ });
+ let new_placeholder_text = match (previous_query_keystrokes, next_query_keystrokes) {
+ (Some(previous_query_keystrokes), Some(next_query_keystrokes)) => {
+ format!(
+ "Search ({}/{} for previous/next query)",
+ previous_query_keystrokes.join(" "),
+ next_query_keystrokes.join(" ")
+ )
+ }
+ (None, Some(next_query_keystrokes)) => {
+ format!(
+ "Search ({} for next query)",
+ next_query_keystrokes.join(" ")
+ )
+ }
+ (Some(previous_query_keystrokes), None) => {
+ format!(
+ "Search ({} for previous query)",
+ previous_query_keystrokes.join(" ")
+ )
+ }
+ (None, None) => String::new(),
+ };
+ let new_placeholder_text = Arc::from(new_placeholder_text);
+ self.query_editor.update(cx, |editor, cx| {
+ editor.set_placeholder_text(new_placeholder_text, cx);
+ });
+ self.replacement_editor.update(cx, |editor, cx| {
+ editor.set_placeholder_text("Replace with...", cx);
+ });
+
+ let search_button_for_mode = |mode| {
+ let is_active = self.current_mode == mode;
+
+ render_search_mode_button(
+ mode,
+ is_active,
+ cx.listener(move |this, _, cx| {
+ this.activate_search_mode(mode, cx);
+ }),
+ )
+ };
+ let search_option_button = |option| {
+ let is_active = self.search_options.contains(option);
+ option.as_button(is_active)
+ };
+ let match_count = self
+ .active_searchable_item
+ .as_ref()
+ .and_then(|searchable_item| {
+ if self.query(cx).is_empty() {
+ return None;
+ }
+ let matches = self
+ .searchable_items_with_matches
+ .get(&searchable_item.downgrade())?;
+ let message = if let Some(match_ix) = self.active_match_index {
+ format!("{}/{}", match_ix + 1, matches.len())
+ } else {
+ "No matches".to_string()
+ };
+
+ Some(ui::Label::new(message))
+ });
+ let nav_button_for_direction = |icon, direction| {
+ render_nav_button(
+ icon,
+ self.active_match_index.is_some(),
+ cx.listener(move |this, _, cx| match direction {
+ Direction::Prev => this.select_prev_match(&Default::default(), cx),
+ Direction::Next => this.select_next_match(&Default::default(), cx),
+ }),
+ )
+ };
+ let should_show_replace_input = self.replace_enabled && supported_options.replacement;
+ let replace_all = should_show_replace_input
+ .then(|| super::render_replace_button(ReplaceAll, ui::Icon::ReplaceAll));
+ let replace_next = should_show_replace_input
+ .then(|| super::render_replace_button(ReplaceNext, ui::Icon::Replace));
+ let in_replace = self.replacement_editor.focus_handle(cx).is_focused(cx);
+
+ h_stack()
+ .key_context("BufferSearchBar")
+ .when(in_replace, |this| {
+ this.key_context("in_replace")
+ .on_action(cx.listener(Self::replace_next))
+ .on_action(cx.listener(Self::replace_all))
+ })
+ .on_action(cx.listener(Self::previous_history_query))
+ .on_action(cx.listener(Self::next_history_query))
+ .w_full()
+ .p_1()
+ .child(
+ div()
+ .flex()
+ .flex_1()
+ .border_1()
+ .border_color(red())
+ .rounded_md()
+ .items_center()
+ .child(IconElement::new(Icon::MagnifyingGlass))
+ .child(self.query_editor.clone())
+ .children(
+ supported_options
+ .case
+ .then(|| search_option_button(SearchOptions::CASE_SENSITIVE)),
+ )
+ .children(
+ supported_options
+ .word
+ .then(|| search_option_button(SearchOptions::WHOLE_WORD)),
+ ),
+ )
+ .child(
+ h_stack()
+ .flex_none()
+ .child(ButtonGroup::new(vec![
+ search_button_for_mode(SearchMode::Text),
+ search_button_for_mode(SearchMode::Regex),
+ ]))
+ .when(supported_options.replacement, |this| {
+ this.child(super::toggle_replace_button(self.replace_enabled))
+ }),
+ )
+ .child(
+ h_stack()
+ .gap_0p5()
+ .flex_1()
+ .when(self.replace_enabled, |this| {
+ this.child(self.replacement_editor.clone())
+ .children(replace_next)
+ .children(replace_all)
+ }),
+ )
+ .child(
+ h_stack()
+ .gap_0p5()
+ .flex_none()
+ .child(self.render_action_button())
+ .children(match_count)
+ .child(nav_button_for_direction(
+ ui::Icon::ChevronLeft,
+ Direction::Prev,
+ ))
+ .child(nav_button_for_direction(
+ ui::Icon::ChevronRight,
+ Direction::Next,
+ )),
+ )
+ }
+}
+
+impl ToolbarItemView for BufferSearchBar {
+ fn set_active_pane_item(
+ &mut self,
+ item: Option<&dyn ItemHandle>,
+ cx: &mut ViewContext<Self>,
+ ) -> ToolbarItemLocation {
+ cx.notify();
+ self.active_searchable_item_subscription.take();
+ self.active_searchable_item.take();
+
+ self.pending_search.take();
+
+ if let Some(searchable_item_handle) =
+ item.and_then(|item| item.to_searchable_item_handle(cx))
+ {
+ let this = cx.view().downgrade();
+
+ searchable_item_handle
+ .subscribe_to_search_events(
+ cx,
+ Box::new(move |search_event, cx| {
+ if let Some(this) = this.upgrade() {
+ this.update(cx, |this, cx| {
+ this.on_active_searchable_item_event(search_event, cx)
+ });
+ }
+ }),
+ )
+ .detach();
+
+ self.active_searchable_item = Some(searchable_item_handle);
+ let _ = self.update_matches(cx);
+ if !self.dismissed {
+ return ToolbarItemLocation::Secondary;
+ }
+ }
+ ToolbarItemLocation::Hidden
+ }
+
+ fn row_count(&self, _: &WindowContext<'_>) -> usize {
+ 1
+ }
+}
+
+impl BufferSearchBar {
+ pub fn register(workspace: &mut Workspace) {
+ workspace.register_action(|workspace, a: &Deploy, cx| {
+ workspace.active_pane().update(cx, |this, cx| {
+ this.toolbar().update(cx, |this, cx| {
+ if let Some(search_bar) = this.item_of_type::<BufferSearchBar>() {
+ search_bar.update(cx, |this, cx| {
+ if this.is_dismissed() {
+ this.show(cx);
+ } else {
+ this.dismiss(&Dismiss, cx);
+ }
+ });
+ return;
+ }
+ let view = cx.build_view(|cx| BufferSearchBar::new(cx));
+ this.add_item(view.clone(), cx);
+ view.update(cx, |this, cx| this.deploy(a, cx));
+ cx.notify();
+ })
+ });
+ });
+ fn register_action<A: Action>(
+ workspace: &mut Workspace,
+ update: fn(&mut BufferSearchBar, &A, &mut ViewContext<BufferSearchBar>),
+ ) {
+ workspace.register_action(move |workspace, action: &A, cx| {
+ workspace.active_pane().update(cx, move |this, cx| {
+ this.toolbar().update(cx, move |this, cx| {
+ if let Some(search_bar) = this.item_of_type::<BufferSearchBar>() {
+ search_bar.update(cx, move |this, cx| update(this, action, cx));
+ cx.notify();
+ }
+ })
+ });
+ });
+ }
+
+ register_action(workspace, |this, action: &ToggleCaseSensitive, cx| {
+ if this.supported_options().case {
+ this.toggle_case_sensitive(action, cx);
+ }
+ });
+ register_action(workspace, |this, action: &ToggleWholeWord, cx| {
+ if this.supported_options().word {
+ this.toggle_whole_word(action, cx);
+ }
+ });
+ register_action(workspace, |this, action: &ToggleReplace, cx| {
+ if this.supported_options().replacement {
+ this.toggle_replace(action, cx);
+ }
+ });
+ register_action(workspace, |this, _: &ActivateRegexMode, cx| {
+ if this.supported_options().regex {
+ this.activate_search_mode(SearchMode::Regex, cx);
+ }
+ });
+ register_action(workspace, |this, _: &ActivateTextMode, cx| {
+ this.activate_search_mode(SearchMode::Text, cx);
+ });
+ register_action(workspace, |this, action: &CycleMode, cx| {
+ if this.supported_options().regex {
+ // If regex is not supported then search has just one mode (text) - in that case there's no point in supporting
+ // cycling.
+ this.cycle_mode(action, cx)
+ }
+ });
+ register_action(workspace, |this, action: &SelectNextMatch, cx| {
+ this.select_next_match(action, cx);
+ });
+ register_action(workspace, |this, action: &SelectPrevMatch, cx| {
+ this.select_prev_match(action, cx);
+ });
+ register_action(workspace, |this, action: &SelectAllMatches, cx| {
+ this.select_all_matches(action, cx);
+ });
+ register_action(workspace, |this, _: &editor::Cancel, cx| {
+ if !this.dismissed {
+ this.dismiss(&Dismiss, cx);
+ }
+ });
+ }
+ pub fn new(cx: &mut ViewContext<Self>) -> Self {
+ let query_editor = cx.build_view(|cx| Editor::single_line(cx));
+ cx.subscribe(&query_editor, Self::on_query_editor_event)
+ .detach();
+ let replacement_editor = cx.build_view(|cx| Editor::single_line(cx));
+ cx.subscribe(&replacement_editor, Self::on_query_editor_event)
+ .detach();
+ Self {
+ query_editor,
+ replacement_editor,
+ active_searchable_item: None,
+ active_searchable_item_subscription: None,
+ active_match_index: None,
+ searchable_items_with_matches: Default::default(),
+ default_options: SearchOptions::NONE,
+ search_options: SearchOptions::NONE,
+ pending_search: None,
+ query_contains_error: false,
+ dismissed: true,
+ search_history: SearchHistory::default(),
+ current_mode: SearchMode::default(),
+ active_search: None,
+ replace_enabled: false,
+ }
+ }
+
+ pub fn is_dismissed(&self) -> bool {
+ self.dismissed
+ }
+
+ pub fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext<Self>) {
+ self.dismissed = true;
+ for searchable_item in self.searchable_items_with_matches.keys() {
+ if let Some(searchable_item) =
+ WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
+ {
+ searchable_item.clear_matches(cx);
+ }
+ }
+ if let Some(active_editor) = self.active_searchable_item.as_ref() {
+ let handle = active_editor.focus_handle(cx);
+ cx.focus(&handle);
+ }
+ cx.emit(Event::UpdateLocation);
+ cx.notify();
+ }
+
+ pub fn deploy(&mut self, deploy: &Deploy, cx: &mut ViewContext<Self>) -> bool {
+ if self.show(cx) {
+ self.search_suggested(cx);
+ if deploy.focus {
+ self.select_query(cx);
+ let handle = cx.focus_handle();
+ cx.focus(&handle);
+ }
+ return true;
+ }
+
+ false
+ }
+
+ pub fn show(&mut self, cx: &mut ViewContext<Self>) -> bool {
+ if self.active_searchable_item.is_none() {
+ return false;
+ }
+ self.dismissed = false;
+ cx.notify();
+ cx.emit(Event::UpdateLocation);
+ true
+ }
+
+ fn supported_options(&self) -> workspace::searchable::SearchOptions {
+ self.active_searchable_item
+ .as_deref()
+ .map(SearchableItemHandle::supported_options)
+ .unwrap_or_default()
+ }
+ pub fn search_suggested(&mut self, cx: &mut ViewContext<Self>) {
+ let search = self
+ .query_suggestion(cx)
+ .map(|suggestion| self.search(&suggestion, Some(self.default_options), cx));
+
+ if let Some(search) = search {
+ cx.spawn(|this, mut cx| async move {
+ search.await?;
+ this.update(&mut cx, |this, cx| this.activate_current_match(cx))
+ })
+ .detach_and_log_err(cx);
+ }
+ }
+
+ pub fn activate_current_match(&mut self, cx: &mut ViewContext<Self>) {
+ if let Some(match_ix) = self.active_match_index {
+ if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
+ if let Some(matches) = self
+ .searchable_items_with_matches
+ .get(&active_searchable_item.downgrade())
+ {
+ active_searchable_item.activate_match(match_ix, matches, cx)
+ }
+ }
+ }
+ }
+
+ pub fn select_query(&mut self, cx: &mut ViewContext<Self>) {
+ self.query_editor.update(cx, |query_editor, cx| {
+ query_editor.select_all(&Default::default(), cx);
+ });
+ }
+
+ pub fn query(&self, cx: &WindowContext) -> String {
+ self.query_editor.read(cx).text(cx)
+ }
+ pub fn replacement(&self, cx: &WindowContext) -> String {
+ self.replacement_editor.read(cx).text(cx)
+ }
+ pub fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> Option<String> {
+ self.active_searchable_item
+ .as_ref()
+ .map(|searchable_item| searchable_item.query_suggestion(cx))
+ .filter(|suggestion| !suggestion.is_empty())
+ }
+
+ pub fn set_replacement(&mut self, replacement: Option<&str>, cx: &mut ViewContext<Self>) {
+ if replacement.is_none() {
+ self.replace_enabled = false;
+ return;
+ }
+ self.replace_enabled = true;
+ self.replacement_editor
+ .update(cx, |replacement_editor, cx| {
+ replacement_editor
+ .buffer()
+ .update(cx, |replacement_buffer, cx| {
+ let len = replacement_buffer.len(cx);
+ replacement_buffer.edit([(0..len, replacement.unwrap())], None, cx);
+ });
+ });
+ }
+
+ pub fn search(
+ &mut self,
+ query: &str,
+ options: Option<SearchOptions>,
+ cx: &mut ViewContext<Self>,
+ ) -> oneshot::Receiver<()> {
+ let options = options.unwrap_or(self.default_options);
+ if query != self.query(cx) || self.search_options != options {
+ self.query_editor.update(cx, |query_editor, cx| {
+ query_editor.buffer().update(cx, |query_buffer, cx| {
+ let len = query_buffer.len(cx);
+ query_buffer.edit([(0..len, query)], None, cx);
+ });
+ });
+ self.search_options = options;
+ self.query_contains_error = false;
+ self.clear_matches(cx);
+ cx.notify();
+ }
+ self.update_matches(cx)
+ }
+
+ fn render_action_button(&self) -> impl RenderOnce {
+ // let tooltip_style = theme.tooltip.clone();
+
+ // let style = theme.search.action_button.clone();
+
+ IconButton::new(0, ui::Icon::SelectAll)
+ .on_click(|_, cx| cx.dispatch_action(Box::new(SelectAllMatches)))
+ }
+
+ pub fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext<Self>) {
+ assert_ne!(
+ mode,
+ SearchMode::Semantic,
+ "Semantic search is not supported in buffer search"
+ );
+ if mode == self.current_mode {
+ return;
+ }
+ self.current_mode = mode;
+ let _ = self.update_matches(cx);
+ cx.notify();
+ }
+
+ pub fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext<Self>) {
+ if let Some(active_editor) = self.active_searchable_item.as_ref() {
+ let handle = active_editor.focus_handle(cx);
+ cx.focus(&handle);
+ }
+ }
+
+ fn toggle_search_option(&mut self, search_option: SearchOptions, cx: &mut ViewContext<Self>) {
+ self.search_options.toggle(search_option);
+ self.default_options = self.search_options;
+ let _ = self.update_matches(cx);
+ cx.notify();
+ }
+
+ pub fn set_search_options(
+ &mut self,
+ search_options: SearchOptions,
+ cx: &mut ViewContext<Self>,
+ ) {
+ self.search_options = search_options;
+ cx.notify();
+ }
+
+ fn select_next_match(&mut self, _: &SelectNextMatch, cx: &mut ViewContext<Self>) {
+ self.select_match(Direction::Next, 1, cx);
+ }
+
+ fn select_prev_match(&mut self, _: &SelectPrevMatch, cx: &mut ViewContext<Self>) {
+ self.select_match(Direction::Prev, 1, cx);
+ }
+
+ fn select_all_matches(&mut self, _: &SelectAllMatches, cx: &mut ViewContext<Self>) {
+ if !self.dismissed && self.active_match_index.is_some() {
+ if let Some(searchable_item) = self.active_searchable_item.as_ref() {
+ if let Some(matches) = self
+ .searchable_items_with_matches
+ .get(&searchable_item.downgrade())
+ {
+ searchable_item.select_matches(matches, cx);
+ self.focus_editor(&FocusEditor, cx);
+ }
+ }
+ }
+ }
+
+ pub fn select_match(&mut self, direction: Direction, count: usize, cx: &mut ViewContext<Self>) {
+ if let Some(index) = self.active_match_index {
+ if let Some(searchable_item) = self.active_searchable_item.as_ref() {
+ if let Some(matches) = self
+ .searchable_items_with_matches
+ .get(&searchable_item.downgrade())
+ {
+ let new_match_index = searchable_item
+ .match_index_for_direction(matches, index, direction, count, cx);
+
+ searchable_item.update_matches(matches, cx);
+ searchable_item.activate_match(new_match_index, matches, cx);
+ }
+ }
+ }
+ }
+
+ pub fn select_last_match(&mut self, cx: &mut ViewContext<Self>) {
+ if let Some(searchable_item) = self.active_searchable_item.as_ref() {
+ if let Some(matches) = self
+ .searchable_items_with_matches
+ .get(&searchable_item.downgrade())
+ {
+ if matches.len() == 0 {
+ return;
+ }
+ let new_match_index = matches.len() - 1;
+ searchable_item.update_matches(matches, cx);
+ searchable_item.activate_match(new_match_index, matches, cx);
+ }
+ }
+ }
+
+ fn on_query_editor_event(
+ &mut self,
+ _: View<Editor>,
+ event: &editor::EditorEvent,
+ cx: &mut ViewContext<Self>,
+ ) {
+ if let editor::EditorEvent::Edited { .. } = event {
+ self.query_contains_error = false;
+ self.clear_matches(cx);
+ let search = self.update_matches(cx);
+ cx.spawn(|this, mut cx| async move {
+ search.await?;
+ this.update(&mut cx, |this, cx| this.activate_current_match(cx))
+ })
+ .detach_and_log_err(cx);
+ }
+ }
+
+ fn on_active_searchable_item_event(&mut self, event: &SearchEvent, cx: &mut ViewContext<Self>) {
+ match event {
+ SearchEvent::MatchesInvalidated => {
+ let _ = self.update_matches(cx);
+ }
+ SearchEvent::ActiveMatchChanged => self.update_match_index(cx),
+ }
+ }
+
+ fn toggle_case_sensitive(&mut self, _: &ToggleCaseSensitive, cx: &mut ViewContext<Self>) {
+ self.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx)
+ }
+ fn toggle_whole_word(&mut self, _: &ToggleWholeWord, cx: &mut ViewContext<Self>) {
+ self.toggle_search_option(SearchOptions::WHOLE_WORD, cx)
+ }
+ fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
+ let mut active_item_matches = None;
+ for (searchable_item, matches) in self.searchable_items_with_matches.drain() {
+ if let Some(searchable_item) =
+ WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
+ {
+ if Some(&searchable_item) == self.active_searchable_item.as_ref() {
+ active_item_matches = Some((searchable_item.downgrade(), matches));
+ } else {
+ searchable_item.clear_matches(cx);
+ }
+ }
+ }
+
+ self.searchable_items_with_matches
+ .extend(active_item_matches);
+ }
+
+ fn update_matches(&mut self, cx: &mut ViewContext<Self>) -> oneshot::Receiver<()> {
+ let (done_tx, done_rx) = oneshot::channel();
+ let query = self.query(cx);
+ self.pending_search.take();
+
+ if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
+ if query.is_empty() {
+ self.active_match_index.take();
+ active_searchable_item.clear_matches(cx);
+ let _ = done_tx.send(());
+ cx.notify();
+ } else {
+ let query: Arc<_> = if self.current_mode == SearchMode::Regex {
+ match SearchQuery::regex(
+ query,
+ self.search_options.contains(SearchOptions::WHOLE_WORD),
+ self.search_options.contains(SearchOptions::CASE_SENSITIVE),
+ false,
+ Vec::new(),
+ Vec::new(),
+ ) {
+ Ok(query) => query.with_replacement(self.replacement(cx)),
+ Err(_) => {
+ self.query_contains_error = true;
+ cx.notify();
+ return done_rx;
+ }
+ }
+ } else {
+ match SearchQuery::text(
+ query,
+ self.search_options.contains(SearchOptions::WHOLE_WORD),
+ self.search_options.contains(SearchOptions::CASE_SENSITIVE),
+ false,
+ Vec::new(),
+ Vec::new(),
+ ) {
+ Ok(query) => query.with_replacement(self.replacement(cx)),
+ Err(_) => {
+ self.query_contains_error = true;
+ cx.notify();
+ return done_rx;
+ }
+ }
+ }
+ .into();
+ self.active_search = Some(query.clone());
+ let query_text = query.as_str().to_string();
+
+ let matches = active_searchable_item.find_matches(query, cx);
+
+ let active_searchable_item = active_searchable_item.downgrade();
+ self.pending_search = Some(cx.spawn(|this, mut cx| async move {
+ let matches = matches.await;
+
+ this.update(&mut cx, |this, cx| {
+ if let Some(active_searchable_item) =
+ WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx)
+ {
+ this.searchable_items_with_matches
+ .insert(active_searchable_item.downgrade(), matches);
+
+ this.update_match_index(cx);
+ this.search_history.add(query_text);
+ if !this.dismissed {
+ let matches = this
+ .searchable_items_with_matches
+ .get(&active_searchable_item.downgrade())
+ .unwrap();
+ active_searchable_item.update_matches(matches, cx);
+ let _ = done_tx.send(());
+ }
+ cx.notify();
+ }
+ })
+ .log_err();
+ }));
+ }
+ }
+ done_rx
+ }
+
+ fn update_match_index(&mut self, cx: &mut ViewContext<Self>) {
+ let new_index = self
+ .active_searchable_item
+ .as_ref()
+ .and_then(|searchable_item| {
+ let matches = self
+ .searchable_items_with_matches
+ .get(&searchable_item.downgrade())?;
+ searchable_item.active_match_index(matches, cx)
+ });
+ if new_index != self.active_match_index {
+ self.active_match_index = new_index;
+ cx.notify();
+ }
+ }
+
+ fn next_history_query(&mut self, _: &NextHistoryQuery, cx: &mut ViewContext<Self>) {
+ if let Some(new_query) = self.search_history.next().map(str::to_string) {
+ let _ = self.search(&new_query, Some(self.search_options), cx);
+ } else {
+ self.search_history.reset_selection();
+ let _ = self.search("", Some(self.search_options), cx);
+ }
+ }
+
+ fn previous_history_query(&mut self, _: &PreviousHistoryQuery, cx: &mut ViewContext<Self>) {
+ if self.query(cx).is_empty() {
+ if let Some(new_query) = self.search_history.current().map(str::to_string) {
+ let _ = self.search(&new_query, Some(self.search_options), cx);
+ return;
+ }
+ }
+
+ if let Some(new_query) = self.search_history.previous().map(str::to_string) {
+ let _ = self.search(&new_query, Some(self.search_options), cx);
+ }
+ }
+ fn cycle_mode(&mut self, _: &CycleMode, cx: &mut ViewContext<Self>) {
+ self.activate_search_mode(next_mode(&self.current_mode, false), cx);
+ }
+ fn toggle_replace(&mut self, _: &ToggleReplace, cx: &mut ViewContext<Self>) {
+ if let Some(_) = &self.active_searchable_item {
+ self.replace_enabled = !self.replace_enabled;
+ if !self.replace_enabled {
+ let handle = self.query_editor.focus_handle(cx);
+ cx.focus(&handle);
+ }
+ cx.notify();
+ }
+ }
+ fn replace_next(&mut self, _: &ReplaceNext, cx: &mut ViewContext<Self>) {
+ let mut should_propagate = true;
+ if !self.dismissed && self.active_search.is_some() {
+ if let Some(searchable_item) = self.active_searchable_item.as_ref() {
+ if let Some(query) = self.active_search.as_ref() {
+ if let Some(matches) = self
+ .searchable_items_with_matches
+ .get(&searchable_item.downgrade())
+ {
+ if let Some(active_index) = self.active_match_index {
+ let query = query
+ .as_ref()
+ .clone()
+ .with_replacement(self.replacement(cx));
+ searchable_item.replace(&matches[active_index], &query, cx);
+ self.select_next_match(&SelectNextMatch, cx);
+ }
+ should_propagate = false;
+ self.focus_editor(&FocusEditor, cx);
+ }
+ }
+ }
+ }
+ if !should_propagate {
+ cx.stop_propagation();
+ }
+ }
+ pub fn replace_all(&mut self, _: &ReplaceAll, cx: &mut ViewContext<Self>) {
+ if !self.dismissed && self.active_search.is_some() {
+ if let Some(searchable_item) = self.active_searchable_item.as_ref() {
+ if let Some(query) = self.active_search.as_ref() {
+ if let Some(matches) = self
+ .searchable_items_with_matches
+ .get(&searchable_item.downgrade())
+ {
+ let query = query
+ .as_ref()
+ .clone()
+ .with_replacement(self.replacement(cx));
+ for m in matches {
+ searchable_item.replace(m, &query, cx);
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use std::ops::Range;
+
+ use super::*;
+ use editor::{DisplayPoint, Editor};
+ use gpui::{Context, EmptyView, Hsla, TestAppContext, VisualTestContext};
+ use language::Buffer;
+ use smol::stream::StreamExt as _;
+ use unindent::Unindent as _;
+
+ fn init_globals(cx: &mut TestAppContext) {
+ cx.update(|cx| {
+ let store = settings::SettingsStore::test(cx);
+ cx.set_global(store);
+ editor::init(cx);
+
+ language::init(cx);
+ theme::init(theme::LoadThemes::JustBase, cx);
+ });
+ }
+ fn init_test(
+ cx: &mut TestAppContext,
+ ) -> (
+ View<Editor>,
+ View<BufferSearchBar>,
+ &mut VisualTestContext<'_>,
+ ) {
+ init_globals(cx);
+ let buffer = cx.build_model(|cx| {
+ Buffer::new(
+ 0,
+ cx.entity_id().as_u64(),
+ r#"
+ A regular expression (shortened as regex or regexp;[1] also referred to as
+ rational expression[2][3]) is a sequence of characters that specifies a search
+ pattern in text. Usually such patterns are used by string-searching algorithms
+ for "find" or "find and replace" operations on strings, or for input validation.
+ "#
+ .unindent(),
+ )
+ });
+ let (_, cx) = cx.add_window_view(|_| EmptyView {});
+ let editor = cx.build_view(|cx| Editor::for_buffer(buffer.clone(), None, cx));
+
+ let search_bar = cx.build_view(|cx| {
+ let mut search_bar = BufferSearchBar::new(cx);
+ search_bar.set_active_pane_item(Some(&editor), cx);
+ search_bar.show(cx);
+ search_bar
+ });
+
+ (editor, search_bar, cx)
+ }
+
+ #[gpui::test]
+ async fn test_search_simple(cx: &mut TestAppContext) {
+ let (editor, search_bar, cx) = init_test(cx);
+ // todo! osiewicz: these tests asserted on background color as well, that should be brought back.
+ let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
+ background_highlights
+ .into_iter()
+ .map(|(range, _)| range)
+ .collect::<Vec<_>>()
+ };
+ // Search for a string that appears with different casing.
+ // By default, search is case-insensitive.
+ search_bar
+ .update(cx, |search_bar, cx| search_bar.search("us", None, cx))
+ .await
+ .unwrap();
+ editor.update(cx, |editor, cx| {
+ assert_eq!(
+ display_points_of(editor.all_text_background_highlights(cx)),
+ &[
+ DisplayPoint::new(2, 17)..DisplayPoint::new(2, 19),
+ DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
+ ]
+ );
+ });
+
+ // Switch to a case sensitive search.
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx);
+ });
+ let mut editor_notifications = cx.notifications(&editor);
+ editor_notifications.next().await;
+ editor.update(cx, |editor, cx| {
+ assert_eq!(
+ display_points_of(editor.all_text_background_highlights(cx)),
+ &[DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),]
+ );
+ });
+
+ // Search for a string that appears both as a whole word and
+ // within other words. By default, all results are found.
+ search_bar
+ .update(cx, |search_bar, cx| search_bar.search("or", None, cx))
+ .await
+ .unwrap();
+ editor.update(cx, |editor, cx| {
+ assert_eq!(
+ display_points_of(editor.all_text_background_highlights(cx)),
+ &[
+ DisplayPoint::new(0, 24)..DisplayPoint::new(0, 26),
+ DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
+ DisplayPoint::new(2, 71)..DisplayPoint::new(2, 73),
+ DisplayPoint::new(3, 1)..DisplayPoint::new(3, 3),
+ DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
+ DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
+ DisplayPoint::new(3, 60)..DisplayPoint::new(3, 62),
+ ]
+ );
+ });
+
+ // Switch to a whole word search.
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
+ });
+ let mut editor_notifications = cx.notifications(&editor);
+ editor_notifications.next().await;
+ editor.update(cx, |editor, cx| {
+ assert_eq!(
+ display_points_of(editor.all_text_background_highlights(cx)),
+ &[
+ DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
+ DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
+ DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
+ ]
+ );
+ });
+
+ editor.update(cx, |editor, cx| {
+ editor.change_selections(None, cx, |s| {
+ s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
+ });
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ assert_eq!(search_bar.active_match_index, Some(0));
+ search_bar.select_next_match(&SelectNextMatch, cx);
+ assert_eq!(
+ editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
+ [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
+ );
+ });
+ search_bar.update(cx, |search_bar, _| {
+ assert_eq!(search_bar.active_match_index, Some(0));
+ });
+
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.select_next_match(&SelectNextMatch, cx);
+ assert_eq!(
+ editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
+ [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
+ );
+ });
+ search_bar.update(cx, |search_bar, _| {
+ assert_eq!(search_bar.active_match_index, Some(1));
+ });
+
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.select_next_match(&SelectNextMatch, cx);
+ assert_eq!(
+ editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
+ [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
+ );
+ });
+ search_bar.update(cx, |search_bar, _| {
+ assert_eq!(search_bar.active_match_index, Some(2));
+ });
+
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.select_next_match(&SelectNextMatch, cx);
+ assert_eq!(
+ editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
+ [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
+ );
+ });
+ search_bar.update(cx, |search_bar, _| {
+ assert_eq!(search_bar.active_match_index, Some(0));
+ });
+
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.select_prev_match(&SelectPrevMatch, cx);
+ assert_eq!(
+ editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
+ [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
+ );
+ });
+ search_bar.update(cx, |search_bar, _| {
+ assert_eq!(search_bar.active_match_index, Some(2));
+ });
+
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.select_prev_match(&SelectPrevMatch, cx);
+ assert_eq!(
+ editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
+ [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
+ );
+ });
+ search_bar.update(cx, |search_bar, _| {
+ assert_eq!(search_bar.active_match_index, Some(1));
+ });
+
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.select_prev_match(&SelectPrevMatch, cx);
+ assert_eq!(
+ editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
+ [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
+ );
+ });
+ search_bar.update(cx, |search_bar, _| {
+ assert_eq!(search_bar.active_match_index, Some(0));
+ });
+
+ // Park the cursor in between matches and ensure that going to the previous match selects
+ // the closest match to the left.
+ editor.update(cx, |editor, cx| {
+ editor.change_selections(None, cx, |s| {
+ s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
+ });
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ assert_eq!(search_bar.active_match_index, Some(1));
+ search_bar.select_prev_match(&SelectPrevMatch, cx);
+ assert_eq!(
+ editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
+ [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
+ );
+ });
+ search_bar.update(cx, |search_bar, _| {
+ assert_eq!(search_bar.active_match_index, Some(0));
+ });
+
+ // Park the cursor in between matches and ensure that going to the next match selects the
+ // closest match to the right.
+ editor.update(cx, |editor, cx| {
+ editor.change_selections(None, cx, |s| {
+ s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
+ });
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ assert_eq!(search_bar.active_match_index, Some(1));
+ search_bar.select_next_match(&SelectNextMatch, cx);
+ assert_eq!(
+ editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
+ [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
+ );
+ });
+ search_bar.update(cx, |search_bar, _| {
+ assert_eq!(search_bar.active_match_index, Some(1));
+ });
+
+ // Park the cursor after the last match and ensure that going to the previous match selects
+ // the last match.
+ editor.update(cx, |editor, cx| {
+ editor.change_selections(None, cx, |s| {
+ s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
+ });
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ assert_eq!(search_bar.active_match_index, Some(2));
+ search_bar.select_prev_match(&SelectPrevMatch, cx);
+ assert_eq!(
+ editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
+ [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
+ );
+ });
+ search_bar.update(cx, |search_bar, _| {
+ assert_eq!(search_bar.active_match_index, Some(2));
+ });
+
+ // Park the cursor after the last match and ensure that going to the next match selects the
+ // first match.
+ editor.update(cx, |editor, cx| {
+ editor.change_selections(None, cx, |s| {
+ s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
+ });
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ assert_eq!(search_bar.active_match_index, Some(2));
+ search_bar.select_next_match(&SelectNextMatch, cx);
+ assert_eq!(
+ editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
+ [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
+ );
+ });
+ search_bar.update(cx, |search_bar, _| {
+ assert_eq!(search_bar.active_match_index, Some(0));
+ });
+
+ // Park the cursor before the first match and ensure that going to the previous match
+ // selects the last match.
+ editor.update(cx, |editor, cx| {
+ editor.change_selections(None, cx, |s| {
+ s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
+ });
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ assert_eq!(search_bar.active_match_index, Some(0));
+ search_bar.select_prev_match(&SelectPrevMatch, cx);
+ assert_eq!(
+ editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
+ [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
+ );
+ });
+ search_bar.update(cx, |search_bar, _| {
+ assert_eq!(search_bar.active_match_index, Some(2));
+ });
+ }
+
+ #[gpui::test]
+ async fn test_search_option_handling(cx: &mut TestAppContext) {
+ let (editor, search_bar, cx) = init_test(cx);
+
+ // show with options should make current search case sensitive
+ search_bar
+ .update(cx, |search_bar, cx| {
+ search_bar.show(cx);
+ search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), cx)
+ })
+ .await
+ .unwrap();
+ // todo! osiewicz: these tests previously asserted on background color highlights; that should be introduced back.
+ let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
+ background_highlights
+ .into_iter()
+ .map(|(range, _)| range)
+ .collect::<Vec<_>>()
+ };
+ editor.update(cx, |editor, cx| {
+ assert_eq!(
+ display_points_of(editor.all_text_background_highlights(cx)),
+ &[DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),]
+ );
+ });
+
+ // search_suggested should restore default options
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.search_suggested(cx);
+ assert_eq!(search_bar.search_options, SearchOptions::NONE)
+ });
+
+ // toggling a search option should update the defaults
+ search_bar
+ .update(cx, |search_bar, cx| {
+ search_bar.search("regex", Some(SearchOptions::CASE_SENSITIVE), cx)
+ })
+ .await
+ .unwrap();
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx)
+ });
+ let mut editor_notifications = cx.notifications(&editor);
+ editor_notifications.next().await;
+ editor.update(cx, |editor, cx| {
+ assert_eq!(
+ display_points_of(editor.all_text_background_highlights(cx)),
+ &[DisplayPoint::new(0, 35)..DisplayPoint::new(0, 40),]
+ );
+ });
+
+ // defaults should still include whole word
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.search_suggested(cx);
+ assert_eq!(
+ search_bar.search_options,
+ SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD
+ )
+ });
+ }
+
+ #[gpui::test]
+ async fn test_search_select_all_matches(cx: &mut TestAppContext) {
+ init_globals(cx);
+ let buffer_text = r#"
+ A regular expression (shortened as regex or regexp;[1] also referred to as
+ rational expression[2][3]) is a sequence of characters that specifies a search
+ pattern in text. Usually such patterns are used by string-searching algorithms
+ for "find" or "find and replace" operations on strings, or for input validation.
+ "#
+ .unindent();
+ let expected_query_matches_count = buffer_text
+ .chars()
+ .filter(|c| c.to_ascii_lowercase() == 'a')
+ .count();
+ assert!(
+ expected_query_matches_count > 1,
+ "Should pick a query with multiple results"
+ );
+ let buffer = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), buffer_text));
+ let window = cx.add_window(|_| EmptyView {});
+
+ let editor = window.build_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx));
+
+ let search_bar = window.build_view(cx, |cx| {
+ let mut search_bar = BufferSearchBar::new(cx);
+ search_bar.set_active_pane_item(Some(&editor), cx);
+ search_bar.show(cx);
+ search_bar
+ });
+
+ window
+ .update(cx, |_, cx| {
+ search_bar.update(cx, |search_bar, cx| search_bar.search("a", None, cx))
+ })
+ .unwrap()
+ .await
+ .unwrap();
+ let initial_selections = window
+ .update(cx, |_, cx| {
+ search_bar.update(cx, |search_bar, cx| {
+ let handle = search_bar.query_editor.focus_handle(cx);
+ cx.focus(&handle);
+ search_bar.activate_current_match(cx);
+ });
+ assert!(
+ !editor.read(cx).is_focused(cx),
+ "Initially, the editor should not be focused"
+ );
+ let initial_selections = editor.update(cx, |editor, cx| {
+ let initial_selections = editor.selections.display_ranges(cx);
+ assert_eq!(
+ initial_selections.len(), 1,
+ "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}",
+ );
+ initial_selections
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ assert_eq!(search_bar.active_match_index, Some(0));
+ let handle = search_bar.query_editor.focus_handle(cx);
+ cx.focus(&handle);
+ search_bar.select_all_matches(&SelectAllMatches, cx);
+ });
+ assert!(
+ editor.read(cx).is_focused(cx),
+ "Should focus editor after successful SelectAllMatches"
+ );
+ search_bar.update(cx, |search_bar, cx| {
+ let all_selections =
+ editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
+ assert_eq!(
+ all_selections.len(),
+ expected_query_matches_count,
+ "Should select all `a` characters in the buffer, but got: {all_selections:?}"
+ );
+ assert_eq!(
+ search_bar.active_match_index,
+ Some(0),
+ "Match index should not change after selecting all matches"
+ );
+ });
+
+ search_bar.update(cx, |this, cx| this.select_next_match(&SelectNextMatch, cx));
+ initial_selections
+ }).unwrap();
+
+ window
+ .update(cx, |_, cx| {
+ assert!(
+ editor.read(cx).is_focused(cx),
+ "Should still have editor focused after SelectNextMatch"
+ );
+ search_bar.update(cx, |search_bar, cx| {
+ let all_selections =
+ editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
+ assert_eq!(
+ all_selections.len(),
+ 1,
+ "On next match, should deselect items and select the next match"
+ );
+ assert_ne!(
+ all_selections, initial_selections,
+ "Next match should be different from the first selection"
+ );
+ assert_eq!(
+ search_bar.active_match_index,
+ Some(1),
+ "Match index should be updated to the next one"
+ );
+ let handle = search_bar.query_editor.focus_handle(cx);
+ cx.focus(&handle);
+ search_bar.select_all_matches(&SelectAllMatches, cx);
+ });
+ })
+ .unwrap();
+ window
+ .update(cx, |_, cx| {
+ assert!(
+ editor.read(cx).is_focused(cx),
+ "Should focus editor after successful SelectAllMatches"
+ );
+ search_bar.update(cx, |search_bar, cx| {
+ let all_selections =
+ editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
+ assert_eq!(
+ all_selections.len(),
+ expected_query_matches_count,
+ "Should select all `a` characters in the buffer, but got: {all_selections:?}"
+ );
+ assert_eq!(
+ search_bar.active_match_index,
+ Some(1),
+ "Match index should not change after selecting all matches"
+ );
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.select_prev_match(&SelectPrevMatch, cx);
+ });
+ })
+ .unwrap();
+ let last_match_selections = window
+ .update(cx, |_, cx| {
+ assert!(
+ editor.read(cx).is_focused(&cx),
+ "Should still have editor focused after SelectPrevMatch"
+ );
+
+ search_bar.update(cx, |search_bar, cx| {
+ let all_selections =
+ editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
+ assert_eq!(
+ all_selections.len(),
+ 1,
+ "On previous match, should deselect items and select the previous item"
+ );
+ assert_eq!(
+ all_selections, initial_selections,
+ "Previous match should be the same as the first selection"
+ );
+ assert_eq!(
+ search_bar.active_match_index,
+ Some(0),
+ "Match index should be updated to the previous one"
+ );
+ all_selections
+ })
+ })
+ .unwrap();
+
+ window
+ .update(cx, |_, cx| {
+ search_bar.update(cx, |search_bar, cx| {
+ let handle = search_bar.query_editor.focus_handle(cx);
+ cx.focus(&handle);
+ search_bar.search("abas_nonexistent_match", None, cx)
+ })
+ })
+ .unwrap()
+ .await
+ .unwrap();
+ window
+ .update(cx, |_, cx| {
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.select_all_matches(&SelectAllMatches, cx);
+ });
+ assert!(
+ editor.update(cx, |this, cx| !this.is_focused(cx.window_context())),
+ "Should not switch focus to editor if SelectAllMatches does not find any matches"
+ );
+ search_bar.update(cx, |search_bar, cx| {
+ let all_selections =
+ editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
+ assert_eq!(
+ all_selections, last_match_selections,
+ "Should not select anything new if there are no matches"
+ );
+ assert!(
+ search_bar.active_match_index.is_none(),
+ "For no matches, there should be no active match index"
+ );
+ });
+ })
+ .unwrap();
+ }
+
+ #[gpui::test]
+ async fn test_search_query_history(cx: &mut TestAppContext) {
+ //crate::project_search::tests::init_test(cx);
+ init_globals(cx);
+ let buffer_text = r#"
+ A regular expression (shortened as regex or regexp;[1] also referred to as
+ rational expression[2][3]) is a sequence of characters that specifies a search
+ pattern in text. Usually such patterns are used by string-searching algorithms
+ for "find" or "find and replace" operations on strings, or for input validation.
+ "#
+ .unindent();
+ let buffer = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), buffer_text));
+ let (_, cx) = cx.add_window_view(|_| EmptyView {});
+
+ let editor = cx.build_view(|cx| Editor::for_buffer(buffer.clone(), None, cx));
+
+ let search_bar = cx.build_view(|cx| {
+ let mut search_bar = BufferSearchBar::new(cx);
+ search_bar.set_active_pane_item(Some(&editor), cx);
+ search_bar.show(cx);
+ search_bar
+ });
+
+ // Add 3 search items into the history.
+ search_bar
+ .update(cx, |search_bar, cx| search_bar.search("a", None, cx))
+ .await
+ .unwrap();
+ search_bar
+ .update(cx, |search_bar, cx| search_bar.search("b", None, cx))
+ .await
+ .unwrap();
+ search_bar
+ .update(cx, |search_bar, cx| {
+ search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), cx)
+ })
+ .await
+ .unwrap();
+ // Ensure that the latest search is active.
+ search_bar.update(cx, |search_bar, cx| {
+ assert_eq!(search_bar.query(cx), "c");
+ assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
+ });
+
+ // Next history query after the latest should set the query to the empty string.
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.next_history_query(&NextHistoryQuery, cx);
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ assert_eq!(search_bar.query(cx), "");
+ assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.next_history_query(&NextHistoryQuery, cx);
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ assert_eq!(search_bar.query(cx), "");
+ assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
+ });
+
+ // First previous query for empty current query should set the query to the latest.
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.previous_history_query(&PreviousHistoryQuery, cx);
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ assert_eq!(search_bar.query(cx), "c");
+ assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
+ });
+
+ // Further previous items should go over the history in reverse order.
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.previous_history_query(&PreviousHistoryQuery, cx);
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ assert_eq!(search_bar.query(cx), "b");
+ assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
+ });
+
+ // Previous items should never go behind the first history item.
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.previous_history_query(&PreviousHistoryQuery, cx);
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ assert_eq!(search_bar.query(cx), "a");
+ assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.previous_history_query(&PreviousHistoryQuery, cx);
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ assert_eq!(search_bar.query(cx), "a");
+ assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
+ });
+
+ // Next items should go over the history in the original order.
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.next_history_query(&NextHistoryQuery, cx);
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ assert_eq!(search_bar.query(cx), "b");
+ assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
+ });
+
+ search_bar
+ .update(cx, |search_bar, cx| search_bar.search("ba", None, cx))
+ .await
+ .unwrap();
+ search_bar.update(cx, |search_bar, cx| {
+ assert_eq!(search_bar.query(cx), "ba");
+ assert_eq!(search_bar.search_options, SearchOptions::NONE);
+ });
+
+ // New search input should add another entry to history and move the selection to the end of the history.
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.previous_history_query(&PreviousHistoryQuery, cx);
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ assert_eq!(search_bar.query(cx), "c");
+ assert_eq!(search_bar.search_options, SearchOptions::NONE);
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.previous_history_query(&PreviousHistoryQuery, cx);
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ assert_eq!(search_bar.query(cx), "b");
+ assert_eq!(search_bar.search_options, SearchOptions::NONE);
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.next_history_query(&NextHistoryQuery, cx);
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ assert_eq!(search_bar.query(cx), "c");
+ assert_eq!(search_bar.search_options, SearchOptions::NONE);
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.next_history_query(&NextHistoryQuery, cx);
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ assert_eq!(search_bar.query(cx), "ba");
+ assert_eq!(search_bar.search_options, SearchOptions::NONE);
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.next_history_query(&NextHistoryQuery, cx);
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ assert_eq!(search_bar.query(cx), "");
+ assert_eq!(search_bar.search_options, SearchOptions::NONE);
+ });
+ }
+ #[gpui::test]
+ async fn test_replace_simple(cx: &mut TestAppContext) {
+ let (editor, search_bar, cx) = init_test(cx);
+
+ search_bar
+ .update(cx, |search_bar, cx| {
+ search_bar.search("expression", None, cx)
+ })
+ .await
+ .unwrap();
+
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.replacement_editor.update(cx, |editor, cx| {
+ // We use $1 here as initially we should be in Text mode, where `$1` should be treated literally.
+ editor.set_text("expr$1", cx);
+ });
+ search_bar.replace_all(&ReplaceAll, cx)
+ });
+ assert_eq!(
+ editor.update(cx, |this, cx| { this.text(cx) }),
+ r#"
+ A regular expr$1 (shortened as regex or regexp;[1] also referred to as
+ rational expr$1[2][3]) is a sequence of characters that specifies a search
+ pattern in text. Usually such patterns are used by string-searching algorithms
+ for "find" or "find and replace" operations on strings, or for input validation.
+ "#
+ .unindent()
+ );
+
+ // Search for word boundaries and replace just a single one.
+ search_bar
+ .update(cx, |search_bar, cx| {
+ search_bar.search("or", Some(SearchOptions::WHOLE_WORD), cx)
+ })
+ .await
+ .unwrap();
+
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.replacement_editor.update(cx, |editor, cx| {
+ editor.set_text("banana", cx);
+ });
+ search_bar.replace_next(&ReplaceNext, cx)
+ });
+ // Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text.
+ assert_eq!(
+ editor.update(cx, |this, cx| { this.text(cx) }),
+ r#"
+ A regular expr$1 (shortened as regex banana regexp;[1] also referred to as
+ rational expr$1[2][3]) is a sequence of characters that specifies a search
+ pattern in text. Usually such patterns are used by string-searching algorithms
+ for "find" or "find and replace" operations on strings, or for input validation.
+ "#
+ .unindent()
+ );
+ // Let's turn on regex mode.
+ search_bar
+ .update(cx, |search_bar, cx| {
+ search_bar.activate_search_mode(SearchMode::Regex, cx);
+ search_bar.search("\\[([^\\]]+)\\]", None, cx)
+ })
+ .await
+ .unwrap();
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.replacement_editor.update(cx, |editor, cx| {
+ editor.set_text("${1}number", cx);
+ });
+ search_bar.replace_all(&ReplaceAll, cx)
+ });
+ assert_eq!(
+ editor.update(cx, |this, cx| { this.text(cx) }),
+ r#"
+ A regular expr$1 (shortened as regex banana regexp;1number also referred to as
+ rational expr$12number3number) is a sequence of characters that specifies a search
+ pattern in text. Usually such patterns are used by string-searching algorithms
+ for "find" or "find and replace" operations on strings, or for input validation.
+ "#
+ .unindent()
+ );
+ // Now with a whole-word twist.
+ search_bar
+ .update(cx, |search_bar, cx| {
+ search_bar.activate_search_mode(SearchMode::Regex, cx);
+ search_bar.search("a\\w+s", Some(SearchOptions::WHOLE_WORD), cx)
+ })
+ .await
+ .unwrap();
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.replacement_editor.update(cx, |editor, cx| {
+ editor.set_text("things", cx);
+ });
+ search_bar.replace_all(&ReplaceAll, cx)
+ });
+ // The only word affected by this edit should be `algorithms`, even though there's a bunch
+ // of words in this text that would match this regex if not for WHOLE_WORD.
+ assert_eq!(
+ editor.update(cx, |this, cx| { this.text(cx) }),
+ r#"
+ A regular expr$1 (shortened as regex banana regexp;1number also referred to as
+ rational expr$12number3number) is a sequence of characters that specifies a search
+ pattern in text. Usually such patterns are used by string-searching things
+ for "find" or "find and replace" operations on strings, or for input validation.
+ "#
+ .unindent()
+ );
+ }
+}
@@ -0,0 +1,184 @@
+use smallvec::SmallVec;
+const SEARCH_HISTORY_LIMIT: usize = 20;
+
+#[derive(Default, Debug, Clone)]
+pub struct SearchHistory {
+ history: SmallVec<[String; SEARCH_HISTORY_LIMIT]>,
+ selected: Option<usize>,
+}
+
+impl SearchHistory {
+ pub fn add(&mut self, search_string: String) {
+ if let Some(i) = self.selected {
+ if search_string == self.history[i] {
+ return;
+ }
+ }
+
+ if let Some(previously_searched) = self.history.last_mut() {
+ if search_string.find(previously_searched.as_str()).is_some() {
+ *previously_searched = search_string;
+ self.selected = Some(self.history.len() - 1);
+ return;
+ }
+ }
+
+ self.history.push(search_string);
+ if self.history.len() > SEARCH_HISTORY_LIMIT {
+ self.history.remove(0);
+ }
+ self.selected = Some(self.history.len() - 1);
+ }
+
+ pub fn next(&mut self) -> Option<&str> {
+ let history_size = self.history.len();
+ if history_size == 0 {
+ return None;
+ }
+
+ let selected = self.selected?;
+ if selected == history_size - 1 {
+ return None;
+ }
+ let next_index = selected + 1;
+ self.selected = Some(next_index);
+ Some(&self.history[next_index])
+ }
+
+ pub fn current(&self) -> Option<&str> {
+ Some(&self.history[self.selected?])
+ }
+
+ pub fn previous(&mut self) -> Option<&str> {
+ let history_size = self.history.len();
+ if history_size == 0 {
+ return None;
+ }
+
+ let prev_index = match self.selected {
+ Some(selected_index) => {
+ if selected_index == 0 {
+ return None;
+ } else {
+ selected_index - 1
+ }
+ }
+ None => history_size - 1,
+ };
+
+ self.selected = Some(prev_index);
+ Some(&self.history[prev_index])
+ }
+
+ pub fn reset_selection(&mut self) {
+ self.selected = None;
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_add() {
+ let mut search_history = SearchHistory::default();
+ assert_eq!(
+ search_history.current(),
+ None,
+ "No current selection should be set fo the default search history"
+ );
+
+ search_history.add("rust".to_string());
+ assert_eq!(
+ search_history.current(),
+ Some("rust"),
+ "Newly added item should be selected"
+ );
+
+ // check if duplicates are not added
+ search_history.add("rust".to_string());
+ assert_eq!(
+ search_history.history.len(),
+ 1,
+ "Should not add a duplicate"
+ );
+ assert_eq!(search_history.current(), Some("rust"));
+
+ // check if new string containing the previous string replaces it
+ search_history.add("rustlang".to_string());
+ assert_eq!(
+ search_history.history.len(),
+ 1,
+ "Should replace previous item if it's a substring"
+ );
+ assert_eq!(search_history.current(), Some("rustlang"));
+
+ // push enough items to test SEARCH_HISTORY_LIMIT
+ for i in 0..SEARCH_HISTORY_LIMIT * 2 {
+ search_history.add(format!("item{i}"));
+ }
+ assert!(search_history.history.len() <= SEARCH_HISTORY_LIMIT);
+ }
+
+ #[test]
+ fn test_next_and_previous() {
+ let mut search_history = SearchHistory::default();
+ assert_eq!(
+ search_history.next(),
+ None,
+ "Default search history should not have a next item"
+ );
+
+ search_history.add("Rust".to_string());
+ assert_eq!(search_history.next(), None);
+ search_history.add("JavaScript".to_string());
+ assert_eq!(search_history.next(), None);
+ search_history.add("TypeScript".to_string());
+ assert_eq!(search_history.next(), None);
+
+ assert_eq!(search_history.current(), Some("TypeScript"));
+
+ assert_eq!(search_history.previous(), Some("JavaScript"));
+ assert_eq!(search_history.current(), Some("JavaScript"));
+
+ assert_eq!(search_history.previous(), Some("Rust"));
+ assert_eq!(search_history.current(), Some("Rust"));
+
+ assert_eq!(search_history.previous(), None);
+ assert_eq!(search_history.current(), Some("Rust"));
+
+ assert_eq!(search_history.next(), Some("JavaScript"));
+ assert_eq!(search_history.current(), Some("JavaScript"));
+
+ assert_eq!(search_history.next(), Some("TypeScript"));
+ assert_eq!(search_history.current(), Some("TypeScript"));
+
+ assert_eq!(search_history.next(), None);
+ assert_eq!(search_history.current(), Some("TypeScript"));
+ }
+
+ #[test]
+ fn test_reset_selection() {
+ let mut search_history = SearchHistory::default();
+ search_history.add("Rust".to_string());
+ search_history.add("JavaScript".to_string());
+ search_history.add("TypeScript".to_string());
+
+ assert_eq!(search_history.current(), Some("TypeScript"));
+ search_history.reset_selection();
+ assert_eq!(search_history.current(), None);
+ assert_eq!(
+ search_history.previous(),
+ Some("TypeScript"),
+ "Should start from the end after reset on previous item query"
+ );
+
+ search_history.previous();
+ assert_eq!(search_history.current(), Some("JavaScript"));
+ search_history.previous();
+ assert_eq!(search_history.current(), Some("Rust"));
+
+ search_history.reset_selection();
+ assert_eq!(search_history.current(), None);
+ }
+}
@@ -0,0 +1,32 @@
+// TODO: Update the default search mode to get from config
+#[derive(Copy, Clone, Debug, Default, PartialEq)]
+pub enum SearchMode {
+ #[default]
+ Text,
+ Semantic,
+ Regex,
+}
+
+impl SearchMode {
+ pub(crate) fn label(&self) -> &'static str {
+ match self {
+ SearchMode::Text => "Text",
+ SearchMode::Semantic => "Semantic",
+ SearchMode::Regex => "Regex",
+ }
+ }
+}
+
+pub(crate) fn next_mode(mode: &SearchMode, semantic_enabled: bool) -> SearchMode {
+ match mode {
+ SearchMode::Text => SearchMode::Regex,
+ SearchMode::Regex => {
+ if semantic_enabled {
+ SearchMode::Semantic
+ } else {
+ SearchMode::Text
+ }
+ }
+ SearchMode::Semantic => SearchMode::Text,
+ }
+}
@@ -0,0 +1,2661 @@
+use crate::{
+ history::SearchHistory,
+ mode::{SearchMode, Side},
+ search_bar::{render_nav_button, render_option_button_icon, render_search_mode_button},
+ ActivateRegexMode, ActivateSemanticMode, ActivateTextMode, CycleMode, NextHistoryQuery,
+ PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions, SelectNextMatch, SelectPrevMatch,
+ ToggleCaseSensitive, ToggleReplace, ToggleWholeWord,
+};
+use anyhow::{Context, 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 gpui::{
+ actions,
+ elements::*,
+ platform::{MouseButton, PromptLevel},
+ Action, AnyElement, AnyViewHandle, AppContext, Entity, ModelContext, ModelHandle, Subscription,
+ Task, View, ViewContext, ViewHandle, WeakModelHandle, WeakViewHandle,
+};
+use menu::Confirm;
+use project::{
+ search::{SearchInputs, SearchQuery},
+ Entry, Project,
+};
+use semantic_index::{SemanticIndex, SemanticIndexStatus};
+use smallvec::SmallVec;
+use std::{
+ any::{Any, TypeId},
+ borrow::Cow,
+ collections::HashSet,
+ mem,
+ ops::{Not, Range},
+ path::PathBuf,
+ sync::Arc,
+ time::{Duration, Instant},
+};
+use util::{paths::PathMatcher, ResultExt as _};
+use workspace::{
+ item::{BreadcrumbText, Item, ItemEvent, ItemHandle},
+ searchable::{Direction, SearchableItem, SearchableItemHandle},
+ ItemNavHistory, Pane, ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceId,
+};
+
+actions!(
+ project_search,
+ [SearchInNew, ToggleFocus, NextField, ToggleFilters,]
+);
+
+#[derive(Default)]
+struct ActiveSearches(HashMap<WeakModelHandle<Project>, WeakViewHandle<ProjectSearchView>>);
+
+#[derive(Default)]
+struct ActiveSettings(HashMap<WeakModelHandle<Project>, ProjectSearchSettings>);
+
+pub fn init(cx: &mut AppContext) {
+ cx.set_global(ActiveSearches::default());
+ cx.set_global(ActiveSettings::default());
+ cx.add_action(ProjectSearchView::deploy);
+ cx.add_action(ProjectSearchView::move_focus_to_results);
+ cx.add_action(ProjectSearchBar::confirm);
+ 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::replace_next);
+ cx.add_action(ProjectSearchBar::replace_all);
+ cx.add_action(ProjectSearchBar::cycle_mode);
+ cx.add_action(ProjectSearchBar::next_history_query);
+ cx.add_action(ProjectSearchBar::previous_history_query);
+ cx.add_action(ProjectSearchBar::activate_regex_mode);
+ cx.add_action(ProjectSearchBar::toggle_replace);
+ cx.add_action(ProjectSearchBar::toggle_replace_on_a_pane);
+ cx.add_action(ProjectSearchBar::activate_text_mode);
+
+ // This action should only be registered if the semantic index is enabled
+ // We are registering it all the time, as I dont want to introduce a dependency
+ // for Semantic Index Settings globally whenever search is tested.
+ cx.add_action(ProjectSearchBar::activate_semantic_mode);
+
+ cx.capture_action(ProjectSearchBar::tab);
+ cx.capture_action(ProjectSearchBar::tab_previous);
+ cx.capture_action(ProjectSearchView::replace_all);
+ cx.capture_action(ProjectSearchView::replace_next);
+ add_toggle_option_action::<ToggleCaseSensitive>(SearchOptions::CASE_SENSITIVE, cx);
+ add_toggle_option_action::<ToggleWholeWord>(SearchOptions::WHOLE_WORD, cx);
+ add_toggle_filters_action::<ToggleFilters>(cx);
+}
+
+fn add_toggle_filters_action<A: Action>(cx: &mut AppContext) {
+ cx.add_action(move |pane: &mut Pane, _: &A, cx: &mut ViewContext<Pane>| {
+ if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<ProjectSearchBar>() {
+ if search_bar.update(cx, |search_bar, cx| search_bar.toggle_filters(cx)) {
+ return;
+ }
+ }
+ cx.propagate_action();
+ });
+}
+
+fn add_toggle_option_action<A: Action>(option: SearchOptions, cx: &mut AppContext) {
+ cx.add_action(move |pane: &mut Pane, _: &A, cx: &mut ViewContext<Pane>| {
+ if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<ProjectSearchBar>() {
+ if search_bar.update(cx, |search_bar, cx| {
+ search_bar.toggle_search_option(option, cx)
+ }) {
+ return;
+ }
+ }
+ cx.propagate_action();
+ });
+}
+
+struct ProjectSearch {
+ project: ModelHandle<Project>,
+ excerpts: ModelHandle<MultiBuffer>,
+ pending_search: Option<Task<Option<()>>>,
+ match_ranges: Vec<Range<Anchor>>,
+ active_query: Option<SearchQuery>,
+ search_id: usize,
+ search_history: SearchHistory,
+ no_results: Option<bool>,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+enum InputPanel {
+ Query,
+ Exclude,
+ Include,
+}
+
+pub struct ProjectSearchView {
+ model: ModelHandle<ProjectSearch>,
+ query_editor: ViewHandle<Editor>,
+ replacement_editor: ViewHandle<Editor>,
+ results_editor: ViewHandle<Editor>,
+ semantic_state: Option<SemanticState>,
+ semantic_permissioned: Option<bool>,
+ search_options: SearchOptions,
+ panels_with_errors: HashSet<InputPanel>,
+ active_match_index: Option<usize>,
+ search_id: usize,
+ query_editor_was_focused: bool,
+ included_files_editor: ViewHandle<Editor>,
+ excluded_files_editor: ViewHandle<Editor>,
+ filters_enabled: bool,
+ replace_enabled: bool,
+ current_mode: SearchMode,
+}
+
+struct SemanticState {
+ index_status: SemanticIndexStatus,
+ maintain_rate_limit: Option<Task<()>>,
+ _subscription: Subscription,
+}
+
+#[derive(Debug, Clone)]
+struct ProjectSearchSettings {
+ search_options: SearchOptions,
+ filters_enabled: bool,
+ current_mode: SearchMode,
+}
+
+pub struct ProjectSearchBar {
+ active_project_search: Option<ViewHandle<ProjectSearchView>>,
+ subscription: Option<Subscription>,
+}
+
+impl Entity for ProjectSearch {
+ type Event = ();
+}
+
+impl ProjectSearch {
+ fn new(project: ModelHandle<Project>, cx: &mut ModelContext<Self>) -> Self {
+ let replica_id = project.read(cx).replica_id();
+ Self {
+ project,
+ excerpts: cx.add_model(|_| MultiBuffer::new(replica_id)),
+ pending_search: Default::default(),
+ match_ranges: Default::default(),
+ active_query: None,
+ search_id: 0,
+ search_history: SearchHistory::default(),
+ no_results: None,
+ }
+ }
+
+ fn clone(&self, cx: &mut ModelContext<Self>) -> ModelHandle<Self> {
+ cx.add_model(|cx| Self {
+ project: self.project.clone(),
+ excerpts: self
+ .excerpts
+ .update(cx, |excerpts, cx| cx.add_model(|cx| excerpts.clone(cx))),
+ pending_search: Default::default(),
+ match_ranges: self.match_ranges.clone(),
+ active_query: self.active_query.clone(),
+ search_id: self.search_id,
+ search_history: self.search_history.clone(),
+ no_results: self.no_results.clone(),
+ })
+ }
+
+ fn search(&mut self, query: SearchQuery, cx: &mut ModelContext<Self>) {
+ let search = self
+ .project
+ .update(cx, |project, cx| project.search(query.clone(), cx));
+ self.search_id += 1;
+ self.search_history.add(query.as_str().to_string());
+ self.active_query = Some(query);
+ self.match_ranges.clear();
+ self.pending_search = Some(cx.spawn_weak(|this, mut cx| async move {
+ let mut matches = search;
+ let this = this.upgrade(&cx)?;
+ this.update(&mut cx, |this, cx| {
+ this.match_ranges.clear();
+ this.excerpts.update(cx, |this, cx| this.clear(cx));
+ this.no_results = Some(true);
+ });
+
+ while let Some((buffer, anchors)) = matches.next().await {
+ let mut ranges = this.update(&mut cx, |this, cx| {
+ this.no_results = Some(false);
+ this.excerpts.update(cx, |excerpts, cx| {
+ excerpts.stream_excerpts_with_context_lines(buffer, anchors, 1, cx)
+ })
+ });
+
+ while let Some(range) = ranges.next().await {
+ this.update(&mut cx, |this, _| this.match_ranges.push(range));
+ }
+ this.update(&mut cx, |_, cx| cx.notify());
+ }
+
+ this.update(&mut cx, |this, cx| {
+ this.pending_search.take();
+ cx.notify();
+ });
+
+ None
+ }));
+ cx.notify();
+ }
+
+ fn semantic_search(&mut self, inputs: &SearchInputs, cx: &mut ModelContext<Self>) {
+ let search = SemanticIndex::global(cx).map(|index| {
+ index.update(cx, |semantic_index, cx| {
+ semantic_index.search_project(
+ self.project.clone(),
+ inputs.as_str().to_owned(),
+ 10,
+ inputs.files_to_include().to_vec(),
+ inputs.files_to_exclude().to_vec(),
+ cx,
+ )
+ })
+ });
+ self.search_id += 1;
+ self.match_ranges.clear();
+ self.search_history.add(inputs.as_str().to_string());
+ self.no_results = None;
+ self.pending_search = Some(cx.spawn(|this, mut cx| async move {
+ let results = search?.await.log_err()?;
+ let matches = results
+ .into_iter()
+ .map(|result| (result.buffer, vec![result.range.start..result.range.start]));
+
+ this.update(&mut cx, |this, cx| {
+ this.no_results = Some(true);
+ this.excerpts.update(cx, |excerpts, cx| {
+ excerpts.clear(cx);
+ });
+ });
+ for (buffer, ranges) in matches {
+ let mut match_ranges = this.update(&mut cx, |this, cx| {
+ this.no_results = Some(false);
+ this.excerpts.update(cx, |excerpts, cx| {
+ excerpts.stream_excerpts_with_context_lines(buffer, ranges, 3, cx)
+ })
+ });
+ while let Some(match_range) = match_ranges.next().await {
+ this.update(&mut cx, |this, cx| {
+ this.match_ranges.push(match_range);
+ while let Ok(Some(match_range)) = match_ranges.try_next() {
+ this.match_ranges.push(match_range);
+ }
+ cx.notify();
+ });
+ }
+ }
+
+ this.update(&mut cx, |this, cx| {
+ this.pending_search.take();
+ cx.notify();
+ });
+
+ None
+ }));
+ cx.notify();
+ }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum ViewEvent {
+ UpdateTab,
+ Activate,
+ EditorEvent(editor::Event),
+ Dismiss,
+}
+
+impl Entity for ProjectSearchView {
+ type Event = ViewEvent;
+}
+
+impl View for ProjectSearchView {
+ fn ui_name() -> &'static str {
+ "ProjectSearchView"
+ }
+
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+ let model = &self.model.read(cx);
+ if model.match_ranges.is_empty() {
+ enum Status {}
+
+ let theme = theme::current(cx).clone();
+
+ // If Search is Active -> Major: Searching..., Minor: None
+ // If Semantic -> Major: "Search using Natural Language", Minor: {Status}/n{ex...}/n{ex...}
+ // If Regex -> Major: "Search using Regex", Minor: {ex...}
+ // If Text -> Major: "Text search all files and folders", Minor: {...}
+
+ let current_mode = self.current_mode;
+ let mut major_text = if model.pending_search.is_some() {
+ Cow::Borrowed("Searching...")
+ } else if model.no_results.is_some_and(|v| v) {
+ Cow::Borrowed("No Results")
+ } else {
+ match current_mode {
+ SearchMode::Text => Cow::Borrowed("Text search all files and folders"),
+ SearchMode::Semantic => {
+ Cow::Borrowed("Search all code objects using Natural Language")
+ }
+ SearchMode::Regex => Cow::Borrowed("Regex search all files and folders"),
+ }
+ };
+
+ let mut show_minor_text = true;
+ let semantic_status = self.semantic_state.as_ref().and_then(|semantic| {
+ let status = semantic.index_status;
+ match status {
+ SemanticIndexStatus::NotAuthenticated => {
+ major_text = Cow::Borrowed("Not Authenticated");
+ show_minor_text = false;
+ Some(vec![
+ "API Key Missing: Please set 'OPENAI_API_KEY' in Environment Variables."
+ .to_string(), "If you authenticated using the Assistant Panel, please restart Zed to Authenticate.".to_string()])
+ }
+ SemanticIndexStatus::Indexed => Some(vec!["Indexing complete".to_string()]),
+ SemanticIndexStatus::Indexing {
+ remaining_files,
+ rate_limit_expiry,
+ } => {
+ if remaining_files == 0 {
+ Some(vec![format!("Indexing...")])
+ } else {
+ if let Some(rate_limit_expiry) = rate_limit_expiry {
+ let remaining_seconds =
+ rate_limit_expiry.duration_since(Instant::now());
+ if remaining_seconds > Duration::from_secs(0) {
+ Some(vec![format!(
+ "Remaining files to index (rate limit resets in {}s): {}",
+ remaining_seconds.as_secs(),
+ remaining_files
+ )])
+ } else {
+ Some(vec![format!("Remaining files to index: {}", remaining_files)])
+ }
+ } else {
+ Some(vec![format!("Remaining files to index: {}", remaining_files)])
+ }
+ }
+ }
+ SemanticIndexStatus::NotIndexed => None,
+ }
+ });
+
+ let minor_text = if let Some(no_results) = model.no_results {
+ if model.pending_search.is_none() && no_results {
+ vec!["No results found in this project for the provided query".to_owned()]
+ } else {
+ vec![]
+ }
+ } else {
+ match current_mode {
+ SearchMode::Semantic => {
+ let mut minor_text: Vec<String> = Vec::new();
+ minor_text.push("".into());
+ if let Some(semantic_status) = semantic_status {
+ minor_text.extend(semantic_status);
+ }
+ if show_minor_text {
+ minor_text
+ .push("Simply explain the code you are looking to find.".into());
+ minor_text.push(
+ "ex. 'prompt user for permissions to index their project'".into(),
+ );
+ }
+ minor_text
+ }
+ _ => vec![
+ "".to_owned(),
+ "Include/exclude specific paths with the filter option.".to_owned(),
+ "Matching exact word and/or casing is available too.".to_owned(),
+ ],
+ }
+ };
+
+ let previous_query_keystrokes =
+ cx.binding_for_action(&PreviousHistoryQuery {})
+ .map(|binding| {
+ binding
+ .keystrokes()
+ .iter()
+ .map(|k| k.to_string())
+ .collect::<Vec<_>>()
+ });
+ let next_query_keystrokes =
+ cx.binding_for_action(&NextHistoryQuery {}).map(|binding| {
+ binding
+ .keystrokes()
+ .iter()
+ .map(|k| k.to_string())
+ .collect::<Vec<_>>()
+ });
+ let new_placeholder_text = match (previous_query_keystrokes, next_query_keystrokes) {
+ (Some(previous_query_keystrokes), Some(next_query_keystrokes)) => {
+ format!(
+ "Search ({}/{} for previous/next query)",
+ previous_query_keystrokes.join(" "),
+ next_query_keystrokes.join(" ")
+ )
+ }
+ (None, Some(next_query_keystrokes)) => {
+ format!(
+ "Search ({} for next query)",
+ next_query_keystrokes.join(" ")
+ )
+ }
+ (Some(previous_query_keystrokes), None) => {
+ format!(
+ "Search ({} for previous query)",
+ previous_query_keystrokes.join(" ")
+ )
+ }
+ (None, None) => String::new(),
+ };
+ self.query_editor.update(cx, |editor, cx| {
+ editor.set_placeholder_text(new_placeholder_text, cx);
+ });
+
+ MouseEventHandler::new::<Status, _>(0, cx, |_, _| {
+ Flex::column()
+ .with_child(Flex::column().contained().flex(1., true))
+ .with_child(
+ Flex::column()
+ .align_children_center()
+ .with_child(Label::new(
+ major_text,
+ theme.search.major_results_status.clone(),
+ ))
+ .with_children(
+ minor_text.into_iter().map(|x| {
+ Label::new(x, theme.search.minor_results_status.clone())
+ }),
+ )
+ .aligned()
+ .top()
+ .contained()
+ .flex(7., true),
+ )
+ .contained()
+ .with_background_color(theme.editor.background)
+ })
+ .on_down(MouseButton::Left, |_, _, cx| {
+ cx.focus_parent();
+ })
+ .into_any_named("project search view")
+ } else {
+ ChildView::new(&self.results_editor, cx)
+ .flex(1., true)
+ .into_any_named("project search view")
+ }
+ }
+
+ fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
+ let handle = cx.weak_handle();
+ cx.update_global(|state: &mut ActiveSearches, cx| {
+ state
+ .0
+ .insert(self.model.read(cx).project.downgrade(), handle)
+ });
+
+ cx.update_global(|state: &mut ActiveSettings, cx| {
+ state.0.insert(
+ self.model.read(cx).project.downgrade(),
+ self.current_settings(),
+ );
+ });
+
+ if cx.is_self_focused() {
+ if self.query_editor_was_focused {
+ cx.focus(&self.query_editor);
+ } else {
+ cx.focus(&self.results_editor);
+ }
+ }
+ }
+}
+
+impl Item for ProjectSearchView {
+ fn tab_tooltip_text(&self, cx: &AppContext) -> Option<Cow<str>> {
+ let query_text = self.query_editor.read(cx).text(cx);
+
+ query_text
+ .is_empty()
+ .not()
+ .then(|| query_text.into())
+ .or_else(|| Some("Project Search".into()))
+ }
+ fn should_close_item_on_event(event: &Self::Event) -> bool {
+ event == &Self::Event::Dismiss
+ }
+
+ fn act_as_type<'a>(
+ &'a self,
+ type_id: TypeId,
+ self_handle: &'a ViewHandle<Self>,
+ _: &'a AppContext,
+ ) -> Option<&'a AnyViewHandle> {
+ if type_id == TypeId::of::<Self>() {
+ Some(self_handle)
+ } else if type_id == TypeId::of::<Editor>() {
+ Some(&self.results_editor)
+ } else {
+ None
+ }
+ }
+
+ fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
+ self.results_editor
+ .update(cx, |editor, cx| editor.deactivated(cx));
+ }
+
+ fn tab_content<T: 'static>(
+ &self,
+ _detail: Option<usize>,
+ tab_theme: &theme::Tab,
+ cx: &AppContext,
+ ) -> AnyElement<T> {
+ Flex::row()
+ .with_child(
+ Svg::new("icons/magnifying_glass.svg")
+ .with_color(tab_theme.label.text.color)
+ .constrained()
+ .with_width(tab_theme.type_icon_width)
+ .aligned()
+ .contained()
+ .with_margin_right(tab_theme.spacing),
+ )
+ .with_child({
+ let tab_name: Option<Cow<_>> = self
+ .model
+ .read(cx)
+ .search_history
+ .current()
+ .as_ref()
+ .map(|query| {
+ let query_text = util::truncate_and_trailoff(query, MAX_TAB_TITLE_LEN);
+ query_text.into()
+ });
+ Label::new(
+ tab_name
+ .filter(|name| !name.is_empty())
+ .unwrap_or("Project search".into()),
+ tab_theme.label.clone(),
+ )
+ .aligned()
+ })
+ .into_any()
+ }
+
+ fn for_each_project_item(&self, cx: &AppContext, f: &mut dyn FnMut(usize, &dyn project::Item)) {
+ self.results_editor.for_each_project_item(cx, f)
+ }
+
+ fn is_singleton(&self, _: &AppContext) -> bool {
+ false
+ }
+
+ fn can_save(&self, _: &AppContext) -> bool {
+ true
+ }
+
+ fn is_dirty(&self, cx: &AppContext) -> bool {
+ self.results_editor.read(cx).is_dirty(cx)
+ }
+
+ fn has_conflict(&self, cx: &AppContext) -> bool {
+ self.results_editor.read(cx).has_conflict(cx)
+ }
+
+ fn save(
+ &mut self,
+ project: ModelHandle<Project>,
+ cx: &mut ViewContext<Self>,
+ ) -> Task<anyhow::Result<()>> {
+ self.results_editor
+ .update(cx, |editor, cx| editor.save(project, cx))
+ }
+
+ fn save_as(
+ &mut self,
+ _: ModelHandle<Project>,
+ _: PathBuf,
+ _: &mut ViewContext<Self>,
+ ) -> Task<anyhow::Result<()>> {
+ unreachable!("save_as should not have been called")
+ }
+
+ fn reload(
+ &mut self,
+ project: ModelHandle<Project>,
+ cx: &mut ViewContext<Self>,
+ ) -> Task<anyhow::Result<()>> {
+ self.results_editor
+ .update(cx, |editor, cx| editor.reload(project, cx))
+ }
+
+ fn clone_on_split(&self, _workspace_id: WorkspaceId, cx: &mut ViewContext<Self>) -> Option<Self>
+ where
+ Self: Sized,
+ {
+ let model = self.model.update(cx, |model, cx| model.clone(cx));
+ Some(Self::new(model, cx, None))
+ }
+
+ fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
+ self.results_editor
+ .update(cx, |editor, cx| editor.added_to_workspace(workspace, cx));
+ }
+
+ fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext<Self>) {
+ self.results_editor.update(cx, |editor, _| {
+ editor.set_nav_history(Some(nav_history));
+ });
+ }
+
+ fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
+ self.results_editor
+ .update(cx, |editor, cx| editor.navigate(data, cx))
+ }
+
+ fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
+ match event {
+ ViewEvent::UpdateTab => {
+ smallvec::smallvec![ItemEvent::UpdateBreadcrumbs, ItemEvent::UpdateTab]
+ }
+ ViewEvent::EditorEvent(editor_event) => Editor::to_item_events(editor_event),
+ ViewEvent::Dismiss => smallvec::smallvec![ItemEvent::CloseItem],
+ _ => SmallVec::new(),
+ }
+ }
+
+ fn breadcrumb_location(&self) -> ToolbarItemLocation {
+ if self.has_matches() {
+ ToolbarItemLocation::Secondary
+ } else {
+ ToolbarItemLocation::Hidden
+ }
+ }
+
+ fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
+ self.results_editor.breadcrumbs(theme, cx)
+ }
+
+ fn serialized_item_kind() -> Option<&'static str> {
+ None
+ }
+
+ fn deserialize(
+ _project: ModelHandle<Project>,
+ _workspace: WeakViewHandle<Workspace>,
+ _workspace_id: workspace::WorkspaceId,
+ _item_id: workspace::ItemId,
+ _cx: &mut ViewContext<Pane>,
+ ) -> Task<anyhow::Result<ViewHandle<Self>>> {
+ unimplemented!()
+ }
+}
+
+impl ProjectSearchView {
+ fn toggle_filters(&mut self, cx: &mut ViewContext<Self>) {
+ self.filters_enabled = !self.filters_enabled;
+ cx.update_global(|state: &mut ActiveSettings, cx| {
+ state.0.insert(
+ self.model.read(cx).project.downgrade(),
+ self.current_settings(),
+ );
+ });
+ }
+
+ fn current_settings(&self) -> ProjectSearchSettings {
+ ProjectSearchSettings {
+ search_options: self.search_options,
+ filters_enabled: self.filters_enabled,
+ current_mode: self.current_mode,
+ }
+ }
+ fn toggle_search_option(&mut self, option: SearchOptions, cx: &mut ViewContext<Self>) {
+ self.search_options.toggle(option);
+ cx.update_global(|state: &mut ActiveSettings, cx| {
+ state.0.insert(
+ self.model.read(cx).project.downgrade(),
+ self.current_settings(),
+ );
+ });
+ }
+
+ fn index_project(&mut self, cx: &mut ViewContext<Self>) {
+ if let Some(semantic_index) = SemanticIndex::global(cx) {
+ // Semantic search uses no options
+ self.search_options = SearchOptions::none();
+
+ let project = self.model.read(cx).project.clone();
+
+ semantic_index.update(cx, |semantic_index, cx| {
+ semantic_index
+ .index_project(project.clone(), cx)
+ .detach_and_log_err(cx);
+ });
+
+ self.semantic_state = Some(SemanticState {
+ index_status: semantic_index.read(cx).status(&project),
+ maintain_rate_limit: None,
+ _subscription: cx.observe(&semantic_index, Self::semantic_index_changed),
+ });
+ self.semantic_index_changed(semantic_index, cx);
+ }
+ }
+
+ fn semantic_index_changed(
+ &mut self,
+ semantic_index: ModelHandle<SemanticIndex>,
+ cx: &mut ViewContext<Self>,
+ ) {
+ let project = self.model.read(cx).project.clone();
+ if let Some(semantic_state) = self.semantic_state.as_mut() {
+ cx.notify();
+ semantic_state.index_status = semantic_index.read(cx).status(&project);
+ if let SemanticIndexStatus::Indexing {
+ rate_limit_expiry: Some(_),
+ ..
+ } = &semantic_state.index_status
+ {
+ if semantic_state.maintain_rate_limit.is_none() {
+ semantic_state.maintain_rate_limit =
+ Some(cx.spawn(|this, mut cx| async move {
+ loop {
+ cx.background().timer(Duration::from_secs(1)).await;
+ this.update(&mut cx, |_, cx| cx.notify()).log_err();
+ }
+ }));
+ return;
+ }
+ } else {
+ semantic_state.maintain_rate_limit = None;
+ }
+ }
+ }
+
+ fn clear_search(&mut self, cx: &mut ViewContext<Self>) {
+ self.model.update(cx, |model, cx| {
+ model.pending_search = None;
+ model.no_results = None;
+ model.match_ranges.clear();
+
+ model.excerpts.update(cx, |excerpts, cx| {
+ excerpts.clear(cx);
+ });
+ });
+ }
+
+ fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext<Self>) {
+ let previous_mode = self.current_mode;
+ if previous_mode == mode {
+ return;
+ }
+
+ self.clear_search(cx);
+ self.current_mode = mode;
+ self.active_match_index = None;
+
+ match mode {
+ SearchMode::Semantic => {
+ let has_permission = self.semantic_permissioned(cx);
+ self.active_match_index = None;
+ cx.spawn(|this, mut cx| async move {
+ let has_permission = has_permission.await?;
+
+ if !has_permission {
+ let mut answer = this.update(&mut cx, |this, cx| {
+ let project = this.model.read(cx).project.clone();
+ let project_name = project
+ .read(cx)
+ .worktree_root_names(cx)
+ .collect::<Vec<&str>>()
+ .join("/");
+ let is_plural =
+ project_name.chars().filter(|letter| *letter == '/').count() > 0;
+ let prompt_text = format!("Would you like to index the '{}' project{} for semantic search? This requires sending code to the OpenAI API", project_name,
+ if is_plural {
+ "s"
+ } else {""});
+ cx.prompt(
+ PromptLevel::Info,
+ prompt_text.as_str(),
+ &["Continue", "Cancel"],
+ )
+ })?;
+
+ if answer.next().await == Some(0) {
+ this.update(&mut cx, |this, _| {
+ this.semantic_permissioned = Some(true);
+ })?;
+ } else {
+ this.update(&mut cx, |this, cx| {
+ this.semantic_permissioned = Some(false);
+ debug_assert_ne!(previous_mode, SearchMode::Semantic, "Tried to re-enable semantic search mode after user modal was rejected");
+ this.activate_search_mode(previous_mode, cx);
+ })?;
+ return anyhow::Ok(());
+ }
+ }
+
+ this.update(&mut cx, |this, cx| {
+ this.index_project(cx);
+ })?;
+
+ anyhow::Ok(())
+ }).detach_and_log_err(cx);
+ }
+ SearchMode::Regex | SearchMode::Text => {
+ self.semantic_state = None;
+ self.active_match_index = None;
+ self.search(cx);
+ }
+ }
+
+ cx.update_global(|state: &mut ActiveSettings, cx| {
+ state.0.insert(
+ self.model.read(cx).project.downgrade(),
+ self.current_settings(),
+ );
+ });
+
+ cx.notify();
+ }
+ fn replace_next(&mut self, _: &ReplaceNext, cx: &mut ViewContext<Self>) {
+ let model = self.model.read(cx);
+ if let Some(query) = model.active_query.as_ref() {
+ if model.match_ranges.is_empty() {
+ return;
+ }
+ if let Some(active_index) = self.active_match_index {
+ let query = query.clone().with_replacement(self.replacement(cx));
+ self.results_editor.replace(
+ &(Box::new(model.match_ranges[active_index].clone()) as _),
+ &query,
+ cx,
+ );
+ self.select_match(Direction::Next, cx)
+ }
+ }
+ }
+ pub fn replacement(&self, cx: &AppContext) -> String {
+ self.replacement_editor.read(cx).text(cx)
+ }
+ fn replace_all(&mut self, _: &ReplaceAll, cx: &mut ViewContext<Self>) {
+ let model = self.model.read(cx);
+ if let Some(query) = model.active_query.as_ref() {
+ if model.match_ranges.is_empty() {
+ return;
+ }
+ if self.active_match_index.is_some() {
+ let query = query.clone().with_replacement(self.replacement(cx));
+ let matches = model
+ .match_ranges
+ .iter()
+ .map(|item| Box::new(item.clone()) as _)
+ .collect::<Vec<_>>();
+ for item in matches {
+ self.results_editor.replace(&item, &query, cx);
+ }
+ }
+ }
+ }
+
+ fn new(
+ model: ModelHandle<ProjectSearch>,
+ cx: &mut ViewContext<Self>,
+ settings: Option<ProjectSearchSettings>,
+ ) -> Self {
+ let project;
+ let excerpts;
+ let mut replacement_text = None;
+ let mut query_text = String::new();
+
+ // Read in settings if available
+ let (mut options, current_mode, filters_enabled) = if let Some(settings) = settings {
+ (
+ settings.search_options,
+ settings.current_mode,
+ settings.filters_enabled,
+ )
+ } else {
+ (SearchOptions::NONE, Default::default(), false)
+ };
+
+ {
+ let model = model.read(cx);
+ project = model.project.clone();
+ excerpts = model.excerpts.clone();
+ if let Some(active_query) = model.active_query.as_ref() {
+ query_text = active_query.as_str().to_string();
+ replacement_text = active_query.replacement().map(ToOwned::to_owned);
+ options = SearchOptions::from_query(active_query);
+ }
+ }
+ cx.observe(&model, |this, _, cx| this.model_changed(cx))
+ .detach();
+
+ let query_editor = cx.add_view(|cx| {
+ let mut editor = Editor::single_line(
+ Some(Arc::new(|theme| theme.search.editor.input.clone())),
+ cx,
+ );
+ editor.set_placeholder_text("Text search all files", cx);
+ editor.set_text(query_text, cx);
+ editor
+ });
+ // Subscribe to query_editor in order to reraise editor events for workspace item activation purposes
+ cx.subscribe(&query_editor, |_, _, event, cx| {
+ cx.emit(ViewEvent::EditorEvent(event.clone()))
+ })
+ .detach();
+ let replacement_editor = cx.add_view(|cx| {
+ let mut editor = Editor::single_line(
+ Some(Arc::new(|theme| theme.search.editor.input.clone())),
+ cx,
+ );
+ editor.set_placeholder_text("Replace in project..", cx);
+ if let Some(text) = replacement_text {
+ editor.set_text(text, cx);
+ }
+ editor
+ });
+ let results_editor = cx.add_view(|cx| {
+ let mut editor = Editor::for_multibuffer(excerpts, Some(project.clone()), cx);
+ editor.set_searchable(false);
+ editor
+ });
+ cx.observe(&results_editor, |_, _, cx| cx.emit(ViewEvent::UpdateTab))
+ .detach();
+
+ cx.subscribe(&results_editor, |this, _, event, cx| {
+ if matches!(event, editor::Event::SelectionsChanged { .. }) {
+ this.update_match_index(cx);
+ }
+ // Reraise editor events for workspace item activation purposes
+ cx.emit(ViewEvent::EditorEvent(event.clone()));
+ })
+ .detach();
+
+ let included_files_editor = cx.add_view(|cx| {
+ let mut editor = Editor::single_line(
+ Some(Arc::new(|theme| {
+ theme.search.include_exclude_editor.input.clone()
+ })),
+ cx,
+ );
+ editor.set_placeholder_text("Include: crates/**/*.toml", cx);
+
+ editor
+ });
+ // Subscribe to include_files_editor in order to reraise editor events for workspace item activation purposes
+ cx.subscribe(&included_files_editor, |_, _, event, cx| {
+ cx.emit(ViewEvent::EditorEvent(event.clone()))
+ })
+ .detach();
+
+ let excluded_files_editor = cx.add_view(|cx| {
+ let mut editor = Editor::single_line(
+ Some(Arc::new(|theme| {
+ theme.search.include_exclude_editor.input.clone()
+ })),
+ cx,
+ );
+ editor.set_placeholder_text("Exclude: vendor/*, *.lock", cx);
+
+ editor
+ });
+ // Subscribe to excluded_files_editor in order to reraise editor events for workspace item activation purposes
+ cx.subscribe(&excluded_files_editor, |_, _, event, cx| {
+ cx.emit(ViewEvent::EditorEvent(event.clone()))
+ })
+ .detach();
+
+ // Check if Worktrees have all been previously indexed
+ let mut this = ProjectSearchView {
+ replacement_editor,
+ search_id: model.read(cx).search_id,
+ model,
+ query_editor,
+ results_editor,
+ semantic_state: None,
+ semantic_permissioned: None,
+ search_options: options,
+ panels_with_errors: HashSet::new(),
+ active_match_index: None,
+ query_editor_was_focused: false,
+ included_files_editor,
+ excluded_files_editor,
+ filters_enabled,
+ current_mode,
+ replace_enabled: false,
+ };
+ this.model_changed(cx);
+ this
+ }
+
+ fn semantic_permissioned(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<bool>> {
+ if let Some(value) = self.semantic_permissioned {
+ return Task::ready(Ok(value));
+ }
+
+ SemanticIndex::global(cx)
+ .map(|semantic| {
+ let project = self.model.read(cx).project.clone();
+ semantic.update(cx, |this, cx| this.project_previously_indexed(&project, cx))
+ })
+ .unwrap_or(Task::ready(Ok(false)))
+ }
+ pub fn new_search_in_directory(
+ workspace: &mut Workspace,
+ dir_entry: &Entry,
+ cx: &mut ViewContext<Workspace>,
+ ) {
+ if !dir_entry.is_dir() {
+ return;
+ }
+ let Some(filter_str) = dir_entry.path.to_str() else {
+ return;
+ };
+
+ let model = cx.add_model(|cx| ProjectSearch::new(workspace.project().clone(), cx));
+ let search = cx.add_view(|cx| ProjectSearchView::new(model, cx, None));
+ workspace.add_item(Box::new(search.clone()), cx);
+ search.update(cx, |search, cx| {
+ search
+ .included_files_editor
+ .update(cx, |editor, cx| editor.set_text(filter_str, cx));
+ search.filters_enabled = true;
+ search.focus_query_editor(cx)
+ });
+ }
+
+ // Re-activate the most recently activated search or the most recent if it has been closed.
+ // If no search exists in the workspace, create a new one.
+ fn deploy(
+ workspace: &mut Workspace,
+ _: &workspace::NewSearch,
+ cx: &mut ViewContext<Workspace>,
+ ) {
+ // Clean up entries for dropped projects
+ cx.update_global(|state: &mut ActiveSearches, cx| {
+ state.0.retain(|project, _| project.is_upgradable(cx))
+ });
+
+ let active_search = cx
+ .global::<ActiveSearches>()
+ .0
+ .get(&workspace.project().downgrade());
+
+ let existing = active_search
+ .and_then(|active_search| {
+ workspace
+ .items_of_type::<ProjectSearchView>(cx)
+ .find(|search| search == active_search)
+ })
+ .or_else(|| workspace.item_of_type::<ProjectSearchView>(cx));
+
+ let query = workspace.active_item(cx).and_then(|item| {
+ let editor = item.act_as::<Editor>(cx)?;
+ let query = editor.query_suggestion(cx);
+ if query.is_empty() {
+ None
+ } else {
+ Some(query)
+ }
+ });
+
+ let search = if let Some(existing) = existing {
+ workspace.activate_item(&existing, cx);
+ existing
+ } else {
+ let settings = cx
+ .global::<ActiveSettings>()
+ .0
+ .get(&workspace.project().downgrade());
+
+ let settings = if let Some(settings) = settings {
+ Some(settings.clone())
+ } else {
+ None
+ };
+
+ let model = cx.add_model(|cx| ProjectSearch::new(workspace.project().clone(), cx));
+ let view = cx.add_view(|cx| ProjectSearchView::new(model, cx, settings));
+
+ workspace.add_item(Box::new(view.clone()), cx);
+ view
+ };
+
+ search.update(cx, |search, cx| {
+ if let Some(query) = query {
+ search.set_query(&query, cx);
+ }
+ search.focus_query_editor(cx)
+ });
+ }
+
+ fn search(&mut self, cx: &mut ViewContext<Self>) {
+ let mode = self.current_mode;
+ match mode {
+ SearchMode::Semantic => {
+ if self.semantic_state.is_some() {
+ if let Some(query) = self.build_search_query(cx) {
+ self.model
+ .update(cx, |model, cx| model.semantic_search(query.as_inner(), cx));
+ }
+ }
+ }
+
+ _ => {
+ if let Some(query) = self.build_search_query(cx) {
+ self.model.update(cx, |model, cx| model.search(query, cx));
+ }
+ }
+ }
+ }
+
+ 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::parse_path_matches(&self.included_files_editor.read(cx).text(cx)) {
+ Ok(included_files) => {
+ self.panels_with_errors.remove(&InputPanel::Include);
+ included_files
+ }
+ Err(_e) => {
+ self.panels_with_errors.insert(InputPanel::Include);
+ cx.notify();
+ return None;
+ }
+ };
+ let excluded_files =
+ match Self::parse_path_matches(&self.excluded_files_editor.read(cx).text(cx)) {
+ Ok(excluded_files) => {
+ self.panels_with_errors.remove(&InputPanel::Exclude);
+ excluded_files
+ }
+ Err(_e) => {
+ self.panels_with_errors.insert(InputPanel::Exclude);
+ cx.notify();
+ return None;
+ }
+ };
+ let current_mode = self.current_mode;
+ match current_mode {
+ SearchMode::Regex => {
+ match SearchQuery::regex(
+ text,
+ self.search_options.contains(SearchOptions::WHOLE_WORD),
+ self.search_options.contains(SearchOptions::CASE_SENSITIVE),
+ included_files,
+ excluded_files,
+ ) {
+ Ok(query) => {
+ self.panels_with_errors.remove(&InputPanel::Query);
+ Some(query)
+ }
+ Err(_e) => {
+ self.panels_with_errors.insert(InputPanel::Query);
+ cx.notify();
+ None
+ }
+ }
+ }
+ _ => match SearchQuery::text(
+ text,
+ self.search_options.contains(SearchOptions::WHOLE_WORD),
+ self.search_options.contains(SearchOptions::CASE_SENSITIVE),
+ included_files,
+ excluded_files,
+ ) {
+ Ok(query) => {
+ self.panels_with_errors.remove(&InputPanel::Query);
+ Some(query)
+ }
+ Err(_e) => {
+ self.panels_with_errors.insert(InputPanel::Query);
+ cx.notify();
+ None
+ }
+ },
+ }
+ }
+
+ fn parse_path_matches(text: &str) -> anyhow::Result<Vec<PathMatcher>> {
+ text.split(',')
+ .map(str::trim)
+ .filter(|maybe_glob_str| !maybe_glob_str.is_empty())
+ .map(|maybe_glob_str| {
+ PathMatcher::new(maybe_glob_str)
+ .with_context(|| format!("parsing {maybe_glob_str} as path 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();
+ let new_index = self.results_editor.update(cx, |editor, cx| {
+ editor.match_index_for_direction(&match_ranges, index, direction, 1, cx)
+ });
+
+ let range_to_select = match_ranges[new_index].clone();
+ self.results_editor.update(cx, |editor, cx| {
+ let range_to_select = editor.range_for_match(&range_to_select);
+ editor.unfold_ranges([range_to_select.clone()], false, true, cx);
+ editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.select_ranges([range_to_select])
+ });
+ });
+ }
+ }
+
+ fn focus_query_editor(&mut self, cx: &mut ViewContext<Self>) {
+ self.query_editor.update(cx, |query_editor, cx| {
+ query_editor.select_all(&SelectAll, cx);
+ });
+ self.query_editor_was_focused = true;
+ cx.focus(&self.query_editor);
+ }
+
+ fn set_query(&mut self, query: &str, cx: &mut ViewContext<Self>) {
+ self.query_editor
+ .update(cx, |query_editor, cx| query_editor.set_text(query, cx));
+ }
+
+ fn focus_results_editor(&mut self, cx: &mut ViewContext<Self>) {
+ self.query_editor.update(cx, |query_editor, cx| {
+ let cursor = query_editor.selections.newest_anchor().head();
+ query_editor.change_selections(None, cx, |s| s.select_ranges([cursor.clone()..cursor]));
+ });
+ self.query_editor_was_focused = false;
+ cx.focus(&self.results_editor);
+ }
+
+ fn model_changed(&mut self, cx: &mut ViewContext<Self>) {
+ let match_ranges = self.model.read(cx).match_ranges.clone();
+ if match_ranges.is_empty() {
+ self.active_match_index = None;
+ } else {
+ self.active_match_index = Some(0);
+ self.update_match_index(cx);
+ let prev_search_id = mem::replace(&mut self.search_id, self.model.read(cx).search_id);
+ let is_new_search = self.search_id != prev_search_id;
+ self.results_editor.update(cx, |editor, cx| {
+ if is_new_search {
+ let range_to_select = match_ranges
+ .first()
+ .clone()
+ .map(|range| editor.range_for_match(range));
+ editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.select_ranges(range_to_select)
+ });
+ }
+ editor.highlight_background::<Self>(
+ match_ranges,
+ |theme| theme.search.match_background,
+ cx,
+ );
+ });
+ if is_new_search && self.query_editor.is_focused(cx) {
+ self.focus_results_editor(cx);
+ }
+ }
+
+ cx.emit(ViewEvent::UpdateTab);
+ cx.notify();
+ }
+
+ fn update_match_index(&mut self, cx: &mut ViewContext<Self>) {
+ let results_editor = self.results_editor.read(cx);
+ let new_index = active_match_index(
+ &self.model.read(cx).match_ranges,
+ &results_editor.selections.newest_anchor().head(),
+ &results_editor.buffer().read(cx).snapshot(cx),
+ );
+ if self.active_match_index != new_index {
+ self.active_match_index = new_index;
+ cx.notify();
+ }
+ }
+
+ pub fn has_matches(&self) -> bool {
+ self.active_match_index.is_some()
+ }
+
+ 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.results_editor.is_focused(cx)
+ && !search_view.model.read(cx).match_ranges.is_empty()
+ {
+ return search_view.focus_results_editor(cx);
+ }
+ });
+ }
+
+ cx.propagate_action();
+ }
+}
+
+impl Default for ProjectSearchBar {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl ProjectSearchBar {
+ pub fn new() -> Self {
+ Self {
+ active_project_search: Default::default(),
+ subscription: Default::default(),
+ }
+ }
+ fn cycle_mode(workspace: &mut Workspace, _: &CycleMode, cx: &mut ViewContext<Workspace>) {
+ if let Some(search_view) = workspace
+ .active_item(cx)
+ .and_then(|item| item.downcast::<ProjectSearchView>())
+ {
+ search_view.update(cx, |this, cx| {
+ let new_mode =
+ crate::mode::next_mode(&this.current_mode, SemanticIndex::enabled(cx));
+ this.activate_search_mode(new_mode, cx);
+ cx.focus(&this.query_editor);
+ })
+ }
+ }
+ fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
+ let mut should_propagate = true;
+ if let Some(search_view) = self.active_project_search.as_ref() {
+ search_view.update(cx, |search_view, cx| {
+ if !search_view.replacement_editor.is_focused(cx) {
+ should_propagate = false;
+ search_view.search(cx);
+ }
+ });
+ }
+ if should_propagate {
+ cx.propagate_action();
+ }
+ }
+
+ fn search_in_new(workspace: &mut Workspace, _: &SearchInNew, cx: &mut ViewContext<Workspace>) {
+ if let Some(search_view) = workspace
+ .active_item(cx)
+ .and_then(|item| item.downcast::<ProjectSearchView>())
+ {
+ let new_query = search_view.update(cx, |search_view, cx| {
+ let new_query = search_view.build_search_query(cx);
+ if new_query.is_some() {
+ if let Some(old_query) = search_view.model.read(cx).active_query.clone() {
+ search_view.query_editor.update(cx, |editor, cx| {
+ editor.set_text(old_query.as_str(), cx);
+ });
+ search_view.search_options = SearchOptions::from_query(&old_query);
+ }
+ }
+ new_query
+ });
+ if let Some(new_query) = new_query {
+ let model = cx.add_model(|cx| {
+ let mut model = ProjectSearch::new(workspace.project().clone(), cx);
+ model.search(new_query, cx);
+ model
+ });
+ workspace.add_item(
+ Box::new(cx.add_view(|cx| ProjectSearchView::new(model, cx, None))),
+ cx,
+ );
+ }
+ }
+ }
+
+ fn select_next_match(pane: &mut Pane, _: &SelectNextMatch, cx: &mut ViewContext<Pane>) {
+ if let Some(search_view) = pane
+ .active_item()
+ .and_then(|item| item.downcast::<ProjectSearchView>())
+ {
+ search_view.update(cx, |view, cx| view.select_match(Direction::Next, cx));
+ } else {
+ cx.propagate_action();
+ }
+ }
+
+ fn replace_next(pane: &mut Pane, _: &ReplaceNext, cx: &mut ViewContext<Pane>) {
+ if let Some(search_view) = pane
+ .active_item()
+ .and_then(|item| item.downcast::<ProjectSearchView>())
+ {
+ search_view.update(cx, |view, cx| view.replace_next(&ReplaceNext, cx));
+ } else {
+ cx.propagate_action();
+ }
+ }
+ fn replace_all(pane: &mut Pane, _: &ReplaceAll, cx: &mut ViewContext<Pane>) {
+ if let Some(search_view) = pane
+ .active_item()
+ .and_then(|item| item.downcast::<ProjectSearchView>())
+ {
+ search_view.update(cx, |view, cx| view.replace_all(&ReplaceAll, cx));
+ } else {
+ cx.propagate_action();
+ }
+ }
+ fn select_prev_match(pane: &mut Pane, _: &SelectPrevMatch, cx: &mut ViewContext<Pane>) {
+ if let Some(search_view) = pane
+ .active_item()
+ .and_then(|item| item.downcast::<ProjectSearchView>())
+ {
+ search_view.update(cx, |view, cx| view.select_match(Direction::Prev, cx));
+ } else {
+ cx.propagate_action();
+ }
+ }
+
+ fn tab(&mut self, _: &editor::Tab, cx: &mut ViewContext<Self>) {
+ self.cycle_field(Direction::Next, cx);
+ }
+
+ fn tab_previous(&mut self, _: &editor::TabPrev, cx: &mut ViewContext<Self>) {
+ self.cycle_field(Direction::Prev, cx);
+ }
+
+ fn cycle_field(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
+ let active_project_search = match &self.active_project_search {
+ Some(active_project_search) => active_project_search,
+
+ None => {
+ cx.propagate_action();
+ return;
+ }
+ };
+
+ active_project_search.update(cx, |project_view, cx| {
+ let mut views = vec![&project_view.query_editor];
+ if project_view.filters_enabled {
+ views.extend([
+ &project_view.included_files_editor,
+ &project_view.excluded_files_editor,
+ ]);
+ }
+ if project_view.replace_enabled {
+ views.push(&project_view.replacement_editor);
+ }
+ let current_index = match views
+ .iter()
+ .enumerate()
+ .find(|(_, view)| view.is_focused(cx))
+ {
+ Some((index, _)) => index,
+
+ None => {
+ cx.propagate_action();
+ return;
+ }
+ };
+
+ let new_index = match direction {
+ Direction::Next => (current_index + 1) % views.len(),
+ Direction::Prev if current_index == 0 => views.len() - 1,
+ Direction::Prev => (current_index - 1) % views.len(),
+ };
+ cx.focus(views[new_index]);
+ });
+ }
+
+ fn toggle_search_option(&mut self, option: SearchOptions, cx: &mut ViewContext<Self>) -> bool {
+ if let Some(search_view) = self.active_project_search.as_ref() {
+ search_view.update(cx, |search_view, cx| {
+ search_view.toggle_search_option(option, cx);
+ search_view.search(cx);
+ });
+
+ cx.notify();
+ true
+ } else {
+ false
+ }
+ }
+ fn toggle_replace(&mut self, _: &ToggleReplace, cx: &mut ViewContext<Self>) {
+ if let Some(search) = &self.active_project_search {
+ search.update(cx, |this, cx| {
+ this.replace_enabled = !this.replace_enabled;
+ if !this.replace_enabled {
+ cx.focus(&this.query_editor);
+ }
+ cx.notify();
+ });
+ }
+ }
+ fn toggle_replace_on_a_pane(pane: &mut Pane, _: &ToggleReplace, cx: &mut ViewContext<Pane>) {
+ let mut should_propagate = true;
+ if let Some(search_view) = pane
+ .active_item()
+ .and_then(|item| item.downcast::<ProjectSearchView>())
+ {
+ search_view.update(cx, |this, cx| {
+ should_propagate = false;
+ this.replace_enabled = !this.replace_enabled;
+ if !this.replace_enabled {
+ cx.focus(&this.query_editor);
+ }
+ cx.notify();
+ });
+ }
+ if should_propagate {
+ cx.propagate_action();
+ }
+ }
+ fn activate_text_mode(pane: &mut Pane, _: &ActivateTextMode, cx: &mut ViewContext<Pane>) {
+ if let Some(search_view) = pane
+ .active_item()
+ .and_then(|item| item.downcast::<ProjectSearchView>())
+ {
+ search_view.update(cx, |view, cx| {
+ view.activate_search_mode(SearchMode::Text, cx)
+ });
+ } else {
+ cx.propagate_action();
+ }
+ }
+
+ fn activate_regex_mode(pane: &mut Pane, _: &ActivateRegexMode, cx: &mut ViewContext<Pane>) {
+ if let Some(search_view) = pane
+ .active_item()
+ .and_then(|item| item.downcast::<ProjectSearchView>())
+ {
+ search_view.update(cx, |view, cx| {
+ view.activate_search_mode(SearchMode::Regex, cx)
+ });
+ } else {
+ cx.propagate_action();
+ }
+ }
+
+ fn activate_semantic_mode(
+ pane: &mut Pane,
+ _: &ActivateSemanticMode,
+ cx: &mut ViewContext<Pane>,
+ ) {
+ if SemanticIndex::enabled(cx) {
+ if let Some(search_view) = pane
+ .active_item()
+ .and_then(|item| item.downcast::<ProjectSearchView>())
+ {
+ search_view.update(cx, |view, cx| {
+ view.activate_search_mode(SearchMode::Semantic, cx)
+ });
+ } else {
+ cx.propagate_action();
+ }
+ }
+ }
+
+ fn toggle_filters(&mut self, cx: &mut ViewContext<Self>) -> bool {
+ if let Some(search_view) = self.active_project_search.as_ref() {
+ search_view.update(cx, |search_view, cx| {
+ search_view.toggle_filters(cx);
+ search_view
+ .included_files_editor
+ .update(cx, |_, cx| cx.notify());
+ search_view
+ .excluded_files_editor
+ .update(cx, |_, cx| cx.notify());
+ cx.refresh_windows();
+ cx.notify();
+ });
+ cx.notify();
+ true
+ } else {
+ false
+ }
+ }
+
+ fn activate_search_mode(&self, mode: SearchMode, cx: &mut ViewContext<Self>) {
+ // Update Current Mode
+ if let Some(search_view) = self.active_project_search.as_ref() {
+ search_view.update(cx, |search_view, cx| {
+ search_view.activate_search_mode(mode, cx);
+ });
+ cx.notify();
+ }
+ }
+
+ fn is_option_enabled(&self, option: SearchOptions, cx: &AppContext) -> bool {
+ if let Some(search) = self.active_project_search.as_ref() {
+ search.read(cx).search_options.contains(option)
+ } else {
+ false
+ }
+ }
+
+ fn next_history_query(&mut self, _: &NextHistoryQuery, cx: &mut ViewContext<Self>) {
+ if let Some(search_view) = self.active_project_search.as_ref() {
+ search_view.update(cx, |search_view, cx| {
+ let new_query = search_view.model.update(cx, |model, _| {
+ if let Some(new_query) = model.search_history.next().map(str::to_string) {
+ new_query
+ } else {
+ model.search_history.reset_selection();
+ String::new()
+ }
+ });
+ search_view.set_query(&new_query, cx);
+ });
+ }
+ }
+
+ fn previous_history_query(&mut self, _: &PreviousHistoryQuery, cx: &mut ViewContext<Self>) {
+ if let Some(search_view) = self.active_project_search.as_ref() {
+ search_view.update(cx, |search_view, cx| {
+ if search_view.query_editor.read(cx).text(cx).is_empty() {
+ if let Some(new_query) = search_view
+ .model
+ .read(cx)
+ .search_history
+ .current()
+ .map(str::to_string)
+ {
+ search_view.set_query(&new_query, cx);
+ return;
+ }
+ }
+
+ if let Some(new_query) = search_view.model.update(cx, |model, _| {
+ model.search_history.previous().map(str::to_string)
+ }) {
+ search_view.set_query(&new_query, cx);
+ }
+ });
+ }
+ }
+}
+
+impl Entity for ProjectSearchBar {
+ type Event = ();
+}
+
+impl View for ProjectSearchBar {
+ fn ui_name() -> &'static str {
+ "ProjectSearchBar"
+ }
+
+ fn update_keymap_context(
+ &self,
+ keymap: &mut gpui::keymap_matcher::KeymapContext,
+ cx: &AppContext,
+ ) {
+ Self::reset_to_default_keymap_context(keymap);
+ let in_replace = self
+ .active_project_search
+ .as_ref()
+ .map(|search| {
+ search
+ .read(cx)
+ .replacement_editor
+ .read_with(cx, |_, cx| cx.is_self_focused())
+ })
+ .flatten()
+ .unwrap_or(false);
+ if in_replace {
+ keymap.add_identifier("in_replace");
+ }
+ }
+
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+ if let Some(_search) = self.active_project_search.as_ref() {
+ let search = _search.read(cx);
+ let theme = theme::current(cx).clone();
+ let query_container_style = if search.panels_with_errors.contains(&InputPanel::Query) {
+ theme.search.invalid_editor
+ } else {
+ theme.search.editor.input.container
+ };
+
+ let search = _search.read(cx);
+ let filter_button = render_option_button_icon(
+ search.filters_enabled,
+ "icons/filter.svg",
+ 0,
+ "Toggle filters",
+ Box::new(ToggleFilters),
+ move |_, this, cx| {
+ this.toggle_filters(cx);
+ },
+ cx,
+ );
+
+ let search = _search.read(cx);
+ let is_semantic_available = SemanticIndex::enabled(cx);
+ let is_semantic_disabled = search.semantic_state.is_none();
+ let icon_style = theme.search.editor_icon.clone();
+ let is_active = search.active_match_index.is_some();
+
+ let render_option_button_icon = |path, option, cx: &mut ViewContext<Self>| {
+ crate::search_bar::render_option_button_icon(
+ self.is_option_enabled(option, cx),
+ path,
+ option.bits as usize,
+ format!("Toggle {}", option.label()),
+ option.to_toggle_action(),
+ move |_, this, cx| {
+ this.toggle_search_option(option, cx);
+ },
+ cx,
+ )
+ };
+ let case_sensitive = is_semantic_disabled.then(|| {
+ render_option_button_icon(
+ "icons/case_insensitive.svg",
+ SearchOptions::CASE_SENSITIVE,
+ cx,
+ )
+ });
+
+ let whole_word = is_semantic_disabled.then(|| {
+ render_option_button_icon("icons/word_search.svg", SearchOptions::WHOLE_WORD, cx)
+ });
+
+ let search_button_for_mode = |mode, side, cx: &mut ViewContext<ProjectSearchBar>| {
+ let is_active = if let Some(search) = self.active_project_search.as_ref() {
+ let search = search.read(cx);
+ search.current_mode == mode
+ } else {
+ false
+ };
+ render_search_mode_button(
+ mode,
+ side,
+ is_active,
+ move |_, this, cx| {
+ this.activate_search_mode(mode, cx);
+ },
+ cx,
+ )
+ };
+
+ let search = _search.read(cx);
+
+ let include_container_style =
+ if search.panels_with_errors.contains(&InputPanel::Include) {
+ theme.search.invalid_include_exclude_editor
+ } else {
+ theme.search.include_exclude_editor.input.container
+ };
+
+ let exclude_container_style =
+ if search.panels_with_errors.contains(&InputPanel::Exclude) {
+ theme.search.invalid_include_exclude_editor
+ } else {
+ theme.search.include_exclude_editor.input.container
+ };
+
+ let matches = search.active_match_index.map(|match_ix| {
+ Label::new(
+ format!(
+ "{}/{}",
+ match_ix + 1,
+ search.model.read(cx).match_ranges.len()
+ ),
+ theme.search.match_index.text.clone(),
+ )
+ .contained()
+ .with_style(theme.search.match_index.container)
+ .aligned()
+ });
+ let should_show_replace_input = search.replace_enabled;
+ let replacement = should_show_replace_input.then(|| {
+ Flex::row()
+ .with_child(
+ Svg::for_style(theme.search.replace_icon.clone().icon)
+ .contained()
+ .with_style(theme.search.replace_icon.clone().container),
+ )
+ .with_child(ChildView::new(&search.replacement_editor, cx).flex(1., true))
+ .align_children_center()
+ .flex(1., true)
+ .contained()
+ .with_style(query_container_style)
+ .constrained()
+ .with_min_width(theme.search.editor.min_width)
+ .with_max_width(theme.search.editor.max_width)
+ .with_height(theme.search.search_bar_row_height)
+ .flex(1., false)
+ });
+ let replace_all = should_show_replace_input.then(|| {
+ super::replace_action(
+ ReplaceAll,
+ "Replace all",
+ "icons/replace_all.svg",
+ theme.tooltip.clone(),
+ theme.search.action_button.clone(),
+ )
+ });
+ let replace_next = should_show_replace_input.then(|| {
+ super::replace_action(
+ ReplaceNext,
+ "Replace next",
+ "icons/replace_next.svg",
+ theme.tooltip.clone(),
+ theme.search.action_button.clone(),
+ )
+ });
+ let query_column = Flex::column()
+ .with_spacing(theme.search.search_row_spacing)
+ .with_child(
+ Flex::row()
+ .with_child(
+ Svg::for_style(icon_style.icon)
+ .contained()
+ .with_style(icon_style.container),
+ )
+ .with_child(ChildView::new(&search.query_editor, cx).flex(1., true))
+ .with_child(
+ Flex::row()
+ .with_child(filter_button)
+ .with_children(case_sensitive)
+ .with_children(whole_word)
+ .flex(1., false)
+ .constrained()
+ .contained(),
+ )
+ .align_children_center()
+ .contained()
+ .with_style(query_container_style)
+ .constrained()
+ .with_min_width(theme.search.editor.min_width)
+ .with_max_width(theme.search.editor.max_width)
+ .with_height(theme.search.search_bar_row_height)
+ .flex(1., false),
+ )
+ .with_children(search.filters_enabled.then(|| {
+ Flex::row()
+ .with_child(
+ ChildView::new(&search.included_files_editor, cx)
+ .contained()
+ .with_style(include_container_style)
+ .constrained()
+ .with_height(theme.search.search_bar_row_height)
+ .flex(1., true),
+ )
+ .with_child(
+ ChildView::new(&search.excluded_files_editor, cx)
+ .contained()
+ .with_style(exclude_container_style)
+ .constrained()
+ .with_height(theme.search.search_bar_row_height)
+ .flex(1., true),
+ )
+ .constrained()
+ .with_min_width(theme.search.editor.min_width)
+ .with_max_width(theme.search.editor.max_width)
+ .flex(1., false)
+ }))
+ .flex(1., false);
+ let switches_column = Flex::row()
+ .align_children_center()
+ .with_child(super::toggle_replace_button(
+ search.replace_enabled,
+ theme.tooltip.clone(),
+ theme.search.option_button_component.clone(),
+ ))
+ .constrained()
+ .with_height(theme.search.search_bar_row_height)
+ .contained()
+ .with_style(theme.search.option_button_group);
+ let mode_column =
+ Flex::row()
+ .with_child(search_button_for_mode(
+ SearchMode::Text,
+ Some(Side::Left),
+ cx,
+ ))
+ .with_child(search_button_for_mode(
+ SearchMode::Regex,
+ if is_semantic_available {
+ None
+ } else {
+ Some(Side::Right)
+ },
+ cx,
+ ))
+ .with_children(is_semantic_available.then(|| {
+ search_button_for_mode(SearchMode::Semantic, Some(Side::Right), cx)
+ }))
+ .contained()
+ .with_style(theme.search.modes_container);
+
+ let nav_button_for_direction = |label, direction, cx: &mut ViewContext<Self>| {
+ render_nav_button(
+ label,
+ direction,
+ is_active,
+ move |_, this, cx| {
+ if let Some(search) = this.active_project_search.as_ref() {
+ search.update(cx, |search, cx| search.select_match(direction, cx));
+ }
+ },
+ cx,
+ )
+ };
+
+ let nav_column = Flex::row()
+ .with_children(replace_next)
+ .with_children(replace_all)
+ .with_child(Flex::row().with_children(matches))
+ .with_child(nav_button_for_direction("<", Direction::Prev, cx))
+ .with_child(nav_button_for_direction(">", Direction::Next, cx))
+ .constrained()
+ .with_height(theme.search.search_bar_row_height)
+ .flex_float();
+
+ Flex::row()
+ .with_child(query_column)
+ .with_child(mode_column)
+ .with_child(switches_column)
+ .with_children(replacement)
+ .with_child(nav_column)
+ .contained()
+ .with_style(theme.search.container)
+ .into_any_named("project search")
+ } else {
+ Empty::new().into_any()
+ }
+ }
+}
+
+impl ToolbarItemView for ProjectSearchBar {
+ fn set_active_pane_item(
+ &mut self,
+ active_pane_item: Option<&dyn ItemHandle>,
+ cx: &mut ViewContext<Self>,
+ ) -> ToolbarItemLocation {
+ cx.notify();
+ self.subscription = None;
+ self.active_project_search = None;
+ if let Some(search) = active_pane_item.and_then(|i| i.downcast::<ProjectSearchView>()) {
+ search.update(cx, |search, cx| {
+ if search.current_mode == SearchMode::Semantic {
+ search.index_project(cx);
+ }
+ });
+
+ self.subscription = Some(cx.observe(&search, |_, _, cx| cx.notify()));
+ self.active_project_search = Some(search);
+ ToolbarItemLocation::PrimaryLeft {
+ flex: Some((1., true)),
+ }
+ } else {
+ ToolbarItemLocation::Hidden
+ }
+ }
+
+ fn row_count(&self, cx: &ViewContext<Self>) -> usize {
+ if let Some(search) = self.active_project_search.as_ref() {
+ if search.read(cx).filters_enabled {
+ return 2;
+ }
+ }
+ 1
+ }
+}
+
+#[cfg(test)]
+pub mod tests {
+ use super::*;
+ use editor::DisplayPoint;
+ use gpui::{color::Color, executor::Deterministic, TestAppContext};
+ use project::FakeFs;
+ use semantic_index::semantic_index_settings::SemanticIndexSettings;
+ use serde_json::json;
+ use settings::SettingsStore;
+ use std::sync::Arc;
+ use theme::ThemeSettings;
+
+ #[gpui::test]
+ async fn test_project_search(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
+ init_test(cx);
+
+ let fs = FakeFs::new(cx.background());
+ fs.insert_tree(
+ "/dir",
+ json!({
+ "one.rs": "const ONE: usize = 1;",
+ "two.rs": "const TWO: usize = one::ONE + one::ONE;",
+ "three.rs": "const THREE: usize = one::ONE + two::TWO;",
+ "four.rs": "const FOUR: usize = one::ONE + three::THREE;",
+ }),
+ )
+ .await;
+ let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
+ let search = cx.add_model(|cx| ProjectSearch::new(project, cx));
+ let search_view = cx
+ .add_window(|cx| ProjectSearchView::new(search.clone(), cx, None))
+ .root(cx);
+
+ search_view.update(cx, |search_view, cx| {
+ search_view
+ .query_editor
+ .update(cx, |query_editor, cx| query_editor.set_text("TWO", cx));
+ search_view.search(cx);
+ });
+ deterministic.run_until_parked();
+ search_view.update(cx, |search_view, cx| {
+ assert_eq!(
+ search_view
+ .results_editor
+ .update(cx, |editor, cx| editor.display_text(cx)),
+ "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;"
+ );
+ assert_eq!(
+ search_view
+ .results_editor
+ .update(cx, |editor, cx| editor.all_text_background_highlights(cx)),
+ &[
+ (
+ DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35),
+ Color::red()
+ ),
+ (
+ DisplayPoint::new(2, 37)..DisplayPoint::new(2, 40),
+ Color::red()
+ ),
+ (
+ DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9),
+ Color::red()
+ )
+ ]
+ );
+ assert_eq!(search_view.active_match_index, Some(0));
+ assert_eq!(
+ search_view
+ .results_editor
+ .update(cx, |editor, cx| editor.selections.display_ranges(cx)),
+ [DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35)]
+ );
+
+ search_view.select_match(Direction::Next, cx);
+ });
+
+ search_view.update(cx, |search_view, cx| {
+ assert_eq!(search_view.active_match_index, Some(1));
+ assert_eq!(
+ search_view
+ .results_editor
+ .update(cx, |editor, cx| editor.selections.display_ranges(cx)),
+ [DisplayPoint::new(2, 37)..DisplayPoint::new(2, 40)]
+ );
+ search_view.select_match(Direction::Next, cx);
+ });
+
+ search_view.update(cx, |search_view, cx| {
+ assert_eq!(search_view.active_match_index, Some(2));
+ assert_eq!(
+ search_view
+ .results_editor
+ .update(cx, |editor, cx| editor.selections.display_ranges(cx)),
+ [DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9)]
+ );
+ search_view.select_match(Direction::Next, cx);
+ });
+
+ search_view.update(cx, |search_view, cx| {
+ assert_eq!(search_view.active_match_index, Some(0));
+ assert_eq!(
+ search_view
+ .results_editor
+ .update(cx, |editor, cx| editor.selections.display_ranges(cx)),
+ [DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35)]
+ );
+ search_view.select_match(Direction::Prev, cx);
+ });
+
+ search_view.update(cx, |search_view, cx| {
+ assert_eq!(search_view.active_match_index, Some(2));
+ assert_eq!(
+ search_view
+ .results_editor
+ .update(cx, |editor, cx| editor.selections.display_ranges(cx)),
+ [DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9)]
+ );
+ search_view.select_match(Direction::Prev, cx);
+ });
+
+ search_view.update(cx, |search_view, cx| {
+ assert_eq!(search_view.active_match_index, Some(1));
+ assert_eq!(
+ search_view
+ .results_editor
+ .update(cx, |editor, cx| editor.selections.display_ranges(cx)),
+ [DisplayPoint::new(2, 37)..DisplayPoint::new(2, 40)]
+ );
+ });
+ }
+
+ #[gpui::test]
+ async fn test_project_search_focus(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
+ init_test(cx);
+
+ let fs = FakeFs::new(cx.background());
+ fs.insert_tree(
+ "/dir",
+ json!({
+ "one.rs": "const ONE: usize = 1;",
+ "two.rs": "const TWO: usize = one::ONE + one::ONE;",
+ "three.rs": "const THREE: usize = one::ONE + two::TWO;",
+ "four.rs": "const FOUR: usize = one::ONE + three::THREE;",
+ }),
+ )
+ .await;
+ let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
+ let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+ let workspace = window.root(cx);
+
+ let active_item = cx.read(|cx| {
+ workspace
+ .read(cx)
+ .active_pane()
+ .read(cx)
+ .active_item()
+ .and_then(|item| item.downcast::<ProjectSearchView>())
+ });
+ assert!(
+ active_item.is_none(),
+ "Expected no search panel to be active, but got: {active_item:?}"
+ );
+
+ workspace.update(cx, |workspace, cx| {
+ ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx)
+ });
+
+ let Some(search_view) = cx.read(|cx| {
+ workspace
+ .read(cx)
+ .active_pane()
+ .read(cx)
+ .active_item()
+ .and_then(|item| item.downcast::<ProjectSearchView>())
+ }) else {
+ panic!("Search view expected to appear after new search event trigger")
+ };
+ let search_view_id = search_view.id();
+
+ cx.spawn(|mut cx| async move {
+ window.dispatch_action(search_view_id, &ToggleFocus, &mut cx);
+ })
+ .detach();
+ deterministic.run_until_parked();
+ search_view.update(cx, |search_view, cx| {
+ assert!(
+ search_view.query_editor.is_focused(cx),
+ "Empty search view should be focused after the toggle focus event: no results panel to focus on",
+ );
+ });
+
+ search_view.update(cx, |search_view, cx| {
+ let query_editor = &search_view.query_editor;
+ assert!(
+ query_editor.is_focused(cx),
+ "Search view should be focused after the new search view is activated",
+ );
+ let query_text = query_editor.read(cx).text(cx);
+ assert!(
+ query_text.is_empty(),
+ "New search query should be empty but got '{query_text}'",
+ );
+ let results_text = search_view
+ .results_editor
+ .update(cx, |editor, cx| editor.display_text(cx));
+ assert!(
+ results_text.is_empty(),
+ "Empty search view should have no results but got '{results_text}'"
+ );
+ });
+
+ search_view.update(cx, |search_view, cx| {
+ search_view.query_editor.update(cx, |query_editor, cx| {
+ query_editor.set_text("sOMETHINGtHATsURELYdOESnOTeXIST", cx)
+ });
+ search_view.search(cx);
+ });
+ deterministic.run_until_parked();
+ search_view.update(cx, |search_view, cx| {
+ let results_text = search_view
+ .results_editor
+ .update(cx, |editor, cx| editor.display_text(cx));
+ assert!(
+ results_text.is_empty(),
+ "Search view for mismatching query should have no results but got '{results_text}'"
+ );
+ assert!(
+ search_view.query_editor.is_focused(cx),
+ "Search view should be focused after mismatching query had been used in search",
+ );
+ });
+ cx.spawn(
+ |mut cx| async move { window.dispatch_action(search_view_id, &ToggleFocus, &mut cx) },
+ )
+ .detach();
+ deterministic.run_until_parked();
+ search_view.update(cx, |search_view, cx| {
+ assert!(
+ search_view.query_editor.is_focused(cx),
+ "Search view with mismatching query should be focused after the toggle focus event: still no results panel to focus on",
+ );
+ });
+
+ search_view.update(cx, |search_view, cx| {
+ search_view
+ .query_editor
+ .update(cx, |query_editor, cx| query_editor.set_text("TWO", cx));
+ search_view.search(cx);
+ });
+ deterministic.run_until_parked();
+ search_view.update(cx, |search_view, cx| {
+ assert_eq!(
+ search_view
+ .results_editor
+ .update(cx, |editor, cx| editor.display_text(cx)),
+ "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;",
+ "Search view results should match the query"
+ );
+ assert!(
+ search_view.results_editor.is_focused(cx),
+ "Search view with mismatching query should be focused after search results are available",
+ );
+ });
+ cx.spawn(|mut cx| async move {
+ window.dispatch_action(search_view_id, &ToggleFocus, &mut cx);
+ })
+ .detach();
+ deterministic.run_until_parked();
+ search_view.update(cx, |search_view, cx| {
+ assert!(
+ search_view.results_editor.is_focused(cx),
+ "Search view with matching query should still have its results editor focused after the toggle focus event",
+ );
+ });
+
+ workspace.update(cx, |workspace, cx| {
+ ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx)
+ });
+ search_view.update(cx, |search_view, cx| {
+ assert_eq!(search_view.query_editor.read(cx).text(cx), "two", "Query should be updated to first search result after search view 2nd open in a row");
+ assert_eq!(
+ search_view
+ .results_editor
+ .update(cx, |editor, cx| editor.display_text(cx)),
+ "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;",
+ "Results should be unchanged after search view 2nd open in a row"
+ );
+ assert!(
+ search_view.query_editor.is_focused(cx),
+ "Focus should be moved into query editor again after search view 2nd open in a row"
+ );
+ });
+
+ cx.spawn(|mut cx| async move {
+ window.dispatch_action(search_view_id, &ToggleFocus, &mut cx);
+ })
+ .detach();
+ deterministic.run_until_parked();
+ search_view.update(cx, |search_view, cx| {
+ assert!(
+ search_view.results_editor.is_focused(cx),
+ "Search view with matching query should switch focus to the results editor after the toggle focus event",
+ );
+ });
+ }
+
+ #[gpui::test]
+ async fn test_new_project_search_in_directory(
+ deterministic: Arc<Deterministic>,
+ cx: &mut TestAppContext,
+ ) {
+ init_test(cx);
+
+ let fs = FakeFs::new(cx.background());
+ fs.insert_tree(
+ "/dir",
+ json!({
+ "a": {
+ "one.rs": "const ONE: usize = 1;",
+ "two.rs": "const TWO: usize = one::ONE + one::ONE;",
+ },
+ "b": {
+ "three.rs": "const THREE: usize = one::ONE + two::TWO;",
+ "four.rs": "const FOUR: usize = one::ONE + three::THREE;",
+ },
+ }),
+ )
+ .await;
+ let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
+ let worktree_id = project.read_with(cx, |project, cx| {
+ project.worktrees(cx).next().unwrap().read(cx).id()
+ });
+ let workspace = cx
+ .add_window(|cx| Workspace::test_new(project, cx))
+ .root(cx);
+
+ let active_item = cx.read(|cx| {
+ workspace
+ .read(cx)
+ .active_pane()
+ .read(cx)
+ .active_item()
+ .and_then(|item| item.downcast::<ProjectSearchView>())
+ });
+ assert!(
+ active_item.is_none(),
+ "Expected no search panel to be active, but got: {active_item:?}"
+ );
+
+ let one_file_entry = cx.update(|cx| {
+ workspace
+ .read(cx)
+ .project()
+ .read(cx)
+ .entry_for_path(&(worktree_id, "a/one.rs").into(), cx)
+ .expect("no entry for /a/one.rs file")
+ });
+ assert!(one_file_entry.is_file());
+ workspace.update(cx, |workspace, cx| {
+ ProjectSearchView::new_search_in_directory(workspace, &one_file_entry, cx)
+ });
+ let active_search_entry = cx.read(|cx| {
+ workspace
+ .read(cx)
+ .active_pane()
+ .read(cx)
+ .active_item()
+ .and_then(|item| item.downcast::<ProjectSearchView>())
+ });
+ assert!(
+ active_search_entry.is_none(),
+ "Expected no search panel to be active for file entry"
+ );
+
+ let a_dir_entry = cx.update(|cx| {
+ workspace
+ .read(cx)
+ .project()
+ .read(cx)
+ .entry_for_path(&(worktree_id, "a").into(), cx)
+ .expect("no entry for /a/ directory")
+ });
+ assert!(a_dir_entry.is_dir());
+ workspace.update(cx, |workspace, cx| {
+ ProjectSearchView::new_search_in_directory(workspace, &a_dir_entry, cx)
+ });
+
+ let Some(search_view) = cx.read(|cx| {
+ workspace
+ .read(cx)
+ .active_pane()
+ .read(cx)
+ .active_item()
+ .and_then(|item| item.downcast::<ProjectSearchView>())
+ }) else {
+ panic!("Search view expected to appear after new search in directory event trigger")
+ };
+ deterministic.run_until_parked();
+ search_view.update(cx, |search_view, cx| {
+ assert!(
+ search_view.query_editor.is_focused(cx),
+ "On new search in directory, focus should be moved into query editor"
+ );
+ search_view.excluded_files_editor.update(cx, |editor, cx| {
+ assert!(
+ editor.display_text(cx).is_empty(),
+ "New search in directory should not have any excluded files"
+ );
+ });
+ search_view.included_files_editor.update(cx, |editor, cx| {
+ assert_eq!(
+ editor.display_text(cx),
+ a_dir_entry.path.to_str().unwrap(),
+ "New search in directory should have included dir entry path"
+ );
+ });
+ });
+
+ search_view.update(cx, |search_view, cx| {
+ search_view
+ .query_editor
+ .update(cx, |query_editor, cx| query_editor.set_text("const", cx));
+ search_view.search(cx);
+ });
+ deterministic.run_until_parked();
+ search_view.update(cx, |search_view, cx| {
+ assert_eq!(
+ search_view
+ .results_editor
+ .update(cx, |editor, cx| editor.display_text(cx)),
+ "\n\nconst ONE: usize = 1;\n\n\nconst TWO: usize = one::ONE + one::ONE;",
+ "New search in directory should have a filter that matches a certain directory"
+ );
+ });
+ }
+
+ #[gpui::test]
+ async fn test_search_query_history(cx: &mut TestAppContext) {
+ init_test(cx);
+
+ let fs = FakeFs::new(cx.background());
+ fs.insert_tree(
+ "/dir",
+ json!({
+ "one.rs": "const ONE: usize = 1;",
+ "two.rs": "const TWO: usize = one::ONE + one::ONE;",
+ "three.rs": "const THREE: usize = one::ONE + two::TWO;",
+ "four.rs": "const FOUR: usize = one::ONE + three::THREE;",
+ }),
+ )
+ .await;
+ let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
+ let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+ let workspace = window.root(cx);
+ workspace.update(cx, |workspace, cx| {
+ ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx)
+ });
+
+ let search_view = cx.read(|cx| {
+ workspace
+ .read(cx)
+ .active_pane()
+ .read(cx)
+ .active_item()
+ .and_then(|item| item.downcast::<ProjectSearchView>())
+ .expect("Search view expected to appear after new search event trigger")
+ });
+
+ let search_bar = window.add_view(cx, |cx| {
+ let mut search_bar = ProjectSearchBar::new();
+ search_bar.set_active_pane_item(Some(&search_view), cx);
+ // search_bar.show(cx);
+ search_bar
+ });
+
+ // Add 3 search items into the history + another unsubmitted one.
+ search_view.update(cx, |search_view, cx| {
+ search_view.search_options = SearchOptions::CASE_SENSITIVE;
+ search_view
+ .query_editor
+ .update(cx, |query_editor, cx| query_editor.set_text("ONE", cx));
+ search_view.search(cx);
+ });
+ cx.foreground().run_until_parked();
+ search_view.update(cx, |search_view, cx| {
+ search_view
+ .query_editor
+ .update(cx, |query_editor, cx| query_editor.set_text("TWO", cx));
+ search_view.search(cx);
+ });
+ cx.foreground().run_until_parked();
+ search_view.update(cx, |search_view, cx| {
+ search_view
+ .query_editor
+ .update(cx, |query_editor, cx| query_editor.set_text("THREE", cx));
+ search_view.search(cx);
+ });
+ cx.foreground().run_until_parked();
+ search_view.update(cx, |search_view, cx| {
+ search_view.query_editor.update(cx, |query_editor, cx| {
+ query_editor.set_text("JUST_TEXT_INPUT", cx)
+ });
+ });
+ cx.foreground().run_until_parked();
+
+ // Ensure that the latest input with search settings is active.
+ search_view.update(cx, |search_view, cx| {
+ assert_eq!(
+ search_view.query_editor.read(cx).text(cx),
+ "JUST_TEXT_INPUT"
+ );
+ assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
+ });
+
+ // Next history query after the latest should set the query to the empty string.
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.next_history_query(&NextHistoryQuery, cx);
+ });
+ search_view.update(cx, |search_view, cx| {
+ assert_eq!(search_view.query_editor.read(cx).text(cx), "");
+ assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.next_history_query(&NextHistoryQuery, cx);
+ });
+ search_view.update(cx, |search_view, cx| {
+ assert_eq!(search_view.query_editor.read(cx).text(cx), "");
+ assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
+ });
+
+ // First previous query for empty current query should set the query to the latest submitted one.
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.previous_history_query(&PreviousHistoryQuery, cx);
+ });
+ search_view.update(cx, |search_view, cx| {
+ assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE");
+ assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
+ });
+
+ // Further previous items should go over the history in reverse order.
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.previous_history_query(&PreviousHistoryQuery, cx);
+ });
+ search_view.update(cx, |search_view, cx| {
+ assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO");
+ assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
+ });
+
+ // Previous items should never go behind the first history item.
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.previous_history_query(&PreviousHistoryQuery, cx);
+ });
+ search_view.update(cx, |search_view, cx| {
+ assert_eq!(search_view.query_editor.read(cx).text(cx), "ONE");
+ assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.previous_history_query(&PreviousHistoryQuery, cx);
+ });
+ search_view.update(cx, |search_view, cx| {
+ assert_eq!(search_view.query_editor.read(cx).text(cx), "ONE");
+ assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
+ });
+
+ // Next items should go over the history in the original order.
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.next_history_query(&NextHistoryQuery, cx);
+ });
+ search_view.update(cx, |search_view, cx| {
+ assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO");
+ assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
+ });
+
+ search_view.update(cx, |search_view, cx| {
+ search_view
+ .query_editor
+ .update(cx, |query_editor, cx| query_editor.set_text("TWO_NEW", cx));
+ search_view.search(cx);
+ });
+ cx.foreground().run_until_parked();
+ search_view.update(cx, |search_view, cx| {
+ assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO_NEW");
+ assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
+ });
+
+ // New search input should add another entry to history and move the selection to the end of the history.
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.previous_history_query(&PreviousHistoryQuery, cx);
+ });
+ search_view.update(cx, |search_view, cx| {
+ assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE");
+ assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.previous_history_query(&PreviousHistoryQuery, cx);
+ });
+ search_view.update(cx, |search_view, cx| {
+ assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO");
+ assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.next_history_query(&NextHistoryQuery, cx);
+ });
+ search_view.update(cx, |search_view, cx| {
+ assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE");
+ assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.next_history_query(&NextHistoryQuery, cx);
+ });
+ search_view.update(cx, |search_view, cx| {
+ assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO_NEW");
+ assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.next_history_query(&NextHistoryQuery, cx);
+ });
+ search_view.update(cx, |search_view, cx| {
+ assert_eq!(search_view.query_editor.read(cx).text(cx), "");
+ assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
+ });
+ }
+
+ pub fn init_test(cx: &mut TestAppContext) {
+ cx.foreground().forbid_parking();
+ let fonts = cx.font_cache();
+ let mut theme = gpui::fonts::with_font_cache(fonts.clone(), theme::Theme::default);
+ theme.search.match_background = Color::red();
+
+ cx.update(|cx| {
+ cx.set_global(SettingsStore::test(cx));
+ cx.set_global(ActiveSearches::default());
+ settings::register::<SemanticIndexSettings>(cx);
+
+ theme::init((), cx);
+ cx.update_global::<SettingsStore, _, _>(|store, _| {
+ let mut settings = store.get::<ThemeSettings>(None).clone();
+ settings.theme = Arc::new(theme);
+ store.override_global(settings)
+ });
+
+ language::init(cx);
+ client::init_settings(cx);
+ editor::init(cx);
+ workspace::init_settings(cx);
+ Project::init_settings(cx);
+ super::init(cx);
+ });
+ }
+}
@@ -0,0 +1,117 @@
+use bitflags::bitflags;
+pub use buffer_search::BufferSearchBar;
+use gpui::{actions, Action, AppContext, RenderOnce};
+pub use mode::SearchMode;
+use project::search::SearchQuery;
+use ui::ButtonVariant;
+//pub use project_search::{ProjectSearchBar, ProjectSearchView};
+// use theme::components::{
+// action_button::Button, svg::Svg, ComponentExt, IconButtonStyle, ToggleIconButtonStyle,
+// };
+
+pub mod buffer_search;
+mod history;
+mod mode;
+//pub mod project_search;
+pub(crate) mod search_bar;
+
+pub fn init(cx: &mut AppContext) {
+ buffer_search::init(cx);
+ //project_search::init(cx);
+}
+
+actions!(
+ CycleMode,
+ ToggleWholeWord,
+ ToggleCaseSensitive,
+ ToggleReplace,
+ SelectNextMatch,
+ SelectPrevMatch,
+ SelectAllMatches,
+ NextHistoryQuery,
+ PreviousHistoryQuery,
+ ActivateTextMode,
+ ActivateSemanticMode,
+ ActivateRegexMode,
+ ReplaceAll,
+ ReplaceNext,
+);
+
+bitflags! {
+ #[derive(Default)]
+ pub struct SearchOptions: u8 {
+ const NONE = 0b000;
+ const WHOLE_WORD = 0b001;
+ const CASE_SENSITIVE = 0b010;
+ }
+}
+
+impl SearchOptions {
+ pub fn label(&self) -> &'static str {
+ match *self {
+ SearchOptions::WHOLE_WORD => "Match Whole Word",
+ SearchOptions::CASE_SENSITIVE => "Match Case",
+ _ => panic!("{:?} is not a named SearchOption", self),
+ }
+ }
+
+ pub fn icon(&self) -> ui::Icon {
+ match *self {
+ SearchOptions::WHOLE_WORD => ui::Icon::WholeWord,
+ SearchOptions::CASE_SENSITIVE => ui::Icon::CaseSensitive,
+ _ => panic!("{:?} is not a named SearchOption", self),
+ }
+ }
+
+ pub fn to_toggle_action(&self) -> Box<dyn Action + Sync + Send + 'static> {
+ match *self {
+ SearchOptions::WHOLE_WORD => Box::new(ToggleWholeWord),
+ SearchOptions::CASE_SENSITIVE => Box::new(ToggleCaseSensitive),
+ _ => panic!("{:?} is not a named SearchOption", self),
+ }
+ }
+
+ pub fn none() -> SearchOptions {
+ SearchOptions::NONE
+ }
+
+ pub fn from_query(query: &SearchQuery) -> SearchOptions {
+ let mut options = SearchOptions::NONE;
+ options.set(SearchOptions::WHOLE_WORD, query.whole_word());
+ options.set(SearchOptions::CASE_SENSITIVE, query.case_sensitive());
+ options
+ }
+
+ pub fn as_button(&self, active: bool) -> impl RenderOnce {
+ ui::IconButton::new(0, self.icon())
+ .on_click({
+ let action = self.to_toggle_action();
+ move |_, cx| {
+ cx.dispatch_action(action.boxed_clone());
+ }
+ })
+ .variant(ui::ButtonVariant::Ghost)
+ .when(active, |button| button.variant(ButtonVariant::Filled))
+ }
+}
+
+fn toggle_replace_button(active: bool) -> impl RenderOnce {
+ // todo: add toggle_replace button
+ ui::IconButton::new(0, ui::Icon::Replace)
+ .on_click(|_, cx| {
+ cx.dispatch_action(Box::new(ToggleReplace));
+ cx.notify();
+ })
+ .variant(ui::ButtonVariant::Ghost)
+ .when(active, |button| button.variant(ButtonVariant::Filled))
+}
+
+fn render_replace_button(
+ action: impl Action + 'static + Send + Sync,
+ icon: ui::Icon,
+) -> impl RenderOnce {
+ // todo: add tooltip
+ ui::IconButton::new(0, icon).on_click(move |_, cx| {
+ cx.dispatch_action(action.boxed_clone());
+ })
+}
@@ -0,0 +1,35 @@
+use gpui::{MouseDownEvent, RenderOnce, WindowContext};
+use ui::{Button, ButtonVariant, IconButton};
+
+use crate::mode::SearchMode;
+
+pub(super) fn render_nav_button(
+ icon: ui::Icon,
+ _active: bool,
+ on_click: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
+) -> impl RenderOnce {
+ // let tooltip_style = cx.theme().tooltip.clone();
+ // let cursor_style = if active {
+ // CursorStyle::PointingHand
+ // } else {
+ // CursorStyle::default()
+ // };
+ // enum NavButton {}
+ IconButton::new("search-nav-button", icon).on_click(on_click)
+}
+
+pub(crate) fn render_search_mode_button(
+ mode: SearchMode,
+ is_active: bool,
+ on_click: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
+) -> Button {
+ let button_variant = if is_active {
+ ButtonVariant::Filled
+ } else {
+ ButtonVariant::Ghost
+ };
+
+ Button::new(mode.label())
+ .on_click(on_click)
+ .variant(button_variant)
+}
@@ -77,6 +77,7 @@ pub fn handle_settings_file_changes(
});
cx.spawn(move |mut cx| async move {
while let Some(user_settings_content) = user_settings_file_rx.next().await {
+ eprintln!("settings file changed");
let result = cx.update_global(|store: &mut SettingsStore, cx| {
store
.set_user_settings(&user_settings_content, cx)
@@ -6,7 +6,7 @@ use ui::prelude::*;
pub struct ColorsStory;
impl Render for ColorsStory {
- type Element = Div<Self>;
+ type Element = Div;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
let color_scales = default_color_scales();
@@ -28,7 +28,7 @@ impl Render for ColorsStory {
div()
.w(px(75.))
.line_height(px(24.))
- .child(scale.name().to_string()),
+ .child(scale.name().clone()),
)
.child(
div()
@@ -26,8 +26,8 @@ impl FocusStory {
}
}
-impl Render for FocusStory {
- type Element = Focusable<Self, Stateful<Self, Div<Self>>>;
+impl Render<Self> for FocusStory {
+ type Element = Focusable<Stateful<Div>>;
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element {
let theme = cx.theme();
@@ -42,18 +42,20 @@ impl Render for FocusStory {
.id("parent")
.focusable()
.key_context("parent")
- .on_action(|_, action: &ActionA, cx| {
+ .on_action(cx.listener(|_, action: &ActionA, cx| {
println!("Action A dispatched on parent");
- })
- .on_action(|_, action: &ActionB, cx| {
+ }))
+ .on_action(cx.listener(|_, action: &ActionB, cx| {
println!("Action B dispatched on parent");
- })
- .on_focus(|_, _, _| println!("Parent focused"))
- .on_blur(|_, _, _| println!("Parent blurred"))
- .on_focus_in(|_, _, _| println!("Parent focus_in"))
- .on_focus_out(|_, _, _| println!("Parent focus_out"))
- .on_key_down(|_, event, phase, _| println!("Key down on parent {:?}", event))
- .on_key_up(|_, event, phase, _| println!("Key up on parent {:?}", event))
+ }))
+ .on_focus(cx.listener(|_, _, _| println!("Parent focused")))
+ .on_blur(cx.listener(|_, _, _| println!("Parent blurred")))
+ .on_focus_in(cx.listener(|_, _, _| println!("Parent focus_in")))
+ .on_focus_out(cx.listener(|_, _, _| println!("Parent focus_out")))
+ .on_key_down(
+ cx.listener(|_, event, phase, _| println!("Key down on parent {:?}", event)),
+ )
+ .on_key_up(cx.listener(|_, event, phase, _| println!("Key up on parent {:?}", event)))
.size_full()
.bg(color_1)
.focus(|style| style.bg(color_2))
@@ -61,38 +63,42 @@ impl Render for FocusStory {
div()
.track_focus(&self.child_1_focus)
.key_context("child-1")
- .on_action(|_, action: &ActionB, cx| {
+ .on_action(cx.listener(|_, action: &ActionB, cx| {
println!("Action B dispatched on child 1 during");
- })
+ }))
.w_full()
.h_6()
.bg(color_4)
.focus(|style| style.bg(color_5))
.in_focus(|style| style.bg(color_6))
- .on_focus(|_, _, _| println!("Child 1 focused"))
- .on_blur(|_, _, _| println!("Child 1 blurred"))
- .on_focus_in(|_, _, _| println!("Child 1 focus_in"))
- .on_focus_out(|_, _, _| println!("Child 1 focus_out"))
- .on_key_down(|_, event, phase, _| println!("Key down on child 1 {:?}", event))
- .on_key_up(|_, event, phase, _| println!("Key up on child 1 {:?}", event))
+ .on_focus(cx.listener(|_, _, _| println!("Child 1 focused")))
+ .on_blur(cx.listener(|_, _, _| println!("Child 1 blurred")))
+ .on_focus_in(cx.listener(|_, _, _| println!("Child 1 focus_in")))
+ .on_focus_out(cx.listener(|_, _, _| println!("Child 1 focus_out")))
+ .on_key_down(
+ cx.listener(|_, event, _| println!("Key down on child 1 {:?}", event)),
+ )
+ .on_key_up(cx.listener(|_, event, _| println!("Key up on child 1 {:?}", event)))
.child("Child 1"),
)
.child(
div()
.track_focus(&self.child_2_focus)
.key_context("child-2")
- .on_action(|_, action: &ActionC, cx| {
+ .on_action(cx.listener(|_, action: &ActionC, cx| {
println!("Action C dispatched on child 2");
- })
+ }))
.w_full()
.h_6()
.bg(color_4)
- .on_focus(|_, _, _| println!("Child 2 focused"))
- .on_blur(|_, _, _| println!("Child 2 blurred"))
- .on_focus_in(|_, _, _| println!("Child 2 focus_in"))
- .on_focus_out(|_, _, _| println!("Child 2 focus_out"))
- .on_key_down(|_, event, phase, _| println!("Key down on child 2 {:?}", event))
- .on_key_up(|_, event, phase, _| println!("Key up on child 2 {:?}", event))
+ .on_focus(cx.listener(|_, _, _| println!("Child 2 focused")))
+ .on_blur(cx.listener(|_, _, _| println!("Child 2 blurred")))
+ .on_focus_in(cx.listener(|_, _, _| println!("Child 2 focus_in")))
+ .on_focus_out(cx.listener(|_, _, _| println!("Child 2 focus_out")))
+ .on_key_down(
+ cx.listener(|_, event, _| println!("Key down on child 2 {:?}", event)),
+ )
+ .on_key_up(cx.listener(|_, event, _| println!("Key up on child 2 {:?}", event)))
.child("Child 2"),
)
}
@@ -12,7 +12,7 @@ impl KitchenSinkStory {
}
impl Render for KitchenSinkStory {
- type Element = Stateful<Self, Div<Self>>;
+ type Element = Stateful<Div>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
let component_stories = ComponentStory::iter()
@@ -1,5 +1,7 @@
use fuzzy::StringMatchCandidate;
-use gpui::{div, prelude::*, Div, KeyBinding, Render, Styled, Task, View, WindowContext};
+use gpui::{
+ div, prelude::*, Div, KeyBinding, Render, SharedString, Styled, Task, View, WindowContext,
+};
use picker::{Picker, PickerDelegate};
use std::sync::Arc;
use theme2::ActiveTheme;
@@ -54,7 +56,8 @@ impl PickerDelegate for Delegate {
let Some(candidate_ix) = self.matches.get(ix) else {
return div();
};
- let candidate = self.candidates[*candidate_ix].string.clone();
+ // TASK: Make StringMatchCandidate::string a SharedString
+ let candidate = SharedString::from(self.candidates[*candidate_ix].string.clone());
div()
.text_color(colors.text)
@@ -202,7 +205,7 @@ impl PickerStory {
}
}
-impl Render for PickerStory {
+impl Render<Self> for PickerStory {
type Element = Div<Self>;
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element {
@@ -10,7 +10,7 @@ impl ScrollStory {
}
}
-impl Render for ScrollStory {
+impl Render<Self> for ScrollStory {
type Element = Stateful<Self, Div<Self>>;
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element {
@@ -1,4 +1,7 @@
-use gpui::{div, white, Div, ParentComponent, Render, Styled, View, VisualContext, WindowContext};
+use gpui::{
+ blue, div, red, white, Div, ParentElement, Render, Styled, View, VisualContext, WindowContext,
+};
+use ui::v_stack;
pub struct TextStory;
@@ -9,13 +12,49 @@ impl TextStory {
}
impl Render for TextStory {
- type Element = Div<Self>;
+ type Element = Div;
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element {
- div().size_full().bg(white()).child(concat!(
- "The quick brown fox jumps over the lazy dog. ",
- "Meanwhile, the lazy dog decided it was time for a change. ",
- "He started daily workout routines, ate healthier and became the fastest dog in town.",
- ))
+ v_stack()
+ .bg(blue())
+ .child(
+ div()
+ .flex()
+ .child(div().max_w_96().bg(white()).child(concat!(
+ "max-width: 96. The quick brown fox jumps over the lazy dog. ",
+ "Meanwhile, the lazy dog decided it was time for a change. ",
+ "He started daily workout routines, ate healthier and became the fastest dog in town.",
+ ))),
+ )
+ .child(div().h_5())
+ .child(div().flex().flex_col().w_96().bg(white()).child(concat!(
+ "flex-col. width: 96; The quick brown fox jumps over the lazy dog. ",
+ "Meanwhile, the lazy dog decided it was time for a change. ",
+ "He started daily workout routines, ate healthier and became the fastest dog in town.",
+ )))
+ .child(div().h_5())
+ .child(
+ div()
+ .flex()
+ .child(div().min_w_96().bg(white()).child(concat!(
+ "min-width: 96. The quick brown fox jumps over the lazy dog. ",
+ "Meanwhile, the lazy dog decided it was time for a change. ",
+ "He started daily workout routines, ate healthier and became the fastest dog in town.",
+))))
+ .child(div().h_5())
+ .child(div().flex().w_96().bg(white()).child(div().overflow_hidden().child(concat!(
+ "flex-row. width 96. overflow-hidden. The quick brown fox jumps over the lazy dog. ",
+ "Meanwhile, the lazy dog decided it was time for a change. ",
+ "He started daily workout routines, ate healthier and became the fastest dog in town.",
+ ))))
+ // NOTE: When rendering text in a horizonal flex container,
+ // Taffy will not pass width constraints down from the parent.
+ // To fix this, render text in a praent with overflow: hidden, which
+ .child(div().h_5())
+ .child(div().flex().w_96().bg(red()).child(concat!(
+ "flex-row. width 96. The quick brown fox jumps over the lazy dog. ",
+ "Meanwhile, the lazy dog decided it was time for a change. ",
+ "He started daily workout routines, ate healthier and became the fastest dog in town.",
+ )))
}
}
@@ -1,4 +1,4 @@
-use gpui::{px, rgb, Div, Hsla, Render};
+use gpui::{px, rgb, Div, Hsla, Render, RenderOnce};
use ui::prelude::*;
use crate::story::Story;
@@ -7,7 +7,7 @@ use crate::story::Story;
/// [https://developer.mozilla.org/en-US/docs/Web/CSS/z-index](https://developer.mozilla.org/en-US/docs/Web/CSS/z-index).
pub struct ZIndexStory;
-impl Render for ZIndexStory {
+impl Render<Self> for ZIndexStory {
type Element = Div<Self>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
@@ -79,17 +79,15 @@ trait Styles: Styled + Sized {
impl<V: 'static> Styles for Div<V> {}
-#[derive(Component)]
+#[derive(RenderOnce)]
struct ZIndexExample {
z_index: u32,
}
-impl ZIndexExample {
- pub fn new(z_index: u32) -> Self {
- Self { z_index }
- }
+impl<V: 'static> Component<V> for ZIndexExample {
+ type Rendered = Div<V>;
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+ fn render(self, view: &mut V, cx: &mut ViewContext<V>) -> Self::Rendered {
div()
.relative()
.size_full()
@@ -109,14 +107,14 @@ impl ZIndexExample {
// HACK: Simulate `text-align: center`.
.pl(px(24.))
.z_index(self.z_index)
- .child(format!(
+ .child(SharedString::from(format!(
"z-index: {}",
if self.z_index == 0 {
"auto".to_string()
} else {
self.z_index.to_string()
}
- )),
+ ))),
)
// Blue blocks.
.child(
@@ -173,3 +171,9 @@ impl ZIndexExample {
)
}
}
+
+impl ZIndexExample {
+ pub fn new(z_index: u32) -> Self {
+ Self { z_index }
+ }
+}
@@ -105,7 +105,7 @@ impl StoryWrapper {
}
}
-impl Render for StoryWrapper {
+impl Render<Self> for StoryWrapper {
type Element = Div<Self>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
@@ -1,9 +1,9 @@
use anyhow::Result;
-use gpui::AssetSource;
use gpui::{
div, px, size, AnyView, Bounds, Div, Render, ViewContext, VisualContext, WindowBounds,
WindowOptions,
};
+use gpui::{white, AssetSource};
use settings::{default_settings, Settings, SettingsStore};
use std::borrow::Cow;
use std::sync::Arc;
@@ -56,18 +56,32 @@ fn main() {
}
struct TestView {
+ #[allow(unused)]
story: AnyView,
}
-impl Render for TestView {
+impl Render<Self> for TestView {
type Element = Div<Self>;
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
div()
.flex()
+ .bg(gpui::blue())
.flex_col()
.size_full()
.font("Helvetica")
- .child(self.story.clone())
+ .child(div().h_5())
+ .child(
+ div()
+ .flex()
+ .w_96()
+ .bg(white())
+ .relative()
+ .child(div().child(concat!(
+ "The quick brown fox jumps over the lazy dog. ",
+ "Meanwhile, the lazy dog decided it was time for a change. ",
+ "He started daily workout routines, ate healthier and became the fastest dog in town.",
+ ))),
+ )
}
}
@@ -4,7 +4,7 @@ use crate::TerminalView;
use db::kvp::KEY_VALUE_STORE;
use gpui::{
actions, div, serde_json, AppContext, AsyncWindowContext, Div, Entity, EventEmitter,
- FocusHandle, FocusableView, ParentComponent, Render, Subscription, Task, View, ViewContext,
+ FocusHandle, FocusableView, ParentElement, Render, Subscription, Task, View, ViewContext,
VisualContext, WeakView, WindowContext,
};
use project::Fs;
@@ -336,7 +336,7 @@ impl TerminalPanel {
impl EventEmitter<PanelEvent> for TerminalPanel {}
impl Render for TerminalPanel {
- type Element = Div<Self>;
+ type Element = Div;
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
div().child(self.pane.clone())
@@ -9,11 +9,10 @@ pub mod terminal_panel;
// use crate::terminal_element::TerminalElement;
use editor::{scroll::autoscroll::Autoscroll, Editor};
use gpui::{
- actions, div, img, red, Action, AnyElement, AppContext, Component, DispatchPhase, Div,
- EventEmitter, FocusEvent, FocusHandle, Focusable, FocusableComponent, FocusableView,
- InputHandler, InteractiveComponent, KeyDownEvent, Keystroke, Model, MouseButton,
- ParentComponent, Pixels, Render, SharedString, Styled, Task, View, ViewContext, VisualContext,
- WeakView,
+ actions, div, Action, AnyElement, AppContext, Div, Element, EventEmitter, FocusEvent,
+ FocusHandle, Focusable, FocusableElement, FocusableView, InputHandler, InteractiveElement,
+ KeyDownEvent, Keystroke, Model, MouseButton, MouseDownEvent, ParentElement, Pixels, Render,
+ SharedString, Styled, Task, View, ViewContext, VisualContext, WeakView, WindowContext,
};
use language::Bias;
use persistence::TERMINAL_DB;
@@ -32,7 +31,7 @@ use workspace::{
notifications::NotifyResultExt,
register_deserializable_item,
searchable::{SearchEvent, SearchOptions, SearchableItem},
- ui::{ContextMenu, Label},
+ ui::{ContextMenu, Icon, IconElement, Label, ListItem},
CloseActiveItem, NewCenterTerminal, Pane, ToolbarItemLocation, Workspace, WorkspaceId,
};
@@ -64,7 +63,6 @@ pub struct SendKeystroke(String);
actions!(Clear, Copy, Paste, ShowCharacterPalette, SearchTest);
pub fn init(cx: &mut AppContext) {
- workspace::ui::init(cx);
terminal_panel::init(cx);
terminal::init(cx);
@@ -300,11 +298,10 @@ impl TerminalView {
position: gpui::Point<Pixels>,
cx: &mut ViewContext<Self>,
) {
- self.context_menu = Some(cx.build_view(|cx| {
- ContextMenu::new(cx)
- .entry(Label::new("Clear"), Box::new(Clear))
- .entry(
- Label::new("Close"),
+ self.context_menu = Some(ContextMenu::build(cx, |menu, _| {
+ menu.action(ListItem::new("clear", Label::new("Clear")), Box::new(Clear))
+ .action(
+ ListItem::new("close", Label::new("Close")),
Box::new(CloseActiveItem { save_intent: None }),
)
}));
@@ -507,12 +504,7 @@ pub fn regex_search_for_query(query: &project::search::SearchQuery) -> Option<Re
}
impl TerminalView {
- fn key_down(
- &mut self,
- event: &KeyDownEvent,
- _dispatch_phase: DispatchPhase,
- cx: &mut ViewContext<Self>,
- ) {
+ fn key_down(&mut self, event: &KeyDownEvent, cx: &mut ViewContext<Self>) {
self.clear_bel(cx);
self.pause_cursor_blinking(cx);
@@ -540,7 +532,7 @@ impl TerminalView {
}
impl Render for TerminalView {
- type Element = Focusable<Self, Div<Self>>;
+ type Element = Focusable<Div>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
let terminal_handle = self.terminal.clone().downgrade();
@@ -554,14 +546,14 @@ impl Render for TerminalView {
div()
.z_index(0)
.absolute()
- .on_key_down(Self::key_down)
- .on_action(TerminalView::send_text)
- .on_action(TerminalView::send_keystroke)
- .on_action(TerminalView::copy)
- .on_action(TerminalView::paste)
- .on_action(TerminalView::clear)
- .on_action(TerminalView::show_character_palette)
- .on_action(TerminalView::select_all)
+ .on_key_down(cx.listener(Self::key_down))
+ .on_action(cx.listener(TerminalView::send_text))
+ .on_action(cx.listener(TerminalView::send_keystroke))
+ .on_action(cx.listener(TerminalView::copy))
+ .on_action(cx.listener(TerminalView::paste))
+ .on_action(cx.listener(TerminalView::clear))
+ .on_action(cx.listener(TerminalView::show_character_palette))
+ .on_action(cx.listener(TerminalView::select_all))
// todo!()
.child(
"TERMINAL HERE", // TerminalElement::new(
@@ -571,19 +563,22 @@ impl Render for TerminalView {
// self.can_navigate_to_selected_word,
// )
)
- .on_mouse_down(MouseButton::Right, |this, event, cx| {
- this.deploy_context_menu(event.position, cx);
- cx.notify();
- }),
+ .on_mouse_down(
+ MouseButton::Right,
+ cx.listener(|this, event: &MouseDownEvent, cx| {
+ this.deploy_context_menu(event.position, cx);
+ cx.notify();
+ }),
+ ),
)
.children(
self.context_menu
.clone()
- .map(|context_menu| div().z_index(1).absolute().child(context_menu.render())),
+ .map(|context_menu| div().z_index(1).absolute().child(context_menu)),
)
.track_focus(&self.focus_handle)
- .on_focus_in(Self::focus_in)
- .on_focus_out(Self::focus_out)
+ .on_focus_in(cx.listener(Self::focus_in))
+ .on_focus_out(cx.listener(Self::focus_out))
}
}
@@ -748,17 +743,13 @@ impl Item for TerminalView {
Some(self.terminal().read(cx).title().into())
}
- fn tab_content<T: 'static>(
- &self,
- _detail: Option<usize>,
- cx: &gpui::AppContext,
- ) -> AnyElement<T> {
+ fn tab_content(&self, _detail: Option<usize>, cx: &WindowContext) -> AnyElement {
let title = self.terminal().read(cx).title();
div()
- .child(img().uri("icons/terminal.svg").bg(red()))
- .child(title)
- .render()
+ .child(IconElement::new(Icon::Terminal))
+ .child(Label::new(title))
+ .into_any()
}
fn clone_on_split(
@@ -794,7 +785,7 @@ impl Item for TerminalView {
// }
fn breadcrumb_location(&self) -> ToolbarItemLocation {
- ToolbarItemLocation::PrimaryLeft { flex: None }
+ ToolbarItemLocation::PrimaryLeft
}
fn breadcrumbs(&self, _: &theme::Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
@@ -184,7 +184,7 @@ impl settings::Settings for ThemeSettings {
) -> schemars::schema::RootSchema {
let mut root_schema = generator.root_schema_for::<ThemeSettingsContent>();
let theme_names = cx
- .global::<Arc<ThemeRegistry>>()
+ .global::<ThemeRegistry>()
.list_names(params.staff_mode)
.map(|theme_name| Value::String(theme_name.to_string()))
.collect();
@@ -1,11 +1,11 @@
-use gpui::{div, Component, Div, ParentComponent, Styled, ViewContext};
+use gpui::{div, Div, Element, ParentElement, SharedString, Styled, WindowContext};
use crate::ActiveTheme;
pub struct Story {}
impl Story {
- pub fn container<V: 'static>(cx: &mut ViewContext<V>) -> Div<V> {
+ pub fn container(cx: &mut WindowContext) -> Div {
div()
.size_full()
.flex()
@@ -16,23 +16,23 @@ impl Story {
.bg(cx.theme().colors().background)
}
- pub fn title<V: 'static>(cx: &mut ViewContext<V>, title: &str) -> impl Component<V> {
+ pub fn title(cx: &mut WindowContext, title: SharedString) -> impl Element {
div()
.text_xl()
.text_color(cx.theme().colors().text)
- .child(title.to_owned())
+ .child(title)
}
- pub fn title_for<V: 'static, T>(cx: &mut ViewContext<V>) -> impl Component<V> {
- Self::title(cx, std::any::type_name::<T>())
+ pub fn title_for<T>(cx: &mut WindowContext) -> impl Element {
+ Self::title(cx, std::any::type_name::<T>().into())
}
- pub fn label<V: 'static>(cx: &mut ViewContext<V>, label: &str) -> impl Component<V> {
+ pub fn label(cx: &mut WindowContext, label: impl Into<SharedString>) -> impl Element {
div()
.mt_4()
.mb_2()
.text_xs()
.text_color(cx.theme().colors().text)
- .child(label.to_owned())
+ .child(label.into())
}
}
@@ -1,6 +1,6 @@
use gpui::Hsla;
-#[derive(Debug, Clone, Copy)]
+#[derive(Debug, Clone, Copy, Default)]
pub struct PlayerColor {
pub cursor: Hsla,
pub background: Hsla,
@@ -143,12 +143,12 @@ use crate::{amber, blue, jade, lime, orange, pink, purple, red};
mod stories {
use super::*;
use crate::{ActiveTheme, Story};
- use gpui::{div, img, px, Div, ParentComponent, Render, Styled, ViewContext};
+ use gpui::{div, img, px, Div, ParentElement, Render, Styled, ViewContext};
pub struct PlayerStory;
impl Render for PlayerStory {
- type Element = Div<Self>;
+ type Element = Div;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
Story::container(cx).child(
@@ -156,7 +156,7 @@ mod stories {
.flex()
.flex_col()
.gap_4()
- .child(Story::title_for::<_, PlayerColors>(cx))
+ .child(Story::title_for::<PlayerColors>(cx))
.child(Story::label(cx, "Player Colors"))
.child(
div()
@@ -63,6 +63,12 @@ impl ActiveTheme for AppContext {
}
}
+// impl<'a> ActiveTheme for WindowContext<'a> {
+// fn theme(&self) -> &Arc<Theme> {
+// &ThemeSettings::get_global(self.app()).active_theme
+// }
+// }
+
pub struct ThemeFamily {
pub id: String,
pub name: SharedString,
@@ -130,7 +136,7 @@ impl Theme {
}
}
-#[derive(Clone, Debug)]
+#[derive(Clone, Debug, Default)]
pub struct DiagnosticStyle {
pub error: Hsla,
pub warning: Hsla,
@@ -4,6 +4,10 @@ version = "0.1.0"
edition = "2021"
publish = false
+[lib]
+name = "ui2"
+path = "src/ui2.rs"
+
[dependencies]
anyhow.workspace = true
chrono = "0.4"
@@ -18,5 +22,5 @@ theme2 = { path = "../theme2" }
rand = "0.8"
[features]
-default = ["stories"]
+default = []
stories = ["dep:itertools"]
@@ -49,13 +49,13 @@ use gpui::hsla
impl<V: 'static> TodoList<V> {
// ...
- fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+ fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Element<V> {
div().size_4().bg(hsla(50.0/360.0, 1.0, 0.5, 1.0))
}
}
~~~
-Every component needs a render method, and it should return `impl Component<V>`. This basic component will render a 16x16px yellow square on the screen.
+Every component needs a render method, and it should return `impl Element<V>`. This basic component will render a 16x16px yellow square on the screen.
A couple of questions might come to mind:
@@ -87,7 +87,7 @@ We can access the current theme's colors like this:
~~~rust
impl<V: 'static> TodoList<V> {
// ...
- fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+ fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Element<V> {
let color = cx.theme().colors()
div().size_4().hsla(50.0/360.0, 1.0, 0.5, 1.0)
@@ -102,7 +102,7 @@ use gpui::hsla
impl<V: 'static> TodoList<V> {
// ...
- fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+ fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Element<V> {
let color = cx.theme().colors()
div().size_4().bg(color.surface)
@@ -117,7 +117,7 @@ use gpui::hsla
impl<V: 'static> TodoList<V> {
// ...
- fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+ fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Element<V> {
let color = cx.theme().colors()
div()
@@ -2,56 +2,34 @@ mod avatar;
mod button;
mod checkbox;
mod context_menu;
-mod details;
+mod disclosure;
mod divider;
-mod elevated_surface;
-mod facepile;
mod icon;
mod icon_button;
-mod indicator;
mod input;
mod keybinding;
mod label;
mod list;
-mod modal;
-mod notification_toast;
-mod palette;
-mod panel;
-mod player;
-mod player_stack;
mod slot;
mod stack;
-mod tab;
-mod toast;
+mod stories;
mod toggle;
-mod tool_divider;
mod tooltip;
pub use avatar::*;
pub use button::*;
pub use checkbox::*;
pub use context_menu::*;
-pub use details::*;
+pub use disclosure::*;
pub use divider::*;
-pub use elevated_surface::*;
-pub use facepile::*;
pub use icon::*;
pub use icon_button::*;
-pub use indicator::*;
pub use input::*;
pub use keybinding::*;
pub use label::*;
pub use list::*;
-pub use modal::*;
-pub use notification_toast::*;
-pub use palette::*;
-pub use panel::*;
-pub use player::*;
-pub use player_stack::*;
pub use slot::*;
pub use stack::*;
-pub use tab::*;
-pub use toast::*;
+pub use stories::*;
pub use toggle::*;
-pub use tool_divider::*;
pub use tooltip::*;
@@ -1,27 +1,23 @@
-use gpui::img;
-
use crate::prelude::*;
+use gpui::{img, Img, RenderOnce};
+
+#[derive(Debug, Default, PartialEq, Clone)]
+pub enum Shape {
+ #[default]
+ Circle,
+ RoundedRectangle,
+}
-#[derive(Component)]
+#[derive(RenderOnce)]
pub struct Avatar {
src: SharedString,
shape: Shape,
}
-impl Avatar {
- pub fn new(src: impl Into<SharedString>) -> Self {
- Self {
- src: src.into(),
- shape: Shape::Circle,
- }
- }
-
- pub fn shape(mut self, shape: Shape) -> Self {
- self.shape = shape;
- self
- }
+impl Component for Avatar {
+ type Rendered = Img;
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+ fn render(self, _: &mut WindowContext) -> Self::Rendered {
let mut img = img();
if self.shape == Shape::Circle {
@@ -37,30 +33,16 @@ impl Avatar {
}
}
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use super::*;
- use crate::Story;
- use gpui::{Div, Render};
-
- pub struct AvatarStory;
-
- impl Render for AvatarStory {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- Story::container(cx)
- .child(Story::title_for::<_, Avatar>(cx))
- .child(Story::label(cx, "Default"))
- .child(Avatar::new(
- "https://avatars.githubusercontent.com/u/1714999?v=4",
- ))
- .child(Avatar::new(
- "https://avatars.githubusercontent.com/u/326587?v=4",
- ))
+impl Avatar {
+ pub fn new(src: impl Into<SharedString>) -> Self {
+ Self {
+ src: src.into(),
+ shape: Shape::Circle,
}
}
+
+ pub fn shape(mut self, shape: Shape) -> Self {
+ self.shape = shape;
+ self
+ }
}
@@ -1,25 +1,28 @@
-use std::sync::Arc;
+use std::rc::Rc;
-use gpui::{div, DefiniteLength, Hsla, MouseButton, StatefulInteractiveComponent, WindowContext};
+use gpui::{
+ DefiniteLength, Div, Hsla, MouseButton, MouseDownEvent, RenderOnce, StatefulInteractiveElement,
+ WindowContext,
+};
use crate::prelude::*;
-use crate::{h_stack, Icon, IconButton, IconElement, Label, LineHeightStyle, TextColor};
+use crate::{h_stack, Color, Icon, IconButton, IconElement, Label, LineHeightStyle};
/// Provides the flexibility to use either a standard
/// button or an icon button in a given context.
-pub enum ButtonOrIconButton<V: 'static> {
- Button(Button<V>),
- IconButton(IconButton<V>),
+pub enum ButtonOrIconButton {
+ Button(Button),
+ IconButton(IconButton),
}
-impl<V: 'static> From<Button<V>> for ButtonOrIconButton<V> {
- fn from(value: Button<V>) -> Self {
+impl From<Button> for ButtonOrIconButton {
+ fn from(value: Button) -> Self {
Self::Button(value)
}
}
-impl<V: 'static> From<IconButton<V>> for ButtonOrIconButton<V> {
- fn from(value: IconButton<V>) -> Self {
+impl From<IconButton> for ButtonOrIconButton {
+ fn from(value: IconButton) -> Self {
Self::IconButton(value)
}
}
@@ -61,38 +64,74 @@ impl ButtonVariant {
}
}
-pub type ClickHandler<V> = Arc<dyn Fn(&mut V, &mut ViewContext<V>)>;
-
-struct ButtonHandlers<V: 'static> {
- click: Option<ClickHandler<V>>,
-}
-
-unsafe impl<S> Send for ButtonHandlers<S> {}
-unsafe impl<S> Sync for ButtonHandlers<S> {}
-
-impl<V: 'static> Default for ButtonHandlers<V> {
- fn default() -> Self {
- Self { click: None }
- }
-}
-
-#[derive(Component)]
-pub struct Button<V: 'static> {
+#[derive(RenderOnce)]
+pub struct Button {
disabled: bool,
- handlers: ButtonHandlers<V>,
+ click_handler: Option<Rc<dyn Fn(&MouseDownEvent, &mut WindowContext)>>,
icon: Option<Icon>,
icon_position: Option<IconPosition>,
label: SharedString,
variant: ButtonVariant,
width: Option<DefiniteLength>,
- color: Option<TextColor>,
+ color: Option<Color>,
}
-impl<V: 'static> Button<V> {
+impl Component for Button {
+ type Rendered = gpui::Stateful<Div>;
+
+ fn render(self, cx: &mut WindowContext) -> Self::Rendered {
+ let (icon_color, label_color) = match (self.disabled, self.color) {
+ (true, _) => (Color::Disabled, Color::Disabled),
+ (_, None) => (Color::Default, Color::Default),
+ (_, Some(color)) => (Color::from(color), color),
+ };
+
+ let mut button = h_stack()
+ .id(SharedString::from(format!("{}", self.label)))
+ .relative()
+ .p_1()
+ .text_ui()
+ .rounded_md()
+ .bg(self.variant.bg_color(cx))
+ .cursor_pointer()
+ .hover(|style| style.bg(self.variant.bg_color_hover(cx)))
+ .active(|style| style.bg(self.variant.bg_color_active(cx)));
+
+ match (self.icon, self.icon_position) {
+ (Some(_), Some(IconPosition::Left)) => {
+ button = button
+ .gap_1()
+ .child(self.render_label(label_color))
+ .children(self.render_icon(icon_color))
+ }
+ (Some(_), Some(IconPosition::Right)) => {
+ button = button
+ .gap_1()
+ .children(self.render_icon(icon_color))
+ .child(self.render_label(label_color))
+ }
+ (_, _) => button = button.child(self.render_label(label_color)),
+ }
+
+ if let Some(width) = self.width {
+ button = button.w(width).justify_center();
+ }
+
+ if let Some(click_handler) = self.click_handler.clone() {
+ button = button.on_mouse_down(MouseButton::Left, move |event, cx| {
+ click_handler(event, cx);
+ });
+ }
+
+ button
+ }
+}
+
+impl Button {
pub fn new(label: impl Into<SharedString>) -> Self {
Self {
disabled: false,
- handlers: ButtonHandlers::default(),
+ click_handler: None,
icon: None,
icon_position: None,
label: label.into(),
@@ -129,8 +168,11 @@ impl<V: 'static> Button<V> {
self
}
- pub fn on_click(mut self, handler: ClickHandler<V>) -> Self {
- self.handlers.click = Some(handler);
+ pub fn on_click(
+ mut self,
+ handler: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
+ ) -> Self {
+ self.click_handler = Some(Rc::new(handler));
self
}
@@ -139,14 +181,14 @@ impl<V: 'static> Button<V> {
self
}
- pub fn color(mut self, color: Option<TextColor>) -> Self {
+ pub fn color(mut self, color: Option<Color>) -> Self {
self.color = color;
self
}
- pub fn label_color(&self, color: Option<TextColor>) -> TextColor {
+ pub fn label_color(&self, color: Option<Color>) -> Color {
if self.disabled {
- TextColor::Disabled
+ Color::Disabled
} else if let Some(color) = color {
color
} else {
@@ -154,249 +196,38 @@ impl<V: 'static> Button<V> {
}
}
- fn render_label(&self, color: TextColor) -> Label {
+ fn render_label(&self, color: Color) -> Label {
Label::new(self.label.clone())
.color(color)
.line_height_style(LineHeightStyle::UILabel)
}
- fn render_icon(&self, icon_color: TextColor) -> Option<IconElement> {
+ fn render_icon(&self, icon_color: Color) -> Option<IconElement> {
self.icon.map(|i| IconElement::new(i).color(icon_color))
}
-
- pub fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- let (icon_color, label_color) = match (self.disabled, self.color) {
- (true, _) => (TextColor::Disabled, TextColor::Disabled),
- (_, None) => (TextColor::Default, TextColor::Default),
- (_, Some(color)) => (TextColor::from(color), color),
- };
-
- let mut button = h_stack()
- .id(SharedString::from(format!("{}", self.label)))
- .relative()
- .p_1()
- .text_ui()
- .rounded_md()
- .bg(self.variant.bg_color(cx))
- .cursor_pointer()
- .hover(|style| style.bg(self.variant.bg_color_hover(cx)))
- .active(|style| style.bg(self.variant.bg_color_active(cx)));
-
- match (self.icon, self.icon_position) {
- (Some(_), Some(IconPosition::Left)) => {
- button = button
- .gap_1()
- .child(self.render_label(label_color))
- .children(self.render_icon(icon_color))
- }
- (Some(_), Some(IconPosition::Right)) => {
- button = button
- .gap_1()
- .children(self.render_icon(icon_color))
- .child(self.render_label(label_color))
- }
- (_, _) => button = button.child(self.render_label(label_color)),
- }
-
- if let Some(width) = self.width {
- button = button.w(width).justify_center();
- }
-
- if let Some(click_handler) = self.handlers.click.clone() {
- button = button.on_mouse_down(MouseButton::Left, move |state, event, cx| {
- click_handler(state, cx);
- });
- }
-
- button
- }
}
-#[derive(Component)]
-pub struct ButtonGroup<V: 'static> {
- buttons: Vec<Button<V>>,
+#[derive(RenderOnce)]
+pub struct ButtonGroup {
+ buttons: Vec<Button>,
}
-impl<V: 'static> ButtonGroup<V> {
- pub fn new(buttons: Vec<Button<V>>) -> Self {
- Self { buttons }
- }
+impl Component for ButtonGroup {
+ type Rendered = Div;
- fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- let mut el = h_stack().text_ui();
+ fn render(self, cx: &mut WindowContext) -> Self::Rendered {
+ let mut group = h_stack();
- for button in self.buttons {
- el = el.child(button.render(_view, cx));
+ for button in self.buttons.into_iter() {
+ group = group.child(button.render(cx));
}
- el
+ group
}
}
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use super::*;
- use crate::{h_stack, v_stack, Story, TextColor};
- use gpui::{rems, Div, Render};
- use strum::IntoEnumIterator;
-
- pub struct ButtonStory;
-
- impl Render for ButtonStory {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- let states = InteractionState::iter();
-
- Story::container(cx)
- .child(Story::title_for::<_, Button<Self>>(cx))
- .child(
- div()
- .flex()
- .gap_8()
- .child(
- div()
- .child(Story::label(cx, "Ghost (Default)"))
- .child(h_stack().gap_2().children(states.clone().map(|state| {
- v_stack()
- .gap_1()
- .child(
- Label::new(state.to_string()).color(TextColor::Muted),
- )
- .child(
- Button::new("Label").variant(ButtonVariant::Ghost), // .state(state),
- )
- })))
- .child(Story::label(cx, "Ghost – Left Icon"))
- .child(h_stack().gap_2().children(states.clone().map(|state| {
- v_stack()
- .gap_1()
- .child(
- Label::new(state.to_string()).color(TextColor::Muted),
- )
- .child(
- Button::new("Label")
- .variant(ButtonVariant::Ghost)
- .icon(Icon::Plus)
- .icon_position(IconPosition::Left), // .state(state),
- )
- })))
- .child(Story::label(cx, "Ghost – Right Icon"))
- .child(h_stack().gap_2().children(states.clone().map(|state| {
- v_stack()
- .gap_1()
- .child(
- Label::new(state.to_string()).color(TextColor::Muted),
- )
- .child(
- Button::new("Label")
- .variant(ButtonVariant::Ghost)
- .icon(Icon::Plus)
- .icon_position(IconPosition::Right), // .state(state),
- )
- }))),
- )
- .child(
- div()
- .child(Story::label(cx, "Filled"))
- .child(h_stack().gap_2().children(states.clone().map(|state| {
- v_stack()
- .gap_1()
- .child(
- Label::new(state.to_string()).color(TextColor::Muted),
- )
- .child(
- Button::new("Label").variant(ButtonVariant::Filled), // .state(state),
- )
- })))
- .child(Story::label(cx, "Filled – Left Button"))
- .child(h_stack().gap_2().children(states.clone().map(|state| {
- v_stack()
- .gap_1()
- .child(
- Label::new(state.to_string()).color(TextColor::Muted),
- )
- .child(
- Button::new("Label")
- .variant(ButtonVariant::Filled)
- .icon(Icon::Plus)
- .icon_position(IconPosition::Left), // .state(state),
- )
- })))
- .child(Story::label(cx, "Filled – Right Button"))
- .child(h_stack().gap_2().children(states.clone().map(|state| {
- v_stack()
- .gap_1()
- .child(
- Label::new(state.to_string()).color(TextColor::Muted),
- )
- .child(
- Button::new("Label")
- .variant(ButtonVariant::Filled)
- .icon(Icon::Plus)
- .icon_position(IconPosition::Right), // .state(state),
- )
- }))),
- )
- .child(
- div()
- .child(Story::label(cx, "Fixed With"))
- .child(h_stack().gap_2().children(states.clone().map(|state| {
- v_stack()
- .gap_1()
- .child(
- Label::new(state.to_string()).color(TextColor::Muted),
- )
- .child(
- Button::new("Label")
- .variant(ButtonVariant::Filled)
- // .state(state)
- .width(Some(rems(6.).into())),
- )
- })))
- .child(Story::label(cx, "Fixed With – Left Icon"))
- .child(h_stack().gap_2().children(states.clone().map(|state| {
- v_stack()
- .gap_1()
- .child(
- Label::new(state.to_string()).color(TextColor::Muted),
- )
- .child(
- Button::new("Label")
- .variant(ButtonVariant::Filled)
- // .state(state)
- .icon(Icon::Plus)
- .icon_position(IconPosition::Left)
- .width(Some(rems(6.).into())),
- )
- })))
- .child(Story::label(cx, "Fixed With – Right Icon"))
- .child(h_stack().gap_2().children(states.clone().map(|state| {
- v_stack()
- .gap_1()
- .child(
- Label::new(state.to_string()).color(TextColor::Muted),
- )
- .child(
- Button::new("Label")
- .variant(ButtonVariant::Filled)
- // .state(state)
- .icon(Icon::Plus)
- .icon_position(IconPosition::Right)
- .width(Some(rems(6.).into())),
- )
- }))),
- ),
- )
- .child(Story::label(cx, "Button with `on_click`"))
- .child(
- Button::new("Label")
- .variant(ButtonVariant::Ghost)
- .on_click(Arc::new(|_view, _cx| println!("Button clicked."))),
- )
- }
+impl ButtonGroup {
+ pub fn new(buttons: Vec<Button>) -> Self {
+ Self { buttons }
}
}
@@ -1,25 +1,147 @@
-use gpui::{div, prelude::*, Component, ElementId, Styled, ViewContext};
-use std::sync::Arc;
+use gpui::{div, prelude::*, Div, Element, ElementId, RenderOnce, Styled, WindowContext};
+
use theme2::ActiveTheme;
-use crate::{Icon, IconElement, Selection, TextColor};
+use crate::{Color, Icon, IconElement, Selection};
-pub type CheckHandler<V> = Arc<dyn Fn(Selection, &mut V, &mut ViewContext<V>) + Send + Sync>;
+pub type CheckHandler = Box<dyn Fn(&Selection, &mut WindowContext) + 'static>;
/// # Checkbox
///
/// Checkboxes are used for multiple choices, not for mutually exclusive choices.
/// Each checkbox works independently from other checkboxes in the list,
/// therefore checking an additional box does not affect any other selections.
-#[derive(Component)]
-pub struct Checkbox<V: 'static> {
+#[derive(RenderOnce)]
+pub struct Checkbox {
id: ElementId,
checked: Selection,
disabled: bool,
- on_click: Option<CheckHandler<V>>,
+ on_click: Option<CheckHandler>,
}
-impl<V: 'static> Checkbox<V> {
+impl Component for Checkbox {
+ type Rendered = gpui::Stateful<Div>;
+
+ fn render(self, cx: &mut WindowContext) -> Self::Rendered {
+ let group_id = format!("checkbox_group_{:?}", self.id);
+
+ let icon = match self.checked {
+ // When selected, we show a checkmark.
+ Selection::Selected => {
+ Some(
+ IconElement::new(Icon::Check)
+ .size(crate::IconSize::Small)
+ .color(
+ // If the checkbox is disabled we change the color of the icon.
+ if self.disabled {
+ Color::Disabled
+ } else {
+ Color::Selected
+ },
+ ),
+ )
+ }
+ // In an indeterminate state, we show a dash.
+ Selection::Indeterminate => {
+ Some(
+ IconElement::new(Icon::Dash)
+ .size(crate::IconSize::Small)
+ .color(
+ // If the checkbox is disabled we change the color of the icon.
+ if self.disabled {
+ Color::Disabled
+ } else {
+ Color::Selected
+ },
+ ),
+ )
+ }
+ // When unselected, we show nothing.
+ Selection::Unselected => None,
+ };
+
+ // A checkbox could be in an indeterminate state,
+ // for example the indeterminate state could represent:
+ // - a group of options of which only some are selected
+ // - an enabled option that is no longer available
+ // - a previously agreed to license that has been updated
+ //
+ // For the sake of styles we treat the indeterminate state as selected,
+ // but it's icon will be different.
+ let selected =
+ self.checked == Selection::Selected || self.checked == Selection::Indeterminate;
+
+ // We could use something like this to make the checkbox background when selected:
+ //
+ // ~~~rust
+ // ...
+ // .when(selected, |this| {
+ // this.bg(cx.theme().colors().element_selected)
+ // })
+ // ~~~
+ //
+ // But we use a match instead here because the checkbox might be disabled,
+ // and it could be disabled _while_ it is selected, as well as while it is not selected.
+ let (bg_color, border_color) = match (self.disabled, selected) {
+ (true, _) => (
+ cx.theme().colors().ghost_element_disabled,
+ cx.theme().colors().border_disabled,
+ ),
+ (false, true) => (
+ cx.theme().colors().element_selected,
+ cx.theme().colors().border,
+ ),
+ (false, false) => (
+ cx.theme().colors().element_background,
+ cx.theme().colors().border,
+ ),
+ };
+
+ div()
+ .id(self.id)
+ // Rather than adding `px_1()` to add some space around the checkbox,
+ // we use a larger parent element to create a slightly larger
+ // click area for the checkbox.
+ .size_5()
+ // Because we've enlarged the click area, we need to create a
+ // `group` to pass down interactivity events to the checkbox.
+ .group(group_id.clone())
+ .child(
+ div()
+ .flex()
+ // This prevent the flex element from growing
+ // or shrinking in response to any size changes
+ .flex_none()
+ // The combo of `justify_center()` and `items_center()`
+ // is used frequently to center elements in a flex container.
+ //
+ // We use this to center the icon in the checkbox.
+ .justify_center()
+ .items_center()
+ .m_1()
+ .size_4()
+ .rounded_sm()
+ .bg(bg_color)
+ .border()
+ .border_color(border_color)
+ // We only want the interactivity states to fire when we
+ // are in a checkbox that isn't disabled.
+ .when(!self.disabled, |this| {
+ // Here instead of `hover()` we use `group_hover()`
+ // to pass it the group id.
+ this.group_hover(group_id.clone(), |el| {
+ el.bg(cx.theme().colors().element_hover)
+ })
+ })
+ .children(icon),
+ )
+ .when_some(
+ self.on_click.filter(|_| !self.disabled),
+ |this, on_click| this.on_click(move |_, cx| on_click(&self.checked.inverse(), cx)),
+ )
+ }
+}
+impl Checkbox {
pub fn new(id: impl Into<ElementId>, checked: Selection) -> Self {
Self {
id: id.into(),
@@ -36,13 +158,13 @@ impl<V: 'static> Checkbox<V> {
pub fn on_click(
mut self,
- handler: impl 'static + Fn(Selection, &mut V, &mut ViewContext<V>) + Send + Sync,
+ handler: impl 'static + Fn(&Selection, &mut WindowContext) + Send + Sync,
) -> Self {
- self.on_click = Some(Arc::new(handler));
+ self.on_click = Some(Box::new(handler));
self
}
- pub fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+ pub fn render(self, cx: &mut WindowContext) -> impl Element {
let group_id = format!("checkbox_group_{:?}", self.id);
let icon = match self.checked {
@@ -54,9 +176,9 @@ impl<V: 'static> Checkbox<V> {
.color(
// If the checkbox is disabled we change the color of the icon.
if self.disabled {
- TextColor::Disabled
+ Color::Disabled
} else {
- TextColor::Selected
+ Color::Selected
},
),
)
@@ -69,9 +191,9 @@ impl<V: 'static> Checkbox<V> {
.color(
// If the checkbox is disabled we change the color of the icon.
if self.disabled {
- TextColor::Disabled
+ Color::Disabled
} else {
- TextColor::Selected
+ Color::Selected
},
),
)
@@ -157,69 +279,7 @@ impl<V: 'static> Checkbox<V> {
)
.when_some(
self.on_click.filter(|_| !self.disabled),
- |this, on_click| {
- this.on_click(move |view, _, cx| on_click(self.checked.inverse(), view, cx))
- },
+ |this, on_click| this.on_click(move |_, cx| on_click(&self.checked.inverse(), cx)),
)
}
}
-
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use super::*;
- use crate::{h_stack, Story};
- use gpui::{Div, Render};
-
- pub struct CheckboxStory;
-
- impl Render for CheckboxStory {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- Story::container(cx)
- .child(Story::title_for::<_, Checkbox<Self>>(cx))
- .child(Story::label(cx, "Default"))
- .child(
- h_stack()
- .p_2()
- .gap_2()
- .rounded_md()
- .border()
- .border_color(cx.theme().colors().border)
- .child(Checkbox::new("checkbox-enabled", Selection::Unselected))
- .child(Checkbox::new(
- "checkbox-intermediate",
- Selection::Indeterminate,
- ))
- .child(Checkbox::new("checkbox-selected", Selection::Selected)),
- )
- .child(Story::label(cx, "Disabled"))
- .child(
- h_stack()
- .p_2()
- .gap_2()
- .rounded_md()
- .border()
- .border_color(cx.theme().colors().border)
- .child(
- Checkbox::new("checkbox-disabled", Selection::Unselected)
- .disabled(true),
- )
- .child(
- Checkbox::new(
- "checkbox-disabled-intermediate",
- Selection::Indeterminate,
- )
- .disabled(true),
- )
- .child(
- Checkbox::new("checkbox-disabled-selected", Selection::Selected)
- .disabled(true),
- ),
- )
- }
- }
-}
@@ -1,103 +1,147 @@
use std::cell::RefCell;
use std::rc::Rc;
-use crate::prelude::*;
-use crate::{v_stack, Label, List, ListEntry, ListItem, ListSeparator, ListSubHeader};
+use crate::{prelude::*, v_stack, List};
+use crate::{ListItem, ListSeparator, ListSubHeader};
use gpui::{
- overlay, px, Action, AnchorCorner, AnyElement, Bounds, Dismiss, DispatchPhase, Div,
- FocusHandle, LayoutId, ManagedView, MouseButton, MouseDownEvent, Pixels, Point, Render, View,
+ overlay, px, Action, AnchorCorner, AnyElement, AppContext, Bounds, ClickEvent, DispatchPhase,
+ Div, EventEmitter, FocusHandle, FocusableView, LayoutId, ManagedView, Manager, MouseButton,
+ MouseDownEvent, Pixels, Point, Render, RenderOnce, View, VisualContext,
};
+pub enum ContextMenuItem {
+ Separator(ListSeparator),
+ Header(ListSubHeader),
+ Entry(ListItem, Rc<dyn Fn(&ClickEvent, &mut WindowContext)>),
+}
+
pub struct ContextMenu {
- items: Vec<ListItem>,
+ items: Vec<ContextMenuItem>,
focus_handle: FocusHandle,
}
-impl ManagedView for ContextMenu {
- fn focus_handle(&self, cx: &gpui::AppContext) -> FocusHandle {
+impl FocusableView for ContextMenu {
+ fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
self.focus_handle.clone()
}
}
+impl EventEmitter<Manager> for ContextMenu {}
+
impl ContextMenu {
- pub fn new(cx: &mut WindowContext) -> Self {
- Self {
- items: Default::default(),
- focus_handle: cx.focus_handle(),
- }
+ pub fn build(
+ cx: &mut WindowContext,
+ f: impl FnOnce(Self, &mut WindowContext) -> Self,
+ ) -> View<Self> {
+ // let handle = cx.view().downgrade();
+ cx.build_view(|cx| {
+ f(
+ Self {
+ items: Default::default(),
+ focus_handle: cx.focus_handle(),
+ },
+ cx,
+ )
+ })
}
pub fn header(mut self, title: impl Into<SharedString>) -> Self {
- self.items.push(ListItem::Header(ListSubHeader::new(title)));
+ self.items
+ .push(ContextMenuItem::Header(ListSubHeader::new(title)));
self
}
pub fn separator(mut self) -> Self {
- self.items.push(ListItem::Separator(ListSeparator));
+ self.items.push(ContextMenuItem::Separator(ListSeparator));
self
}
- pub fn entry(mut self, label: Label, action: Box<dyn Action>) -> Self {
- self.items.push(ListEntry::new(label).action(action).into());
+ pub fn entry(
+ mut self,
+ view: ListItem,
+ on_click: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
+ ) -> Self {
+ self.items
+ .push(ContextMenuItem::Entry(view, Rc::new(on_click)));
self
}
+ pub fn action(self, view: ListItem, action: Box<dyn Action>) -> Self {
+ // todo: add the keybindings to the list entry
+ self.entry(view, move |_, cx| cx.dispatch_action(action.boxed_clone()))
+ }
+
pub fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
// todo!()
- cx.emit(Dismiss);
+ cx.emit(Manager::Dismiss);
}
pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
- cx.emit(Dismiss);
+ cx.emit(Manager::Dismiss);
}
}
impl Render for ContextMenu {
- type Element = Div<Self>;
- // todo!()
+ type Element = Div;
+
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
div().elevation_2(cx).flex().flex_row().child(
v_stack()
.min_w(px(200.))
.track_focus(&self.focus_handle)
- .on_mouse_down_out(|this: &mut Self, _, cx| this.cancel(&Default::default(), cx))
+ .on_mouse_down_out(
+ cx.listener(|this: &mut Self, _, cx| this.cancel(&Default::default(), cx)),
+ )
// .on_action(ContextMenu::select_first)
// .on_action(ContextMenu::select_last)
// .on_action(ContextMenu::select_next)
// .on_action(ContextMenu::select_prev)
- .on_action(ContextMenu::confirm)
- .on_action(ContextMenu::cancel)
+ .on_action(cx.listener(ContextMenu::confirm))
+ .on_action(cx.listener(ContextMenu::cancel))
.flex_none()
// .bg(cx.theme().colors().elevated_surface_background)
// .border()
// .border_color(cx.theme().colors().border)
- .child(List::new(self.items.clone())),
+ .child(
+ List::new().children(self.items.iter().map(|item| match item {
+ ContextMenuItem::Separator(separator) => {
+ separator.clone().render_into_any()
+ }
+ ContextMenuItem::Header(header) => header.clone().render_into_any(),
+ ContextMenuItem::Entry(entry, callback) => {
+ let callback = callback.clone();
+ let dismiss = cx.listener(|_, _, cx| cx.emit(Manager::Dismiss));
+
+ entry
+ .clone()
+ .on_click(move |event, cx| {
+ callback(event, cx);
+ dismiss(event, cx)
+ })
+ .render_into_any()
+ }
+ })),
+ ),
)
}
}
-pub struct MenuHandle<V: 'static, M: ManagedView> {
- id: Option<ElementId>,
- child_builder: Option<Box<dyn FnOnce(bool) -> AnyElement<V> + 'static>>,
- menu_builder: Option<Rc<dyn Fn(&mut V, &mut ViewContext<V>) -> View<M> + 'static>>,
-
+pub struct MenuHandle<M: ManagedView> {
+ id: ElementId,
+ child_builder: Option<Box<dyn FnOnce(bool) -> AnyElement + 'static>>,
+ menu_builder: Option<Rc<dyn Fn(&mut WindowContext) -> View<M> + 'static>>,
anchor: Option<AnchorCorner>,
attach: Option<AnchorCorner>,
}
-impl<V: 'static, M: ManagedView> MenuHandle<V, M> {
- pub fn id(mut self, id: impl Into<ElementId>) -> Self {
- self.id = Some(id.into());
- self
- }
-
- pub fn menu(mut self, f: impl Fn(&mut V, &mut ViewContext<V>) -> View<M> + 'static) -> Self {
+impl<M: ManagedView> MenuHandle<M> {
+ pub fn menu(mut self, f: impl Fn(&mut WindowContext) -> View<M> + 'static) -> Self {
self.menu_builder = Some(Rc::new(f));
self
}
- pub fn child<R: Component<V>>(mut self, f: impl FnOnce(bool) -> R + 'static) -> Self {
- self.child_builder = Some(Box::new(|b| f(b).render()));
+ pub fn child<R: RenderOnce>(mut self, f: impl FnOnce(bool) -> R + 'static) -> Self {
+ self.child_builder = Some(Box::new(|b| f(b).render_once().into_any()));
self
}
@@ -115,9 +159,9 @@ impl<V: 'static, M: ManagedView> MenuHandle<V, M> {
}
}
-pub fn menu_handle<V: 'static, M: ManagedView>() -> MenuHandle<V, M> {
+pub fn menu_handle<M: ManagedView>(id: impl Into<ElementId>) -> MenuHandle<M> {
MenuHandle {
- id: None,
+ id: id.into(),
child_builder: None,
menu_builder: None,
anchor: None,
@@ -125,26 +169,21 @@ pub fn menu_handle<V: 'static, M: ManagedView>() -> MenuHandle<V, M> {
}
}
-pub struct MenuHandleState<V, M> {
+pub struct MenuHandleState<M> {
menu: Rc<RefCell<Option<View<M>>>>,
position: Rc<RefCell<Point<Pixels>>>,
child_layout_id: Option<LayoutId>,
- child_element: Option<AnyElement<V>>,
- menu_element: Option<AnyElement<V>>,
+ child_element: Option<AnyElement>,
+ menu_element: Option<AnyElement>,
}
-impl<V: 'static, M: ManagedView> Element<V> for MenuHandle<V, M> {
- type ElementState = MenuHandleState<V, M>;
-
- fn element_id(&self) -> Option<gpui::ElementId> {
- Some(self.id.clone().expect("menu_handle must have an id()"))
- }
+impl<M: ManagedView> Element for MenuHandle<M> {
+ type State = MenuHandleState<M>;
fn layout(
&mut self,
- view_state: &mut V,
- element_state: Option<Self::ElementState>,
- cx: &mut crate::ViewContext<V>,
- ) -> (gpui::LayoutId, Self::ElementState) {
+ element_state: Option<Self::State>,
+ cx: &mut WindowContext,
+ ) -> (gpui::LayoutId, Self::State) {
let (menu, position) = if let Some(element_state) = element_state {
(element_state.menu, element_state.position)
} else {
@@ -154,15 +193,15 @@ impl<V: 'static, M: ManagedView> Element<V> for MenuHandle<V, M> {
let mut menu_layout_id = None;
let menu_element = menu.borrow_mut().as_mut().map(|menu| {
- let mut overlay = overlay::<V>().snap_to_window();
+ let mut overlay = overlay().snap_to_window();
if let Some(anchor) = self.anchor {
overlay = overlay.anchor(anchor);
}
overlay = overlay.position(*position.borrow());
- let mut view = overlay.child(menu.clone()).render();
- menu_layout_id = Some(view.layout(view_state, cx));
- view
+ let mut element = overlay.child(menu.clone()).into_any();
+ menu_layout_id = Some(element.layout(cx));
+ element
});
let mut child_element = self
@@ -172,7 +211,7 @@ impl<V: 'static, M: ManagedView> Element<V> for MenuHandle<V, M> {
let child_layout_id = child_element
.as_mut()
- .map(|child_element| child_element.layout(view_state, cx));
+ .map(|child_element| child_element.layout(cx));
let layout_id = cx.request_layout(
&gpui::Style::default(),
@@ -192,22 +231,21 @@ impl<V: 'static, M: ManagedView> Element<V> for MenuHandle<V, M> {
}
fn paint(
- &mut self,
+ self,
bounds: Bounds<gpui::Pixels>,
- view_state: &mut V,
- element_state: &mut Self::ElementState,
- cx: &mut crate::ViewContext<V>,
+ element_state: &mut Self::State,
+ cx: &mut WindowContext,
) {
- if let Some(child) = element_state.child_element.as_mut() {
- child.paint(view_state, cx);
+ if let Some(child) = element_state.child_element.take() {
+ child.paint(cx);
}
- if let Some(menu) = element_state.menu_element.as_mut() {
- menu.paint(view_state, cx);
+ if let Some(menu) = element_state.menu_element.take() {
+ menu.paint(cx);
return;
}
- let Some(builder) = self.menu_builder.clone() else {
+ let Some(builder) = self.menu_builder else {
return;
};
let menu = element_state.menu.clone();
@@ -215,7 +253,7 @@ impl<V: 'static, M: ManagedView> Element<V> for MenuHandle<V, M> {
let attach = self.attach.clone();
let child_layout_id = element_state.child_layout_id.clone();
- cx.on_mouse_event(move |view_state, event: &MouseDownEvent, phase, cx| {
+ cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| {
if phase == DispatchPhase::Bubble
&& event.button == MouseButton::Right
&& bounds.contains_point(&event.position)
@@ -223,15 +261,16 @@ impl<V: 'static, M: ManagedView> Element<V> for MenuHandle<V, M> {
cx.stop_propagation();
cx.prevent_default();
- let new_menu = (builder)(view_state, cx);
+ let new_menu = (builder)(cx);
let menu2 = menu.clone();
- cx.subscribe(&new_menu, move |this, modal, e, cx| match e {
- &Dismiss => {
+ cx.subscribe(&new_menu, move |modal, e, cx| match e {
+ &Manager::Dismiss => {
*menu2.borrow_mut() = None;
cx.notify();
}
})
.detach();
+ cx.focus_view(&new_menu);
*menu.borrow_mut() = Some(new_menu);
*position.borrow_mut() = if attach.is_some() && child_layout_id.is_some() {
@@ -247,116 +286,14 @@ impl<V: 'static, M: ManagedView> Element<V> for MenuHandle<V, M> {
}
}
-impl<V: 'static, M: ManagedView> Component<V> for MenuHandle<V, M> {
- fn render(self) -> AnyElement<V> {
- AnyElement::new(self)
- }
-}
-
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use super::*;
- use crate::story::Story;
- use gpui::{actions, Div, Render, VisualContext};
+impl<M: ManagedView> RenderOnce for MenuHandle<M> {
+ type Element = Self;
- actions!(PrintCurrentDate);
-
- fn build_menu(cx: &mut WindowContext, header: impl Into<SharedString>) -> View<ContextMenu> {
- cx.build_view(|cx| {
- ContextMenu::new(cx).header(header).separator().entry(
- Label::new("Print current time"),
- PrintCurrentDate.boxed_clone(),
- )
- })
+ fn element_id(&self) -> Option<gpui::ElementId> {
+ Some(self.id.clone())
}
- pub struct ContextMenuStory;
-
- impl Render for ContextMenuStory {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- Story::container(cx)
- .on_action(|_, _: &PrintCurrentDate, _| {
- if let Ok(unix_time) = std::time::UNIX_EPOCH.elapsed() {
- println!("Current Unix time is {:?}", unix_time.as_secs());
- }
- })
- .flex()
- .flex_row()
- .justify_between()
- .child(
- div()
- .flex()
- .flex_col()
- .justify_between()
- .child(
- menu_handle()
- .id("test2")
- .child(|is_open| {
- Label::new(if is_open {
- "TOP LEFT"
- } else {
- "RIGHT CLICK ME"
- })
- .render()
- })
- .menu(move |_, cx| build_menu(cx, "top left")),
- )
- .child(
- menu_handle()
- .id("test1")
- .child(|is_open| {
- Label::new(if is_open {
- "BOTTOM LEFT"
- } else {
- "RIGHT CLICK ME"
- })
- .render()
- })
- .anchor(AnchorCorner::BottomLeft)
- .attach(AnchorCorner::TopLeft)
- .menu(move |_, cx| build_menu(cx, "bottom left")),
- ),
- )
- .child(
- div()
- .flex()
- .flex_col()
- .justify_between()
- .child(
- menu_handle()
- .id("test3")
- .child(|is_open| {
- Label::new(if is_open {
- "TOP RIGHT"
- } else {
- "RIGHT CLICK ME"
- })
- .render()
- })
- .anchor(AnchorCorner::TopRight)
- .menu(move |_, cx| build_menu(cx, "top right")),
- )
- .child(
- menu_handle()
- .id("test4")
- .child(|is_open| {
- Label::new(if is_open {
- "BOTTOM RIGHT"
- } else {
- "RIGHT CLICK ME"
- })
- .render()
- })
- .anchor(AnchorCorner::BottomRight)
- .attach(AnchorCorner::TopRight)
- .menu(move |_, cx| build_menu(cx, "bottom right")),
- ),
- )
- }
+ fn render_once(self) -> Self::Element {
+ self
}
}
@@ -1,78 +0,0 @@
-use crate::prelude::*;
-use crate::{v_stack, ButtonGroup};
-
-#[derive(Component)]
-pub struct Details<V: 'static> {
- text: &'static str,
- meta: Option<&'static str>,
- actions: Option<ButtonGroup<V>>,
-}
-
-impl<V: 'static> Details<V> {
- pub fn new(text: &'static str) -> Self {
- Self {
- text,
- meta: None,
- actions: None,
- }
- }
-
- pub fn meta_text(mut self, meta: &'static str) -> Self {
- self.meta = Some(meta);
- self
- }
-
- pub fn actions(mut self, actions: ButtonGroup<V>) -> Self {
- self.actions = Some(actions);
- self
- }
-
- fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- v_stack()
- .p_1()
- .gap_0p5()
- .text_ui_sm()
- .text_color(cx.theme().colors().text)
- .size_full()
- .child(self.text)
- .children(self.meta.map(|m| m))
- .children(self.actions.map(|a| a))
- }
-}
-
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use super::*;
- use crate::{Button, Story};
- use gpui::{Div, Render};
-
- pub struct DetailsStory;
-
- impl Render for DetailsStory {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- Story::container(cx)
- .child(Story::title_for::<_, Details<Self>>(cx))
- .child(Story::label(cx, "Default"))
- .child(Details::new("The quick brown fox jumps over the lazy dog"))
- .child(Story::label(cx, "With meta"))
- .child(
- Details::new("The quick brown fox jumps over the lazy dog")
- .meta_text("Sphinx of black quartz, judge my vow."),
- )
- .child(Story::label(cx, "With meta and actions"))
- .child(
- Details::new("The quick brown fox jumps over the lazy dog")
- .meta_text("Sphinx of black quartz, judge my vow.")
- .actions(ButtonGroup::new(vec![
- Button::new("Decline"),
- Button::new("Accept").variant(crate::ButtonVariant::Filled),
- ])),
- )
- }
- }
-}
@@ -0,0 +1,19 @@
+use gpui::{div, Element, ParentElement};
+
+use crate::{Color, Icon, IconElement, IconSize, Toggle};
+
+pub fn disclosure_control(toggle: Toggle) -> impl Element {
+ match (toggle.is_toggleable(), toggle.is_toggled()) {
+ (false, _) => div(),
+ (_, true) => div().child(
+ IconElement::new(Icon::ChevronDown)
+ .color(Color::Muted)
+ .size(IconSize::Small),
+ ),
+ (_, false) => div().child(
+ IconElement::new(Icon::ChevronRight)
+ .color(Color::Muted)
+ .size(IconSize::Small),
+ ),
+ }
+}
@@ -1,3 +1,5 @@
+use gpui::{Div, RenderOnce};
+
use crate::prelude::*;
enum DividerDirection {
@@ -5,12 +7,29 @@ enum DividerDirection {
Vertical,
}
-#[derive(Component)]
+#[derive(RenderOnce)]
pub struct Divider {
direction: DividerDirection,
inset: bool,
}
+impl Component for Divider {
+ type Rendered = Div;
+
+ fn render(self, cx: &mut WindowContext) -> Self::Rendered {
+ div()
+ .map(|this| match self.direction {
+ DividerDirection::Horizontal => {
+ this.h_px().w_full().when(self.inset, |this| this.mx_1p5())
+ }
+ DividerDirection::Vertical => {
+ this.w_px().h_full().when(self.inset, |this| this.my_1p5())
+ }
+ })
+ .bg(cx.theme().colors().border_variant)
+ }
+}
+
impl Divider {
pub fn horizontal() -> Self {
Self {
@@ -31,7 +50,7 @@ impl Divider {
self
}
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+ fn render(self, cx: &mut WindowContext) -> impl Element {
div()
.map(|this| match self.direction {
DividerDirection::Horizontal => {
@@ -1,28 +0,0 @@
-use gpui::Div;
-
-use crate::{prelude::*, v_stack};
-
-/// Create an elevated surface.
-///
-/// Must be used inside of a relative parent element
-pub fn elevated_surface<V: 'static>(level: ElevationIndex, cx: &mut ViewContext<V>) -> Div<V> {
- let colors = cx.theme().colors();
-
- // let shadow = BoxShadow {
- // color: hsla(0., 0., 0., 0.1),
- // offset: point(px(0.), px(1.)),
- // blur_radius: px(3.),
- // spread_radius: px(0.),
- // };
-
- v_stack()
- .rounded_lg()
- .bg(colors.elevated_surface_background)
- .border()
- .border_color(colors.border)
- .shadow(level.shadow())
-}
-
-pub fn modal<V: 'static>(cx: &mut ViewContext<V>) -> Div<V> {
- elevated_surface(ElevationIndex::ModalSurface, cx)
-}
@@ -1,59 +0,0 @@
-use crate::prelude::*;
-use crate::{Avatar, Player};
-
-#[derive(Component)]
-pub struct Facepile {
- players: Vec<Player>,
-}
-
-impl Facepile {
- pub fn new<P: Iterator<Item = Player>>(players: P) -> Self {
- Self {
- players: players.collect(),
- }
- }
-
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- let player_count = self.players.len();
- let player_list = self.players.iter().enumerate().map(|(ix, player)| {
- let isnt_last = ix < player_count - 1;
-
- div()
- .when(isnt_last, |div| div.neg_mr_1())
- .child(Avatar::new(player.avatar_src().to_string()))
- });
- div().p_1().flex().items_center().children(player_list)
- }
-}
-
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use super::*;
- use crate::{static_players, Story};
- use gpui::{Div, Render};
-
- pub struct FacepileStory;
-
- impl Render for FacepileStory {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- let players = static_players();
-
- Story::container(cx)
- .child(Story::title_for::<_, Facepile>(cx))
- .child(Story::label(cx, "Default"))
- .child(
- div()
- .flex()
- .gap_3()
- .child(Facepile::new(players.clone().into_iter().take(1)))
- .child(Facepile::new(players.clone().into_iter().take(2)))
- .child(Facepile::new(players.clone().into_iter().take(3))),
- )
- }
- }
-}
@@ -1,4 +1,4 @@
-use gpui::{rems, svg};
+use gpui::{rems, svg, RenderOnce, Svg};
use strum::EnumIter;
use crate::prelude::*;
@@ -16,9 +16,14 @@ pub enum Icon {
ArrowLeft,
ArrowRight,
ArrowUpRight,
+ AtSign,
AudioOff,
AudioOn,
+ Bell,
+ BellOff,
+ BellRing,
Bolt,
+ CaseSensitive,
Check,
ChevronDown,
ChevronLeft,
@@ -26,12 +31,14 @@ pub enum Icon {
ChevronUp,
Close,
Collab,
+ Copilot,
Dash,
- Exit,
+ Envelope,
ExclamationTriangle,
+ Exit,
File,
- FileGeneric,
FileDoc,
+ FileGeneric,
FileGit,
FileLock,
FileRust,
@@ -44,6 +51,7 @@ pub enum Icon {
InlayHint,
MagicWand,
MagnifyingGlass,
+ MailOpen,
Maximize,
Menu,
MessageBubbles,
@@ -58,14 +66,8 @@ pub enum Icon {
Split,
SplitMessage,
Terminal,
+ WholeWord,
XCircle,
- Copilot,
- Envelope,
- Bell,
- BellOff,
- BellRing,
- MailOpen,
- AtSign,
}
impl Icon {
@@ -75,9 +77,14 @@ impl Icon {
Icon::ArrowLeft => "icons/arrow_left.svg",
Icon::ArrowRight => "icons/arrow_right.svg",
Icon::ArrowUpRight => "icons/arrow_up_right.svg",
+ Icon::AtSign => "icons/at-sign.svg",
Icon::AudioOff => "icons/speaker-off.svg",
Icon::AudioOn => "icons/speaker-loud.svg",
+ Icon::Bell => "icons/bell.svg",
+ Icon::BellOff => "icons/bell-off.svg",
+ Icon::BellRing => "icons/bell-ring.svg",
Icon::Bolt => "icons/bolt.svg",
+ Icon::CaseSensitive => "icons/case_insensitive.svg",
Icon::Check => "icons/check.svg",
Icon::ChevronDown => "icons/chevron_down.svg",
Icon::ChevronLeft => "icons/chevron_left.svg",
@@ -85,12 +92,14 @@ impl Icon {
Icon::ChevronUp => "icons/chevron_up.svg",
Icon::Close => "icons/x.svg",
Icon::Collab => "icons/user_group_16.svg",
+ Icon::Copilot => "icons/copilot.svg",
Icon::Dash => "icons/dash.svg",
- Icon::Exit => "icons/exit.svg",
+ Icon::Envelope => "icons/feedback.svg",
Icon::ExclamationTriangle => "icons/warning.svg",
+ Icon::Exit => "icons/exit.svg",
Icon::File => "icons/file.svg",
- Icon::FileGeneric => "icons/file_icons/file.svg",
Icon::FileDoc => "icons/file_icons/book.svg",
+ Icon::FileGeneric => "icons/file_icons/file.svg",
Icon::FileGit => "icons/file_icons/git.svg",
Icon::FileLock => "icons/file_icons/lock.svg",
Icon::FileRust => "icons/file_icons/rust.svg",
@@ -103,6 +112,7 @@ impl Icon {
Icon::InlayHint => "icons/inlay_hint.svg",
Icon::MagicWand => "icons/magic-wand.svg",
Icon::MagnifyingGlass => "icons/magnifying_glass.svg",
+ Icon::MailOpen => "icons/mail-open.svg",
Icon::Maximize => "icons/maximize.svg",
Icon::Menu => "icons/menu.svg",
Icon::MessageBubbles => "icons/conversations.svg",
@@ -117,30 +127,41 @@ impl Icon {
Icon::Split => "icons/split.svg",
Icon::SplitMessage => "icons/split_message.svg",
Icon::Terminal => "icons/terminal.svg",
+ Icon::WholeWord => "icons/word_search.svg",
Icon::XCircle => "icons/error.svg",
- Icon::Copilot => "icons/copilot.svg",
- Icon::Envelope => "icons/feedback.svg",
- Icon::Bell => "icons/bell.svg",
- Icon::BellOff => "icons/bell-off.svg",
- Icon::BellRing => "icons/bell-ring.svg",
- Icon::MailOpen => "icons/mail-open.svg",
- Icon::AtSign => "icons/at-sign.svg",
}
}
}
-#[derive(Component)]
+#[derive(RenderOnce)]
pub struct IconElement {
path: SharedString,
- color: TextColor,
+ color: Color,
size: IconSize,
}
+impl Component for IconElement {
+ type Rendered = Svg;
+
+ fn render(self, cx: &mut WindowContext) -> Self::Rendered {
+ let svg_size = match self.size {
+ IconSize::Small => rems(0.75),
+ IconSize::Medium => rems(0.9375),
+ };
+
+ svg()
+ .size(svg_size)
+ .flex_none()
+ .path(self.path)
+ .text_color(self.color.color(cx))
+ }
+}
+
impl IconElement {
pub fn new(icon: Icon) -> Self {
Self {
path: icon.path().into(),
- color: TextColor::default(),
+ color: Color::default(),
size: IconSize::default(),
}
}
@@ -148,12 +169,12 @@ impl IconElement {
pub fn from_path(path: impl Into<SharedString>) -> Self {
Self {
path: path.into(),
- color: TextColor::default(),
+ color: Color::default(),
size: IconSize::default(),
}
}
- pub fn color(mut self, color: TextColor) -> Self {
+ pub fn color(mut self, color: Color) -> Self {
self.color = color;
self
}
@@ -163,7 +184,7 @@ impl IconElement {
self
}
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+ fn render(self, cx: &mut WindowContext) -> impl Element {
let svg_size = match self.size {
IconSize::Small => rems(0.75),
IconSize::Medium => rems(0.9375),
@@ -176,31 +197,3 @@ impl IconElement {
.text_color(self.color.color(cx))
}
}
-
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use gpui::{Div, Render};
- use strum::IntoEnumIterator;
-
- use crate::Story;
-
- use super::*;
-
- pub struct IconStory;
-
- impl Render for IconStory {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- let icons = Icon::iter();
-
- Story::container(cx)
- .child(Story::title_for::<_, IconElement>(cx))
- .child(Story::label(cx, "All Icons"))
- .child(div().flex().gap_3().children(icons.map(IconElement::new)))
- }
- }
-}
@@ -1,40 +1,87 @@
-use crate::{h_stack, prelude::*, ClickHandler, Icon, IconElement};
-use gpui::{prelude::*, Action, AnyView, MouseButton};
-use std::sync::Arc;
+use crate::{h_stack, prelude::*, Icon, IconElement};
+use gpui::{prelude::*, Action, AnyView, Div, MouseButton, MouseDownEvent, Stateful};
-struct IconButtonHandlers<V: 'static> {
- click: Option<ClickHandler<V>>,
-}
-
-impl<V: 'static> Default for IconButtonHandlers<V> {
- fn default() -> Self {
- Self { click: None }
- }
-}
-
-#[derive(Component)]
-pub struct IconButton<V: 'static> {
+#[derive(RenderOnce)]
+pub struct IconButton {
id: ElementId,
icon: Icon,
- color: TextColor,
+ color: Color,
variant: ButtonVariant,
state: InteractionState,
selected: bool,
- tooltip: Option<Box<dyn Fn(&mut V, &mut ViewContext<V>) -> AnyView + 'static>>,
- handlers: IconButtonHandlers<V>,
+ tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView + 'static>>,
+ on_mouse_down: Option<Box<dyn Fn(&MouseDownEvent, &mut WindowContext) + 'static>>,
}
-impl<V: 'static> IconButton<V> {
+impl Component for IconButton {
+ type Rendered = Stateful<Div>;
+
+ fn render(self, cx: &mut WindowContext) -> Self::Rendered {
+ let icon_color = match (self.state, self.color) {
+ (InteractionState::Disabled, _) => Color::Disabled,
+ (InteractionState::Active, _) => Color::Selected,
+ _ => self.color,
+ };
+
+ let (mut bg_color, bg_hover_color, bg_active_color) = match self.variant {
+ ButtonVariant::Filled => (
+ cx.theme().colors().element_background,
+ cx.theme().colors().element_hover,
+ cx.theme().colors().element_active,
+ ),
+ ButtonVariant::Ghost => (
+ cx.theme().colors().ghost_element_background,
+ cx.theme().colors().ghost_element_hover,
+ cx.theme().colors().ghost_element_active,
+ ),
+ };
+
+ if self.selected {
+ bg_color = bg_hover_color;
+ }
+
+ let mut button = h_stack()
+ .id(self.id.clone())
+ .justify_center()
+ .rounded_md()
+ .p_1()
+ .bg(bg_color)
+ .cursor_pointer()
+ // Nate: Trying to figure out the right places we want to show a
+ // hover state here. I think it is a bit heavy to have it on every
+ // place we use an icon button.
+ // .hover(|style| style.bg(bg_hover_color))
+ .active(|style| style.bg(bg_active_color))
+ .child(IconElement::new(self.icon).color(icon_color));
+
+ if let Some(click_handler) = self.on_mouse_down {
+ button = button.on_mouse_down(MouseButton::Left, move |event, cx| {
+ cx.stop_propagation();
+ click_handler(event, cx);
+ })
+ }
+
+ if let Some(tooltip) = self.tooltip {
+ if !self.selected {
+ button = button.tooltip(move |cx| tooltip(cx))
+ }
+ }
+
+ button
+ }
+}
+
+impl IconButton {
pub fn new(id: impl Into<ElementId>, icon: Icon) -> Self {
Self {
id: id.into(),
icon,
- color: TextColor::default(),
+ color: Color::default(),
variant: ButtonVariant::default(),
state: InteractionState::default(),
selected: false,
tooltip: None,
- handlers: IconButtonHandlers::default(),
+ on_mouse_down: None,
}
}
@@ -43,7 +90,7 @@ impl<V: 'static> IconButton<V> {
self
}
- pub fn color(mut self, color: TextColor) -> Self {
+ pub fn color(mut self, color: Color) -> Self {
self.color = color;
self
}
@@ -63,71 +110,20 @@ impl<V: 'static> IconButton<V> {
self
}
- pub fn tooltip(
- mut self,
- tooltip: impl Fn(&mut V, &mut ViewContext<V>) -> AnyView + 'static,
- ) -> Self {
+ pub fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {
self.tooltip = Some(Box::new(tooltip));
self
}
- pub fn on_click(mut self, handler: impl 'static + Fn(&mut V, &mut ViewContext<V>)) -> Self {
- self.handlers.click = Some(Arc::new(handler));
+ pub fn on_click(
+ mut self,
+ handler: impl 'static + Fn(&MouseDownEvent, &mut WindowContext),
+ ) -> Self {
+ self.on_mouse_down = Some(Box::new(handler));
self
}
pub fn action(self, action: Box<dyn Action>) -> Self {
self.on_click(move |this, cx| cx.dispatch_action(action.boxed_clone()))
}
-
- fn render(mut self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- let icon_color = match (self.state, self.color) {
- (InteractionState::Disabled, _) => TextColor::Disabled,
- (InteractionState::Active, _) => TextColor::Selected,
- _ => self.color,
- };
-
- let (mut bg_color, bg_hover_color, bg_active_color) = match self.variant {
- ButtonVariant::Filled => (
- cx.theme().colors().element_background,
- cx.theme().colors().element_hover,
- cx.theme().colors().element_active,
- ),
- ButtonVariant::Ghost => (
- cx.theme().colors().ghost_element_background,
- cx.theme().colors().ghost_element_hover,
- cx.theme().colors().ghost_element_active,
- ),
- };
-
- if self.selected {
- bg_color = bg_hover_color;
- }
-
- let mut button = h_stack()
- .id(self.id.clone())
- .justify_center()
- .rounded_md()
- .p_1()
- .bg(bg_color)
- .cursor_pointer()
- .hover(|style| style.bg(bg_hover_color))
- .active(|style| style.bg(bg_active_color))
- .child(IconElement::new(self.icon).color(icon_color));
-
- if let Some(click_handler) = self.handlers.click.clone() {
- button = button.on_mouse_down(MouseButton::Left, move |state, event, cx| {
- cx.stop_propagation();
- click_handler(state, cx);
- })
- }
-
- if let Some(tooltip) = self.tooltip.take() {
- if !self.selected {
- button = button.tooltip(move |view: &mut V, cx| (tooltip)(view, cx))
- }
- }
-
- button
- }
}
@@ -1,23 +0,0 @@
-use gpui::px;
-
-use crate::prelude::*;
-
-#[derive(Component)]
-pub struct UnreadIndicator;
-
-impl UnreadIndicator {
- pub fn new() -> Self {
- Self
- }
-
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- div()
- .rounded_full()
- .border_2()
- .border_color(cx.theme().colors().surface_background)
- .w(px(9.0))
- .h(px(9.0))
- .z_index(2)
- .bg(cx.theme().status().info)
- }
-}
@@ -1,5 +1,5 @@
use crate::{prelude::*, Label};
-use gpui::prelude::*;
+use gpui::{prelude::*, Div, RenderOnce, Stateful};
#[derive(Default, PartialEq)]
pub enum InputVariant {
@@ -8,7 +8,7 @@ pub enum InputVariant {
Filled,
}
-#[derive(Component)]
+#[derive(RenderOnce)]
pub struct Input {
placeholder: SharedString,
value: String,
@@ -18,44 +18,10 @@ pub struct Input {
is_active: bool,
}
-impl Input {
- pub fn new(placeholder: impl Into<SharedString>) -> Self {
- Self {
- placeholder: placeholder.into(),
- value: "".to_string(),
- state: InteractionState::default(),
- variant: InputVariant::default(),
- disabled: false,
- is_active: false,
- }
- }
-
- pub fn value(mut self, value: String) -> Self {
- self.value = value;
- self
- }
-
- pub fn state(mut self, state: InteractionState) -> Self {
- self.state = state;
- self
- }
-
- pub fn variant(mut self, variant: InputVariant) -> Self {
- self.variant = variant;
- self
- }
-
- pub fn disabled(mut self, disabled: bool) -> Self {
- self.disabled = disabled;
- self
- }
-
- pub fn is_active(mut self, is_active: bool) -> Self {
- self.is_active = is_active;
- self
- }
+impl Component for Input {
+ type Rendered = Stateful<Div>;
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+ fn render(self, cx: &mut WindowContext) -> Self::Rendered {
let (input_bg, input_hover_bg, input_active_bg) = match self.variant {
InputVariant::Ghost => (
cx.theme().colors().ghost_element_background,
@@ -70,15 +36,15 @@ impl Input {
};
let placeholder_label = Label::new(self.placeholder.clone()).color(if self.disabled {
- TextColor::Disabled
+ Color::Disabled
} else {
- TextColor::Placeholder
+ Color::Placeholder
});
let label = Label::new(self.value.clone()).color(if self.disabled {
- TextColor::Disabled
+ Color::Disabled
} else {
- TextColor::Default
+ Color::Default
});
div()
@@ -93,7 +59,7 @@ impl Input {
.active(|style| style.bg(input_active_bg))
.flex()
.items_center()
- .child(div().flex().items_center().text_ui_sm().map(|this| {
+ .child(div().flex().items_center().text_ui_sm().map(move |this| {
if self.value.is_empty() {
this.child(placeholder_label)
} else {
@@ -103,25 +69,40 @@ impl Input {
}
}
-#[cfg(feature = "stories")]
-pub use stories::*;
+impl Input {
+ pub fn new(placeholder: impl Into<SharedString>) -> Self {
+ Self {
+ placeholder: placeholder.into(),
+ value: "".to_string(),
+ state: InteractionState::default(),
+ variant: InputVariant::default(),
+ disabled: false,
+ is_active: false,
+ }
+ }
+
+ pub fn value(mut self, value: String) -> Self {
+ self.value = value;
+ self
+ }
-#[cfg(feature = "stories")]
-mod stories {
- use super::*;
- use crate::Story;
- use gpui::{Div, Render};
+ pub fn state(mut self, state: InteractionState) -> Self {
+ self.state = state;
+ self
+ }
- pub struct InputStory;
+ pub fn variant(mut self, variant: InputVariant) -> Self {
+ self.variant = variant;
+ self
+ }
- impl Render for InputStory {
- type Element = Div<Self>;
+ pub fn disabled(mut self, disabled: bool) -> Self {
+ self.disabled = disabled;
+ self
+ }
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- Story::container(cx)
- .child(Story::title_for::<_, Input>(cx))
- .child(Story::label(cx, "Default"))
- .child(div().flex().child(Input::new("Search")))
- }
+ pub fn is_active(mut self, is_active: bool) -> Self {
+ self.is_active = is_active;
+ self
}
}
@@ -1,9 +1,7 @@
-use gpui::Action;
-use strum::EnumIter;
-
use crate::prelude::*;
+use gpui::{Action, Div, RenderOnce};
-#[derive(Component, Clone)]
+#[derive(RenderOnce, Clone)]
pub struct KeyBinding {
/// A keybinding consists of a key and a set of modifier keys.
/// More then one keybinding produces a chord.
@@ -12,19 +10,10 @@ pub struct KeyBinding {
key_binding: gpui::KeyBinding,
}
-impl KeyBinding {
- pub fn for_action(action: &dyn Action, cx: &mut WindowContext) -> Option<Self> {
- // todo! this last is arbitrary, we want to prefer users key bindings over defaults,
- // and vim over normal (in vim mode), etc.
- let key_binding = cx.bindings_for_action(action).last().cloned()?;
- Some(Self::new(key_binding))
- }
+impl Component for KeyBinding {
+ type Rendered = Div;
- pub fn new(key_binding: gpui::KeyBinding) -> Self {
- Self { key_binding }
- }
-
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+ fn render(self, cx: &mut WindowContext) -> Self::Rendered {
div()
.flex()
.gap_2()
@@ -42,17 +31,28 @@ impl KeyBinding {
}
}
-#[derive(Component)]
+impl KeyBinding {
+ pub fn for_action(action: &dyn Action, cx: &mut WindowContext) -> Option<Self> {
+ // todo! this last is arbitrary, we want to prefer users key bindings over defaults,
+ // and vim over normal (in vim mode), etc.
+ let key_binding = cx.bindings_for_action(action).last().cloned()?;
+ Some(Self::new(key_binding))
+ }
+
+ pub fn new(key_binding: gpui::KeyBinding) -> Self {
+ Self { key_binding }
+ }
+}
+
+#[derive(RenderOnce)]
pub struct Key {
key: SharedString,
}
-impl Key {
- pub fn new(key: impl Into<SharedString>) -> Self {
- Self { key: key.into() }
- }
+impl Component for Key {
+ type Rendered = Div;
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+ fn render(self, cx: &mut WindowContext) -> Self::Rendered {
div()
.px_2()
.py_0()
@@ -64,79 +64,8 @@ impl Key {
}
}
-// NOTE: The order the modifier keys appear in this enum impacts the order in
-// which they are rendered in the UI.
-#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
-pub enum ModifierKey {
- Control,
- Alt,
- Command,
- Shift,
-}
-
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use super::*;
- use crate::Story;
- use gpui::{actions, Div, Render};
- use itertools::Itertools;
-
- pub struct KeybindingStory;
-
- actions!(NoAction);
-
- pub fn binding(key: &str) -> gpui::KeyBinding {
- gpui::KeyBinding::new(key, NoAction {}, None)
- }
-
- impl Render for KeybindingStory {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- let all_modifier_permutations =
- ["ctrl", "alt", "cmd", "shift"].into_iter().permutations(2);
-
- Story::container(cx)
- .child(Story::title_for::<_, KeyBinding>(cx))
- .child(Story::label(cx, "Single Key"))
- .child(KeyBinding::new(binding("Z")))
- .child(Story::label(cx, "Single Key with Modifier"))
- .child(
- div()
- .flex()
- .gap_3()
- .child(KeyBinding::new(binding("ctrl-c")))
- .child(KeyBinding::new(binding("alt-c")))
- .child(KeyBinding::new(binding("cmd-c")))
- .child(KeyBinding::new(binding("shift-c"))),
- )
- .child(Story::label(cx, "Single Key with Modifier (Permuted)"))
- .child(
- div().flex().flex_col().children(
- all_modifier_permutations
- .chunks(4)
- .into_iter()
- .map(|chunk| {
- div()
- .flex()
- .gap_4()
- .py_3()
- .children(chunk.map(|permutation| {
- KeyBinding::new(binding(&*(permutation.join("-") + "-x")))
- }))
- }),
- ),
- )
- .child(Story::label(cx, "Single Key with All Modifiers"))
- .child(KeyBinding::new(binding("ctrl-alt-cmd-shift-z")))
- .child(Story::label(cx, "Chord"))
- .child(KeyBinding::new(binding("a z")))
- .child(Story::label(cx, "Chord with Modifier"))
- .child(KeyBinding::new(binding("ctrl-a shift-z")))
- .child(KeyBinding::new(binding("fn-s")))
- }
+impl Key {
+ pub fn new(key: impl Into<SharedString>) -> Self {
+ Self { key: key.into() }
}
}
@@ -1,7 +1,6 @@
-use gpui::{relative, Hsla, Text, TextRun, WindowContext};
-
use crate::prelude::*;
use crate::styled_ext::StyledExt;
+use gpui::{relative, Div, Hsla, RenderOnce, StyledText, TextRun, WindowContext};
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
pub enum LabelSize {
@@ -10,48 +9,6 @@ pub enum LabelSize {
Small,
}
-#[derive(Default, PartialEq, Copy, Clone)]
-pub enum TextColor {
- #[default]
- Default,
- Accent,
- Created,
- Deleted,
- Disabled,
- Error,
- Hidden,
- Info,
- Modified,
- Muted,
- Placeholder,
- Player(u32),
- Selected,
- Success,
- Warning,
-}
-
-impl TextColor {
- pub fn color(&self, cx: &WindowContext) -> Hsla {
- match self {
- TextColor::Default => cx.theme().colors().text,
- TextColor::Muted => cx.theme().colors().text_muted,
- TextColor::Created => cx.theme().status().created,
- TextColor::Modified => cx.theme().status().modified,
- TextColor::Deleted => cx.theme().status().deleted,
- TextColor::Disabled => cx.theme().colors().text_disabled,
- TextColor::Hidden => cx.theme().status().hidden,
- TextColor::Info => cx.theme().status().info,
- TextColor::Placeholder => cx.theme().colors().text_placeholder,
- TextColor::Accent => cx.theme().colors().text_accent,
- TextColor::Player(i) => cx.theme().styles.player.0[i.clone() as usize].cursor,
- TextColor::Error => cx.theme().status().error,
- TextColor::Selected => cx.theme().colors().text_accent,
- TextColor::Success => cx.theme().status().success,
- TextColor::Warning => cx.theme().status().warning,
- }
- }
-}
-
#[derive(Default, PartialEq, Copy, Clone)]
pub enum LineHeightStyle {
#[default]
@@ -60,47 +17,19 @@ pub enum LineHeightStyle {
UILabel,
}
-#[derive(Clone, Component)]
+#[derive(Clone, RenderOnce)]
pub struct Label {
label: SharedString,
size: LabelSize,
line_height_style: LineHeightStyle,
- color: TextColor,
+ color: Color,
strikethrough: bool,
}
-impl Label {
- pub fn new(label: impl Into<SharedString>) -> Self {
- Self {
- label: label.into(),
- size: LabelSize::Default,
- line_height_style: LineHeightStyle::default(),
- color: TextColor::Default,
- strikethrough: false,
- }
- }
-
- pub fn size(mut self, size: LabelSize) -> Self {
- self.size = size;
- self
- }
+impl Component for Label {
+ type Rendered = Div;
- pub fn color(mut self, color: TextColor) -> Self {
- self.color = color;
- self
- }
-
- pub fn line_height_style(mut self, line_height_style: LineHeightStyle) -> Self {
- self.line_height_style = line_height_style;
- self
- }
-
- pub fn set_strikethrough(mut self, strikethrough: bool) -> Self {
- self.strikethrough = strikethrough;
- self
- }
-
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+ fn render(self, cx: &mut WindowContext) -> Self::Rendered {
div()
.when(self.strikethrough, |this| {
this.relative().child(
@@ -109,7 +38,7 @@ impl Label {
.top_1_2()
.w_full()
.h_px()
- .bg(TextColor::Hidden.color(cx)),
+ .bg(Color::Hidden.color(cx)),
)
})
.map(|this| match self.size {
@@ -124,24 +53,13 @@ impl Label {
}
}
-#[derive(Component)]
-pub struct HighlightedLabel {
- label: SharedString,
- size: LabelSize,
- color: TextColor,
- highlight_indices: Vec<usize>,
- strikethrough: bool,
-}
-
-impl HighlightedLabel {
- /// shows a label with the given characters highlighted.
- /// characters are identified by utf8 byte position.
- pub fn new(label: impl Into<SharedString>, highlight_indices: Vec<usize>) -> Self {
+impl Label {
+ pub fn new(label: impl Into<SharedString>) -> Self {
Self {
label: label.into(),
size: LabelSize::Default,
- color: TextColor::Default,
- highlight_indices,
+ line_height_style: LineHeightStyle::default(),
+ color: Color::Default,
strikethrough: false,
}
}
@@ -151,17 +69,35 @@ impl HighlightedLabel {
self
}
- pub fn color(mut self, color: TextColor) -> Self {
+ pub fn color(mut self, color: Color) -> Self {
self.color = color;
self
}
+ pub fn line_height_style(mut self, line_height_style: LineHeightStyle) -> Self {
+ self.line_height_style = line_height_style;
+ self
+ }
+
pub fn set_strikethrough(mut self, strikethrough: bool) -> Self {
self.strikethrough = strikethrough;
self
}
+}
+
+#[derive(RenderOnce)]
+pub struct HighlightedLabel {
+ label: SharedString,
+ size: LabelSize,
+ color: Color,
+ highlight_indices: Vec<usize>,
+ strikethrough: bool,
+}
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+impl Component for HighlightedLabel {
+ type Rendered = Div;
+
+ fn render(self, cx: &mut WindowContext) -> Self::Rendered {
let highlight_color = cx.theme().colors().text_accent;
let mut text_style = cx.text_style().clone();
@@ -207,51 +143,48 @@ impl HighlightedLabel {
.my_auto()
.w_full()
.h_px()
- .bg(TextColor::Hidden.color(cx)),
+ .bg(Color::Hidden.color(cx)),
)
})
.map(|this| match self.size {
LabelSize::Default => this.text_ui(),
LabelSize::Small => this.text_ui_sm(),
})
- .child(Text::styled(self.label, runs))
+ .child(StyledText::new(self.label, runs))
}
}
-/// A run of text that receives the same style.
-struct Run {
- pub text: String,
- pub color: Hsla,
-}
-
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use super::*;
- use crate::Story;
- use gpui::{Div, Render};
+impl HighlightedLabel {
+ /// shows a label with the given characters highlighted.
+ /// characters are identified by utf8 byte position.
+ pub fn new(label: impl Into<SharedString>, highlight_indices: Vec<usize>) -> Self {
+ Self {
+ label: label.into(),
+ size: LabelSize::Default,
+ color: Color::Default,
+ highlight_indices,
+ strikethrough: false,
+ }
+ }
- pub struct LabelStory;
+ pub fn size(mut self, size: LabelSize) -> Self {
+ self.size = size;
+ self
+ }
- impl Render for LabelStory {
- type Element = Div<Self>;
+ pub fn color(mut self, color: Color) -> Self {
+ self.color = color;
+ self
+ }
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- Story::container(cx)
- .child(Story::title_for::<_, Label>(cx))
- .child(Story::label(cx, "Default"))
- .child(Label::new("Hello, world!"))
- .child(Story::label(cx, "Highlighted"))
- .child(HighlightedLabel::new(
- "Hello, world!",
- vec![0, 1, 2, 7, 8, 12],
- ))
- .child(HighlightedLabel::new(
- "Héllo, world!",
- vec![0, 1, 3, 8, 9, 13],
- ))
- }
+ pub fn set_strikethrough(mut self, strikethrough: bool) -> Self {
+ self.strikethrough = strikethrough;
+ self
}
}
+
+/// A run of text that receives the same style.
+struct Run {
+ pub text: String,
+ pub color: Hsla,
+}
@@ -1,6 +1,9 @@
-use gpui::{div, Action};
+use gpui::{
+ div, px, AnyElement, ClickEvent, Div, RenderOnce, Stateful, StatefulInteractiveElement,
+};
+use smallvec::SmallVec;
+use std::rc::Rc;
-use crate::settings::user_settings;
use crate::{
disclosure_control, h_stack, v_stack, Avatar, Icon, IconElement, IconSize, Label, Toggle,
};
@@ -22,7 +25,7 @@ pub enum ListHeaderMeta {
Text(Label),
}
-#[derive(Component)]
+#[derive(RenderOnce)]
pub struct ListHeader {
label: SharedString,
left_icon: Option<Icon>,
@@ -31,33 +34,10 @@ pub struct ListHeader {
toggle: Toggle,
}
-impl ListHeader {
- pub fn new(label: impl Into<SharedString>) -> Self {
- Self {
- label: label.into(),
- left_icon: None,
- meta: None,
- variant: ListItemVariant::default(),
- toggle: Toggle::NotToggleable,
- }
- }
-
- pub fn toggle(mut self, toggle: Toggle) -> Self {
- self.toggle = toggle;
- self
- }
-
- pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
- self.left_icon = left_icon;
- self
- }
-
- pub fn meta(mut self, meta: Option<ListHeaderMeta>) -> Self {
- self.meta = meta;
- self
- }
+impl Component for ListHeader {
+ type Rendered = Div;
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+ fn render(self, cx: &mut WindowContext) -> Self::Rendered {
let disclosure_control = disclosure_control(self.toggle);
let meta = match self.meta {
@@ -67,7 +47,7 @@ impl ListHeader {
.items_center()
.children(icons.into_iter().map(|i| {
IconElement::new(i)
- .color(TextColor::Muted)
+ .color(Color::Muted)
.size(IconSize::Small)
})),
),
@@ -79,11 +59,6 @@ impl ListHeader {
h_stack()
.w_full()
.bg(cx.theme().colors().surface_background)
- // TODO: Add focus state
- // .when(self.state == InteractionState::Focused, |this| {
- // this.border()
- // .border_color(cx.theme().colors().border_focused)
- // })
.relative()
.child(
div()
@@ -105,10 +80,10 @@ impl ListHeader {
.items_center()
.children(self.left_icon.map(|i| {
IconElement::new(i)
- .color(TextColor::Muted)
+ .color(Color::Muted)
.size(IconSize::Small)
}))
- .child(Label::new(self.label.clone()).color(TextColor::Muted)),
+ .child(Label::new(self.label.clone()).color(Color::Muted)),
)
.child(disclosure_control),
)
@@ -117,7 +92,94 @@ impl ListHeader {
}
}
-#[derive(Component, Clone)]
+impl ListHeader {
+ pub fn new(label: impl Into<SharedString>) -> Self {
+ Self {
+ label: label.into(),
+ left_icon: None,
+ meta: None,
+ variant: ListItemVariant::default(),
+ toggle: Toggle::NotToggleable,
+ }
+ }
+
+ pub fn toggle(mut self, toggle: Toggle) -> Self {
+ self.toggle = toggle;
+ self
+ }
+
+ pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
+ self.left_icon = left_icon;
+ self
+ }
+
+ pub fn meta(mut self, meta: Option<ListHeaderMeta>) -> Self {
+ self.meta = meta;
+ self
+ }
+
+ // before_ship!("delete")
+ // fn render<V: 'static>(self, cx: &mut WindowContext) -> impl Element<V> {
+ // let disclosure_control = disclosure_control(self.toggle);
+
+ // let meta = match self.meta {
+ // Some(ListHeaderMeta::Tools(icons)) => div().child(
+ // h_stack()
+ // .gap_2()
+ // .items_center()
+ // .children(icons.into_iter().map(|i| {
+ // IconElement::new(i)
+ // .color(TextColor::Muted)
+ // .size(IconSize::Small)
+ // })),
+ // ),
+ // Some(ListHeaderMeta::Button(label)) => div().child(label),
+ // Some(ListHeaderMeta::Text(label)) => div().child(label),
+ // None => div(),
+ // };
+
+ // h_stack()
+ // .w_full()
+ // .bg(cx.theme().colors().surface_background)
+ // // TODO: Add focus state
+ // // .when(self.state == InteractionState::Focused, |this| {
+ // // this.border()
+ // // .border_color(cx.theme().colors().border_focused)
+ // // })
+ // .relative()
+ // .child(
+ // div()
+ // .h_5()
+ // .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
+ // .flex()
+ // .flex_1()
+ // .items_center()
+ // .justify_between()
+ // .w_full()
+ // .gap_1()
+ // .child(
+ // h_stack()
+ // .gap_1()
+ // .child(
+ // div()
+ // .flex()
+ // .gap_1()
+ // .items_center()
+ // .children(self.left_icon.map(|i| {
+ // IconElement::new(i)
+ // .color(TextColor::Muted)
+ // .size(IconSize::Small)
+ // }))
+ // .child(Label::new(self.label.clone()).color(TextColor::Muted)),
+ // )
+ // .child(disclosure_control),
+ // )
+ // .child(meta),
+ // )
+ // }
+}
+
+#[derive(RenderOnce, Clone)]
pub struct ListSubHeader {
label: SharedString,
left_icon: Option<Icon>,
@@ -137,8 +199,12 @@ impl ListSubHeader {
self.left_icon = left_icon;
self
}
+}
+
+impl Component for ListSubHeader {
+ type Rendered = Div;
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+ fn render(self, cx: &mut WindowContext) -> Self::Rendered {
h_stack().flex_1().w_full().relative().py_1().child(
div()
.h_6()
@@ -156,10 +222,10 @@ impl ListSubHeader {
.items_center()
.children(self.left_icon.map(|i| {
IconElement::new(i)
- .color(TextColor::Muted)
+ .color(Color::Muted)
.size(IconSize::Small)
}))
- .child(Label::new(self.label.clone()).color(TextColor::Muted)),
+ .child(Label::new(self.label.clone()).color(Color::Muted)),
),
)
}
@@ -172,55 +238,9 @@ pub enum ListEntrySize {
Medium,
}
-#[derive(Component, Clone)]
-pub enum ListItem {
- Entry(ListEntry),
- Separator(ListSeparator),
- Header(ListSubHeader),
-}
-
-impl From<ListEntry> for ListItem {
- fn from(entry: ListEntry) -> Self {
- Self::Entry(entry)
- }
-}
-
-impl From<ListSeparator> for ListItem {
- fn from(entry: ListSeparator) -> Self {
- Self::Separator(entry)
- }
-}
-
-impl From<ListSubHeader> for ListItem {
- fn from(entry: ListSubHeader) -> Self {
- Self::Header(entry)
- }
-}
-
-impl ListItem {
- fn render<V: 'static>(self, view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- match self {
- ListItem::Entry(entry) => div().child(entry.render(view, cx)),
- ListItem::Separator(separator) => div().child(separator.render(view, cx)),
- ListItem::Header(header) => div().child(header.render(view, cx)),
- }
- }
-
- pub fn new(label: Label) -> Self {
- Self::Entry(ListEntry::new(label))
- }
-
- pub fn as_entry(&mut self) -> Option<&mut ListEntry> {
- if let Self::Entry(entry) = self {
- Some(entry)
- } else {
- None
- }
- }
-}
-
-#[derive(Component)]
-pub struct ListEntry {
+#[derive(RenderOnce)]
+pub struct ListItem {
+ id: ElementId,
disabled: bool,
// TODO: Reintroduce this
// disclosure_control_style: DisclosureControlVisibility,
@@ -231,15 +251,14 @@ pub struct ListEntry {
size: ListEntrySize,
toggle: Toggle,
variant: ListItemVariant,
- on_click: Option<Box<dyn Action>>,
+ on_click: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
}
-impl Clone for ListEntry {
+impl Clone for ListItem {
fn clone(&self) -> Self {
Self {
+ id: self.id.clone(),
disabled: self.disabled,
- // TODO: Reintroduce this
- // disclosure_control_style: DisclosureControlVisibility,
indent_level: self.indent_level,
label: self.label.clone(),
left_slot: self.left_slot.clone(),
@@ -247,14 +266,15 @@ impl Clone for ListEntry {
size: self.size,
toggle: self.toggle,
variant: self.variant,
- on_click: self.on_click.as_ref().map(|opt| opt.boxed_clone()),
+ on_click: self.on_click.clone(),
}
}
}
-impl ListEntry {
- pub fn new(label: Label) -> Self {
+impl ListItem {
+ pub fn new(id: impl Into<ElementId>, label: Label) -> Self {
Self {
+ id: id.into(),
disabled: false,
indent_level: 0,
label,
@@ -267,8 +287,8 @@ impl ListEntry {
}
}
- pub fn action(mut self, action: impl Into<Box<dyn Action>>) -> Self {
- self.on_click = Some(action.into());
+ pub fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self {
+ self.on_click = Some(Rc::new(handler));
self
}
@@ -306,16 +326,18 @@ impl ListEntry {
self.size = size;
self
}
+}
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- let settings = user_settings(cx);
+impl Component for ListItem {
+ type Rendered = Stateful<Div>;
+ fn render(self, cx: &mut WindowContext) -> Self::Rendered {
let left_content = match self.left_slot.clone() {
Some(GraphicSlot::Icon(i)) => Some(
h_stack().child(
IconElement::new(i)
.size(IconSize::Small)
- .color(TextColor::Muted),
+ .color(Color::Muted),
),
),
Some(GraphicSlot::Avatar(src)) => Some(h_stack().child(Avatar::new(src))),
@@ -328,21 +350,20 @@ impl ListEntry {
ListEntrySize::Medium => div().h_7(),
};
div()
+ .id(self.id)
.relative()
.hover(|mut style| {
style.background = Some(cx.theme().colors().editor_background.into());
style
})
- .on_mouse_down(gpui::MouseButton::Left, {
- let action = self.on_click.map(|action| action.boxed_clone());
-
- move |entry: &mut V, event, cx| {
- if let Some(action) = action.as_ref() {
- cx.dispatch_action(action.boxed_clone());
+ .on_click({
+ let on_click = self.on_click.clone();
+ move |event, cx| {
+ if let Some(on_click) = &on_click {
+ (on_click)(event, cx)
}
}
})
- .group("")
.bg(cx.theme().colors().surface_background)
// TODO: Add focus state
// .when(self.state == InteractionState::Focused, |this| {
@@ -355,7 +376,7 @@ impl ListEntry {
// .ml(rems(0.75 * self.indent_level as f32))
.children((0..self.indent_level).map(|_| {
div()
- .w(*settings.list_indent_depth)
+ .w(px(4.))
.h_full()
.flex()
.justify_center()
@@ -377,36 +398,58 @@ impl ListEntry {
}
}
-#[derive(Clone, Component)]
+#[derive(RenderOnce, Clone)]
pub struct ListSeparator;
impl ListSeparator {
pub fn new() -> Self {
Self
}
+}
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+impl Component for ListSeparator {
+ type Rendered = Div;
+
+ fn render(self, cx: &mut WindowContext) -> Self::Rendered {
div().h_px().w_full().bg(cx.theme().colors().border_variant)
}
}
-#[derive(Component)]
+#[derive(RenderOnce)]
pub struct List {
- items: Vec<ListItem>,
/// Message to display when the list is empty
/// Defaults to "No items"
empty_message: SharedString,
header: Option<ListHeader>,
toggle: Toggle,
+ children: SmallVec<[AnyElement; 2]>,
+}
+
+impl Component for List {
+ type Rendered = Div;
+
+ fn render(self, cx: &mut WindowContext) -> Self::Rendered {
+ let list_content = match (self.children.is_empty(), self.toggle) {
+ (false, _) => div().children(self.children),
+ (true, Toggle::Toggled(false)) => div(),
+ (true, _) => div().child(Label::new(self.empty_message.clone()).color(Color::Muted)),
+ };
+
+ v_stack()
+ .w_full()
+ .py_1()
+ .children(self.header.map(|header| header))
+ .child(list_content)
+ }
}
impl List {
- pub fn new(items: Vec<ListItem>) -> Self {
+ pub fn new() -> Self {
Self {
- items,
empty_message: "No items".into(),
header: None,
toggle: Toggle::NotToggleable,
+ children: SmallVec::new(),
}
}
@@ -424,20 +467,10 @@ impl List {
self.toggle = toggle;
self
}
+}
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- let list_content = match (self.items.is_empty(), self.toggle) {
- (false, _) => div().children(self.items),
- (true, Toggle::Toggled(false)) => div(),
- (true, _) => {
- div().child(Label::new(self.empty_message.clone()).color(TextColor::Muted))
- }
- };
-
- v_stack()
- .w_full()
- .py_1()
- .children(self.header.map(|header| header))
- .child(list_content)
+impl ParentElement for List {
+ fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
+ &mut self.children
}
}
@@ -1,81 +0,0 @@
-use gpui::AnyElement;
-use smallvec::SmallVec;
-
-use crate::{h_stack, prelude::*, v_stack, Button, Icon, IconButton, Label};
-
-#[derive(Component)]
-pub struct Modal<V: 'static> {
- id: ElementId,
- title: Option<SharedString>,
- primary_action: Option<Button<V>>,
- secondary_action: Option<Button<V>>,
- children: SmallVec<[AnyElement<V>; 2]>,
-}
-
-impl<V: 'static> Modal<V> {
- pub fn new(id: impl Into<ElementId>) -> Self {
- Self {
- id: id.into(),
- title: None,
- primary_action: None,
- secondary_action: None,
- children: SmallVec::new(),
- }
- }
-
- pub fn title(mut self, title: impl Into<SharedString>) -> Self {
- self.title = Some(title.into());
- self
- }
-
- pub fn primary_action(mut self, action: Button<V>) -> Self {
- self.primary_action = Some(action);
- self
- }
-
- pub fn secondary_action(mut self, action: Button<V>) -> Self {
- self.secondary_action = Some(action);
- self
- }
-
- fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- v_stack()
- .id(self.id.clone())
- .w_96()
- // .rounded_xl()
- .bg(cx.theme().colors().background)
- .border()
- .border_color(cx.theme().colors().border)
- .shadow_2xl()
- .child(
- h_stack()
- .justify_between()
- .p_1()
- .border_b()
- .border_color(cx.theme().colors().border)
- .child(div().children(self.title.clone().map(|t| Label::new(t))))
- .child(IconButton::new("close", Icon::Close)),
- )
- .child(v_stack().p_1().children(self.children))
- .when(
- self.primary_action.is_some() || self.secondary_action.is_some(),
- |this| {
- this.child(
- h_stack()
- .border_t()
- .border_color(cx.theme().colors().border)
- .p_1()
- .justify_end()
- .children(self.secondary_action)
- .children(self.primary_action),
- )
- },
- )
- }
-}
-
-impl<V: 'static> ParentComponent<V> for Modal<V> {
- fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
- &mut self.children
- }
-}
@@ -1,40 +0,0 @@
-use gpui::rems;
-
-use crate::prelude::*;
-use crate::{h_stack, Icon};
-
-#[derive(Component)]
-pub struct NotificationToast {
- label: SharedString,
- icon: Option<Icon>,
-}
-
-impl NotificationToast {
- pub fn new(label: SharedString) -> Self {
- Self { label, icon: None }
- }
-
- pub fn icon<I>(mut self, icon: I) -> Self
- where
- I: Into<Option<Icon>>,
- {
- self.icon = icon.into();
- self
- }
-
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- h_stack()
- .z_index(5)
- .absolute()
- .top_1()
- .right_1()
- .w(rems(9999.))
- .max_w_56()
- .py_1()
- .px_1p5()
- .rounded_lg()
- .shadow_md()
- .bg(cx.theme().colors().elevated_surface_background)
- .child(div().size_full().child(self.label.clone()))
- }
-}
@@ -1,204 +0,0 @@
-use crate::{h_stack, prelude::*, v_stack, KeyBinding, Label};
-use gpui::prelude::*;
-
-#[derive(Component)]
-pub struct Palette {
- id: ElementId,
- input_placeholder: SharedString,
- empty_string: SharedString,
- items: Vec<PaletteItem>,
- default_order: OrderMethod,
-}
-
-impl Palette {
- pub fn new(id: impl Into<ElementId>) -> Self {
- Self {
- id: id.into(),
- input_placeholder: "Find something...".into(),
- empty_string: "No items found.".into(),
- items: vec![],
- default_order: OrderMethod::default(),
- }
- }
-
- pub fn items(mut self, items: Vec<PaletteItem>) -> Self {
- self.items = items;
- self
- }
-
- pub fn placeholder(mut self, input_placeholder: impl Into<SharedString>) -> Self {
- self.input_placeholder = input_placeholder.into();
- self
- }
-
- pub fn empty_string(mut self, empty_string: impl Into<SharedString>) -> Self {
- self.empty_string = empty_string.into();
- self
- }
-
- // TODO: Hook up sort order
- pub fn default_order(mut self, default_order: OrderMethod) -> Self {
- self.default_order = default_order;
- self
- }
-
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- v_stack()
- .id(self.id.clone())
- .w_96()
- .rounded_lg()
- .bg(cx.theme().colors().elevated_surface_background)
- .border()
- .border_color(cx.theme().colors().border)
- .child(
- v_stack()
- .gap_px()
- .child(v_stack().py_0p5().px_1().child(div().px_2().py_0p5().child(
- Label::new(self.input_placeholder.clone()).color(TextColor::Placeholder),
- )))
- .child(
- div()
- .h_px()
- .w_full()
- .bg(cx.theme().colors().element_background),
- )
- .child(
- v_stack()
- .id("items")
- .py_0p5()
- .px_1()
- .grow()
- .max_h_96()
- .overflow_y_scroll()
- .children(
- vec![if self.items.is_empty() {
- Some(
- h_stack().justify_between().px_2().py_1().child(
- Label::new(self.empty_string.clone())
- .color(TextColor::Muted),
- ),
- )
- } else {
- None
- }]
- .into_iter()
- .flatten(),
- )
- .children(self.items.into_iter().enumerate().map(|(index, item)| {
- h_stack()
- .id(index)
- .justify_between()
- .px_2()
- .py_0p5()
- .rounded_lg()
- .hover(|style| {
- style.bg(cx.theme().colors().ghost_element_hover)
- })
- .active(|style| {
- style.bg(cx.theme().colors().ghost_element_active)
- })
- .child(item)
- })),
- ),
- )
- }
-}
-
-#[derive(Component)]
-pub struct PaletteItem {
- pub label: SharedString,
- pub sublabel: Option<SharedString>,
- pub key_binding: Option<KeyBinding>,
-}
-
-impl PaletteItem {
- pub fn new(label: impl Into<SharedString>) -> Self {
- Self {
- label: label.into(),
- sublabel: None,
- key_binding: None,
- }
- }
-
- pub fn label(mut self, label: impl Into<SharedString>) -> Self {
- self.label = label.into();
- self
- }
-
- pub fn sublabel(mut self, sublabel: impl Into<Option<SharedString>>) -> Self {
- self.sublabel = sublabel.into();
- self
- }
-
- pub fn key_binding(mut self, key_binding: impl Into<Option<KeyBinding>>) -> Self {
- self.key_binding = key_binding.into();
- self
- }
-
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- div()
- .flex()
- .flex_row()
- .grow()
- .justify_between()
- .child(
- v_stack()
- .child(Label::new(self.label.clone()))
- .children(self.sublabel.clone().map(|sublabel| Label::new(sublabel))),
- )
- .children(self.key_binding)
- }
-}
-
-use gpui::ElementId;
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use gpui::{Div, Render};
-
- use crate::{binding, Story};
-
- use super::*;
-
- pub struct PaletteStory;
-
- impl Render for PaletteStory {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- {
- Story::container(cx)
- .child(Story::title_for::<_, Palette>(cx))
- .child(Story::label(cx, "Default"))
- .child(Palette::new("palette-1"))
- .child(Story::label(cx, "With Items"))
- .child(
- Palette::new("palette-2")
- .placeholder("Execute a command...")
- .items(vec![
- PaletteItem::new("theme selector: toggle")
- .key_binding(KeyBinding::new(binding("cmd-k cmd-t"))),
- PaletteItem::new("assistant: inline assist")
- .key_binding(KeyBinding::new(binding("cmd-enter"))),
- PaletteItem::new("assistant: quote selection")
- .key_binding(KeyBinding::new(binding("cmd-<"))),
- PaletteItem::new("assistant: toggle focus")
- .key_binding(KeyBinding::new(binding("cmd-?"))),
- PaletteItem::new("auto update: check"),
- PaletteItem::new("auto update: view release notes"),
- PaletteItem::new("branches: open recent")
- .key_binding(KeyBinding::new(binding("cmd-alt-b"))),
- PaletteItem::new("chat panel: toggle focus"),
- PaletteItem::new("cli: install"),
- PaletteItem::new("client: sign in"),
- PaletteItem::new("client: sign out"),
- PaletteItem::new("editor: cancel")
- .key_binding(KeyBinding::new(binding("escape"))),
- ]),
- )
- }
- }
- }
-}
@@ -1,150 +0,0 @@
-use gpui::{prelude::*, AbsoluteLength, AnyElement};
-use smallvec::SmallVec;
-
-use crate::prelude::*;
-use crate::settings::user_settings;
-use crate::v_stack;
-
-#[derive(Default, Debug, PartialEq, Eq, Hash, Clone, Copy)]
-pub enum PanelAllowedSides {
- LeftOnly,
- RightOnly,
- BottomOnly,
- #[default]
- LeftAndRight,
- All,
-}
-
-impl PanelAllowedSides {
- /// Return a `HashSet` that contains the allowable `PanelSide`s.
- pub fn allowed_sides(&self) -> HashSet<PanelSide> {
- match self {
- Self::LeftOnly => HashSet::from_iter([PanelSide::Left]),
- Self::RightOnly => HashSet::from_iter([PanelSide::Right]),
- Self::BottomOnly => HashSet::from_iter([PanelSide::Bottom]),
- Self::LeftAndRight => HashSet::from_iter([PanelSide::Left, PanelSide::Right]),
- Self::All => HashSet::from_iter([PanelSide::Left, PanelSide::Right, PanelSide::Bottom]),
- }
- }
-}
-
-#[derive(Default, Debug, PartialEq, Eq, Hash, Clone, Copy)]
-pub enum PanelSide {
- #[default]
- Left,
- Right,
- Bottom,
-}
-
-use std::collections::HashSet;
-
-#[derive(Component)]
-pub struct Panel<V: 'static> {
- id: ElementId,
- current_side: PanelSide,
- /// Defaults to PanelAllowedSides::LeftAndRight
- allowed_sides: PanelAllowedSides,
- initial_width: AbsoluteLength,
- width: Option<AbsoluteLength>,
- children: SmallVec<[AnyElement<V>; 2]>,
-}
-
-impl<V: 'static> Panel<V> {
- pub fn new(id: impl Into<ElementId>, cx: &mut WindowContext) -> Self {
- let settings = user_settings(cx);
-
- Self {
- id: id.into(),
- current_side: PanelSide::default(),
- allowed_sides: PanelAllowedSides::default(),
- initial_width: *settings.default_panel_size,
- width: None,
- children: SmallVec::new(),
- }
- }
-
- pub fn initial_width(mut self, initial_width: AbsoluteLength) -> Self {
- self.initial_width = initial_width;
- self
- }
-
- pub fn width(mut self, width: AbsoluteLength) -> Self {
- self.width = Some(width);
- self
- }
-
- pub fn allowed_sides(mut self, allowed_sides: PanelAllowedSides) -> Self {
- self.allowed_sides = allowed_sides;
- self
- }
-
- pub fn side(mut self, side: PanelSide) -> Self {
- let allowed_sides = self.allowed_sides.allowed_sides();
-
- if allowed_sides.contains(&side) {
- self.current_side = side;
- } else {
- panic!(
- "The panel side {:?} was not added as allowed before it was set.",
- side
- );
- }
- self
- }
-
- fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- let current_size = self.width.unwrap_or(self.initial_width);
-
- v_stack()
- .id(self.id.clone())
- .flex_initial()
- .map(|this| match self.current_side {
- PanelSide::Left | PanelSide::Right => this.h_full().w(current_size),
- PanelSide::Bottom => this,
- })
- .map(|this| match self.current_side {
- PanelSide::Left => this.border_r(),
- PanelSide::Right => this.border_l(),
- PanelSide::Bottom => this.border_b().w_full().h(current_size),
- })
- .bg(cx.theme().colors().surface_background)
- .border_color(cx.theme().colors().border)
- .children(self.children)
- }
-}
-
-impl<V: 'static> ParentComponent<V> for Panel<V> {
- fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
- &mut self.children
- }
-}
-
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use super::*;
- use crate::{Label, Story};
- use gpui::{Div, InteractiveComponent, Render};
-
- pub struct PanelStory;
-
- impl Render for PanelStory {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- Story::container(cx)
- .child(Story::title_for::<_, Panel<Self>>(cx))
- .child(Story::label(cx, "Default"))
- .child(
- Panel::new("panel", cx).child(
- div()
- .id("panel-contents")
- .overflow_y_scroll()
- .children((0..100).map(|ix| Label::new(format!("Item {}", ix + 1)))),
- ),
- )
- }
- }
-}
@@ -1,174 +0,0 @@
-use gpui::{Hsla, ViewContext};
-
-use crate::prelude::*;
-
-/// Represents a person with a Zed account's public profile.
-/// All data in this struct should be considered public.
-pub struct PublicPlayer {
- pub username: SharedString,
- pub avatar: SharedString,
- pub is_contact: bool,
-}
-
-impl PublicPlayer {
- pub fn new(username: impl Into<SharedString>, avatar: impl Into<SharedString>) -> Self {
- Self {
- username: username.into(),
- avatar: avatar.into(),
- is_contact: false,
- }
- }
-}
-
-#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
-pub enum PlayerStatus {
- #[default]
- Offline,
- Online,
- InCall,
- Away,
- DoNotDisturb,
- Invisible,
-}
-
-#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
-pub enum MicStatus {
- Muted,
- #[default]
- Unmuted,
-}
-
-impl MicStatus {
- pub fn inverse(&self) -> Self {
- match self {
- Self::Muted => Self::Unmuted,
- Self::Unmuted => Self::Muted,
- }
- }
-}
-
-#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
-pub enum VideoStatus {
- On,
- #[default]
- Off,
-}
-
-impl VideoStatus {
- pub fn inverse(&self) -> Self {
- match self {
- Self::On => Self::Off,
- Self::Off => Self::On,
- }
- }
-}
-
-#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
-pub enum ScreenShareStatus {
- Shared,
- #[default]
- NotShared,
-}
-
-impl ScreenShareStatus {
- pub fn inverse(&self) -> Self {
- match self {
- Self::Shared => Self::NotShared,
- Self::NotShared => Self::Shared,
- }
- }
-}
-
-#[derive(Clone)]
-pub struct PlayerCallStatus {
- pub mic_status: MicStatus,
- /// Indicates if the player is currently speaking
- /// And the intensity of the volume coming through
- ///
- /// 0.0 - 1.0
- pub voice_activity: f32,
- pub video_status: VideoStatus,
- pub screen_share_status: ScreenShareStatus,
- pub in_current_project: bool,
- pub disconnected: bool,
- pub following: Option<Vec<Player>>,
- pub followers: Option<Vec<Player>>,
-}
-
-impl PlayerCallStatus {
- pub fn new() -> Self {
- Self {
- mic_status: MicStatus::default(),
- voice_activity: 0.,
- video_status: VideoStatus::default(),
- screen_share_status: ScreenShareStatus::default(),
- in_current_project: true,
- disconnected: false,
- following: None,
- followers: None,
- }
- }
-}
-
-#[derive(PartialEq, Clone)]
-pub struct Player {
- index: usize,
- avatar_src: String,
- username: String,
- status: PlayerStatus,
-}
-
-#[derive(Clone)]
-pub struct PlayerWithCallStatus {
- player: Player,
- call_status: PlayerCallStatus,
-}
-
-impl PlayerWithCallStatus {
- pub fn new(player: Player, call_status: PlayerCallStatus) -> Self {
- Self {
- player,
- call_status,
- }
- }
-
- pub fn get_player(&self) -> &Player {
- &self.player
- }
-
- pub fn get_call_status(&self) -> &PlayerCallStatus {
- &self.call_status
- }
-}
-
-impl Player {
- pub fn new(index: usize, avatar_src: String, username: String) -> Self {
- Self {
- index,
- avatar_src,
- username,
- status: Default::default(),
- }
- }
-
- pub fn set_status(mut self, status: PlayerStatus) -> Self {
- self.status = status;
- self
- }
-
- pub fn cursor_color<V: 'static>(&self, cx: &mut ViewContext<V>) -> Hsla {
- cx.theme().styles.player.0[self.index % cx.theme().styles.player.0.len()].cursor
- }
-
- pub fn selection_color<V: 'static>(&self, cx: &mut ViewContext<V>) -> Hsla {
- cx.theme().styles.player.0[self.index % cx.theme().styles.player.0.len()].selection
- }
-
- pub fn avatar_src(&self) -> &str {
- &self.avatar_src
- }
-
- pub fn index(&self) -> usize {
- self.index
- }
-}
@@ -1,61 +0,0 @@
-use crate::prelude::*;
-use crate::{Avatar, Facepile, PlayerWithCallStatus};
-
-#[derive(Component)]
-pub struct PlayerStack {
- player_with_call_status: PlayerWithCallStatus,
-}
-
-impl PlayerStack {
- pub fn new(player_with_call_status: PlayerWithCallStatus) -> Self {
- Self {
- player_with_call_status,
- }
- }
-
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- let player = self.player_with_call_status.get_player();
-
- let followers = self
- .player_with_call_status
- .get_call_status()
- .followers
- .as_ref()
- .map(|followers| followers.clone());
-
- // if we have no followers return a slightly different element
- // if mic_status == muted add a red ring to avatar
-
- div()
- .h_full()
- .flex()
- .flex_col()
- .gap_px()
- .justify_center()
- .child(
- div()
- .flex()
- .justify_center()
- .w_full()
- .child(div().w_4().h_0p5().rounded_sm().bg(player.cursor_color(cx))),
- )
- .child(
- div()
- .flex()
- .items_center()
- .justify_center()
- .h_6()
- .pl_1()
- .rounded_lg()
- .bg(if followers.is_none() {
- cx.theme().styles.system.transparent
- } else {
- player.selection_color(cx)
- })
- .child(Avatar::new(player.avatar_src().to_string()))
- .children(followers.map(|followers| {
- div().neg_ml_2().child(Facepile::new(followers.into_iter()))
- })),
- )
- }
-}
@@ -5,13 +5,13 @@ use crate::StyledExt;
/// Horizontally stacks elements.
///
/// Sets `flex()`, `flex_row()`, `items_center()`
-pub fn h_stack<V: 'static>() -> Div<V> {
+pub fn h_stack() -> Div {
div().h_flex()
}
/// Vertically stacks elements.
///
/// Sets `flex()`, `flex_col()`
-pub fn v_stack<V: 'static>() -> Div<V> {
+pub fn v_stack() -> Div {
div().v_flex()
}
@@ -0,0 +1,27 @@
+#[cfg(feature = "stories")]
+pub use stories::*;
+
+#[cfg(feature = "stories")]
+mod stories {
+ use super::*;
+ use crate::Story;
+ use gpui::{Div, Render};
+
+ pub struct AvatarStory;
+
+ impl Render for AvatarStory {
+ type Element = Div;
+
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+ Story::container(cx)
+ .child(Story::title_for::<Avatar>(cx))
+ .child(Story::label(cx, "Default"))
+ .child(Avatar::new(
+ "https://avatars.githubusercontent.com/u/1714999?v=4",
+ ))
+ .child(Avatar::new(
+ "https://avatars.githubusercontent.com/u/326587?v=4",
+ ))
+ }
+ }
+}
@@ -0,0 +1,167 @@
+#[cfg(feature = "stories")]
+pub use stories::*;
+
+#[cfg(feature = "stories")]
+mod stories {
+ use super::*;
+ use crate::{h_stack, v_stack, Color, Story};
+ use gpui::{rems, Div, Render};
+ use strum::IntoEnumIterator;
+
+ pub struct ButtonStory;
+
+ impl Render for ButtonStory {
+ type Element = Div;
+
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+ let states = InteractionState::iter();
+
+ Story::container(cx)
+ .child(Story::title_for::<Button>(cx))
+ .child(
+ div()
+ .flex()
+ .gap_8()
+ .child(
+ div()
+ .child(Story::label(cx, "Ghost (Default)"))
+ .child(h_stack().gap_2().children(states.clone().map(|state| {
+ v_stack()
+ .gap_1()
+ .child(
+ Label::new(state.to_string()).color(TextColor::Muted),
+ )
+ .child(
+ Button::new("Label").variant(ButtonVariant::Ghost), // .state(state),
+ )
+ })))
+ .child(Story::label(cx, "Ghost – Left Icon"))
+ .child(h_stack().gap_2().children(states.clone().map(|state| {
+ v_stack()
+ .gap_1()
+ .child(
+ Label::new(state.to_string()).color(TextColor::Muted),
+ )
+ .child(
+ Button::new("Label")
+ .variant(ButtonVariant::Ghost)
+ .icon(Icon::Plus)
+ .icon_position(IconPosition::Left), // .state(state),
+ )
+ })))
+ .child(Story::label(cx, "Ghost – Right Icon"))
+ .child(h_stack().gap_2().children(states.clone().map(|state| {
+ v_stack()
+ .gap_1()
+ .child(
+ Label::new(state.to_string()).color(TextColor::Muted),
+ )
+ .child(
+ Button::new("Label")
+ .variant(ButtonVariant::Ghost)
+ .icon(Icon::Plus)
+ .icon_position(IconPosition::Right), // .state(state),
+ )
+ }))),
+ )
+ .child(
+ div()
+ .child(Story::label(cx, "Filled"))
+ .child(h_stack().gap_2().children(states.clone().map(|state| {
+ v_stack()
+ .gap_1()
+ .child(
+ Label::new(state.to_string()).color(TextColor::Muted),
+ )
+ .child(
+ Button::new("Label").variant(ButtonVariant::Filled), // .state(state),
+ )
+ })))
+ .child(Story::label(cx, "Filled – Left Button"))
+ .child(h_stack().gap_2().children(states.clone().map(|state| {
+ v_stack()
+ .gap_1()
+ .child(
+ Label::new(state.to_string()).color(TextColor::Muted),
+ )
+ .child(
+ Button::new("Label")
+ .variant(ButtonVariant::Filled)
+ .icon(Icon::Plus)
+ .icon_position(IconPosition::Left), // .state(state),
+ )
+ })))
+ .child(Story::label(cx, "Filled – Right Button"))
+ .child(h_stack().gap_2().children(states.clone().map(|state| {
+ v_stack()
+ .gap_1()
+ .child(
+ Label::new(state.to_string()).color(TextColor::Muted),
+ )
+ .child(
+ Button::new("Label")
+ .variant(ButtonVariant::Filled)
+ .icon(Icon::Plus)
+ .icon_position(IconPosition::Right), // .state(state),
+ )
+ }))),
+ )
+ .child(
+ div()
+ .child(Story::label(cx, "Fixed With"))
+ .child(h_stack().gap_2().children(states.clone().map(|state| {
+ v_stack()
+ .gap_1()
+ .child(
+ Label::new(state.to_string()).color(TextColor::Muted),
+ )
+ .child(
+ Button::new("Label")
+ .variant(ButtonVariant::Filled)
+ // .state(state)
+ .width(Some(rems(6.).into())),
+ )
+ })))
+ .child(Story::label(cx, "Fixed With – Left Icon"))
+ .child(h_stack().gap_2().children(states.clone().map(|state| {
+ v_stack()
+ .gap_1()
+ .child(
+ Label::new(state.to_string()).color(TextColor::Muted),
+ )
+ .child(
+ Button::new("Label")
+ .variant(ButtonVariant::Filled)
+ // .state(state)
+ .icon(Icon::Plus)
+ .icon_position(IconPosition::Left)
+ .width(Some(rems(6.).into())),
+ )
+ })))
+ .child(Story::label(cx, "Fixed With – Right Icon"))
+ .child(h_stack().gap_2().children(states.clone().map(|state| {
+ v_stack()
+ .gap_1()
+ .child(
+ Label::new(state.to_string()).color(TextColor::Muted),
+ )
+ .child(
+ Button::new("Label")
+ .variant(ButtonVariant::Filled)
+ // .state(state)
+ .icon(Icon::Plus)
+ .icon_position(IconPosition::Right)
+ .width(Some(rems(6.).into())),
+ )
+ }))),
+ ),
+ )
+ .child(Story::label(cx, "Button with `on_click`"))
+ .child(
+ Button::new("Label")
+ .variant(ButtonVariant::Ghost)
+ .on_click(|_, cx| println!("Button clicked.")),
+ )
+ }
+ }
+}
@@ -0,0 +1,59 @@
+#[cfg(feature = "stories")]
+pub use stories::*;
+
+#[cfg(feature = "stories")]
+mod stories {
+ use super::*;
+ use crate::{h_stack, Story};
+ use gpui::{Div, Render, ViewContext};
+
+ pub struct CheckboxStory;
+
+ impl Render for CheckboxStory {
+ type Element = Div;
+
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+ Story::container(cx)
+ .child(Story::title_for::<Checkbox>(cx))
+ .child(Story::label(cx, "Default"))
+ .child(
+ h_stack()
+ .p_2()
+ .gap_2()
+ .rounded_md()
+ .border()
+ .border_color(cx.theme().colors().border)
+ .child(Checkbox::new("checkbox-enabled", Selection::Unselected))
+ .child(Checkbox::new(
+ "checkbox-intermediate",
+ Selection::Indeterminate,
+ ))
+ .child(Checkbox::new("checkbox-selected", Selection::Selected)),
+ )
+ .child(Story::label(cx, "Disabled"))
+ .child(
+ h_stack()
+ .p_2()
+ .gap_2()
+ .rounded_md()
+ .border()
+ .border_color(cx.theme().colors().border)
+ .child(
+ Checkbox::new("checkbox-disabled", Selection::Unselected)
+ .disabled(true),
+ )
+ .child(
+ Checkbox::new(
+ "checkbox-disabled-intermediate",
+ Selection::Indeterminate,
+ )
+ .disabled(true),
+ )
+ .child(
+ Checkbox::new("checkbox-disabled-selected", Selection::Selected)
+ .disabled(true),
+ ),
+ )
+ }
+ }
+}
@@ -0,0 +1,112 @@
+#[cfg(feature = "stories")]
+pub use stories::*;
+
+#[cfg(feature = "stories")]
+mod stories {
+ use super::*;
+ use crate::{story::Story, Label};
+ use gpui::{actions, Div, Render};
+
+ actions!(PrintCurrentDate, PrintBestFood);
+
+ fn build_menu(cx: &mut WindowContext, header: impl Into<SharedString>) -> View<ContextMenu> {
+ ContextMenu::build(cx, |menu, _| {
+ menu.header(header)
+ .separator()
+ .entry(
+ ListItem::new("Print current time", Label::new("Print current time")),
+ |v, cx| {
+ println!("dispatching PrintCurrentTime action");
+ cx.dispatch_action(PrintCurrentDate.boxed_clone())
+ },
+ )
+ .entry(
+ ListItem::new("Print best food", Label::new("Print best food")),
+ |v, cx| cx.dispatch_action(PrintBestFood.boxed_clone()),
+ )
+ })
+ }
+
+ pub struct ContextMenuStory;
+
+ impl Render for ContextMenuStory {
+ type Element = Div;
+
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+ Story::container(cx)
+ .on_action(|_: &PrintCurrentDate, _| {
+ println!("printing unix time!");
+ if let Ok(unix_time) = std::time::UNIX_EPOCH.elapsed() {
+ println!("Current Unix time is {:?}", unix_time.as_secs());
+ }
+ })
+ .on_action(|_: &PrintBestFood, _| {
+ println!("burrito");
+ })
+ .flex()
+ .flex_row()
+ .justify_between()
+ .child(
+ div()
+ .flex()
+ .flex_col()
+ .justify_between()
+ .child(
+ menu_handle("test2")
+ .child(|is_open| {
+ Label::new(if is_open {
+ "TOP LEFT"
+ } else {
+ "RIGHT CLICK ME"
+ })
+ })
+ .menu(move |cx| build_menu(cx, "top left")),
+ )
+ .child(
+ menu_handle("test1")
+ .child(|is_open| {
+ Label::new(if is_open {
+ "BOTTOM LEFT"
+ } else {
+ "RIGHT CLICK ME"
+ })
+ })
+ .anchor(AnchorCorner::BottomLeft)
+ .attach(AnchorCorner::TopLeft)
+ .menu(move |cx| build_menu(cx, "bottom left")),
+ ),
+ )
+ .child(
+ div()
+ .flex()
+ .flex_col()
+ .justify_between()
+ .child(
+ menu_handle("test3")
+ .child(|is_open| {
+ Label::new(if is_open {
+ "TOP RIGHT"
+ } else {
+ "RIGHT CLICK ME"
+ })
+ })
+ .anchor(AnchorCorner::TopRight)
+ .menu(move |cx| build_menu(cx, "top right")),
+ )
+ .child(
+ menu_handle("test4")
+ .child(|is_open| {
+ Label::new(if is_open {
+ "BOTTOM RIGHT"
+ } else {
+ "RIGHT CLICK ME"
+ })
+ })
+ .anchor(AnchorCorner::BottomRight)
+ .attach(AnchorCorner::TopRight)
+ .menu(move |cx| build_menu(cx, "bottom right")),
+ ),
+ )
+ }
+ }
+}
@@ -0,0 +1,27 @@
+#[cfg(feature = "stories")]
+pub use stories::*;
+
+#[cfg(feature = "stories")]
+mod stories {
+ use gpui::{Div, Render};
+ use strum::IntoEnumIterator;
+
+ use crate::Story;
+
+ use super::*;
+
+ pub struct IconStory;
+
+ impl Render for IconStory {
+ type Element = Div;
+
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+ let icons = Icon::iter();
+
+ Story::container(cx)
+ .child(Story::title_for::<IconElement>(cx))
+ .child(Story::label(cx, "All Icons"))
+ .child(div().flex().gap_3().children(icons.map(IconElement::new)))
+ }
+ }
+}
@@ -0,0 +1,22 @@
+#[cfg(feature = "stories")]
+pub use stories::*;
+
+#[cfg(feature = "stories")]
+mod stories {
+ use super::*;
+ use crate::Story;
+ use gpui::{Div, Render};
+
+ pub struct InputStory;
+
+ impl Render for InputStory {
+ type Element = Div;
+
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+ Story::container(cx)
+ .child(Story::title_for::<Input>(cx))
+ .child(Story::label(cx, "Default"))
+ .child(div().flex().child(Input::new("Search")))
+ }
+ }
+}
@@ -0,0 +1,66 @@
+#[cfg(feature = "stories")]
+pub use stories::*;
+
+#[cfg(feature = "stories")]
+mod stories {
+ use super::*;
+ pub use crate::KeyBinding;
+ use crate::Story;
+ use gpui::{actions, Div, Render};
+ use itertools::Itertools;
+ pub struct KeybindingStory;
+
+ actions!(NoAction);
+
+ pub fn binding(key: &str) -> gpui::KeyBinding {
+ gpui::KeyBinding::new(key, NoAction {}, None)
+ }
+
+ impl Render for KeybindingStory {
+ type Element = Div;
+
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+ let all_modifier_permutations =
+ ["ctrl", "alt", "cmd", "shift"].into_iter().permutations(2);
+
+ Story::container(cx)
+ .child(Story::title_for::<KeyBinding>(cx))
+ .child(Story::label(cx, "Single Key"))
+ .child(KeyBinding::new(binding("Z")))
+ .child(Story::label(cx, "Single Key with Modifier"))
+ .child(
+ div()
+ .flex()
+ .gap_3()
+ .child(KeyBinding::new(binding("ctrl-c")))
+ .child(KeyBinding::new(binding("alt-c")))
+ .child(KeyBinding::new(binding("cmd-c")))
+ .child(KeyBinding::new(binding("shift-c"))),
+ )
+ .child(Story::label(cx, "Single Key with Modifier (Permuted)"))
+ .child(
+ div().flex().flex_col().children(
+ all_modifier_permutations
+ .chunks(4)
+ .into_iter()
+ .map(|chunk| {
+ div()
+ .flex()
+ .gap_4()
+ .py_3()
+ .children(chunk.map(|permutation| {
+ KeyBinding::new(binding(&*(permutation.join("-") + "-x")))
+ }))
+ }),
+ ),
+ )
+ .child(Story::label(cx, "Single Key with All Modifiers"))
+ .child(KeyBinding::new(binding("ctrl-alt-cmd-shift-z")))
+ .child(Story::label(cx, "Chord"))
+ .child(KeyBinding::new(binding("a z")))
+ .child(Story::label(cx, "Chord with Modifier"))
+ .child(KeyBinding::new(binding("ctrl-a shift-z")))
+ .child(KeyBinding::new(binding("fn-s")))
+ }
+ }
+}
@@ -0,0 +1,31 @@
+#[cfg(feature = "stories")]
+pub use stories::*;
+
+#[cfg(feature = "stories")]
+mod stories {
+ use super::*;
+ use crate::Story;
+ use gpui::{Div, Render};
+
+ pub struct LabelStory;
+
+ impl Render for LabelStory {
+ type Element = Div;
+
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+ Story::container(cx)
+ .child(Story::title_for::<Label>(cx))
+ .child(Story::label(cx, "Default"))
+ .child(Label::new("Hello, world!"))
+ .child(Story::label(cx, "Highlighted"))
+ .child(HighlightedLabel::new(
+ "Hello, world!",
+ vec![0, 1, 2, 7, 8, 12],
+ ))
+ .child(HighlightedLabel::new(
+ "Héllo, world!",
+ vec![0, 1, 3, 8, 9, 13],
+ ))
+ }
+ }
+}
@@ -0,0 +1 @@
+
@@ -1,272 +0,0 @@
-use crate::prelude::*;
-use crate::{Icon, IconElement, Label, TextColor};
-use gpui::{prelude::*, red, Div, ElementId, Render, View};
-
-#[derive(Component, Clone)]
-pub struct Tab {
- id: ElementId,
- title: String,
- icon: Option<Icon>,
- current: bool,
- dirty: bool,
- fs_status: FileSystemStatus,
- git_status: GitStatus,
- diagnostic_status: DiagnosticStatus,
- close_side: IconSide,
-}
-
-#[derive(Clone, Debug)]
-struct TabDragState {
- title: String,
-}
-
-impl Render for TabDragState {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- div().w_8().h_4().bg(red())
- }
-}
-
-impl Tab {
- pub fn new(id: impl Into<ElementId>) -> Self {
- Self {
- id: id.into(),
- title: "untitled".to_string(),
- icon: None,
- current: false,
- dirty: false,
- fs_status: FileSystemStatus::None,
- git_status: GitStatus::None,
- diagnostic_status: DiagnosticStatus::None,
- close_side: IconSide::Right,
- }
- }
-
- pub fn current(mut self, current: bool) -> Self {
- self.current = current;
- self
- }
-
- pub fn title(mut self, title: String) -> Self {
- self.title = title;
- self
- }
-
- pub fn icon<I>(mut self, icon: I) -> Self
- where
- I: Into<Option<Icon>>,
- {
- self.icon = icon.into();
- self
- }
-
- pub fn dirty(mut self, dirty: bool) -> Self {
- self.dirty = dirty;
- self
- }
-
- pub fn fs_status(mut self, fs_status: FileSystemStatus) -> Self {
- self.fs_status = fs_status;
- self
- }
-
- pub fn git_status(mut self, git_status: GitStatus) -> Self {
- self.git_status = git_status;
- self
- }
-
- pub fn diagnostic_status(mut self, diagnostic_status: DiagnosticStatus) -> Self {
- self.diagnostic_status = diagnostic_status;
- self
- }
-
- pub fn close_side(mut self, close_side: IconSide) -> Self {
- self.close_side = close_side;
- self
- }
-
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- let has_fs_conflict = self.fs_status == FileSystemStatus::Conflict;
- let is_deleted = self.fs_status == FileSystemStatus::Deleted;
-
- let label = match (self.git_status, is_deleted) {
- (_, true) | (GitStatus::Deleted, false) => Label::new(self.title.clone())
- .color(TextColor::Hidden)
- .set_strikethrough(true),
- (GitStatus::None, false) => Label::new(self.title.clone()),
- (GitStatus::Created, false) => Label::new(self.title.clone()).color(TextColor::Created),
- (GitStatus::Modified, false) => {
- Label::new(self.title.clone()).color(TextColor::Modified)
- }
- (GitStatus::Renamed, false) => Label::new(self.title.clone()).color(TextColor::Accent),
- (GitStatus::Conflict, false) => Label::new(self.title.clone()),
- };
-
- let close_icon = || IconElement::new(Icon::Close).color(TextColor::Muted);
-
- let (tab_bg, tab_hover_bg, tab_active_bg) = match self.current {
- false => (
- cx.theme().colors().tab_inactive_background,
- cx.theme().colors().ghost_element_hover,
- cx.theme().colors().ghost_element_active,
- ),
- true => (
- cx.theme().colors().tab_active_background,
- cx.theme().colors().element_hover,
- cx.theme().colors().element_active,
- ),
- };
-
- let drag_state = TabDragState {
- title: self.title.clone(),
- };
-
- div()
- .id(self.id.clone())
- .on_drag(move |_view, cx| cx.build_view(|cx| drag_state.clone()))
- .drag_over::<TabDragState>(|d| d.bg(cx.theme().colors().drop_target_background))
- .on_drop(|_view, state: View<TabDragState>, cx| {
- eprintln!("{:?}", state.read(cx));
- })
- .px_2()
- .py_0p5()
- .flex()
- .items_center()
- .justify_center()
- .bg(tab_bg)
- .hover(|h| h.bg(tab_hover_bg))
- .active(|a| a.bg(tab_active_bg))
- .child(
- div()
- .px_1()
- .flex()
- .items_center()
- .gap_1p5()
- .children(has_fs_conflict.then(|| {
- IconElement::new(Icon::ExclamationTriangle)
- .size(crate::IconSize::Small)
- .color(TextColor::Warning)
- }))
- .children(self.icon.map(IconElement::new))
- .children(if self.close_side == IconSide::Left {
- Some(close_icon())
- } else {
- None
- })
- .child(label)
- .children(if self.close_side == IconSide::Right {
- Some(close_icon())
- } else {
- None
- }),
- )
- }
-}
-
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use super::*;
- use crate::{h_stack, v_stack, Icon, Story};
- use strum::IntoEnumIterator;
-
- pub struct TabStory;
-
- impl Render for TabStory {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- let git_statuses = GitStatus::iter();
- let fs_statuses = FileSystemStatus::iter();
-
- Story::container(cx)
- .child(Story::title_for::<_, Tab>(cx))
- .child(
- h_stack().child(
- v_stack()
- .gap_2()
- .child(Story::label(cx, "Default"))
- .child(Tab::new("default")),
- ),
- )
- .child(
- h_stack().child(
- v_stack().gap_2().child(Story::label(cx, "Current")).child(
- h_stack()
- .gap_4()
- .child(
- Tab::new("current")
- .title("Current".to_string())
- .current(true),
- )
- .child(
- Tab::new("not_current")
- .title("Not Current".to_string())
- .current(false),
- ),
- ),
- ),
- )
- .child(
- h_stack().child(
- v_stack()
- .gap_2()
- .child(Story::label(cx, "Titled"))
- .child(Tab::new("titled").title("label".to_string())),
- ),
- )
- .child(
- h_stack().child(
- v_stack()
- .gap_2()
- .child(Story::label(cx, "With Icon"))
- .child(
- Tab::new("with_icon")
- .title("label".to_string())
- .icon(Some(Icon::Envelope)),
- ),
- ),
- )
- .child(
- h_stack().child(
- v_stack()
- .gap_2()
- .child(Story::label(cx, "Close Side"))
- .child(
- h_stack()
- .gap_4()
- .child(
- Tab::new("left")
- .title("Left".to_string())
- .close_side(IconSide::Left),
- )
- .child(Tab::new("right").title("Right".to_string())),
- ),
- ),
- )
- .child(
- v_stack()
- .gap_2()
- .child(Story::label(cx, "Git Status"))
- .child(h_stack().gap_4().children(git_statuses.map(|git_status| {
- Tab::new("git_status")
- .title(git_status.to_string())
- .git_status(git_status)
- }))),
- )
- .child(
- v_stack()
- .gap_2()
- .child(Story::label(cx, "File System Status"))
- .child(h_stack().gap_4().children(fs_statuses.map(|fs_status| {
- Tab::new("file_system_status")
- .title(fs_status.to_string())
- .fs_status(fs_status)
- }))),
- )
- }
- }
-}
@@ -1,90 +0,0 @@
-use crate::prelude::*;
-use gpui::{prelude::*, AnyElement};
-use smallvec::SmallVec;
-
-#[derive(Default, Debug, PartialEq, Eq, Clone, Copy)]
-pub enum ToastOrigin {
- #[default]
- Bottom,
- BottomRight,
-}
-
-/// Don't use toast directly:
-///
-/// - For messages with a required action, use a `NotificationToast`.
-/// - For messages that convey information, use a `StatusToast`.
-///
-/// A toast is a small, temporary window that appears to show a message to the user
-/// or indicate a required action.
-///
-/// Toasts should not persist on the screen for more than a few seconds unless
-/// they are actively showing the a process in progress.
-///
-/// Only one toast may be visible at a time.
-#[derive(Component)]
-pub struct Toast<V: 'static> {
- origin: ToastOrigin,
- children: SmallVec<[AnyElement<V>; 2]>,
-}
-
-impl<V: 'static> Toast<V> {
- pub fn new(origin: ToastOrigin) -> Self {
- Self {
- origin,
- children: SmallVec::new(),
- }
- }
-
- fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- let mut div = div();
-
- if self.origin == ToastOrigin::Bottom {
- div = div.right_1_2();
- } else {
- div = div.right_2();
- }
-
- div.z_index(5)
- .absolute()
- .bottom_9()
- .flex()
- .py_1()
- .px_1p5()
- .rounded_lg()
- .shadow_md()
- .overflow_hidden()
- .bg(cx.theme().colors().elevated_surface_background)
- .children(self.children)
- }
-}
-
-impl<V: 'static> ParentComponent<V> for Toast<V> {
- fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
- &mut self.children
- }
-}
-
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use gpui::{Div, Render};
-
- use crate::{Label, Story};
-
- use super::*;
-
- pub struct ToastStory;
-
- impl Render for ToastStory {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- Story::container(cx)
- .child(Story::title_for::<_, Toast<Self>>(cx))
- .child(Story::label(cx, "Default"))
- .child(Toast::new(ToastOrigin::Bottom).child(Label::new("label")))
- }
- }
-}
@@ -1,7 +1,3 @@
-use gpui::{div, Component, ParentComponent};
-
-use crate::{Icon, IconElement, IconSize, TextColor};
-
/// Whether the entry is toggleable, and if so, whether it is currently toggled.
///
/// To make an element toggleable, simply add a `Toggle::Toggled(_)` and handle it's cases.
@@ -43,19 +39,3 @@ impl From<bool> for Toggle {
Toggle::Toggled(toggled)
}
}
-
-pub fn disclosure_control<V: 'static>(toggle: Toggle) -> impl Component<V> {
- match (toggle.is_toggleable(), toggle.is_toggled()) {
- (false, _) => div(),
- (_, true) => div().child(
- IconElement::new(Icon::ChevronDown)
- .color(TextColor::Muted)
- .size(IconSize::Small),
- ),
- (_, false) => div().child(
- IconElement::new(Icon::ChevronRight)
- .color(TextColor::Muted)
- .size(IconSize::Small),
- ),
- }
-}
@@ -1,14 +0,0 @@
-use crate::prelude::*;
-
-#[derive(Component)]
-pub struct ToolDivider;
-
-impl ToolDivider {
- pub fn new() -> Self {
- Self
- }
-
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- div().w_px().h_3().bg(cx.theme().colors().border)
- }
-}
@@ -1,9 +1,9 @@
-use gpui::{overlay, Action, AnyView, Overlay, Render, VisualContext};
+use gpui::{overlay, Action, AnyView, Overlay, Render, RenderOnce, VisualContext};
use settings2::Settings;
use theme2::{ActiveTheme, ThemeSettings};
use crate::prelude::*;
-use crate::{h_stack, v_stack, KeyBinding, Label, LabelSize, StyledExt, TextColor};
+use crate::{h_stack, v_stack, Color, KeyBinding, Label, LabelSize, StyledExt};
pub struct Tooltip {
title: SharedString,
@@ -68,7 +68,7 @@ impl Tooltip {
}
impl Render for Tooltip {
- type Element = Overlay<Self>;
+ type Element = Overlay;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
@@ -90,11 +90,7 @@ impl Render for Tooltip {
}),
)
.when_some(self.meta.clone(), |this, meta| {
- this.child(
- Label::new(meta)
- .size(LabelSize::Small)
- .color(TextColor::Muted),
- )
+ this.child(Label::new(meta).size(LabelSize::Small).color(Color::Muted))
}),
),
)
@@ -1,116 +1,14 @@
-use gpui::rems;
-use gpui::Rems;
pub use gpui::{
- div, Component, Element, ElementId, InteractiveComponent, ParentComponent, SharedString,
- Styled, ViewContext, WindowContext,
+ div, Component, Element, ElementId, InteractiveElement, ParentElement, SharedString, Styled,
+ ViewContext, WindowContext,
};
-pub use crate::elevation::*;
pub use crate::StyledExt;
-pub use crate::{ButtonVariant, TextColor};
+pub use crate::{ButtonVariant, Color};
pub use theme2::ActiveTheme;
-use gpui::Hsla;
use strum::EnumIter;
-#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
-pub enum UITextSize {
- /// The default size for UI text.
- ///
- /// `0.825rem` or `14px` at the default scale of `1rem` = `16px`.
- ///
- /// Note: The absolute size of this text will change based on a user's `ui_scale` setting.
- #[default]
- Default,
- /// The small size for UI text.
- ///
- /// `0.75rem` or `12px` at the default scale of `1rem` = `16px`.
- ///
- /// Note: The absolute size of this text will change based on a user's `ui_scale` setting.
- Small,
-}
-
-impl UITextSize {
- pub fn rems(self) -> Rems {
- match self {
- Self::Default => rems(0.875),
- Self::Small => rems(0.75),
- }
- }
-}
-
-#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
-pub enum FileSystemStatus {
- #[default]
- None,
- Conflict,
- Deleted,
-}
-
-impl std::fmt::Display for FileSystemStatus {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(
- f,
- "{}",
- match self {
- Self::None => "None",
- Self::Conflict => "Conflict",
- Self::Deleted => "Deleted",
- }
- )
- }
-}
-
-#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
-pub enum GitStatus {
- #[default]
- None,
- Created,
- Modified,
- Deleted,
- Conflict,
- Renamed,
-}
-
-impl GitStatus {
- pub fn hsla(&self, cx: &WindowContext) -> Hsla {
- match self {
- Self::None => cx.theme().system().transparent,
- Self::Created => cx.theme().status().created,
- Self::Modified => cx.theme().status().modified,
- Self::Deleted => cx.theme().status().deleted,
- Self::Conflict => cx.theme().status().conflict,
- Self::Renamed => cx.theme().status().renamed,
- }
- }
-}
-
-impl std::fmt::Display for GitStatus {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(
- f,
- "{}",
- match self {
- Self::None => "None",
- Self::Created => "Created",
- Self::Modified => "Modified",
- Self::Deleted => "Deleted",
- Self::Conflict => "Conflict",
- Self::Renamed => "Renamed",
- }
- )
- }
-}
-
-#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
-pub enum DiagnosticStatus {
- #[default]
- None,
- Error,
- Warning,
- Info,
-}
-
#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
pub enum IconSide {
#[default]
@@ -118,45 +16,6 @@ pub enum IconSide {
Right,
}
-#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
-pub enum OrderMethod {
- #[default]
- Ascending,
- Descending,
- MostRecent,
-}
-
-#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
-pub enum Shape {
- #[default]
- Circle,
- RoundedRectangle,
-}
-
-#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
-pub enum DisclosureControlVisibility {
- #[default]
- OnHover,
- Always,
-}
-
-#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
-pub enum DisclosureControlStyle {
- /// Shows the disclosure control only when hovered where possible.
- ///
- /// More compact, but not available everywhere.
- ChevronOnHover,
- /// Shows an icon where possible, otherwise shows a chevron.
- ///
- /// For example, in a file tree a folder or file icon is shown
- /// instead of a chevron
- Icon,
- /// Always shows a chevron.
- Chevron,
- /// Completely hides the disclosure control where possible.
- None,
-}
-
#[derive(Debug, PartialEq, Eq, Clone, Copy, EnumIter)]
pub enum OverflowStyle {
Hidden,
@@ -1,74 +0,0 @@
-use std::ops::Deref;
-
-use gpui::{rems, AbsoluteLength, AppContext, WindowContext};
-
-use crate::prelude::*;
-
-pub fn init(cx: &mut AppContext) {
- cx.set_global(FakeSettings::default());
-}
-
-/// Returns the user settings.
-pub fn user_settings(cx: &WindowContext) -> FakeSettings {
- cx.global::<FakeSettings>().clone()
-}
-
-pub fn user_settings_mut<'cx>(cx: &'cx mut WindowContext) -> &'cx mut FakeSettings {
- cx.global_mut::<FakeSettings>()
-}
-
-#[derive(Clone)]
-pub enum SettingValue<T> {
- UserDefined(T),
- Default(T),
-}
-
-impl<T> Deref for SettingValue<T> {
- type Target = T;
-
- fn deref(&self) -> &Self::Target {
- match self {
- Self::UserDefined(value) => value,
- Self::Default(value) => value,
- }
- }
-}
-
-#[derive(Clone)]
-pub struct TitlebarSettings {
- pub show_project_owner: SettingValue<bool>,
- pub show_git_status: SettingValue<bool>,
- pub show_git_controls: SettingValue<bool>,
-}
-
-impl Default for TitlebarSettings {
- fn default() -> Self {
- Self {
- show_project_owner: SettingValue::Default(true),
- show_git_status: SettingValue::Default(true),
- show_git_controls: SettingValue::Default(true),
- }
- }
-}
-
-// These should be merged into settings
-#[derive(Clone)]
-pub struct FakeSettings {
- pub default_panel_size: SettingValue<AbsoluteLength>,
- pub list_disclosure_style: SettingValue<DisclosureControlStyle>,
- pub list_indent_depth: SettingValue<AbsoluteLength>,
- pub titlebar: TitlebarSettings,
-}
-
-impl Default for FakeSettings {
- fn default() -> Self {
- Self {
- titlebar: TitlebarSettings::default(),
- list_disclosure_style: SettingValue::Default(DisclosureControlStyle::ChevronOnHover),
- list_indent_depth: SettingValue::Default(rems(0.3).into()),
- default_panel_size: SettingValue::Default(rems(16.).into()),
- }
- }
-}
-
-impl FakeSettings {}
@@ -1,1111 +0,0 @@
-use std::path::PathBuf;
-use std::str::FromStr;
-use std::sync::Arc;
-
-use chrono::DateTime;
-use gpui::{AppContext, ViewContext};
-use rand::Rng;
-use theme2::ActiveTheme;
-
-use crate::{binding, HighlightedText};
-use crate::{
- Buffer, BufferRow, BufferRows, Button, EditorPane, FileSystemStatus, GitStatus,
- HighlightedLine, Icon, KeyBinding, Label, ListEntry, ListEntrySize, Livestream, MicStatus,
- Notification, PaletteItem, Player, PlayerCallStatus, PlayerWithCallStatus, PublicPlayer,
- ScreenShareStatus, Symbol, Tab, TextColor, Toggle, VideoStatus,
-};
-use crate::{ListItem, NotificationAction};
-
-pub fn static_tabs_example() -> Vec<Tab> {
- vec![
- Tab::new("wip.rs")
- .title("wip.rs".to_string())
- .icon(Icon::FileRust)
- .current(false)
- .fs_status(FileSystemStatus::Deleted),
- Tab::new("Cargo.toml")
- .title("Cargo.toml".to_string())
- .icon(Icon::FileToml)
- .current(false)
- .git_status(GitStatus::Modified),
- Tab::new("Channels Panel")
- .title("Channels Panel".to_string())
- .icon(Icon::Hash)
- .current(false),
- Tab::new("channels_panel.rs")
- .title("channels_panel.rs".to_string())
- .icon(Icon::FileRust)
- .current(true)
- .git_status(GitStatus::Modified),
- Tab::new("workspace.rs")
- .title("workspace.rs".to_string())
- .current(false)
- .icon(Icon::FileRust)
- .git_status(GitStatus::Modified),
- Tab::new("icon_button.rs")
- .title("icon_button.rs".to_string())
- .icon(Icon::FileRust)
- .current(false),
- Tab::new("storybook.rs")
- .title("storybook.rs".to_string())
- .icon(Icon::FileRust)
- .current(false)
- .git_status(GitStatus::Created),
- Tab::new("theme.rs")
- .title("theme.rs".to_string())
- .icon(Icon::FileRust)
- .current(false),
- Tab::new("theme_registry.rs")
- .title("theme_registry.rs".to_string())
- .icon(Icon::FileRust)
- .current(false),
- Tab::new("styleable_helpers.rs")
- .title("styleable_helpers.rs".to_string())
- .icon(Icon::FileRust)
- .current(false),
- ]
-}
-
-pub fn static_tabs_1() -> Vec<Tab> {
- vec![
- Tab::new("project_panel.rs")
- .title("project_panel.rs".to_string())
- .icon(Icon::FileRust)
- .current(false)
- .fs_status(FileSystemStatus::Deleted),
- Tab::new("tab_bar.rs")
- .title("tab_bar.rs".to_string())
- .icon(Icon::FileRust)
- .current(false)
- .git_status(GitStatus::Modified),
- Tab::new("workspace.rs")
- .title("workspace.rs".to_string())
- .icon(Icon::FileRust)
- .current(false),
- Tab::new("tab.rs")
- .title("tab.rs".to_string())
- .icon(Icon::FileRust)
- .current(true)
- .git_status(GitStatus::Modified),
- ]
-}
-
-pub fn static_tabs_2() -> Vec<Tab> {
- vec![
- Tab::new("tab_bar.rs")
- .title("tab_bar.rs".to_string())
- .icon(Icon::FileRust)
- .current(false)
- .fs_status(FileSystemStatus::Deleted),
- Tab::new("static_data.rs")
- .title("static_data.rs".to_string())
- .icon(Icon::FileRust)
- .current(true)
- .git_status(GitStatus::Modified),
- ]
-}
-
-pub fn static_tabs_3() -> Vec<Tab> {
- vec![Tab::new("static_tabs_3")
- .git_status(GitStatus::Created)
- .current(true)]
-}
-
-pub fn static_players() -> Vec<Player> {
- vec![
- Player::new(
- 0,
- "https://avatars.githubusercontent.com/u/1714999?v=4".into(),
- "nathansobo".into(),
- ),
- Player::new(
- 1,
- "https://avatars.githubusercontent.com/u/326587?v=4".into(),
- "maxbrunsfeld".into(),
- ),
- Player::new(
- 2,
- "https://avatars.githubusercontent.com/u/482957?v=4".into(),
- "as-cii".into(),
- ),
- Player::new(
- 3,
- "https://avatars.githubusercontent.com/u/1714999?v=4".into(),
- "iamnbutler".into(),
- ),
- Player::new(
- 4,
- "https://avatars.githubusercontent.com/u/1486634?v=4".into(),
- "maxdeviant".into(),
- ),
- ]
-}
-
-#[derive(Debug)]
-pub struct PlayerData {
- pub url: String,
- pub name: String,
-}
-
-pub fn static_player_data() -> Vec<PlayerData> {
- vec![
- PlayerData {
- url: "https://avatars.githubusercontent.com/u/1714999?v=4".into(),
- name: "iamnbutler".into(),
- },
- PlayerData {
- url: "https://avatars.githubusercontent.com/u/326587?v=4".into(),
- name: "maxbrunsfeld".into(),
- },
- PlayerData {
- url: "https://avatars.githubusercontent.com/u/482957?v=4".into(),
- name: "as-cii".into(),
- },
- PlayerData {
- url: "https://avatars.githubusercontent.com/u/1789?v=4".into(),
- name: "nathansobo".into(),
- },
- PlayerData {
- url: "https://avatars.githubusercontent.com/u/1486634?v=4".into(),
- name: "ForLoveOfCats".into(),
- },
- PlayerData {
- url: "https://avatars.githubusercontent.com/u/2690773?v=4".into(),
- name: "SomeoneToIgnore".into(),
- },
- PlayerData {
- url: "https://avatars.githubusercontent.com/u/19867440?v=4".into(),
- name: "JosephTLyons".into(),
- },
- PlayerData {
- url: "https://avatars.githubusercontent.com/u/24362066?v=4".into(),
- name: "osiewicz".into(),
- },
- PlayerData {
- url: "https://avatars.githubusercontent.com/u/22121886?v=4".into(),
- name: "KCaverly".into(),
- },
- PlayerData {
- url: "https://avatars.githubusercontent.com/u/1486634?v=4".into(),
- name: "maxdeviant".into(),
- },
- ]
-}
-
-pub fn create_static_players(player_data: Vec<PlayerData>) -> Vec<Player> {
- let mut players = Vec::new();
- for data in player_data {
- players.push(Player::new(players.len(), data.url, data.name));
- }
- players
-}
-
-pub fn static_player_1(data: &Vec<PlayerData>) -> Player {
- Player::new(1, data[0].url.clone(), data[0].name.clone())
-}
-
-pub fn static_player_2(data: &Vec<PlayerData>) -> Player {
- Player::new(2, data[1].url.clone(), data[1].name.clone())
-}
-
-pub fn static_player_3(data: &Vec<PlayerData>) -> Player {
- Player::new(3, data[2].url.clone(), data[2].name.clone())
-}
-
-pub fn static_player_4(data: &Vec<PlayerData>) -> Player {
- Player::new(4, data[3].url.clone(), data[3].name.clone())
-}
-
-pub fn static_player_5(data: &Vec<PlayerData>) -> Player {
- Player::new(5, data[4].url.clone(), data[4].name.clone())
-}
-
-pub fn static_player_6(data: &Vec<PlayerData>) -> Player {
- Player::new(6, data[5].url.clone(), data[5].name.clone())
-}
-
-pub fn static_player_7(data: &Vec<PlayerData>) -> Player {
- Player::new(7, data[6].url.clone(), data[6].name.clone())
-}
-
-pub fn static_player_8(data: &Vec<PlayerData>) -> Player {
- Player::new(8, data[7].url.clone(), data[7].name.clone())
-}
-
-pub fn static_player_9(data: &Vec<PlayerData>) -> Player {
- Player::new(9, data[8].url.clone(), data[8].name.clone())
-}
-
-pub fn static_player_10(data: &Vec<PlayerData>) -> Player {
- Player::new(10, data[9].url.clone(), data[9].name.clone())
-}
-
-pub fn static_livestream() -> Livestream {
- Livestream {
- players: random_players_with_call_status(7),
- channel: Some("gpui2-ui".to_string()),
- }
-}
-
-pub fn populate_player_call_status(
- player: Player,
- followers: Option<Vec<Player>>,
-) -> PlayerCallStatus {
- let mut rng = rand::thread_rng();
- let in_current_project: bool = rng.gen();
- let disconnected: bool = rng.gen();
- let voice_activity: f32 = rng.gen();
- let mic_status = if rng.gen_bool(0.5) {
- MicStatus::Muted
- } else {
- MicStatus::Unmuted
- };
- let video_status = if rng.gen_bool(0.5) {
- VideoStatus::On
- } else {
- VideoStatus::Off
- };
- let screen_share_status = if rng.gen_bool(0.5) {
- ScreenShareStatus::Shared
- } else {
- ScreenShareStatus::NotShared
- };
- PlayerCallStatus {
- mic_status,
- voice_activity,
- video_status,
- screen_share_status,
- in_current_project,
- disconnected,
- following: None,
- followers,
- }
-}
-
-pub fn random_players_with_call_status(number_of_players: usize) -> Vec<PlayerWithCallStatus> {
- let players = create_static_players(static_player_data());
- let mut player_status = vec![];
- for i in 0..number_of_players {
- let followers = if i == 0 {
- Some(vec![
- players[1].clone(),
- players[3].clone(),
- players[5].clone(),
- players[6].clone(),
- ])
- } else if i == 1 {
- Some(vec![players[2].clone(), players[6].clone()])
- } else {
- None
- };
- let call_status = populate_player_call_status(players[i].clone(), followers);
- player_status.push(PlayerWithCallStatus::new(players[i].clone(), call_status));
- }
- player_status
-}
-
-pub fn static_players_with_call_status() -> Vec<PlayerWithCallStatus> {
- let players = static_players();
- let mut player_0_status = PlayerCallStatus::new();
- let player_1_status = PlayerCallStatus::new();
- let player_2_status = PlayerCallStatus::new();
- let mut player_3_status = PlayerCallStatus::new();
- let mut player_4_status = PlayerCallStatus::new();
-
- player_0_status.screen_share_status = ScreenShareStatus::Shared;
- player_0_status.followers = Some(vec![players[1].clone(), players[3].clone()]);
-
- player_3_status.voice_activity = 0.5;
- player_4_status.mic_status = MicStatus::Muted;
- player_4_status.in_current_project = false;
-
- vec![
- PlayerWithCallStatus::new(players[0].clone(), player_0_status),
- PlayerWithCallStatus::new(players[1].clone(), player_1_status),
- PlayerWithCallStatus::new(players[2].clone(), player_2_status),
- PlayerWithCallStatus::new(players[3].clone(), player_3_status),
- PlayerWithCallStatus::new(players[4].clone(), player_4_status),
- ]
-}
-
-pub fn static_new_notification_items_2<V: 'static>() -> Vec<Notification<V>> {
- vec![
- Notification::new_icon_message(
- "notif-1",
- "You were mentioned in a note.",
- DateTime::parse_from_rfc3339("2023-11-02T11:59:57Z")
- .unwrap()
- .naive_local(),
- Icon::AtSign,
- Arc::new(|_, _| {}),
- ),
- Notification::new_actor_with_actions(
- "notif-2",
- "as-cii sent you a contact request.",
- DateTime::parse_from_rfc3339("2023-11-02T12:09:07Z")
- .unwrap()
- .naive_local(),
- PublicPlayer::new("as-cii", "http://github.com/as-cii.png?s=50"),
- [
- NotificationAction::new(
- Button::new("Decline"),
- "Decline Request",
- (Some(Icon::XCircle), "Declined"),
- ),
- NotificationAction::new(
- Button::new("Accept").variant(crate::ButtonVariant::Filled),
- "Accept Request",
- (Some(Icon::Check), "Accepted"),
- ),
- ],
- ),
- Notification::new_icon_message(
- "notif-3",
- "You were mentioned #design.",
- DateTime::parse_from_rfc3339("2023-11-02T12:09:07Z")
- .unwrap()
- .naive_local(),
- Icon::MessageBubbles,
- Arc::new(|_, _| {}),
- ),
- Notification::new_actor_with_actions(
- "notif-4",
- "as-cii sent you a contact request.",
- DateTime::parse_from_rfc3339("2023-11-01T12:09:07Z")
- .unwrap()
- .naive_local(),
- PublicPlayer::new("as-cii", "http://github.com/as-cii.png?s=50"),
- [
- NotificationAction::new(
- Button::new("Decline"),
- "Decline Request",
- (Some(Icon::XCircle), "Declined"),
- ),
- NotificationAction::new(
- Button::new("Accept").variant(crate::ButtonVariant::Filled),
- "Accept Request",
- (Some(Icon::Check), "Accepted"),
- ),
- ],
- ),
- Notification::new_icon_message(
- "notif-5",
- "You were mentioned in a note.",
- DateTime::parse_from_rfc3339("2023-10-28T12:09:07Z")
- .unwrap()
- .naive_local(),
- Icon::AtSign,
- Arc::new(|_, _| {}),
- ),
- Notification::new_actor_with_actions(
- "notif-6",
- "as-cii sent you a contact request.",
- DateTime::parse_from_rfc3339("2022-10-25T12:09:07Z")
- .unwrap()
- .naive_local(),
- PublicPlayer::new("as-cii", "http://github.com/as-cii.png?s=50"),
- [
- NotificationAction::new(
- Button::new("Decline"),
- "Decline Request",
- (Some(Icon::XCircle), "Declined"),
- ),
- NotificationAction::new(
- Button::new("Accept").variant(crate::ButtonVariant::Filled),
- "Accept Request",
- (Some(Icon::Check), "Accepted"),
- ),
- ],
- ),
- Notification::new_icon_message(
- "notif-7",
- "You were mentioned in a note.",
- DateTime::parse_from_rfc3339("2022-10-14T12:09:07Z")
- .unwrap()
- .naive_local(),
- Icon::AtSign,
- Arc::new(|_, _| {}),
- ),
- Notification::new_actor_with_actions(
- "notif-8",
- "as-cii sent you a contact request.",
- DateTime::parse_from_rfc3339("2021-10-12T12:09:07Z")
- .unwrap()
- .naive_local(),
- PublicPlayer::new("as-cii", "http://github.com/as-cii.png?s=50"),
- [
- NotificationAction::new(
- Button::new("Decline"),
- "Decline Request",
- (Some(Icon::XCircle), "Declined"),
- ),
- NotificationAction::new(
- Button::new("Accept").variant(crate::ButtonVariant::Filled),
- "Accept Request",
- (Some(Icon::Check), "Accepted"),
- ),
- ],
- ),
- Notification::new_icon_message(
- "notif-9",
- "You were mentioned in a note.",
- DateTime::parse_from_rfc3339("2021-02-02T12:09:07Z")
- .unwrap()
- .naive_local(),
- Icon::AtSign,
- Arc::new(|_, _| {}),
- ),
- Notification::new_actor_with_actions(
- "notif-10",
- "as-cii sent you a contact request.",
- DateTime::parse_from_rfc3339("1969-07-20T00:00:00Z")
- .unwrap()
- .naive_local(),
- PublicPlayer::new("as-cii", "http://github.com/as-cii.png?s=50"),
- [
- NotificationAction::new(
- Button::new("Decline"),
- "Decline Request",
- (Some(Icon::XCircle), "Declined"),
- ),
- NotificationAction::new(
- Button::new("Accept").variant(crate::ButtonVariant::Filled),
- "Accept Request",
- (Some(Icon::Check), "Accepted"),
- ),
- ],
- ),
- ]
-}
-
-pub fn static_project_panel_project_items() -> Vec<ListItem> {
- vec![
- ListEntry::new(Label::new("zed"))
- .left_icon(Icon::FolderOpen.into())
- .indent_level(0)
- .toggle(Toggle::Toggled(true)),
- ListEntry::new(Label::new(".cargo"))
- .left_icon(Icon::Folder.into())
- .indent_level(1),
- ListEntry::new(Label::new(".config"))
- .left_icon(Icon::Folder.into())
- .indent_level(1),
- ListEntry::new(Label::new(".git").color(TextColor::Hidden))
- .left_icon(Icon::Folder.into())
- .indent_level(1),
- ListEntry::new(Label::new(".cargo"))
- .left_icon(Icon::Folder.into())
- .indent_level(1),
- ListEntry::new(Label::new(".idea").color(TextColor::Hidden))
- .left_icon(Icon::Folder.into())
- .indent_level(1),
- ListEntry::new(Label::new("assets"))
- .left_icon(Icon::Folder.into())
- .indent_level(1)
- .toggle(Toggle::Toggled(true)),
- ListEntry::new(Label::new("cargo-target").color(TextColor::Hidden))
- .left_icon(Icon::Folder.into())
- .indent_level(1),
- ListEntry::new(Label::new("crates"))
- .left_icon(Icon::FolderOpen.into())
- .indent_level(1)
- .toggle(Toggle::Toggled(true)),
- ListEntry::new(Label::new("activity_indicator"))
- .left_icon(Icon::Folder.into())
- .indent_level(2),
- ListEntry::new(Label::new("ai"))
- .left_icon(Icon::Folder.into())
- .indent_level(2),
- ListEntry::new(Label::new("audio"))
- .left_icon(Icon::Folder.into())
- .indent_level(2),
- ListEntry::new(Label::new("auto_update"))
- .left_icon(Icon::Folder.into())
- .indent_level(2),
- ListEntry::new(Label::new("breadcrumbs"))
- .left_icon(Icon::Folder.into())
- .indent_level(2),
- ListEntry::new(Label::new("call"))
- .left_icon(Icon::Folder.into())
- .indent_level(2),
- ListEntry::new(Label::new("sqlez").color(TextColor::Modified))
- .left_icon(Icon::Folder.into())
- .indent_level(2)
- .toggle(Toggle::Toggled(false)),
- ListEntry::new(Label::new("gpui2"))
- .left_icon(Icon::FolderOpen.into())
- .indent_level(2)
- .toggle(Toggle::Toggled(true)),
- ListEntry::new(Label::new("src"))
- .left_icon(Icon::FolderOpen.into())
- .indent_level(3)
- .toggle(Toggle::Toggled(true)),
- ListEntry::new(Label::new("derive_element.rs"))
- .left_icon(Icon::FileRust.into())
- .indent_level(4),
- ListEntry::new(Label::new("storybook").color(TextColor::Modified))
- .left_icon(Icon::FolderOpen.into())
- .indent_level(1)
- .toggle(Toggle::Toggled(true)),
- ListEntry::new(Label::new("docs").color(TextColor::Default))
- .left_icon(Icon::Folder.into())
- .indent_level(2)
- .toggle(Toggle::Toggled(true)),
- ListEntry::new(Label::new("src").color(TextColor::Modified))
- .left_icon(Icon::FolderOpen.into())
- .indent_level(3)
- .toggle(Toggle::Toggled(true)),
- ListEntry::new(Label::new("ui").color(TextColor::Modified))
- .left_icon(Icon::FolderOpen.into())
- .indent_level(4)
- .toggle(Toggle::Toggled(true)),
- ListEntry::new(Label::new("component").color(TextColor::Created))
- .left_icon(Icon::FolderOpen.into())
- .indent_level(5)
- .toggle(Toggle::Toggled(true)),
- ListEntry::new(Label::new("facepile.rs").color(TextColor::Default))
- .left_icon(Icon::FileRust.into())
- .indent_level(6),
- ListEntry::new(Label::new("follow_group.rs").color(TextColor::Default))
- .left_icon(Icon::FileRust.into())
- .indent_level(6),
- ListEntry::new(Label::new("list_item.rs").color(TextColor::Created))
- .left_icon(Icon::FileRust.into())
- .indent_level(6),
- ListEntry::new(Label::new("tab.rs").color(TextColor::Default))
- .left_icon(Icon::FileRust.into())
- .indent_level(6),
- ListEntry::new(Label::new("target").color(TextColor::Hidden))
- .left_icon(Icon::Folder.into())
- .indent_level(1),
- ListEntry::new(Label::new(".dockerignore"))
- .left_icon(Icon::FileGeneric.into())
- .indent_level(1),
- ListEntry::new(Label::new(".DS_Store").color(TextColor::Hidden))
- .left_icon(Icon::FileGeneric.into())
- .indent_level(1),
- ListEntry::new(Label::new("Cargo.lock"))
- .left_icon(Icon::FileLock.into())
- .indent_level(1),
- ListEntry::new(Label::new("Cargo.toml"))
- .left_icon(Icon::FileToml.into())
- .indent_level(1),
- ListEntry::new(Label::new("Dockerfile"))
- .left_icon(Icon::FileGeneric.into())
- .indent_level(1),
- ListEntry::new(Label::new("Procfile"))
- .left_icon(Icon::FileGeneric.into())
- .indent_level(1),
- ListEntry::new(Label::new("README.md"))
- .left_icon(Icon::FileDoc.into())
- .indent_level(1),
- ]
- .into_iter()
- .map(From::from)
- .collect()
-}
-
-pub fn static_project_panel_single_items() -> Vec<ListItem> {
- vec![
- ListEntry::new(Label::new("todo.md"))
- .left_icon(Icon::FileDoc.into())
- .indent_level(0),
- ListEntry::new(Label::new("README.md"))
- .left_icon(Icon::FileDoc.into())
- .indent_level(0),
- ListEntry::new(Label::new("config.json"))
- .left_icon(Icon::FileGeneric.into())
- .indent_level(0),
- ]
- .into_iter()
- .map(From::from)
- .collect()
-}
-
-pub fn static_collab_panel_current_call() -> Vec<ListItem> {
- vec![
- ListEntry::new(Label::new("as-cii")).left_avatar("http://github.com/as-cii.png?s=50"),
- ListEntry::new(Label::new("nathansobo"))
- .left_avatar("http://github.com/nathansobo.png?s=50"),
- ListEntry::new(Label::new("maxbrunsfeld"))
- .left_avatar("http://github.com/maxbrunsfeld.png?s=50"),
- ]
- .into_iter()
- .map(From::from)
- .collect()
-}
-
-pub fn static_collab_panel_channels() -> Vec<ListItem> {
- vec![
- ListEntry::new(Label::new("zed"))
- .left_icon(Icon::Hash.into())
- .size(ListEntrySize::Medium)
- .indent_level(0),
- ListEntry::new(Label::new("community"))
- .left_icon(Icon::Hash.into())
- .size(ListEntrySize::Medium)
- .indent_level(1),
- ListEntry::new(Label::new("dashboards"))
- .left_icon(Icon::Hash.into())
- .size(ListEntrySize::Medium)
- .indent_level(2),
- ListEntry::new(Label::new("feedback"))
- .left_icon(Icon::Hash.into())
- .size(ListEntrySize::Medium)
- .indent_level(2),
- ListEntry::new(Label::new("teams-in-channels-alpha"))
- .left_icon(Icon::Hash.into())
- .size(ListEntrySize::Medium)
- .indent_level(2),
- ListEntry::new(Label::new("current-projects"))
- .left_icon(Icon::Hash.into())
- .size(ListEntrySize::Medium)
- .indent_level(1),
- ListEntry::new(Label::new("codegen"))
- .left_icon(Icon::Hash.into())
- .size(ListEntrySize::Medium)
- .indent_level(2),
- ListEntry::new(Label::new("gpui2"))
- .left_icon(Icon::Hash.into())
- .size(ListEntrySize::Medium)
- .indent_level(2),
- ListEntry::new(Label::new("livestreaming"))
- .left_icon(Icon::Hash.into())
- .size(ListEntrySize::Medium)
- .indent_level(2),
- ListEntry::new(Label::new("open-source"))
- .left_icon(Icon::Hash.into())
- .size(ListEntrySize::Medium)
- .indent_level(2),
- ListEntry::new(Label::new("replace"))
- .left_icon(Icon::Hash.into())
- .size(ListEntrySize::Medium)
- .indent_level(2),
- ListEntry::new(Label::new("semantic-index"))
- .left_icon(Icon::Hash.into())
- .size(ListEntrySize::Medium)
- .indent_level(2),
- ListEntry::new(Label::new("vim"))
- .left_icon(Icon::Hash.into())
- .size(ListEntrySize::Medium)
- .indent_level(2),
- ListEntry::new(Label::new("web-tech"))
- .left_icon(Icon::Hash.into())
- .size(ListEntrySize::Medium)
- .indent_level(2),
- ]
- .into_iter()
- .map(From::from)
- .collect()
-}
-
-pub fn example_editor_actions() -> Vec<PaletteItem> {
- vec![
- PaletteItem::new("New File").key_binding(KeyBinding::new(binding("cmd-n"))),
- PaletteItem::new("Open File").key_binding(KeyBinding::new(binding("cmd-o"))),
- PaletteItem::new("Save File").key_binding(KeyBinding::new(binding("cmd-s"))),
- PaletteItem::new("Cut").key_binding(KeyBinding::new(binding("cmd-x"))),
- PaletteItem::new("Copy").key_binding(KeyBinding::new(binding("cmd-c"))),
- PaletteItem::new("Paste").key_binding(KeyBinding::new(binding("cmd-v"))),
- PaletteItem::new("Undo").key_binding(KeyBinding::new(binding("cmd-z"))),
- PaletteItem::new("Redo").key_binding(KeyBinding::new(binding("cmd-shift-z"))),
- PaletteItem::new("Find").key_binding(KeyBinding::new(binding("cmd-f"))),
- PaletteItem::new("Replace").key_binding(KeyBinding::new(binding("cmd-r"))),
- PaletteItem::new("Jump to Line"),
- PaletteItem::new("Select All"),
- PaletteItem::new("Deselect All"),
- PaletteItem::new("Switch Document"),
- PaletteItem::new("Insert Line Below"),
- PaletteItem::new("Insert Line Above"),
- PaletteItem::new("Move Line Up"),
- PaletteItem::new("Move Line Down"),
- PaletteItem::new("Toggle Comment"),
- PaletteItem::new("Delete Line"),
- ]
-}
-
-pub fn empty_editor_example(cx: &mut ViewContext<EditorPane>) -> EditorPane {
- EditorPane::new(
- cx,
- static_tabs_example(),
- PathBuf::from_str("crates/ui/src/static_data.rs").unwrap(),
- vec![],
- empty_buffer_example(),
- )
-}
-
-pub fn empty_buffer_example() -> Buffer {
- Buffer::new("empty-buffer").set_rows(Some(BufferRows::default()))
-}
-
-pub fn hello_world_rust_editor_example(cx: &mut ViewContext<EditorPane>) -> EditorPane {
- EditorPane::new(
- cx,
- static_tabs_example(),
- PathBuf::from_str("crates/ui/src/static_data.rs").unwrap(),
- vec![Symbol(vec![
- HighlightedText {
- text: "fn ".to_string(),
- color: cx.theme().syntax_color("keyword"),
- },
- HighlightedText {
- text: "main".to_string(),
- color: cx.theme().syntax_color("function"),
- },
- ])],
- hello_world_rust_buffer_example(cx),
- )
-}
-
-pub fn hello_world_rust_buffer_example(cx: &AppContext) -> Buffer {
- Buffer::new("hello-world-rust-buffer")
- .set_title("hello_world.rs".to_string())
- .set_path("src/hello_world.rs".to_string())
- .set_language("rust".to_string())
- .set_rows(Some(BufferRows {
- show_line_numbers: true,
- rows: hello_world_rust_buffer_rows(cx),
- }))
-}
-
-pub fn hello_world_rust_buffer_rows(cx: &AppContext) -> Vec<BufferRow> {
- let show_line_number = true;
-
- vec![
- BufferRow {
- line_number: 1,
- code_action: false,
- current: true,
- line: Some(HighlightedLine {
- highlighted_texts: vec![
- HighlightedText {
- text: "fn ".to_string(),
- color: cx.theme().syntax_color("keyword"),
- },
- HighlightedText {
- text: "main".to_string(),
- color: cx.theme().syntax_color("function"),
- },
- HighlightedText {
- text: "() {".to_string(),
- color: cx.theme().colors().text,
- },
- ],
- }),
- cursors: None,
- status: GitStatus::None,
- show_line_number,
- },
- BufferRow {
- line_number: 2,
- code_action: false,
- current: false,
- line: Some(HighlightedLine {
- highlighted_texts: vec![HighlightedText {
- text: " // Statements here are executed when the compiled binary is called."
- .to_string(),
- color: cx.theme().syntax_color("comment"),
- }],
- }),
- cursors: None,
- status: GitStatus::None,
- show_line_number,
- },
- BufferRow {
- line_number: 3,
- code_action: false,
- current: false,
- line: None,
- cursors: None,
- status: GitStatus::None,
- show_line_number,
- },
- BufferRow {
- line_number: 4,
- code_action: false,
- current: false,
- line: Some(HighlightedLine {
- highlighted_texts: vec![HighlightedText {
- text: " // Print text to the console.".to_string(),
- color: cx.theme().syntax_color("comment"),
- }],
- }),
- cursors: None,
- status: GitStatus::None,
- show_line_number,
- },
- BufferRow {
- line_number: 5,
- code_action: false,
- current: false,
- line: Some(HighlightedLine {
- highlighted_texts: vec![
- HighlightedText {
- text: " println!(".to_string(),
- color: cx.theme().colors().text,
- },
- HighlightedText {
- text: "\"Hello, world!\"".to_string(),
- color: cx.theme().syntax_color("string"),
- },
- HighlightedText {
- text: ");".to_string(),
- color: cx.theme().colors().text,
- },
- ],
- }),
- cursors: None,
- status: GitStatus::None,
- show_line_number,
- },
- BufferRow {
- line_number: 6,
- code_action: false,
- current: false,
- line: Some(HighlightedLine {
- highlighted_texts: vec![HighlightedText {
- text: "}".to_string(),
- color: cx.theme().colors().text,
- }],
- }),
- cursors: None,
- status: GitStatus::None,
- show_line_number,
- },
- ]
-}
-
-pub fn hello_world_rust_editor_with_status_example(cx: &mut ViewContext<EditorPane>) -> EditorPane {
- EditorPane::new(
- cx,
- static_tabs_example(),
- PathBuf::from_str("crates/ui/src/static_data.rs").unwrap(),
- vec![Symbol(vec![
- HighlightedText {
- text: "fn ".to_string(),
- color: cx.theme().syntax_color("keyword"),
- },
- HighlightedText {
- text: "main".to_string(),
- color: cx.theme().syntax_color("function"),
- },
- ])],
- hello_world_rust_buffer_with_status_example(cx),
- )
-}
-
-pub fn hello_world_rust_buffer_with_status_example(cx: &AppContext) -> Buffer {
- Buffer::new("hello-world-rust-buffer-with-status")
- .set_title("hello_world.rs".to_string())
- .set_path("src/hello_world.rs".to_string())
- .set_language("rust".to_string())
- .set_rows(Some(BufferRows {
- show_line_numbers: true,
- rows: hello_world_rust_with_status_buffer_rows(cx),
- }))
-}
-
-pub fn hello_world_rust_with_status_buffer_rows(cx: &AppContext) -> Vec<BufferRow> {
- let show_line_number = true;
-
- vec![
- BufferRow {
- line_number: 1,
- code_action: false,
- current: true,
- line: Some(HighlightedLine {
- highlighted_texts: vec![
- HighlightedText {
- text: "fn ".to_string(),
- color: cx.theme().syntax_color("keyword"),
- },
- HighlightedText {
- text: "main".to_string(),
- color: cx.theme().syntax_color("function"),
- },
- HighlightedText {
- text: "() {".to_string(),
- color: cx.theme().colors().text,
- },
- ],
- }),
- cursors: None,
- status: GitStatus::None,
- show_line_number,
- },
- BufferRow {
- line_number: 2,
- code_action: false,
- current: false,
- line: Some(HighlightedLine {
- highlighted_texts: vec![HighlightedText {
- text: "// Statements here are executed when the compiled binary is called."
- .to_string(),
- color: cx.theme().syntax_color("comment"),
- }],
- }),
- cursors: None,
- status: GitStatus::Modified,
- show_line_number,
- },
- BufferRow {
- line_number: 3,
- code_action: false,
- current: false,
- line: None,
- cursors: None,
- status: GitStatus::None,
- show_line_number,
- },
- BufferRow {
- line_number: 4,
- code_action: false,
- current: false,
- line: Some(HighlightedLine {
- highlighted_texts: vec![HighlightedText {
- text: " // Print text to the console.".to_string(),
- color: cx.theme().syntax_color("comment"),
- }],
- }),
- cursors: None,
- status: GitStatus::None,
- show_line_number,
- },
- BufferRow {
- line_number: 5,
- code_action: false,
- current: false,
- line: Some(HighlightedLine {
- highlighted_texts: vec![
- HighlightedText {
- text: " println!(".to_string(),
- color: cx.theme().colors().text,
- },
- HighlightedText {
- text: "\"Hello, world!\"".to_string(),
- color: cx.theme().syntax_color("string"),
- },
- HighlightedText {
- text: ");".to_string(),
- color: cx.theme().colors().text,
- },
- ],
- }),
- cursors: None,
- status: GitStatus::None,
- show_line_number,
- },
- BufferRow {
- line_number: 6,
- code_action: false,
- current: false,
- line: Some(HighlightedLine {
- highlighted_texts: vec![HighlightedText {
- text: "}".to_string(),
- color: cx.theme().colors().text,
- }],
- }),
- cursors: None,
- status: GitStatus::None,
- show_line_number,
- },
- BufferRow {
- line_number: 7,
- code_action: false,
- current: false,
- line: Some(HighlightedLine {
- highlighted_texts: vec![HighlightedText {
- text: "".to_string(),
- color: cx.theme().colors().text,
- }],
- }),
- cursors: None,
- status: GitStatus::Created,
- show_line_number,
- },
- BufferRow {
- line_number: 8,
- code_action: false,
- current: false,
- line: Some(HighlightedLine {
- highlighted_texts: vec![HighlightedText {
- text: "// Marshall and Nate were here".to_string(),
- color: cx.theme().syntax_color("comment"),
- }],
- }),
- cursors: None,
- status: GitStatus::Created,
- show_line_number,
- },
- ]
-}
-
-pub fn terminal_buffer(cx: &AppContext) -> Buffer {
- Buffer::new("terminal")
- .set_title("zed — fish".to_string())
- .set_rows(Some(BufferRows {
- show_line_numbers: false,
- rows: terminal_buffer_rows(cx),
- }))
-}
-
-pub fn terminal_buffer_rows(cx: &AppContext) -> Vec<BufferRow> {
- let show_line_number = false;
-
- vec![
- BufferRow {
- line_number: 1,
- code_action: false,
- current: false,
- line: Some(HighlightedLine {
- highlighted_texts: vec![
- HighlightedText {
- text: "maxdeviant ".to_string(),
- color: cx.theme().syntax_color("keyword"),
- },
- HighlightedText {
- text: "in ".to_string(),
- color: cx.theme().colors().text,
- },
- HighlightedText {
- text: "profaned-capital ".to_string(),
- color: cx.theme().syntax_color("function"),
- },
- HighlightedText {
- text: "in ".to_string(),
- color: cx.theme().colors().text,
- },
- HighlightedText {
- text: "~/p/zed ".to_string(),
- color: cx.theme().syntax_color("function"),
- },
- HighlightedText {
- text: "on ".to_string(),
- color: cx.theme().colors().text,
- },
- HighlightedText {
- text: " gpui2-ui ".to_string(),
- color: cx.theme().syntax_color("keyword"),
- },
- ],
- }),
- cursors: None,
- status: GitStatus::None,
- show_line_number,
- },
- BufferRow {
- line_number: 2,
- code_action: false,
- current: false,
- line: Some(HighlightedLine {
- highlighted_texts: vec![HighlightedText {
- text: "λ ".to_string(),
- color: cx.theme().syntax_color("string"),
- }],
- }),
- cursors: None,
- status: GitStatus::None,
- show_line_number,
- },
- ]
-}
@@ -5,7 +5,7 @@ use crate::prelude::*;
pub struct Story {}
impl Story {
- pub fn container<V: 'static>(cx: &mut ViewContext<V>) -> Div<V> {
+ pub fn container(cx: &mut gpui::WindowContext) -> Div {
div()
.size_full()
.flex()
@@ -15,23 +15,23 @@ impl Story {
.bg(cx.theme().colors().background)
}
- pub fn title<V: 'static>(cx: &mut ViewContext<V>, title: &str) -> impl Component<V> {
+ pub fn title(cx: &mut WindowContext, title: impl Into<SharedString>) -> impl Element {
div()
.text_xl()
.text_color(cx.theme().colors().text)
- .child(title.to_owned())
+ .child(title.into())
}
- pub fn title_for<V: 'static, T>(cx: &mut ViewContext<V>) -> impl Component<V> {
+ pub fn title_for<T>(cx: &mut WindowContext) -> impl Element {
Self::title(cx, std::any::type_name::<T>())
}
- pub fn label<V: 'static>(cx: &mut ViewContext<V>, label: &str) -> impl Component<V> {
+ pub fn label(cx: &mut WindowContext, label: impl Into<SharedString>) -> impl Element {
div()
.mt_4()
.mb_2()
.text_xs()
.text_color(cx.theme().colors().text)
- .child(label.to_owned())
+ .child(label.into())
}
}
@@ -1,9 +1,9 @@
-use gpui::{Styled, ViewContext};
+use gpui::{Styled, WindowContext};
use theme2::ActiveTheme;
use crate::{ElevationIndex, UITextSize};
-fn elevated<E: Styled, V: 'static>(this: E, cx: &mut ViewContext<V>, index: ElevationIndex) -> E {
+fn elevated<E: Styled>(this: E, cx: &mut WindowContext, index: ElevationIndex) -> E {
this.bg(cx.theme().colors().elevated_surface_background)
.z_index(index.z_index())
.rounded_lg()
@@ -65,7 +65,7 @@ pub trait StyledExt: Styled + Sized {
/// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()`
///
/// Example Elements: Title Bar, Panel, Tab Bar, Editor
- fn elevation_1<V: 'static>(self, cx: &mut ViewContext<V>) -> Self {
+ fn elevation_1(self, cx: &mut WindowContext) -> Self {
elevated(self, cx, ElevationIndex::Surface)
}
@@ -74,12 +74,10 @@ pub trait StyledExt: Styled + Sized {
/// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()`
///
/// Examples: Notifications, Palettes, Detached/Floating Windows, Detached/Floating Panels
- fn elevation_2<V: 'static>(self, cx: &mut ViewContext<V>) -> Self {
+ fn elevation_2(self, cx: &mut WindowContext) -> Self {
elevated(self, cx, ElevationIndex::ElevatedSurface)
}
- // There is no elevation 3, as the third elevation level is reserved for wash layers. See [`Elevation`](ui2::Elevation).
-
/// Modal Surfaces are used for elements that should appear above all other UI elements and are located above the wash layer. This is the maximum elevation at which UI elements can be rendered in their default state.
///
/// Elements rendered at this layer should have an enforced behavior: Any interaction outside of the modal will either dismiss the modal or prompt an action (Save your progress, etc) then dismiss the modal.
@@ -89,7 +87,7 @@ pub trait StyledExt: Styled + Sized {
/// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()`
///
/// Examples: Settings Modal, Channel Management, Wizards/Setup UI, Dialogs
- fn elevation_4<V: 'static>(self, cx: &mut ViewContext<V>) -> Self {
+ fn elevation_3(self, cx: &mut WindowContext) -> Self {
elevated(self, cx, ElevationIndex::ModalSurface)
}
}
@@ -0,0 +1,7 @@
+mod color;
+mod elevation;
+mod typography;
+
+pub use color::*;
+pub use elevation::*;
+pub use typography::*;
@@ -0,0 +1,44 @@
+use gpui::{Hsla, WindowContext};
+use theme2::ActiveTheme;
+
+#[derive(Default, PartialEq, Copy, Clone)]
+pub enum Color {
+ #[default]
+ Default,
+ Accent,
+ Created,
+ Deleted,
+ Disabled,
+ Error,
+ Hidden,
+ Info,
+ Modified,
+ Muted,
+ Placeholder,
+ Player(u32),
+ Selected,
+ Success,
+ Warning,
+}
+
+impl Color {
+ pub fn color(&self, cx: &WindowContext) -> Hsla {
+ match self {
+ Color::Default => cx.theme().colors().text,
+ Color::Muted => cx.theme().colors().text_muted,
+ Color::Created => cx.theme().status().created,
+ Color::Modified => cx.theme().status().modified,
+ Color::Deleted => cx.theme().status().deleted,
+ Color::Disabled => cx.theme().colors().text_disabled,
+ Color::Hidden => cx.theme().status().hidden,
+ Color::Info => cx.theme().status().info,
+ Color::Placeholder => cx.theme().colors().text_placeholder,
+ Color::Accent => cx.theme().colors().text_accent,
+ Color::Player(i) => cx.theme().styles.player.0[i.clone() as usize].cursor,
+ Color::Error => cx.theme().status().error,
+ Color::Selected => cx.theme().colors().text_accent,
+ Color::Success => cx.theme().status().success,
+ Color::Warning => cx.theme().status().warning,
+ }
+ }
+}
@@ -1,27 +1,10 @@
-TODO: Originally sourced from Material Design 3, Rewrite to be more Zed specific
-
# Elevation
-Zed applies elevation to all surfaces and components, which are categorized into levels.
-
-Elevation accomplishes the following:
-- Allows surfaces to move in front of or behind others, such as content scrolling beneath app top bars.
-- Reflects spatial relationships, for instance, how a floating action button’s shadow intimates its disconnection from a collection of cards.
-- Directs attention to structures at the highest elevation, like a temporary dialog arising in front of other surfaces.
-
-Elevations are the initial elevation values assigned to components by default.
-
-Components may transition to a higher elevation in some cases, like user interations.
-
-On such occasions, components transition to predetermined dynamic elevation offsets. These are the typical elevations to which components move when they are not at rest.
-
-## Understanding Elevation
-
Elevation can be thought of as the physical closeness of an element to the user. Elements with lower elevations are physically further away from the user on the z-axis and appear to be underneath elements with higher elevations.
Material Design 3 has a some great visualizations of elevation that may be helpful to understanding the mental modal of elevation. [Material Design – Elevation](https://m3.material.io/styles/elevation/overview)
-## Elevation
+## Elevation Levels
1. App Background (e.x.: Workspace, system window)
1. UI Surface (e.x.: Title Bar, Panel, Tab Bar)
@@ -59,27 +42,3 @@ Modal Surfaces are used for elements that should appear above all other UI eleme
Elements rendered at this layer have an enforced behavior: Any interaction outside of the modal will either dismiss the modal or prompt an action (Save your progress, etc) then dismiss the modal.
If the element does not have this behavior, it should be rendered at the Elevated Surface layer.
-
-## Layer
-Each elevation that can contain elements has its own set of layers that are nested within the elevations.
-
-1. TBD (Z -1 layer)
-1. Element (Text, button, surface, etc)
-1. Elevated Element (Popover, Context Menu, Tooltip)
-999. Dragged Element -> Highest Elevation
-
-Dragged elements jump to the highest elevation the app can render. An active drag should _always_ be the most foreground element in the app at any time.
-
-🚧 Work in Progress 🚧
-
-## Element
-Each elevation that can contain elements has it's own set of layers:
-
-1. Effects
-1. Background
-1. Tint
-1. Highlight
-1. Content
-1. Overlay
-
-🚧 Work in Progress 🚧
@@ -1,7 +1,7 @@
use gpui::{hsla, point, px, BoxShadow};
use smallvec::{smallvec, SmallVec};
-#[doc = include_str!("elevation.md")]
+#[doc = include_str!("docs/elevation.md")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Elevation {
ElevationIndex(ElevationIndex),
@@ -25,8 +25,8 @@ impl ElevationIndex {
ElevationIndex::Background => 0,
ElevationIndex::Surface => 100,
ElevationIndex::ElevatedSurface => 200,
- ElevationIndex::Wash => 300,
- ElevationIndex::ModalSurface => 400,
+ ElevationIndex::Wash => 250,
+ ElevationIndex::ModalSurface => 300,
ElevationIndex::DraggedElement => 900,
}
}
@@ -50,7 +50,7 @@ impl ElevationIndex {
spread_radius: px(0.),
},
BoxShadow {
- color: hsla(0., 0., 0., 0.16),
+ color: hsla(0., 0., 0., 0.20),
offset: point(px(3.), px(1.)),
blur_radius: px(12.),
spread_radius: px(0.),
@@ -0,0 +1,27 @@
+use gpui::{rems, Rems};
+
+#[derive(Debug, Default, Clone)]
+pub enum UITextSize {
+ /// The default size for UI text.
+ ///
+ /// `0.825rem` or `14px` at the default scale of `1rem` = `16px`.
+ ///
+ /// Note: The absolute size of this text will change based on a user's `ui_scale` setting.
+ #[default]
+ Default,
+ /// The small size for UI text.
+ ///
+ /// `0.75rem` or `12px` at the default scale of `1rem` = `16px`.
+ ///
+ /// Note: The absolute size of this text will change based on a user's `ui_scale` setting.
+ Small,
+}
+
+impl UITextSize {
+ pub fn rems(self) -> Rems {
+ match self {
+ Self::Default => rems(0.875),
+ Self::Small => rems(0.75),
+ }
+ }
+}
@@ -1,47 +0,0 @@
-mod assistant_panel;
-mod breadcrumb;
-mod buffer;
-mod buffer_search;
-mod chat_panel;
-mod collab_panel;
-mod command_palette;
-mod copilot;
-mod editor_pane;
-mod language_selector;
-mod multi_buffer;
-mod notifications_panel;
-mod panes;
-mod project_panel;
-mod recent_projects;
-mod status_bar;
-mod tab_bar;
-mod terminal;
-mod theme_selector;
-mod title_bar;
-mod toolbar;
-mod traffic_lights;
-mod workspace;
-
-pub use assistant_panel::*;
-pub use breadcrumb::*;
-pub use buffer::*;
-pub use buffer_search::*;
-pub use chat_panel::*;
-pub use collab_panel::*;
-pub use command_palette::*;
-pub use copilot::*;
-pub use editor_pane::*;
-pub use language_selector::*;
-pub use multi_buffer::*;
-pub use notifications_panel::*;
-pub use panes::*;
-pub use project_panel::*;
-pub use recent_projects::*;
-pub use status_bar::*;
-pub use tab_bar::*;
-pub use terminal::*;
-pub use theme_selector::*;
-pub use title_bar::*;
-pub use toolbar::*;
-pub use traffic_lights::*;
-pub use workspace::*;
@@ -1,93 +0,0 @@
-use crate::prelude::*;
-use crate::{Icon, IconButton, Label, Panel, PanelSide};
-use gpui::{prelude::*, rems, AbsoluteLength};
-
-#[derive(Component)]
-pub struct AssistantPanel {
- id: ElementId,
- current_side: PanelSide,
-}
-
-impl AssistantPanel {
- pub fn new(id: impl Into<ElementId>) -> Self {
- Self {
- id: id.into(),
- current_side: PanelSide::default(),
- }
- }
-
- pub fn side(mut self, side: PanelSide) -> Self {
- self.current_side = side;
- self
- }
-
- fn render<V: 'static>(self, view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- Panel::new(self.id.clone(), cx)
- .children(vec![div()
- .flex()
- .flex_col()
- .h_full()
- .px_2()
- .gap_2()
- // Header
- .child(
- div()
- .flex()
- .justify_between()
- .gap_2()
- .child(
- div()
- .flex()
- .child(IconButton::new("menu", Icon::Menu))
- .child(Label::new("New Conversation")),
- )
- .child(
- div()
- .flex()
- .items_center()
- .gap_px()
- .child(IconButton::new("split_message", Icon::SplitMessage))
- .child(IconButton::new("quote", Icon::Quote))
- .child(IconButton::new("magic_wand", Icon::MagicWand))
- .child(IconButton::new("plus", Icon::Plus))
- .child(IconButton::new("maximize", Icon::Maximize)),
- ),
- )
- // Chat Body
- .child(
- div()
- .id("chat-body")
- .w_full()
- .flex()
- .flex_col()
- .gap_3()
- .overflow_y_scroll()
- .child(Label::new("Is this thing on?")),
- )
- .render()])
- .side(self.current_side)
- .width(AbsoluteLength::Rems(rems(32.)))
- }
-}
-
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use super::*;
- use crate::Story;
- use gpui::{Div, Render};
- pub struct AssistantPanelStory;
-
- impl Render for AssistantPanelStory {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- Story::container(cx)
- .child(Story::title_for::<_, AssistantPanel>(cx))
- .child(Story::label(cx, "Default"))
- .child(AssistantPanel::new("assistant-panel"))
- }
- }
-}
@@ -1,113 +0,0 @@
-use crate::{h_stack, prelude::*, HighlightedText};
-use gpui::{prelude::*, Div};
-use std::path::PathBuf;
-
-#[derive(Clone)]
-pub struct Symbol(pub Vec<HighlightedText>);
-
-#[derive(Component)]
-pub struct Breadcrumb {
- path: PathBuf,
- symbols: Vec<Symbol>,
-}
-
-impl Breadcrumb {
- pub fn new(path: PathBuf, symbols: Vec<Symbol>) -> Self {
- Self { path, symbols }
- }
-
- fn render_separator<V: 'static>(&self, cx: &WindowContext) -> Div<V> {
- div()
- .child(" › ")
- .text_color(cx.theme().colors().text_muted)
- }
-
- fn render<V: 'static>(self, view_state: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- let symbols_len = self.symbols.len();
-
- h_stack()
- .id("breadcrumb")
- .px_1()
- .text_ui_sm()
- .text_color(cx.theme().colors().text_muted)
- .rounded_md()
- .hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
- .active(|style| style.bg(cx.theme().colors().ghost_element_active))
- .child(self.path.clone().to_str().unwrap().to_string())
- .child(if !self.symbols.is_empty() {
- self.render_separator(cx)
- } else {
- div()
- })
- .child(
- div().flex().children(
- self.symbols
- .iter()
- .enumerate()
- // TODO: Could use something like `intersperse` here instead.
- .flat_map(|(ix, symbol)| {
- let mut items =
- vec![div().flex().children(symbol.0.iter().map(|segment| {
- div().child(segment.text.clone()).text_color(segment.color)
- }))];
-
- let is_last_segment = ix == symbols_len - 1;
- if !is_last_segment {
- items.push(self.render_separator(cx));
- }
-
- items
- })
- .collect::<Vec<_>>(),
- ),
- )
- }
-}
-
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use super::*;
- use crate::Story;
- use gpui::Render;
- use std::str::FromStr;
-
- pub struct BreadcrumbStory;
-
- impl Render for BreadcrumbStory {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- Story::container(cx)
- .child(Story::title_for::<_, Breadcrumb>(cx))
- .child(Story::label(cx, "Default"))
- .child(Breadcrumb::new(
- PathBuf::from_str("crates/ui/src/components/toolbar.rs").unwrap(),
- vec![
- Symbol(vec![
- HighlightedText {
- text: "impl ".to_string(),
- color: cx.theme().syntax_color("keyword"),
- },
- HighlightedText {
- text: "BreadcrumbStory".to_string(),
- color: cx.theme().syntax_color("function"),
- },
- ]),
- Symbol(vec![
- HighlightedText {
- text: "fn ".to_string(),
- color: cx.theme().syntax_color("keyword"),
- },
- HighlightedText {
- text: "render".to_string(),
- color: cx.theme().syntax_color("function"),
- },
- ]),
- ],
- ))
- }
- }
-}
@@ -1,266 +0,0 @@
-use gpui::{Hsla, WindowContext};
-
-use crate::prelude::*;
-use crate::{h_stack, v_stack, Icon, IconElement};
-
-#[derive(Default, PartialEq, Copy, Clone)]
-pub struct PlayerCursor {
- color: Hsla,
- index: usize,
-}
-
-#[derive(Default, PartialEq, Clone)]
-pub struct HighlightedText {
- pub text: String,
- pub color: Hsla,
-}
-
-#[derive(Default, PartialEq, Clone)]
-pub struct HighlightedLine {
- pub highlighted_texts: Vec<HighlightedText>,
-}
-
-#[derive(Default, PartialEq, Clone)]
-pub struct BufferRow {
- pub line_number: usize,
- pub code_action: bool,
- pub current: bool,
- pub line: Option<HighlightedLine>,
- pub cursors: Option<Vec<PlayerCursor>>,
- pub status: GitStatus,
- pub show_line_number: bool,
-}
-
-#[derive(Clone)]
-pub struct BufferRows {
- pub show_line_numbers: bool,
- pub rows: Vec<BufferRow>,
-}
-
-impl Default for BufferRows {
- fn default() -> Self {
- Self {
- show_line_numbers: true,
- rows: vec![BufferRow {
- line_number: 1,
- code_action: false,
- current: true,
- line: None,
- cursors: None,
- status: GitStatus::None,
- show_line_number: true,
- }],
- }
- }
-}
-
-impl BufferRow {
- pub fn new(line_number: usize) -> Self {
- Self {
- line_number,
- code_action: false,
- current: false,
- line: None,
- cursors: None,
- status: GitStatus::None,
- show_line_number: true,
- }
- }
-
- pub fn set_line(mut self, line: Option<HighlightedLine>) -> Self {
- self.line = line;
- self
- }
-
- pub fn set_cursors(mut self, cursors: Option<Vec<PlayerCursor>>) -> Self {
- self.cursors = cursors;
- self
- }
-
- pub fn add_cursor(mut self, cursor: PlayerCursor) -> Self {
- if let Some(cursors) = &mut self.cursors {
- cursors.push(cursor);
- } else {
- self.cursors = Some(vec![cursor]);
- }
- self
- }
-
- pub fn set_status(mut self, status: GitStatus) -> Self {
- self.status = status;
- self
- }
-
- pub fn set_show_line_number(mut self, show_line_number: bool) -> Self {
- self.show_line_number = show_line_number;
- self
- }
-
- pub fn set_code_action(mut self, code_action: bool) -> Self {
- self.code_action = code_action;
- self
- }
-
- pub fn set_current(mut self, current: bool) -> Self {
- self.current = current;
- self
- }
-}
-
-#[derive(Component, Clone)]
-pub struct Buffer {
- id: ElementId,
- rows: Option<BufferRows>,
- readonly: bool,
- language: Option<String>,
- title: Option<String>,
- path: Option<String>,
-}
-
-impl Buffer {
- pub fn new(id: impl Into<ElementId>) -> Self {
- Self {
- id: id.into(),
- rows: Some(BufferRows::default()),
- readonly: false,
- language: None,
- title: Some("untitled".to_string()),
- path: None,
- }
- }
-
- pub fn set_title<T: Into<Option<String>>>(mut self, title: T) -> Self {
- self.title = title.into();
- self
- }
-
- pub fn set_path<P: Into<Option<String>>>(mut self, path: P) -> Self {
- self.path = path.into();
- self
- }
-
- pub fn set_readonly(mut self, readonly: bool) -> Self {
- self.readonly = readonly;
- self
- }
-
- pub fn set_rows<R: Into<Option<BufferRows>>>(mut self, rows: R) -> Self {
- self.rows = rows.into();
- self
- }
-
- pub fn set_language<L: Into<Option<String>>>(mut self, language: L) -> Self {
- self.language = language.into();
- self
- }
-
- fn render_row<V: 'static>(row: BufferRow, cx: &WindowContext) -> impl Component<V> {
- let line_background = if row.current {
- cx.theme().colors().editor_active_line_background
- } else {
- cx.theme().styles.system.transparent
- };
-
- let line_number_color = if row.current {
- cx.theme().colors().text
- } else {
- cx.theme().syntax_color("comment")
- };
-
- h_stack()
- .bg(line_background)
- .w_full()
- .gap_2()
- .px_1()
- .child(
- h_stack()
- .w_4()
- .h_full()
- .px_0p5()
- .when(row.code_action, |c| {
- div().child(IconElement::new(Icon::Bolt))
- }),
- )
- .when(row.show_line_number, |this| {
- this.child(
- h_stack().justify_end().px_0p5().w_3().child(
- div()
- .text_color(line_number_color)
- .child(row.line_number.to_string()),
- ),
- )
- })
- .child(div().mx_0p5().w_1().h_full().bg(row.status.hsla(cx)))
- .children(row.line.map(|line| {
- div()
- .flex()
- .children(line.highlighted_texts.iter().map(|highlighted_text| {
- div()
- .text_color(highlighted_text.color)
- .child(highlighted_text.text.clone())
- }))
- }))
- }
-
- fn render_rows<V: 'static>(&self, cx: &WindowContext) -> Vec<impl Component<V>> {
- match &self.rows {
- Some(rows) => rows
- .rows
- .iter()
- .map(|row| Self::render_row(row.clone(), cx))
- .collect(),
- None => vec![],
- }
- }
-
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- let rows = self.render_rows(cx);
-
- v_stack()
- .flex_1()
- .w_full()
- .h_full()
- .bg(cx.theme().colors().editor_background)
- .children(rows)
- }
-}
-
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use super::*;
- use crate::{
- empty_buffer_example, hello_world_rust_buffer_example,
- hello_world_rust_buffer_with_status_example, Story,
- };
- use gpui::{rems, Div, Render};
-
- pub struct BufferStory;
-
- impl Render for BufferStory {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- Story::container(cx)
- .child(Story::title_for::<_, Buffer>(cx))
- .child(Story::label(cx, "Default"))
- .child(div().w(rems(64.)).h_96().child(empty_buffer_example()))
- .child(Story::label(cx, "Hello World (Rust)"))
- .child(
- div()
- .w(rems(64.))
- .h_96()
- .child(hello_world_rust_buffer_example(cx)),
- )
- .child(Story::label(cx, "Hello World (Rust) with Status"))
- .child(
- div()
- .w(rems(64.))
- .h_96()
- .child(hello_world_rust_buffer_with_status_example(cx)),
- )
- }
- }
-}
@@ -1,46 +0,0 @@
-use gpui::{Div, Render, View, VisualContext};
-
-use crate::prelude::*;
-use crate::{h_stack, Icon, IconButton, Input, TextColor};
-
-#[derive(Clone)]
-pub struct BufferSearch {
- is_replace_open: bool,
-}
-
-impl BufferSearch {
- pub fn new() -> Self {
- Self {
- is_replace_open: false,
- }
- }
-
- fn toggle_replace(&mut self, cx: &mut ViewContext<Self>) {
- self.is_replace_open = !self.is_replace_open;
-
- cx.notify();
- }
-
- pub fn view(cx: &mut WindowContext) -> View<Self> {
- cx.build_view(|cx| Self::new())
- }
-}
-
-impl Render for BufferSearch {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Div<Self> {
- h_stack()
- .bg(cx.theme().colors().toolbar_background)
- .p_2()
- .child(
- h_stack().child(Input::new("Search")).child(
- IconButton::<Self>::new("replace", Icon::Replace)
- .when(self.is_replace_open, |this| this.color(TextColor::Accent))
- .on_click(|buffer_search, cx| {
- buffer_search.toggle_replace(cx);
- }),
- ),
- )
- }
-}
@@ -1,150 +0,0 @@
-use crate::{prelude::*, Icon, IconButton, Input, Label};
-use chrono::NaiveDateTime;
-use gpui::prelude::*;
-
-#[derive(Component)]
-pub struct ChatPanel {
- element_id: ElementId,
- messages: Vec<ChatMessage>,
-}
-
-impl ChatPanel {
- pub fn new(element_id: impl Into<ElementId>) -> Self {
- Self {
- element_id: element_id.into(),
- messages: Vec::new(),
- }
- }
-
- pub fn messages(mut self, messages: Vec<ChatMessage>) -> Self {
- self.messages = messages;
- self
- }
-
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- div()
- .id(self.element_id.clone())
- .flex()
- .flex_col()
- .justify_between()
- .h_full()
- .px_2()
- .gap_2()
- // Header
- .child(
- div()
- .flex()
- .justify_between()
- .py_2()
- .child(div().flex().child(Label::new("#design")))
- .child(
- div()
- .flex()
- .items_center()
- .gap_px()
- .child(IconButton::new("file", Icon::File))
- .child(IconButton::new("audio_on", Icon::AudioOn)),
- ),
- )
- .child(
- div()
- .flex()
- .flex_col()
- // Chat Body
- .child(
- div()
- .id("chat-body")
- .w_full()
- .flex()
- .flex_col()
- .gap_3()
- .overflow_y_scroll()
- .children(self.messages),
- )
- // Composer
- .child(div().flex().my_2().child(Input::new("Message #design"))),
- )
- }
-}
-
-#[derive(Component)]
-pub struct ChatMessage {
- author: String,
- text: String,
- sent_at: NaiveDateTime,
-}
-
-impl ChatMessage {
- pub fn new(author: String, text: String, sent_at: NaiveDateTime) -> Self {
- Self {
- author,
- text,
- sent_at,
- }
- }
-
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- div()
- .flex()
- .flex_col()
- .child(
- div()
- .flex()
- .gap_2()
- .child(Label::new(self.author.clone()))
- .child(
- Label::new(self.sent_at.format("%m/%d/%Y").to_string())
- .color(TextColor::Muted),
- ),
- )
- .child(div().child(Label::new(self.text.clone())))
- }
-}
-
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use chrono::DateTime;
- use gpui::{Div, Render};
-
- use crate::{Panel, Story};
-
- use super::*;
-
- pub struct ChatPanelStory;
-
- impl Render for ChatPanelStory {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- Story::container(cx)
- .child(Story::title_for::<_, ChatPanel>(cx))
- .child(Story::label(cx, "Default"))
- .child(
- Panel::new("chat-panel-1-outer", cx)
- .child(ChatPanel::new("chat-panel-1-inner")),
- )
- .child(Story::label(cx, "With Mesages"))
- .child(Panel::new("chat-panel-2-outer", cx).child(
- ChatPanel::new("chat-panel-2-inner").messages(vec![
- ChatMessage::new(
- "osiewicz".to_string(),
- "is this thing on?".to_string(),
- DateTime::parse_from_rfc3339("2023-09-27T15:40:52.707Z")
- .unwrap()
- .naive_local(),
- ),
- ChatMessage::new(
- "maxdeviant".to_string(),
- "Reading you loud and clear!".to_string(),
- DateTime::parse_from_rfc3339("2023-09-28T15:40:52.707Z")
- .unwrap()
- .naive_local(),
- ),
- ]),
- ))
- }
- }
-}
@@ -1,110 +0,0 @@
-use crate::{
- prelude::*, static_collab_panel_channels, static_collab_panel_current_call, v_stack, Icon,
- List, ListHeader, Toggle,
-};
-use gpui::prelude::*;
-
-#[derive(Component)]
-pub struct CollabPanel {
- id: ElementId,
-}
-
-impl CollabPanel {
- pub fn new(id: impl Into<ElementId>) -> Self {
- Self { id: id.into() }
- }
-
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- v_stack()
- .id(self.id.clone())
- .h_full()
- .bg(cx.theme().colors().surface_background)
- .child(
- v_stack()
- .id("crdb")
- .w_full()
- .overflow_y_scroll()
- .child(
- div()
- .pb_1()
- .border_color(cx.theme().colors().border)
- .border_b()
- .child(
- List::new(static_collab_panel_current_call())
- .header(
- ListHeader::new("CRDB")
- .left_icon(Icon::Hash.into())
- .toggle(Toggle::Toggled(true)),
- )
- .toggle(Toggle::Toggled(true)),
- ),
- )
- .child(
- v_stack().id("channels").py_1().child(
- List::new(static_collab_panel_channels())
- .header(ListHeader::new("CHANNELS").toggle(Toggle::Toggled(true)))
- .empty_message("No channels yet. Add a channel to get started.")
- .toggle(Toggle::Toggled(true)),
- ),
- )
- .child(
- v_stack().id("contacts-online").py_1().child(
- List::new(static_collab_panel_current_call())
- .header(
- ListHeader::new("CONTACTS – ONLINE")
- .toggle(Toggle::Toggled(true)),
- )
- .toggle(Toggle::Toggled(true)),
- ),
- )
- .child(
- v_stack().id("contacts-offline").py_1().child(
- List::new(static_collab_panel_current_call())
- .header(
- ListHeader::new("CONTACTS – OFFLINE")
- .toggle(Toggle::Toggled(false)),
- )
- .toggle(Toggle::Toggled(false)),
- ),
- ),
- )
- .child(
- div()
- .h_7()
- .px_2()
- .border_t()
- .border_color(cx.theme().colors().border)
- .flex()
- .items_center()
- .child(
- div()
- .text_ui_sm()
- .text_color(cx.theme().colors().text_placeholder)
- .child("Find..."),
- ),
- )
- }
-}
-
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use super::*;
- use crate::Story;
- use gpui::{Div, Render};
-
- pub struct CollabPanelStory;
-
- impl Render for CollabPanelStory {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- Story::container(cx)
- .child(Story::title_for::<_, CollabPanel>(cx))
- .child(Story::label(cx, "Default"))
- .child(CollabPanel::new("collab-panel"))
- }
- }
-}
@@ -1,48 +0,0 @@
-use crate::prelude::*;
-use crate::{example_editor_actions, OrderMethod, Palette};
-
-#[derive(Component)]
-pub struct CommandPalette {
- id: ElementId,
-}
-
-impl CommandPalette {
- pub fn new(id: impl Into<ElementId>) -> Self {
- Self { id: id.into() }
- }
-
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- div().id(self.id.clone()).child(
- Palette::new("palette")
- .items(example_editor_actions())
- .placeholder("Execute a command...")
- .empty_string("No items found.")
- .default_order(OrderMethod::Ascending),
- )
- }
-}
-
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use gpui::{Div, Render};
-
- use crate::Story;
-
- use super::*;
-
- pub struct CommandPaletteStory;
-
- impl Render for CommandPaletteStory {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- Story::container(cx)
- .child(Story::title_for::<_, CommandPalette>(cx))
- .child(Story::label(cx, "Default"))
- .child(CommandPalette::new("command-palette"))
- }
- }
-}
@@ -1,46 +0,0 @@
-use crate::{prelude::*, Button, Label, Modal, TextColor};
-
-#[derive(Component)]
-pub struct CopilotModal {
- id: ElementId,
-}
-
-impl CopilotModal {
- pub fn new(id: impl Into<ElementId>) -> Self {
- Self { id: id.into() }
- }
-
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- div().id(self.id.clone()).child(
- Modal::new("some-id")
- .title("Connect Copilot to Zed")
- .child(Label::new("You can update your settings or sign out from the Copilot menu in the status bar.").color(TextColor::Muted))
- .primary_action(Button::new("Connect to Github").variant(ButtonVariant::Filled)),
- )
- }
-}
-
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use gpui::{Div, Render};
-
- use crate::Story;
-
- use super::*;
-
- pub struct CopilotModalStory;
-
- impl Render for CopilotModalStory {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- Story::container(cx)
- .child(Story::title_for::<_, CopilotModal>(cx))
- .child(Story::label(cx, "Default"))
- .child(CopilotModal::new("copilot-modal"))
- }
- }
-}
@@ -1,77 +0,0 @@
-use std::path::PathBuf;
-
-use gpui::{Div, Render, View, VisualContext};
-
-use crate::prelude::*;
-use crate::{
- hello_world_rust_editor_with_status_example, v_stack, Breadcrumb, Buffer, BufferSearch, Icon,
- IconButton, Symbol, Tab, TabBar, TextColor, Toolbar,
-};
-
-#[derive(Clone)]
-pub struct EditorPane {
- tabs: Vec<Tab>,
- path: PathBuf,
- symbols: Vec<Symbol>,
- buffer: Buffer,
- buffer_search: View<BufferSearch>,
- is_buffer_search_open: bool,
-}
-
-impl EditorPane {
- pub fn new(
- cx: &mut ViewContext<Self>,
- tabs: Vec<Tab>,
- path: PathBuf,
- symbols: Vec<Symbol>,
- buffer: Buffer,
- ) -> Self {
- Self {
- tabs,
- path,
- symbols,
- buffer,
- buffer_search: BufferSearch::view(cx),
- is_buffer_search_open: false,
- }
- }
-
- pub fn toggle_buffer_search(&mut self, cx: &mut ViewContext<Self>) {
- self.is_buffer_search_open = !self.is_buffer_search_open;
-
- cx.notify();
- }
-
- pub fn view(cx: &mut WindowContext) -> View<Self> {
- cx.build_view(|cx| hello_world_rust_editor_with_status_example(cx))
- }
-}
-
-impl Render for EditorPane {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Div<Self> {
- v_stack()
- .w_full()
- .h_full()
- .flex_1()
- .child(TabBar::new("editor-pane-tabs", self.tabs.clone()).can_navigate((false, true)))
- .child(
- Toolbar::new()
- .left_item(Breadcrumb::new(self.path.clone(), self.symbols.clone()))
- .right_items(vec![
- IconButton::<Self>::new("toggle_inlay_hints", Icon::InlayHint),
- IconButton::<Self>::new("buffer_search", Icon::MagnifyingGlass)
- .when(self.is_buffer_search_open, |this| {
- this.color(TextColor::Accent)
- })
- .on_click(|editor: &mut Self, cx| {
- editor.toggle_buffer_search(cx);
- }),
- IconButton::new("inline_assist", Icon::MagicWand),
- ]),
- )
- .children(Some(self.buffer_search.clone()).filter(|_| self.is_buffer_search_open))
- .child(self.buffer.clone())
- }
-}
@@ -1,57 +0,0 @@
-use crate::prelude::*;
-use crate::{OrderMethod, Palette, PaletteItem};
-
-#[derive(Component)]
-pub struct LanguageSelector {
- id: ElementId,
-}
-
-impl LanguageSelector {
- pub fn new(id: impl Into<ElementId>) -> Self {
- Self { id: id.into() }
- }
-
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- div().id(self.id.clone()).child(
- Palette::new("palette")
- .items(vec![
- PaletteItem::new("C"),
- PaletteItem::new("C++"),
- PaletteItem::new("CSS"),
- PaletteItem::new("Elixir"),
- PaletteItem::new("Elm"),
- PaletteItem::new("ERB"),
- PaletteItem::new("Rust (current)"),
- PaletteItem::new("Scheme"),
- PaletteItem::new("TOML"),
- PaletteItem::new("TypeScript"),
- ])
- .placeholder("Select a language...")
- .empty_string("No matches")
- .default_order(OrderMethod::Ascending),
- )
- }
-}
-
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use super::*;
- use crate::Story;
- use gpui::{Div, Render};
-
- pub struct LanguageSelectorStory;
-
- impl Render for LanguageSelectorStory {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- Story::container(cx)
- .child(Story::title_for::<_, LanguageSelector>(cx))
- .child(Story::label(cx, "Default"))
- .child(LanguageSelector::new("language-selector"))
- }
- }
-}
@@ -1,63 +0,0 @@
-use crate::prelude::*;
-use crate::{v_stack, Buffer, Icon, IconButton, Label};
-
-#[derive(Component)]
-pub struct MultiBuffer {
- buffers: Vec<Buffer>,
-}
-
-impl MultiBuffer {
- pub fn new(buffers: Vec<Buffer>) -> Self {
- Self { buffers }
- }
-
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- v_stack()
- .w_full()
- .h_full()
- .flex_1()
- .children(self.buffers.clone().into_iter().map(|buffer| {
- v_stack()
- .child(
- div()
- .flex()
- .items_center()
- .justify_between()
- .p_4()
- .bg(cx.theme().colors().editor_subheader_background)
- .child(Label::new("main.rs"))
- .child(IconButton::new("arrow_up_right", Icon::ArrowUpRight)),
- )
- .child(buffer)
- }))
- }
-}
-
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use super::*;
- use crate::{hello_world_rust_buffer_example, Story};
- use gpui::{Div, Render};
-
- pub struct MultiBufferStory;
-
- impl Render for MultiBufferStory {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- Story::container(cx)
- .child(Story::title_for::<_, MultiBuffer>(cx))
- .child(Story::label(cx, "Default"))
- .child(MultiBuffer::new(vec![
- hello_world_rust_buffer_example(cx),
- hello_world_rust_buffer_example(cx),
- hello_world_rust_buffer_example(cx),
- hello_world_rust_buffer_example(cx),
- hello_world_rust_buffer_example(cx),
- ]))
- }
- }
-}
@@ -1,371 +0,0 @@
-use crate::{
- h_stack, prelude::*, static_new_notification_items_2, utils::naive_format_distance_from_now,
- v_stack, Avatar, ButtonOrIconButton, ClickHandler, Icon, IconElement, Label, LineHeightStyle,
- ListHeader, ListHeaderMeta, ListSeparator, PublicPlayer, TextColor, UnreadIndicator,
-};
-use gpui::prelude::*;
-
-#[derive(Component)]
-pub struct NotificationsPanel {
- id: ElementId,
-}
-
-impl NotificationsPanel {
- pub fn new(id: impl Into<ElementId>) -> Self {
- Self { id: id.into() }
- }
-
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- div()
- .id(self.id.clone())
- .flex()
- .flex_col()
- .size_full()
- .bg(cx.theme().colors().surface_background)
- .child(
- ListHeader::new("Notifications").meta(Some(ListHeaderMeta::Tools(vec![
- Icon::AtSign,
- Icon::BellOff,
- Icon::MailOpen,
- ]))),
- )
- .child(ListSeparator::new())
- .child(
- v_stack()
- .id("notifications-panel-scroll-view")
- .py_1()
- .overflow_y_scroll()
- .flex_1()
- .child(
- div()
- .mx_2()
- .p_1()
- // TODO: Add cursor style
- // .cursor(Cursor::IBeam)
- .bg(cx.theme().colors().element_background)
- .border()
- .border_color(cx.theme().colors().border_variant)
- .child(
- Label::new("Search...")
- .color(TextColor::Placeholder)
- .line_height_style(LineHeightStyle::UILabel),
- ),
- )
- .child(v_stack().px_1().children(static_new_notification_items_2())),
- )
- }
-}
-
-pub struct NotificationAction<V: 'static> {
- button: ButtonOrIconButton<V>,
- tooltip: SharedString,
- /// Shows after action is chosen
- ///
- /// For example, if the action is "Accept" the taken message could be:
- ///
- /// - `(None,"Accepted")` - "Accepted"
- ///
- /// - `(Some(Icon::Check),"Accepted")` - ✓ "Accepted"
- taken_message: (Option<Icon>, SharedString),
-}
-
-impl<V: 'static> NotificationAction<V> {
- pub fn new(
- button: impl Into<ButtonOrIconButton<V>>,
- tooltip: impl Into<SharedString>,
- (icon, taken_message): (Option<Icon>, impl Into<SharedString>),
- ) -> Self {
- Self {
- button: button.into(),
- tooltip: tooltip.into(),
- taken_message: (icon, taken_message.into()),
- }
- }
-}
-
-pub enum ActorOrIcon {
- Actor(PublicPlayer),
- Icon(Icon),
-}
-
-pub struct NotificationMeta<V: 'static> {
- items: Vec<(Option<Icon>, SharedString, Option<ClickHandler<V>>)>,
-}
-
-struct NotificationHandlers<V: 'static> {
- click: Option<ClickHandler<V>>,
-}
-
-impl<V: 'static> Default for NotificationHandlers<V> {
- fn default() -> Self {
- Self { click: None }
- }
-}
-
-#[derive(Component)]
-pub struct Notification<V: 'static> {
- id: ElementId,
- slot: ActorOrIcon,
- message: SharedString,
- date_received: NaiveDateTime,
- meta: Option<NotificationMeta<V>>,
- actions: Option<[NotificationAction<V>; 2]>,
- unread: bool,
- new: bool,
- action_taken: Option<NotificationAction<V>>,
- handlers: NotificationHandlers<V>,
-}
-
-impl<V> Notification<V> {
- fn new(
- id: ElementId,
- message: SharedString,
- date_received: NaiveDateTime,
- slot: ActorOrIcon,
- click_action: Option<ClickHandler<V>>,
- ) -> Self {
- let handlers = if click_action.is_some() {
- NotificationHandlers {
- click: click_action,
- }
- } else {
- NotificationHandlers::default()
- };
-
- Self {
- id,
- date_received,
- message,
- meta: None,
- slot,
- actions: None,
- unread: true,
- new: false,
- action_taken: None,
- handlers,
- }
- }
-
- /// Creates a new notification with an actor slot.
- ///
- /// Requires a click action.
- pub fn new_actor_message(
- id: impl Into<ElementId>,
- message: impl Into<SharedString>,
- date_received: NaiveDateTime,
- actor: PublicPlayer,
- click_action: ClickHandler<V>,
- ) -> Self {
- Self::new(
- id.into(),
- message.into(),
- date_received,
- ActorOrIcon::Actor(actor),
- Some(click_action),
- )
- }
-
- /// Creates a new notification with an icon slot.
- ///
- /// Requires a click action.
- pub fn new_icon_message(
- id: impl Into<ElementId>,
- message: impl Into<SharedString>,
- date_received: NaiveDateTime,
- icon: Icon,
- click_action: ClickHandler<V>,
- ) -> Self {
- Self::new(
- id.into(),
- message.into(),
- date_received,
- ActorOrIcon::Icon(icon),
- Some(click_action),
- )
- }
-
- /// Creates a new notification with an actor slot
- /// and a Call To Action row.
- ///
- /// Cannot take a click action due to required actions.
- pub fn new_actor_with_actions(
- id: impl Into<ElementId>,
- message: impl Into<SharedString>,
- date_received: NaiveDateTime,
- actor: PublicPlayer,
- actions: [NotificationAction<V>; 2],
- ) -> Self {
- Self::new(
- id.into(),
- message.into(),
- date_received,
- ActorOrIcon::Actor(actor),
- None,
- )
- .actions(actions)
- }
-
- /// Creates a new notification with an icon slot
- /// and a Call To Action row.
- ///
- /// Cannot take a click action due to required actions.
- pub fn new_icon_with_actions(
- id: impl Into<ElementId>,
- message: impl Into<SharedString>,
- date_received: NaiveDateTime,
- icon: Icon,
- actions: [NotificationAction<V>; 2],
- ) -> Self {
- Self::new(
- id.into(),
- message.into(),
- date_received,
- ActorOrIcon::Icon(icon),
- None,
- )
- .actions(actions)
- }
-
- fn on_click(mut self, handler: ClickHandler<V>) -> Self {
- self.handlers.click = Some(handler);
- self
- }
-
- pub fn actions(mut self, actions: [NotificationAction<V>; 2]) -> Self {
- self.actions = Some(actions);
- self
- }
-
- pub fn meta(mut self, meta: NotificationMeta<V>) -> Self {
- self.meta = Some(meta);
- self
- }
-
- fn render_meta_items(&self, cx: &mut ViewContext<V>) -> impl Component<V> {
- if let Some(meta) = &self.meta {
- h_stack().children(
- meta.items
- .iter()
- .map(|(icon, text, _)| {
- let mut meta_el = div();
- if let Some(icon) = icon {
- meta_el = meta_el.child(IconElement::new(icon.clone()));
- }
- meta_el.child(Label::new(text.clone()).color(TextColor::Muted))
- })
- .collect::<Vec<_>>(),
- )
- } else {
- div()
- }
- }
-
- fn render_slot(&self, cx: &mut ViewContext<V>) -> impl Component<V> {
- match &self.slot {
- ActorOrIcon::Actor(actor) => Avatar::new(actor.avatar.clone()).render(),
- ActorOrIcon::Icon(icon) => IconElement::new(icon.clone()).render(),
- }
- }
-
- fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- div()
- .relative()
- .id(self.id.clone())
- .p_1()
- .flex()
- .flex_col()
- .w_full()
- .children(
- Some(
- div()
- .absolute()
- .left(px(3.0))
- .top_3()
- .z_index(2)
- .child(UnreadIndicator::new()),
- )
- .filter(|_| self.unread),
- )
- .child(
- v_stack()
- .z_index(1)
- .gap_1()
- .w_full()
- .child(
- h_stack()
- .w_full()
- .gap_2()
- .child(self.render_slot(cx))
- .child(div().flex_1().child(Label::new(self.message.clone()))),
- )
- .child(
- h_stack()
- .justify_between()
- .child(
- h_stack()
- .gap_1()
- .child(
- Label::new(naive_format_distance_from_now(
- self.date_received,
- true,
- true,
- ))
- .color(TextColor::Muted),
- )
- .child(self.render_meta_items(cx)),
- )
- .child(match (self.actions, self.action_taken) {
- // Show nothing
- (None, _) => div(),
- // Show the taken_message
- (Some(_), Some(action_taken)) => h_stack()
- .children(action_taken.taken_message.0.map(|icon| {
- IconElement::new(icon).color(crate::TextColor::Muted)
- }))
- .child(
- Label::new(action_taken.taken_message.1.clone())
- .color(TextColor::Muted),
- ),
- // Show the actions
- (Some(actions), None) => {
- h_stack().children(actions.map(|action| match action.button {
- ButtonOrIconButton::Button(button) => {
- Component::render(button)
- }
- ButtonOrIconButton::IconButton(icon_button) => {
- Component::render(icon_button)
- }
- }))
- }
- }),
- ),
- )
- }
-}
-
-use chrono::NaiveDateTime;
-use gpui::{px, Styled};
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use super::*;
- use crate::{Panel, Story};
- use gpui::{Div, Render};
-
- pub struct NotificationsPanelStory;
-
- impl Render for NotificationsPanelStory {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- Story::container(cx)
- .child(Story::title_for::<_, NotificationsPanel>(cx))
- .child(Story::label(cx, "Default"))
- .child(
- Panel::new("panel", cx).child(NotificationsPanel::new("notifications_panel")),
- )
- }
- }
-}
@@ -1,128 +0,0 @@
-use gpui::{hsla, red, AnyElement, ElementId, ExternalPaths, Hsla, Length, Size, View};
-use smallvec::SmallVec;
-
-use crate::prelude::*;
-
-#[derive(Default, PartialEq)]
-pub enum SplitDirection {
- #[default]
- Horizontal,
- Vertical,
-}
-
-#[derive(Component)]
-pub struct Pane<V: 'static> {
- id: ElementId,
- size: Size<Length>,
- fill: Hsla,
- children: SmallVec<[AnyElement<V>; 2]>,
-}
-
-impl<V: 'static> Pane<V> {
- pub fn new(id: impl Into<ElementId>, size: Size<Length>) -> Self {
- // Fill is only here for debugging purposes, remove before release
-
- Self {
- id: id.into(),
- size,
- fill: hsla(0.3, 0.3, 0.3, 1.),
- children: SmallVec::new(),
- }
- }
-
- pub fn fill(mut self, fill: Hsla) -> Self {
- self.fill = fill;
- self
- }
-
- fn render(self, view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- div()
- .id(self.id.clone())
- .flex()
- .flex_initial()
- .bg(self.fill)
- .w(self.size.width)
- .h(self.size.height)
- .relative()
- .child(div().z_index(0).size_full().children(self.children))
- .child(
- div()
- .z_index(1)
- .id("drag-target")
- .drag_over::<ExternalPaths>(|d| d.bg(red()))
- .on_drop(|_, files: View<ExternalPaths>, cx| {
- eprintln!("dropped files! {:?}", files.read(cx));
- })
- .absolute()
- .inset_0(),
- )
- }
-}
-
-impl<V: 'static> ParentComponent<V> for Pane<V> {
- fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
- &mut self.children
- }
-}
-
-#[derive(Component)]
-pub struct PaneGroup<V: 'static> {
- groups: Vec<PaneGroup<V>>,
- panes: Vec<Pane<V>>,
- split_direction: SplitDirection,
-}
-
-impl<V: 'static> PaneGroup<V> {
- pub fn new_groups(groups: Vec<PaneGroup<V>>, split_direction: SplitDirection) -> Self {
- Self {
- groups,
- panes: Vec::new(),
- split_direction,
- }
- }
-
- pub fn new_panes(panes: Vec<Pane<V>>, split_direction: SplitDirection) -> Self {
- Self {
- groups: Vec::new(),
- panes,
- split_direction,
- }
- }
-
- fn render(self, view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- if !self.panes.is_empty() {
- let el = div()
- .flex()
- .flex_1()
- .gap_px()
- .w_full()
- .h_full()
- .children(self.panes.into_iter().map(|pane| pane.render(view, cx)));
-
- if self.split_direction == SplitDirection::Horizontal {
- return el;
- } else {
- return el.flex_col();
- }
- }
-
- if !self.groups.is_empty() {
- let el = div()
- .flex()
- .flex_1()
- .gap_px()
- .w_full()
- .h_full()
- .bg(cx.theme().colors().editor_background)
- .children(self.groups.into_iter().map(|group| group.render(view, cx)));
-
- if self.split_direction == SplitDirection::Horizontal {
- return el;
- } else {
- return el.flex_col();
- }
- }
-
- unreachable!()
- }
-}
@@ -1,75 +0,0 @@
-use crate::{
- prelude::*, static_project_panel_project_items, static_project_panel_single_items, Input, List,
- ListHeader,
-};
-use gpui::prelude::*;
-
-#[derive(Component)]
-pub struct ProjectPanel {
- id: ElementId,
-}
-
-impl ProjectPanel {
- pub fn new(id: impl Into<ElementId>) -> Self {
- Self { id: id.into() }
- }
-
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- div()
- .id(self.id.clone())
- .flex()
- .flex_col()
- .size_full()
- .bg(cx.theme().colors().surface_background)
- .child(
- div()
- .id("project-panel-contents")
- .w_full()
- .flex()
- .flex_col()
- .overflow_y_scroll()
- .child(
- List::new(static_project_panel_single_items())
- .header(ListHeader::new("FILES"))
- .empty_message("No files in directory"),
- )
- .child(
- List::new(static_project_panel_project_items())
- .header(ListHeader::new("PROJECT"))
- .empty_message("No folders in directory"),
- ),
- )
- .child(
- Input::new("Find something...")
- .value("buffe".to_string())
- .state(InteractionState::Focused),
- )
- }
-}
-
-use gpui::ElementId;
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use super::*;
- use crate::{Panel, Story};
- use gpui::{Div, Render};
-
- pub struct ProjectPanelStory;
-
- impl Render for ProjectPanelStory {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- Story::container(cx)
- .child(Story::title_for::<_, ProjectPanel>(cx))
- .child(Story::label(cx, "Default"))
- .child(
- Panel::new("project-panel-outer", cx)
- .child(ProjectPanel::new("project-panel-inner")),
- )
- }
- }
-}
@@ -1,53 +0,0 @@
-use crate::prelude::*;
-use crate::{OrderMethod, Palette, PaletteItem};
-
-#[derive(Component)]
-pub struct RecentProjects {
- id: ElementId,
-}
-
-impl RecentProjects {
- pub fn new(id: impl Into<ElementId>) -> Self {
- Self { id: id.into() }
- }
-
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- div().id(self.id.clone()).child(
- Palette::new("palette")
- .items(vec![
- PaletteItem::new("zed").sublabel(SharedString::from("~/projects/zed")),
- PaletteItem::new("saga").sublabel(SharedString::from("~/projects/saga")),
- PaletteItem::new("journal").sublabel(SharedString::from("~/journal")),
- PaletteItem::new("dotfiles").sublabel(SharedString::from("~/dotfiles")),
- PaletteItem::new("zed.dev").sublabel(SharedString::from("~/projects/zed.dev")),
- PaletteItem::new("laminar").sublabel(SharedString::from("~/projects/laminar")),
- ])
- .placeholder("Recent Projects...")
- .empty_string("No matches")
- .default_order(OrderMethod::Ascending),
- )
- }
-}
-
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use super::*;
- use crate::Story;
- use gpui::{Div, Render};
-
- pub struct RecentProjectsStory;
-
- impl Render for RecentProjectsStory {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- Story::container(cx)
- .child(Story::title_for::<_, RecentProjects>(cx))
- .child(Story::label(cx, "Default"))
- .child(RecentProjects::new("recent-projects"))
- }
- }
-}
@@ -1,203 +0,0 @@
-use std::sync::Arc;
-
-use crate::prelude::*;
-use crate::{Button, Icon, IconButton, TextColor, ToolDivider, Workspace};
-
-#[derive(Default, PartialEq)]
-pub enum Tool {
- #[default]
- ProjectPanel,
- CollaborationPanel,
- Terminal,
- Assistant,
- Feedback,
- Diagnostics,
-}
-
-struct ToolGroup {
- active_index: Option<usize>,
- tools: Vec<Tool>,
-}
-
-impl Default for ToolGroup {
- fn default() -> Self {
- ToolGroup {
- active_index: None,
- tools: vec![],
- }
- }
-}
-
-#[derive(Component)]
-#[component(view_type = "Workspace")]
-pub struct StatusBar {
- left_tools: Option<ToolGroup>,
- right_tools: Option<ToolGroup>,
- bottom_tools: Option<ToolGroup>,
-}
-
-impl StatusBar {
- pub fn new() -> Self {
- Self {
- left_tools: None,
- right_tools: None,
- bottom_tools: None,
- }
- }
-
- pub fn left_tool(mut self, tool: Tool, active_index: Option<usize>) -> Self {
- self.left_tools = {
- let mut tools = vec![tool];
- tools.extend(self.left_tools.take().unwrap_or_default().tools);
- Some(ToolGroup {
- active_index,
- tools,
- })
- };
- self
- }
-
- pub fn right_tool(mut self, tool: Tool, active_index: Option<usize>) -> Self {
- self.right_tools = {
- let mut tools = vec![tool];
- tools.extend(self.left_tools.take().unwrap_or_default().tools);
- Some(ToolGroup {
- active_index,
- tools,
- })
- };
- self
- }
-
- pub fn bottom_tool(mut self, tool: Tool, active_index: Option<usize>) -> Self {
- self.bottom_tools = {
- let mut tools = vec![tool];
- tools.extend(self.left_tools.take().unwrap_or_default().tools);
- Some(ToolGroup {
- active_index,
- tools,
- })
- };
- self
- }
-
- fn render(
- self,
- view: &mut Workspace,
- cx: &mut ViewContext<Workspace>,
- ) -> impl Component<Workspace> {
- div()
- .py_0p5()
- .px_1()
- .flex()
- .items_center()
- .justify_between()
- .w_full()
- .bg(cx.theme().colors().status_bar_background)
- .child(self.left_tools(view, cx))
- .child(self.right_tools(view, cx))
- }
-
- fn left_tools(
- &self,
- workspace: &mut Workspace,
- cx: &WindowContext,
- ) -> impl Component<Workspace> {
- div()
- .flex()
- .items_center()
- .gap_1()
- .child(
- IconButton::<Workspace>::new("project_panel", Icon::FileTree)
- .when(workspace.is_project_panel_open(), |this| {
- this.color(TextColor::Accent)
- })
- .on_click(|workspace: &mut Workspace, cx| {
- workspace.toggle_project_panel(cx);
- }),
- )
- .child(
- IconButton::<Workspace>::new("collab_panel", Icon::Hash)
- .when(workspace.is_collab_panel_open(), |this| {
- this.color(TextColor::Accent)
- })
- .on_click(|workspace: &mut Workspace, cx| {
- workspace.toggle_collab_panel();
- }),
- )
- .child(ToolDivider::new())
- .child(IconButton::new("diagnostics", Icon::XCircle))
- }
-
- fn right_tools(
- &self,
- workspace: &mut Workspace,
- cx: &WindowContext,
- ) -> impl Component<Workspace> {
- div()
- .flex()
- .items_center()
- .gap_2()
- .child(
- div()
- .flex()
- .items_center()
- .gap_1()
- .child(Button::new("116:25"))
- .child(
- Button::<Workspace>::new("Rust").on_click(Arc::new(|workspace, cx| {
- workspace.toggle_language_selector(cx);
- })),
- ),
- )
- .child(ToolDivider::new())
- .child(
- div()
- .flex()
- .items_center()
- .gap_1()
- .child(
- IconButton::new("copilot", Icon::Copilot)
- .on_click(|_, _| println!("Copilot clicked.")),
- )
- .child(
- IconButton::new("envelope", Icon::Envelope)
- .on_click(|_, _| println!("Send Feedback clicked.")),
- ),
- )
- .child(ToolDivider::new())
- .child(
- div()
- .flex()
- .items_center()
- .gap_1()
- .child(
- IconButton::<Workspace>::new("terminal", Icon::Terminal)
- .when(workspace.is_terminal_open(), |this| {
- this.color(TextColor::Accent)
- })
- .on_click(|workspace: &mut Workspace, cx| {
- workspace.toggle_terminal(cx);
- }),
- )
- .child(
- IconButton::<Workspace>::new("chat_panel", Icon::MessageBubbles)
- .when(workspace.is_chat_panel_open(), |this| {
- this.color(TextColor::Accent)
- })
- .on_click(|workspace: &mut Workspace, cx| {
- workspace.toggle_chat_panel(cx);
- }),
- )
- .child(
- IconButton::<Workspace>::new("assistant_panel", Icon::Ai)
- .when(workspace.is_assistant_panel_open(), |this| {
- this.color(TextColor::Accent)
- })
- .on_click(|workspace: &mut Workspace, cx| {
- workspace.toggle_assistant_panel(cx);
- }),
- ),
- )
- }
-}
@@ -1,150 +0,0 @@
-use crate::{prelude::*, Icon, IconButton, Tab};
-use gpui::prelude::*;
-
-#[derive(Component)]
-pub struct TabBar {
- id: ElementId,
- /// Backwards, Forwards
- can_navigate: (bool, bool),
- tabs: Vec<Tab>,
-}
-
-impl TabBar {
- pub fn new(id: impl Into<ElementId>, tabs: Vec<Tab>) -> Self {
- Self {
- id: id.into(),
- can_navigate: (false, false),
- tabs,
- }
- }
-
- pub fn can_navigate(mut self, can_navigate: (bool, bool)) -> Self {
- self.can_navigate = can_navigate;
- self
- }
-
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- let (can_navigate_back, can_navigate_forward) = self.can_navigate;
-
- div()
- .group("tab_bar")
- .id(self.id.clone())
- .w_full()
- .flex()
- .bg(cx.theme().colors().tab_bar_background)
- // Left Side
- .child(
- div()
- .relative()
- .px_1()
- .flex()
- .flex_none()
- .gap_2()
- // Nav Buttons
- .child(
- div()
- .right_0()
- .flex()
- .items_center()
- .gap_px()
- .child(
- IconButton::new("arrow_left", Icon::ArrowLeft)
- .state(InteractionState::Enabled.if_enabled(can_navigate_back)),
- )
- .child(
- IconButton::new("arrow_right", Icon::ArrowRight).state(
- InteractionState::Enabled.if_enabled(can_navigate_forward),
- ),
- ),
- ),
- )
- .child(
- div().w_0().flex_1().h_full().child(
- div()
- .id("tabs")
- .flex()
- .overflow_x_scroll()
- .children(self.tabs.clone()),
- ),
- )
- // Right Side
- .child(
- div()
- // We only use absolute here since we don't
- // have opacity or `hidden()` yet
- .absolute()
- .neg_top_7()
- .px_1()
- .flex()
- .flex_none()
- .gap_2()
- .group_hover("tab_bar", |this| this.top_0())
- // Nav Buttons
- .child(
- div()
- .flex()
- .items_center()
- .gap_px()
- .child(IconButton::new("plus", Icon::Plus))
- .child(IconButton::new("split", Icon::Split)),
- ),
- )
- }
-}
-
-use gpui::ElementId;
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use super::*;
- use crate::Story;
- use gpui::{Div, Render};
-
- pub struct TabBarStory;
-
- impl Render for TabBarStory {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- Story::container(cx)
- .child(Story::title_for::<_, TabBar>(cx))
- .child(Story::label(cx, "Default"))
- .child(TabBar::new(
- "tab-bar",
- vec![
- Tab::new(1)
- .title("Cargo.toml".to_string())
- .current(false)
- .git_status(GitStatus::Modified),
- Tab::new(2)
- .title("Channels Panel".to_string())
- .current(false),
- Tab::new(3)
- .title("channels_panel.rs".to_string())
- .current(true)
- .git_status(GitStatus::Modified),
- Tab::new(4)
- .title("workspace.rs".to_string())
- .current(false)
- .git_status(GitStatus::Modified),
- Tab::new(5)
- .title("icon_button.rs".to_string())
- .current(false),
- Tab::new(6)
- .title("storybook.rs".to_string())
- .current(false)
- .git_status(GitStatus::Created),
- Tab::new(7).title("theme.rs".to_string()).current(false),
- Tab::new(8)
- .title("theme_registry.rs".to_string())
- .current(false),
- Tab::new(9)
- .title("styleable_helpers.rs".to_string())
- .current(false),
- ],
- ))
- }
- }
-}
@@ -1,99 +0,0 @@
-use gpui::{relative, rems, Size};
-
-use crate::prelude::*;
-use crate::{Icon, IconButton, Pane, Tab};
-
-#[derive(Component)]
-pub struct Terminal;
-
-impl Terminal {
- pub fn new() -> Self {
- Self
- }
-
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- let can_navigate_back = true;
- let can_navigate_forward = false;
-
- div()
- .flex()
- .flex_col()
- .w_full()
- .child(
- // Terminal Tabs.
- div()
- .w_full()
- .flex()
- .bg(cx.theme().colors().surface_background)
- .child(
- div().px_1().flex().flex_none().gap_2().child(
- div()
- .flex()
- .items_center()
- .gap_px()
- .child(
- IconButton::new("arrow_left", Icon::ArrowLeft).state(
- InteractionState::Enabled.if_enabled(can_navigate_back),
- ),
- )
- .child(IconButton::new("arrow_right", Icon::ArrowRight).state(
- InteractionState::Enabled.if_enabled(can_navigate_forward),
- )),
- ),
- )
- .child(
- div().w_0().flex_1().h_full().child(
- div()
- .flex()
- .child(
- Tab::new(1)
- .title("zed — fish".to_string())
- .icon(Icon::Terminal)
- .close_side(IconSide::Right)
- .current(true),
- )
- .child(
- Tab::new(2)
- .title("zed — fish".to_string())
- .icon(Icon::Terminal)
- .close_side(IconSide::Right)
- .current(false),
- ),
- ),
- ),
- )
- // Terminal Pane.
- .child(
- Pane::new(
- "terminal",
- Size {
- width: relative(1.).into(),
- height: rems(36.).into(),
- },
- )
- .child(crate::static_data::terminal_buffer(cx)),
- )
- }
-}
-
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use super::*;
- use crate::Story;
- use gpui::{Div, Render};
- pub struct TerminalStory;
-
- impl Render for TerminalStory {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- Story::container(cx)
- .child(Story::title_for::<_, Terminal>(cx))
- .child(Story::label(cx, "Default"))
- .child(Terminal::new())
- }
- }
-}
@@ -1,60 +0,0 @@
-use crate::prelude::*;
-use crate::{OrderMethod, Palette, PaletteItem};
-
-#[derive(Component)]
-pub struct ThemeSelector {
- id: ElementId,
-}
-
-impl ThemeSelector {
- pub fn new(id: impl Into<ElementId>) -> Self {
- Self { id: id.into() }
- }
-
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- div().child(
- Palette::new(self.id.clone())
- .items(vec![
- PaletteItem::new("One Dark"),
- PaletteItem::new("Rosé Pine"),
- PaletteItem::new("Rosé Pine Moon"),
- PaletteItem::new("Sandcastle"),
- PaletteItem::new("Solarized Dark"),
- PaletteItem::new("Summercamp"),
- PaletteItem::new("Atelier Cave Light"),
- PaletteItem::new("Atelier Dune Light"),
- PaletteItem::new("Atelier Estuary Light"),
- PaletteItem::new("Atelier Forest Light"),
- PaletteItem::new("Atelier Heath Light"),
- ])
- .placeholder("Select Theme...")
- .empty_string("No matches")
- .default_order(OrderMethod::Ascending),
- )
- }
-}
-
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use gpui::{Div, Render};
-
- use crate::Story;
-
- use super::*;
-
- pub struct ThemeSelectorStory;
-
- impl Render for ThemeSelectorStory {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- Story::container(cx)
- .child(Story::title_for::<_, ThemeSelector>(cx))
- .child(Story::label(cx, "Default"))
- .child(ThemeSelector::new("theme-selector"))
- }
- }
-}
@@ -1,218 +0,0 @@
-use std::sync::atomic::AtomicBool;
-use std::sync::Arc;
-
-use gpui::{Div, Render, View, VisualContext};
-
-use crate::prelude::*;
-use crate::settings::user_settings;
-use crate::{
- Avatar, Button, Icon, IconButton, MicStatus, PlayerStack, PlayerWithCallStatus,
- ScreenShareStatus, TextColor, ToolDivider, TrafficLights,
-};
-
-#[derive(Clone)]
-pub struct Livestream {
- pub players: Vec<PlayerWithCallStatus>,
- pub channel: Option<String>, // projects
- // windows
-}
-
-#[derive(Clone)]
-pub struct TitleBar {
- /// If the window is active from the OS's perspective.
- is_active: Arc<AtomicBool>,
- livestream: Option<Livestream>,
- mic_status: MicStatus,
- is_deafened: bool,
- screen_share_status: ScreenShareStatus,
-}
-
-impl TitleBar {
- pub fn new(cx: &mut ViewContext<Self>) -> Self {
- let is_active = Arc::new(AtomicBool::new(true));
- let active = is_active.clone();
-
- // cx.observe_window_activation(move |_, is_active, cx| {
- // active.store(is_active, std::sync::atomic::Ordering::SeqCst);
- // cx.notify();
- // })
- // .detach();
-
- Self {
- is_active,
- livestream: None,
- mic_status: MicStatus::Unmuted,
- is_deafened: false,
- screen_share_status: ScreenShareStatus::NotShared,
- }
- }
-
- pub fn set_livestream(mut self, livestream: Option<Livestream>) -> Self {
- self.livestream = livestream;
- self
- }
-
- pub fn is_mic_muted(&self) -> bool {
- self.mic_status == MicStatus::Muted
- }
-
- pub fn toggle_mic_status(&mut self, cx: &mut ViewContext<Self>) {
- self.mic_status = self.mic_status.inverse();
-
- // Undeafen yourself when unmuting the mic while deafened.
- if self.is_deafened && self.mic_status == MicStatus::Unmuted {
- self.is_deafened = false;
- }
-
- cx.notify();
- }
-
- pub fn toggle_deafened(&mut self, cx: &mut ViewContext<Self>) {
- self.is_deafened = !self.is_deafened;
- self.mic_status = MicStatus::Muted;
-
- cx.notify()
- }
-
- pub fn toggle_screen_share_status(&mut self, cx: &mut ViewContext<Self>) {
- self.screen_share_status = self.screen_share_status.inverse();
-
- cx.notify();
- }
-
- pub fn view(cx: &mut WindowContext, livestream: Option<Livestream>) -> View<Self> {
- cx.build_view(|cx| Self::new(cx).set_livestream(livestream))
- }
-}
-
-impl Render for TitleBar {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Div<Self> {
- let settings = user_settings(cx);
-
- // let has_focus = cx.window_is_active();
- let has_focus = true;
-
- let player_list = if let Some(livestream) = &self.livestream {
- livestream.players.clone().into_iter()
- } else {
- vec![].into_iter()
- };
-
- div()
- .flex()
- .items_center()
- .justify_between()
- .w_full()
- .bg(cx.theme().colors().background)
- .py_1()
- .child(
- div()
- .flex()
- .items_center()
- .h_full()
- .gap_4()
- .px_2()
- .child(TrafficLights::new().window_has_focus(has_focus))
- // === Project Info === //
- .child(
- div()
- .flex()
- .items_center()
- .gap_1()
- .when(*settings.titlebar.show_project_owner, |this| {
- this.child(Button::new("iamnbutler"))
- })
- .child(Button::new("zed"))
- .child(Button::new("nate/gpui2-ui-components")),
- )
- .children(player_list.map(|p| PlayerStack::new(p)))
- .child(IconButton::new("plus", Icon::Plus)),
- )
- .child(
- div()
- .flex()
- .items_center()
- .child(
- div()
- .px_2()
- .flex()
- .items_center()
- .gap_1()
- .child(IconButton::new("folder_x", Icon::FolderX))
- .child(IconButton::new("exit", Icon::Exit)),
- )
- .child(ToolDivider::new())
- .child(
- div()
- .px_2()
- .flex()
- .items_center()
- .gap_1()
- .child(
- IconButton::<TitleBar>::new("toggle_mic_status", Icon::Mic)
- .when(self.is_mic_muted(), |this| this.color(TextColor::Error))
- .on_click(|title_bar: &mut TitleBar, cx| {
- title_bar.toggle_mic_status(cx)
- }),
- )
- .child(
- IconButton::<TitleBar>::new("toggle_deafened", Icon::AudioOn)
- .when(self.is_deafened, |this| this.color(TextColor::Error))
- .on_click(|title_bar: &mut TitleBar, cx| {
- title_bar.toggle_deafened(cx)
- }),
- )
- .child(
- IconButton::<TitleBar>::new("toggle_screen_share", Icon::Screen)
- .when(
- self.screen_share_status == ScreenShareStatus::Shared,
- |this| this.color(TextColor::Accent),
- )
- .on_click(|title_bar: &mut TitleBar, cx| {
- title_bar.toggle_screen_share_status(cx)
- }),
- ),
- )
- .child(
- div().px_2().flex().items_center().child(
- Avatar::new("https://avatars.githubusercontent.com/u/1714999?v=4")
- .shape(Shape::RoundedRectangle),
- ),
- ),
- )
- }
-}
-
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use super::*;
- use crate::Story;
-
- pub struct TitleBarStory {
- title_bar: View<TitleBar>,
- }
-
- impl TitleBarStory {
- pub fn view(cx: &mut WindowContext) -> View<Self> {
- cx.build_view(|cx| Self {
- title_bar: TitleBar::view(cx, None),
- })
- }
- }
-
- impl Render for TitleBarStory {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Div<Self> {
- Story::container(cx)
- .child(Story::title_for::<_, TitleBar>(cx))
- .child(Story::label(cx, "Default"))
- .child(self.title_bar.clone())
- }
- }
-}
@@ -1,126 +0,0 @@
-use gpui::AnyElement;
-use smallvec::SmallVec;
-
-use crate::prelude::*;
-
-#[derive(Clone)]
-pub struct ToolbarItem {}
-
-#[derive(Component)]
-pub struct Toolbar<V: 'static> {
- left_items: SmallVec<[AnyElement<V>; 2]>,
- right_items: SmallVec<[AnyElement<V>; 2]>,
-}
-
-impl<V: 'static> Toolbar<V> {
- pub fn new() -> Self {
- Self {
- left_items: SmallVec::new(),
- right_items: SmallVec::new(),
- }
- }
-
- pub fn left_item(mut self, child: impl Component<V>) -> Self
- where
- Self: Sized,
- {
- self.left_items.push(child.render());
- self
- }
-
- pub fn left_items(mut self, iter: impl IntoIterator<Item = impl Component<V>>) -> Self
- where
- Self: Sized,
- {
- self.left_items
- .extend(iter.into_iter().map(|item| item.render()));
- self
- }
-
- pub fn right_item(mut self, child: impl Component<V>) -> Self
- where
- Self: Sized,
- {
- self.right_items.push(child.render());
- self
- }
-
- pub fn right_items(mut self, iter: impl IntoIterator<Item = impl Component<V>>) -> Self
- where
- Self: Sized,
- {
- self.right_items
- .extend(iter.into_iter().map(|item| item.render()));
- self
- }
-
- fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- div()
- .bg(cx.theme().colors().toolbar_background)
- .p_2()
- .flex()
- .justify_between()
- .child(div().flex().children(self.left_items))
- .child(div().flex().children(self.right_items))
- }
-}
-
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use std::path::PathBuf;
- use std::str::FromStr;
-
- use gpui::{Div, Render};
-
- use crate::{Breadcrumb, HighlightedText, Icon, IconButton, Story, Symbol};
-
- use super::*;
-
- pub struct ToolbarStory;
-
- impl Render for ToolbarStory {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- Story::container(cx)
- .child(Story::title_for::<_, Toolbar<Self>>(cx))
- .child(Story::label(cx, "Default"))
- .child(
- Toolbar::new()
- .left_item(Breadcrumb::new(
- PathBuf::from_str("crates/ui/src/components/toolbar.rs").unwrap(),
- vec![
- Symbol(vec![
- HighlightedText {
- text: "impl ".to_string(),
- color: cx.theme().syntax_color("keyword"),
- },
- HighlightedText {
- text: "ToolbarStory".to_string(),
- color: cx.theme().syntax_color("function"),
- },
- ]),
- Symbol(vec![
- HighlightedText {
- text: "fn ".to_string(),
- color: cx.theme().syntax_color("keyword"),
- },
- HighlightedText {
- text: "render".to_string(),
- color: cx.theme().syntax_color("function"),
- },
- ]),
- ],
- ))
- .right_items(vec![
- IconButton::new("toggle_inlay_hints", Icon::InlayHint),
- IconButton::new("buffer_search", Icon::MagnifyingGlass),
- IconButton::new("inline_assist", Icon::MagicWand),
- ]),
- )
- }
- }
-}
@@ -1,100 +0,0 @@
-use crate::prelude::*;
-
-#[derive(Clone, Copy)]
-enum TrafficLightColor {
- Red,
- Yellow,
- Green,
-}
-
-#[derive(Component)]
-struct TrafficLight {
- color: TrafficLightColor,
- window_has_focus: bool,
-}
-
-impl TrafficLight {
- fn new(color: TrafficLightColor, window_has_focus: bool) -> Self {
- Self {
- color,
- window_has_focus,
- }
- }
-
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- let system_colors = &cx.theme().styles.system;
-
- let fill = match (self.window_has_focus, self.color) {
- (true, TrafficLightColor::Red) => system_colors.mac_os_traffic_light_red,
- (true, TrafficLightColor::Yellow) => system_colors.mac_os_traffic_light_yellow,
- (true, TrafficLightColor::Green) => system_colors.mac_os_traffic_light_green,
- (false, _) => cx.theme().colors().element_background,
- };
-
- div().w_3().h_3().rounded_full().bg(fill)
- }
-}
-
-#[derive(Component)]
-pub struct TrafficLights {
- window_has_focus: bool,
-}
-
-impl TrafficLights {
- pub fn new() -> Self {
- Self {
- window_has_focus: true,
- }
- }
-
- pub fn window_has_focus(mut self, window_has_focus: bool) -> Self {
- self.window_has_focus = window_has_focus;
- self
- }
-
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- div()
- .flex()
- .items_center()
- .gap_2()
- .child(TrafficLight::new(
- TrafficLightColor::Red,
- self.window_has_focus,
- ))
- .child(TrafficLight::new(
- TrafficLightColor::Yellow,
- self.window_has_focus,
- ))
- .child(TrafficLight::new(
- TrafficLightColor::Green,
- self.window_has_focus,
- ))
- }
-}
-
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use gpui::{Div, Render};
-
- use crate::Story;
-
- use super::*;
-
- pub struct TrafficLightsStory;
-
- impl Render for TrafficLightsStory {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- Story::container(cx)
- .child(Story::title_for::<_, TrafficLights>(cx))
- .child(Story::label(cx, "Default"))
- .child(TrafficLights::new())
- .child(Story::label(cx, "Unfocused"))
- .child(TrafficLights::new().window_has_focus(false))
- }
- }
-}
@@ -1,398 +0,0 @@
-use std::sync::Arc;
-
-use chrono::DateTime;
-use gpui::{px, relative, Div, Render, Size, View, VisualContext};
-use settings2::Settings;
-use theme2::ThemeSettings;
-
-use crate::prelude::*;
-use crate::{
- static_livestream, v_stack, AssistantPanel, Button, ChatMessage, ChatPanel, Checkbox,
- CollabPanel, EditorPane, Label, LanguageSelector, NotificationsPanel, Pane, PaneGroup, Panel,
- PanelAllowedSides, PanelSide, ProjectPanel, SplitDirection, StatusBar, Terminal, TitleBar,
- Toast, ToastOrigin,
-};
-
-#[derive(Clone)]
-pub struct Gpui2UiDebug {
- pub in_livestream: bool,
- pub enable_user_settings: bool,
- pub show_toast: bool,
-}
-
-impl Default for Gpui2UiDebug {
- fn default() -> Self {
- Self {
- in_livestream: false,
- enable_user_settings: false,
- show_toast: false,
- }
- }
-}
-
-#[derive(Clone)]
-pub struct Workspace {
- title_bar: View<TitleBar>,
- editor_1: View<EditorPane>,
- show_project_panel: bool,
- show_collab_panel: bool,
- show_chat_panel: bool,
- show_assistant_panel: bool,
- show_notifications_panel: bool,
- show_terminal: bool,
- show_debug: bool,
- show_language_selector: bool,
- test_checkbox_selection: Selection,
- debug: Gpui2UiDebug,
-}
-
-impl Workspace {
- pub fn new(cx: &mut ViewContext<Self>) -> Self {
- Self {
- title_bar: TitleBar::view(cx, None),
- editor_1: EditorPane::view(cx),
- show_project_panel: true,
- show_collab_panel: false,
- show_chat_panel: false,
- show_assistant_panel: false,
- show_terminal: true,
- show_language_selector: false,
- show_debug: false,
- show_notifications_panel: true,
- test_checkbox_selection: Selection::Unselected,
- debug: Gpui2UiDebug::default(),
- }
- }
-
- pub fn is_project_panel_open(&self) -> bool {
- self.show_project_panel
- }
-
- pub fn toggle_project_panel(&mut self, cx: &mut ViewContext<Self>) {
- self.show_project_panel = !self.show_project_panel;
-
- self.show_collab_panel = false;
-
- cx.notify();
- }
-
- pub fn is_collab_panel_open(&self) -> bool {
- self.show_collab_panel
- }
-
- pub fn toggle_collab_panel(&mut self) {
- self.show_collab_panel = !self.show_collab_panel;
-
- self.show_project_panel = false;
- }
-
- pub fn is_terminal_open(&self) -> bool {
- self.show_terminal
- }
-
- pub fn toggle_terminal(&mut self, cx: &mut ViewContext<Self>) {
- self.show_terminal = !self.show_terminal;
-
- cx.notify();
- }
-
- pub fn is_chat_panel_open(&self) -> bool {
- self.show_chat_panel
- }
-
- pub fn toggle_chat_panel(&mut self, cx: &mut ViewContext<Self>) {
- self.show_chat_panel = !self.show_chat_panel;
-
- self.show_assistant_panel = false;
- self.show_notifications_panel = false;
-
- cx.notify();
- }
-
- pub fn is_notifications_panel_open(&self) -> bool {
- self.show_notifications_panel
- }
-
- pub fn toggle_notifications_panel(&mut self, cx: &mut ViewContext<Self>) {
- self.show_notifications_panel = !self.show_notifications_panel;
-
- self.show_chat_panel = false;
- self.show_assistant_panel = false;
-
- cx.notify();
- }
-
- pub fn is_assistant_panel_open(&self) -> bool {
- self.show_assistant_panel
- }
-
- pub fn toggle_assistant_panel(&mut self, cx: &mut ViewContext<Self>) {
- self.show_assistant_panel = !self.show_assistant_panel;
-
- self.show_chat_panel = false;
- self.show_notifications_panel = false;
-
- cx.notify();
- }
-
- pub fn is_language_selector_open(&self) -> bool {
- self.show_language_selector
- }
-
- pub fn toggle_language_selector(&mut self, cx: &mut ViewContext<Self>) {
- self.show_language_selector = !self.show_language_selector;
-
- cx.notify();
- }
-
- pub fn toggle_debug(&mut self, cx: &mut ViewContext<Self>) {
- self.show_debug = !self.show_debug;
-
- cx.notify();
- }
-
- pub fn debug_toggle_user_settings(&mut self, cx: &mut ViewContext<Self>) {
- self.debug.enable_user_settings = !self.debug.enable_user_settings;
-
- let mut theme_settings = ThemeSettings::get_global(cx).clone();
-
- if self.debug.enable_user_settings {
- theme_settings.ui_font_size = 18.0.into();
- } else {
- theme_settings.ui_font_size = 16.0.into();
- }
-
- ThemeSettings::override_global(theme_settings.clone(), cx);
-
- cx.set_rem_size(theme_settings.ui_font_size);
-
- cx.notify();
- }
-
- pub fn debug_toggle_livestream(&mut self, cx: &mut ViewContext<Self>) {
- self.debug.in_livestream = !self.debug.in_livestream;
-
- self.title_bar = TitleBar::view(
- cx,
- Some(static_livestream()).filter(|_| self.debug.in_livestream),
- );
-
- cx.notify();
- }
-
- pub fn debug_toggle_toast(&mut self, cx: &mut ViewContext<Self>) {
- self.debug.show_toast = !self.debug.show_toast;
-
- cx.notify();
- }
-
- pub fn view(cx: &mut WindowContext) -> View<Self> {
- cx.build_view(|cx| Self::new(cx))
- }
-}
-
-impl Render for Workspace {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Div<Self> {
- let root_group = PaneGroup::new_panes(
- vec![Pane::new(
- "pane-0",
- Size {
- width: relative(1.).into(),
- height: relative(1.).into(),
- },
- )
- .child(self.editor_1.clone())],
- SplitDirection::Horizontal,
- );
- let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
-
- div()
- .relative()
- .size_full()
- .flex()
- .flex_col()
- .font(ui_font)
- .gap_0()
- .justify_start()
- .items_start()
- .text_color(cx.theme().colors().text)
- .bg(cx.theme().colors().background)
- .child(self.title_bar.clone())
- .child(
- div()
- .absolute()
- .top_12()
- .left_12()
- .z_index(99)
- .bg(cx.theme().colors().background)
- .child(
- Checkbox::new("test_checkbox", self.test_checkbox_selection).on_click(
- |selection, workspace: &mut Workspace, cx| {
- workspace.test_checkbox_selection = selection;
-
- cx.notify();
- },
- ),
- ),
- )
- .child(
- div()
- .flex_1()
- .w_full()
- .flex()
- .flex_row()
- .overflow_hidden()
- .border_t()
- .border_b()
- .border_color(cx.theme().colors().border)
- .children(
- Some(
- Panel::new("project-panel-outer", cx)
- .side(PanelSide::Left)
- .child(ProjectPanel::new("project-panel-inner")),
- )
- .filter(|_| self.is_project_panel_open()),
- )
- .children(
- Some(
- Panel::new("collab-panel-outer", cx)
- .child(CollabPanel::new("collab-panel-inner"))
- .side(PanelSide::Left),
- )
- .filter(|_| self.is_collab_panel_open()),
- )
- // .child(NotificationToast::new(
- // "maxbrunsfeld has requested to add you as a contact.".into(),
- // ))
- .child(
- v_stack()
- .flex_1()
- .h_full()
- .child(div().flex().flex_1().child(root_group))
- .children(
- Some(
- Panel::new("terminal-panel", cx)
- .child(Terminal::new())
- .allowed_sides(PanelAllowedSides::BottomOnly)
- .side(PanelSide::Bottom),
- )
- .filter(|_| self.is_terminal_open()),
- ),
- )
- .children(
- Some(
- Panel::new("chat-panel-outer", cx)
- .side(PanelSide::Right)
- .child(ChatPanel::new("chat-panel-inner").messages(vec![
- ChatMessage::new(
- "osiewicz".to_string(),
- "is this thing on?".to_string(),
- DateTime::parse_from_rfc3339("2023-09-27T15:40:52.707Z")
- .unwrap()
- .naive_local(),
- ),
- ChatMessage::new(
- "maxdeviant".to_string(),
- "Reading you loud and clear!".to_string(),
- DateTime::parse_from_rfc3339("2023-09-28T15:40:52.707Z")
- .unwrap()
- .naive_local(),
- ),
- ])),
- )
- .filter(|_| self.is_chat_panel_open()),
- )
- .children(
- Some(
- Panel::new("notifications-panel-outer", cx)
- .side(PanelSide::Right)
- .child(NotificationsPanel::new("notifications-panel-inner")),
- )
- .filter(|_| self.is_notifications_panel_open()),
- )
- .children(
- Some(
- Panel::new("assistant-panel-outer", cx)
- .child(AssistantPanel::new("assistant-panel-inner")),
- )
- .filter(|_| self.is_assistant_panel_open()),
- ),
- )
- .child(StatusBar::new())
- .when(self.debug.show_toast, |this| {
- this.child(Toast::new(ToastOrigin::Bottom).child(Label::new("A toast")))
- })
- .children(
- Some(
- div()
- .absolute()
- .top(px(50.))
- .left(px(640.))
- .z_index(8)
- .child(LanguageSelector::new("language-selector")),
- )
- .filter(|_| self.is_language_selector_open()),
- )
- .z_index(8)
- // Debug
- .child(
- v_stack()
- .z_index(9)
- .absolute()
- .top_20()
- .left_1_4()
- .w_40()
- .gap_2()
- .when(self.show_debug, |this| {
- this.child(Button::<Workspace>::new("Toggle User Settings").on_click(
- Arc::new(|workspace, cx| workspace.debug_toggle_user_settings(cx)),
- ))
- .child(
- Button::<Workspace>::new("Toggle Toasts").on_click(Arc::new(
- |workspace, cx| workspace.debug_toggle_toast(cx),
- )),
- )
- .child(
- Button::<Workspace>::new("Toggle Livestream").on_click(Arc::new(
- |workspace, cx| workspace.debug_toggle_livestream(cx),
- )),
- )
- })
- .child(
- Button::<Workspace>::new("Toggle Debug")
- .on_click(Arc::new(|workspace, cx| workspace.toggle_debug(cx))),
- ),
- )
- }
-}
-
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use super::*;
- use gpui::VisualContext;
-
- pub struct WorkspaceStory {
- workspace: View<Workspace>,
- }
-
- impl WorkspaceStory {
- pub fn view(cx: &mut WindowContext) -> View<Self> {
- cx.build_view(|cx| Self {
- workspace: Workspace::view(cx),
- })
- }
- }
-
- impl Render for WorkspaceStory {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- div().child(self.workspace.clone())
- }
- }
-}
@@ -15,28 +15,15 @@
#![allow(dead_code, unused_variables)]
mod components;
-mod elevation;
pub mod prelude;
-pub mod settings;
-mod static_data;
mod styled_ext;
-mod to_extract;
+mod styles;
pub mod utils;
pub use components::*;
pub use prelude::*;
-pub use static_data::*;
pub use styled_ext::*;
-pub use to_extract::*;
-
-// This needs to be fully qualified with `crate::` otherwise we get a panic
-// at:
-// thread '<unnamed>' panicked at crates/gpui2/src/platform/mac/platform.rs:66:81:
-// called `Option::unwrap()` on a `None` value
-//
-// AFAICT this is something to do with conflicting names between crates and modules that
-// interfaces with declaring the `ClassDecl`.
-pub use crate::settings::*;
+pub use styles::*;
#[cfg(feature = "stories")]
mod story;
@@ -1,6 +1,5 @@
-use std::env;
-
use lazy_static::lazy_static;
+use std::env;
lazy_static! {
pub static ref RELEASE_CHANNEL_NAME: String = if cfg!(debug_assertions) {
@@ -9,18 +8,22 @@ lazy_static! {
} else {
include_str!("../../zed/RELEASE_CHANNEL").to_string()
};
- pub static ref RELEASE_CHANNEL: ReleaseChannel = match RELEASE_CHANNEL_NAME.as_str() {
+ pub static ref RELEASE_CHANNEL: ReleaseChannel = match RELEASE_CHANNEL_NAME.as_str().trim() {
"dev" => ReleaseChannel::Dev,
+ "nightly" => ReleaseChannel::Nightly,
"preview" => ReleaseChannel::Preview,
"stable" => ReleaseChannel::Stable,
_ => panic!("invalid release channel {}", *RELEASE_CHANNEL_NAME),
};
}
+pub struct AppCommitSha(pub String);
+
#[derive(Copy, Clone, PartialEq, Eq, Default)]
pub enum ReleaseChannel {
#[default]
Dev,
+ Nightly,
Preview,
Stable,
}
@@ -29,6 +32,7 @@ impl ReleaseChannel {
pub fn display_name(&self) -> &'static str {
match self {
ReleaseChannel::Dev => "Zed Dev",
+ ReleaseChannel::Nightly => "Zed Nightly",
ReleaseChannel::Preview => "Zed Preview",
ReleaseChannel::Stable => "Zed",
}
@@ -37,6 +41,7 @@ impl ReleaseChannel {
pub fn dev_name(&self) -> &'static str {
match self {
ReleaseChannel::Dev => "dev",
+ ReleaseChannel::Nightly => "nightly",
ReleaseChannel::Preview => "preview",
ReleaseChannel::Stable => "stable",
}
@@ -45,6 +50,7 @@ impl ReleaseChannel {
pub fn url_scheme(&self) -> &'static str {
match self {
ReleaseChannel::Dev => "zed-dev://",
+ ReleaseChannel::Nightly => "zed-nightly://",
ReleaseChannel::Preview => "zed-preview://",
ReleaseChannel::Stable => "zed://",
}
@@ -53,15 +59,27 @@ impl ReleaseChannel {
pub fn link_prefix(&self) -> &'static str {
match self {
ReleaseChannel::Dev => "https://zed.dev/dev/",
+ // TODO kb need to add server handling
+ ReleaseChannel::Nightly => "https://zed.dev/nightly/",
ReleaseChannel::Preview => "https://zed.dev/preview/",
ReleaseChannel::Stable => "https://zed.dev/",
}
}
+
+ pub fn release_query_param(&self) -> Option<&'static str> {
+ match self {
+ Self::Dev => None,
+ Self::Nightly => Some("nightly=1"),
+ Self::Preview => Some("preview=1"),
+ Self::Stable => None,
+ }
+ }
}
pub fn parse_zed_link(link: &str) -> Option<&str> {
for release in [
ReleaseChannel::Dev,
+ ReleaseChannel::Nightly,
ReleaseChannel::Preview,
ReleaseChannel::Stable,
] {
@@ -202,6 +202,14 @@ impl std::fmt::Display for PathMatcher {
}
}
+impl PartialEq for PathMatcher {
+ fn eq(&self, other: &Self) -> bool {
+ self.maybe_path.eq(&other.maybe_path)
+ }
+}
+
+impl Eq for PathMatcher {}
+
impl PathMatcher {
pub fn new(maybe_glob: &str) -> Result<Self, globset::Error> {
Ok(PathMatcher {
@@ -211,7 +219,19 @@ impl PathMatcher {
}
pub fn is_match<P: AsRef<Path>>(&self, other: P) -> bool {
- other.as_ref().starts_with(&self.maybe_path) || self.glob.is_match(other)
+ other.as_ref().starts_with(&self.maybe_path)
+ || self.glob.is_match(&other)
+ || self.check_with_end_separator(other.as_ref())
+ }
+
+ fn check_with_end_separator(&self, path: &Path) -> bool {
+ let path_str = path.to_string_lossy();
+ let separator = std::path::MAIN_SEPARATOR_STR;
+ if path_str.ends_with(separator) {
+ self.glob.is_match(path)
+ } else {
+ self.glob.is_match(path_str.to_string() + separator)
+ }
}
}
@@ -388,4 +408,14 @@ mod tests {
let path = Path::new("/a/b/c/.eslintrc.js");
assert_eq!(path.extension_or_hidden_file_name(), Some("js"));
}
+
+ #[test]
+ fn edge_of_glob() {
+ let path = Path::new("/work/node_modules");
+ let path_matcher = PathMatcher::new("**/node_modules/**").unwrap();
+ assert!(
+ path_matcher.is_match(&path),
+ "Path matcher {path_matcher} should match {path:?}"
+ );
+ }
}
@@ -1,14 +1,16 @@
use crate::{status_bar::StatusItemView, Axis, Workspace};
use gpui::{
- div, px, Action, AnchorCorner, AnyView, AppContext, Component, Div, Entity, EntityId,
- EventEmitter, FocusHandle, FocusableView, ParentComponent, Render, SharedString, Styled,
+ div, px, Action, AnchorCorner, AnyView, AppContext, Div, Entity, EntityId, EventEmitter,
+ FocusHandle, FocusableView, ParentElement, Render, RenderOnce, SharedString, Styled,
Subscription, View, ViewContext, VisualContext, WeakView, WindowContext,
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use theme2::ActiveTheme;
-use ui::{h_stack, menu_handle, ContextMenu, IconButton, InteractionState, Tooltip};
+use ui::{
+ h_stack, menu_handle, ContextMenu, IconButton, InteractionState, Label, ListItem, Tooltip,
+};
pub enum PanelEvent {
ChangePosition,
@@ -40,7 +42,7 @@ pub trait Panel: FocusableView + EventEmitter<PanelEvent> {
}
pub trait PanelHandle: Send + Sync {
- fn id(&self) -> EntityId;
+ fn entity_id(&self) -> EntityId;
fn persistent_name(&self) -> &'static str;
fn position(&self, cx: &WindowContext) -> DockPosition;
fn position_is_valid(&self, position: DockPosition, cx: &WindowContext) -> bool;
@@ -62,8 +64,8 @@ impl<T> PanelHandle for View<T>
where
T: Panel,
{
- fn id(&self) -> EntityId {
- self.entity_id()
+ fn entity_id(&self) -> EntityId {
+ Entity::entity_id(self)
}
fn persistent_name(&self) -> &'static str {
@@ -254,20 +256,19 @@ impl Dock {
}
}
- // todo!()
- // pub fn set_panel_zoomed(&mut self, panel: &AnyView, zoomed: bool, cx: &mut ViewContext<Self>) {
- // for entry in &mut self.panel_entries {
- // if entry.panel.as_any() == panel {
- // if zoomed != entry.panel.is_zoomed(cx) {
- // entry.panel.set_zoomed(zoomed, cx);
- // }
- // } else if entry.panel.is_zoomed(cx) {
- // entry.panel.set_zoomed(false, cx);
- // }
- // }
+ pub fn set_panel_zoomed(&mut self, panel: &AnyView, zoomed: bool, cx: &mut ViewContext<Self>) {
+ for entry in &mut self.panel_entries {
+ if entry.panel.entity_id() == panel.entity_id() {
+ if zoomed != entry.panel.is_zoomed(cx) {
+ entry.panel.set_zoomed(zoomed, cx);
+ }
+ } else if entry.panel.is_zoomed(cx) {
+ entry.panel.set_zoomed(false, cx);
+ }
+ }
- // cx.notify();
- // }
+ cx.notify();
+ }
pub fn zoom_out(&mut self, cx: &mut ViewContext<Self>) {
for entry in &mut self.panel_entries {
@@ -277,42 +278,91 @@ impl Dock {
}
}
- pub(crate) fn add_panel<T: Panel>(&mut self, panel: View<T>, cx: &mut ViewContext<Self>) {
+ pub(crate) fn add_panel<T: Panel>(
+ &mut self,
+ panel: View<T>,
+ workspace: WeakView<Workspace>,
+ cx: &mut ViewContext<Self>,
+ ) {
let subscriptions = [
cx.observe(&panel, |_, _, cx| cx.notify()),
- cx.subscribe(&panel, |this, panel, event, cx| {
- match event {
- PanelEvent::ChangePosition => {
- //todo!()
- // see: Workspace::add_panel_with_extra_event_handler
- }
- PanelEvent::ZoomIn => {
- //todo!()
- // see: Workspace::add_panel_with_extra_event_handler
- }
- PanelEvent::ZoomOut => {
- // todo!()
- // // see: Workspace::add_panel_with_extra_event_handler
- }
- PanelEvent::Activate => {
- if let Some(ix) = this
- .panel_entries
- .iter()
- .position(|entry| entry.panel.id() == panel.id())
- {
- this.set_open(true, cx);
- this.activate_panel(ix, cx);
- //` todo!()
- // cx.focus(&panel);
+ cx.subscribe(&panel, move |this, panel, event, cx| match event {
+ PanelEvent::ChangePosition => {
+ let new_position = panel.read(cx).position(cx);
+
+ let Ok(new_dock) = workspace.update(cx, |workspace, cx| {
+ if panel.is_zoomed(cx) {
+ workspace.zoomed_position = Some(new_position);
}
- }
- PanelEvent::Close => {
- if this.visible_panel().map_or(false, |p| p.id() == panel.id()) {
- this.set_open(false, cx);
+ match new_position {
+ DockPosition::Left => &workspace.left_dock,
+ DockPosition::Bottom => &workspace.bottom_dock,
+ DockPosition::Right => &workspace.right_dock,
+ }
+ .clone()
+ }) else {
+ return;
+ };
+
+ let was_visible = this.is_open()
+ && this.visible_panel().map_or(false, |active_panel| {
+ active_panel.entity_id() == Entity::entity_id(&panel)
+ });
+
+ this.remove_panel(&panel, cx);
+
+ new_dock.update(cx, |new_dock, cx| {
+ new_dock.add_panel(panel.clone(), workspace.clone(), cx);
+ if was_visible {
+ new_dock.set_open(true, cx);
+ new_dock.activate_panel(this.panels_len() - 1, cx);
}
+ });
+ }
+ PanelEvent::ZoomIn => {
+ this.set_panel_zoomed(&panel.to_any(), true, cx);
+ if !panel.has_focus(cx) {
+ cx.focus_view(&panel);
+ }
+ workspace
+ .update(cx, |workspace, cx| {
+ workspace.zoomed = Some(panel.downgrade().into());
+ workspace.zoomed_position = Some(panel.read(cx).position(cx));
+ })
+ .ok();
+ }
+ PanelEvent::ZoomOut => {
+ this.set_panel_zoomed(&panel.to_any(), false, cx);
+ workspace
+ .update(cx, |workspace, cx| {
+ if workspace.zoomed_position == Some(this.position) {
+ workspace.zoomed = None;
+ workspace.zoomed_position = None;
+ }
+ cx.notify();
+ })
+ .ok();
+ }
+ PanelEvent::Activate => {
+ if let Some(ix) = this
+ .panel_entries
+ .iter()
+ .position(|entry| entry.panel.entity_id() == Entity::entity_id(&panel))
+ {
+ this.set_open(true, cx);
+ this.activate_panel(ix, cx);
+ cx.focus_view(&panel);
+ }
+ }
+ PanelEvent::Close => {
+ if this
+ .visible_panel()
+ .map_or(false, |p| p.entity_id() == Entity::entity_id(&panel))
+ {
+ this.set_open(false, cx);
}
- PanelEvent::Focus => todo!(),
}
+ PanelEvent::Focus => todo!(),
}),
];
@@ -335,7 +385,7 @@ impl Dock {
if let Some(panel_ix) = self
.panel_entries
.iter()
- .position(|entry| entry.panel.id() == panel.id())
+ .position(|entry| entry.panel.entity_id() == Entity::entity_id(panel))
{
if panel_ix == self.active_panel_index {
self.active_panel_index = 0;
@@ -396,7 +446,7 @@ impl Dock {
pub fn panel_size(&self, panel: &dyn PanelHandle, cx: &WindowContext) -> Option<f32> {
self.panel_entries
.iter()
- .find(|entry| entry.panel.id() == panel.id())
+ .find(|entry| entry.panel.entity_id() == panel.entity_id())
.map(|entry| entry.panel.size(cx))
}
@@ -427,7 +477,7 @@ impl Dock {
}
impl Render for Dock {
- type Element = Div<Self>;
+ type Element = Div;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
if let Some(entry) = self.visible_entry() {
@@ -613,13 +663,14 @@ impl PanelButtons {
// here be kittens
impl Render for PanelButtons {
- type Element = Div<Self>;
+ type Element = Div;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
// todo!()
let dock = self.dock.read(cx);
let active_index = dock.active_panel_index;
let is_open = dock.is_open;
+ let dock_position = dock.position;
let (menu_anchor, menu_attach) = match dock.position {
DockPosition::Left => (AnchorCorner::BottomLeft, AnchorCorner::TopLeft),
@@ -632,31 +683,55 @@ impl Render for PanelButtons {
.panel_entries
.iter()
.enumerate()
- .filter_map(|(i, panel)| {
- let icon = panel.panel.icon(cx)?;
- let name = panel.panel.persistent_name();
+ .filter_map(|(i, entry)| {
+ let icon = entry.panel.icon(cx)?;
+ let name = entry.panel.persistent_name();
+ let panel = entry.panel.clone();
- let mut button: IconButton<Self> = if i == active_index && is_open {
+ let mut button: IconButton = if i == active_index && is_open {
let action = dock.toggle_action();
let tooltip: SharedString =
format!("Close {} dock", dock.position.to_label()).into();
IconButton::new(name, icon)
.state(InteractionState::Active)
.action(action.boxed_clone())
- .tooltip(move |_, cx| Tooltip::for_action(tooltip.clone(), &*action, cx))
+ .tooltip(move |cx| Tooltip::for_action(tooltip.clone(), &*action, cx))
} else {
- let action = panel.panel.toggle_action(cx);
+ let action = entry.panel.toggle_action(cx);
IconButton::new(name, icon)
.action(action.boxed_clone())
- .tooltip(move |_, cx| Tooltip::for_action(name, &*action, cx))
+ .tooltip(move |cx| Tooltip::for_action(name, &*action, cx))
};
Some(
- menu_handle()
- .id(name)
- .menu(move |_, cx| {
- cx.build_view(|cx| ContextMenu::new(cx).header("SECTION"))
+ menu_handle(name)
+ .menu(move |cx| {
+ const POSITIONS: [DockPosition; 3] = [
+ DockPosition::Left,
+ DockPosition::Right,
+ DockPosition::Bottom,
+ ];
+
+ ContextMenu::build(cx, |mut menu, cx| {
+ for position in POSITIONS {
+ if position != dock_position
+ && panel.position_is_valid(position, cx)
+ {
+ let panel = panel.clone();
+ menu = menu.entry(
+ ListItem::new(
+ panel.entity_id(),
+ Label::new(format!("Dock {}", position.to_label())),
+ ),
+ move |_, cx| {
+ panel.set_position(position, cx);
+ },
+ )
+ }
+ }
+ menu
+ })
})
.anchor(menu_anchor)
.attach(menu_attach)
@@ -707,7 +782,7 @@ pub mod test {
}
impl Render for TestPanel {
- type Element = Div<Self>;
+ type Element = Div;
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
div()
@@ -104,7 +104,7 @@ pub trait Item: FocusableView + EventEmitter<ItemEvent> {
fn tab_description(&self, _: usize, _: &AppContext) -> Option<SharedString> {
None
}
- fn tab_content<V: 'static>(&self, detail: Option<usize>, cx: &AppContext) -> AnyElement<V>;
+ fn tab_content(&self, detail: Option<usize>, cx: &WindowContext) -> AnyElement;
/// (model id, Item)
fn for_each_project_item(
@@ -214,8 +214,8 @@ pub trait ItemHandle: 'static + Send {
) -> gpui::Subscription;
fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString>;
fn tab_description(&self, detail: usize, cx: &AppContext) -> Option<SharedString>;
- fn tab_content(&self, detail: Option<usize>, cx: &AppContext) -> AnyElement<Pane>;
- fn dragged_tab_content(&self, detail: Option<usize>, cx: &AppContext) -> AnyElement<Workspace>;
+ fn tab_content(&self, detail: Option<usize>, cx: &WindowContext) -> AnyElement;
+ fn dragged_tab_content(&self, detail: Option<usize>, cx: &WindowContext) -> AnyElement;
fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]>;
fn project_item_model_ids(&self, cx: &AppContext) -> SmallVec<[EntityId; 3]>;
@@ -307,11 +307,11 @@ impl<T: Item> ItemHandle for View<T> {
self.read(cx).tab_description(detail, cx)
}
- fn tab_content(&self, detail: Option<usize>, cx: &AppContext) -> AnyElement<Pane> {
+ fn tab_content(&self, detail: Option<usize>, cx: &WindowContext) -> AnyElement {
self.read(cx).tab_content(detail, cx)
}
- fn dragged_tab_content(&self, detail: Option<usize>, cx: &AppContext) -> AnyElement<Workspace> {
+ fn dragged_tab_content(&self, detail: Option<usize>, cx: &WindowContext) -> AnyElement {
self.read(cx).tab_content(detail, cx)
}
@@ -72,7 +72,7 @@ impl ModalLayer {
}
impl Render for ModalLayer {
- type Element = Div<Self>;
+ type Element = Div;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
let Some(active_modal) = &self.active_modal else {
@@ -97,11 +97,11 @@ impl Render for ModalLayer {
h_stack()
// needed to prevent mouse events leaking to the
// UI below. // todo! for gpui3.
- .on_any_mouse_down(|_, _, cx| cx.stop_propagation())
- .on_any_mouse_up(|_, _, cx| cx.stop_propagation())
- .on_mouse_down_out(|this: &mut Self, event, cx| {
+ .on_any_mouse_down(|_, cx| cx.stop_propagation())
+ .on_any_mouse_up(|_, cx| cx.stop_propagation())
+ .on_mouse_down_out(cx.listener(|this, _, cx| {
this.hide_modal(cx);
- })
+ }))
.child(active_modal.modal.clone()),
),
)
@@ -15,6 +15,8 @@ pub enum NotificationEvent {
pub trait Notification: EventEmitter<NotificationEvent> + Render {}
+impl<V: EventEmitter<NotificationEvent> + Render> Notification for V {}
+
pub trait NotificationHandle: Send {
fn id(&self) -> EntityId;
fn to_any(&self) -> AnyView;
@@ -164,7 +166,7 @@ impl Workspace {
}
pub mod simple_message_notification {
- use super::{Notification, NotificationEvent};
+ use super::NotificationEvent;
use gpui::{AnyElement, AppContext, Div, EventEmitter, Render, TextStyle, ViewContext};
use serde::Deserialize;
use std::{borrow::Cow, sync::Arc};
@@ -196,7 +198,7 @@ pub mod simple_message_notification {
enum NotificationMessage {
Text(Cow<'static, str>),
- Element(fn(TextStyle, &AppContext) -> AnyElement<MessageNotification>),
+ Element(fn(TextStyle, &AppContext) -> AnyElement),
}
pub struct MessageNotification {
@@ -220,7 +222,7 @@ pub mod simple_message_notification {
}
pub fn new_element(
- message: fn(TextStyle, &AppContext) -> AnyElement<MessageNotification>,
+ message: fn(TextStyle, &AppContext) -> AnyElement,
) -> MessageNotification {
Self {
message: NotificationMessage::Element(message),
@@ -252,7 +254,7 @@ pub mod simple_message_notification {
}
impl Render for MessageNotification {
- type Element = Div<Self>;
+ type Element = Div;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
todo!()
@@ -359,7 +361,6 @@ pub mod simple_message_notification {
// }
impl EventEmitter<NotificationEvent> for MessageNotification {}
- impl Notification for MessageNotification {}
}
pub trait NotifyResultExt {
@@ -7,9 +7,9 @@ use crate::{
use anyhow::Result;
use collections::{HashMap, HashSet, VecDeque};
use gpui::{
- actions, prelude::*, Action, AppContext, AsyncWindowContext, Component, Div, EntityId,
- EventEmitter, FocusHandle, Focusable, FocusableView, Model, Pixels, Point, PromptLevel, Render,
- Task, View, ViewContext, VisualContext, WeakView, WindowContext,
+ actions, prelude::*, Action, AppContext, AsyncWindowContext, Div, EntityId, EventEmitter,
+ FocusHandle, Focusable, FocusableView, Model, Pixels, Point, PromptLevel, Render, Task, View,
+ ViewContext, VisualContext, WeakView, WindowContext,
};
use parking_lot::Mutex;
use project2::{Project, ProjectEntryId, ProjectPath};
@@ -24,8 +24,9 @@ use std::{
Arc,
},
};
+
use ui::v_stack;
-use ui::{prelude::*, Icon, IconButton, IconElement, TextColor, Tooltip};
+use ui::{prelude::*, Color, Icon, IconButton, IconElement, Tooltip};
use util::truncate_and_remove_front;
#[derive(PartialEq, Clone, Copy, Deserialize, Debug)]
@@ -1343,7 +1344,7 @@ impl Pane {
item: &Box<dyn ItemHandle>,
detail: usize,
cx: &mut ViewContext<'_, Pane>,
- ) -> impl Component<Self> {
+ ) -> impl RenderOnce {
let label = item.tab_content(Some(detail), cx);
let close_icon = || {
let id = item.item_id();
@@ -1352,12 +1353,14 @@ impl Pane {
.id(item.item_id())
.invisible()
.group_hover("", |style| style.visible())
- .child(IconButton::new("close_tab", Icon::Close).on_click(
- move |pane: &mut Self, cx| {
- pane.close_item_by_id(id, SaveIntent::Close, cx)
- .detach_and_log_err(cx);
- },
- ))
+ .child(
+ IconButton::new("close_tab", Icon::Close).on_click(cx.listener(
+ move |pane, _, cx| {
+ pane.close_item_by_id(id, SaveIntent::Close, cx)
+ .detach_and_log_err(cx);
+ },
+ )),
+ )
};
let (text_color, tab_bg, tab_hover_bg, tab_active_bg) = match ix == self.active_item_index {
@@ -1382,9 +1385,9 @@ impl Pane {
.id(item.item_id())
.cursor_pointer()
.when_some(item.tab_tooltip_text(cx), |div, text| {
- div.tooltip(move |_, cx| cx.build_view(|cx| Tooltip::new(text.clone())).into())
+ div.tooltip(move |cx| cx.build_view(|cx| Tooltip::new(text.clone())).into())
})
- .on_click(move |v: &mut Self, e, cx| v.activate_item(ix, true, true, cx))
+ .on_click(cx.listener(move |v: &mut Self, e, cx| v.activate_item(ix, true, true, cx)))
// .on_drag(move |pane, cx| pane.render_tab(ix, item.boxed_clone(), detail, cx))
// .drag_over::<DraggedTab>(|d| d.bg(cx.theme().colors().element_drop_target))
// .on_drop(|_view, state: View<DraggedTab>, cx| {
@@ -1422,12 +1425,12 @@ impl Pane {
.then(|| {
IconElement::new(Icon::ExclamationTriangle)
.size(ui::IconSize::Small)
- .color(TextColor::Warning)
+ .color(Color::Warning)
})
.or(item.is_dirty(cx).then(|| {
IconElement::new(Icon::ExclamationTriangle)
.size(ui::IconSize::Small)
- .color(TextColor::Info)
+ .color(Color::Info)
})),
)
.children((!close_right).then(|| close_icon()))
@@ -1436,7 +1439,7 @@ impl Pane {
)
}
- fn render_tab_bar(&mut self, cx: &mut ViewContext<'_, Pane>) -> impl Component<Self> {
+ fn render_tab_bar(&mut self, cx: &mut ViewContext<'_, Pane>) -> impl RenderOnce {
div()
.group("tab_bar")
.id("tab_bar")
@@ -1480,15 +1483,10 @@ impl Pane {
// Right Side
.child(
div()
- // We only use absolute here since we don't
- // have opacity or `hidden()` yet
- .absolute()
- .neg_top_7()
.px_1()
.flex()
.flex_none()
.gap_2()
- .group_hover("tab_bar", |this| this.top_0())
// Nav Buttons
.child(
div()
@@ -1896,16 +1894,24 @@ impl FocusableView for Pane {
}
impl Render for Pane {
- type Element = Focusable<Self, Div<Self>>;
+ type Element = Focusable<Div>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
v_stack()
.key_context("Pane")
.track_focus(&self.focus_handle)
- .on_action(|pane: &mut Pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx))
- .on_action(|pane: &mut Pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx))
- .on_action(|pane: &mut Pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx))
- .on_action(|pane: &mut Pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx))
+ .on_action(cx.listener(|pane: &mut Pane, _: &SplitLeft, cx| {
+ pane.split(SplitDirection::Left, cx)
+ }))
+ .on_action(
+ cx.listener(|pane: &mut Pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx)),
+ )
+ .on_action(cx.listener(|pane: &mut Pane, _: &SplitRight, cx| {
+ pane.split(SplitDirection::Right, cx)
+ }))
+ .on_action(cx.listener(|pane: &mut Pane, _: &SplitDown, cx| {
+ pane.split(SplitDirection::Down, cx)
+ }))
// cx.add_action(Pane::toggle_zoom);
// cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| {
// pane.activate_item(action.0, true, true, cx);
@@ -1926,14 +1932,16 @@ impl Render for Pane {
// cx.add_async_action(Pane::close_items_to_the_right);
// cx.add_async_action(Pane::close_all_items);
.size_full()
- .on_action(|pane: &mut Self, action: &CloseActiveItem, cx| {
- pane.close_active_item(action, cx)
- .map(|task| task.detach_and_log_err(cx));
- })
+ .on_action(
+ cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
+ pane.close_active_item(action, cx)
+ .map(|task| task.detach_and_log_err(cx));
+ }),
+ )
.child(self.render_tab_bar(cx))
- .child(div() /* todo!(toolbar) */)
+ .child(self.toolbar.clone())
.child(if let Some(item) = self.active_item() {
- div().flex_1().child(item.to_any())
+ div().flex().flex_1().child(item.to_any())
} else {
// todo!()
div().child("Empty Pane")
@@ -2950,7 +2958,7 @@ struct DraggedTab {
}
impl Render for DraggedTab {
- type Element = Div<Self>;
+ type Element = Div;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
div().w_8().h_4().bg(gpui::red())
@@ -6,7 +6,9 @@ use db2::sqlez::{
bindable::{Bind, Column, StaticColumnCount},
statement::Statement,
};
-use gpui::{point, size, AnyElement, AnyWeakView, Bounds, Model, Pixels, Point, View, ViewContext};
+use gpui::{
+ point, size, AnyWeakView, Bounds, Div, Model, Pixels, Point, RenderOnce, View, ViewContext,
+};
use parking_lot::Mutex;
use project2::Project;
use serde::Deserialize;
@@ -130,7 +132,7 @@ impl PaneGroup {
zoomed: Option<&AnyWeakView>,
app_state: &Arc<AppState>,
cx: &mut ViewContext<Workspace>,
- ) -> impl Component<Workspace> {
+ ) -> impl RenderOnce {
self.root.render(
project,
0,
@@ -202,7 +204,7 @@ impl Member {
zoomed: Option<&AnyWeakView>,
app_state: &Arc<AppState>,
cx: &mut ViewContext<Workspace>,
- ) -> impl Component<Workspace> {
+ ) -> impl RenderOnce {
match self {
Member::Pane(pane) => {
// todo!()
@@ -212,7 +214,7 @@ impl Member {
// Some(pane)
// };
- div().size_full().child(pane.clone()).render()
+ div().size_full().child(pane.clone())
// Stack::new()
// .with_child(pane_element.contained().with_border(leader_border))
@@ -559,7 +561,7 @@ impl PaneAxis {
zoomed: Option<&AnyWeakView>,
app_state: &Arc<AppState>,
cx: &mut ViewContext<Workspace>,
- ) -> AnyElement<Workspace> {
+ ) -> Div {
debug_assert!(self.members.len() == self.flexes.lock().len());
div()
@@ -582,11 +584,10 @@ impl PaneAxis {
app_state,
cx,
)
- .render(),
- Member::Pane(pane) => pane.clone().render(),
+ .render_into_any(),
+ Member::Pane(pane) => pane.clone().render_into_any(),
}
}))
- .render()
// let mut pane_axis = PaneAxisElement::new(
// self.axis,
@@ -1,7 +1,8 @@
use std::{any::Any, sync::Arc};
use gpui::{
- AnyView, AppContext, EventEmitter, Subscription, Task, View, ViewContext, WindowContext,
+ AnyView, AppContext, EventEmitter, Subscription, Task, View, ViewContext, WeakView,
+ WindowContext,
};
use project2::search::SearchQuery;
@@ -129,8 +130,7 @@ pub trait SearchableItemHandle: ItemHandle {
// todo!("here is where we need to use AnyWeakView");
impl<T: SearchableItem> SearchableItemHandle for View<T> {
fn downgrade(&self) -> Box<dyn WeakSearchableItemHandle> {
- // Box::new(self.downgrade())
- todo!()
+ Box::new(self.downgrade())
}
fn boxed_clone(&self) -> Box<dyn SearchableItemHandle> {
@@ -252,16 +252,15 @@ pub trait WeakSearchableItemHandle: WeakItemHandle {
// fn into_any(self) -> AnyWeakView;
}
-// todo!()
-// impl<T: SearchableItem> WeakSearchableItemHandle for WeakView<T> {
-// fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn SearchableItemHandle>> {
-// Some(Box::new(self.upgrade(cx)?))
-// }
+impl<T: SearchableItem> WeakSearchableItemHandle for WeakView<T> {
+ fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn SearchableItemHandle>> {
+ Some(Box::new(self.upgrade()?))
+ }
-// // fn into_any(self) -> AnyView {
-// // self.into_any()
-// // }
-// }
+ // fn into_any(self) -> AnyView {
+ // self.into_any()
+ // }
+}
impl PartialEq for Box<dyn WeakSearchableItemHandle> {
fn eq(&self, other: &Self) -> bool {
@@ -2,7 +2,7 @@ use std::any::TypeId;
use crate::{ItemHandle, Pane};
use gpui::{
- div, AnyView, Component, Div, ParentComponent, Render, Styled, Subscription, View, ViewContext,
+ div, AnyView, Div, ParentElement, Render, RenderOnce, Styled, Subscription, View, ViewContext,
WindowContext,
};
use theme2::ActiveTheme;
@@ -35,7 +35,7 @@ pub struct StatusBar {
}
impl Render for StatusBar {
- type Element = Div<Self>;
+ type Element = Div;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
div()
@@ -53,14 +53,14 @@ impl Render for StatusBar {
}
impl StatusBar {
- fn render_left_tools(&self, cx: &mut ViewContext<Self>) -> impl Component<Self> {
+ fn render_left_tools(&self, cx: &mut ViewContext<Self>) -> impl RenderOnce {
h_stack()
.items_center()
- .gap_1()
+ .gap_2()
.children(self.left_items.iter().map(|item| item.to_any()))
}
- fn render_right_tools(&self, cx: &mut ViewContext<Self>) -> impl Component<Self> {
+ fn render_right_tools(&self, cx: &mut ViewContext<Self>) -> impl RenderOnce {
h_stack()
.items_center()
.gap_2()
@@ -1,7 +1,10 @@
use crate::ItemHandle;
use gpui::{
- AnyView, Div, Entity, EntityId, EventEmitter, Render, View, ViewContext, WindowContext,
+ AnyView, Div, Entity, EntityId, EventEmitter, ParentElement as _, Render, Styled, View,
+ ViewContext, WindowContext,
};
+use theme2::ActiveTheme;
+use ui::{h_stack, v_stack, Button, Color, Icon, IconButton, Label};
pub enum ToolbarItemEvent {
ChangeLocation(ToolbarItemLocation),
@@ -39,8 +42,8 @@ trait ToolbarItemViewHandle: Send {
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum ToolbarItemLocation {
Hidden,
- PrimaryLeft { flex: Option<(f32, bool)> },
- PrimaryRight { flex: Option<(f32, bool)> },
+ PrimaryLeft,
+ PrimaryRight,
Secondary,
}
@@ -51,11 +54,56 @@ pub struct Toolbar {
items: Vec<(Box<dyn ToolbarItemViewHandle>, ToolbarItemLocation)>,
}
+impl Toolbar {
+ fn left_items(&self) -> impl Iterator<Item = &dyn ToolbarItemViewHandle> {
+ self.items.iter().filter_map(|(item, location)| {
+ if *location == ToolbarItemLocation::PrimaryLeft {
+ Some(item.as_ref())
+ } else {
+ None
+ }
+ })
+ }
+
+ fn right_items(&self) -> impl Iterator<Item = &dyn ToolbarItemViewHandle> {
+ self.items.iter().filter_map(|(item, location)| {
+ if *location == ToolbarItemLocation::PrimaryRight {
+ Some(item.as_ref())
+ } else {
+ None
+ }
+ })
+ }
+}
+
impl Render for Toolbar {
- type Element = Div<Self>;
+ type Element = Div;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- todo!()
+ //dbg!(&self.items.len());
+ v_stack()
+ .border_b()
+ .border_color(cx.theme().colors().border)
+ .child(
+ h_stack()
+ .justify_between()
+ .child(
+ // Toolbar left side
+ h_stack()
+ .p_1()
+ .child(Button::new("crates"))
+ .child(Label::new("/").color(Color::Muted))
+ .child(Button::new("workspace2")),
+ )
+ // Toolbar right side
+ .child(
+ h_stack()
+ .p_1()
+ .child(IconButton::new("buffer-search", Icon::MagnifyingGlass))
+ .child(IconButton::new("inline-assist", Icon::MagicWand)),
+ ),
+ )
+ .children(self.items.iter().map(|(child, _)| child.to_any()))
}
}
@@ -31,10 +31,10 @@ use futures::{
use gpui::{
actions, div, point, size, Action, AnyModel, AnyView, AnyWeakView, AppContext, AsyncAppContext,
AsyncWindowContext, Bounds, Context, Div, Entity, EntityId, EventEmitter, FocusHandle,
- FocusableView, GlobalPixels, InteractiveComponent, KeyContext, ManagedView, Model,
- ModelContext, ParentComponent, PathPromptOptions, Point, PromptLevel, Render, Size, Styled,
- Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowBounds, WindowContext,
- WindowHandle, WindowOptions,
+ FocusableView, GlobalPixels, InteractiveElement, KeyContext, ManagedView, Model, ModelContext,
+ ParentElement, PathPromptOptions, Point, PromptLevel, Render, Size, Styled, Subscription, Task,
+ View, ViewContext, VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle,
+ WindowOptions,
};
use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem};
use itertools::Itertools;
@@ -64,7 +64,7 @@ use std::{
time::Duration,
};
use theme2::{ActiveTheme, ThemeSettings};
-pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
+pub use toolbar::{ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
pub use ui;
use util::ResultExt;
use uuid::Uuid;
@@ -411,7 +411,7 @@ pub enum Event {
pub struct Workspace {
window_self: WindowHandle<Self>,
weak_self: WeakView<Self>,
- workspace_actions: Vec<Box<dyn Fn(Div<Workspace>) -> Div<Workspace>>>,
+ workspace_actions: Vec<Box<dyn Fn(Div, &mut ViewContext<Self>) -> Div>>,
zoomed: Option<AnyWeakView>,
zoomed_position: Option<DockPosition>,
center: PaneGroup,
@@ -813,7 +813,9 @@ impl Workspace {
DockPosition::Right => &self.right_dock,
};
- dock.update(cx, |dock, cx| dock.add_panel(panel, cx));
+ dock.update(cx, |dock, cx| {
+ dock.add_panel(panel, self.weak_self.clone(), cx)
+ });
}
pub fn status_bar(&self) -> &View<StatusBar> {
@@ -3202,53 +3204,63 @@ impl Workspace {
})
}
- fn actions(&self, div: Div<Self>) -> Div<Self> {
- self.add_workspace_actions_listeners(div)
+ fn actions(&self, div: Div, cx: &mut ViewContext<Self>) -> Div {
+ self.add_workspace_actions_listeners(div, cx)
// cx.add_async_action(Workspace::open);
// cx.add_async_action(Workspace::follow_next_collaborator);
// cx.add_async_action(Workspace::close);
- .on_action(Self::close_inactive_items_and_panes)
- .on_action(Self::close_all_items_and_panes)
+ .on_action(cx.listener(Self::close_inactive_items_and_panes))
+ .on_action(cx.listener(Self::close_all_items_and_panes))
// cx.add_global_action(Workspace::close_global);
// cx.add_global_action(restart);
- .on_action(Self::save_all)
- .on_action(Self::add_folder_to_project)
- .on_action(|workspace, _: &Unfollow, cx| {
+ .on_action(cx.listener(Self::save_all))
+ .on_action(cx.listener(Self::add_folder_to_project))
+ .on_action(cx.listener(|workspace, _: &Unfollow, cx| {
let pane = workspace.active_pane().clone();
workspace.unfollow(&pane, cx);
- })
- .on_action(|workspace, action: &Save, cx| {
+ }))
+ .on_action(cx.listener(|workspace, action: &Save, cx| {
workspace
.save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), cx)
.detach_and_log_err(cx);
- })
- .on_action(|workspace, _: &SaveAs, cx| {
+ }))
+ .on_action(cx.listener(|workspace, _: &SaveAs, cx| {
workspace
.save_active_item(SaveIntent::SaveAs, cx)
.detach_and_log_err(cx);
- })
- .on_action(|workspace, _: &ActivatePreviousPane, cx| {
+ }))
+ .on_action(cx.listener(|workspace, _: &ActivatePreviousPane, cx| {
workspace.activate_previous_pane(cx)
- })
- .on_action(|workspace, _: &ActivateNextPane, cx| workspace.activate_next_pane(cx))
- .on_action(|workspace, action: &ActivatePaneInDirection, cx| {
- workspace.activate_pane_in_direction(action.0, cx)
- })
- .on_action(|workspace, action: &SwapPaneInDirection, cx| {
+ }))
+ .on_action(
+ cx.listener(|workspace, _: &ActivateNextPane, cx| workspace.activate_next_pane(cx)),
+ )
+ .on_action(
+ cx.listener(|workspace, action: &ActivatePaneInDirection, cx| {
+ workspace.activate_pane_in_direction(action.0, cx)
+ }),
+ )
+ .on_action(cx.listener(|workspace, action: &SwapPaneInDirection, cx| {
workspace.swap_pane_in_direction(action.0, cx)
- })
- .on_action(|this, e: &ToggleLeftDock, cx| {
+ }))
+ .on_action(cx.listener(|this, e: &ToggleLeftDock, cx| {
this.toggle_dock(DockPosition::Left, cx);
- })
- .on_action(|workspace: &mut Workspace, _: &ToggleRightDock, cx| {
- workspace.toggle_dock(DockPosition::Right, cx);
- })
- .on_action(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| {
- workspace.toggle_dock(DockPosition::Bottom, cx);
- })
- .on_action(|workspace: &mut Workspace, _: &CloseAllDocks, cx| {
- workspace.close_all_docks(cx);
- })
+ }))
+ .on_action(
+ cx.listener(|workspace: &mut Workspace, _: &ToggleRightDock, cx| {
+ workspace.toggle_dock(DockPosition::Right, cx);
+ }),
+ )
+ .on_action(
+ cx.listener(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| {
+ workspace.toggle_dock(DockPosition::Bottom, cx);
+ }),
+ )
+ .on_action(
+ cx.listener(|workspace: &mut Workspace, _: &CloseAllDocks, cx| {
+ workspace.close_all_docks(cx);
+ }),
+ )
// cx.add_action(Workspace::activate_pane_at_index);
// cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
// workspace.reopen_closed_item(cx).detach();
@@ -3344,22 +3356,24 @@ impl Workspace {
) -> &mut Self {
let callback = Arc::new(callback);
- self.workspace_actions.push(Box::new(move |div| {
+ self.workspace_actions.push(Box::new(move |div, cx| {
let callback = callback.clone();
- div.on_action(move |workspace, event, cx| (callback.clone())(workspace, event, cx))
+ div.on_action(
+ cx.listener(move |workspace, event, cx| (callback.clone())(workspace, event, cx)),
+ )
}));
self
}
- fn add_workspace_actions_listeners(&self, mut div: Div<Workspace>) -> Div<Workspace> {
+ fn add_workspace_actions_listeners(&self, mut div: Div, cx: &mut ViewContext<Self>) -> Div {
let mut div = div
- .on_action(Self::close_inactive_items_and_panes)
- .on_action(Self::close_all_items_and_panes)
- .on_action(Self::add_folder_to_project)
- .on_action(Self::save_all)
- .on_action(Self::open);
+ .on_action(cx.listener(Self::close_inactive_items_and_panes))
+ .on_action(cx.listener(Self::close_all_items_and_panes))
+ .on_action(cx.listener(Self::add_folder_to_project))
+ .on_action(cx.listener(Self::save_all))
+ .on_action(cx.listener(Self::open));
for action in self.workspace_actions.iter() {
- div = (action)(div)
+ div = (action)(div, cx)
}
div
}
@@ -3593,7 +3607,7 @@ impl FocusableView for Workspace {
}
impl Render for Workspace {
- type Element = Div<Self>;
+ type Element = Div;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
let mut context = KeyContext::default();
@@ -3609,7 +3623,7 @@ impl Render for Workspace {
cx.set_rem_size(ui_font_size);
- self.actions(div())
+ self.actions(div(), cx)
.key_context(context)
.relative()
.size_full()
@@ -3664,7 +3678,7 @@ impl Render for Workspace {
&self.app_state,
cx,
))
- .child(div().flex().flex_1().child(self.bottom_dock.clone())),
+ .child(self.bottom_dock.clone()),
)
// Right Dock
.child(
@@ -3677,19 +3691,6 @@ impl Render for Workspace {
),
)
.child(self.status_bar.clone())
- .z_index(8)
- // Debug
- .child(
- div()
- .flex()
- .flex_col()
- .z_index(9)
- .absolute()
- .top_20()
- .left_1_4()
- .w_40()
- .gap_2(),
- )
}
}
@@ -170,6 +170,15 @@ osx_minimum_system_version = "10.15.7"
osx_info_plist_exts = ["resources/info/*"]
osx_url_schemes = ["zed-dev"]
+[package.metadata.bundle-nightly]
+# TODO kb different icon?
+icon = ["resources/app-icon-preview@2x.png", "resources/app-icon-preview.png"]
+identifier = "dev.zed.Zed-Nightly"
+name = "Zed Nightly"
+osx_minimum_system_version = "10.15.7"
+osx_info_plist_exts = ["resources/info/*"]
+osx_url_schemes = ["zed-nightly"]
+
[package.metadata.bundle-preview]
icon = ["resources/app-icon-preview@2x.png", "resources/app-icon-preview.png"]
identifier = "dev.zed.Zed-Preview"
@@ -178,7 +187,6 @@ osx_minimum_system_version = "10.15.7"
osx_info_plist_exts = ["resources/info/*"]
osx_url_schemes = ["zed-preview"]
-
[package.metadata.bundle-stable]
icon = ["resources/app-icon@2x.png", "resources/app-icon.png"]
identifier = "dev.zed.Zed"
@@ -3,6 +3,7 @@
use anyhow::{anyhow, Context, Result};
use backtrace::Backtrace;
+use chrono::Utc;
use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
use client::{
self, Client, TelemetrySettings, UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN,
@@ -34,7 +35,6 @@ use std::{
Arc, Weak,
},
thread,
- time::{SystemTime, UNIX_EPOCH},
};
use util::{
channel::{parse_zed_link, ReleaseChannel},
@@ -404,7 +404,7 @@ struct Panic {
os_name: String,
os_version: Option<String>,
architecture: String,
- panicked_on: u128,
+ panicked_on: i64,
#[serde(skip_serializing_if = "Option::is_none")]
installation_id: Option<String>,
session_id: String,
@@ -490,10 +490,7 @@ fn init_panic_hook(app: &App, installation_id: Option<String>, session_id: Strin
.ok()
.map(|os_version| os_version.to_string()),
architecture: env::consts::ARCH.into(),
- panicked_on: SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .unwrap()
- .as_millis(),
+ panicked_on: Utc::now().timestamp_millis(),
backtrace,
installation_id: installation_id.clone(),
session_id: session_id.clone(),
@@ -17,6 +17,7 @@ fn address() -> SocketAddr {
ReleaseChannel::Dev => 43737,
ReleaseChannel::Preview => 43738,
ReleaseChannel::Stable => 43739,
+ ReleaseChannel::Nightly => 43740,
};
SocketAddr::V4(SocketAddrV4::new(LOCALHOST, port))
@@ -25,6 +26,7 @@ fn address() -> SocketAddr {
fn instance_handshake() -> &'static str {
match *util::channel::RELEASE_CHANNEL {
ReleaseChannel::Dev => "Zed Editor Dev Instance Running",
+ ReleaseChannel::Nightly => "Zed Editor Nightly Instance Running",
ReleaseChannel::Preview => "Zed Editor Preview Instance Running",
ReleaseChannel::Stable => "Zed Editor Stable Instance Running",
}
@@ -11,14 +11,14 @@ path = "src/zed2.rs"
doctest = false
[[bin]]
-name = "Zed2"
+name = "zed2"
path = "src/main.rs"
[dependencies]
ai = { package = "ai2", path = "../ai2"}
# audio = { path = "../audio" }
# activity_indicator = { path = "../activity_indicator" }
-# auto_update = { path = "../auto_update" }
+auto_update = { package = "auto_update2", path = "../auto_update2" }
# breadcrumbs = { path = "../breadcrumbs" }
call = { package = "call2", path = "../call2" }
# channel = { path = "../channel" }
@@ -31,15 +31,14 @@ client = { package = "client2", path = "../client2" }
# clock = { path = "../clock" }
copilot = { package = "copilot2", path = "../copilot2" }
# copilot_button = { path = "../copilot_button" }
-# diagnostics = { path = "../diagnostics" }
+diagnostics = { package = "diagnostics2", path = "../diagnostics2" }
db = { package = "db2", path = "../db2" }
editor = { package="editor2", path = "../editor2" }
# feedback = { path = "../feedback" }
file_finder = { package="file_finder2", path = "../file_finder2" }
-# search = { path = "../search" }
+search = { package = "search2", path = "../search2" }
fs = { package = "fs2", path = "../fs2" }
fsevent = { path = "../fsevent" }
-fuzzy = { path = "../fuzzy" }
go_to_line = { package = "go_to_line2", path = "../go_to_line2" }
gpui = { package = "gpui2", path = "../gpui2" }
install_cli = { package = "install_cli2", path = "../install_cli2" }
@@ -48,7 +47,7 @@ language = { package = "language2", path = "../language2" }
# language_selector = { path = "../language_selector" }
lsp = { package = "lsp2", path = "../lsp2" }
menu = { package = "menu2", path = "../menu2" }
-language_tools = { path = "../language_tools" }
+# language_tools = { path = "../language_tools" }
node_runtime = { path = "../node_runtime" }
# assistant = { path = "../assistant" }
# outline = { path = "../outline" }
@@ -166,6 +165,14 @@ osx_minimum_system_version = "10.15.7"
osx_info_plist_exts = ["resources/info/*"]
osx_url_schemes = ["zed-dev"]
+[package.metadata.bundle-nightly]
+icon = ["resources/app-icon-preview@2x.png", "resources/app-icon-preview.png"]
+identifier = "dev.zed.Zed-Dev"
+name = "Zed Nightly"
+osx_minimum_system_version = "10.15.7"
+osx_info_plist_exts = ["resources/info/*"]
+osx_url_schemes = ["zed-dev"]
+
[package.metadata.bundle-preview]
icon = ["resources/app-icon-preview@2x.png", "resources/app-icon-preview.png"]
identifier = "dev.zed.Zed-Preview"
@@ -1,3 +1,5 @@
+use std::process::Command;
+
fn main() {
println!("cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET=10.15.7");
@@ -21,4 +23,14 @@ fn main() {
// Register exported Objective-C selectors, protocols, etc
println!("cargo:rustc-link-arg=-Wl,-ObjC");
+
+ // Populate git sha environment variable if git is available
+ if let Ok(output) = Command::new("git").args(["rev-parse", "HEAD"]).output() {
+ if output.status.success() {
+ println!(
+ "cargo:rustc-env=ZED_COMMIT_SHA={}",
+ String::from_utf8_lossy(&output.stdout).trim()
+ );
+ }
+ }
}
@@ -6,6 +6,7 @@
use anyhow::{anyhow, Context as _, Result};
use backtrace::Backtrace;
+use chrono::Utc;
use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
use client::UserStore;
use db::kvp::KEY_VALUE_STORE;
@@ -38,12 +39,11 @@ use std::{
Arc,
},
thread,
- time::{SystemTime, UNIX_EPOCH},
};
use theme::ActiveTheme;
use util::{
async_maybe,
- channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL},
+ channel::{parse_zed_link, AppCommitSha, ReleaseChannel, RELEASE_CHANNEL},
http::{self, HttpClient},
paths, ResultExt,
};
@@ -113,6 +113,10 @@ fn main() {
app.run(move |cx| {
cx.set_global(*RELEASE_CHANNEL);
+ if let Some(build_sha) = option_env!("ZED_COMMIT_SHA") {
+ cx.set_global(AppCommitSha(build_sha.into()))
+ }
+
cx.set_global(listener.clone());
load_embedded_fonts(cx);
@@ -146,6 +150,7 @@ fn main() {
command_palette::init(cx);
language::init(cx);
editor::init(cx);
+ diagnostics::init(cx);
copilot::init(
copilot_language_server_id,
http.clone(),
@@ -167,7 +172,7 @@ fn main() {
// })
// .detach();
- // client.telemetry().start(installation_id, session_id, cx);
+ client.telemetry().start(installation_id, session_id, cx);
let app_state = Arc::new(AppState {
languages,
@@ -182,7 +187,7 @@ fn main() {
cx.set_global(Arc::downgrade(&app_state));
// audio::init(Assets, cx);
- // auto_update::init(http.clone(), client::ZED_SERVER_URL.clone(), cx);
+ auto_update::init(http.clone(), client::ZED_SERVER_URL.clone(), cx);
workspace::init(app_state.clone(), cx);
// recent_projects::init(cx);
@@ -194,7 +199,7 @@ fn main() {
project_panel::init(Assets, cx);
// channel::init(&client, user_store.clone(), cx);
// diagnostics::init(cx);
- // search::init(cx);
+ search::init(cx);
// semantic_index::init(fs.clone(), http.clone(), languages.clone(), cx);
// vim::init(cx);
terminal_view::init(cx);
@@ -423,7 +428,7 @@ struct Panic {
os_name: String,
os_version: Option<String>,
architecture: String,
- panicked_on: u128,
+ panicked_on: i64,
#[serde(skip_serializing_if = "Option::is_none")]
installation_id: Option<String>,
session_id: String,
@@ -509,10 +514,7 @@ fn init_panic_hook(app: &App, installation_id: Option<String>, session_id: Strin
.as_ref()
.map(SemanticVersion::to_string),
architecture: env::consts::ARCH.into(),
- panicked_on: SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .unwrap()
- .as_millis(),
+ panicked_on: Utc::now().timestamp_millis(),
backtrace,
installation_id: installation_id.clone(),
session_id: session_id.clone(),
@@ -17,6 +17,7 @@ fn address() -> SocketAddr {
ReleaseChannel::Dev => 43737,
ReleaseChannel::Preview => 43738,
ReleaseChannel::Stable => 43739,
+ ReleaseChannel::Nightly => 43740,
};
SocketAddr::V4(SocketAddrV4::new(LOCALHOST, port))
@@ -25,6 +26,7 @@ fn address() -> SocketAddr {
fn instance_handshake() -> &'static str {
match *util::channel::RELEASE_CHANNEL {
ReleaseChannel::Dev => "Zed Editor Dev Instance Running",
+ ReleaseChannel::Nightly => "Zed Editor Nightly Instance Running",
ReleaseChannel::Preview => "Zed Editor Preview Instance Running",
ReleaseChannel::Stable => "Zed Editor Stable Instance Running",
}
@@ -10,8 +10,8 @@ pub use assets::*;
use collections::VecDeque;
use editor::{Editor, MultiBuffer};
use gpui::{
- actions, point, px, AppContext, Context, PromptLevel, TitlebarOptions, ViewContext,
- VisualContext, WindowBounds, WindowKind, WindowOptions,
+ actions, point, px, AppContext, Context, FocusableView, PromptLevel, TitlebarOptions,
+ ViewContext, VisualContext, WindowBounds, WindowKind, WindowOptions,
};
pub use only_instance::*;
pub use open_listener::*;
@@ -23,7 +23,7 @@ use std::{borrow::Cow, ops::Deref, sync::Arc};
use terminal_view::terminal_panel::TerminalPanel;
use util::{
asset_str,
- channel::ReleaseChannel,
+ channel::{AppCommitSha, ReleaseChannel},
paths::{self, LOCAL_SETTINGS_RELATIVE_PATH},
ResultExt,
};
@@ -98,14 +98,14 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
// todo!()
// let breadcrumbs = cx.add_view(|_| Breadcrumbs::new(workspace));
// toolbar.add_item(breadcrumbs, cx);
- // let buffer_search_bar = cx.add_view(BufferSearchBar::new);
- // toolbar.add_item(buffer_search_bar.clone(), cx);
+ let buffer_search_bar = cx.build_view(search::BufferSearchBar::new);
+ toolbar.add_item(buffer_search_bar.clone(), cx);
// let quick_action_bar = cx.add_view(|_| {
// QuickActionBar::new(buffer_search_bar, workspace)
// });
// toolbar.add_item(quick_action_bar, cx);
- // let diagnostic_editor_controls =
- // cx.add_view(|_| diagnostics2::ToolbarControls::new());
+ let diagnostic_editor_controls =
+ cx.build_view(|_| diagnostics::ToolbarControls::new());
// toolbar.add_item(diagnostic_editor_controls, cx);
// let project_search_bar = cx.add_view(|_| ProjectSearchBar::new());
// toolbar.add_item(project_search_bar, cx);
@@ -137,8 +137,8 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
// let copilot =
// cx.add_view(|cx| copilot_button::CopilotButton::new(app_state.fs.clone(), cx));
- // let diagnostic_summary =
- // cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx));
+ let diagnostic_summary =
+ cx.build_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx));
// let activity_indicator = activity_indicator::ActivityIndicator::new(
// workspace,
// app_state.languages.clone(),
@@ -152,7 +152,7 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
// });
// let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new());
workspace.status_bar().update(cx, |status_bar, cx| {
- // status_bar.add_left_item(diagnostic_summary, cx);
+ status_bar.add_left_item(diagnostic_summary, cx);
// status_bar.add_left_item(activity_indicator, cx);
// status_bar.add_right_item(feedback_button, cx);
@@ -162,7 +162,7 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
// status_bar.add_right_item(cursor_position, cx);
});
- // auto_update::notify_of_any_new_update(cx.weak_handle(), cx);
+ auto_update::notify_of_any_new_update(cx);
// vim::observe_keystrokes(cx);
@@ -425,6 +425,8 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
}
}
});
+
+ workspace.focus_handle(cx).focus(cx);
//todo!()
// load_default_keymap(cx);
})
@@ -432,9 +434,16 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
}
fn about(_: &mut Workspace, _: &About, cx: &mut gpui::ViewContext<Workspace>) {
+ use std::fmt::Write as _;
+
let app_name = cx.global::<ReleaseChannel>().display_name();
let version = env!("CARGO_PKG_VERSION");
- let prompt = cx.prompt(PromptLevel::Info, &format!("{app_name} {version}"), &["OK"]);
+ let mut message = format!("{app_name} {version}");
+ if let Some(sha) = cx.try_global::<AppCommitSha>() {
+ write!(&mut message, "\n\n{}", sha.0).unwrap();
+ }
+
+ let prompt = cx.prompt(PromptLevel::Info, &message, &["OK"]);
cx.foreground_executor()
.spawn(async {
prompt.await.ok();
@@ -0,0 +1,13 @@
+#!/bin/bash
+
+set -e
+
+branch=$(git rev-parse --abbrev-ref HEAD)
+if [ "$branch" != "main" ]; then
+ echo "You must be on main to run this script"
+ exit 1
+fi
+
+git pull --ff-only origin main
+git tag -f nightly
+git push -f origin nightly
@@ -43,8 +43,8 @@ if [[ $patch != 0 ]]; then
echo "patch version on main should be zero"
exit 1
fi
-if [[ $(cat crates/zed/RELEASE_CHANNEL) != dev ]]; then
- echo "release channel on main should be dev"
+if [[ $(cat crates/zed/RELEASE_CHANNEL) != dev && $(cat crates/zed/RELEASE_CHANNEL) != nightly ]]; then
+ echo "release channel on main should be dev or nightly"
exit 1
fi
if git show-ref --quiet refs/tags/${preview_tag_name}; then
@@ -59,6 +59,7 @@ if ! git show-ref --quiet refs/heads/${prev_minor_branch_name}; then
echo "previous branch ${minor_branch_name} doesn't exist"
exit 1
fi
+# TODO kb anything else for RELEASE_CHANNEL == nightly needs to be done below?
if [[ $(git show ${prev_minor_branch_name}:crates/zed/RELEASE_CHANNEL) != preview ]]; then
echo "release channel on branch ${prev_minor_branch_name} should be preview"
exit 1
@@ -9,8 +9,11 @@ case $channel in
preview)
tag_suffix="-pre"
;;
+ nightly)
+ tag_suffix="-nightly"
+ ;;
*)
- echo "this must be run on a stable or preview release branch" >&2
+ echo "this must be run on either of stable|preview|nightly release branches" >&2
exit 1
;;
esac
@@ -9,6 +9,8 @@ local_arch=false
local_only=false
overwrite_local_app=false
bundle_name=""
+zed_crate="zed"
+binary_name="Zed"
# This must match the team in the provsiioning profile.
APPLE_NOTORIZATION_TEAM="MQ55VZLNZQ"
@@ -25,13 +27,11 @@ Options:
-o Open the resulting DMG or the app itself in local mode.
-f Overwrite the local app bundle if it exists.
-h Display this help and exit.
+ -2 Build zed 2 instead of zed 1.
"
}
-# If -o option is specified, the folder of the resulting dmg will be opened in finder
-# If -d is specified, Zed will be compiled in debug mode and the application's path printed
-# If -od or -do is specified Zed will be bundled in debug and the application will be run.
-while getopts 'dlfoh' flag
+while getopts 'dlfoh2' flag
do
case "${flag}" in
o) open_result=true;;
@@ -39,7 +39,6 @@ do
export CARGO_INCREMENTAL=true
export CARGO_BUNDLE_SKIP_BUILD=true
build_flag="";
- local_arch=true
target_dir="debug"
;;
l)
@@ -51,6 +50,10 @@ do
target_dir="debug"
;;
f) overwrite_local_app=true;;
+ 2)
+ zed_crate="zed2"
+ binary_name="Zed2"
+ ;;
h)
help_info
exit 0
@@ -83,16 +86,19 @@ local_target_triple=${host_line#*: }
if [ "$local_arch" = true ]; then
echo "Building for local target only."
- cargo build ${build_flag} --package zed
+ cargo build ${build_flag} --package ${zed_crate}
cargo build ${build_flag} --package cli
else
echo "Compiling zed binaries"
- cargo build ${build_flag} --package zed --package cli --target aarch64-apple-darwin --target x86_64-apple-darwin
+ cargo build ${build_flag} --package ${zed_crate} --package cli --target aarch64-apple-darwin --target x86_64-apple-darwin
fi
echo "Creating application bundle"
pushd crates/zed
channel=$(<RELEASE_CHANNEL)
+popd
+
+pushd crates/${zed_crate}
cp Cargo.toml Cargo.toml.backup
sed \
-i .backup \
@@ -113,9 +119,9 @@ if [ "$local_arch" = false ]; then
echo "Creating fat binaries"
lipo \
-create \
- target/{x86_64-apple-darwin,aarch64-apple-darwin}/${target_dir}/Zed \
+ target/{x86_64-apple-darwin,aarch64-apple-darwin}/${target_dir}/${binary_name} \
-output \
- "${app_path}/Contents/MacOS/zed"
+ "${app_path}/Contents/MacOS/${zed_crate}"
lipo \
-create \
target/{x86_64-apple-darwin,aarch64-apple-darwin}/${target_dir}/cli \
@@ -131,7 +137,8 @@ else
cp -R target/${target_dir}/WebRTC.framework "${app_path}/Contents/Frameworks/"
fi
-cp crates/zed/contents/$channel/embedded.provisionprofile "${app_path}/Contents/"
+#todo!(The app identifier has been set to 'Dev', but the channel is nightly, RATIONALIZE ALL OF THIS MESS)
+cp crates/${zed_crate}/contents/$channel/embedded.provisionprofile "${app_path}/Contents/"
if [[ -n $MACOS_CERTIFICATE && -n $MACOS_CERTIFICATE_PASSWORD && -n $APPLE_NOTARIZATION_USERNAME && -n $APPLE_NOTARIZATION_PASSWORD ]]; then
echo "Signing bundle with Apple-issued certificate"
@@ -145,9 +152,14 @@ if [[ -n $MACOS_CERTIFICATE && -n $MACOS_CERTIFICATE_PASSWORD && -n $APPLE_NOTAR
# sequence of codesign commands modeled after this example: https://developer.apple.com/forums/thread/701514
/usr/bin/codesign --deep --force --timestamp --sign "Zed Industries, Inc." "${app_path}/Contents/Frameworks/WebRTC.framework" -v
- /usr/bin/codesign --deep --force --timestamp --options runtime --sign "Zed Industries, Inc." "${app_path}/Contents/MacOS/cli" -v
- /usr/bin/codesign --deep --force --timestamp --options runtime --entitlements crates/zed/resources/zed.entitlements --sign "Zed Industries, Inc." "${app_path}/Contents/MacOS/zed" -v
- /usr/bin/codesign --force --timestamp --options runtime --entitlements crates/zed/resources/zed.entitlements --sign "Zed Industries, Inc." "${app_path}" -v
+
+ # todo!(restore cli to zed2)
+ if [[ "$zed_crate" == "zed" ]]; then
+ /usr/bin/codesign --deep --force --timestamp --options runtime --sign "Zed Industries, Inc." "${app_path}/Contents/MacOS/cli" -v
+ fi
+
+ /usr/bin/codesign --deep --force --timestamp --options runtime --entitlements crates/${zed_crate}/resources/zed.entitlements --sign "Zed Industries, Inc." "${app_path}/Contents/MacOS/${zed_crate}" -v
+ /usr/bin/codesign --force --timestamp --options runtime --entitlements crates/${zed_crate}/resources/zed.entitlements --sign "Zed Industries, Inc." "${app_path}" -v
security default-keychain -s login.keychain
else
@@ -166,7 +178,7 @@ else
# - get a signing key for the MQ55VZLNZQ team from Nathan.
# - create your own signing key, and update references to MQ55VZLNZQ to your own team ID
# then comment out this line.
- cat crates/zed/resources/zed.entitlements | sed '/com.apple.developer.associated-domains/,+1d' > "${app_path}/Contents/Resources/zed.entitlements"
+ cat crates/${zed_crate}/resources/zed.entitlements | sed '/com.apple.developer.associated-domains/,+1d' > "${app_path}/Contents/Resources/zed.entitlements"
codesign --force --deep --entitlements "${app_path}/Contents/Resources/zed.entitlements" --sign ${MACOS_SIGNING_KEY:- -} "${app_path}" -v
fi
@@ -4,12 +4,17 @@ set -eu
source script/lib/deploy-helpers.sh
if [[ $# < 2 ]]; then
- echo "Usage: $0 <production|staging|preview> <tag-name>"
+ echo "Usage: $0 <production|staging|preview> <tag-name> (nightly is not yet supported)"
exit 1
fi
environment=$1
version=$2
+if [[ ${environment} == "nightly" ]]; then
+ echo "nightly is not yet supported"
+ exit 1
+fi
+
export_vars_for_environment ${environment}
image_id=$(image_id_for_version ${version})
@@ -0,0 +1,160 @@
+#!/bin/bash
+
+# Check if the script is run from the root of the repository
+if [ ! -f "Cargo.toml" ] || [ ! -d "crates/zed" ]; then
+ echo "Please run the script from the root of the repository."
+ exit 1
+fi
+
+# Set the environment variables
+TARGET_DIR="../zed-docs"
+PUSH_CHANGES=false
+CLEAN_FOLDERS=false
+
+# Parse command line arguments
+while getopts "pc" opt; do
+ case ${opt} in
+ p )
+ PUSH_CHANGES=true
+ ;;
+ c )
+ CLEAN_FOLDERS=true
+ ;;
+ \? )
+ echo "Invalid option: $OPTARG" 1>&2
+ exit 1
+ ;;
+ esac
+done
+
+# Check if the target documentation directory exists
+if [ ! -d "$TARGET_DIR" ]; then
+ # Prompt the user for input
+ read -p "Can't find ../zed-docs. Do you want to clone the repository (y/n)?" -n 1 -r
+ echo # Move to a new line
+
+ if [[ $REPLY =~ ^[Yy]$ ]]; then
+ # Clone the repo if the user agrees
+ git clone https://github.com/zed-industries/zed-docs.git "$TARGET_DIR"
+ else
+ # Exit if the user does not agree to clone the repo
+ echo "Exiting without cloning the repository."
+ exit 1
+ fi
+else
+ # If the directory exists, pull the latest changes
+ pushd "$TARGET_DIR" > /dev/null
+ git pull
+ popd > /dev/null
+fi
+
+if "$CLEAN_FOLDERS"; then
+ echo "Cleaning ./doc and ./debug folders..."
+ rm -rf "$TARGET_DIR/doc"
+ rm -rf "$TARGET_DIR/debug"
+fi
+
+# Build the documentation
+CARGO_TARGET_DIR="$TARGET_DIR" cargo doc --workspace --no-deps --open \
+--exclude activity_indicator \
+--exclude ai \
+--exclude assistant \
+--exclude audio \
+--exclude auto_update \
+--exclude breadcrumbs \
+--exclude call \
+--exclude channel \
+--exclude cli \
+--exclude client \
+--exclude clock \
+--exclude collab \
+--exclude collab_ui \
+--exclude collections \
+--exclude command_palette \
+--exclude component_test \
+--exclude context_menu \
+--exclude copilot \
+--exclude copilot_button \
+--exclude db \
+--exclude diagnostics \
+--exclude drag_and_drop \
+--exclude editor \
+--exclude feature_flags \
+--exclude feedback \
+--exclude file_finder \
+--exclude fs \
+--exclude fsevent \
+--exclude fuzzy \
+--exclude git \
+--exclude go_to_line \
+--exclude gpui \
+--exclude gpui_macros \
+--exclude install_cli \
+--exclude journal \
+--exclude language \
+--exclude language_selector \
+--exclude language_tools \
+--exclude live_kit_client \
+--exclude live_kit_server \
+--exclude lsp \
+--exclude media \
+--exclude menu \
+--exclude multi_buffer \
+--exclude node_runtime \
+--exclude notifications \
+--exclude outline \
+--exclude picker \
+--exclude plugin \
+--exclude plugin_macros \
+--exclude plugin_runtime \
+--exclude prettier \
+--exclude project \
+--exclude project_panel \
+--exclude project_symbols \
+--exclude quick_action_bar \
+--exclude recent_projects \
+--exclude refineable \
+--exclude rich_text \
+--exclude rope \
+--exclude rpc \
+--exclude search \
+--exclude semantic_index \
+--exclude settings \
+--exclude snippet \
+--exclude sqlez \
+--exclude sqlez_macros \
+--exclude storybook3 \
+--exclude sum_tree \
+--exclude terminal \
+--exclude terminal_view \
+--exclude text \
+--exclude theme \
+--exclude theme_importer \
+--exclude theme_selector \
+--exclude util \
+--exclude vcs_menu \
+--exclude vim \
+--exclude welcome \
+--exclude xtask \
+--exclude zed \
+--exclude zed-actions
+
+if "$PUSH_CHANGES"; then
+ # Commit the changes and push
+ pushd "$TARGET_DIR" > /dev/null
+ # Check if there are any changes to commit
+ if git diff --quiet && git diff --staged --quiet; then
+ echo "No changes to the documentation."
+ else
+ # Staging the changes
+ git add .
+
+ # Creating a commit with the current datetime
+ DATETIME=$(date +"%Y-%m-%d %H:%M:%S")
+ git commit -m "Update docs – $DATETIME"
+
+ # Pushing the changes
+ git push
+ fi
+ popd > /dev/null
+fi
@@ -4,12 +4,17 @@ set -eu
source script/lib/deploy-helpers.sh
if [[ $# < 2 ]]; then
- echo "Usage: $0 <production|staging|preview> <tag-name>"
+ echo "Usage: $0 <production|staging|preview> <tag-name> (nightly is not yet supported)"
exit 1
fi
environment=$1
version=$2
+if [[ ${environment} == "nightly" ]]; then
+ echo "nightly is not yet supported"
+ exit 1
+fi
+
export_vars_for_environment ${environment}
image_id=$(image_id_for_version ${version})
@@ -23,4 +28,4 @@ envsubst < crates/collab/k8s/migrate.template.yml | kubectl apply -f -
pod=$(kubectl --namespace=${environment} get pods --selector=job-name=${ZED_MIGRATE_JOB_NAME} --output=jsonpath='{.items[0].metadata.name}')
echo "Job pod:" $pod
-kubectl --namespace=${environment} logs -f ${pod}
+kubectl --namespace=${environment} logs -f ${pod}
@@ -0,0 +1,37 @@
+#!/bin/bash
+
+# Based on the template in: https://docs.digitalocean.com/reference/api/spaces-api/
+set -ux
+
+# Step 1: Define the parameters for the Space you want to upload to.
+SPACE="zed-nightly-host" # Find your endpoint in the control panel, under Settings.
+REGION="nyc3" # Must be "us-east-1" when creating new Spaces. Otherwise, use the region in your endpoint (e.g. nyc3).
+
+# Step 2: Define a function that uploads your object via cURL.
+function uploadToSpaces
+{
+ file_to_upload="$1"
+ file_name="$2"
+ space_path="nightly"
+ date=$(date +"%a, %d %b %Y %T %z")
+ acl="x-amz-acl:private"
+ content_type="application/octet-stream"
+ storage_type="x-amz-storage-class:STANDARD"
+ string="PUT\n\n${content_type}\n${date}\n${acl}\n${storage_type}\n/${SPACE}/${space_path}/${file_name}"
+ signature=$(echo -en "${string}" | openssl sha1 -hmac "${DIGITALOCEAN_SPACES_SECRET_KEY}" -binary | base64)
+
+ curl -vv -s -X PUT -T "$file_to_upload" \
+ -H "Host: ${SPACE}.${REGION}.digitaloceanspaces.com" \
+ -H "Date: $date" \
+ -H "Content-Type: $content_type" \
+ -H "$storage_type" \
+ -H "$acl" \
+ -H "Authorization: AWS ${DIGITALOCEAN_SPACES_ACCESS_KEY}:$signature" \
+ "https://${SPACE}.${REGION}.digitaloceanspaces.com/${space_path}/${file_name}"
+}
+
+sha=$(git rev-parse HEAD)
+echo ${sha} > target/latest-sha
+
+uploadToSpaces "target/release/Zed.dmg" "Zed.dmg"
+uploadToSpaces "target/latest-sha" "latest-sha"
@@ -4,11 +4,16 @@ set -eu
source script/lib/deploy-helpers.sh
if [[ $# < 1 ]]; then
- echo "Usage: $0 <production|staging|preview>"
+ echo "Usage: $0 <production|staging|preview> (nightly is not yet supported)"
exit 1
fi
environment=$1
+if [[ ${environment} == "nightly" ]]; then
+ echo "nightly is not yet supported"
+ exit 1
+fi
+
export_vars_for_environment ${environment}
target_zed_kube_cluster