mirror of
https://github.com/zed-industries/zed.git
synced 2024-09-16 00:47:39 +03:00
Merge branch 'main' into search2
This commit is contained in:
commit
07cc5904f8
15
.github/actions/check_formatting/action.yml
vendored
Normal file
15
.github/actions/check_formatting/action.yml
vendored
Normal file
@ -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
|
34
.github/actions/run_tests/action.yml
vendored
Normal file
34
.github/actions/run_tests/action.yml
vendored
Normal file
@ -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
|
37
.github/workflows/ci.yml
vendored
37
.github/workflows/ci.yml
vendored
@ -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' }}
|
||||
|
98
.github/workflows/release_nightly.yml
vendored
Normal file
98
.github/workflows/release_nightly.yml
vendored
Normal file
@ -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
|
82
Cargo.lock
generated
82
Cargo.lock
generated
@ -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"
|
||||
@ -1526,6 +1550,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion 0.3.2",
|
||||
"async-tungstenite",
|
||||
"chrono",
|
||||
"collections",
|
||||
"db",
|
||||
"feature_flags",
|
||||
@ -1562,6 +1587,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion 0.3.2",
|
||||
"async-tungstenite",
|
||||
"chrono",
|
||||
"collections",
|
||||
"db2",
|
||||
"feature_flags2",
|
||||
@ -2614,6 +2640,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 +3813,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 +3878,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 +3913,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 +4546,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"dirs 4.0.0",
|
||||
"editor",
|
||||
"editor2",
|
||||
"gpui2",
|
||||
"log",
|
||||
"schemars",
|
||||
@ -9082,13 +9142,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",
|
||||
]
|
||||
@ -11572,6 +11643,7 @@ dependencies = [
|
||||
"async-recursion 0.3.2",
|
||||
"async-tar",
|
||||
"async-trait",
|
||||
"auto_update2",
|
||||
"backtrace",
|
||||
"call2",
|
||||
"chrono",
|
||||
@ -11583,6 +11655,7 @@ dependencies = [
|
||||
"copilot2",
|
||||
"ctor",
|
||||
"db2",
|
||||
"diagnostics2",
|
||||
"editor2",
|
||||
"env_logger 0.9.3",
|
||||
"feature_flags2",
|
||||
@ -11600,7 +11673,6 @@ dependencies = [
|
||||
"isahc",
|
||||
"journal2",
|
||||
"language2",
|
||||
"language_tools",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"log",
|
||||
|
@ -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",
|
||||
|
@ -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>
|
||||
|
Before Width: | Height: | Size: 835 B After Width: | Height: | Size: 351 B |
@ -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
|
||||
|
29
crates/auto_update2/Cargo.toml
Normal file
29
crates/auto_update2/Cargo.toml
Normal file
@ -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
|
406
crates/auto_update2/src/auto_update.rs
Normal file
406
crates/auto_update2/src/auto_update.rs
Normal file
@ -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())
|
||||
})
|
||||
}
|
||||
}
|
87
crates/auto_update2/src/update_notification.rs
Normal file
87
crates/auto_update2/src/update_notification.rs
Normal file
@ -0,0 +1,87 @@
|
||||
use gpui::{div, Div, EventEmitter, ParentComponent, 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<Self>;
|
||||
|
||||
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)]
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -1,8 +1,9 @@
|
||||
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, Component, Div, EventEmitter, FocusHandle,
|
||||
FocusableView, Keystroke, Manager, ParentComponent, Render, Styled, View, ViewContext,
|
||||
VisualContext, WeakView,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use std::{
|
||||
@ -68,7 +69,9 @@ 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)
|
||||
}
|
||||
@ -114,6 +117,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.
|
||||
@ -265,7 +269,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();
|
||||
}
|
||||
|
||||
|
43
crates/diagnostics2/Cargo.toml
Normal file
43
crates/diagnostics2/Cargo.toml
Normal file
@ -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
|
1572
crates/diagnostics2/src/diagnostics.rs
Normal file
1572
crates/diagnostics2/src/diagnostics.rs
Normal file
File diff suppressed because it is too large
Load Diff
151
crates/diagnostics2/src/items.rs
Normal file
151
crates/diagnostics2/src/items.rs
Normal file
@ -0,0 +1,151 @@
|
||||
use collections::HashSet;
|
||||
use editor::{Editor, GoToDiagnostic};
|
||||
use gpui::{
|
||||
rems, Div, EventEmitter, InteractiveComponent, ParentComponent, Render, Stateful,
|
||||
StatefulInteractiveComponent, Styled, Subscription, View, ViewContext, WeakView,
|
||||
};
|
||||
use language::Diagnostic;
|
||||
use lsp::LanguageServerId;
|
||||
use theme::ActiveTheme;
|
||||
use ui::{h_stack, Icon, IconElement, Label, TextColor, 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<Self, Div<Self>>;
|
||||
|
||||
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(TextColor::Success)),
|
||||
(0, warning_count) => h_stack()
|
||||
.gap_1()
|
||||
.child(IconElement::new(Icon::ExclamationTriangle).color(TextColor::Warning))
|
||||
.child(Label::new(warning_count.to_string())),
|
||||
(error_count, 0) => h_stack()
|
||||
.gap_1()
|
||||
.child(IconElement::new(Icon::XCircle).color(TextColor::Error))
|
||||
.child(Label::new(error_count.to_string())),
|
||||
(error_count, warning_count) => h_stack()
|
||||
.gap_1()
|
||||
.child(IconElement::new(Icon::XCircle).color(TextColor::Error))
|
||||
.child(Label::new(error_count.to_string()))
|
||||
.child(IconElement::new(Icon::ExclamationTriangle).color(TextColor::Warning))
|
||||
.child(Label::new(warning_count.to_string())),
|
||||
};
|
||||
|
||||
h_stack()
|
||||
.id(cx.entity_id())
|
||||
.on_action(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(|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();
|
||||
}
|
||||
}
|
28
crates/diagnostics2/src/project_diagnostics_settings.rs
Normal file
28
crates/diagnostics2/src/project_diagnostics_settings.rs
Normal file
@ -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)
|
||||
}
|
||||
}
|
66
crates/diagnostics2/src/toolbar_controls.rs
Normal file
66
crates/diagnostics2/src/toolbar_controls.rs
Normal file
@ -0,0 +1,66 @@
|
||||
use crate::ProjectDiagnosticsEditor;
|
||||
use gpui::{div, Div, EventEmitter, ParentComponent, 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<Self>;
|
||||
|
||||
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(|this: &mut Self, 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 { flex: None }
|
||||
} 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,
|
||||
|
@ -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};
|
||||
@ -585,7 +585,7 @@ pub enum SoftWrap {
|
||||
Column(u32),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Default)]
|
||||
pub struct EditorStyle {
|
||||
pub background: Hsla,
|
||||
pub local_player: PlayerColor,
|
||||
@ -2318,7 +2318,7 @@ 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 {
|
||||
@ -4243,7 +4243,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(),
|
||||
});
|
||||
@ -4402,6 +4402,7 @@ impl Editor {
|
||||
editor.fold_at(&FoldAt { buffer_row }, cx);
|
||||
}
|
||||
})
|
||||
.color(ui::TextColor::Muted)
|
||||
.render()
|
||||
})
|
||||
})
|
||||
@ -5640,7 +5641,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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -5655,7 +5656,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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -8124,7 +8125,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
|
||||
@ -8712,7 +8713,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);
|
||||
@ -8751,7 +8752,7 @@ impl Editor {
|
||||
predecessor,
|
||||
excerpts,
|
||||
} => {
|
||||
cx.emit(Event::ExcerptsAdded {
|
||||
cx.emit(EditorEvent::ExcerptsAdded {
|
||||
buffer: buffer.clone(),
|
||||
predecessor: *predecessor,
|
||||
excerpts: excerpts.clone(),
|
||||
@ -8760,7 +8761,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);
|
||||
@ -8774,7 +8775,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);
|
||||
@ -8968,12 +8969,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"))]
|
||||
@ -9020,14 +9021,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,
|
||||
@ -9114,7 +9115,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 {
|
||||
@ -9174,7 +9175,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();
|
||||
@ -9204,7 +9205,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();
|
||||
}
|
||||
}
|
||||
@ -9327,7 +9328,7 @@ impl Deref for EditorSnapshot {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum Event {
|
||||
pub enum EditorEvent {
|
||||
InputIgnored {
|
||||
text: Arc<str>,
|
||||
},
|
||||
@ -9345,8 +9346,12 @@ pub enum Event {
|
||||
},
|
||||
BufferEdited,
|
||||
Edited,
|
||||
Reparsed,
|
||||
Focused,
|
||||
Blurred,
|
||||
DirtyChanged,
|
||||
Saved,
|
||||
TitleChanged,
|
||||
DiffBaseChanged,
|
||||
SelectionsChanged {
|
||||
local: bool,
|
||||
@ -9355,6 +9360,7 @@ pub enum Event {
|
||||
local: bool,
|
||||
autoscroll: bool,
|
||||
},
|
||||
Closed,
|
||||
}
|
||||
|
||||
pub struct EditorFocused(pub View<Editor>);
|
||||
@ -9369,7 +9375,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 {
|
||||
@ -9572,7 +9578,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;
|
||||
}
|
||||
|
||||
@ -9602,7 +9608,7 @@ impl InputHandler for Editor {
|
||||
})
|
||||
});
|
||||
|
||||
cx.emit(Event::InputHandled {
|
||||
cx.emit(EditorEvent::InputHandled {
|
||||
utf16_range_to_replace: range_to_replace,
|
||||
text: text.into(),
|
||||
});
|
||||
@ -9633,7 +9639,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;
|
||||
}
|
||||
|
||||
@ -9676,7 +9682,7 @@ impl InputHandler for Editor {
|
||||
})
|
||||
});
|
||||
|
||||
cx.emit(Event::InputHandled {
|
||||
cx.emit(EditorEvent::InputHandled {
|
||||
utf16_range_to_replace: range_to_replace,
|
||||
text: text.into(),
|
||||
});
|
||||
|
@ -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| {
|
||||
|
@ -1970,6 +1970,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 +2004,7 @@ impl EditorElement {
|
||||
editor_style: &self.style,
|
||||
})
|
||||
}
|
||||
|
||||
TransformBlock::ExcerptHeader {
|
||||
buffer,
|
||||
range,
|
||||
@ -2046,6 +2048,7 @@ impl EditorElement {
|
||||
}
|
||||
|
||||
h_stack()
|
||||
.id("path header block")
|
||||
.size_full()
|
||||
.bg(gpui::red())
|
||||
.child(filename.unwrap_or_else(|| "untitled".to_string()))
|
||||
@ -2054,6 +2057,7 @@ impl EditorElement {
|
||||
} else {
|
||||
let text_style = style.text.clone();
|
||||
h_stack()
|
||||
.id("collapsed context")
|
||||
.size_full()
|
||||
.bg(gpui::red())
|
||||
.child("⋯")
|
||||
|
@ -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};
|
||||
@ -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()
|
||||
|
@ -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, Component, Div, EventEmitter, FocusHandle, FocusableView,
|
||||
InteractiveComponent, Manager, Model, ParentComponent, Render, Styled, Task, View, ViewContext,
|
||||
VisualContext, WeakView,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
|
||||
@ -111,7 +111,8 @@ 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)
|
||||
}
|
||||
@ -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,7 +1,8 @@
|
||||
use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Editor};
|
||||
use gpui::{
|
||||
actions, div, prelude::*, AppContext, Dismiss, Div, FocusHandle, ManagedView, ParentComponent,
|
||||
Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext, WindowContext,
|
||||
actions, div, prelude::*, AppContext, Div, EventEmitter, FocusHandle, FocusableView, Manager,
|
||||
ParentComponent, Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext,
|
||||
WindowContext,
|
||||
};
|
||||
use text::{Bias, Point};
|
||||
use theme::ActiveTheme;
|
||||
@ -23,11 +24,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 +84,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 +124,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,7 +141,7 @@ impl GoToLine {
|
||||
self.prev_scroll_position.take();
|
||||
}
|
||||
|
||||
cx.emit(Dismiss);
|
||||
cx.emit(Manager::Dismiss);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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 _};
|
||||
@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -385,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,
|
||||
@ -578,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 {
|
||||
|
@ -87,6 +87,7 @@ pub trait ParentComponent<V: 'static> {
|
||||
}
|
||||
|
||||
trait ElementObject<V> {
|
||||
fn element_id(&self) -> Option<ElementId>;
|
||||
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>);
|
||||
fn measure(
|
||||
@ -144,6 +145,10 @@ where
|
||||
E: Element<V>,
|
||||
E::ElementState: 'static,
|
||||
{
|
||||
fn element_id(&self) -> Option<ElementId> {
|
||||
self.element.element_id()
|
||||
}
|
||||
|
||||
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 => {
|
||||
@ -266,6 +271,10 @@ impl<V> AnyElement<V> {
|
||||
AnyElement(Box::new(RenderedElement::new(element)))
|
||||
}
|
||||
|
||||
pub fn element_id(&self) -> Option<ElementId> {
|
||||
self.0.element_id()
|
||||
}
|
||||
|
||||
pub fn layout(&mut self, view_state: &mut V, cx: &mut ViewContext<V>) -> LayoutId {
|
||||
self.0.layout(view_state, cx)
|
||||
}
|
||||
|
@ -1124,9 +1124,14 @@ where
|
||||
}
|
||||
}
|
||||
}
|
||||
// if self.hover_style.is_some() {
|
||||
if bounds.contains_point(&mouse_position) {
|
||||
// eprintln!("div hovered {bounds:?} {mouse_position:?}");
|
||||
style.refine(&self.hover_style);
|
||||
} else {
|
||||
// eprintln!("div NOT hovered {bounds:?} {mouse_position:?}");
|
||||
}
|
||||
// }
|
||||
|
||||
if let Some(drag) = cx.active_drag.take() {
|
||||
for (state_type, group_drag_style) in &self.group_drag_over_styles {
|
||||
|
@ -81,7 +81,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| {
|
||||
@ -90,7 +90,7 @@ impl<V> Element<V> for Img<V> {
|
||||
});
|
||||
} else {
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
if image_future.await.log_err().is_some() {
|
||||
if image_future.await.ok().is_some() {
|
||||
cx.on_next_frame(|cx| cx.notify());
|
||||
}
|
||||
})
|
||||
|
@ -64,19 +64,34 @@ impl<V: 'static> Element<V> for Text {
|
||||
|
||||
let layout_id = cx.request_measured_layout(Default::default(), rem_size, {
|
||||
let element_state = element_state.clone();
|
||||
move |known_dimensions, _| {
|
||||
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,9 +103,12 @@ 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
|
||||
}
|
||||
@ -133,6 +151,8 @@ impl TextState {
|
||||
struct TextStateInner {
|
||||
lines: SmallVec<[WrappedLine; 1]>,
|
||||
line_height: Pixels,
|
||||
wrap_width: Option<Pixels>,
|
||||
size: Option<Size<Pixels>>,
|
||||
}
|
||||
|
||||
struct InteractiveText {
|
||||
|
@ -141,6 +141,10 @@ pub trait VisualContext: Context {
|
||||
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();
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -191,6 +191,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>,
|
||||
@ -277,17 +281,17 @@ 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>,
|
||||
{
|
||||
type Element = E;
|
||||
// impl<T, E> Render for T
|
||||
// where
|
||||
// T: 'static + FnMut(&mut WindowContext) -> E,
|
||||
// E: 'static + Send + Element<T>,
|
||||
// {
|
||||
// type Element = E;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
(self)(cx)
|
||||
}
|
||||
}
|
||||
// fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
// (self)(cx)
|
||||
// }
|
||||
// }
|
||||
|
||||
pub struct RenderViewWith<C, V> {
|
||||
view: View<V>,
|
||||
|
@ -2,14 +2,14 @@ use crate::{
|
||||
key_dispatch::DispatchActionListener, px, size, Action, AnyBox, 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;
|
||||
@ -193,17 +193,12 @@ pub trait FocusableView: Render {
|
||||
|
||||
/// 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.
|
||||
@ -1582,6 +1577,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> {
|
||||
@ -2275,6 +2277,13 @@ 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))
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> Context for ViewContext<'_, V> {
|
||||
@ -2354,6 +2363,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 +2411,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 +2567,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)
|
||||
|
@ -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"] }
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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(
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -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());
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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};
|
||||
@ -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,17 @@ 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| {
|
||||
dbg!("OLA!");
|
||||
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
|
||||
});
|
||||
@ -1485,7 +1486,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 +1572,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 +1673,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 +2911,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());
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -1,4 +1,7 @@
|
||||
use gpui::{div, white, Div, ParentComponent, Render, Styled, View, VisualContext, WindowContext};
|
||||
use gpui::{
|
||||
blue, div, red, white, Div, ParentComponent, Render, Styled, View, VisualContext, WindowContext,
|
||||
};
|
||||
use ui::v_stack;
|
||||
|
||||
pub struct TextStory;
|
||||
|
||||
@ -12,10 +15,46 @@ impl Render for TextStory {
|
||||
type Element = Div<Self>;
|
||||
|
||||
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,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,6 +56,7 @@ fn main() {
|
||||
}
|
||||
|
||||
struct TestView {
|
||||
#[allow(unused)]
|
||||
story: AnyView,
|
||||
}
|
||||
|
||||
@ -65,9 +66,22 @@ impl Render for TestView {
|
||||
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.",
|
||||
))),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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, 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,
|
||||
};
|
||||
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, ListEntry},
|
||||
CloseActiveItem, NewCenterTerminal, Pane, ToolbarItemLocation, Workspace, WorkspaceId,
|
||||
};
|
||||
|
||||
@ -85,7 +84,7 @@ pub struct TerminalView {
|
||||
has_new_content: bool,
|
||||
//Currently using iTerm bell, show bell emoji in tab until input is received
|
||||
has_bell: bool,
|
||||
context_menu: Option<View<ContextMenu>>,
|
||||
context_menu: Option<View<ContextMenu<Self>>>,
|
||||
blink_state: bool,
|
||||
blinking_on: bool,
|
||||
blinking_paused: bool,
|
||||
@ -300,11 +299,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(ListEntry::new(Label::new("Clear")), Box::new(Clear))
|
||||
.action(
|
||||
ListEntry::new(Label::new("Close")),
|
||||
Box::new(CloseActiveItem { save_intent: None }),
|
||||
)
|
||||
}));
|
||||
@ -756,7 +754,7 @@ impl Item for TerminalView {
|
||||
let title = self.terminal().read(cx).title();
|
||||
|
||||
div()
|
||||
.child(img().uri("icons/terminal.svg").bg(red()))
|
||||
.child(IconElement::new(Icon::Terminal))
|
||||
.child(title)
|
||||
.render()
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -130,7 +130,7 @@ impl Theme {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct DiagnosticStyle {
|
||||
pub error: Hsla,
|
||||
pub warning: Hsla,
|
||||
|
@ -18,5 +18,5 @@ theme2 = { path = "../theme2" }
|
||||
rand = "0.8"
|
||||
|
||||
[features]
|
||||
default = ["stories"]
|
||||
default = []
|
||||
stories = ["dep:itertools"]
|
||||
|
@ -1,6 +1,6 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use gpui::{div, DefiniteLength, Hsla, MouseButton, StatefulInteractiveComponent, WindowContext};
|
||||
use gpui::{DefiniteLength, Hsla, MouseButton, StatefulInteractiveComponent, WindowContext};
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::{h_stack, Icon, IconButton, IconElement, Label, LineHeightStyle, TextColor};
|
||||
|
@ -4,57 +4,91 @@ use std::rc::Rc;
|
||||
use crate::prelude::*;
|
||||
use crate::{v_stack, Label, List, ListEntry, 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, DispatchPhase, Div,
|
||||
EventEmitter, FocusHandle, FocusableView, LayoutId, ManagedView, Manager, MouseButton,
|
||||
MouseDownEvent, Pixels, Point, Render, View, VisualContext, WeakView,
|
||||
};
|
||||
|
||||
pub struct ContextMenu {
|
||||
items: Vec<ListItem>,
|
||||
focus_handle: FocusHandle,
|
||||
pub enum ContextMenuItem<V> {
|
||||
Separator(ListSeparator),
|
||||
Header(ListSubHeader),
|
||||
Entry(
|
||||
ListEntry<ContextMenu<V>>,
|
||||
Rc<dyn Fn(&mut V, &mut ViewContext<V>)>,
|
||||
),
|
||||
}
|
||||
|
||||
impl ManagedView for ContextMenu {
|
||||
fn focus_handle(&self, cx: &gpui::AppContext) -> FocusHandle {
|
||||
pub struct ContextMenu<V> {
|
||||
items: Vec<ContextMenuItem<V>>,
|
||||
focus_handle: FocusHandle,
|
||||
handle: WeakView<V>,
|
||||
}
|
||||
|
||||
impl<V: Render> FocusableView for ContextMenu<V> {
|
||||
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl ContextMenu {
|
||||
pub fn new(cx: &mut WindowContext) -> Self {
|
||||
Self {
|
||||
items: Default::default(),
|
||||
focus_handle: cx.focus_handle(),
|
||||
}
|
||||
impl<V: Render> EventEmitter<Manager> for ContextMenu<V> {}
|
||||
|
||||
impl<V: Render> ContextMenu<V> {
|
||||
pub fn build(
|
||||
cx: &mut ViewContext<V>,
|
||||
f: impl FnOnce(Self, &mut ViewContext<Self>) -> Self,
|
||||
) -> View<Self> {
|
||||
let handle = cx.view().downgrade();
|
||||
cx.build_view(|cx| {
|
||||
f(
|
||||
Self {
|
||||
handle,
|
||||
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: ListEntry<Self>,
|
||||
on_click: impl Fn(&mut V, &mut ViewContext<V>) + 'static,
|
||||
) -> Self {
|
||||
self.items
|
||||
.push(ContextMenuItem::Entry(view, Rc::new(on_click)));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn action(self, view: ListEntry<Self>, 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 {
|
||||
impl<V: Render> Render for ContextMenu<V> {
|
||||
type Element = Div<Self>;
|
||||
// todo!()
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
div().elevation_2(cx).flex().flex_row().child(
|
||||
v_stack()
|
||||
@ -71,7 +105,25 @@ impl Render for ContextMenu {
|
||||
// .bg(cx.theme().colors().elevated_surface_background)
|
||||
// .border()
|
||||
// .border_color(cx.theme().colors().border)
|
||||
.child(List::new(self.items.clone())),
|
||||
.child(List::new(
|
||||
self.items
|
||||
.iter()
|
||||
.map(|item| match item {
|
||||
ContextMenuItem::Separator(separator) => {
|
||||
ListItem::Separator(separator.clone())
|
||||
}
|
||||
ContextMenuItem::Header(header) => ListItem::Header(header.clone()),
|
||||
ContextMenuItem::Entry(entry, callback) => {
|
||||
let callback = callback.clone();
|
||||
let handle = self.handle.clone();
|
||||
ListItem::Entry(entry.clone().on_click(move |this, cx| {
|
||||
handle.update(cx, |view, cx| callback(view, cx)).ok();
|
||||
cx.emit(Manager::Dismiss);
|
||||
}))
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
)),
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -226,12 +278,13 @@ impl<V: 'static, M: ManagedView> Element<V> for MenuHandle<V, M> {
|
||||
let new_menu = (builder)(view_state, cx);
|
||||
let menu2 = menu.clone();
|
||||
cx.subscribe(&new_menu, move |this, modal, e, cx| match e {
|
||||
&Dismiss => {
|
||||
&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() {
|
||||
@ -260,16 +313,25 @@ pub use stories::*;
|
||||
mod stories {
|
||||
use super::*;
|
||||
use crate::story::Story;
|
||||
use gpui::{actions, Div, Render, VisualContext};
|
||||
use gpui::{actions, Div, Render};
|
||||
|
||||
actions!(PrintCurrentDate);
|
||||
actions!(PrintCurrentDate, PrintBestFood);
|
||||
|
||||
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 build_menu<V: Render>(
|
||||
cx: &mut ViewContext<V>,
|
||||
header: impl Into<SharedString>,
|
||||
) -> View<ContextMenu<V>> {
|
||||
let handle = cx.view().clone();
|
||||
ContextMenu::build(cx, |menu, _| {
|
||||
menu.header(header)
|
||||
.separator()
|
||||
.entry(ListEntry::new(Label::new("Print current time")), |v, cx| {
|
||||
println!("dispatching PrintCurrentTime action");
|
||||
cx.dispatch_action(PrintCurrentDate.boxed_clone())
|
||||
})
|
||||
.entry(ListEntry::new(Label::new("Print best food")), |v, cx| {
|
||||
cx.dispatch_action(PrintBestFood.boxed_clone())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@ -281,10 +343,14 @@ mod stories {
|
||||
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()
|
||||
|
@ -16,8 +16,12 @@ pub enum Icon {
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
ArrowUpRight,
|
||||
AtSign,
|
||||
AudioOff,
|
||||
AudioOn,
|
||||
Bell,
|
||||
BellOff,
|
||||
BellRing,
|
||||
Bolt,
|
||||
Check,
|
||||
ChevronDown,
|
||||
@ -26,12 +30,14 @@ pub enum Icon {
|
||||
ChevronUp,
|
||||
Close,
|
||||
Collab,
|
||||
Copilot,
|
||||
Dash,
|
||||
Exit,
|
||||
Envelope,
|
||||
ExclamationTriangle,
|
||||
Exit,
|
||||
File,
|
||||
FileGeneric,
|
||||
FileDoc,
|
||||
FileGeneric,
|
||||
FileGit,
|
||||
FileLock,
|
||||
FileRust,
|
||||
@ -44,6 +50,7 @@ pub enum Icon {
|
||||
InlayHint,
|
||||
MagicWand,
|
||||
MagnifyingGlass,
|
||||
MailOpen,
|
||||
Maximize,
|
||||
Menu,
|
||||
MessageBubbles,
|
||||
@ -59,13 +66,6 @@ pub enum Icon {
|
||||
SplitMessage,
|
||||
Terminal,
|
||||
XCircle,
|
||||
Copilot,
|
||||
Envelope,
|
||||
Bell,
|
||||
BellOff,
|
||||
BellRing,
|
||||
MailOpen,
|
||||
AtSign,
|
||||
WholeWord,
|
||||
CaseSensitive,
|
||||
}
|
||||
@ -77,8 +77,12 @@ 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::Check => "icons/check.svg",
|
||||
Icon::ChevronDown => "icons/chevron_down.svg",
|
||||
@ -87,12 +91,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",
|
||||
@ -105,6 +111,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",
|
||||
|
@ -111,7 +111,10 @@ impl<V: 'static> IconButton<V> {
|
||||
.p_1()
|
||||
.bg(bg_color)
|
||||
.cursor_pointer()
|
||||
.hover(|style| style.bg(bg_hover_color))
|
||||
// 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));
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
use gpui::Action;
|
||||
use gpui::{actions, Action};
|
||||
use strum::EnumIter;
|
||||
|
||||
use crate::prelude::*;
|
||||
@ -74,24 +74,24 @@ pub enum ModifierKey {
|
||||
Shift,
|
||||
}
|
||||
|
||||
actions!(NoAction);
|
||||
|
||||
pub fn binding(key: &str) -> gpui::KeyBinding {
|
||||
gpui::KeyBinding::new(key, NoAction {}, None)
|
||||
}
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
pub use stories::*;
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
mod stories {
|
||||
use super::*;
|
||||
use crate::Story;
|
||||
use gpui::{actions, Div, Render};
|
||||
pub use crate::KeyBinding;
|
||||
use crate::{binding, Story};
|
||||
use gpui::{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>;
|
||||
|
||||
|
@ -1,4 +1,6 @@
|
||||
use gpui::{div, Action};
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::{div, Div, Stateful, StatefulInteractiveComponent};
|
||||
|
||||
use crate::settings::user_settings;
|
||||
use crate::{
|
||||
@ -172,35 +174,35 @@ pub enum ListEntrySize {
|
||||
Medium,
|
||||
}
|
||||
|
||||
#[derive(Component, Clone)]
|
||||
pub enum ListItem {
|
||||
Entry(ListEntry),
|
||||
#[derive(Clone)]
|
||||
pub enum ListItem<V: 'static> {
|
||||
Entry(ListEntry<V>),
|
||||
Separator(ListSeparator),
|
||||
Header(ListSubHeader),
|
||||
}
|
||||
|
||||
impl From<ListEntry> for ListItem {
|
||||
fn from(entry: ListEntry) -> Self {
|
||||
impl<V: 'static> From<ListEntry<V>> for ListItem<V> {
|
||||
fn from(entry: ListEntry<V>) -> Self {
|
||||
Self::Entry(entry)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ListSeparator> for ListItem {
|
||||
impl<V: 'static> From<ListSeparator> for ListItem<V> {
|
||||
fn from(entry: ListSeparator) -> Self {
|
||||
Self::Separator(entry)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ListSubHeader> for ListItem {
|
||||
impl<V: 'static> From<ListSubHeader> for ListItem<V> {
|
||||
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> {
|
||||
impl<V: 'static> ListItem<V> {
|
||||
fn render(self, view: &mut V, ix: usize, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
match self {
|
||||
ListItem::Entry(entry) => div().child(entry.render(view, cx)),
|
||||
ListItem::Entry(entry) => div().child(entry.render(ix, cx)),
|
||||
ListItem::Separator(separator) => div().child(separator.render(view, cx)),
|
||||
ListItem::Header(header) => div().child(header.render(view, cx)),
|
||||
}
|
||||
@ -210,7 +212,7 @@ impl ListItem {
|
||||
Self::Entry(ListEntry::new(label))
|
||||
}
|
||||
|
||||
pub fn as_entry(&mut self) -> Option<&mut ListEntry> {
|
||||
pub fn as_entry(&mut self) -> Option<&mut ListEntry<V>> {
|
||||
if let Self::Entry(entry) = self {
|
||||
Some(entry)
|
||||
} else {
|
||||
@ -219,8 +221,7 @@ impl ListItem {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct ListEntry {
|
||||
pub struct ListEntry<V> {
|
||||
disabled: bool,
|
||||
// TODO: Reintroduce this
|
||||
// disclosure_control_style: DisclosureControlVisibility,
|
||||
@ -231,15 +232,13 @@ pub struct ListEntry {
|
||||
size: ListEntrySize,
|
||||
toggle: Toggle,
|
||||
variant: ListItemVariant,
|
||||
on_click: Option<Box<dyn Action>>,
|
||||
on_click: Option<Rc<dyn Fn(&mut V, &mut ViewContext<V>) + 'static>>,
|
||||
}
|
||||
|
||||
impl Clone for ListEntry {
|
||||
impl<V> Clone for ListEntry<V> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
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,12 +246,12 @@ 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 {
|
||||
impl<V: 'static> ListEntry<V> {
|
||||
pub fn new(label: Label) -> Self {
|
||||
Self {
|
||||
disabled: false,
|
||||
@ -267,8 +266,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(&mut V, &mut ViewContext<V>) + 'static) -> Self {
|
||||
self.on_click = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
|
||||
@ -307,7 +306,7 @@ impl ListEntry {
|
||||
self
|
||||
}
|
||||
|
||||
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
fn render(self, ix: usize, cx: &mut ViewContext<V>) -> Stateful<V, Div<V>> {
|
||||
let settings = user_settings(cx);
|
||||
|
||||
let left_content = match self.left_slot.clone() {
|
||||
@ -328,21 +327,21 @@ impl ListEntry {
|
||||
ListEntrySize::Medium => div().h_7(),
|
||||
};
|
||||
div()
|
||||
.id(ix)
|
||||
.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());
|
||||
.on_click({
|
||||
let on_click = self.on_click.clone();
|
||||
|
||||
move |entry: &mut V, event, cx| {
|
||||
if let Some(action) = action.as_ref() {
|
||||
cx.dispatch_action(action.boxed_clone());
|
||||
move |view: &mut V, event, cx| {
|
||||
if let Some(on_click) = &on_click {
|
||||
(on_click)(view, cx)
|
||||
}
|
||||
}
|
||||
})
|
||||
.group("")
|
||||
.bg(cx.theme().colors().surface_background)
|
||||
// TODO: Add focus state
|
||||
// .when(self.state == InteractionState::Focused, |this| {
|
||||
@ -391,8 +390,8 @@ impl ListSeparator {
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct List {
|
||||
items: Vec<ListItem>,
|
||||
pub struct List<V: 'static> {
|
||||
items: Vec<ListItem<V>>,
|
||||
/// Message to display when the list is empty
|
||||
/// Defaults to "No items"
|
||||
empty_message: SharedString,
|
||||
@ -400,8 +399,8 @@ pub struct List {
|
||||
toggle: Toggle,
|
||||
}
|
||||
|
||||
impl List {
|
||||
pub fn new(items: Vec<ListItem>) -> Self {
|
||||
impl<V: 'static> List<V> {
|
||||
pub fn new(items: Vec<ListItem<V>>) -> Self {
|
||||
Self {
|
||||
items,
|
||||
empty_message: "No items".into(),
|
||||
@ -425,9 +424,14 @@ impl List {
|
||||
self
|
||||
}
|
||||
|
||||
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>) -> impl Component<V> {
|
||||
let list_content = match (self.items.is_empty(), self.toggle) {
|
||||
(false, _) => div().children(self.items),
|
||||
(false, _) => div().children(
|
||||
self.items
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(ix, item)| item.render(view, ix, cx)),
|
||||
),
|
||||
(true, Toggle::Toggled(false)) => div(),
|
||||
(true, _) => {
|
||||
div().child(Label::new(self.empty_message.clone()).color(TextColor::Muted))
|
||||
|
@ -24,6 +24,7 @@ mod to_extract;
|
||||
pub mod utils;
|
||||
|
||||
pub use components::*;
|
||||
use gpui::actions;
|
||||
pub use prelude::*;
|
||||
pub use static_data::*;
|
||||
pub use styled_ext::*;
|
||||
@ -42,3 +43,8 @@ pub use crate::settings::*;
|
||||
mod story;
|
||||
#[cfg(feature = "stories")]
|
||||
pub use story::*;
|
||||
actions!(NoAction);
|
||||
|
||||
pub fn binding(key: &str) -> gpui::KeyBinding {
|
||||
gpui::KeyBinding::new(key, NoAction {}, None)
|
||||
}
|
||||
|
@ -478,7 +478,7 @@ pub fn static_new_notification_items_2<V: 'static>() -> Vec<Notification<V>> {
|
||||
]
|
||||
}
|
||||
|
||||
pub fn static_project_panel_project_items() -> Vec<ListItem> {
|
||||
pub fn static_project_panel_project_items<V>() -> Vec<ListItem<V>> {
|
||||
vec![
|
||||
ListEntry::new(Label::new("zed"))
|
||||
.left_icon(Icon::FolderOpen.into())
|
||||
@ -605,7 +605,7 @@ pub fn static_project_panel_project_items() -> Vec<ListItem> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn static_project_panel_single_items() -> Vec<ListItem> {
|
||||
pub fn static_project_panel_single_items<V>() -> Vec<ListItem<V>> {
|
||||
vec![
|
||||
ListEntry::new(Label::new("todo.md"))
|
||||
.left_icon(Icon::FileDoc.into())
|
||||
@ -622,7 +622,7 @@ pub fn static_project_panel_single_items() -> Vec<ListItem> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn static_collab_panel_current_call() -> Vec<ListItem> {
|
||||
pub fn static_collab_panel_current_call<V>() -> Vec<ListItem<V>> {
|
||||
vec![
|
||||
ListEntry::new(Label::new("as-cii")).left_avatar("http://github.com/as-cii.png?s=50"),
|
||||
ListEntry::new(Label::new("nathansobo"))
|
||||
@ -635,7 +635,7 @@ pub fn static_collab_panel_current_call() -> Vec<ListItem> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn static_collab_panel_channels() -> Vec<ListItem> {
|
||||
pub fn static_collab_panel_channels<V>() -> Vec<ListItem<V>> {
|
||||
vec![
|
||||
ListEntry::new(Label::new("zed"))
|
||||
.left_icon(Icon::Hash.into())
|
||||
|
@ -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:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,9 @@ 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, ListEntry, 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,
|
||||
}
|
||||
}
|
||||
PanelEvent::Focus => todo!(),
|
||||
.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!(),
|
||||
}),
|
||||
];
|
||||
|
||||
@ -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))
|
||||
}
|
||||
|
||||
@ -620,6 +670,7 @@ impl Render for PanelButtons {
|
||||
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,9 +683,10 @@ 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 action = dock.toggle_action();
|
||||
@ -645,7 +697,7 @@ impl Render for PanelButtons {
|
||||
.action(action.boxed_clone())
|
||||
.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())
|
||||
@ -656,7 +708,30 @@ impl Render for PanelButtons {
|
||||
menu_handle()
|
||||
.id(name)
|
||||
.menu(move |_, cx| {
|
||||
cx.build_view(|cx| ContextMenu::new(cx).header("SECTION"))
|
||||
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(
|
||||
ListEntry::new(Label::new(format!(
|
||||
"Dock {}",
|
||||
position.to_label()
|
||||
))),
|
||||
move |_, cx| {
|
||||
panel.set_position(position, cx);
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
menu
|
||||
})
|
||||
})
|
||||
.anchor(menu_anchor)
|
||||
.attach(menu_attach)
|
||||
|
@ -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};
|
||||
@ -359,7 +361,6 @@ pub mod simple_message_notification {
|
||||
// }
|
||||
|
||||
impl EventEmitter<NotificationEvent> for MessageNotification {}
|
||||
impl Notification for MessageNotification {}
|
||||
}
|
||||
|
||||
pub trait NotifyResultExt {
|
||||
|
@ -24,6 +24,7 @@ use std::{
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
|
||||
use ui::v_stack;
|
||||
use ui::{prelude::*, Icon, IconButton, IconElement, TextColor, Tooltip};
|
||||
use util::truncate_and_remove_front;
|
||||
@ -1480,15 +1481,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()
|
||||
@ -1933,7 +1929,7 @@ impl Render for Pane {
|
||||
.child(self.render_tab_bar(cx))
|
||||
.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")
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user