Merge branch 'main' into derive-element-redux

This commit is contained in:
Conrad Irwin 2023-11-20 09:15:38 -07:00
commit 0798cfd58c
117 changed files with 7260 additions and 2951 deletions

View 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
View 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

View file

@ -23,19 +23,14 @@ jobs:
- self-hosted - self-hosted
- test - test
steps: steps:
- name: Install Rust
run: |
rustup set profile minimal
rustup update stable
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
clean: false clean: false
submodules: "recursive" submodules: "recursive"
- name: cargo fmt - name: Run rustfmt
run: cargo fmt --all -- --check uses: ./.github/actions/check_formatting
tests: tests:
name: Run tests name: Run tests
@ -43,35 +38,15 @@ jobs:
- self-hosted - self-hosted
- test - test
needs: rustfmt needs: rustfmt
env:
RUSTFLAGS: -D warnings
steps: 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 - name: Checkout repo
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
clean: false clean: false
submodules: "recursive" 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 - name: Run tests
run: cargo nextest run --workspace --no-fail-fast uses: ./.github/actions/run_tests
- name: Build collab - name: Build collab
run: cargo build -p collab run: cargo build -p collab
@ -130,6 +105,8 @@ jobs:
expected_tag_name="v${version}";; expected_tag_name="v${version}";;
preview) preview)
expected_tag_name="v${version}-pre";; expected_tag_name="v${version}-pre";;
nightly)
expected_tag_name="v${version}-nightly";;
*) *)
echo "can't publish a release on channel ${channel}" echo "can't publish a release on channel ${channel}"
exit 1;; exit 1;;
@ -154,7 +131,9 @@ jobs:
- uses: softprops/action-gh-release@v1 - uses: softprops/action-gh-release@v1
name: Upload app bundle to release 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: with:
draft: true draft: true
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }} prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}

98
.github/workflows/release_nightly.yml vendored Normal file
View 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
View file

@ -724,6 +724,30 @@ dependencies = [
"workspace", "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]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.1.0" version = "1.1.0"
@ -1526,6 +1550,7 @@ dependencies = [
"anyhow", "anyhow",
"async-recursion 0.3.2", "async-recursion 0.3.2",
"async-tungstenite", "async-tungstenite",
"chrono",
"collections", "collections",
"db", "db",
"feature_flags", "feature_flags",
@ -1562,6 +1587,7 @@ dependencies = [
"anyhow", "anyhow",
"async-recursion 0.3.2", "async-recursion 0.3.2",
"async-tungstenite", "async-tungstenite",
"chrono",
"collections", "collections",
"db2", "db2",
"feature_flags2", "feature_flags2",
@ -2614,6 +2640,34 @@ dependencies = [
"workspace", "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]] [[package]]
name = "diff" name = "diff"
version = "0.1.13" version = "0.1.13"
@ -3759,7 +3813,7 @@ dependencies = [
"smol", "smol",
"sqlez", "sqlez",
"sum_tree", "sum_tree",
"taffy", "taffy 0.3.11 (git+https://github.com/DioxusLabs/taffy?rev=4fb530bdd71609bb1d3f76c6a8bde1ba82805d5e)",
"thiserror", "thiserror",
"time", "time",
"tiny-skia", "tiny-skia",
@ -3824,7 +3878,7 @@ dependencies = [
"smol", "smol",
"sqlez", "sqlez",
"sum_tree", "sum_tree",
"taffy", "taffy 0.3.11 (git+https://github.com/DioxusLabs/taffy?rev=1876f72bee5e376023eaa518aa7b8a34c769bd1b)",
"thiserror", "thiserror",
"time", "time",
"tiny-skia", "tiny-skia",
@ -3859,6 +3913,12 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eec1c01eb1de97451ee0d60de7d81cf1e72aabefb021616027f3d1c3ec1c723c" checksum = "eec1c01eb1de97451ee0d60de7d81cf1e72aabefb021616027f3d1c3ec1c723c"
[[package]]
name = "grid"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1df00eed8d1f0db937f6be10e46e8072b0671accb504cf0f959c5c52c679f5b9"
[[package]] [[package]]
name = "h2" name = "h2"
version = "0.3.21" version = "0.3.21"
@ -4486,7 +4546,7 @@ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
"dirs 4.0.0", "dirs 4.0.0",
"editor", "editor2",
"gpui2", "gpui2",
"log", "log",
"schemars", "schemars",
@ -9053,13 +9113,24 @@ dependencies = [
"winx", "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]] [[package]]
name = "taffy" name = "taffy"
version = "0.3.11" version = "0.3.11"
source = "git+https://github.com/DioxusLabs/taffy?rev=4fb530bdd71609bb1d3f76c6a8bde1ba82805d5e#4fb530bdd71609bb1d3f76c6a8bde1ba82805d5e" source = "git+https://github.com/DioxusLabs/taffy?rev=4fb530bdd71609bb1d3f76c6a8bde1ba82805d5e#4fb530bdd71609bb1d3f76c6a8bde1ba82805d5e"
dependencies = [ dependencies = [
"arrayvec 0.7.4", "arrayvec 0.7.4",
"grid", "grid 0.10.0",
"num-traits", "num-traits",
"slotmap", "slotmap",
] ]
@ -11543,6 +11614,7 @@ dependencies = [
"async-recursion 0.3.2", "async-recursion 0.3.2",
"async-tar", "async-tar",
"async-trait", "async-trait",
"auto_update2",
"backtrace", "backtrace",
"call2", "call2",
"chrono", "chrono",
@ -11554,6 +11626,7 @@ dependencies = [
"copilot2", "copilot2",
"ctor", "ctor",
"db2", "db2",
"diagnostics2",
"editor2", "editor2",
"env_logger 0.9.3", "env_logger 0.9.3",
"feature_flags2", "feature_flags2",
@ -11571,7 +11644,6 @@ dependencies = [
"isahc", "isahc",
"journal2", "journal2",
"language2", "language2",
"language_tools",
"lazy_static", "lazy_static",
"libc", "libc",
"log", "log",

View file

@ -6,6 +6,7 @@ members = [
"crates/audio", "crates/audio",
"crates/audio2", "crates/audio2",
"crates/auto_update", "crates/auto_update",
"crates/auto_update2",
"crates/breadcrumbs", "crates/breadcrumbs",
"crates/call", "crates/call",
"crates/call2", "crates/call2",
@ -32,6 +33,7 @@ members = [
"crates/refineable", "crates/refineable",
"crates/refineable/derive_refineable", "crates/refineable/derive_refineable",
"crates/diagnostics", "crates/diagnostics",
"crates/diagnostics2",
"crates/drag_and_drop", "crates/drag_and_drop",
"crates/editor", "crates/editor",
"crates/feature_flags", "crates/feature_flags",

View file

@ -1,6 +1 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/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>
<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>

Before

Width:  |  Height:  |  Size: 835 B

After

Width:  |  Height:  |  Size: 351 B

Before After
Before After

View file

@ -268,6 +268,19 @@
// Whether to show warnings or not by default. // Whether to show warnings or not by default.
"include_warnings": true "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 gutter behavior configuration.
"git": { "git": {
// Control whether the git gutter is shown. May take 2 values: // Control whether the git gutter is shown. May take 2 values:

View file

@ -15,7 +15,7 @@ use ai::{
use ai::prompts::repository_context::PromptCodeSnippet; use ai::prompts::repository_context::PromptCodeSnippet;
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use chrono::{DateTime, Local}; use chrono::{DateTime, Local};
use client::{telemetry::AssistantKind, ClickhouseEvent, TelemetrySettings}; use client::{telemetry::AssistantKind, TelemetrySettings};
use collections::{hash_map, HashMap, HashSet, VecDeque}; use collections::{hash_map, HashMap, HashSet, VecDeque};
use editor::{ use editor::{
display_map::{ display_map::{
@ -3803,12 +3803,12 @@ fn report_assistant_event(
.default_open_ai_model .default_open_ai_model
.clone(); .clone();
let event = ClickhouseEvent::Assistant {
conversation_id,
kind: assistant_kind,
model: model.full_name(),
};
let telemetry_settings = *settings::get::<TelemetrySettings>(cx); 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(),
)
} }

View file

@ -118,14 +118,18 @@ fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) {
let auto_updater = auto_updater.read(cx); let auto_updater = auto_updater.read(cx);
let server_url = &auto_updater.server_url; let server_url = &auto_updater.server_url;
let current_version = auto_updater.current_version; let current_version = auto_updater.current_version;
let latest_release_url = if cx.has_global::<ReleaseChannel>() if cx.has_global::<ReleaseChannel>() {
&& *cx.global::<ReleaseChannel>() == ReleaseChannel::Preview match cx.global::<ReleaseChannel>() {
{ ReleaseChannel::Dev => {}
format!("{server_url}/releases/preview/{current_version}") ReleaseChannel::Nightly => {}
} else { ReleaseChannel::Preview => cx
format!("{server_url}/releases/stable/{current_version}") .platform()
}; .open_url(&format!("{server_url}/releases/preview/{current_version}")),
cx.platform().open_url(&latest_release_url); 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.has_global::<ReleaseChannel>() {
if *cx.global::<ReleaseChannel>() == ReleaseChannel::Preview { if let Some(param) = cx.global::<ReleaseChannel>().release_query_param() {
return "&preview=1"; url_string += "&";
url_string += param;
} }
} }
""
}); });
let mut response = client let mut response = client.get(&url_string, Default::default(), true).await?;
.get(
&format!("{server_url}/api/releases/latest?token={ZED_SECRET_CLIENT_TOKEN}&asset=Zed.dmg{preview_param}"),
Default::default(),
true,
)
.await?;
let mut body = Vec::new(); let mut body = Vec::new();
response response

View 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

View 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())
})
}
}

View 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);
}
}

View file

@ -5,10 +5,7 @@ pub mod room;
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use audio::Audio; use audio::Audio;
use call_settings::CallSettings; use call_settings::CallSettings;
use client::{ use client::{proto, Client, TelemetrySettings, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE};
proto, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore,
ZED_ALWAYS_ACTIVE,
};
use collections::HashSet; use collections::HashSet;
use futures::{channel::oneshot, future::Shared, Future, FutureExt}; use futures::{channel::oneshot, future::Shared, Future, FutureExt};
use gpui::{ use gpui::{
@ -485,12 +482,8 @@ pub fn report_call_event_for_room(
) { ) {
let telemetry = client.telemetry(); let telemetry = client.telemetry();
let telemetry_settings = *settings::get::<TelemetrySettings>(cx); let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
let event = ClickhouseEvent::Call {
operation, telemetry.report_call_event(telemetry_settings, operation, Some(room_id), channel_id)
room_id: Some(room_id),
channel_id,
};
telemetry.report_clickhouse_event(event, telemetry_settings);
} }
pub fn report_call_event_for_channel( pub fn report_call_event_for_channel(
@ -504,12 +497,12 @@ pub fn report_call_event_for_channel(
let telemetry = client.telemetry(); let telemetry = client.telemetry();
let telemetry_settings = *settings::get::<TelemetrySettings>(cx); let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
let event = ClickhouseEvent::Call { telemetry.report_call_event(
telemetry_settings,
operation, operation,
room_id: room.map(|r| r.read(cx).id()), room.map(|r| r.read(cx).id()),
channel_id: Some(channel_id), Some(channel_id),
}; )
telemetry.report_clickhouse_event(event, telemetry_settings);
} }
#[cfg(test)] #[cfg(test)]

View file

@ -5,10 +5,7 @@ pub mod room;
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use audio::Audio; use audio::Audio;
use call_settings::CallSettings; use call_settings::CallSettings;
use client::{ use client::{proto, Client, TelemetrySettings, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE};
proto, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore,
ZED_ALWAYS_ACTIVE,
};
use collections::HashSet; use collections::HashSet;
use futures::{channel::oneshot, future::Shared, Future, FutureExt}; use futures::{channel::oneshot, future::Shared, Future, FutureExt};
use gpui::{ use gpui::{
@ -484,12 +481,8 @@ pub fn report_call_event_for_room(
) { ) {
let telemetry = client.telemetry(); let telemetry = client.telemetry();
let telemetry_settings = *TelemetrySettings::get_global(cx); let telemetry_settings = *TelemetrySettings::get_global(cx);
let event = ClickhouseEvent::Call {
operation, telemetry.report_call_event(telemetry_settings, operation, Some(room_id), channel_id)
room_id: Some(room_id),
channel_id,
};
telemetry.report_clickhouse_event(event, telemetry_settings);
} }
pub fn report_call_event_for_channel( 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 telemetry_settings = *TelemetrySettings::get_global(cx);
let event = ClickhouseEvent::Call { telemetry.report_call_event(
telemetry_settings,
operation, operation,
room_id: room.map(|r| r.read(cx).id()), room.map(|r| r.read(cx).id()),
channel_id: Some(channel_id), Some(channel_id),
}; )
telemetry.report_clickhouse_event(event, telemetry_settings);
} }
#[cfg(test)] #[cfg(test)]

View file

@ -12,6 +12,7 @@ doctest = false
test-support = ["collections/test-support", "gpui/test-support", "rpc/test-support"] test-support = ["collections/test-support", "gpui/test-support", "rpc/test-support"]
[dependencies] [dependencies]
chrono = { version = "0.4", features = ["serde"] }
collections = { path = "../collections" } collections = { path = "../collections" }
db = { path = "../db" } db = { path = "../db" }
gpui = { path = "../gpui" } gpui = { path = "../gpui" }

View file

@ -987,9 +987,17 @@ impl Client {
self.establish_websocket_connection(credentials, cx) self.establish_websocket_connection(credentials, cx)
} }
async fn get_rpc_url(http: Arc<dyn HttpClient>, is_preview: bool) -> Result<Url> { async fn get_rpc_url(
let preview_param = if is_preview { "?preview=1" } else { "" }; http: Arc<dyn HttpClient>,
let url = format!("{}/rpc{preview_param}", *ZED_SERVER_URL); 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?; let response = http.get(&url, Default::default(), false).await?;
// Normally, ZED_SERVER_URL is set to the URL of zed.dev website. // Normally, ZED_SERVER_URL is set to the URL of zed.dev website.
@ -1024,11 +1032,11 @@ impl Client {
credentials: &Credentials, credentials: &Credentials,
cx: &AsyncAppContext, cx: &AsyncAppContext,
) -> Task<Result<Connection, EstablishConnectionError>> { ) -> Task<Result<Connection, EstablishConnectionError>> {
let use_preview_server = cx.read(|cx| { let release_channel = cx.read(|cx| {
if cx.has_global::<ReleaseChannel>() { if cx.has_global::<ReleaseChannel>() {
*cx.global::<ReleaseChannel>() != ReleaseChannel::Stable Some(*cx.global::<ReleaseChannel>())
} else { } else {
false None
} }
}); });
@ -1041,7 +1049,7 @@ impl Client {
let http = self.http.clone(); let http = self.http.clone();
cx.background().spawn(async move { 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 let rpc_host = rpc_url
.host_str() .host_str()
.zip(rpc_url.port_or_known_default()) .zip(rpc_url.port_or_known_default())
@ -1191,7 +1199,7 @@ impl Client {
// Use the collab server's admin API to retrieve the id // Use the collab server's admin API to retrieve the id
// of the impersonated user. // 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_path("/user");
url.set_query(Some(&format!("github_login={login}"))); url.set_query(Some(&format!("github_login={login}")));
let request = Request::get(url.as_str()) let request = Request::get(url.as_str())

View file

@ -1,4 +1,5 @@
use crate::{TelemetrySettings, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL}; use crate::{TelemetrySettings, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
use chrono::{DateTime, Utc};
use gpui::{executor::Background, serde_json, AppContext, Task}; use gpui::{executor::Background, serde_json, AppContext, Task};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use parking_lot::Mutex; use parking_lot::Mutex;
@ -20,7 +21,7 @@ pub struct Telemetry {
#[derive(Default)] #[derive(Default)]
struct TelemetryState { struct TelemetryState {
metrics_id: Option<Arc<str>>, // Per logged-in user 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 session_id: Option<Arc<str>>, // Per app launch
app_version: Option<Arc<str>>, app_version: Option<Arc<str>>,
release_channel: Option<&'static str>, release_channel: Option<&'static str>,
@ -31,6 +32,7 @@ struct TelemetryState {
flush_clickhouse_events_task: Option<Task<()>>, flush_clickhouse_events_task: Option<Task<()>>,
log_file: Option<NamedTempFile>, log_file: Option<NamedTempFile>,
is_staff: Option<bool>, is_staff: Option<bool>,
first_event_datetime: Option<DateTime<Utc>>,
} }
const CLICKHOUSE_EVENTS_URL_PATH: &'static str = "/api/events"; const CLICKHOUSE_EVENTS_URL_PATH: &'static str = "/api/events";
@ -77,29 +79,35 @@ pub enum ClickhouseEvent {
vim_mode: bool, vim_mode: bool,
copilot_enabled: bool, copilot_enabled: bool,
copilot_enabled_for_language: bool, copilot_enabled_for_language: bool,
milliseconds_since_first_event: i64,
}, },
Copilot { Copilot {
suggestion_id: Option<String>, suggestion_id: Option<String>,
suggestion_accepted: bool, suggestion_accepted: bool,
file_extension: Option<String>, file_extension: Option<String>,
milliseconds_since_first_event: i64,
}, },
Call { Call {
operation: &'static str, operation: &'static str,
room_id: Option<u64>, room_id: Option<u64>,
channel_id: Option<u64>, channel_id: Option<u64>,
milliseconds_since_first_event: i64,
}, },
Assistant { Assistant {
conversation_id: Option<String>, conversation_id: Option<String>,
kind: AssistantKind, kind: AssistantKind,
model: &'static str, model: &'static str,
milliseconds_since_first_event: i64,
}, },
Cpu { Cpu {
usage_as_percentage: f32, usage_as_percentage: f32,
core_count: u32, core_count: u32,
milliseconds_since_first_event: i64,
}, },
Memory { Memory {
memory_in_bytes: u64, memory_in_bytes: u64,
virtual_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(), flush_clickhouse_events_task: Default::default(),
log_file: None, log_file: None,
is_staff: None, is_staff: None,
first_event_datetime: None,
}), }),
}); });
@ -195,20 +204,18 @@ impl Telemetry {
return; 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)); let telemetry_settings = cx.update(|cx| *settings::get::<TelemetrySettings>(cx));
this.report_clickhouse_event(memory_event, telemetry_settings); this.report_memory_event(
this.report_clickhouse_event(cpu_event, telemetry_settings); telemetry_settings,
process.memory(),
process.virtual_memory(),
);
this.report_cpu_event(
telemetry_settings,
process.cpu_usage(),
system.cpus().len() as u32,
);
} }
}) })
.detach(); .detach();
@ -231,7 +238,123 @@ impl Telemetry {
drop(state); 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>, self: &Arc<Self>,
event: ClickhouseEvent, event: ClickhouseEvent,
telemetry_settings: TelemetrySettings, telemetry_settings: TelemetrySettings,
@ -275,6 +398,7 @@ impl Telemetry {
fn flush_clickhouse_events(self: &Arc<Self>) { fn flush_clickhouse_events(self: &Arc<Self>) {
let mut state = self.state.lock(); let mut state = self.state.lock();
state.first_event_datetime = None;
let mut events = mem::take(&mut state.clickhouse_events_queue); let mut events = mem::take(&mut state.clickhouse_events_queue);
state.flush_clickhouse_events_task.take(); state.flush_clickhouse_events_task.take();
drop(state); drop(state);

View file

@ -12,6 +12,7 @@ doctest = false
test-support = ["collections/test-support", "gpui/test-support", "rpc/test-support"] test-support = ["collections/test-support", "gpui/test-support", "rpc/test-support"]
[dependencies] [dependencies]
chrono = { version = "0.4", features = ["serde"] }
collections = { path = "../collections" } collections = { path = "../collections" }
db = { package = "db2", path = "../db2" } db = { package = "db2", path = "../db2" }
gpui = { package = "gpui2", path = "../gpui2" } gpui = { package = "gpui2", path = "../gpui2" }

View file

@ -923,9 +923,17 @@ impl Client {
self.establish_websocket_connection(credentials, cx) self.establish_websocket_connection(credentials, cx)
} }
async fn get_rpc_url(http: Arc<dyn HttpClient>, is_preview: bool) -> Result<Url> { async fn get_rpc_url(
let preview_param = if is_preview { "?preview=1" } else { "" }; http: Arc<dyn HttpClient>,
let url = format!("{}/rpc{preview_param}", *ZED_SERVER_URL); 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?; let response = http.get(&url, Default::default(), false).await?;
// Normally, ZED_SERVER_URL is set to the URL of zed.dev website. // Normally, ZED_SERVER_URL is set to the URL of zed.dev website.
@ -960,9 +968,7 @@ impl Client {
credentials: &Credentials, credentials: &Credentials,
cx: &AsyncAppContext, cx: &AsyncAppContext,
) -> Task<Result<Connection, EstablishConnectionError>> { ) -> Task<Result<Connection, EstablishConnectionError>> {
let use_preview_server = cx let release_channel = cx.try_read_global(|channel: &ReleaseChannel, _| *channel);
.try_read_global(|channel: &ReleaseChannel, _| *channel != ReleaseChannel::Stable)
.unwrap_or(false);
let request = Request::builder() let request = Request::builder()
.header( .header(
@ -973,7 +979,7 @@ impl Client {
let http = self.http.clone(); let http = self.http.clone();
cx.background_executor().spawn(async move { 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 let rpc_host = rpc_url
.host_str() .host_str()
.zip(rpc_url.port_or_known_default()) .zip(rpc_url.port_or_known_default())
@ -1120,7 +1126,7 @@ impl Client {
// Use the collab server's admin API to retrieve the id // Use the collab server's admin API to retrieve the id
// of the impersonated user. // 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_path("/user");
url.set_query(Some(&format!("github_login={login}"))); url.set_query(Some(&format!("github_login={login}")));
let request = Request::get(url.as_str()) let request = Request::get(url.as_str())

View file

@ -1,4 +1,5 @@
use crate::{TelemetrySettings, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL}; use crate::{TelemetrySettings, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
use chrono::{DateTime, Utc};
use gpui::{serde_json, AppContext, AppMetadata, BackgroundExecutor, Task}; use gpui::{serde_json, AppContext, AppMetadata, BackgroundExecutor, Task};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use parking_lot::Mutex; use parking_lot::Mutex;
@ -20,7 +21,7 @@ pub struct Telemetry {
struct TelemetryState { struct TelemetryState {
metrics_id: Option<Arc<str>>, // Per logged-in user 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 session_id: Option<Arc<str>>, // Per app launch
release_channel: Option<&'static str>, release_channel: Option<&'static str>,
app_metadata: AppMetadata, app_metadata: AppMetadata,
@ -29,6 +30,7 @@ struct TelemetryState {
flush_clickhouse_events_task: Option<Task<()>>, flush_clickhouse_events_task: Option<Task<()>>,
log_file: Option<NamedTempFile>, log_file: Option<NamedTempFile>,
is_staff: Option<bool>, is_staff: Option<bool>,
first_event_datetime: Option<DateTime<Utc>>,
} }
const CLICKHOUSE_EVENTS_URL_PATH: &'static str = "/api/events"; const CLICKHOUSE_EVENTS_URL_PATH: &'static str = "/api/events";
@ -75,29 +77,35 @@ pub enum ClickhouseEvent {
vim_mode: bool, vim_mode: bool,
copilot_enabled: bool, copilot_enabled: bool,
copilot_enabled_for_language: bool, copilot_enabled_for_language: bool,
milliseconds_since_first_event: i64,
}, },
Copilot { Copilot {
suggestion_id: Option<String>, suggestion_id: Option<String>,
suggestion_accepted: bool, suggestion_accepted: bool,
file_extension: Option<String>, file_extension: Option<String>,
milliseconds_since_first_event: i64,
}, },
Call { Call {
operation: &'static str, operation: &'static str,
room_id: Option<u64>, room_id: Option<u64>,
channel_id: Option<u64>, channel_id: Option<u64>,
milliseconds_since_first_event: i64,
}, },
Assistant { Assistant {
conversation_id: Option<String>, conversation_id: Option<String>,
kind: AssistantKind, kind: AssistantKind,
model: &'static str, model: &'static str,
milliseconds_since_first_event: i64,
}, },
Cpu { Cpu {
usage_as_percentage: f32, usage_as_percentage: f32,
core_count: u32, core_count: u32,
milliseconds_since_first_event: i64,
}, },
Memory { Memory {
memory_in_bytes: u64, memory_in_bytes: u64,
virtual_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(), flush_clickhouse_events_task: Default::default(),
log_file: None, log_file: None,
is_staff: None, is_staff: None,
first_event_datetime: None,
}), }),
}); });
@ -190,16 +199,6 @@ impl Telemetry {
return; 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) = let telemetry_settings = if let Ok(telemetry_settings) =
cx.update(|cx| *TelemetrySettings::get_global(cx)) cx.update(|cx| *TelemetrySettings::get_global(cx))
{ {
@ -208,8 +207,16 @@ impl Telemetry {
break; break;
}; };
this.report_clickhouse_event(memory_event, telemetry_settings); this.report_memory_event(
this.report_clickhouse_event(cpu_event, telemetry_settings); telemetry_settings,
process.memory(),
process.virtual_memory(),
);
this.report_cpu_event(
telemetry_settings,
process.cpu_usage(),
system.cpus().len() as u32,
);
} }
}) })
.detach(); .detach();
@ -232,7 +239,123 @@ impl Telemetry {
drop(state); 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>, self: &Arc<Self>,
event: ClickhouseEvent, event: ClickhouseEvent,
telemetry_settings: TelemetrySettings, telemetry_settings: TelemetrySettings,
@ -276,6 +399,7 @@ impl Telemetry {
fn flush_clickhouse_events(self: &Arc<Self>) { fn flush_clickhouse_events(self: &Arc<Self>) {
let mut state = self.state.lock(); let mut state = self.state.lock();
state.first_event_datetime = None;
let mut events = mem::take(&mut state.clickhouse_events_queue); let mut events = mem::take(&mut state.clickhouse_events_queue);
state.flush_clickhouse_events_task.take(); state.flush_clickhouse_events_task.take();
drop(state); drop(state);

View file

@ -5052,7 +5052,7 @@ async fn test_project_search(
let mut results = HashMap::default(); let mut results = HashMap::default();
let mut search_rx = project_b.update(cx_b, |project, cx| { let mut search_rx = project_b.update(cx_b, |project, cx| {
project.search( project.search(
SearchQuery::text("world", false, false, Vec::new(), Vec::new()).unwrap(), SearchQuery::text("world", false, false, false, Vec::new(), Vec::new()).unwrap(),
cx, cx,
) )
}); });

View file

@ -869,7 +869,8 @@ impl RandomizedTest for ProjectCollaborationTest {
let mut search = project.update(cx, |project, cx| { let mut search = project.update(cx, |project, cx| {
project.search( project.search(
SearchQuery::text(query, false, false, Vec::new(), Vec::new()).unwrap(), SearchQuery::text(query, false, false, false, Vec::new(), Vec::new())
.unwrap(),
cx, cx,
) )
}); });

View file

@ -4599,7 +4599,7 @@ async fn test_project_search(
let mut results = HashMap::default(); let mut results = HashMap::default();
let mut search_rx = project_b.update(cx_b, |project, cx| { let mut search_rx = project_b.update(cx_b, |project, cx| {
project.search( project.search(
SearchQuery::text("world", false, false, Vec::new(), Vec::new()).unwrap(), SearchQuery::text("world", false, false, false, Vec::new(), Vec::new()).unwrap(),
cx, cx,
) )
}); });

View file

@ -870,7 +870,8 @@ impl RandomizedTest for ProjectCollaborationTest {
let mut search = project.update(cx, |project, cx| { let mut search = project.update(cx, |project, cx| {
project.search( project.search(
SearchQuery::text(query, false, false, Vec::new(), Vec::new()).unwrap(), SearchQuery::text(query, false, false, false, Vec::new(), Vec::new())
.unwrap(),
cx, cx,
) )
}); });

View file

@ -14,14 +14,8 @@ use std::{sync::Arc, time::Duration};
const MENTIONS_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(50); const MENTIONS_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(50);
lazy_static! { lazy_static! {
static ref MENTIONS_SEARCH: SearchQuery = SearchQuery::regex( static ref MENTIONS_SEARCH: SearchQuery =
"@[-_\\w]+", SearchQuery::regex("@[-_\\w]+", false, false, false, Vec::new(), Vec::new()).unwrap();
false,
false,
Default::default(),
Default::default()
)
.unwrap();
} }
pub struct MessageEditor { pub struct MessageEditor {

View file

@ -14,14 +14,8 @@ use std::{sync::Arc, time::Duration};
const MENTIONS_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(50); const MENTIONS_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(50);
lazy_static! { lazy_static! {
static ref MENTIONS_SEARCH: SearchQuery = SearchQuery::regex( static ref MENTIONS_SEARCH: SearchQuery =
"@[-_\\w]+", SearchQuery::regex("@[-_\\w]+", false, false, false, Vec::new(), Vec::new()).unwrap();
false,
false,
Default::default(),
Default::default()
)
.unwrap();
} }
pub struct MessageEditor { pub struct MessageEditor {

View file

@ -1,8 +1,9 @@
use collections::{CommandPaletteFilter, HashMap}; use collections::{CommandPaletteFilter, HashMap};
use fuzzy::{StringMatch, StringMatchCandidate}; use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{ use gpui::{
actions, div, prelude::*, Action, AppContext, Dismiss, Div, FocusHandle, Keystroke, actions, actions, div, prelude::*, prelude::*, Action, AppContext, Component, Dismiss, Div,
ManagedView, ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView, EventEmitter, FocusHandle, FocusableView, Keystroke, ManagedView, Manager, ParentComponent,
ParentElement, Render, Render, Styled, View, ViewContext, VisualContext, WeakView,
}; };
use picker::{Picker, PickerDelegate}; use picker::{Picker, PickerDelegate};
use std::{ 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 { fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.picker.focus_handle(cx) self.picker.focus_handle(cx)
} }
@ -114,6 +117,7 @@ impl Clone for Command {
} }
} }
} }
/// Hit count for each command in the palette. /// 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 /// 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. /// 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>>) { fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
self.command_palette self.command_palette
.update(cx, |_, cx| cx.emit(Dismiss)) .update(cx, |_, cx| cx.emit(Manager::Dismiss))
.log_err(); .log_err();
} }

View 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

File diff suppressed because it is too large Load diff

View 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();
}
}

View 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)
}
}

View 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 }
}
}

View file

@ -24,7 +24,7 @@ use ::git::diff::DiffHunk;
use aho_corasick::AhoCorasick; use aho_corasick::AhoCorasick;
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use blink_manager::BlinkManager; use blink_manager::BlinkManager;
use client::{ClickhouseEvent, Client, Collaborator, ParticipantIndex, TelemetrySettings}; use client::{Client, Collaborator, ParticipantIndex, TelemetrySettings};
use clock::{Global, ReplicaId}; use clock::{Global, ReplicaId};
use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque}; use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
use convert_case::{Case, Casing}; use convert_case::{Case, Casing};
@ -8946,12 +8946,12 @@ impl Editor {
let telemetry = project.read(cx).client().telemetry().clone(); let telemetry = project.read(cx).client().telemetry().clone();
let telemetry_settings = *settings::get::<TelemetrySettings>(cx); let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
let event = ClickhouseEvent::Copilot { telemetry.report_copilot_event(
telemetry_settings,
suggestion_id, suggestion_id,
suggestion_accepted, suggestion_accepted,
file_extension, file_extension,
}; )
telemetry.report_clickhouse_event(event, telemetry_settings);
} }
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
@ -8998,14 +8998,14 @@ impl Editor {
.show_copilot_suggestions; .show_copilot_suggestions;
let telemetry = project.read(cx).client().telemetry().clone(); let telemetry = project.read(cx).client().telemetry().clone();
let event = ClickhouseEvent::Editor { telemetry.report_editor_event(
telemetry_settings,
file_extension, file_extension,
vim_mode, vim_mode,
operation, operation,
copilot_enabled, copilot_enabled,
copilot_enabled_for_language, 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, /// Copy the highlighted chunks to the clipboard as JSON. The format is an array of lines,

View file

@ -24,7 +24,7 @@ use ::git::diff::DiffHunk;
use aho_corasick::AhoCorasick; use aho_corasick::AhoCorasick;
use anyhow::{anyhow, Context as _, Result}; use anyhow::{anyhow, Context as _, Result};
use blink_manager::BlinkManager; use blink_manager::BlinkManager;
use client::{ClickhouseEvent, Client, Collaborator, ParticipantIndex, TelemetrySettings}; use client::{Client, Collaborator, ParticipantIndex, TelemetrySettings};
use clock::ReplicaId; use clock::ReplicaId;
use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque}; use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
use convert_case::{Case, Casing}; use convert_case::{Case, Casing};
@ -585,7 +585,7 @@ pub enum SoftWrap {
Column(u32), Column(u32),
} }
#[derive(Clone)] #[derive(Clone, Default)]
pub struct EditorStyle { pub struct EditorStyle {
pub background: Hsla, pub background: Hsla,
pub local_player: PlayerColor, pub local_player: PlayerColor,
@ -2319,7 +2319,7 @@ impl Editor {
} }
self.blink_manager.update(cx, BlinkManager::pause_blinking); self.blink_manager.update(cx, BlinkManager::pause_blinking);
cx.emit(Event::SelectionsChanged { local }); cx.emit(EditorEvent::SelectionsChanged { local });
if self.selections.disjoint_anchors().len() == 1 { if self.selections.disjoint_anchors().len() == 1 {
cx.emit(SearchEvent::ActiveMatchChanged) cx.emit(SearchEvent::ActiveMatchChanged)
@ -4243,7 +4243,7 @@ impl Editor {
self.report_copilot_event(Some(completion.uuid.clone()), true, cx) self.report_copilot_event(Some(completion.uuid.clone()), true, cx)
} }
cx.emit(Event::InputHandled { cx.emit(EditorEvent::InputHandled {
utf16_range_to_replace: None, utf16_range_to_replace: None,
text: suggestion.text.to_string().into(), text: suggestion.text.to_string().into(),
}); });
@ -4393,16 +4393,17 @@ impl Editor {
FoldStatus::Folded => ui::Icon::ChevronRight, FoldStatus::Folded => ui::Icon::ChevronRight,
FoldStatus::Foldable => ui::Icon::ChevronDown, FoldStatus::Foldable => ui::Icon::ChevronDown,
}; };
IconButton::new(ix as usize, icon).on_click( IconButton::new(ix as usize, icon)
move |editor: &mut Editor, cx| match fold_status { .on_click(move |editor: &mut Editor, cx| match fold_status {
FoldStatus::Folded => { FoldStatus::Folded => {
editor.unfold_at(&UnfoldAt { buffer_row }, cx); editor.unfold_at(&UnfoldAt { buffer_row }, cx);
} }
FoldStatus::Foldable => { FoldStatus::Foldable => {
editor.fold_at(&FoldAt { buffer_row }, cx); editor.fold_at(&FoldAt { buffer_row }, cx);
} }
}, })
) .color(ui::TextColor::Muted)
.render()
}) })
}) })
.flatten() .flatten()
@ -5640,7 +5641,7 @@ impl Editor {
self.request_autoscroll(Autoscroll::fit(), cx); self.request_autoscroll(Autoscroll::fit(), cx);
self.unmark_text(cx); self.unmark_text(cx);
self.refresh_copilot_suggestions(true, 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.request_autoscroll(Autoscroll::fit(), cx);
self.unmark_text(cx); self.unmark_text(cx);
self.refresh_copilot_suggestions(true, 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"); log::error!("unexpectedly ended a transaction that wasn't started by this editor");
} }
cx.emit(Event::Edited); cx.emit(EditorEvent::Edited);
Some(tx_id) Some(tx_id)
} else { } else {
None None
@ -8712,7 +8713,7 @@ impl Editor {
if self.has_active_copilot_suggestion(cx) { if self.has_active_copilot_suggestion(cx) {
self.update_visible_copilot_suggestion(cx); self.update_visible_copilot_suggestion(cx);
} }
cx.emit(Event::BufferEdited); cx.emit(EditorEvent::BufferEdited);
cx.emit(ItemEvent::Edit); cx.emit(ItemEvent::Edit);
cx.emit(ItemEvent::UpdateBreadcrumbs); cx.emit(ItemEvent::UpdateBreadcrumbs);
cx.emit(SearchEvent::MatchesInvalidated); cx.emit(SearchEvent::MatchesInvalidated);
@ -8751,7 +8752,7 @@ impl Editor {
predecessor, predecessor,
excerpts, excerpts,
} => { } => {
cx.emit(Event::ExcerptsAdded { cx.emit(EditorEvent::ExcerptsAdded {
buffer: buffer.clone(), buffer: buffer.clone(),
predecessor: *predecessor, predecessor: *predecessor,
excerpts: excerpts.clone(), excerpts: excerpts.clone(),
@ -8760,7 +8761,7 @@ impl Editor {
} }
multi_buffer::Event::ExcerptsRemoved { ids } => { multi_buffer::Event::ExcerptsRemoved { ids } => {
self.refresh_inlay_hints(InlayHintRefreshReason::ExcerptsRemoved(ids.clone()), cx); 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 => { multi_buffer::Event::Reparsed => {
cx.emit(ItemEvent::UpdateBreadcrumbs); cx.emit(ItemEvent::UpdateBreadcrumbs);
@ -8774,7 +8775,7 @@ impl Editor {
cx.emit(ItemEvent::UpdateTab); cx.emit(ItemEvent::UpdateTab);
cx.emit(ItemEvent::UpdateBreadcrumbs); 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::Closed => cx.emit(ItemEvent::CloseItem),
multi_buffer::Event::DiagnosticsUpdated => { multi_buffer::Event::DiagnosticsUpdated => {
self.refresh_active_diagnostics(cx); self.refresh_active_diagnostics(cx);
@ -8968,12 +8969,12 @@ impl Editor {
let telemetry = project.read(cx).client().telemetry().clone(); let telemetry = project.read(cx).client().telemetry().clone();
let telemetry_settings = *TelemetrySettings::get_global(cx); let telemetry_settings = *TelemetrySettings::get_global(cx);
let event = ClickhouseEvent::Copilot { telemetry.report_copilot_event(
telemetry_settings,
suggestion_id, suggestion_id,
suggestion_accepted, suggestion_accepted,
file_extension, file_extension,
}; )
telemetry.report_clickhouse_event(event, telemetry_settings);
} }
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
@ -9020,14 +9021,14 @@ impl Editor {
.show_copilot_suggestions; .show_copilot_suggestions;
let telemetry = project.read(cx).client().telemetry().clone(); let telemetry = project.read(cx).client().telemetry().clone();
let event = ClickhouseEvent::Editor { telemetry.report_editor_event(
telemetry_settings,
file_extension, file_extension,
vim_mode, vim_mode,
operation, operation,
copilot_enabled, copilot_enabled,
copilot_enabled_for_language, 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, /// 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>, cx: &mut ViewContext<Self>,
) { ) {
if !self.input_enabled { if !self.input_enabled {
cx.emit(Event::InputIgnored { text: text.into() }); cx.emit(EditorEvent::InputIgnored { text: text.into() });
return; return;
} }
if let Some(relative_utf16_range) = relative_utf16_range { if let Some(relative_utf16_range) = relative_utf16_range {
@ -9174,7 +9175,7 @@ impl Editor {
} }
fn handle_focus(&mut self, cx: &mut ViewContext<Self>) { 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() { if let Some(rename) = self.pending_rename.as_ref() {
let rename_editor_focus_handle = rename.editor.read(cx).focus_handle.clone(); 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)); .update(cx, |buffer, cx| buffer.remove_active_selections(cx));
self.hide_context_menu(cx); self.hide_context_menu(cx);
hide_hover(self, cx); hide_hover(self, cx);
cx.emit(Event::Blurred); cx.emit(EditorEvent::Blurred);
cx.notify(); cx.notify();
} }
} }
@ -9327,7 +9328,7 @@ impl Deref for EditorSnapshot {
} }
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub enum Event { pub enum EditorEvent {
InputIgnored { InputIgnored {
text: Arc<str>, text: Arc<str>,
}, },
@ -9345,8 +9346,12 @@ pub enum Event {
}, },
BufferEdited, BufferEdited,
Edited, Edited,
Reparsed,
Focused, Focused,
Blurred, Blurred,
DirtyChanged,
Saved,
TitleChanged,
DiffBaseChanged, DiffBaseChanged,
SelectionsChanged { SelectionsChanged {
local: bool, local: bool,
@ -9355,6 +9360,7 @@ pub enum Event {
local: bool, local: bool,
autoscroll: bool, autoscroll: bool,
}, },
Closed,
} }
pub struct EditorFocused(pub View<Editor>); 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 { impl FocusableView for Editor {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle { fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
@ -9572,7 +9578,7 @@ impl InputHandler for Editor {
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) {
if !self.input_enabled { if !self.input_enabled {
cx.emit(Event::InputIgnored { text: text.into() }); cx.emit(EditorEvent::InputIgnored { text: text.into() });
return; return;
} }
@ -9602,7 +9608,7 @@ impl InputHandler for Editor {
}) })
}); });
cx.emit(Event::InputHandled { cx.emit(EditorEvent::InputHandled {
utf16_range_to_replace: range_to_replace, utf16_range_to_replace: range_to_replace,
text: text.into(), text: text.into(),
}); });
@ -9633,7 +9639,7 @@ impl InputHandler for Editor {
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) {
if !self.input_enabled { if !self.input_enabled {
cx.emit(Event::InputIgnored { text: text.into() }); cx.emit(EditorEvent::InputIgnored { text: text.into() });
return; return;
} }
@ -9676,7 +9682,7 @@ impl InputHandler for Editor {
}) })
}); });
cx.emit(Event::InputHandled { cx.emit(EditorEvent::InputHandled {
utf16_range_to_replace: range_to_replace, utf16_range_to_replace: range_to_replace,
text: text.into(), text: text.into(),
}); });

View file

@ -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 buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
let (view, cx) = cx.add_window_view(|cx| build_editor(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; .await;
view.update(cx, |view, cx| { 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 buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
editor 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; .await;
editor.update(cx, |editor, cx| { 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 buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
let (view, cx) = cx.add_window_view(|cx| build_editor(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; .await;
view.update(cx, |view, cx| { 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 buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
editor 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; .await;
editor.update(cx, |editor, cx| { 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 buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
let (view, cx) = cx.add_window_view(|cx| build_editor(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; .await;
view.update(cx, |view, cx| { view.update(cx, |view, cx| {

View file

@ -1972,6 +1972,7 @@ impl EditorElement {
TransformBlock::ExcerptHeader { .. } => false, TransformBlock::ExcerptHeader { .. } => false,
TransformBlock::Custom(block) => block.style() == BlockStyle::Fixed, TransformBlock::Custom(block) => block.style() == BlockStyle::Fixed,
}); });
let mut render_block = |block: &TransformBlock, let mut render_block = |block: &TransformBlock,
available_space: Size<AvailableSpace>, available_space: Size<AvailableSpace>,
block_id: usize, block_id: usize,
@ -2005,6 +2006,7 @@ impl EditorElement {
editor_style: &self.style, editor_style: &self.style,
}) })
} }
TransformBlock::ExcerptHeader { TransformBlock::ExcerptHeader {
buffer, buffer,
range, range,
@ -2049,6 +2051,7 @@ impl EditorElement {
} }
h_stack() h_stack()
.id("path header block")
.size_full() .size_full()
.bg(gpui::red()) .bg(gpui::red())
.child( .child(
@ -2061,6 +2064,7 @@ impl EditorElement {
} else { } else {
let text_style = style.text.clone(); let text_style = style.text.clone();
h_stack() h_stack()
.id("collapsed context")
.size_full() .size_full()
.bg(gpui::red()) .bg(gpui::red())
.child("") .child("")

View file

@ -1,7 +1,7 @@
use crate::{ use crate::{
editor_settings::SeedQuerySetting, link_go_to_definition::hide_link_definition, editor_settings::SeedQuerySetting, link_go_to_definition::hide_link_definition,
movement::surrounding_word, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor, 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 _, NavigationData, ToPoint as _,
}; };
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
@ -41,11 +41,12 @@ use workspace::{
pub const MAX_TAB_TITLE_LEN: usize = 24; 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> { fn to_follow_event(&self) -> Option<workspace::item::FollowEvent> {
match self { match self {
Event::Edited => Some(FollowEvent::Unfollow), EditorEvent::Edited => Some(FollowEvent::Unfollow),
Event::SelectionsChanged { local } | Event::ScrollPositionChanged { local, .. } => { EditorEvent::SelectionsChanged { local }
| EditorEvent::ScrollPositionChanged { local, .. } => {
if *local { if *local {
Some(FollowEvent::Unfollow) Some(FollowEvent::Unfollow)
} else { } else {
@ -60,7 +61,7 @@ impl FollowableEvents for Event {
impl EventEmitter<ItemEvent> for Editor {} impl EventEmitter<ItemEvent> for Editor {}
impl FollowableItem for Editor { impl FollowableItem for Editor {
type FollowableEvent = Event; type FollowableEvent = EditorEvent;
fn remote_id(&self) -> Option<ViewId> { fn remote_id(&self) -> Option<ViewId> {
self.remote_id self.remote_id
} }
@ -248,7 +249,7 @@ impl FollowableItem for Editor {
match update { match update {
proto::update_view::Variant::Editor(update) => match event { proto::update_view::Variant::Editor(update) => match event {
Event::ExcerptsAdded { EditorEvent::ExcerptsAdded {
buffer, buffer,
predecessor, predecessor,
excerpts, excerpts,
@ -269,20 +270,20 @@ impl FollowableItem for Editor {
} }
true true
} }
Event::ExcerptsRemoved { ids } => { EditorEvent::ExcerptsRemoved { ids } => {
update update
.deleted_excerpts .deleted_excerpts
.extend(ids.iter().map(ExcerptId::to_proto)); .extend(ids.iter().map(ExcerptId::to_proto));
true true
} }
Event::ScrollPositionChanged { .. } => { EditorEvent::ScrollPositionChanged { .. } => {
let scroll_anchor = self.scroll_manager.anchor(); let scroll_anchor = self.scroll_manager.anchor();
update.scroll_top_anchor = Some(serialize_anchor(&scroll_anchor.anchor)); update.scroll_top_anchor = Some(serialize_anchor(&scroll_anchor.anchor));
update.scroll_x = scroll_anchor.offset.x; update.scroll_x = scroll_anchor.offset.x;
update.scroll_y = scroll_anchor.offset.y; update.scroll_y = scroll_anchor.offset.y;
true true
} }
Event::SelectionsChanged { .. } => { EditorEvent::SelectionsChanged { .. } => {
update.selections = self update.selections = self
.selections .selections
.disjoint_anchors() .disjoint_anchors()

View file

@ -6,8 +6,8 @@ use crate::{
display_map::{DisplaySnapshot, ToDisplayPoint}, display_map::{DisplaySnapshot, ToDisplayPoint},
hover_popover::hide_hover, hover_popover::hide_hover,
persistence::DB, persistence::DB,
Anchor, DisplayPoint, Editor, EditorMode, Event, InlayHintRefreshReason, MultiBufferSnapshot, Anchor, DisplayPoint, Editor, EditorEvent, EditorMode, InlayHintRefreshReason,
ToPoint, MultiBufferSnapshot, ToPoint,
}; };
use gpui::{point, px, AppContext, Entity, Pixels, Styled, Task, ViewContext}; use gpui::{point, px, AppContext, Entity, Pixels, Styled, Task, ViewContext};
use language::{Bias, Point}; use language::{Bias, Point};
@ -224,7 +224,7 @@ impl ScrollManager {
cx: &mut ViewContext<Editor>, cx: &mut ViewContext<Editor>,
) { ) {
self.anchor = anchor; self.anchor = anchor;
cx.emit(Event::ScrollPositionChanged { local, autoscroll }); cx.emit(EditorEvent::ScrollPositionChanged { local, autoscroll });
self.show_scrollbar(cx); self.show_scrollbar(cx);
self.autoscroll_request.take(); self.autoscroll_request.take();
if let Some(workspace_id) = workspace_id { if let Some(workspace_id) = workspace_id {

View file

@ -71,7 +71,8 @@ impl<'a> EditorTestContext<'a> {
&self, &self,
predicate: impl FnMut(&Editor, &AppContext) -> bool, predicate: impl FnMut(&Editor, &AppContext) -> bool,
) -> impl Future<Output = ()> { ) -> impl Future<Output = ()> {
self.editor.condition::<crate::Event>(&self.cx, predicate) self.editor
.condition::<crate::EditorEvent>(&self.cx, predicate)
} }
#[track_caller] #[track_caller]

View file

@ -2,8 +2,10 @@ use collections::HashMap;
use editor::{scroll::autoscroll::Autoscroll, Bias, Editor}; use editor::{scroll::autoscroll::Autoscroll, Bias, Editor};
use fuzzy::{CharBag, PathMatch, PathMatchCandidate}; use fuzzy::{CharBag, PathMatch, PathMatchCandidate};
use gpui::{ use gpui::{
actions, div, AppContext, Dismiss, Div, FocusHandle, InteractiveElement, ManagedView, Model, actions, div, AppContext, Component, Dismiss, Div, EventEmitter, FocusHandle, FocusableView,
ParentElement, Render, RenderOnce, Styled, Task, View, ViewContext, VisualContext, WeakView, InteractiveComponent, InteractiveElement, ManagedView, Manager, Model, ParentComponent,
ParentElement, Render, RenderOnce, Styled, Styled, Task, View, ViewContext, VisualContext,
WeakView,
}; };
use picker::{Picker, PickerDelegate}; use picker::{Picker, PickerDelegate};
use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId}; use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
@ -110,7 +112,8 @@ impl FileFinder {
} }
} }
impl ManagedView for FileFinder { impl EventEmitter<Manager> for FileFinder {}
impl FocusableView for FileFinder {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle { fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.picker.focus_handle(cx) self.picker.focus_handle(cx)
} }
@ -687,7 +690,9 @@ impl PickerDelegate for FileFinderDelegate {
.log_err(); .log_err();
} }
} }
finder.update(&mut cx, |_, cx| cx.emit(Dismiss)).ok()?; finder
.update(&mut cx, |_, cx| cx.emit(Manager::Dismiss))
.ok()?;
Some(()) Some(())
}) })
@ -698,7 +703,7 @@ impl PickerDelegate for FileFinderDelegate {
fn dismissed(&mut self, cx: &mut ViewContext<Picker<FileFinderDelegate>>) { fn dismissed(&mut self, cx: &mut ViewContext<Picker<FileFinderDelegate>>) {
self.file_finder self.file_finder
.update(cx, |_, cx| cx.emit(Dismiss)) .update(cx, |_, cx| cx.emit(Manager::Dismiss))
.log_err(); .log_err();
} }

View file

@ -1,7 +1,8 @@
use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Editor}; use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Editor};
use gpui::{ use gpui::{
actions, div, prelude::*, AppContext, Dismiss, Div, FocusHandle, ManagedView, ParentElement, actions, div, prelude::*, AppContext, Div, EventEmitter, FocusHandle, FocusableView, Manager,
Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext, WindowContext, ParentComponent, Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext,
WindowContext,
}; };
use text::{Bias, Point}; use text::{Bias, Point};
use theme::ActiveTheme; use theme::ActiveTheme;
@ -23,11 +24,12 @@ pub struct GoToLine {
_subscriptions: Vec<Subscription>, _subscriptions: Vec<Subscription>,
} }
impl ManagedView for GoToLine { impl FocusableView for GoToLine {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle { 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 { impl GoToLine {
fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) { fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
@ -82,13 +84,13 @@ impl GoToLine {
fn on_line_editor_event( fn on_line_editor_event(
&mut self, &mut self,
_: View<Editor>, _: View<Editor>,
event: &editor::Event, event: &editor::EditorEvent,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) {
match event { match event {
// todo!() this isn't working... // todo!() this isn't working...
editor::Event::Blurred => cx.emit(Dismiss), editor::EditorEvent::Blurred => cx.emit(Manager::Dismiss),
editor::Event::BufferEdited { .. } => self.highlight_current_line(cx), editor::EditorEvent::BufferEdited { .. } => self.highlight_current_line(cx),
_ => {} _ => {}
} }
} }
@ -122,7 +124,7 @@ impl GoToLine {
} }
fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) { 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>) { fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
@ -139,7 +141,7 @@ impl GoToLine {
self.prev_scroll_position.take(); self.prev_scroll_position.take();
} }
cx.emit(Dismiss); cx.emit(Manager::Dismiss);
} }
} }

View file

@ -47,7 +47,7 @@ serde_derive.workspace = true
serde_json.workspace = true serde_json.workspace = true
smallvec.workspace = true smallvec.workspace = true
smol.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 thiserror.workspace = true
time.workspace = true time.workspace = true
tiny-skia = "0.5" tiny-skia = "0.5"

View file

@ -492,6 +492,10 @@ impl AppContext {
self.platform.open_url(url); 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> { pub fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf> {
self.platform.path_for_auxiliary_executable(name) self.platform.path_for_auxiliary_executable(name)
} }

View file

@ -1,6 +1,6 @@
use crate::{ use crate::{
AnyView, AnyWindowHandle, AppCell, AppContext, BackgroundExecutor, Context, FocusableView, 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, VisualContext, WindowContext, WindowHandle,
}; };
use anyhow::{anyhow, Context as _}; use anyhow::{anyhow, Context as _};
@ -320,4 +320,13 @@ impl VisualContext for AsyncWindowContext {
view.read(cx).focus_handle(cx).clone().focus(cx); 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))
})
}
} }

View file

@ -71,11 +71,12 @@ impl EntityMap {
#[track_caller] #[track_caller]
pub fn lease<'a, T>(&mut self, model: &'a Model<T>) -> Lease<'a, T> { pub fn lease<'a, T>(&mut self, model: &'a Model<T>) -> Lease<'a, T> {
self.assert_valid_context(model); self.assert_valid_context(model);
let entity = Some( let entity = Some(self.entities.remove(model.entity_id).unwrap_or_else(|| {
self.entities panic!(
.remove(model.entity_id) "Circular entity lease of {}. Is it already being updated?",
.expect("Circular entity lease. Is the entity already being updated?"), std::any::type_name::<T>()
); )
}));
Lease { Lease {
model, model,
entity, entity,

View file

@ -386,6 +386,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> { impl<V> View<V> {
pub fn condition<Evt>( pub fn condition<Evt>(
&self, &self,
@ -579,6 +605,17 @@ impl<'a> VisualContext for VisualTestContext<'a> {
}) })
.unwrap() .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 { impl AnyWindowHandle {

View file

@ -1124,9 +1124,14 @@ where
} }
} }
} }
// if self.hover_style.is_some() {
if bounds.contains_point(&mouse_position) { if bounds.contains_point(&mouse_position) {
// eprintln!("div hovered {bounds:?} {mouse_position:?}");
style.refine(&self.hover_style); style.refine(&self.hover_style);
} else {
// eprintln!("div NOT hovered {bounds:?} {mouse_position:?}");
} }
// }
if let Some(drag) = cx.active_drag.take() { if let Some(drag) = cx.active_drag.take() {
for (state_type, group_drag_style) in &self.group_drag_over_styles { for (state_type, group_drag_style) in &self.group_drag_over_styles {

View file

@ -70,7 +70,7 @@ impl<V> Element<V> for Img<V> {
if let Some(data) = image_future if let Some(data) = image_future
.clone() .clone()
.now_or_never() .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()); let corner_radii = corner_radii.to_pixels(bounds.size, cx.rem_size());
cx.with_z_index(1, |cx| { cx.with_z_index(1, |cx| {
@ -79,7 +79,7 @@ impl<V> Element<V> for Img<V> {
}); });
} else { } else {
cx.spawn(|_, mut cx| async move { 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()); cx.on_next_frame(|cx| cx.notify());
} }
}) })

View file

@ -130,19 +130,34 @@ impl<V: 'static> Element<V> for StyledText {
let layout_id = cx.request_measured_layout(Default::default(), rem_size, { let layout_id = cx.request_measured_layout(Default::default(), rem_size, {
let element_state = element_state.clone(); 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 let Some(lines) = text_system
.shape_text( .shape_text(
&text, &text,
font_size, font_size,
&runs[..], &runs[..],
known_dimensions.width, // Wrap if we know the width. wrap_width, // Wrap if we know the width.
) )
.log_err() .log_err()
else { else {
element_state.lock().replace(TextStateInner { element_state.lock().replace(TextStateInner {
lines: Default::default(), lines: Default::default(),
line_height, line_height,
wrap_width,
size: Some(Size::default()),
}); });
return Size::default(); return Size::default();
}; };
@ -154,9 +169,12 @@ impl<V: 'static> Element<V> for StyledText {
size.width = size.width.max(line_size.width); size.width = size.width.max(line_size.width);
} }
element_state element_state.lock().replace(TextStateInner {
.lock() lines,
.replace(TextStateInner { lines, line_height }); line_height,
wrap_width,
size: Some(size),
});
size size
} }
@ -205,6 +223,8 @@ pub struct TextState(Arc<Mutex<Option<TextStateInner>>>);
struct TextStateInner { struct TextStateInner {
lines: SmallVec<[WrappedLine; 1]>, lines: SmallVec<[WrappedLine; 1]>,
line_height: Pixels, line_height: Pixels,
wrap_width: Option<Pixels>,
size: Option<Size<Pixels>>,
} }
impl TextState { impl TextState {

View file

@ -139,6 +139,10 @@ pub trait VisualContext: Context {
fn focus_view<V>(&mut self, view: &View<V>) -> Self::Result<()> fn focus_view<V>(&mut self, view: &View<V>) -> Self::Result<()>
where where
V: FocusableView; V: FocusableView;
fn dismiss_view<V>(&mut self, view: &View<V>) -> Self::Result<()>
where
V: ManagedView;
} }
pub trait Entity<T>: Sealed { pub trait Entity<T>: Sealed {

View file

@ -2,7 +2,7 @@ use crate::{ImageData, ImageId, SharedString};
use collections::HashMap; use collections::HashMap;
use futures::{ use futures::{
future::{BoxFuture, Shared}, future::{BoxFuture, Shared},
AsyncReadExt, FutureExt, AsyncReadExt, FutureExt, TryFutureExt,
}; };
use image::ImageError; use image::ImageError;
use parking_lot::Mutex; use parking_lot::Mutex;
@ -88,6 +88,14 @@ impl ImageCache {
Ok(Arc::new(ImageData::new(image))) Ok(Arc::new(ImageData::new(image)))
} }
} }
.map_err({
let uri = uri.clone();
move |error| {
log::log!(log::Level::Error, "{:?} {:?}", &uri, &error);
error
}
})
.boxed() .boxed()
.shared(); .shared();

View file

@ -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::MouseMove(_) if !(is_active || lock.kind == WindowKind::PopUp) => return,
InputEvent::MouseUp(MouseUpEvent { InputEvent::MouseUp(MouseUpEvent { .. }) => {
button: MouseButton::Left,
..
}) => {
lock.synthetic_drag_counter += 1; lock.synthetic_drag_counter += 1;
} }

View file

@ -5,12 +5,14 @@ use std::fmt::Debug;
use taffy::{ use taffy::{
geometry::{Point as TaffyPoint, Rect as TaffyRect, Size as TaffySize}, geometry::{Point as TaffyPoint, Rect as TaffyRect, Size as TaffySize},
style::AvailableSpace as TaffyAvailableSpace, style::AvailableSpace as TaffyAvailableSpace,
tree::{Measurable, MeasureFunc, NodeId}, tree::NodeId,
Taffy, Taffy,
}; };
type Measureable = dyn Fn(Size<Option<Pixels>>, Size<AvailableSpace>) -> Size<Pixels> + Send + Sync;
pub struct TaffyLayoutEngine { pub struct TaffyLayoutEngine {
taffy: Taffy, taffy: Taffy<Box<Measureable>>,
children_to_parents: HashMap<LayoutId, LayoutId>, children_to_parents: HashMap<LayoutId, LayoutId>,
absolute_layout_bounds: HashMap<LayoutId, Bounds<Pixels>>, absolute_layout_bounds: HashMap<LayoutId, Bounds<Pixels>>,
computed_layouts: HashSet<LayoutId>, computed_layouts: HashSet<LayoutId>,
@ -70,9 +72,9 @@ impl TaffyLayoutEngine {
) -> LayoutId { ) -> LayoutId {
let style = style.to_taffy(rem_size); let style = style.to_taffy(rem_size);
let measurable = Box::new(Measureable(measure)) as Box<dyn Measurable>; let measurable = Box::new(measure);
self.taffy self.taffy
.new_leaf_with_measure(style, MeasureFunc::Boxed(measurable)) .new_leaf_with_context(style, measurable)
.expect(EXPECT_MESSAGE) .expect(EXPECT_MESSAGE)
.into() .into()
} }
@ -154,7 +156,22 @@ impl TaffyLayoutEngine {
// let started_at = std::time::Instant::now(); // let started_at = std::time::Instant::now();
self.taffy 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); .expect(EXPECT_MESSAGE);
// println!("compute_layout took {:?}", started_at.elapsed()); // 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> { trait ToTaffy<Output> {
fn to_taffy(&self, rem_size: Pixels) -> Output; fn to_taffy(&self, rem_size: Pixels) -> Output;
} }

View file

@ -179,6 +179,10 @@ impl AnyView {
self.model.entity_type self.model.entity_type
} }
pub fn entity_id(&self) -> EntityId {
self.model.entity_id()
}
pub(crate) fn draw( pub(crate) fn draw(
&self, &self,
origin: Point<Pixels>, origin: Point<Pixels>,
@ -309,18 +313,6 @@ impl<V: 'static + Render<V>> From<WeakView<V>> for AnyWeakView {
} }
} }
impl<F, E> Render<F> for F
where
F: 'static + FnMut(&mut WindowContext) -> E,
E: 'static + Send + Element<F>,
{
type Element = E;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
(self)(cx)
}
}
pub struct RenderViewWith<E, V> { pub struct RenderViewWith<E, V> {
view: View<V>, view: View<V>,
element: Option<E>, element: Option<E>,

View file

@ -6,8 +6,8 @@ use crate::{
InputEvent, IsZero, KeyBinding, KeyContext, KeyDownEvent, LayoutId, Model, ModelContext, InputEvent, IsZero, KeyBinding, KeyContext, KeyDownEvent, LayoutId, Model, ModelContext,
Modifiers, MonochromeSprite, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Path, Modifiers, MonochromeSprite, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Path,
Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point, Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point,
PolychromeSprite, PromptLevel, Quad, RenderGlyphParams, RenderImageParams, RenderSvgParams, PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams,
Render, ScaledPixels, SceneBuilder, Shadow, SharedString, Size, Style, SubscriberSet, RenderSvgParams, ScaledPixels, SceneBuilder, Shadow, SharedString, Size, Style, SubscriberSet,
Subscription, TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, VisualContext, Subscription, TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, VisualContext,
WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS, WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS,
}; };
@ -193,17 +193,12 @@ pub trait FocusableView: 'static + Render<Self> {
/// ManagedView is a view (like a Modal, Popover, Menu, etc.) /// ManagedView is a view (like a Modal, Popover, Menu, etc.)
/// where the lifecycle of the view is handled by another view. /// where the lifecycle of the view is handled by another view.
pub trait ManagedView: 'static + Render<Self> { pub trait ManagedView: FocusableView + EventEmitter<Manager> {}
fn focus_handle(&self, cx: &AppContext) -> FocusHandle;
}
pub struct Dismiss; impl<M: FocusableView + EventEmitter<Manager>> ManagedView for M {}
impl<T: ManagedView> EventEmitter<Dismiss> for T {}
impl<T: ManagedView> FocusableView for T { pub enum Manager {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle { Dismiss,
self.focus_handle(cx)
}
} }
// Holds the state for a specific window. // Holds the state for a specific window.
@ -1582,6 +1577,13 @@ impl VisualContext for WindowContext<'_> {
view.focus_handle(cx).clone().focus(cx); 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> { 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)) 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> { 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<()> { fn focus_view<W: FocusableView>(&mut self, view: &View<W>) -> Self::Result<()> {
self.window_cx.focus_view(view) 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> { impl<'a, V> std::ops::Deref for ViewContext<'a, V> {
@ -2398,6 +2411,17 @@ impl<V: 'static + Render<V>> 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>( pub fn update<C, R>(
&self, &self,
cx: &mut C, cx: &mut C,
@ -2543,6 +2567,18 @@ pub enum ElementId {
FocusHandle(FocusId), 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 { impl From<EntityId> for ElementId {
fn from(id: EntityId) -> Self { fn from(id: EntityId) -> Self {
ElementId::View(id) ElementId::View(id)

View file

@ -9,7 +9,7 @@ path = "src/journal2.rs"
doctest = false doctest = false
[dependencies] [dependencies]
editor = { path = "../editor" } editor = { package = "editor2", path = "../editor2" }
gpui = { package = "gpui2", path = "../gpui2" } gpui = { package = "gpui2", path = "../gpui2" }
util = { path = "../util" } util = { path = "../util" }
workspace2 = { path = "../workspace2" } workspace2 = { path = "../workspace2" }
@ -24,4 +24,4 @@ log.workspace = true
shellexpand = "2.1.0" shellexpand = "2.1.0"
[dev-dependencies] [dev-dependencies]
editor = { path = "../editor", features = ["test-support"] } editor = { package="editor2", path = "../editor2", features = ["test-support"] }

View file

@ -143,10 +143,10 @@ impl<D: PickerDelegate> Picker<D> {
fn on_input_editor_event( fn on_input_editor_event(
&mut self, &mut self,
_: View<Editor>, _: View<Editor>,
event: &editor::Event, event: &editor::EditorEvent,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) {
if let editor::Event::BufferEdited = event { if let editor::EditorEvent::BufferEdited = event {
let query = self.editor.read(cx).text(cx); let query = self.editor.read(cx).text(cx);
self.update_matches(query, cx); self.update_matches(query, cx);
} }

View file

@ -20,10 +20,6 @@ impl IgnoreStack {
Arc::new(Self::All) 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> { pub fn append(self: Arc<Self>, abs_base_path: Arc<Path>, ignore: Arc<Gitignore>) -> Arc<Self> {
match self.as_ref() { match self.as_ref() {
IgnoreStack::All => self, IgnoreStack::All => self,

View file

@ -5548,7 +5548,16 @@ impl Project {
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let background = cx.background().clone(); 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 { if path_count == 0 {
let (_, rx) = smol::channel::bounded(1024); let (_, rx) = smol::channel::bounded(1024);
return rx; return rx;
@ -5561,8 +5570,16 @@ impl Project {
.iter() .iter()
.filter_map(|(_, b)| { .filter_map(|(_, b)| {
let buffer = b.upgrade(cx)?; let buffer = b.upgrade(cx)?;
let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot()); let (is_ignored, snapshot) = buffer.update(cx, |buffer, cx| {
if let Some(path) = snapshot.file().map(|file| file.path()) { 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))) Some((path.clone(), (buffer, snapshot)))
} else { } else {
unnamed_files.push(buffer); unnamed_files.push(buffer);
@ -5735,7 +5752,12 @@ impl Project {
let mut snapshot_start_ix = 0; let mut snapshot_start_ix = 0;
let mut abs_path = PathBuf::new(); let mut abs_path = PathBuf::new();
for snapshot in snapshots { 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 { if worker_end_ix <= snapshot_start_ix {
break; break;
} else if worker_start_ix > snapshot_end_ix { } 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; cmp::min(worker_end_ix, snapshot_end_ix) - snapshot_start_ix;
for entry in snapshot for entry in snapshot
.files(false, start_in_snapshot) .files(query.include_ignored(), start_in_snapshot)
.take(end_in_snapshot - start_in_snapshot) .take(end_in_snapshot - start_in_snapshot)
{ {
if matching_paths_tx.is_closed() { if matching_paths_tx.is_closed() {

View file

@ -10,6 +10,8 @@ pub struct ProjectSettings {
pub lsp: HashMap<Arc<str>, LspSettings>, pub lsp: HashMap<Arc<str>, LspSettings>,
#[serde(default)] #[serde(default)]
pub git: GitSettings, pub git: GitSettings,
#[serde(default)]
pub file_scan_exclusions: Option<Vec<String>>,
} }
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] #[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]

View file

@ -3598,7 +3598,7 @@ async fn test_search(cx: &mut gpui::TestAppContext) {
assert_eq!( assert_eq!(
search( search(
&project, &project,
SearchQuery::text("TWO", false, true, Vec::new(), Vec::new()).unwrap(), SearchQuery::text("TWO", false, true, false, Vec::new(), Vec::new()).unwrap(),
cx cx
) )
.await .await
@ -3623,7 +3623,7 @@ async fn test_search(cx: &mut gpui::TestAppContext) {
assert_eq!( assert_eq!(
search( search(
&project, &project,
SearchQuery::text("TWO", false, true, Vec::new(), Vec::new()).unwrap(), SearchQuery::text("TWO", false, true, false, Vec::new(), Vec::new()).unwrap(),
cx cx
) )
.await .await
@ -3662,6 +3662,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
search_query, search_query,
false, false,
true, true,
false,
vec![PathMatcher::new("*.odd").unwrap()], vec![PathMatcher::new("*.odd").unwrap()],
Vec::new() Vec::new()
) )
@ -3681,6 +3682,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
search_query, search_query,
false, false,
true, true,
false,
vec![PathMatcher::new("*.rs").unwrap()], vec![PathMatcher::new("*.rs").unwrap()],
Vec::new() Vec::new()
) )
@ -3703,6 +3705,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
search_query, search_query,
false, false,
true, true,
false,
vec![ vec![
PathMatcher::new("*.ts").unwrap(), PathMatcher::new("*.ts").unwrap(),
PathMatcher::new("*.odd").unwrap(), PathMatcher::new("*.odd").unwrap(),
@ -3727,6 +3730,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
search_query, search_query,
false, false,
true, true,
false,
vec![ vec![
PathMatcher::new("*.rs").unwrap(), PathMatcher::new("*.rs").unwrap(),
PathMatcher::new("*.ts").unwrap(), PathMatcher::new("*.ts").unwrap(),
@ -3774,6 +3778,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
search_query, search_query,
false, false,
true, true,
false,
Vec::new(), Vec::new(),
vec![PathMatcher::new("*.odd").unwrap()], vec![PathMatcher::new("*.odd").unwrap()],
) )
@ -3798,6 +3803,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
search_query, search_query,
false, false,
true, true,
false,
Vec::new(), Vec::new(),
vec![PathMatcher::new("*.rs").unwrap()], vec![PathMatcher::new("*.rs").unwrap()],
) )
@ -3820,6 +3826,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
search_query, search_query,
false, false,
true, true,
false,
Vec::new(), Vec::new(),
vec![ vec![
PathMatcher::new("*.ts").unwrap(), PathMatcher::new("*.ts").unwrap(),
@ -3844,6 +3851,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
search_query, search_query,
false, false,
true, true,
false,
Vec::new(), Vec::new(),
vec![ vec![
PathMatcher::new("*.rs").unwrap(), PathMatcher::new("*.rs").unwrap(),
@ -3885,6 +3893,7 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
search_query, search_query,
false, false,
true, true,
false,
vec![PathMatcher::new("*.odd").unwrap()], vec![PathMatcher::new("*.odd").unwrap()],
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, search_query,
false, false,
true, true,
false,
vec![PathMatcher::new("*.ts").unwrap()], vec![PathMatcher::new("*.ts").unwrap()],
vec![PathMatcher::new("*.ts").unwrap()], vec![PathMatcher::new("*.ts").unwrap()],
).unwrap(), ).unwrap(),
@ -3922,6 +3932,7 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
search_query, search_query,
false, false,
true, true,
false,
vec![ vec![
PathMatcher::new("*.ts").unwrap(), PathMatcher::new("*.ts").unwrap(),
PathMatcher::new("*.odd").unwrap() PathMatcher::new("*.odd").unwrap()
@ -3947,6 +3958,7 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
search_query, search_query,
false, false,
true, true,
false,
vec![ vec![
PathMatcher::new("*.ts").unwrap(), PathMatcher::new("*.ts").unwrap(),
PathMatcher::new("*.odd").unwrap() PathMatcher::new("*.odd").unwrap()

View file

@ -39,6 +39,7 @@ pub enum SearchQuery {
replacement: Option<String>, replacement: Option<String>,
whole_word: bool, whole_word: bool,
case_sensitive: bool, case_sensitive: bool,
include_ignored: bool,
inner: SearchInputs, inner: SearchInputs,
}, },
@ -48,6 +49,7 @@ pub enum SearchQuery {
multiline: bool, multiline: bool,
whole_word: bool, whole_word: bool,
case_sensitive: bool, case_sensitive: bool,
include_ignored: bool,
inner: SearchInputs, inner: SearchInputs,
}, },
} }
@ -57,6 +59,7 @@ impl SearchQuery {
query: impl ToString, query: impl ToString,
whole_word: bool, whole_word: bool,
case_sensitive: bool, case_sensitive: bool,
include_ignored: bool,
files_to_include: Vec<PathMatcher>, files_to_include: Vec<PathMatcher>,
files_to_exclude: Vec<PathMatcher>, files_to_exclude: Vec<PathMatcher>,
) -> Result<Self> { ) -> Result<Self> {
@ -74,6 +77,7 @@ impl SearchQuery {
replacement: None, replacement: None,
whole_word, whole_word,
case_sensitive, case_sensitive,
include_ignored,
inner, inner,
}) })
} }
@ -82,6 +86,7 @@ impl SearchQuery {
query: impl ToString, query: impl ToString,
whole_word: bool, whole_word: bool,
case_sensitive: bool, case_sensitive: bool,
include_ignored: bool,
files_to_include: Vec<PathMatcher>, files_to_include: Vec<PathMatcher>,
files_to_exclude: Vec<PathMatcher>, files_to_exclude: Vec<PathMatcher>,
) -> Result<Self> { ) -> Result<Self> {
@ -111,6 +116,7 @@ impl SearchQuery {
multiline, multiline,
whole_word, whole_word,
case_sensitive, case_sensitive,
include_ignored,
inner, inner,
}) })
} }
@ -121,6 +127,7 @@ impl SearchQuery {
message.query, message.query,
message.whole_word, message.whole_word,
message.case_sensitive, message.case_sensitive,
message.include_ignored,
deserialize_path_matches(&message.files_to_include)?, deserialize_path_matches(&message.files_to_include)?,
deserialize_path_matches(&message.files_to_exclude)?, deserialize_path_matches(&message.files_to_exclude)?,
) )
@ -129,6 +136,7 @@ impl SearchQuery {
message.query, message.query,
message.whole_word, message.whole_word,
message.case_sensitive, message.case_sensitive,
message.include_ignored,
deserialize_path_matches(&message.files_to_include)?, deserialize_path_matches(&message.files_to_include)?,
deserialize_path_matches(&message.files_to_exclude)?, deserialize_path_matches(&message.files_to_exclude)?,
) )
@ -156,6 +164,7 @@ impl SearchQuery {
regex: self.is_regex(), regex: self.is_regex(),
whole_word: self.whole_word(), whole_word: self.whole_word(),
case_sensitive: self.case_sensitive(), case_sensitive: self.case_sensitive(),
include_ignored: self.include_ignored(),
files_to_include: self files_to_include: self
.files_to_include() .files_to_include()
.iter() .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 { pub fn is_regex(&self) -> bool {
matches!(self, Self::Regex { .. }) matches!(self, Self::Regex { .. })
} }

View file

@ -1,5 +1,6 @@
use crate::{ 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 ::ignore::gitignore::{Gitignore, GitignoreBuilder};
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
@ -21,7 +22,10 @@ use futures::{
}; };
use fuzzy::CharBag; use fuzzy::CharBag;
use git::{DOT_GIT, GITIGNORE}; 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::{ use language::{
proto::{ proto::{
deserialize_fingerprint, deserialize_version, serialize_fingerprint, serialize_line_ending, deserialize_fingerprint, deserialize_version, serialize_fingerprint, serialize_line_ending,
@ -36,6 +40,7 @@ use postage::{
prelude::{Sink as _, Stream as _}, prelude::{Sink as _, Stream as _},
watch, watch,
}; };
use settings::SettingsStore;
use smol::channel::{self, Sender}; use smol::channel::{self, Sender};
use std::{ use std::{
any::Any, any::Any,
@ -55,7 +60,10 @@ use std::{
time::{Duration, SystemTime}, time::{Duration, SystemTime},
}; };
use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet}; 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)] #[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)]
pub struct WorktreeId(usize); pub struct WorktreeId(usize);
@ -70,7 +78,8 @@ pub struct LocalWorktree {
scan_requests_tx: channel::Sender<ScanRequest>, scan_requests_tx: channel::Sender<ScanRequest>,
path_prefixes_to_scan_tx: channel::Sender<Arc<Path>>, path_prefixes_to_scan_tx: channel::Sender<Arc<Path>>,
is_scanning: (watch::Sender<bool>, watch::Receiver<bool>), is_scanning: (watch::Sender<bool>, watch::Receiver<bool>),
_background_scanner_task: Task<()>, _settings_subscription: Subscription,
_background_scanner_tasks: Vec<Task<()>>,
share: Option<ShareState>, share: Option<ShareState>,
diagnostics: HashMap< diagnostics: HashMap<
Arc<Path>, Arc<Path>,
@ -216,6 +225,7 @@ pub struct LocalSnapshot {
/// All of the git repositories in the worktree, indexed by the project entry /// All of the git repositories in the worktree, indexed by the project entry
/// id of their parent directory. /// id of their parent directory.
git_repositories: TreeMap<ProjectEntryId, LocalRepositoryEntry>, git_repositories: TreeMap<ProjectEntryId, LocalRepositoryEntry>,
file_scan_exclusions: Vec<PathMatcher>,
} }
struct BackgroundScannerState { struct BackgroundScannerState {
@ -299,17 +309,54 @@ impl Worktree {
.await .await
.context("failed to stat worktree path")?; .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>| { 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 let root_name = abs_path
.file_name() .file_name()
.map_or(String::new(), |f| f.to_string_lossy().to_string()); .map_or(String::new(), |f| f.to_string_lossy().to_string());
let mut snapshot = LocalSnapshot { let mut snapshot = LocalSnapshot {
file_scan_exclusions: file_scan_exclusions(settings::get::<ProjectSettings>(cx)),
ignores_by_parent_abs_path: Default::default(), ignores_by_parent_abs_path: Default::default(),
git_repositories: Default::default(), git_repositories: Default::default(),
snapshot: Snapshot { snapshot: Snapshot {
id: WorktreeId::from_usize(cx.model_id()), 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_name: root_name.clone(),
root_char_bag: root_name.chars().map(|c| c.to_ascii_lowercase()).collect(), root_char_bag: root_name.chars().map(|c| c.to_ascii_lowercase()).collect(),
entries_by_path: Default::default(), entries_by_path: Default::default(),
@ -334,60 +381,23 @@ impl Worktree {
let (scan_requests_tx, scan_requests_rx) = channel::unbounded(); let (scan_requests_tx, scan_requests_rx) = channel::unbounded();
let (path_prefixes_to_scan_tx, path_prefixes_to_scan_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(); let task_snapshot = snapshot.clone();
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;
}
});
Worktree::Local(LocalWorktree { Worktree::Local(LocalWorktree {
snapshot, snapshot,
is_scanning: watch::channel_with(true), is_scanning: watch::channel_with(true),
share: None, share: None,
scan_requests_tx, scan_requests_tx,
path_prefixes_to_scan_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(), diagnostics: Default::default(),
diagnostic_summaries: Default::default(), diagnostic_summaries: Default::default(),
client, 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 { impl LocalWorktree {
pub fn contains_abs_path(&self, path: &Path) -> bool { pub fn contains_abs_path(&self, path: &Path) -> bool {
path.starts_with(&self.abs_path) path.starts_with(&self.abs_path)
@ -1481,7 +1561,7 @@ impl Snapshot {
self.entries_by_id.get(&entry_id, &()).is_some() 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 entry = Entry::try_from((&self.root_char_bag, entry))?;
let old_entry = self.entries_by_id.insert_or_replace( let old_entry = self.entries_by_id.insert_or_replace(
PathEntry { PathEntry {
@ -2145,6 +2225,12 @@ impl LocalSnapshot {
paths.sort_by(|a, b| a.0.cmp(b.0)); paths.sort_by(|a, b| a.0.cmp(b.0));
paths 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 { impl BackgroundScannerState {
@ -2167,7 +2253,7 @@ impl BackgroundScannerState {
let ignore_stack = self.snapshot.ignore_stack_for_abs_path(&abs_path, true); 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 ancestor_inodes = self.snapshot.ancestor_inodes_for_path(&path);
let mut containing_repository = None; 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 Some((workdir_path, repo)) = self.snapshot.local_repo_for_path(&path) {
if let Ok(repo_path) = path.strip_prefix(&workdir_path.0) { if let Ok(repo_path) = path.strip_prefix(&workdir_path.0) {
containing_repository = Some(( containing_repository = Some((
@ -2378,18 +2464,30 @@ impl BackgroundScannerState {
// Remove any git repositories whose .git entry no longer exists. // Remove any git repositories whose .git entry no longer exists.
let snapshot = &mut self.snapshot; let snapshot = &mut self.snapshot;
let mut repositories = mem::take(&mut snapshot.git_repositories); let mut ids_to_preserve = HashSet::default();
let mut repository_entries = mem::take(&mut snapshot.repository_entries); for (&work_directory_id, entry) in snapshot.git_repositories.iter() {
repositories.retain(|work_directory_id, _| { let exists_in_snapshot = snapshot
snapshot .entry_for_id(work_directory_id)
.entry_for_id(*work_directory_id)
.map_or(false, |entry| { .map_or(false, |entry| {
snapshot.entry_for_path(entry.path.join(*DOT_GIT)).is_some() snapshot.entry_for_path(entry.path.join(*DOT_GIT)).is_some()
}) });
}); if exists_in_snapshot {
repository_entries.retain(|_, entry| repositories.get(&entry.work_directory.0).is_some()); ids_to_preserve.insert(work_directory_id);
snapshot.git_repositories = repositories; } else {
snapshot.repository_entries = repository_entries; 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( fn build_git_repository(
@ -3094,7 +3192,7 @@ impl BackgroundScanner {
let ignore_stack = state let ignore_stack = state
.snapshot .snapshot
.ignore_stack_for_abs_path(&root_abs_path, true); .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; root_entry.is_ignored = true;
state.insert_entry(root_entry.clone(), self.fs.as_ref()); state.insert_entry(root_entry.clone(), self.fs.as_ref());
} }
@ -3231,14 +3329,22 @@ impl BackgroundScanner {
return false; return false;
}; };
let parent_dir_is_loaded = relative_path.parent().map_or(true, |parent| { if !is_git_related(&abs_path) {
snapshot let parent_dir_is_loaded = relative_path.parent().map_or(true, |parent| {
.entry_for_path(parent) snapshot
.map_or(false, |entry| entry.kind == EntryKind::Dir) .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"); if !parent_dir_is_loaded {
return false; 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); relative_paths.push(relative_path);
@ -3401,18 +3507,26 @@ impl BackgroundScanner {
} }
async fn scan_dir(&self, job: &ScanJob) -> Result<()> { async fn scan_dir(&self, job: &ScanJob) -> Result<()> {
log::debug!("scan directory {:?}", job.path); let root_abs_path;
let mut ignore_stack;
let mut ignore_stack = job.ignore_stack.clone(); let mut new_ignore;
let mut new_ignore = None; let root_char_bag;
let (root_abs_path, root_char_bag, next_entry_id) = { let next_entry_id;
let snapshot = &self.state.lock().snapshot; {
( let state = self.state.lock();
snapshot.abs_path().clone(), let snapshot = &state.snapshot;
snapshot.root_char_bag, root_abs_path = snapshot.abs_path().clone();
self.next_entry_id.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 dotgit_path = None;
let mut root_canonical_path = None; let mut root_canonical_path = None;
@ -3427,18 +3541,8 @@ impl BackgroundScanner {
continue; continue;
} }
}; };
let child_name = child_abs_path.file_name().unwrap(); let child_name = child_abs_path.file_name().unwrap();
let child_path: Arc<Path> = job.path.join(child_name).into(); 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 we find a .gitignore, add it to the stack of ignores used to determine which paths are ignored
if child_name == *GITIGNORE { if child_name == *GITIGNORE {
match build_gitignore(&child_abs_path, self.fs.as_ref()).await { match build_gitignore(&child_abs_path, self.fs.as_ref()).await {
@ -3482,6 +3586,26 @@ impl BackgroundScanner {
dotgit_path = Some(child_path.clone()); 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( let mut child_entry = Entry::new(
child_path.clone(), child_path.clone(),
&child_metadata, &child_metadata,
@ -3662,19 +3786,16 @@ impl BackgroundScanner {
self.next_entry_id.as_ref(), self.next_entry_id.as_ref(),
state.snapshot.root_char_bag, 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); fs_entry.is_external = !canonical_path.starts_with(&root_canonical_path);
if !fs_entry.is_ignored { if !is_dir && !fs_entry.is_ignored {
if !fs_entry.is_dir() { if let Some((work_dir, repo)) = state.snapshot.local_repo_for_path(&path) {
if let Some((work_dir, repo)) = if let Ok(repo_path) = path.strip_prefix(work_dir.0) {
state.snapshot.local_repo_for_path(&path) let repo_path = RepoPath(repo_path.into());
{ let repo = repo.repo_ptr.lock();
if let Ok(repo_path) = path.strip_prefix(work_dir.0) { fs_entry.git_status = repo.status(&repo_path, fs_entry.mtime);
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() ignore_stack.clone()
}; };
// Scan any directories that were previously ignored and weren't // Scan any directories that were previously ignored and weren't previously scanned.
// previously scanned.
if was_ignored && !entry.is_ignored && entry.kind.is_unloaded() { if was_ignored && !entry.is_ignored && entry.kind.is_unloaded() {
let state = self.state.lock(); let state = self.state.lock();
if state.should_scan_directory(&entry) { 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 { fn char_bag_for_path(root_char_bag: CharBag, path: &Path) -> CharBag {
let mut result = root_char_bag; let mut result = root_char_bag;
result.extend( result.extend(

View file

@ -1,6 +1,7 @@
use crate::{ use crate::{
project_settings::ProjectSettings,
worktree::{Event, Snapshot, WorktreeModelHandle}, worktree::{Event, Snapshot, WorktreeModelHandle},
Entry, EntryKind, PathChange, Worktree, Entry, EntryKind, PathChange, Project, Worktree,
}; };
use anyhow::Result; use anyhow::Result;
use client::Client; use client::Client;
@ -12,6 +13,7 @@ use postage::stream::Stream;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use rand::prelude::*; use rand::prelude::*;
use serde_json::json; use serde_json::json;
use settings::SettingsStore;
use std::{ use std::{
env, env,
fmt::Write, fmt::Write,
@ -23,6 +25,7 @@ use util::{http::FakeHttpClient, test::temp_tree, ResultExt};
#[gpui::test] #[gpui::test]
async fn test_traversal(cx: &mut TestAppContext) { async fn test_traversal(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.background()); let fs = FakeFs::new(cx.background());
fs.insert_tree( fs.insert_tree(
"/root", "/root",
@ -78,6 +81,7 @@ async fn test_traversal(cx: &mut TestAppContext) {
#[gpui::test] #[gpui::test]
async fn test_descendent_entries(cx: &mut TestAppContext) { async fn test_descendent_entries(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.background()); let fs = FakeFs::new(cx.background());
fs.insert_tree( fs.insert_tree(
"/root", "/root",
@ -185,6 +189,7 @@ async fn test_descendent_entries(cx: &mut TestAppContext) {
#[gpui::test(iterations = 10)] #[gpui::test(iterations = 10)]
async fn test_circular_symlinks(executor: Arc<Deterministic>, cx: &mut TestAppContext) { async fn test_circular_symlinks(executor: Arc<Deterministic>, cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.background()); let fs = FakeFs::new(cx.background());
fs.insert_tree( fs.insert_tree(
"/root", "/root",
@ -264,6 +269,7 @@ async fn test_circular_symlinks(executor: Arc<Deterministic>, cx: &mut TestAppCo
#[gpui::test] #[gpui::test]
async fn test_symlinks_pointing_outside(cx: &mut TestAppContext) { async fn test_symlinks_pointing_outside(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.background()); let fs = FakeFs::new(cx.background());
fs.insert_tree( fs.insert_tree(
"/root", "/root",
@ -439,6 +445,7 @@ async fn test_symlinks_pointing_outside(cx: &mut TestAppContext) {
#[gpui::test] #[gpui::test]
async fn test_open_gitignored_files(cx: &mut TestAppContext) { async fn test_open_gitignored_files(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.background()); let fs = FakeFs::new(cx.background());
fs.insert_tree( fs.insert_tree(
"/root", "/root",
@ -599,6 +606,7 @@ async fn test_open_gitignored_files(cx: &mut TestAppContext) {
#[gpui::test] #[gpui::test]
async fn test_dirs_no_longer_ignored(cx: &mut TestAppContext) { async fn test_dirs_no_longer_ignored(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.background()); let fs = FakeFs::new(cx.background());
fs.insert_tree( fs.insert_tree(
"/root", "/root",
@ -722,6 +730,14 @@ async fn test_dirs_no_longer_ignored(cx: &mut TestAppContext) {
#[gpui::test(iterations = 10)] #[gpui::test(iterations = 10)]
async fn test_rescan_with_gitignore(cx: &mut TestAppContext) { 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()); let fs = FakeFs::new(cx.background());
fs.insert_tree( fs.insert_tree(
"/root", "/root",
@ -827,6 +843,7 @@ async fn test_rescan_with_gitignore(cx: &mut TestAppContext) {
#[gpui::test] #[gpui::test]
async fn test_write_file(cx: &mut TestAppContext) { async fn test_write_file(cx: &mut TestAppContext) {
init_test(cx);
let dir = temp_tree(json!({ let dir = temp_tree(json!({
".git": {}, ".git": {},
".gitignore": "ignored-dir\n", ".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)] #[gpui::test(iterations = 30)]
async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) { async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.background()); let fs = FakeFs::new(cx.background());
fs.insert_tree( fs.insert_tree(
"/root", "/root",
@ -938,6 +1052,7 @@ async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
#[gpui::test] #[gpui::test]
async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) { 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 client_fake = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
let fs_fake = FakeFs::new(cx.background()); let fs_fake = FakeFs::new(cx.background());
@ -1054,6 +1169,7 @@ async fn test_random_worktree_operations_during_initial_scan(
cx: &mut TestAppContext, cx: &mut TestAppContext,
mut rng: StdRng, mut rng: StdRng,
) { ) {
init_test(cx);
let operations = env::var("OPERATIONS") let operations = env::var("OPERATIONS")
.map(|o| o.parse().unwrap()) .map(|o| o.parse().unwrap())
.unwrap_or(5); .unwrap_or(5);
@ -1143,6 +1259,7 @@ async fn test_random_worktree_operations_during_initial_scan(
#[gpui::test(iterations = 100)] #[gpui::test(iterations = 100)]
async fn test_random_worktree_changes(cx: &mut TestAppContext, mut rng: StdRng) { async fn test_random_worktree_changes(cx: &mut TestAppContext, mut rng: StdRng) {
init_test(cx);
let operations = env::var("OPERATIONS") let operations = env::var("OPERATIONS")
.map(|o| o.parse().unwrap()) .map(|o| o.parse().unwrap())
.unwrap_or(40); .unwrap_or(40);
@ -1557,6 +1674,7 @@ fn random_filename(rng: &mut impl Rng) -> String {
#[gpui::test] #[gpui::test]
async fn test_rename_work_directory(cx: &mut TestAppContext) { async fn test_rename_work_directory(cx: &mut TestAppContext) {
init_test(cx);
let root = temp_tree(json!({ let root = temp_tree(json!({
"projects": { "projects": {
"project1": { "project1": {
@ -1627,6 +1745,7 @@ async fn test_rename_work_directory(cx: &mut TestAppContext) {
#[gpui::test] #[gpui::test]
async fn test_git_repository_for_path(cx: &mut TestAppContext) { async fn test_git_repository_for_path(cx: &mut TestAppContext) {
init_test(cx);
let root = temp_tree(json!({ let root = temp_tree(json!({
"c.txt": "", "c.txt": "",
"dir1": { "dir1": {
@ -1747,6 +1866,15 @@ async fn test_git_repository_for_path(cx: &mut TestAppContext) {
#[gpui::test] #[gpui::test]
async fn test_git_status(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) { 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"; const IGNORE_RULE: &'static str = "**/target";
let root = temp_tree(json!({ let root = temp_tree(json!({
@ -1935,6 +2063,7 @@ async fn test_git_status(deterministic: Arc<Deterministic>, cx: &mut TestAppCont
#[gpui::test] #[gpui::test]
async fn test_propagate_git_statuses(cx: &mut TestAppContext) { async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.background()); let fs = FakeFs::new(cx.background());
fs.insert_tree( fs.insert_tree(
"/root", "/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())) .map(|status| (status.path().unwrap().to_string(), status.status()))
.collect() .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);
});
}

View file

@ -20,10 +20,6 @@ impl IgnoreStack {
Arc::new(Self::All) 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> { pub fn append(self: Arc<Self>, abs_base_path: Arc<Path>, ignore: Arc<Gitignore>) -> Arc<Self> {
match self.as_ref() { match self.as_ref() {
IgnoreStack::All => self, IgnoreStack::All => self,

View file

@ -5618,7 +5618,16 @@ impl Project {
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let background = cx.background_executor().clone(); 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 { if path_count == 0 {
let (_, rx) = smol::channel::bounded(1024); let (_, rx) = smol::channel::bounded(1024);
return rx; return rx;
@ -5631,8 +5640,16 @@ impl Project {
.iter() .iter()
.filter_map(|(_, b)| { .filter_map(|(_, b)| {
let buffer = b.upgrade()?; let buffer = b.upgrade()?;
let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot()); let (is_ignored, snapshot) = buffer.update(cx, |buffer, cx| {
if let Some(path) = snapshot.file().map(|file| file.path()) { 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))) Some((path.clone(), (buffer, snapshot)))
} else { } else {
unnamed_files.push(buffer); unnamed_files.push(buffer);
@ -5806,7 +5823,12 @@ impl Project {
let mut snapshot_start_ix = 0; let mut snapshot_start_ix = 0;
let mut abs_path = PathBuf::new(); let mut abs_path = PathBuf::new();
for snapshot in snapshots { 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 { if worker_end_ix <= snapshot_start_ix {
break; break;
} else if worker_start_ix > snapshot_end_ix { } 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; cmp::min(worker_end_ix, snapshot_end_ix) - snapshot_start_ix;
for entry in snapshot for entry in snapshot
.files(false, start_in_snapshot) .files(query.include_ignored(), start_in_snapshot)
.take(end_in_snapshot - start_in_snapshot) .take(end_in_snapshot - start_in_snapshot)
{ {
if matching_paths_tx.is_closed() { if matching_paths_tx.is_closed() {

View file

@ -11,6 +11,8 @@ pub struct ProjectSettings {
pub lsp: HashMap<Arc<str>, LspSettings>, pub lsp: HashMap<Arc<str>, LspSettings>,
#[serde(default)] #[serde(default)]
pub git: GitSettings, pub git: GitSettings,
#[serde(default)]
pub file_scan_exclusions: Option<Vec<String>>,
} }
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] #[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]

View file

@ -3730,7 +3730,7 @@ async fn test_search(cx: &mut gpui::TestAppContext) {
assert_eq!( assert_eq!(
search( search(
&project, &project,
SearchQuery::text("TWO", false, true, Vec::new(), Vec::new()).unwrap(), SearchQuery::text("TWO", false, true, false, Vec::new(), Vec::new()).unwrap(),
cx cx
) )
.await .await
@ -3755,7 +3755,7 @@ async fn test_search(cx: &mut gpui::TestAppContext) {
assert_eq!( assert_eq!(
search( search(
&project, &project,
SearchQuery::text("TWO", false, true, Vec::new(), Vec::new()).unwrap(), SearchQuery::text("TWO", false, true, false, Vec::new(), Vec::new()).unwrap(),
cx cx
) )
.await .await
@ -3794,6 +3794,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
search_query, search_query,
false, false,
true, true,
false,
vec![PathMatcher::new("*.odd").unwrap()], vec![PathMatcher::new("*.odd").unwrap()],
Vec::new() Vec::new()
) )
@ -3813,6 +3814,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
search_query, search_query,
false, false,
true, true,
false,
vec![PathMatcher::new("*.rs").unwrap()], vec![PathMatcher::new("*.rs").unwrap()],
Vec::new() Vec::new()
) )
@ -3835,6 +3837,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
search_query, search_query,
false, false,
true, true,
false,
vec![ vec![
PathMatcher::new("*.ts").unwrap(), PathMatcher::new("*.ts").unwrap(),
PathMatcher::new("*.odd").unwrap(), PathMatcher::new("*.odd").unwrap(),
@ -3859,6 +3862,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
search_query, search_query,
false, false,
true, true,
false,
vec![ vec![
PathMatcher::new("*.rs").unwrap(), PathMatcher::new("*.rs").unwrap(),
PathMatcher::new("*.ts").unwrap(), PathMatcher::new("*.ts").unwrap(),
@ -3906,6 +3910,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
search_query, search_query,
false, false,
true, true,
false,
Vec::new(), Vec::new(),
vec![PathMatcher::new("*.odd").unwrap()], vec![PathMatcher::new("*.odd").unwrap()],
) )
@ -3930,6 +3935,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
search_query, search_query,
false, false,
true, true,
false,
Vec::new(), Vec::new(),
vec![PathMatcher::new("*.rs").unwrap()], vec![PathMatcher::new("*.rs").unwrap()],
) )
@ -3952,6 +3958,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
search_query, search_query,
false, false,
true, true,
false,
Vec::new(), Vec::new(),
vec![ vec![
PathMatcher::new("*.ts").unwrap(), PathMatcher::new("*.ts").unwrap(),
@ -3976,6 +3983,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
search_query, search_query,
false, false,
true, true,
false,
Vec::new(), Vec::new(),
vec![ vec![
PathMatcher::new("*.rs").unwrap(), PathMatcher::new("*.rs").unwrap(),
@ -4017,6 +4025,7 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
search_query, search_query,
false, false,
true, true,
false,
vec![PathMatcher::new("*.odd").unwrap()], vec![PathMatcher::new("*.odd").unwrap()],
vec![PathMatcher::new("*.odd").unwrap()], vec![PathMatcher::new("*.odd").unwrap()],
) )
@ -4036,6 +4045,7 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
search_query, search_query,
false, false,
true, true,
false,
vec![PathMatcher::new("*.ts").unwrap()], vec![PathMatcher::new("*.ts").unwrap()],
vec![PathMatcher::new("*.ts").unwrap()], vec![PathMatcher::new("*.ts").unwrap()],
).unwrap(), ).unwrap(),
@ -4054,6 +4064,7 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
search_query, search_query,
false, false,
true, true,
false,
vec![ vec![
PathMatcher::new("*.ts").unwrap(), PathMatcher::new("*.ts").unwrap(),
PathMatcher::new("*.odd").unwrap() PathMatcher::new("*.odd").unwrap()
@ -4079,6 +4090,7 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
search_query, search_query,
false, false,
true, true,
false,
vec![ vec![
PathMatcher::new("*.ts").unwrap(), PathMatcher::new("*.ts").unwrap(),
PathMatcher::new("*.odd").unwrap() PathMatcher::new("*.odd").unwrap()

View file

@ -39,6 +39,7 @@ pub enum SearchQuery {
replacement: Option<String>, replacement: Option<String>,
whole_word: bool, whole_word: bool,
case_sensitive: bool, case_sensitive: bool,
include_ignored: bool,
inner: SearchInputs, inner: SearchInputs,
}, },
@ -48,6 +49,7 @@ pub enum SearchQuery {
multiline: bool, multiline: bool,
whole_word: bool, whole_word: bool,
case_sensitive: bool, case_sensitive: bool,
include_ignored: bool,
inner: SearchInputs, inner: SearchInputs,
}, },
} }
@ -57,6 +59,7 @@ impl SearchQuery {
query: impl ToString, query: impl ToString,
whole_word: bool, whole_word: bool,
case_sensitive: bool, case_sensitive: bool,
include_ignored: bool,
files_to_include: Vec<PathMatcher>, files_to_include: Vec<PathMatcher>,
files_to_exclude: Vec<PathMatcher>, files_to_exclude: Vec<PathMatcher>,
) -> Result<Self> { ) -> Result<Self> {
@ -74,6 +77,7 @@ impl SearchQuery {
replacement: None, replacement: None,
whole_word, whole_word,
case_sensitive, case_sensitive,
include_ignored,
inner, inner,
}) })
} }
@ -82,6 +86,7 @@ impl SearchQuery {
query: impl ToString, query: impl ToString,
whole_word: bool, whole_word: bool,
case_sensitive: bool, case_sensitive: bool,
include_ignored: bool,
files_to_include: Vec<PathMatcher>, files_to_include: Vec<PathMatcher>,
files_to_exclude: Vec<PathMatcher>, files_to_exclude: Vec<PathMatcher>,
) -> Result<Self> { ) -> Result<Self> {
@ -111,6 +116,7 @@ impl SearchQuery {
multiline, multiline,
whole_word, whole_word,
case_sensitive, case_sensitive,
include_ignored,
inner, inner,
}) })
} }
@ -121,6 +127,7 @@ impl SearchQuery {
message.query, message.query,
message.whole_word, message.whole_word,
message.case_sensitive, message.case_sensitive,
message.include_ignored,
deserialize_path_matches(&message.files_to_include)?, deserialize_path_matches(&message.files_to_include)?,
deserialize_path_matches(&message.files_to_exclude)?, deserialize_path_matches(&message.files_to_exclude)?,
) )
@ -129,6 +136,7 @@ impl SearchQuery {
message.query, message.query,
message.whole_word, message.whole_word,
message.case_sensitive, message.case_sensitive,
message.include_ignored,
deserialize_path_matches(&message.files_to_include)?, deserialize_path_matches(&message.files_to_include)?,
deserialize_path_matches(&message.files_to_exclude)?, deserialize_path_matches(&message.files_to_exclude)?,
) )
@ -156,6 +164,7 @@ impl SearchQuery {
regex: self.is_regex(), regex: self.is_regex(),
whole_word: self.whole_word(), whole_word: self.whole_word(),
case_sensitive: self.case_sensitive(), case_sensitive: self.case_sensitive(),
include_ignored: self.include_ignored(),
files_to_include: self files_to_include: self
.files_to_include() .files_to_include()
.iter() .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 { pub fn is_regex(&self) -> bool {
matches!(self, Self::Regex { .. }) matches!(self, Self::Regex { .. })
} }

View file

@ -1,5 +1,6 @@
use crate::{ 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 ::ignore::gitignore::{Gitignore, GitignoreBuilder};
use anyhow::{anyhow, Context as _, Result}; use anyhow::{anyhow, Context as _, Result};
@ -25,6 +26,7 @@ use gpui::{
AppContext, AsyncAppContext, BackgroundExecutor, Context, EventEmitter, Model, ModelContext, AppContext, AsyncAppContext, BackgroundExecutor, Context, EventEmitter, Model, ModelContext,
Task, Task,
}; };
use itertools::Itertools;
use language::{ use language::{
proto::{ proto::{
deserialize_fingerprint, deserialize_version, serialize_fingerprint, serialize_line_ending, deserialize_fingerprint, deserialize_version, serialize_fingerprint, serialize_line_ending,
@ -39,6 +41,7 @@ use postage::{
prelude::{Sink as _, Stream as _}, prelude::{Sink as _, Stream as _},
watch, watch,
}; };
use settings::{Settings, SettingsStore};
use smol::channel::{self, Sender}; use smol::channel::{self, Sender};
use std::{ use std::{
any::Any, any::Any,
@ -58,7 +61,10 @@ use std::{
time::{Duration, SystemTime}, time::{Duration, SystemTime},
}; };
use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet}; 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)] #[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)]
pub struct WorktreeId(usize); pub struct WorktreeId(usize);
@ -73,7 +79,7 @@ pub struct LocalWorktree {
scan_requests_tx: channel::Sender<ScanRequest>, scan_requests_tx: channel::Sender<ScanRequest>,
path_prefixes_to_scan_tx: channel::Sender<Arc<Path>>, path_prefixes_to_scan_tx: channel::Sender<Arc<Path>>,
is_scanning: (watch::Sender<bool>, watch::Receiver<bool>), is_scanning: (watch::Sender<bool>, watch::Receiver<bool>),
_background_scanner_task: Task<()>, _background_scanner_tasks: Vec<Task<()>>,
share: Option<ShareState>, share: Option<ShareState>,
diagnostics: HashMap< diagnostics: HashMap<
Arc<Path>, Arc<Path>,
@ -219,6 +225,7 @@ pub struct LocalSnapshot {
/// All of the git repositories in the worktree, indexed by the project entry /// All of the git repositories in the worktree, indexed by the project entry
/// id of their parent directory. /// id of their parent directory.
git_repositories: TreeMap<ProjectEntryId, LocalRepositoryEntry>, git_repositories: TreeMap<ProjectEntryId, LocalRepositoryEntry>,
file_scan_exclusions: Vec<PathMatcher>,
} }
struct BackgroundScannerState { struct BackgroundScannerState {
@ -302,17 +309,56 @@ impl Worktree {
.await .await
.context("failed to stat worktree path")?; .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.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 let root_name = abs_path
.file_name() .file_name()
.map_or(String::new(), |f| f.to_string_lossy().to_string()); .map_or(String::new(), |f| f.to_string_lossy().to_string());
let mut snapshot = LocalSnapshot { let mut snapshot = LocalSnapshot {
file_scan_exclusions: file_scan_exclusions(ProjectSettings::get_global(cx)),
ignores_by_parent_abs_path: Default::default(), ignores_by_parent_abs_path: Default::default(),
git_repositories: Default::default(), git_repositories: Default::default(),
snapshot: Snapshot { snapshot: Snapshot {
id: WorktreeId::from_usize(cx.entity_id().as_u64() as usize), 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_name: root_name.clone(),
root_char_bag: root_name.chars().map(|c| c.to_ascii_lowercase()).collect(), root_char_bag: root_name.chars().map(|c| c.to_ascii_lowercase()).collect(),
entries_by_path: Default::default(), entries_by_path: Default::default(),
@ -337,61 +383,22 @@ impl Worktree {
let (scan_requests_tx, scan_requests_rx) = channel::unbounded(); let (scan_requests_tx, scan_requests_rx) = channel::unbounded();
let (path_prefixes_to_scan_tx, path_prefixes_to_scan_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(); let task_snapshot = snapshot.clone();
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;
}
});
Worktree::Local(LocalWorktree { Worktree::Local(LocalWorktree {
snapshot, snapshot,
is_scanning: watch::channel_with(true), is_scanning: watch::channel_with(true),
share: None, share: None,
scan_requests_tx, scan_requests_tx,
path_prefixes_to_scan_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(), diagnostics: Default::default(),
diagnostic_summaries: Default::default(), diagnostic_summaries: Default::default(),
client, 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 { impl LocalWorktree {
pub fn contains_abs_path(&self, path: &Path) -> bool { pub fn contains_abs_path(&self, path: &Path) -> bool {
path.starts_with(&self.abs_path) path.starts_with(&self.abs_path)
@ -1482,7 +1560,7 @@ impl Snapshot {
self.entries_by_id.get(&entry_id, &()).is_some() 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 entry = Entry::try_from((&self.root_char_bag, entry))?;
let old_entry = self.entries_by_id.insert_or_replace( let old_entry = self.entries_by_id.insert_or_replace(
PathEntry { PathEntry {
@ -2143,6 +2221,12 @@ impl LocalSnapshot {
paths.sort_by(|a, b| a.0.cmp(b.0)); paths.sort_by(|a, b| a.0.cmp(b.0));
paths 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 { impl BackgroundScannerState {
@ -2165,7 +2249,7 @@ impl BackgroundScannerState {
let ignore_stack = self.snapshot.ignore_stack_for_abs_path(&abs_path, true); 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 ancestor_inodes = self.snapshot.ancestor_inodes_for_path(&path);
let mut containing_repository = None; 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 Some((workdir_path, repo)) = self.snapshot.local_repo_for_path(&path) {
if let Ok(repo_path) = path.strip_prefix(&workdir_path.0) { if let Ok(repo_path) = path.strip_prefix(&workdir_path.0) {
containing_repository = Some(( containing_repository = Some((
@ -2376,18 +2460,30 @@ impl BackgroundScannerState {
// Remove any git repositories whose .git entry no longer exists. // Remove any git repositories whose .git entry no longer exists.
let snapshot = &mut self.snapshot; let snapshot = &mut self.snapshot;
let mut repositories = mem::take(&mut snapshot.git_repositories); let mut ids_to_preserve = HashSet::default();
let mut repository_entries = mem::take(&mut snapshot.repository_entries); for (&work_directory_id, entry) in snapshot.git_repositories.iter() {
repositories.retain(|work_directory_id, _| { let exists_in_snapshot = snapshot
snapshot .entry_for_id(work_directory_id)
.entry_for_id(*work_directory_id)
.map_or(false, |entry| { .map_or(false, |entry| {
snapshot.entry_for_path(entry.path.join(*DOT_GIT)).is_some() snapshot.entry_for_path(entry.path.join(*DOT_GIT)).is_some()
}) });
}); if exists_in_snapshot {
repository_entries.retain(|_, entry| repositories.get(&entry.work_directory.0).is_some()); ids_to_preserve.insert(work_directory_id);
snapshot.git_repositories = repositories; } else {
snapshot.repository_entries = repository_entries; 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( fn build_git_repository(
@ -3085,7 +3181,7 @@ impl BackgroundScanner {
let ignore_stack = state let ignore_stack = state
.snapshot .snapshot
.ignore_stack_for_abs_path(&root_abs_path, true); .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; root_entry.is_ignored = true;
state.insert_entry(root_entry.clone(), self.fs.as_ref()); state.insert_entry(root_entry.clone(), self.fs.as_ref());
} }
@ -3222,14 +3318,22 @@ impl BackgroundScanner {
return false; return false;
}; };
let parent_dir_is_loaded = relative_path.parent().map_or(true, |parent| { if !is_git_related(&abs_path) {
snapshot let parent_dir_is_loaded = relative_path.parent().map_or(true, |parent| {
.entry_for_path(parent) snapshot
.map_or(false, |entry| entry.kind == EntryKind::Dir) .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"); if !parent_dir_is_loaded {
return false; 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); relative_paths.push(relative_path);
@ -3392,18 +3496,26 @@ impl BackgroundScanner {
} }
async fn scan_dir(&self, job: &ScanJob) -> Result<()> { async fn scan_dir(&self, job: &ScanJob) -> Result<()> {
log::debug!("scan directory {:?}", job.path); let root_abs_path;
let mut ignore_stack;
let mut ignore_stack = job.ignore_stack.clone(); let mut new_ignore;
let mut new_ignore = None; let root_char_bag;
let (root_abs_path, root_char_bag, next_entry_id) = { let next_entry_id;
let snapshot = &self.state.lock().snapshot; {
( let state = self.state.lock();
snapshot.abs_path().clone(), let snapshot = &state.snapshot;
snapshot.root_char_bag, root_abs_path = snapshot.abs_path().clone();
self.next_entry_id.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 dotgit_path = None;
let mut root_canonical_path = None; let mut root_canonical_path = None;
@ -3418,18 +3530,8 @@ impl BackgroundScanner {
continue; continue;
} }
}; };
let child_name = child_abs_path.file_name().unwrap(); let child_name = child_abs_path.file_name().unwrap();
let child_path: Arc<Path> = job.path.join(child_name).into(); 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 we find a .gitignore, add it to the stack of ignores used to determine which paths are ignored
if child_name == *GITIGNORE { if child_name == *GITIGNORE {
match build_gitignore(&child_abs_path, self.fs.as_ref()).await { match build_gitignore(&child_abs_path, self.fs.as_ref()).await {
@ -3473,6 +3575,26 @@ impl BackgroundScanner {
dotgit_path = Some(child_path.clone()); 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( let mut child_entry = Entry::new(
child_path.clone(), child_path.clone(),
&child_metadata, &child_metadata,
@ -3653,19 +3775,16 @@ impl BackgroundScanner {
self.next_entry_id.as_ref(), self.next_entry_id.as_ref(),
state.snapshot.root_char_bag, 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); fs_entry.is_external = !canonical_path.starts_with(&root_canonical_path);
if !fs_entry.is_ignored { if !is_dir && !fs_entry.is_ignored {
if !fs_entry.is_dir() { if let Some((work_dir, repo)) = state.snapshot.local_repo_for_path(&path) {
if let Some((work_dir, repo)) = if let Ok(repo_path) = path.strip_prefix(work_dir.0) {
state.snapshot.local_repo_for_path(&path) let repo_path = RepoPath(repo_path.into());
{ let repo = repo.repo_ptr.lock();
if let Ok(repo_path) = path.strip_prefix(work_dir.0) { fs_entry.git_status = repo.status(&repo_path, fs_entry.mtime);
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() ignore_stack.clone()
}; };
// Scan any directories that were previously ignored and weren't // Scan any directories that were previously ignored and weren't previously scanned.
// previously scanned.
if was_ignored && !entry.is_ignored && entry.kind.is_unloaded() { if was_ignored && !entry.is_ignored && entry.kind.is_unloaded() {
let state = self.state.lock(); let state = self.state.lock();
if state.should_scan_directory(&entry) { 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 { fn char_bag_for_path(root_char_bag: CharBag, path: &Path) -> CharBag {
let mut result = root_char_bag; let mut result = root_char_bag;
result.extend( result.extend(

File diff suppressed because it is too large Load diff

View file

@ -1732,7 +1732,7 @@ mod tests {
use super::*; use super::*;
use gpui::{AnyWindowHandle, TestAppContext, ViewHandle, WindowHandle}; use gpui::{AnyWindowHandle, TestAppContext, ViewHandle, WindowHandle};
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use project::FakeFs; use project::{project_settings::ProjectSettings, FakeFs};
use serde_json::json; use serde_json::json;
use settings::SettingsStore; use settings::SettingsStore;
use std::{ 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)] #[gpui::test(iterations = 30)]
async fn test_editing_files(cx: &mut gpui::TestAppContext) { async fn test_editing_files(cx: &mut gpui::TestAppContext) {
init_test(cx); init_test(cx);
@ -2929,6 +3046,12 @@ mod tests {
workspace::init_settings(cx); workspace::init_settings(cx);
client::init_settings(cx); client::init_settings(cx);
Project::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());
});
});
}); });
} }

View file

@ -1,6 +1,6 @@
pub mod file_associations; pub mod file_associations;
mod project_panel_settings; mod project_panel_settings;
use settings::Settings; use settings::{Settings, SettingsStore};
use db::kvp::KEY_VALUE_STORE; use db::kvp::KEY_VALUE_STORE;
use editor::{scroll::autoscroll::Autoscroll, Cancel, Editor}; use editor::{scroll::autoscroll::Autoscroll, Cancel, Editor};
@ -34,7 +34,7 @@ use ui::{h_stack, v_stack, IconElement, Label};
use unicase::UniCase; use unicase::UniCase;
use util::{maybe, ResultExt, TryFutureExt}; use util::{maybe, ResultExt, TryFutureExt};
use workspace::{ use workspace::{
dock::{DockPosition, PanelEvent}, dock::{DockPosition, Panel, PanelEvent},
Workspace, Workspace,
}; };
@ -148,7 +148,6 @@ pub enum Event {
SplitEntry { SplitEntry {
entry_id: ProjectEntryId, entry_id: ProjectEntryId,
}, },
DockPositionChanged,
Focus, Focus,
NewSearchInDirectory { NewSearchInDirectory {
dir_entry: Entry, dir_entry: Entry,
@ -200,10 +199,11 @@ impl ProjectPanel {
let filename_editor = cx.build_view(|cx| Editor::single_line(cx)); let filename_editor = cx.build_view(|cx| Editor::single_line(cx));
cx.subscribe(&filename_editor, |this, _, event, cx| match event { cx.subscribe(&filename_editor, |this, _, event, cx| match event {
editor::Event::BufferEdited | editor::Event::SelectionsChanged { .. } => { editor::EditorEvent::BufferEdited
| editor::EditorEvent::SelectionsChanged { .. } => {
this.autoscroll(cx); this.autoscroll(cx);
} }
editor::Event::Blurred => { editor::EditorEvent::Blurred => {
if this if this
.edit_state .edit_state
.as_ref() .as_ref()
@ -244,16 +244,17 @@ impl ProjectPanel {
this.update_visible_entries(None, cx); this.update_visible_entries(None, cx);
// Update the dock position when the setting changes. // Update the dock position when the setting changes.
// todo!() let mut old_dock_position = this.position(cx);
// let mut old_dock_position = this.position(cx); ProjectPanelSettings::register(cx);
// cx.observe_global::<SettingsStore, _>(move |this, cx| { cx.observe_global::<SettingsStore>(move |this, cx| {
// let new_dock_position = this.position(cx); dbg!("OLA!");
// if new_dock_position != old_dock_position { let new_dock_position = this.position(cx);
// old_dock_position = new_dock_position; if new_dock_position != old_dock_position {
// cx.emit(Event::DockPositionChanged); old_dock_position = new_dock_position;
// } cx.emit(PanelEvent::ChangePosition);
// }) }
// .detach(); })
.detach();
this this
}); });
@ -1485,7 +1486,7 @@ impl EventEmitter<Event> for ProjectPanel {}
impl EventEmitter<PanelEvent> for ProjectPanel {} impl EventEmitter<PanelEvent> for ProjectPanel {}
impl workspace::dock::Panel for ProjectPanel { impl Panel for ProjectPanel {
fn position(&self, cx: &WindowContext) -> DockPosition { fn position(&self, cx: &WindowContext) -> DockPosition {
match ProjectPanelSettings::get_global(cx).dock { match ProjectPanelSettings::get_global(cx).dock {
ProjectPanelDockPosition::Left => DockPosition::Left, ProjectPanelDockPosition::Left => DockPosition::Left,
@ -1571,7 +1572,7 @@ mod tests {
use super::*; use super::*;
use gpui::{TestAppContext, View, VisualTestContext, WindowHandle}; use gpui::{TestAppContext, View, VisualTestContext, WindowHandle};
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use project::FakeFs; use project::{project_settings::ProjectSettings, FakeFs};
use serde_json::json; use serde_json::json;
use settings::SettingsStore; use settings::SettingsStore;
use std::{ 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)] #[gpui::test(iterations = 30)]
async fn test_editing_files(cx: &mut gpui::TestAppContext) { async fn test_editing_files(cx: &mut gpui::TestAppContext) {
init_test(cx); init_test(cx);
@ -2792,6 +2911,12 @@ mod tests {
workspace::init_settings(cx); workspace::init_settings(cx);
client::init_settings(cx); client::init_settings(cx);
Project::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());
});
});
}); });
} }

View file

@ -884,6 +884,7 @@ message SearchProject {
bool case_sensitive = 5; bool case_sensitive = 5;
string files_to_include = 6; string files_to_include = 6;
string files_to_exclude = 7; string files_to_exclude = 7;
bool include_ignored = 8;
} }
message SearchProjectResponse { message SearchProjectResponse {

View file

@ -884,6 +884,7 @@ message SearchProject {
bool case_sensitive = 5; bool case_sensitive = 5;
string files_to_include = 6; string files_to_include = 6;
string files_to_exclude = 7; string files_to_exclude = 7;
bool include_ignored = 8;
} }
message SearchProjectResponse { message SearchProjectResponse {

View file

@ -805,6 +805,7 @@ impl BufferSearchBar {
query, query,
self.search_options.contains(SearchOptions::WHOLE_WORD), self.search_options.contains(SearchOptions::WHOLE_WORD),
self.search_options.contains(SearchOptions::CASE_SENSITIVE), self.search_options.contains(SearchOptions::CASE_SENSITIVE),
false,
Vec::new(), Vec::new(),
Vec::new(), Vec::new(),
) { ) {
@ -820,6 +821,7 @@ impl BufferSearchBar {
query, query,
self.search_options.contains(SearchOptions::WHOLE_WORD), self.search_options.contains(SearchOptions::WHOLE_WORD),
self.search_options.contains(SearchOptions::CASE_SENSITIVE), self.search_options.contains(SearchOptions::CASE_SENSITIVE),
false,
Vec::new(), Vec::new(),
Vec::new(), Vec::new(),
) { ) {

View file

@ -4,7 +4,7 @@ use crate::{
search_bar::{render_nav_button, render_option_button_icon, render_search_mode_button}, search_bar::{render_nav_button, render_option_button_icon, render_search_mode_button},
ActivateRegexMode, ActivateSemanticMode, ActivateTextMode, CycleMode, NextHistoryQuery, ActivateRegexMode, ActivateSemanticMode, ActivateTextMode, CycleMode, NextHistoryQuery,
PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions, SelectNextMatch, SelectPrevMatch, PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions, SelectNextMatch, SelectPrevMatch,
ToggleCaseSensitive, ToggleReplace, ToggleWholeWord, ToggleCaseSensitive, ToggleIncludeIgnored, ToggleReplace, ToggleWholeWord,
}; };
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use collections::HashMap; use collections::HashMap;
@ -85,6 +85,7 @@ pub fn init(cx: &mut AppContext) {
cx.capture_action(ProjectSearchView::replace_next); cx.capture_action(ProjectSearchView::replace_next);
add_toggle_option_action::<ToggleCaseSensitive>(SearchOptions::CASE_SENSITIVE, cx); add_toggle_option_action::<ToggleCaseSensitive>(SearchOptions::CASE_SENSITIVE, cx);
add_toggle_option_action::<ToggleWholeWord>(SearchOptions::WHOLE_WORD, 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); add_toggle_filters_action::<ToggleFilters>(cx);
} }
@ -1192,6 +1193,7 @@ impl ProjectSearchView {
text, text,
self.search_options.contains(SearchOptions::WHOLE_WORD), self.search_options.contains(SearchOptions::WHOLE_WORD),
self.search_options.contains(SearchOptions::CASE_SENSITIVE), self.search_options.contains(SearchOptions::CASE_SENSITIVE),
self.search_options.contains(SearchOptions::INCLUDE_IGNORED),
included_files, included_files,
excluded_files, excluded_files,
) { ) {
@ -1210,6 +1212,7 @@ impl ProjectSearchView {
text, text,
self.search_options.contains(SearchOptions::WHOLE_WORD), self.search_options.contains(SearchOptions::WHOLE_WORD),
self.search_options.contains(SearchOptions::CASE_SENSITIVE), self.search_options.contains(SearchOptions::CASE_SENSITIVE),
self.search_options.contains(SearchOptions::INCLUDE_IGNORED),
included_files, included_files,
excluded_files, excluded_files,
) { ) {
@ -1764,6 +1767,17 @@ impl View for ProjectSearchBar {
render_option_button_icon("icons/word_search.svg", SearchOptions::WHOLE_WORD, cx) 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 search_button_for_mode = |mode, side, cx: &mut ViewContext<ProjectSearchBar>| {
let is_active = if let Some(search) = self.active_project_search.as_ref() { let is_active = if let Some(search) = self.active_project_search.as_ref() {
let search = search.read(cx); let search = search.read(cx);
@ -1879,7 +1893,15 @@ impl View for ProjectSearchBar {
.with_children(search.filters_enabled.then(|| { .with_children(search.filters_enabled.then(|| {
Flex::row() Flex::row()
.with_child( .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() .contained()
.with_style(include_container_style) .with_style(include_container_style)
.constrained() .constrained()

View file

@ -29,6 +29,7 @@ actions!(
CycleMode, CycleMode,
ToggleWholeWord, ToggleWholeWord,
ToggleCaseSensitive, ToggleCaseSensitive,
ToggleIncludeIgnored,
ToggleReplace, ToggleReplace,
SelectNextMatch, SelectNextMatch,
SelectPrevMatch, SelectPrevMatch,
@ -49,31 +50,35 @@ bitflags! {
const NONE = 0b000; const NONE = 0b000;
const WHOLE_WORD = 0b001; const WHOLE_WORD = 0b001;
const CASE_SENSITIVE = 0b010; const CASE_SENSITIVE = 0b010;
const INCLUDE_IGNORED = 0b100;
} }
} }
impl SearchOptions { impl SearchOptions {
pub fn label(&self) -> &'static str { pub fn label(&self) -> &'static str {
match *self { match *self {
SearchOptions::WHOLE_WORD => "Match Whole Word", Self::WHOLE_WORD => "Match Whole Word",
SearchOptions::CASE_SENSITIVE => "Match Case", Self::CASE_SENSITIVE => "Match Case",
_ => panic!("{:?} is not a named SearchOption", self), Self::INCLUDE_IGNORED => "Include Ignored",
_ => panic!("{self:?} is not a named SearchOption"),
} }
} }
pub fn icon(&self) -> &'static str { pub fn icon(&self) -> &'static str {
match *self { match *self {
SearchOptions::WHOLE_WORD => "icons/word_search.svg", Self::WHOLE_WORD => "icons/word_search.svg",
SearchOptions::CASE_SENSITIVE => "icons/case_insensitive.svg", Self::CASE_SENSITIVE => "icons/case_insensitive.svg",
_ => panic!("{:?} is not a named SearchOption", self), Self::INCLUDE_IGNORED => "icons/case_insensitive.svg",
_ => panic!("{self:?} is not a named SearchOption"),
} }
} }
pub fn to_toggle_action(&self) -> Box<dyn Action> { pub fn to_toggle_action(&self) -> Box<dyn Action> {
match *self { match *self {
SearchOptions::WHOLE_WORD => Box::new(ToggleWholeWord), Self::WHOLE_WORD => Box::new(ToggleWholeWord),
SearchOptions::CASE_SENSITIVE => Box::new(ToggleCaseSensitive), Self::CASE_SENSITIVE => Box::new(ToggleCaseSensitive),
_ => panic!("{:?} is not a named SearchOption", self), Self::INCLUDE_IGNORED => Box::new(ToggleIncludeIgnored),
_ => panic!("{self:?} is not a named SearchOption"),
} }
} }
@ -85,6 +90,7 @@ impl SearchOptions {
let mut options = SearchOptions::NONE; let mut options = SearchOptions::NONE;
options.set(SearchOptions::WHOLE_WORD, query.whole_word()); options.set(SearchOptions::WHOLE_WORD, query.whole_word());
options.set(SearchOptions::CASE_SENSITIVE, query.case_sensitive()); options.set(SearchOptions::CASE_SENSITIVE, query.case_sensitive());
options.set(SearchOptions::INCLUDE_IGNORED, query.include_ignored());
options options
} }

View file

@ -77,6 +77,7 @@ pub fn handle_settings_file_changes(
}); });
cx.spawn(move |mut cx| async move { cx.spawn(move |mut cx| async move {
while let Some(user_settings_content) = user_settings_file_rx.next().await { 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| { let result = cx.update_global(|store: &mut SettingsStore, cx| {
store store
.set_user_settings(&user_settings_content, cx) .set_user_settings(&user_settings_content, cx)

View file

@ -1,4 +1,7 @@
use gpui::{div, white, Div, ParentElement, Render, Styled, View, VisualContext, WindowContext}; use gpui::{
blue, div, red, white, Div, ParentElement, Render, Styled, View, VisualContext, WindowContext,
};
use ui::v_stack;
pub struct TextStory; pub struct TextStory;
@ -12,10 +15,46 @@ impl Render<Self> for TextStory {
type Element = Div<Self>; type Element = Div<Self>;
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element { fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element {
div().size_full().bg(white()).child(concat!( v_stack()
"The quick brown fox jumps over the lazy dog. ", .bg(blue())
"Meanwhile, the lazy dog decided it was time for a change. ", .child(
"He started daily workout routines, ate healthier and became the fastest dog in town.", 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.",
)))
} }
} }

View file

@ -1,9 +1,9 @@
use anyhow::Result; use anyhow::Result;
use gpui::AssetSource;
use gpui::{ use gpui::{
div, px, size, AnyView, Bounds, Div, Render, ViewContext, VisualContext, WindowBounds, div, px, size, AnyView, Bounds, Div, Render, ViewContext, VisualContext, WindowBounds,
WindowOptions, WindowOptions,
}; };
use gpui::{white, AssetSource};
use settings::{default_settings, Settings, SettingsStore}; use settings::{default_settings, Settings, SettingsStore};
use std::borrow::Cow; use std::borrow::Cow;
use std::sync::Arc; use std::sync::Arc;
@ -56,6 +56,7 @@ fn main() {
} }
struct TestView { struct TestView {
#[allow(unused)]
story: AnyView, story: AnyView,
} }
@ -65,9 +66,22 @@ impl Render<Self> for TestView {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element { fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
div() div()
.flex() .flex()
.bg(gpui::blue())
.flex_col() .flex_col()
.size_full() .size_full()
.font("Helvetica") .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.",
))),
)
} }
} }

View file

@ -31,7 +31,7 @@ use workspace::{
notifications::NotifyResultExt, notifications::NotifyResultExt,
register_deserializable_item, register_deserializable_item,
searchable::{SearchEvent, SearchOptions, SearchableItem}, searchable::{SearchEvent, SearchOptions, SearchableItem},
ui::{ContextMenu, Label}, ui::{ContextMenu, Icon, IconElement, Label, ListEntry},
CloseActiveItem, NewCenterTerminal, Pane, ToolbarItemLocation, Workspace, WorkspaceId, CloseActiveItem, NewCenterTerminal, Pane, ToolbarItemLocation, Workspace, WorkspaceId,
}; };
@ -84,7 +84,7 @@ pub struct TerminalView {
has_new_content: bool, has_new_content: bool,
//Currently using iTerm bell, show bell emoji in tab until input is received //Currently using iTerm bell, show bell emoji in tab until input is received
has_bell: bool, has_bell: bool,
context_menu: Option<View<ContextMenu>>, context_menu: Option<View<ContextMenu<Self>>>,
blink_state: bool, blink_state: bool,
blinking_on: bool, blinking_on: bool,
blinking_paused: bool, blinking_paused: bool,
@ -299,11 +299,10 @@ impl TerminalView {
position: gpui::Point<Pixels>, position: gpui::Point<Pixels>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) {
self.context_menu = Some(cx.build_view(|cx| { self.context_menu = Some(ContextMenu::build(cx, |menu, _| {
ContextMenu::new(cx) menu.action(ListEntry::new(Label::new("Clear")), Box::new(Clear))
.entry(Label::new("Clear"), Box::new(Clear)) .action(
.entry( ListEntry::new(Label::new("Close")),
Label::new("Close"),
Box::new(CloseActiveItem { save_intent: None }), Box::new(CloseActiveItem { save_intent: None }),
) )
})); }));
@ -755,8 +754,8 @@ impl Item for TerminalView {
let title = self.terminal().read(cx).title(); let title = self.terminal().read(cx).title();
div() div()
.child(img().uri("icons/terminal.svg").bg(red())) .child(IconElement::new(Icon::Terminal))
.child(SharedString::from(title)) .child(title)
.into_any() .into_any()
} }

View file

@ -1,6 +1,6 @@
use gpui::Hsla; use gpui::Hsla;
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy, Default)]
pub struct PlayerColor { pub struct PlayerColor {
pub cursor: Hsla, pub cursor: Hsla,
pub background: Hsla, pub background: Hsla,

View file

@ -130,7 +130,7 @@ impl Theme {
} }
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug, Default)]
pub struct DiagnosticStyle { pub struct DiagnosticStyle {
pub error: Hsla, pub error: Hsla,
pub warning: Hsla, pub warning: Hsla,

View file

@ -18,5 +18,5 @@ theme2 = { path = "../theme2" }
rand = "0.8" rand = "0.8"
[features] [features]
default = ["stories"] default = []
stories = ["dep:itertools"] stories = ["dep:itertools"]

View file

@ -4,58 +4,101 @@ use std::rc::Rc;
use crate::prelude::*; use crate::prelude::*;
use crate::{v_stack, Label, List, ListEntry, ListItem, ListSeparator, ListSubHeader}; use crate::{v_stack, Label, List, ListEntry, ListItem, ListSeparator, ListSubHeader};
use gpui::{ use gpui::{
<<<<<<< HEAD
overlay, px, Action, AnchorCorner, AnyElement, Bounds, Dismiss, DispatchPhase, Div, overlay, px, Action, AnchorCorner, AnyElement, Bounds, Dismiss, DispatchPhase, Div,
FocusHandle, LayoutId, ManagedView, MouseButton, MouseDownEvent, Pixels, Point, Render, FocusHandle, LayoutId, ManagedView, MouseButton, MouseDownEvent, Pixels, Point, Render,
RenderOnce, View, RenderOnce, View,
=======
overlay, px, Action, AnchorCorner, AnyElement, AppContext, Bounds, DispatchPhase, Div,
EventEmitter, FocusHandle, FocusableView, LayoutId, ManagedView, Manager, MouseButton,
MouseDownEvent, Pixels, Point, Render, View, VisualContext, WeakView,
>>>>>>> main
}; };
pub struct ContextMenu { pub enum ContextMenuItem<V> {
items: Vec<ListItem>, Separator(ListSeparator),
focus_handle: FocusHandle, Header(ListSubHeader),
Entry(
ListEntry<ContextMenu<V>>,
Rc<dyn Fn(&mut V, &mut ViewContext<V>)>,
),
} }
impl ManagedView for ContextMenu { pub struct ContextMenu<V> {
fn focus_handle(&self, cx: &gpui::AppContext) -> FocusHandle { 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() self.focus_handle.clone()
} }
} }
impl ContextMenu { impl<V: Render> EventEmitter<Manager> for ContextMenu<V> {}
pub fn new(cx: &mut WindowContext) -> Self {
Self { impl<V: Render> ContextMenu<V> {
items: Default::default(), pub fn build(
focus_handle: cx.focus_handle(), 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 { 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 self
} }
pub fn separator(mut self) -> Self { pub fn separator(mut self) -> Self {
self.items.push(ListItem::Separator(ListSeparator)); self.items.push(ContextMenuItem::Separator(ListSeparator));
self self
} }
pub fn entry(mut self, label: Label, action: Box<dyn Action>) -> Self { pub fn entry(
self.items.push(ListEntry::new(label).action(action).into()); 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 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>) { pub fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
// todo!() // todo!()
cx.emit(Dismiss); cx.emit(Manager::Dismiss);
} }
pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) { pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
cx.emit(Dismiss); cx.emit(Manager::Dismiss);
} }
} }
<<<<<<< HEAD
impl Render<Self> for ContextMenu { impl Render<Self> for ContextMenu {
=======
impl<V: Render> Render for ContextMenu<V> {
>>>>>>> main
type Element = Div<Self>; type Element = Div<Self>;
// todo!()
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element { fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
div().elevation_2(cx).flex().flex_row().child( div().elevation_2(cx).flex().flex_row().child(
v_stack() v_stack()
@ -72,7 +115,25 @@ impl Render<Self> for ContextMenu {
// .bg(cx.theme().colors().elevated_surface_background) // .bg(cx.theme().colors().elevated_surface_background)
// .border() // .border()
// .border_color(cx.theme().colors().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(),
)),
) )
} }
} }
@ -218,12 +279,13 @@ impl<V: 'static, M: ManagedView> Element<V> for MenuHandle<V, M> {
let new_menu = (builder)(view_state, cx); let new_menu = (builder)(view_state, cx);
let menu2 = menu.clone(); let menu2 = menu.clone();
cx.subscribe(&new_menu, move |this, modal, e, cx| match e { cx.subscribe(&new_menu, move |this, modal, e, cx| match e {
&Dismiss => { &Manager::Dismiss => {
*menu2.borrow_mut() = None; *menu2.borrow_mut() = None;
cx.notify(); cx.notify();
} }
}) })
.detach(); .detach();
cx.focus_view(&new_menu);
*menu.borrow_mut() = Some(new_menu); *menu.borrow_mut() = Some(new_menu);
*position.borrow_mut() = if attach.is_some() && child_layout_id.is_some() { *position.borrow_mut() = if attach.is_some() && child_layout_id.is_some() {
@ -258,16 +320,25 @@ pub use stories::*;
mod stories { mod stories {
use super::*; use super::*;
use crate::story::Story; 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> { fn build_menu<V: Render>(
cx.build_view(|cx| { cx: &mut ViewContext<V>,
ContextMenu::new(cx).header(header).separator().entry( header: impl Into<SharedString>,
Label::new("Print current time"), ) -> View<ContextMenu<V>> {
PrintCurrentDate.boxed_clone(), 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())
})
}) })
} }
@ -279,10 +350,14 @@ mod stories {
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element { fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
Story::container(cx) Story::container(cx)
.on_action(|_, _: &PrintCurrentDate, _| { .on_action(|_, _: &PrintCurrentDate, _| {
println!("printing unix time!");
if let Ok(unix_time) = std::time::UNIX_EPOCH.elapsed() { if let Ok(unix_time) = std::time::UNIX_EPOCH.elapsed() {
println!("Current Unix time is {:?}", unix_time.as_secs()); println!("Current Unix time is {:?}", unix_time.as_secs());
} }
}) })
.on_action(|_, _: &PrintBestFood, _| {
println!("burrito");
})
.flex() .flex()
.flex_row() .flex_row()
.justify_between() .justify_between()

View file

@ -16,8 +16,12 @@ pub enum Icon {
ArrowLeft, ArrowLeft,
ArrowRight, ArrowRight,
ArrowUpRight, ArrowUpRight,
AtSign,
AudioOff, AudioOff,
AudioOn, AudioOn,
Bell,
BellOff,
BellRing,
Bolt, Bolt,
Check, Check,
ChevronDown, ChevronDown,
@ -26,12 +30,14 @@ pub enum Icon {
ChevronUp, ChevronUp,
Close, Close,
Collab, Collab,
Copilot,
Dash, Dash,
Exit, Envelope,
ExclamationTriangle, ExclamationTriangle,
Exit,
File, File,
FileGeneric,
FileDoc, FileDoc,
FileGeneric,
FileGit, FileGit,
FileLock, FileLock,
FileRust, FileRust,
@ -44,6 +50,7 @@ pub enum Icon {
InlayHint, InlayHint,
MagicWand, MagicWand,
MagnifyingGlass, MagnifyingGlass,
MailOpen,
Maximize, Maximize,
Menu, Menu,
MessageBubbles, MessageBubbles,
@ -59,13 +66,6 @@ pub enum Icon {
SplitMessage, SplitMessage,
Terminal, Terminal,
XCircle, XCircle,
Copilot,
Envelope,
Bell,
BellOff,
BellRing,
MailOpen,
AtSign,
} }
impl Icon { impl Icon {
@ -75,8 +75,12 @@ impl Icon {
Icon::ArrowLeft => "icons/arrow_left.svg", Icon::ArrowLeft => "icons/arrow_left.svg",
Icon::ArrowRight => "icons/arrow_right.svg", Icon::ArrowRight => "icons/arrow_right.svg",
Icon::ArrowUpRight => "icons/arrow_up_right.svg", Icon::ArrowUpRight => "icons/arrow_up_right.svg",
Icon::AtSign => "icons/at-sign.svg",
Icon::AudioOff => "icons/speaker-off.svg", Icon::AudioOff => "icons/speaker-off.svg",
Icon::AudioOn => "icons/speaker-loud.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::Bolt => "icons/bolt.svg",
Icon::Check => "icons/check.svg", Icon::Check => "icons/check.svg",
Icon::ChevronDown => "icons/chevron_down.svg", Icon::ChevronDown => "icons/chevron_down.svg",
@ -85,12 +89,14 @@ impl Icon {
Icon::ChevronUp => "icons/chevron_up.svg", Icon::ChevronUp => "icons/chevron_up.svg",
Icon::Close => "icons/x.svg", Icon::Close => "icons/x.svg",
Icon::Collab => "icons/user_group_16.svg", Icon::Collab => "icons/user_group_16.svg",
Icon::Copilot => "icons/copilot.svg",
Icon::Dash => "icons/dash.svg", Icon::Dash => "icons/dash.svg",
Icon::Exit => "icons/exit.svg", Icon::Envelope => "icons/feedback.svg",
Icon::ExclamationTriangle => "icons/warning.svg", Icon::ExclamationTriangle => "icons/warning.svg",
Icon::Exit => "icons/exit.svg",
Icon::File => "icons/file.svg", Icon::File => "icons/file.svg",
Icon::FileGeneric => "icons/file_icons/file.svg",
Icon::FileDoc => "icons/file_icons/book.svg", Icon::FileDoc => "icons/file_icons/book.svg",
Icon::FileGeneric => "icons/file_icons/file.svg",
Icon::FileGit => "icons/file_icons/git.svg", Icon::FileGit => "icons/file_icons/git.svg",
Icon::FileLock => "icons/file_icons/lock.svg", Icon::FileLock => "icons/file_icons/lock.svg",
Icon::FileRust => "icons/file_icons/rust.svg", Icon::FileRust => "icons/file_icons/rust.svg",
@ -103,6 +109,7 @@ impl Icon {
Icon::InlayHint => "icons/inlay_hint.svg", Icon::InlayHint => "icons/inlay_hint.svg",
Icon::MagicWand => "icons/magic-wand.svg", Icon::MagicWand => "icons/magic-wand.svg",
Icon::MagnifyingGlass => "icons/magnifying_glass.svg", Icon::MagnifyingGlass => "icons/magnifying_glass.svg",
Icon::MailOpen => "icons/mail-open.svg",
Icon::Maximize => "icons/maximize.svg", Icon::Maximize => "icons/maximize.svg",
Icon::Menu => "icons/menu.svg", Icon::Menu => "icons/menu.svg",
Icon::MessageBubbles => "icons/conversations.svg", Icon::MessageBubbles => "icons/conversations.svg",
@ -118,13 +125,6 @@ impl Icon {
Icon::SplitMessage => "icons/split_message.svg", Icon::SplitMessage => "icons/split_message.svg",
Icon::Terminal => "icons/terminal.svg", Icon::Terminal => "icons/terminal.svg",
Icon::XCircle => "icons/error.svg", Icon::XCircle => "icons/error.svg",
Icon::Copilot => "icons/copilot.svg",
Icon::Envelope => "icons/feedback.svg",
Icon::Bell => "icons/bell.svg",
Icon::BellOff => "icons/bell-off.svg",
Icon::BellRing => "icons/bell-ring.svg",
Icon::MailOpen => "icons/mail-open.svg",
Icon::AtSign => "icons/at-sign.svg",
} }
} }
} }

View file

@ -82,16 +82,22 @@ pub enum ModifierKey {
Shift, Shift,
} }
actions!(NoAction);
pub fn binding(key: &str) -> gpui::KeyBinding {
gpui::KeyBinding::new(key, NoAction {}, None)
}
#[cfg(feature = "stories")] #[cfg(feature = "stories")]
pub use stories::*; pub use stories::*;
#[cfg(feature = "stories")] #[cfg(feature = "stories")]
mod stories { mod stories {
use super::*; use super::*;
use crate::Story; pub use crate::KeyBinding;
use crate::{binding, Story};
use gpui::{actions, Div, Render}; use gpui::{actions, Div, Render};
use itertools::Itertools; use itertools::Itertools;
pub struct KeybindingStory; pub struct KeybindingStory;
actions!(NoAction); actions!(NoAction);
@ -100,7 +106,7 @@ mod stories {
gpui::KeyBinding::new(key, NoAction {}, None) gpui::KeyBinding::new(key, NoAction {}, None)
} }
impl Render<Self> for KeybindingStory { impl Render for KeybindingStory {
type Element = Div<Self>; type Element = Div<Self>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element { fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {

View file

@ -1,4 +1,5 @@
use gpui::{div, Action, Div, RenderOnce}; use gpui::{div, Action, Div, RenderOnce};
use std::rc::Rc;
use crate::settings::user_settings; use crate::settings::user_settings;
use crate::{ use crate::{
@ -232,36 +233,36 @@ pub enum ListEntrySize {
} }
#[derive(RenderOnce, Clone)] #[derive(RenderOnce, Clone)]
pub enum ListItem { pub enum ListItem<V: 'static> {
Entry(ListEntry), Entry(ListEntry<V>),
Separator(ListSeparator), Separator(ListSeparator),
Header(ListSubHeader), Header(ListSubHeader),
} }
impl From<ListEntry> for ListItem { impl<V: 'static> From<ListEntry<V>> for ListItem<V> {
fn from(entry: ListEntry) -> Self { fn from(entry: ListEntry<V>) -> Self {
Self::Entry(entry) Self::Entry(entry)
} }
} }
impl From<ListSeparator> for ListItem { impl<V: 'static> From<ListSeparator> for ListItem<V> {
fn from(entry: ListSeparator) -> Self { fn from(entry: ListSeparator) -> Self {
Self::Separator(entry) Self::Separator(entry)
} }
} }
impl From<ListSubHeader> for ListItem { impl<V: 'static> From<ListSubHeader> for ListItem<V> {
fn from(entry: ListSubHeader) -> Self { fn from(entry: ListSubHeader) -> Self {
Self::Header(entry) Self::Header(entry)
} }
} }
impl<V: 'static> Component<V> for ListItem { impl<V: 'static> Component<V> for ListItem<V> {
type Rendered = Div<V>; type Rendered = Div<V>;
fn render(self, view: &mut V, cx: &mut ViewContext<V>) -> Self::Rendered { fn render(self, view: &mut V, cx: &mut ViewContext<V>) -> Self::Rendered {
match self { 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::Separator(separator) => div().child(separator.render(view, cx)),
ListItem::Header(header) => div().child(header.render(view, cx)), ListItem::Header(header) => div().child(header.render(view, cx)),
} }
@ -273,7 +274,7 @@ impl ListItem {
Self::Entry(ListEntry::new(label)) 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 { if let Self::Entry(entry) = self {
Some(entry) Some(entry)
} else { } else {
@ -283,7 +284,7 @@ impl ListItem {
} }
// #[derive(RenderOnce)] // #[derive(RenderOnce)]
pub struct ListEntry { pub struct ListEntry<V> {
disabled: bool, disabled: bool,
// TODO: Reintroduce this // TODO: Reintroduce this
// disclosure_control_style: DisclosureControlVisibility, // disclosure_control_style: DisclosureControlVisibility,
@ -294,15 +295,13 @@ pub struct ListEntry {
size: ListEntrySize, size: ListEntrySize,
toggle: Toggle, toggle: Toggle,
variant: ListItemVariant, 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 { fn clone(&self) -> Self {
Self { Self {
disabled: self.disabled, disabled: self.disabled,
// TODO: Reintroduce this
// disclosure_control_style: DisclosureControlVisibility,
indent_level: self.indent_level, indent_level: self.indent_level,
label: self.label.clone(), label: self.label.clone(),
left_slot: self.left_slot.clone(), left_slot: self.left_slot.clone(),
@ -310,12 +309,12 @@ impl Clone for ListEntry {
size: self.size, size: self.size,
toggle: self.toggle, toggle: self.toggle,
variant: self.variant, 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 { pub fn new(label: Label) -> Self {
Self { Self {
disabled: false, disabled: false,
@ -330,8 +329,8 @@ impl ListEntry {
} }
} }
pub fn action(mut self, action: impl Into<Box<dyn Action>>) -> Self { pub fn on_click(mut self, handler: impl Fn(&mut V, &mut ViewContext<V>) + 'static) -> Self {
self.on_click = Some(action.into()); self.on_click = Some(Rc::new(handler));
self self
} }
@ -370,7 +369,7 @@ impl ListEntry {
self self
} }
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Element<V> { fn render(self, ix: usize, cx: &mut ViewContext<V>) -> Stateful<V, Div<V>> {
let settings = user_settings(cx); let settings = user_settings(cx);
let left_content = match self.left_slot.clone() { let left_content = match self.left_slot.clone() {
@ -391,21 +390,21 @@ impl ListEntry {
ListEntrySize::Medium => div().h_7(), ListEntrySize::Medium => div().h_7(),
}; };
div() div()
.id(ix)
.relative() .relative()
.hover(|mut style| { .hover(|mut style| {
style.background = Some(cx.theme().colors().editor_background.into()); style.background = Some(cx.theme().colors().editor_background.into());
style style
}) })
.on_mouse_down(gpui::MouseButton::Left, { .on_click({
let action = self.on_click.map(|action| action.boxed_clone()); let on_click = self.on_click.clone();
move |entry: &mut V, event, cx| { move |view: &mut V, event, cx| {
if let Some(action) = action.as_ref() { if let Some(on_click) = &on_click {
cx.dispatch_action(action.boxed_clone()); (on_click)(view, cx)
} }
} }
}) })
.group("")
.bg(cx.theme().colors().surface_background) .bg(cx.theme().colors().surface_background)
// TODO: Add focus state // TODO: Add focus state
// .when(self.state == InteractionState::Focused, |this| { // .when(self.state == InteractionState::Focused, |this| {
@ -458,7 +457,7 @@ impl<V: 'static> Component<V> for ListSeparator {
} }
#[derive(RenderOnce)] #[derive(RenderOnce)]
pub struct List { pub struct List<V: 'static> {
items: Vec<ListItem>, items: Vec<ListItem>,
/// Message to display when the list is empty /// Message to display when the list is empty
/// Defaults to "No items" /// Defaults to "No items"
@ -467,7 +466,7 @@ pub struct List {
toggle: Toggle, toggle: Toggle,
} }
impl<V: 'static> Component<V> for List { impl<V: 'static> Component<V> for List<V> {
type Rendered = Div<V>; type Rendered = Div<V>;
fn render(self, view: &mut V, cx: &mut ViewContext<V>) -> Self::Rendered { fn render(self, view: &mut V, cx: &mut ViewContext<V>) -> Self::Rendered {
@ -487,7 +486,7 @@ impl<V: 'static> Component<V> for List {
} }
} }
impl List { impl<V: 'static> List<V> {
pub fn new(items: Vec<ListItem>) -> Self { pub fn new(items: Vec<ListItem>) -> Self {
Self { Self {
items, items,
@ -514,7 +513,12 @@ impl List {
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Element<V> { fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Element<V> {
let list_content = match (self.items.is_empty(), self.toggle) { 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, Toggle::Toggled(false)) => div(),
(true, _) => { (true, _) => {
div().child(Label::new(self.empty_message.clone()).color(TextColor::Muted)) div().child(Label::new(self.empty_message.clone()).color(TextColor::Muted))

View file

@ -24,6 +24,7 @@ mod to_extract;
pub mod utils; pub mod utils;
pub use components::*; pub use components::*;
use gpui::actions;
pub use prelude::*; pub use prelude::*;
pub use static_data::*; pub use static_data::*;
pub use styled_ext::*; pub use styled_ext::*;
@ -42,3 +43,8 @@ pub use crate::settings::*;
mod story; mod story;
#[cfg(feature = "stories")] #[cfg(feature = "stories")]
pub use story::*; pub use story::*;
actions!(NoAction);
pub fn binding(key: &str) -> gpui::KeyBinding {
gpui::KeyBinding::new(key, NoAction {}, None)
}

View file

@ -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![ vec![
ListEntry::new(Label::new("zed")) ListEntry::new(Label::new("zed"))
.left_icon(Icon::FolderOpen.into()) .left_icon(Icon::FolderOpen.into())
@ -605,7 +605,7 @@ pub fn static_project_panel_project_items() -> Vec<ListItem> {
.collect() .collect()
} }
pub fn static_project_panel_single_items() -> Vec<ListItem> { pub fn static_project_panel_single_items<V>() -> Vec<ListItem<V>> {
vec![ vec![
ListEntry::new(Label::new("todo.md")) ListEntry::new(Label::new("todo.md"))
.left_icon(Icon::FileDoc.into()) .left_icon(Icon::FileDoc.into())
@ -622,7 +622,7 @@ pub fn static_project_panel_single_items() -> Vec<ListItem> {
.collect() .collect()
} }
pub fn static_collab_panel_current_call() -> Vec<ListItem> { pub fn static_collab_panel_current_call<V>() -> Vec<ListItem<V>> {
vec![ vec![
ListEntry::new(Label::new("as-cii")).left_avatar("http://github.com/as-cii.png?s=50"), ListEntry::new(Label::new("as-cii")).left_avatar("http://github.com/as-cii.png?s=50"),
ListEntry::new(Label::new("nathansobo")) ListEntry::new(Label::new("nathansobo"))
@ -635,7 +635,7 @@ pub fn static_collab_panel_current_call() -> Vec<ListItem> {
.collect() .collect()
} }
pub fn static_collab_panel_channels() -> Vec<ListItem> { pub fn static_collab_panel_channels<V>() -> Vec<ListItem<V>> {
vec![ vec![
ListEntry::new(Label::new("zed")) ListEntry::new(Label::new("zed"))
.left_icon(Icon::Hash.into()) .left_icon(Icon::Hash.into())

View file

@ -1,6 +1,5 @@
use std::env;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use std::env;
lazy_static! { lazy_static! {
pub static ref RELEASE_CHANNEL_NAME: String = if cfg!(debug_assertions) { pub static ref RELEASE_CHANNEL_NAME: String = if cfg!(debug_assertions) {
@ -9,18 +8,22 @@ lazy_static! {
} else { } else {
include_str!("../../zed/RELEASE_CHANNEL").to_string() 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, "dev" => ReleaseChannel::Dev,
"nightly" => ReleaseChannel::Nightly,
"preview" => ReleaseChannel::Preview, "preview" => ReleaseChannel::Preview,
"stable" => ReleaseChannel::Stable, "stable" => ReleaseChannel::Stable,
_ => panic!("invalid release channel {}", *RELEASE_CHANNEL_NAME), _ => panic!("invalid release channel {}", *RELEASE_CHANNEL_NAME),
}; };
} }
pub struct AppCommitSha(pub String);
#[derive(Copy, Clone, PartialEq, Eq, Default)] #[derive(Copy, Clone, PartialEq, Eq, Default)]
pub enum ReleaseChannel { pub enum ReleaseChannel {
#[default] #[default]
Dev, Dev,
Nightly,
Preview, Preview,
Stable, Stable,
} }
@ -29,6 +32,7 @@ impl ReleaseChannel {
pub fn display_name(&self) -> &'static str { pub fn display_name(&self) -> &'static str {
match self { match self {
ReleaseChannel::Dev => "Zed Dev", ReleaseChannel::Dev => "Zed Dev",
ReleaseChannel::Nightly => "Zed Nightly",
ReleaseChannel::Preview => "Zed Preview", ReleaseChannel::Preview => "Zed Preview",
ReleaseChannel::Stable => "Zed", ReleaseChannel::Stable => "Zed",
} }
@ -37,6 +41,7 @@ impl ReleaseChannel {
pub fn dev_name(&self) -> &'static str { pub fn dev_name(&self) -> &'static str {
match self { match self {
ReleaseChannel::Dev => "dev", ReleaseChannel::Dev => "dev",
ReleaseChannel::Nightly => "nightly",
ReleaseChannel::Preview => "preview", ReleaseChannel::Preview => "preview",
ReleaseChannel::Stable => "stable", ReleaseChannel::Stable => "stable",
} }
@ -45,6 +50,7 @@ impl ReleaseChannel {
pub fn url_scheme(&self) -> &'static str { pub fn url_scheme(&self) -> &'static str {
match self { match self {
ReleaseChannel::Dev => "zed-dev://", ReleaseChannel::Dev => "zed-dev://",
ReleaseChannel::Nightly => "zed-nightly://",
ReleaseChannel::Preview => "zed-preview://", ReleaseChannel::Preview => "zed-preview://",
ReleaseChannel::Stable => "zed://", ReleaseChannel::Stable => "zed://",
} }
@ -53,15 +59,27 @@ impl ReleaseChannel {
pub fn link_prefix(&self) -> &'static str { pub fn link_prefix(&self) -> &'static str {
match self { match self {
ReleaseChannel::Dev => "https://zed.dev/dev/", 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::Preview => "https://zed.dev/preview/",
ReleaseChannel::Stable => "https://zed.dev/", 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> { pub fn parse_zed_link(link: &str) -> Option<&str> {
for release in [ for release in [
ReleaseChannel::Dev, ReleaseChannel::Dev,
ReleaseChannel::Nightly,
ReleaseChannel::Preview, ReleaseChannel::Preview,
ReleaseChannel::Stable, ReleaseChannel::Stable,
] { ] {

View file

@ -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 { impl PathMatcher {
pub fn new(maybe_glob: &str) -> Result<Self, globset::Error> { pub fn new(maybe_glob: &str) -> Result<Self, globset::Error> {
Ok(PathMatcher { Ok(PathMatcher {
@ -211,7 +219,19 @@ impl PathMatcher {
} }
pub fn is_match<P: AsRef<Path>>(&self, other: P) -> bool { 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"); let path = Path::new("/a/b/c/.eslintrc.js");
assert_eq!(path.extension_or_hidden_file_name(), Some("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:?}"
);
}
} }

View file

@ -8,7 +8,9 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::sync::Arc; use std::sync::Arc;
use theme2::ActiveTheme; 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 { pub enum PanelEvent {
ChangePosition, ChangePosition,
@ -40,7 +42,7 @@ pub trait Panel: FocusableView + EventEmitter<PanelEvent> {
} }
pub trait PanelHandle: Send + Sync { pub trait PanelHandle: Send + Sync {
fn id(&self) -> EntityId; fn entity_id(&self) -> EntityId;
fn persistent_name(&self) -> &'static str; fn persistent_name(&self) -> &'static str;
fn position(&self, cx: &WindowContext) -> DockPosition; fn position(&self, cx: &WindowContext) -> DockPosition;
fn position_is_valid(&self, position: DockPosition, cx: &WindowContext) -> bool; fn position_is_valid(&self, position: DockPosition, cx: &WindowContext) -> bool;
@ -62,8 +64,8 @@ impl<T> PanelHandle for View<T>
where where
T: Panel, T: Panel,
{ {
fn id(&self) -> EntityId { fn entity_id(&self) -> EntityId {
self.entity_id() Entity::entity_id(self)
} }
fn persistent_name(&self) -> &'static str { 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>) {
// pub fn set_panel_zoomed(&mut self, panel: &AnyView, zoomed: bool, cx: &mut ViewContext<Self>) { for entry in &mut self.panel_entries {
// for entry in &mut self.panel_entries { if entry.panel.entity_id() == panel.entity_id() {
// if entry.panel.as_any() == panel { if zoomed != entry.panel.is_zoomed(cx) {
// if zoomed != entry.panel.is_zoomed(cx) { entry.panel.set_zoomed(zoomed, cx);
// entry.panel.set_zoomed(zoomed, cx); }
// } } else if entry.panel.is_zoomed(cx) {
// } else if entry.panel.is_zoomed(cx) { entry.panel.set_zoomed(false, cx);
// entry.panel.set_zoomed(false, cx); }
// } }
// }
// cx.notify(); cx.notify();
// } }
pub fn zoom_out(&mut self, cx: &mut ViewContext<Self>) { pub fn zoom_out(&mut self, cx: &mut ViewContext<Self>) {
for entry in &mut self.panel_entries { 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 = [ let subscriptions = [
cx.observe(&panel, |_, _, cx| cx.notify()), cx.observe(&panel, |_, _, cx| cx.notify()),
cx.subscribe(&panel, |this, panel, event, cx| { cx.subscribe(&panel, move |this, panel, event, cx| match event {
match event { PanelEvent::ChangePosition => {
PanelEvent::ChangePosition => { let new_position = panel.read(cx).position(cx);
//todo!()
// see: Workspace::add_panel_with_extra_event_handler let Ok(new_dock) = workspace.update(cx, |workspace, cx| {
} if panel.is_zoomed(cx) {
PanelEvent::ZoomIn => { workspace.zoomed_position = Some(new_position);
//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);
} }
} match new_position {
PanelEvent::Close => { DockPosition::Left => &workspace.left_dock,
if this.visible_panel().map_or(false, |p| p.id() == panel.id()) { DockPosition::Bottom => &workspace.bottom_dock,
this.set_open(false, cx); DockPosition::Right => &workspace.right_dock,
} }
} .clone()
PanelEvent::Focus => todo!(), }) 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 if let Some(panel_ix) = self
.panel_entries .panel_entries
.iter() .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 { if panel_ix == self.active_panel_index {
self.active_panel_index = 0; self.active_panel_index = 0;
@ -396,7 +446,7 @@ impl Dock {
pub fn panel_size(&self, panel: &dyn PanelHandle, cx: &WindowContext) -> Option<f32> { pub fn panel_size(&self, panel: &dyn PanelHandle, cx: &WindowContext) -> Option<f32> {
self.panel_entries self.panel_entries
.iter() .iter()
.find(|entry| entry.panel.id() == panel.id()) .find(|entry| entry.panel.entity_id() == panel.entity_id())
.map(|entry| entry.panel.size(cx)) .map(|entry| entry.panel.size(cx))
} }
@ -620,6 +670,7 @@ impl Render<Self> for PanelButtons {
let dock = self.dock.read(cx); let dock = self.dock.read(cx);
let active_index = dock.active_panel_index; let active_index = dock.active_panel_index;
let is_open = dock.is_open; let is_open = dock.is_open;
let dock_position = dock.position;
let (menu_anchor, menu_attach) = match dock.position { let (menu_anchor, menu_attach) = match dock.position {
DockPosition::Left => (AnchorCorner::BottomLeft, AnchorCorner::TopLeft), DockPosition::Left => (AnchorCorner::BottomLeft, AnchorCorner::TopLeft),
@ -632,9 +683,10 @@ impl Render<Self> for PanelButtons {
.panel_entries .panel_entries
.iter() .iter()
.enumerate() .enumerate()
.filter_map(|(i, panel)| { .filter_map(|(i, entry)| {
let icon = panel.panel.icon(cx)?; let icon = entry.panel.icon(cx)?;
let name = panel.panel.persistent_name(); let name = entry.panel.persistent_name();
let panel = entry.panel.clone();
let mut button: IconButton<Self> = if i == active_index && is_open { let mut button: IconButton<Self> = if i == active_index && is_open {
let action = dock.toggle_action(); let action = dock.toggle_action();
@ -645,7 +697,7 @@ impl Render<Self> for PanelButtons {
.action(action.boxed_clone()) .action(action.boxed_clone())
.tooltip(move |_, cx| Tooltip::for_action(tooltip.clone(), &*action, cx)) .tooltip(move |_, cx| Tooltip::for_action(tooltip.clone(), &*action, cx))
} else { } else {
let action = panel.panel.toggle_action(cx); let action = entry.panel.toggle_action(cx);
IconButton::new(name, icon) IconButton::new(name, icon)
.action(action.boxed_clone()) .action(action.boxed_clone())
@ -655,7 +707,30 @@ impl Render<Self> for PanelButtons {
Some( Some(
menu_handle(name) menu_handle(name)
.menu(move |_, cx| { .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) .anchor(menu_anchor)
.attach(menu_attach) .attach(menu_attach)

View file

@ -15,6 +15,8 @@ pub enum NotificationEvent {
pub trait Notification: EventEmitter<NotificationEvent> + Render<Self> {} pub trait Notification: EventEmitter<NotificationEvent> + Render<Self> {}
impl<V: EventEmitter<NotificationEvent> + Render> Notification for V {}
pub trait NotificationHandle: Send { pub trait NotificationHandle: Send {
fn id(&self) -> EntityId; fn id(&self) -> EntityId;
fn to_any(&self) -> AnyView; fn to_any(&self) -> AnyView;
@ -164,7 +166,7 @@ impl Workspace {
} }
pub mod simple_message_notification { pub mod simple_message_notification {
use super::{Notification, NotificationEvent}; use super::NotificationEvent;
use gpui::{AnyElement, AppContext, Div, EventEmitter, Render, TextStyle, ViewContext}; use gpui::{AnyElement, AppContext, Div, EventEmitter, Render, TextStyle, ViewContext};
use serde::Deserialize; use serde::Deserialize;
use std::{borrow::Cow, sync::Arc}; use std::{borrow::Cow, sync::Arc};
@ -359,7 +361,6 @@ pub mod simple_message_notification {
// } // }
impl EventEmitter<NotificationEvent> for MessageNotification {} impl EventEmitter<NotificationEvent> for MessageNotification {}
impl Notification for MessageNotification {}
} }
pub trait NotifyResultExt { pub trait NotifyResultExt {

View file

@ -24,6 +24,7 @@ use std::{
Arc, Arc,
}, },
}; };
use ui::v_stack; use ui::v_stack;
use ui::{prelude::*, Icon, IconButton, IconElement, TextColor, Tooltip}; use ui::{prelude::*, Icon, IconButton, IconElement, TextColor, Tooltip};
use util::truncate_and_remove_front; use util::truncate_and_remove_front;
@ -1480,15 +1481,10 @@ impl Pane {
// Right Side // Right Side
.child( .child(
div() div()
// We only use absolute here since we don't
// have opacity or `hidden()` yet
.absolute()
.neg_top_7()
.px_1() .px_1()
.flex() .flex()
.flex_none() .flex_none()
.gap_2() .gap_2()
.group_hover("tab_bar", |this| this.top_0())
// Nav Buttons // Nav Buttons
.child( .child(
div() div()
@ -1931,9 +1927,11 @@ impl Render<Self> for Pane {
.map(|task| task.detach_and_log_err(cx)); .map(|task| task.detach_and_log_err(cx));
}) })
.child(self.render_tab_bar(cx)) .child(self.render_tab_bar(cx))
.child(div() /* todo!(toolbar) */) // .child(
// div()
// ) /* todo!(toolbar) */
.child(if let Some(item) = self.active_item() { .child(if let Some(item) = self.active_item() {
div().flex_1().child(item.to_any()) div().flex().flex_1().child(item.to_any())
} else { } else {
// todo!() // todo!()
div().child("Empty Pane") div().child("Empty Pane")

View file

@ -56,7 +56,7 @@ impl StatusBar {
fn render_left_tools(&self, cx: &mut ViewContext<Self>) -> impl RenderOnce<Self> { fn render_left_tools(&self, cx: &mut ViewContext<Self>) -> impl RenderOnce<Self> {
h_stack() h_stack()
.items_center() .items_center()
.gap_1() .gap_2()
.children(self.left_items.iter().map(|item| item.to_any())) .children(self.left_items.iter().map(|item| item.to_any()))
} }

View file

@ -64,7 +64,7 @@ use std::{
time::Duration, time::Duration,
}; };
use theme2::{ActiveTheme, ThemeSettings}; use theme2::{ActiveTheme, ThemeSettings};
pub use toolbar::{ToolbarItemLocation, ToolbarItemView}; pub use toolbar::{ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
pub use ui; pub use ui;
use util::ResultExt; use util::ResultExt;
use uuid::Uuid; use uuid::Uuid;
@ -813,7 +813,9 @@ impl Workspace {
DockPosition::Right => &self.right_dock, DockPosition::Right => &self.right_dock,
}; };
dock.update(cx, |dock, cx| dock.add_panel(panel, cx)); dock.update(cx, |dock, cx| {
dock.add_panel(panel, self.weak_self.clone(), cx)
});
} }
pub fn status_bar(&self) -> &View<StatusBar> { pub fn status_bar(&self) -> &View<StatusBar> {
@ -3664,7 +3666,7 @@ impl Render<Self> for Workspace {
&self.app_state, &self.app_state,
cx, cx,
)) ))
.child(div().flex().flex_1().child(self.bottom_dock.clone())), .child(self.bottom_dock.clone()),
) )
// Right Dock // Right Dock
.child( .child(
@ -3677,19 +3679,6 @@ impl Render<Self> for Workspace {
), ),
) )
.child(self.status_bar.clone()) .child(self.status_bar.clone())
.z_index(8)
// Debug
.child(
div()
.flex()
.flex_col()
.z_index(9)
.absolute()
.top_20()
.left_1_4()
.w_40()
.gap_2(),
)
} }
} }

View file

@ -170,6 +170,15 @@ osx_minimum_system_version = "10.15.7"
osx_info_plist_exts = ["resources/info/*"] osx_info_plist_exts = ["resources/info/*"]
osx_url_schemes = ["zed-dev"] osx_url_schemes = ["zed-dev"]
[package.metadata.bundle-nightly]
# TODO kb different icon?
icon = ["resources/app-icon-preview@2x.png", "resources/app-icon-preview.png"]
identifier = "dev.zed.Zed-Nightly"
name = "Zed Nightly"
osx_minimum_system_version = "10.15.7"
osx_info_plist_exts = ["resources/info/*"]
osx_url_schemes = ["zed-nightly"]
[package.metadata.bundle-preview] [package.metadata.bundle-preview]
icon = ["resources/app-icon-preview@2x.png", "resources/app-icon-preview.png"] icon = ["resources/app-icon-preview@2x.png", "resources/app-icon-preview.png"]
identifier = "dev.zed.Zed-Preview" identifier = "dev.zed.Zed-Preview"
@ -178,7 +187,6 @@ osx_minimum_system_version = "10.15.7"
osx_info_plist_exts = ["resources/info/*"] osx_info_plist_exts = ["resources/info/*"]
osx_url_schemes = ["zed-preview"] osx_url_schemes = ["zed-preview"]
[package.metadata.bundle-stable] [package.metadata.bundle-stable]
icon = ["resources/app-icon@2x.png", "resources/app-icon.png"] icon = ["resources/app-icon@2x.png", "resources/app-icon.png"]
identifier = "dev.zed.Zed" identifier = "dev.zed.Zed"

Some files were not shown because too many files have changed in this diff Show more