Merge branch 'main' into editor2-autocomplete

# Conflicts:
#	crates/editor2/src/editor.rs
This commit is contained in:
Antonio Scandurra 2023-11-21 17:43:09 +01:00
commit f2c63781f9
227 changed files with 15903 additions and 11916 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

181
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"
@ -817,17 +841,6 @@ dependencies = [
"rustc-demangle", "rustc-demangle",
] ]
[[package]]
name = "backtrace-on-stack-overflow"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fd2d70527f3737a1ad17355e260706c1badebabd1fa06a7a053407380df841b"
dependencies = [
"backtrace",
"libc",
"nix 0.23.2",
]
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.13.1" version = "0.13.1"
@ -1374,7 +1387,7 @@ dependencies = [
"smol", "smol",
"sum_tree", "sum_tree",
"tempfile", "tempfile",
"text", "text2",
"thiserror", "thiserror",
"time", "time",
"tiny_http", "tiny_http",
@ -1526,6 +1539,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 +1576,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",
@ -1843,7 +1858,7 @@ dependencies = [
"editor2", "editor2",
"feature_flags2", "feature_flags2",
"futures 0.3.28", "futures 0.3.28",
"fuzzy", "fuzzy2",
"gpui2", "gpui2",
"language2", "language2",
"lazy_static", "lazy_static",
@ -2614,6 +2629,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 +3802,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 +3867,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 +3902,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 +4535,7 @@ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
"dirs 4.0.0", "dirs 4.0.0",
"editor", "editor2",
"gpui2", "gpui2",
"log", "log",
"schemars", "schemars",
@ -5510,19 +5559,6 @@ dependencies = [
"winapi 0.3.9", "winapi 0.3.9",
] ]
[[package]]
name = "nix"
version = "0.23.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c"
dependencies = [
"bitflags 1.3.2",
"cc",
"cfg-if 1.0.0",
"libc",
"memoffset 0.6.5",
]
[[package]] [[package]]
name = "nix" name = "nix"
version = "0.24.3" version = "0.24.3"
@ -6703,7 +6739,6 @@ dependencies = [
"anyhow", "anyhow",
"client2", "client2",
"collections", "collections",
"context_menu",
"db2", "db2",
"editor2", "editor2",
"futures 0.3.28", "futures 0.3.28",
@ -7972,6 +8007,35 @@ dependencies = [
"workspace", "workspace",
] ]
[[package]]
name = "search2"
version = "0.1.0"
dependencies = [
"anyhow",
"bitflags 1.3.2",
"client2",
"collections",
"editor2",
"futures 0.3.28",
"gpui2",
"language2",
"log",
"menu2",
"postage",
"project2",
"serde",
"serde_derive",
"serde_json",
"settings2",
"smallvec",
"smol",
"theme2",
"ui2",
"unindent",
"util",
"workspace2",
]
[[package]] [[package]]
name = "security-framework" name = "security-framework"
version = "2.9.2" version = "2.9.2"
@ -8795,45 +8859,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "storybook2"
version = "0.1.0"
dependencies = [
"anyhow",
"backtrace-on-stack-overflow",
"chrono",
"clap 4.4.4",
"editor2",
"fuzzy2",
"gpui2",
"itertools 0.11.0",
"language2",
"log",
"menu2",
"picker2",
"rust-embed",
"serde",
"settings2",
"simplelog",
"smallvec",
"strum",
"theme",
"theme2",
"ui2",
"util",
]
[[package]]
name = "storybook3"
version = "0.1.0"
dependencies = [
"anyhow",
"gpui2",
"settings2",
"theme2",
"ui2",
]
[[package]] [[package]]
name = "stringprep" name = "stringprep"
version = "0.1.4" version = "0.1.4"
@ -9053,13 +9078,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 +11579,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 +11591,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",
@ -11561,7 +11599,6 @@ dependencies = [
"fs2", "fs2",
"fsevent", "fsevent",
"futures 0.3.28", "futures 0.3.28",
"fuzzy",
"go_to_line2", "go_to_line2",
"gpui2", "gpui2",
"ignore", "ignore",
@ -11571,7 +11608,6 @@ dependencies = [
"isahc", "isahc",
"journal2", "journal2",
"language2", "language2",
"language_tools",
"lazy_static", "lazy_static",
"libc", "libc",
"log", "log",
@ -11590,6 +11626,7 @@ dependencies = [
"rsa 0.4.0", "rsa 0.4.0",
"rust-embed", "rust-embed",
"schemars", "schemars",
"search2",
"serde", "serde",
"serde_derive", "serde_derive",
"serde_json", "serde_json",

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",
@ -88,14 +90,15 @@ members = [
"crates/rpc", "crates/rpc",
"crates/rpc2", "crates/rpc2",
"crates/search", "crates/search",
"crates/search2",
"crates/settings", "crates/settings",
"crates/settings2", "crates/settings2",
"crates/snippet", "crates/snippet",
"crates/sqlez", "crates/sqlez",
"crates/sqlez_macros", "crates/sqlez_macros",
"crates/rich_text", "crates/rich_text",
"crates/storybook2", # "crates/storybook2",
"crates/storybook3", # "crates/storybook3",
"crates/sum_tree", "crates/sum_tree",
"crates/terminal", "crates/terminal",
"crates/terminal2", "crates/terminal2",

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, ParentElement, Render, SemanticVersion, ViewContext};
use menu::Cancel;
use workspace::notifications::NotificationEvent;
pub struct UpdateNotification {
_version: SemanticVersion,
}
impl EventEmitter<NotificationEvent> for UpdateNotification {}
impl Render for UpdateNotification {
type Element = Div;
fn render(&mut self, _cx: &mut gpui::ViewContext<Self>) -> Self::Element {
div().child("Updated zed!")
// let theme = theme::current(cx).clone();
// let theme = &theme.update_notification;
// let app_name = cx.global::<ReleaseChannel>().display_name();
// MouseEventHandler::new::<ViewReleaseNotes, _>(0, cx, |state, cx| {
// Flex::column()
// .with_child(
// Flex::row()
// .with_child(
// Text::new(
// format!("Updated to {app_name} {}", self.version),
// theme.message.text.clone(),
// )
// .contained()
// .with_style(theme.message.container)
// .aligned()
// .top()
// .left()
// .flex(1., true),
// )
// .with_child(
// MouseEventHandler::new::<Cancel, _>(0, cx, |state, _| {
// let style = theme.dismiss_button.style_for(state);
// Svg::new("icons/x.svg")
// .with_color(style.color)
// .constrained()
// .with_width(style.icon_width)
// .aligned()
// .contained()
// .with_style(style.container)
// .constrained()
// .with_width(style.button_width)
// .with_height(style.button_width)
// })
// .with_padding(Padding::uniform(5.))
// .on_click(MouseButton::Left, move |_, this, cx| {
// this.dismiss(&Default::default(), cx)
// })
// .aligned()
// .constrained()
// .with_height(cx.font_cache().line_height(theme.message.text.font_size))
// .aligned()
// .top()
// .flex_float(),
// ),
// )
// .with_child({
// let style = theme.action_message.style_for(state);
// Text::new("View the release notes", style.text.clone())
// .contained()
// .with_style(style.container)
// })
// .contained()
// })
// .with_cursor_style(CursorStyle::PointingHand)
// .on_click(MouseButton::Left, |_, _, cx| {
// crate::view_release_notes(&Default::default(), cx)
// })
// .into_any_named("update notification")
}
}
impl UpdateNotification {
pub fn new(version: SemanticVersion) -> Self {
Self { _version: version }
}
pub fn _dismiss(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
cx.emit(NotificationEvent::Dismiss);
}
}

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

@ -18,7 +18,7 @@ db = { package = "db2", path = "../db2" }
gpui = { package = "gpui2", path = "../gpui2" } gpui = { package = "gpui2", path = "../gpui2" }
util = { path = "../util" } util = { path = "../util" }
rpc = { package = "rpc2", path = "../rpc2" } rpc = { package = "rpc2", path = "../rpc2" }
text = { path = "../text" } text = { package = "text2", path = "../text2" }
language = { package = "language2", path = "../language2" } language = { package = "language2", path = "../language2" }
settings = { package = "settings2", path = "../settings2" } settings = { package = "settings2", path = "../settings2" }
feature_flags = { package = "feature_flags2", path = "../feature_flags2" } feature_flags = { package = "feature_flags2", path = "../feature_flags2" }

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

@ -33,7 +33,7 @@ collections = { path = "../collections" }
# drag_and_drop = { path = "../drag_and_drop" } # drag_and_drop = { path = "../drag_and_drop" }
editor = { package="editor2", path = "../editor2" } editor = { package="editor2", path = "../editor2" }
#feedback = { path = "../feedback" } #feedback = { path = "../feedback" }
fuzzy = { path = "../fuzzy" } fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
gpui = { package = "gpui2", path = "../gpui2" } gpui = { package = "gpui2", path = "../gpui2" }
language = { package = "language2", path = "../language2" } language = { package = "language2", path = "../language2" }
menu = { package = "menu2", path = "../menu2" } menu = { package = "menu2", path = "../menu2" }

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

@ -160,7 +160,7 @@ use std::sync::Arc;
use db::kvp::KEY_VALUE_STORE; use db::kvp::KEY_VALUE_STORE;
use gpui::{ use gpui::{
actions, div, serde_json, AppContext, AsyncWindowContext, Div, EventEmitter, FocusHandle, actions, div, serde_json, AppContext, AsyncWindowContext, Div, EventEmitter, FocusHandle,
Focusable, FocusableView, InteractiveComponent, ParentComponent, Render, View, ViewContext, Focusable, FocusableView, InteractiveElement, ParentElement, Render, View, ViewContext,
VisualContext, WeakView, VisualContext, WeakView,
}; };
use project::Fs; use project::Fs;
@ -3295,7 +3295,7 @@ impl CollabPanel {
// } // }
impl Render for CollabPanel { impl Render for CollabPanel {
type Element = Focusable<Self, Div<Self>>; type Element = Focusable<Div>;
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element { fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
div() div()

View file

@ -31,13 +31,13 @@ use std::sync::Arc;
use call::ActiveCall; use call::ActiveCall;
use client::{Client, UserStore}; use client::{Client, UserStore};
use gpui::{ use gpui::{
div, px, rems, AppContext, Component, Div, InteractiveComponent, Model, ParentComponent, div, px, rems, AppContext, Div, InteractiveElement, Model, ParentElement, Render, RenderOnce,
Render, Stateful, StatefulInteractiveComponent, Styled, Subscription, ViewContext, Stateful, StatefulInteractiveElement, Styled, Subscription, ViewContext, VisualContext,
VisualContext, WeakView, WindowBounds, WeakView, WindowBounds,
}; };
use project::Project; use project::Project;
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::{h_stack, Button, ButtonVariant, KeyBinding, Label, TextColor, Tooltip}; use ui::{h_stack, Button, ButtonVariant, Color, KeyBinding, Label, Tooltip};
use workspace::Workspace; use workspace::Workspace;
// const MAX_PROJECT_NAME_LENGTH: usize = 40; // const MAX_PROJECT_NAME_LENGTH: usize = 40;
@ -82,7 +82,7 @@ pub struct CollabTitlebarItem {
} }
impl Render for CollabTitlebarItem { impl Render for CollabTitlebarItem {
type Element = Stateful<Self, Div<Self>>; type Element = Stateful<Div>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element { fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
h_stack() h_stack()
@ -100,7 +100,7 @@ impl Render for CollabTitlebarItem {
|s| s.pl(px(68.)), |s| s.pl(px(68.)),
) )
.bg(cx.theme().colors().title_bar_background) .bg(cx.theme().colors().title_bar_background)
.on_click(|_, event, cx| { .on_click(|event, cx| {
if event.up.click_count == 2 { if event.up.click_count == 2 {
cx.zoom_window(); cx.zoom_window();
} }
@ -115,16 +115,16 @@ impl Render for CollabTitlebarItem {
.child( .child(
Button::new("player") Button::new("player")
.variant(ButtonVariant::Ghost) .variant(ButtonVariant::Ghost)
.color(Some(TextColor::Player(0))), .color(Some(Color::Player(0))),
) )
.tooltip(move |_, cx| Tooltip::text("Toggle following", cx)), .tooltip(move |cx| Tooltip::text("Toggle following", cx)),
) )
// TODO - Add project menu // TODO - Add project menu
.child( .child(
div() div()
.id("titlebar_project_menu_button") .id("titlebar_project_menu_button")
.child(Button::new("project_name").variant(ButtonVariant::Ghost)) .child(Button::new("project_name").variant(ButtonVariant::Ghost))
.tooltip(move |_, cx| Tooltip::text("Recent Projects", cx)), .tooltip(move |cx| Tooltip::text("Recent Projects", cx)),
) )
// TODO - Add git menu // TODO - Add git menu
.child( .child(
@ -133,9 +133,9 @@ impl Render for CollabTitlebarItem {
.child( .child(
Button::new("branch_name") Button::new("branch_name")
.variant(ButtonVariant::Ghost) .variant(ButtonVariant::Ghost)
.color(Some(TextColor::Muted)), .color(Some(Color::Muted)),
) )
.tooltip(move |_, cx| { .tooltip(move |cx| {
cx.build_view(|_| { cx.build_view(|_| {
Tooltip::new("Recent Branches") Tooltip::new("Recent Branches")
.key_binding(KeyBinding::new(gpui::KeyBinding::new( .key_binding(KeyBinding::new(gpui::KeyBinding::new(

View file

@ -1,8 +1,8 @@
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, Component, Dismiss, Div, FocusHandle, Keystroke, actions, div, prelude::*, Action, AppContext, Div, EventEmitter, FocusHandle, FocusableView,
ManagedView, ParentComponent, Render, Styled, View, ViewContext, VisualContext, WeakView, Keystroke, Manager, ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView,
}; };
use picker::{Picker, PickerDelegate}; use picker::{Picker, PickerDelegate};
use std::{ use std::{
@ -68,14 +68,16 @@ impl CommandPalette {
} }
} }
impl ManagedView for CommandPalette { impl EventEmitter<Manager> for CommandPalette {}
impl FocusableView for CommandPalette {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle { fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.picker.focus_handle(cx) self.picker.focus_handle(cx)
} }
} }
impl Render for CommandPalette { impl Render for CommandPalette {
type Element = Div<Self>; type Element = Div;
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element { fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
v_stack().w_96().child(self.picker.clone()) v_stack().w_96().child(self.picker.clone())
@ -114,6 +116,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.
@ -137,7 +140,7 @@ impl CommandPaletteDelegate {
} }
impl PickerDelegate for CommandPaletteDelegate { impl PickerDelegate for CommandPaletteDelegate {
type ListItem = Div<Picker<Self>>; type ListItem = Div;
fn placeholder_text(&self) -> Arc<str> { fn placeholder_text(&self) -> Arc<str> {
"Execute a command...".into() "Execute a command...".into()
@ -265,7 +268,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, InteractiveElement, ParentElement, Render, Stateful,
StatefulInteractiveElement, Styled, Subscription, View, ViewContext, WeakView,
};
use language::Diagnostic;
use lsp::LanguageServerId;
use theme::ActiveTheme;
use ui::{h_stack, Color, Icon, IconElement, Label, Tooltip};
use workspace::{item::ItemHandle, StatusItemView, ToolbarItemEvent, Workspace};
use crate::ProjectDiagnosticsEditor;
pub struct DiagnosticIndicator {
summary: project::DiagnosticSummary,
active_editor: Option<WeakView<Editor>>,
workspace: WeakView<Workspace>,
current_diagnostic: Option<Diagnostic>,
in_progress_checks: HashSet<LanguageServerId>,
_observe_active_editor: Option<Subscription>,
}
impl Render for DiagnosticIndicator {
type Element = Stateful<Div>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
let diagnostic_indicator = match (self.summary.error_count, self.summary.warning_count) {
(0, 0) => h_stack().child(IconElement::new(Icon::Check).color(Color::Success)),
(0, warning_count) => h_stack()
.gap_1()
.child(IconElement::new(Icon::ExclamationTriangle).color(Color::Warning))
.child(Label::new(warning_count.to_string())),
(error_count, 0) => h_stack()
.gap_1()
.child(IconElement::new(Icon::XCircle).color(Color::Error))
.child(Label::new(error_count.to_string())),
(error_count, warning_count) => h_stack()
.gap_1()
.child(IconElement::new(Icon::XCircle).color(Color::Error))
.child(Label::new(error_count.to_string()))
.child(IconElement::new(Icon::ExclamationTriangle).color(Color::Warning))
.child(Label::new(warning_count.to_string())),
};
h_stack()
.id(cx.entity_id())
.on_action(cx.listener(Self::go_to_next_diagnostic))
.rounded_md()
.flex_none()
.h(rems(1.375))
.px_1()
.cursor_pointer()
.bg(cx.theme().colors().ghost_element_background)
.hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
.active(|style| style.bg(cx.theme().colors().ghost_element_active))
.tooltip(|cx| Tooltip::text("Project Diagnostics", cx))
.on_click(cx.listener(|this, _, cx| {
if let Some(workspace) = this.workspace.upgrade() {
workspace.update(cx, |workspace, cx| {
ProjectDiagnosticsEditor::deploy(workspace, &Default::default(), cx)
})
}
}))
.child(diagnostic_indicator)
}
}
impl DiagnosticIndicator {
pub fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
let project = workspace.project();
cx.subscribe(project, |this, project, event, cx| match event {
project::Event::DiskBasedDiagnosticsStarted { language_server_id } => {
this.in_progress_checks.insert(*language_server_id);
cx.notify();
}
project::Event::DiskBasedDiagnosticsFinished { language_server_id }
| project::Event::LanguageServerRemoved(language_server_id) => {
this.summary = project.read(cx).diagnostic_summary(cx);
this.in_progress_checks.remove(language_server_id);
cx.notify();
}
project::Event::DiagnosticsUpdated { .. } => {
this.summary = project.read(cx).diagnostic_summary(cx);
cx.notify();
}
_ => {}
})
.detach();
Self {
summary: project.read(cx).diagnostic_summary(cx),
in_progress_checks: project
.read(cx)
.language_servers_running_disk_based_diagnostics()
.collect(),
active_editor: None,
workspace: workspace.weak_handle(),
current_diagnostic: None,
_observe_active_editor: None,
}
}
fn go_to_next_diagnostic(&mut self, _: &GoToDiagnostic, cx: &mut ViewContext<Self>) {
if let Some(editor) = self.active_editor.as_ref().and_then(|e| e.upgrade()) {
editor.update(cx, |editor, cx| {
editor.go_to_diagnostic_impl(editor::Direction::Next, cx);
})
}
}
fn update(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
let editor = editor.read(cx);
let buffer = editor.buffer().read(cx);
let cursor_position = editor.selections.newest::<usize>(cx).head();
let new_diagnostic = buffer
.snapshot(cx)
.diagnostics_in_range::<_, usize>(cursor_position..cursor_position, false)
.filter(|entry| !entry.range.is_empty())
.min_by_key(|entry| (entry.diagnostic.severity, entry.range.len()))
.map(|entry| entry.diagnostic);
if new_diagnostic != self.current_diagnostic {
self.current_diagnostic = new_diagnostic;
cx.notify();
}
}
}
impl EventEmitter<ToolbarItemEvent> for DiagnosticIndicator {}
impl StatusItemView for DiagnosticIndicator {
fn set_active_pane_item(
&mut self,
active_pane_item: Option<&dyn ItemHandle>,
cx: &mut ViewContext<Self>,
) {
if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
self.active_editor = Some(editor.downgrade());
self._observe_active_editor = Some(cx.observe(&editor, Self::update));
self.update(editor, cx);
} else {
self.active_editor = None;
self.current_diagnostic = None;
self._observe_active_editor = None;
}
cx.notify();
}
}

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, ParentElement, Render, ViewContext, WeakView};
use ui::{Icon, IconButton, Tooltip};
use workspace::{item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
pub struct ToolbarControls {
editor: Option<WeakView<ProjectDiagnosticsEditor>>,
}
impl Render for ToolbarControls {
type Element = Div;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
let include_warnings = self
.editor
.as_ref()
.and_then(|editor| editor.upgrade())
.map(|editor| editor.read(cx).include_warnings)
.unwrap_or(false);
let tooltip = if include_warnings {
"Exclude Warnings"
} else {
"Include Warnings"
};
div().child(
IconButton::new("toggle-warnings", Icon::ExclamationTriangle)
.tooltip(move |cx| Tooltip::text(tooltip, cx))
.on_click(cx.listener(|this, _, cx| {
if let Some(editor) = this.editor.as_ref().and_then(|editor| editor.upgrade()) {
editor.update(cx, |editor, cx| {
editor.toggle_warnings(&Default::default(), cx);
});
}
})),
)
}
}
impl EventEmitter<ToolbarItemEvent> for ToolbarControls {}
impl ToolbarItemView for ToolbarControls {
fn set_active_pane_item(
&mut self,
active_pane_item: Option<&dyn ItemHandle>,
_: &mut ViewContext<Self>,
) -> ToolbarItemLocation {
if let Some(pane_item) = active_pane_item.as_ref() {
if let Some(editor) = pane_item.downcast::<ProjectDiagnosticsEditor>() {
self.editor = Some(editor.downgrade());
ToolbarItemLocation::PrimaryRight
} else {
ToolbarItemLocation::Hidden
}
} else {
ToolbarItemLocation::Hidden
}
}
}
impl ToolbarControls {
pub fn new() -> Self {
ToolbarControls { editor: None }
}
}

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

@ -50,7 +50,7 @@ struct BlockRow(u32);
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)] #[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
struct WrapRow(u32); struct WrapRow(u32);
pub type RenderBlock = Arc<dyn Fn(&mut BlockContext) -> AnyElement<Editor>>; pub type RenderBlock = Arc<dyn Fn(&mut BlockContext) -> AnyElement>;
pub struct Block { pub struct Block {
id: BlockId, id: BlockId,
@ -69,7 +69,7 @@ where
pub position: P, pub position: P,
pub height: u8, pub height: u8,
pub style: BlockStyle, pub style: BlockStyle,
pub render: Arc<dyn Fn(&mut BlockContext) -> AnyElement<Editor>>, pub render: Arc<dyn Fn(&mut BlockContext) -> AnyElement>,
pub disposition: BlockDisposition, pub disposition: BlockDisposition,
} }
@ -947,7 +947,7 @@ impl DerefMut for BlockContext<'_, '_> {
} }
impl Block { impl Block {
pub fn render(&self, cx: &mut BlockContext) -> AnyElement<Editor> { pub fn render(&self, cx: &mut BlockContext) -> AnyElement {
self.render.lock()(cx) self.render.lock()(cx)
} }

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};
@ -42,9 +42,9 @@ use gpui::{
actions, div, point, prelude::*, px, relative, rems, size, uniform_list, Action, AnyElement, actions, div, point, prelude::*, px, relative, rems, size, uniform_list, Action, AnyElement,
AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Component, Context, AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Component, Context,
EventEmitter, FocusHandle, FocusableView, FontFeatures, FontStyle, FontWeight, HighlightStyle, EventEmitter, FocusHandle, FocusableView, FontFeatures, FontStyle, FontWeight, HighlightStyle,
Hsla, InputHandler, KeyContext, Model, MouseButton, ParentComponent, Pixels, Render, Styled, Hsla, InputHandler, KeyContext, Model, MouseButton, ParentElement, Pixels, Render,
Subscription, Task, TextStyle, UniformListScrollHandle, View, ViewContext, VisualContext, SharedString, Styled, Subscription, Task, TextStyle, UniformListScrollHandle, View,
WeakView, WindowContext, ViewContext, VisualContext, WeakView, WindowContext,
}; };
use highlight_matching_bracket::refresh_matching_bracket_highlights; use highlight_matching_bracket::refresh_matching_bracket_highlights;
use hover_popover::{hide_hover, HoverState}; use hover_popover::{hide_hover, HoverState};
@ -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,
@ -907,7 +907,7 @@ impl ContextMenu {
style: &EditorStyle, style: &EditorStyle,
workspace: Option<WeakView<Workspace>>, workspace: Option<WeakView<Workspace>>,
cx: &mut ViewContext<Editor>, cx: &mut ViewContext<Editor>,
) -> (DisplayPoint, AnyElement<Editor>) { ) -> (DisplayPoint, AnyElement) {
match self { match self {
ContextMenu::Completions(menu) => (cursor_position, menu.render(style, workspace, cx)), ContextMenu::Completions(menu) => (cursor_position, menu.render(style, workspace, cx)),
ContextMenu::CodeActions(menu) => menu.render(cursor_position, style, cx), ContextMenu::CodeActions(menu) => menu.render(cursor_position, style, cx),
@ -1223,9 +1223,7 @@ impl CompletionsMenu {
style: &EditorStyle, style: &EditorStyle,
workspace: Option<WeakView<Workspace>>, workspace: Option<WeakView<Workspace>>,
cx: &mut ViewContext<Editor>, cx: &mut ViewContext<Editor>,
) -> AnyElement<Editor> { ) -> AnyElement {
// enum CompletionTag {}
let settings = EditorSettings::get_global(cx); let settings = EditorSettings::get_global(cx);
let show_completion_documentation = settings.show_completion_documentation; let show_completion_documentation = settings.show_completion_documentation;
@ -1253,121 +1251,126 @@ impl CompletionsMenu {
let matches = self.matches.clone(); let matches = self.matches.clone();
let selected_item = self.selected_item; let selected_item = self.selected_item;
let list = uniform_list("completions", matches.len(), move |editor, range, cx| { let list = uniform_list(
let start_ix = range.start; cx.view().clone(),
let completions_guard = completions.read(); "completions",
matches.len(),
move |editor, range, cx| {
let start_ix = range.start;
let completions_guard = completions.read();
matches[range] matches[range]
.iter() .iter()
.enumerate() .enumerate()
.map(|(ix, mat)| { .map(|(ix, mat)| {
let item_ix = start_ix + ix; let item_ix = start_ix + ix;
let candidate_id = mat.candidate_id; let candidate_id = mat.candidate_id;
let completion = &completions_guard[candidate_id]; let completion = &completions_guard[candidate_id];
let documentation = if show_completion_documentation { let documentation = if show_completion_documentation {
&completion.documentation &completion.documentation
} else { } else {
&None &None
}; };
// todo!("highlights") // todo!("highlights")
// let highlights = combine_syntax_and_fuzzy_match_highlights( // let highlights = combine_syntax_and_fuzzy_match_highlights(
// &completion.label.text, // &completion.label.text,
// style.text.color.into(), // style.text.color.into(),
// styled_runs_for_code_label(&completion.label, &style.syntax), // styled_runs_for_code_label(&completion.label, &style.syntax),
// &mat.positions, // &mat.positions,
// ) // )
// todo!("documentation") // todo!("documentation")
// MouseEventHandler::new::<CompletionTag, _>(mat.candidate_id, cx, |state, _| { // MouseEventHandler::new::<CompletionTag, _>(mat.candidate_id, cx, |state, _| {
// let completion_label = HighlightedLabel::new( // let completion_label = HighlightedLabel::new(
// completion.label.text.clone(), // completion.label.text.clone(),
// combine_syntax_and_fuzzy_match_highlights( // combine_syntax_and_fuzzy_match_highlights(
// &completion.label.text, // &completion.label.text,
// style.text.color.into(), // style.text.color.into(),
// styled_runs_for_code_label(&completion.label, &style.syntax), // styled_runs_for_code_label(&completion.label, &style.syntax),
// &mat.positions, // &mat.positions,
// ), // ),
// ); // );
// Text::new(completion.label.text.clone(), style.text.clone()) // Text::new(completion.label.text.clone(), style.text.clone())
// .with_soft_wrap(false) // .with_soft_wrap(false)
// .with_highlights(); // .with_highlights();
// if let Some(Documentation::SingleLine(text)) = documentation { // if let Some(Documentation::SingleLine(text)) = documentation {
// h_stack() // h_stack()
// .child(completion_label) // .child(completion_label)
// .with_children((|| { // .with_children((|| {
// let text_style = TextStyle { // let text_style = TextStyle {
// color: style.autocomplete.inline_docs_color, // color: style.autocomplete.inline_docs_color,
// font_size: style.text.font_size // font_size: style.text.font_size
// * style.autocomplete.inline_docs_size_percent, // * style.autocomplete.inline_docs_size_percent,
// ..style.text.clone() // ..style.text.clone()
// }; // };
// let label = Text::new(text.clone(), text_style) // let label = Text::new(text.clone(), text_style)
// .aligned() // .aligned()
// .constrained() // .constrained()
// .dynamically(move |constraint, _, _| gpui::SizeConstraint { // .dynamically(move |constraint, _, _| gpui::SizeConstraint {
// min: constraint.min, // min: constraint.min,
// max: vec2f(constraint.max.x(), constraint.min.y()), // max: vec2f(constraint.max.x(), constraint.min.y()),
// }); // });
// if Some(item_ix) == widest_completion_ix { // if Some(item_ix) == widest_completion_ix {
// Some( // Some(
// label // label
// .contained() // .contained()
// .with_style(style.autocomplete.inline_docs_container) // .with_style(style.autocomplete.inline_docs_container)
// .into_any(), // .into_any(),
// ) // )
// } else { // } else {
// Some(label.flex_float().into_any()) // Some(label.flex_float().into_any())
// } // }
// })()) // })())
// .into_any() // .into_any()
// } else { // } else {
// completion_label.into_any() // completion_label.into_any()
// } // }
// .contained() // .contained()
// .with_style(item_style) // .with_style(item_style)
// .constrained() // .constrained()
// .dynamically(move |constraint, _, _| { // .dynamically(move |constraint, _, _| {
// if Some(item_ix) == widest_completion_ix { // if Some(item_ix) == widest_completion_ix {
// constraint // constraint
// } else { // } else {
// gpui::SizeConstraint { // gpui::SizeConstraint {
// min: constraint.min, // min: constraint.min,
// max: constraint.min, // max: constraint.min,
// } // }
// } // }
// }) // })
// }) // })
// .with_cursor_style(CursorStyle::PointingHand) // .with_cursor_style(CursorStyle::PointingHand)
// .on_down(MouseButton::Left, move |_, this, cx| { // .on_down(MouseButton::Left, move |_, this, cx| {
// this.confirm_completion( // this.confirm_completion(
// &ConfirmCompletion { // &ConfirmCompletion {
// item_ix: Some(item_ix), // item_ix: Some(item_ix),
// }, // },
// cx, // cx,
// ) // )
// .map(|task| task.detach()); // .map(|task| task.detach());
// }) // })
// .constrained() // .constrained()
// //
div() div()
.id(mat.candidate_id) .id(mat.candidate_id)
.bg(gpui::green()) .bg(gpui::green())
.hover(|style| style.bg(gpui::blue())) .hover(|style| style.bg(gpui::blue()))
.when(item_ix == selected_item, |div| div.bg(gpui::blue())) .when(item_ix == selected_item, |div| div.bg(gpui::blue()))
.child(completion.label.text.clone()) .child(SharedString::from(completion.label.text.clone()))
.min_w(px(300.)) .min_w(px(300.))
.max_w(px(700.)) .max_w(px(700.))
}) })
.collect() .collect()
}) },
)
.with_width_from_item(widest_completion_ix); .with_width_from_item(widest_completion_ix);
list.render() list.render_into_any()
// todo!("multiline documentation") // todo!("multiline documentation")
// enum MultiLineDocumentation {} // enum MultiLineDocumentation {}
@ -1529,13 +1532,15 @@ impl CodeActionsMenu {
mut cursor_position: DisplayPoint, mut cursor_position: DisplayPoint,
style: &EditorStyle, style: &EditorStyle,
cx: &mut ViewContext<Editor>, cx: &mut ViewContext<Editor>,
) -> (DisplayPoint, AnyElement<Editor>) { ) -> (DisplayPoint, AnyElement) {
let actions = self.actions.clone(); let actions = self.actions.clone();
let selected_item = self.selected_item; let selected_item = self.selected_item;
let element = uniform_list( let element = uniform_list(
cx.view().clone(),
"code_actions_menu", "code_actions_menu",
self.actions.len(), self.actions.len(),
move |editor, range, cx| { move |this, range, cx| {
actions[range.clone()] actions[range.clone()]
.iter() .iter()
.enumerate() .enumerate()
@ -1557,18 +1562,22 @@ impl CodeActionsMenu {
.bg(colors.element_hover) .bg(colors.element_hover)
.text_color(colors.text_accent) .text_color(colors.text_accent)
}) })
.on_mouse_down(MouseButton::Left, move |editor: &mut Editor, _, cx| { .on_mouse_down(
cx.stop_propagation(); MouseButton::Left,
editor cx.listener(move |editor, _, cx| {
.confirm_code_action( cx.stop_propagation();
&ConfirmCodeAction { editor
item_ix: Some(item_ix), .confirm_code_action(
}, &ConfirmCodeAction {
cx, item_ix: Some(item_ix),
) },
.map(|task| task.detach_and_log_err(cx)); cx,
}) )
.child(action.lsp_action.title.clone()) .map(|task| task.detach_and_log_err(cx));
}),
)
// TASK: It would be good to make lsp_action.title a SharedString to avoid allocating here.
.child(SharedString::from(action.lsp_action.title.clone()))
}) })
.collect() .collect()
}, },
@ -1583,7 +1592,7 @@ impl CodeActionsMenu {
.max_by_key(|(_, action)| action.lsp_action.title.chars().count()) .max_by_key(|(_, action)| action.lsp_action.title.chars().count())
.map(|(ix, _)| ix), .map(|(ix, _)| ix),
) )
.render(); .render_into_any();
if self.deployed_from_indicator { if self.deployed_from_indicator {
*cursor_position.column_mut() = 0; *cursor_position.column_mut() = 0;
@ -2306,7 +2315,8 @@ 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 });
cx.emit(SearchEvent::MatchesInvalidated);
if self.selections.disjoint_anchors().len() == 1 { if self.selections.disjoint_anchors().len() == 1 {
cx.emit(SearchEvent::ActiveMatchChanged) cx.emit(SearchEvent::ActiveMatchChanged)
@ -4230,7 +4240,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(),
}); });
@ -4341,19 +4351,19 @@ impl Editor {
style: &EditorStyle, style: &EditorStyle,
is_active: bool, is_active: bool,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Option<AnyElement<Self>> { ) -> Option<IconButton> {
if self.available_code_actions.is_some() { if self.available_code_actions.is_some() {
Some( Some(
IconButton::new("code_actions_indicator", ui::Icon::Bolt) IconButton::new("code_actions_indicator", ui::Icon::Bolt).on_click(cx.listener(
.on_click(|editor: &mut Editor, cx| { |editor, e, cx| {
editor.toggle_code_actions( editor.toggle_code_actions(
&ToggleCodeActions { &ToggleCodeActions {
deployed_from_indicator: true, deployed_from_indicator: true,
}, },
cx, cx,
); );
}) },
.render(), )),
) )
} else { } else {
None None
@ -4368,7 +4378,7 @@ impl Editor {
line_height: Pixels, line_height: Pixels,
gutter_margin: Pixels, gutter_margin: Pixels,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Vec<Option<AnyElement<Self>>> { ) -> Vec<Option<IconButton>> {
fold_data fold_data
.iter() .iter()
.enumerate() .enumerate()
@ -4381,15 +4391,15 @@ impl Editor {
FoldStatus::Foldable => ui::Icon::ChevronDown, FoldStatus::Foldable => ui::Icon::ChevronDown,
}; };
IconButton::new(ix as usize, icon) IconButton::new(ix as usize, icon)
.on_click(move |editor: &mut Editor, cx| match fold_status { .on_click(cx.listener(move |editor, e, cx| match fold_status {
FoldStatus::Folded => { 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);
} }
}) }))
.render() .color(ui::Color::Muted)
}) })
}) })
.flatten() .flatten()
@ -4409,7 +4419,7 @@ impl Editor {
cursor_position: DisplayPoint, cursor_position: DisplayPoint,
style: &EditorStyle, style: &EditorStyle,
cx: &mut ViewContext<Editor>, cx: &mut ViewContext<Editor>,
) -> Option<(DisplayPoint, AnyElement<Editor>)> { ) -> Option<(DisplayPoint, AnyElement)> {
self.context_menu.read().as_ref().map(|menu| { self.context_menu.read().as_ref().map(|menu| {
menu.render( menu.render(
cursor_position, cursor_position,
@ -5627,7 +5637,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);
} }
} }
@ -5642,7 +5652,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);
} }
} }
@ -7768,7 +7778,7 @@ impl Editor {
} }
div() div()
.pl(cx.anchor_x) .pl(cx.anchor_x)
.child(rename_editor.render_with(EditorElement::new( .child(EditorElement::new(
&rename_editor, &rename_editor,
EditorStyle { EditorStyle {
background: cx.theme().system().transparent, background: cx.theme().system().transparent,
@ -7776,11 +7786,13 @@ impl Editor {
text: text_style, text: text_style,
scrollbar_width: cx.editor_style.scrollbar_width, scrollbar_width: cx.editor_style.scrollbar_width,
syntax: cx.editor_style.syntax.clone(), syntax: cx.editor_style.syntax.clone(),
diagnostic_style: diagnostic_style: cx
cx.editor_style.diagnostic_style.clone(), .editor_style
.diagnostic_style
.clone(),
}, },
))) ))
.render() .render_into_any()
} }
}), }),
disposition: BlockDisposition::Below, disposition: BlockDisposition::Below,
@ -8111,7 +8123,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
@ -8699,7 +8711,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);
@ -8738,7 +8750,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(),
@ -8747,7 +8759,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);
@ -8761,7 +8773,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);
@ -8955,12 +8967,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"))]
@ -9007,14 +9019,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,
@ -9101,7 +9113,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 {
@ -9161,7 +9173,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();
@ -9191,7 +9203,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();
} }
} }
@ -9314,7 +9326,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>,
}, },
@ -9332,8 +9344,12 @@ pub enum Event {
}, },
BufferEdited, BufferEdited,
Edited, Edited,
Reparsed,
Focused, Focused,
Blurred, Blurred,
DirtyChanged,
Saved,
TitleChanged,
DiffBaseChanged, DiffBaseChanged,
SelectionsChanged { SelectionsChanged {
local: bool, local: bool,
@ -9342,6 +9358,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>);
@ -9356,7 +9373,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 {
@ -9559,7 +9576,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;
} }
@ -9589,7 +9606,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(),
}); });
@ -9620,7 +9637,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;
} }
@ -9663,7 +9680,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(),
}); });
@ -9978,11 +9995,11 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend
.ml(cx.anchor_x) .ml(cx.anchor_x)
})) }))
.cursor_pointer() .cursor_pointer()
.on_click(move |_, _, cx| { .on_click(cx.listener(move |_, _, cx| {
cx.write_to_clipboard(ClipboardItem::new(message.clone())); cx.write_to_clipboard(ClipboardItem::new(message.clone()));
}) }))
.tooltip(|_, cx| Tooltip::text("Copy diagnostic message", cx)) .tooltip(|cx| Tooltip::text("Copy diagnostic message", cx))
.render() .render_into_any()
}) })
} }

View file

@ -3048,7 +3048,7 @@ fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
position: snapshot.anchor_after(Point::new(2, 0)), position: snapshot.anchor_after(Point::new(2, 0)),
disposition: BlockDisposition::Below, disposition: BlockDisposition::Below,
height: 1, height: 1,
render: Arc::new(|_| div().render()), render: Arc::new(|_| div().into_any()),
}], }],
Some(Autoscroll::fit()), Some(Autoscroll::fit()),
cx, cx,
@ -3853,7 +3853,7 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); let 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

@ -20,10 +20,10 @@ use collections::{BTreeMap, HashMap};
use gpui::{ use gpui::{
div, point, px, relative, size, transparent_black, Action, AnyElement, AvailableSpace, div, point, px, relative, size, transparent_black, Action, AnyElement, AvailableSpace,
BorrowWindow, Bounds, Component, ContentMask, Corners, DispatchPhase, Edges, Element, BorrowWindow, Bounds, Component, ContentMask, Corners, DispatchPhase, Edges, Element,
ElementId, ElementInputHandler, Entity, EntityId, Hsla, InteractiveComponent, LineLayout, ElementId, ElementInputHandler, Entity, EntityId, Hsla, InteractiveElement, LineLayout,
MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentComponent, Pixels, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, RenderOnce,
ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveComponent, Style, Styled, ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled,
TextRun, TextStyle, View, ViewContext, WindowContext, WrappedLine, TextRun, TextStyle, View, ViewContext, WeakView, WindowContext, WrappedLine,
}; };
use itertools::Itertools; use itertools::Itertools;
use language::language_settings::ShowWhitespaceSetting; use language::language_settings::ShowWhitespaceSetting;
@ -112,14 +112,14 @@ impl SelectionLayout {
} }
pub struct EditorElement { pub struct EditorElement {
editor_id: EntityId, editor: View<Editor>,
style: EditorStyle, style: EditorStyle,
} }
impl EditorElement { impl EditorElement {
pub fn new(editor: &View<Editor>, style: EditorStyle) -> Self { pub fn new(editor: &View<Editor>, style: EditorStyle) -> Self {
Self { Self {
editor_id: editor.entity_id(), editor: editor.clone(),
style, style,
} }
} }
@ -349,7 +349,7 @@ impl EditorElement {
gutter_bounds: Bounds<Pixels>, gutter_bounds: Bounds<Pixels>,
text_bounds: Bounds<Pixels>, text_bounds: Bounds<Pixels>,
layout: &LayoutState, layout: &LayoutState,
cx: &mut ViewContext<Editor>, cx: &mut WindowContext,
) { ) {
let bounds = gutter_bounds.union(&text_bounds); let bounds = gutter_bounds.union(&text_bounds);
let scroll_top = let scroll_top =
@ -460,7 +460,7 @@ impl EditorElement {
bounds: Bounds<Pixels>, bounds: Bounds<Pixels>,
layout: &mut LayoutState, layout: &mut LayoutState,
editor: &mut Editor, editor: &mut Editor,
cx: &mut ViewContext<Editor>, cx: &mut WindowContext,
) { ) {
let line_height = layout.position_map.line_height; let line_height = layout.position_map.line_height;
@ -488,13 +488,14 @@ impl EditorElement {
} }
} }
for (ix, fold_indicator) in layout.fold_indicators.iter_mut().enumerate() { for (ix, fold_indicator) in layout.fold_indicators.drain(..).enumerate() {
if let Some(fold_indicator) = fold_indicator.as_mut() { if let Some(mut fold_indicator) = fold_indicator {
let mut fold_indicator = fold_indicator.render_into_any();
let available_space = size( let available_space = size(
AvailableSpace::MinContent, AvailableSpace::MinContent,
AvailableSpace::Definite(line_height * 0.55), AvailableSpace::Definite(line_height * 0.55),
); );
let fold_indicator_size = fold_indicator.measure(available_space, editor, cx); let fold_indicator_size = fold_indicator.measure(available_space, cx);
let position = point( let position = point(
bounds.size.width - layout.gutter_padding, bounds.size.width - layout.gutter_padding,
@ -505,32 +506,29 @@ impl EditorElement {
(line_height - fold_indicator_size.height) / 2., (line_height - fold_indicator_size.height) / 2.,
); );
let origin = bounds.origin + position + centering_offset; let origin = bounds.origin + position + centering_offset;
fold_indicator.draw(origin, available_space, editor, cx); fold_indicator.draw(origin, available_space, cx);
} }
} }
if let Some(indicator) = layout.code_actions_indicator.as_mut() { if let Some(indicator) = layout.code_actions_indicator.take() {
let mut button = indicator.button.render_into_any();
let available_space = size( let available_space = size(
AvailableSpace::MinContent, AvailableSpace::MinContent,
AvailableSpace::Definite(line_height), AvailableSpace::Definite(line_height),
); );
let indicator_size = indicator.element.measure(available_space, editor, cx); let indicator_size = button.measure(available_space, cx);
let mut x = Pixels::ZERO; let mut x = Pixels::ZERO;
let mut y = indicator.row as f32 * line_height - scroll_top; let mut y = indicator.row as f32 * line_height - scroll_top;
// Center indicator. // Center indicator.
x += ((layout.gutter_padding + layout.gutter_margin) - indicator_size.width) / 2.; x += ((layout.gutter_padding + layout.gutter_margin) - indicator_size.width) / 2.;
y += (line_height - indicator_size.height) / 2.; y += (line_height - indicator_size.height) / 2.;
indicator
.element button.draw(bounds.origin + point(x, y), available_space, cx);
.draw(bounds.origin + point(x, y), available_space, editor, cx);
} }
} }
fn paint_diff_hunks( fn paint_diff_hunks(bounds: Bounds<Pixels>, layout: &LayoutState, cx: &mut WindowContext) {
bounds: Bounds<Pixels>,
layout: &LayoutState,
cx: &mut ViewContext<Editor>,
) {
// todo!() // todo!()
// let diff_style = &theme::current(cx).editor.diff.clone(); // let diff_style = &theme::current(cx).editor.diff.clone();
// let line_height = layout.position_map.line_height; // let line_height = layout.position_map.line_height;
@ -619,7 +617,7 @@ impl EditorElement {
text_bounds: Bounds<Pixels>, text_bounds: Bounds<Pixels>,
layout: &mut LayoutState, layout: &mut LayoutState,
editor: &mut Editor, editor: &mut Editor,
cx: &mut ViewContext<Editor>, cx: &mut WindowContext,
) { ) {
let scroll_position = layout.position_map.snapshot.scroll_position(); let scroll_position = layout.position_map.snapshot.scroll_position();
let start_row = layout.visible_display_row_range.start; let start_row = layout.visible_display_row_range.start;
@ -674,20 +672,22 @@ impl EditorElement {
div() div()
.id(fold.id) .id(fold.id)
.size_full() .size_full()
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
.on_click(move |editor: &mut Editor, _, cx| { .on_click(cx.listener_for(
editor.unfold_ranges( &self.editor,
[fold_range.start..fold_range.end], move |editor: &mut Editor, _, cx| {
true, editor.unfold_ranges(
false, [fold_range.start..fold_range.end],
cx, true,
); false,
cx.stop_propagation(); cx,
}) );
cx.stop_propagation();
},
))
.draw( .draw(
fold_bounds.origin, fold_bounds.origin,
fold_bounds.size, fold_bounds.size,
editor,
cx, cx,
|fold_element_state, cx| { |fold_element_state, cx| {
if fold_element_state.is_active() { if fold_element_state.is_active() {
@ -840,7 +840,7 @@ impl EditorElement {
} }
}); });
if let Some((position, context_menu)) = layout.context_menu.as_mut() { if let Some((position, mut context_menu)) = layout.context_menu.take() {
cx.with_z_index(1, |cx| { cx.with_z_index(1, |cx| {
let line_height = self.style.text.line_height_in_pixels(cx.rem_size()); let line_height = self.style.text.line_height_in_pixels(cx.rem_size());
let available_space = size( let available_space = size(
@ -850,7 +850,7 @@ impl EditorElement {
.min((text_bounds.size.height - line_height) / 2.), .min((text_bounds.size.height - line_height) / 2.),
), ),
); );
let context_menu_size = context_menu.measure(available_space, editor, cx); let context_menu_size = context_menu.measure(available_space, cx);
let cursor_row_layout = &layout.position_map.line_layouts let cursor_row_layout = &layout.position_map.line_layouts
[(position.row() - start_row) as usize] [(position.row() - start_row) as usize]
@ -874,7 +874,7 @@ impl EditorElement {
list_origin.y -= layout.position_map.line_height - list_height; list_origin.y -= layout.position_map.line_height - list_height;
} }
context_menu.draw(list_origin, available_space, editor, cx); context_menu.draw(list_origin, available_space, cx);
}) })
} }
@ -1165,7 +1165,7 @@ impl EditorElement {
layout: &LayoutState, layout: &LayoutState,
content_origin: gpui::Point<Pixels>, content_origin: gpui::Point<Pixels>,
bounds: Bounds<Pixels>, bounds: Bounds<Pixels>,
cx: &mut ViewContext<Editor>, cx: &mut WindowContext,
) { ) {
let start_row = layout.visible_display_row_range.start; let start_row = layout.visible_display_row_range.start;
let end_row = layout.visible_display_row_range.end; let end_row = layout.visible_display_row_range.end;
@ -1218,13 +1218,13 @@ impl EditorElement {
bounds: Bounds<Pixels>, bounds: Bounds<Pixels>,
layout: &mut LayoutState, layout: &mut LayoutState,
editor: &mut Editor, editor: &mut Editor,
cx: &mut ViewContext<Editor>, cx: &mut WindowContext,
) { ) {
let scroll_position = layout.position_map.snapshot.scroll_position(); let scroll_position = layout.position_map.snapshot.scroll_position();
let scroll_left = scroll_position.x * layout.position_map.em_width; let scroll_left = scroll_position.x * layout.position_map.em_width;
let scroll_top = scroll_position.y * layout.position_map.line_height; let scroll_top = scroll_position.y * layout.position_map.line_height;
for block in &mut layout.blocks { for block in layout.blocks.drain(..) {
let mut origin = bounds.origin let mut origin = bounds.origin
+ point( + point(
Pixels::ZERO, Pixels::ZERO,
@ -1233,9 +1233,7 @@ impl EditorElement {
if !matches!(block.style, BlockStyle::Sticky) { if !matches!(block.style, BlockStyle::Sticky) {
origin += point(-scroll_left, Pixels::ZERO); origin += point(-scroll_left, Pixels::ZERO);
} }
block block.element.draw(origin, block.available_space, cx);
.element
.draw(origin, block.available_space, editor, cx);
} }
} }
@ -1810,7 +1808,7 @@ impl EditorElement {
.render_code_actions_indicator(&style, active, cx) .render_code_actions_indicator(&style, active, cx)
.map(|element| CodeActionsIndicator { .map(|element| CodeActionsIndicator {
row: newest_selection_head.row(), row: newest_selection_head.row(),
element, button: element,
}); });
} }
} }
@ -1970,6 +1968,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,
@ -2003,6 +2002,7 @@ impl EditorElement {
editor_style: &self.style, editor_style: &self.style,
}) })
} }
TransformBlock::ExcerptHeader { TransformBlock::ExcerptHeader {
buffer, buffer,
range, range,
@ -2026,12 +2026,10 @@ impl EditorElement {
let jump_position = language::ToPoint::to_point(&jump_anchor, buffer); let jump_position = language::ToPoint::to_point(&jump_anchor, buffer);
IconButton::new(block_id, ui::Icon::ArrowUpRight) IconButton::new(block_id, ui::Icon::ArrowUpRight)
.on_click(move |editor: &mut Editor, cx| { .on_click(cx.listener_for(&self.editor, move |editor, e, cx| {
editor.jump(jump_path.clone(), jump_position, jump_anchor, cx); editor.jump(jump_path.clone(), jump_position, jump_anchor, cx);
}) }))
.tooltip(move |_, cx| { .tooltip(|cx| Tooltip::for_action("Jump to Buffer", &OpenExcerpts, cx))
Tooltip::for_action("Jump to Buffer", &OpenExcerpts, cx)
})
}); });
let element = if *starts_new_buffer { let element = if *starts_new_buffer {
@ -2041,29 +2039,36 @@ impl EditorElement {
// Can't use .and_then() because `.file_name()` and `.parent()` return references :( // Can't use .and_then() because `.file_name()` and `.parent()` return references :(
if let Some(path) = path { if let Some(path) = path {
filename = path.file_name().map(|f| f.to_string_lossy().to_string()); filename = path.file_name().map(|f| f.to_string_lossy().to_string());
parent_path = parent_path = path
path.parent().map(|p| p.to_string_lossy().to_string() + "/"); .parent()
.map(|p| SharedString::from(p.to_string_lossy().to_string() + "/"));
} }
h_stack() h_stack()
.id("path header block")
.size_full() .size_full()
.bg(gpui::red()) .bg(gpui::red())
.child(filename.unwrap_or_else(|| "untitled".to_string())) .child(
filename
.map(SharedString::from)
.unwrap_or_else(|| "untitled".into()),
)
.children(parent_path) .children(parent_path)
.children(jump_icon) // .p_x(gutter_padding) .children(jump_icon) // .p_x(gutter_padding)
} 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("")
.children(jump_icon) // .p_x(gutter_padding) .children(jump_icon) // .p_x(gutter_padding)
}; };
element.render() element.into_any()
} }
}; };
let size = element.measure(available_space, editor, cx); let size = element.measure(available_space, cx);
(element, size) (element, size)
}; };
@ -2122,47 +2127,61 @@ impl EditorElement {
gutter_bounds: Bounds<Pixels>, gutter_bounds: Bounds<Pixels>,
text_bounds: Bounds<Pixels>, text_bounds: Bounds<Pixels>,
layout: &LayoutState, layout: &LayoutState,
cx: &mut ViewContext<Editor>, cx: &mut WindowContext,
) { ) {
let content_origin = text_bounds.origin + point(layout.gutter_margin, Pixels::ZERO); let content_origin = text_bounds.origin + point(layout.gutter_margin, Pixels::ZERO);
cx.on_mouse_event({ cx.on_mouse_event({
let position_map = layout.position_map.clone(); let position_map = layout.position_map.clone();
move |editor, event: &ScrollWheelEvent, phase, cx| { let editor = self.editor.clone();
move |event: &ScrollWheelEvent, phase, cx| {
if phase != DispatchPhase::Bubble { if phase != DispatchPhase::Bubble {
return; return;
} }
if Self::scroll(editor, event, &position_map, bounds, cx) { let should_cancel = editor.update(cx, |editor, cx| {
Self::scroll(editor, event, &position_map, bounds, cx)
});
if should_cancel {
cx.stop_propagation(); cx.stop_propagation();
} }
} }
}); });
cx.on_mouse_event({ cx.on_mouse_event({
let position_map = layout.position_map.clone(); let position_map = layout.position_map.clone();
move |editor, event: &MouseDownEvent, phase, cx| { let editor = self.editor.clone();
move |event: &MouseDownEvent, phase, cx| {
if phase != DispatchPhase::Bubble { if phase != DispatchPhase::Bubble {
return; return;
} }
if Self::mouse_down(editor, event, &position_map, text_bounds, gutter_bounds, cx) { let should_cancel = editor.update(cx, |editor, cx| {
Self::mouse_down(editor, event, &position_map, text_bounds, gutter_bounds, cx)
});
if should_cancel {
cx.stop_propagation() cx.stop_propagation()
} }
} }
}); });
cx.on_mouse_event({ cx.on_mouse_event({
let position_map = layout.position_map.clone(); let position_map = layout.position_map.clone();
move |editor, event: &MouseUpEvent, phase, cx| { let editor = self.editor.clone();
if phase != DispatchPhase::Bubble { move |event: &MouseUpEvent, phase, cx| {
return; let should_cancel = editor.update(cx, |editor, cx| {
} Self::mouse_up(editor, event, &position_map, text_bounds, cx)
});
if Self::mouse_up(editor, event, &position_map, text_bounds, cx) { if should_cancel {
cx.stop_propagation() cx.stop_propagation()
} }
} }
}); });
// todo!() //todo!()
// on_down(MouseButton::Right, { // on_down(MouseButton::Right, {
// let position_map = layout.position_map.clone(); // let position_map = layout.position_map.clone();
// move |event, editor, cx| { // move |event, editor, cx| {
@ -2179,12 +2198,17 @@ impl EditorElement {
// }); // });
cx.on_mouse_event({ cx.on_mouse_event({
let position_map = layout.position_map.clone(); let position_map = layout.position_map.clone();
move |editor, event: &MouseMoveEvent, phase, cx| { let editor = self.editor.clone();
move |event: &MouseMoveEvent, phase, cx| {
if phase != DispatchPhase::Bubble { if phase != DispatchPhase::Bubble {
return; return;
} }
if Self::mouse_moved(editor, event, &position_map, text_bounds, gutter_bounds, cx) { let stop_propogating = editor.update(cx, |editor, cx| {
Self::mouse_moved(editor, event, &position_map, text_bounds, gutter_bounds, cx)
});
if stop_propogating {
cx.stop_propagation() cx.stop_propagation()
} }
} }
@ -2313,7 +2337,7 @@ impl LineWithInvisibles {
content_origin: gpui::Point<Pixels>, content_origin: gpui::Point<Pixels>,
whitespace_setting: ShowWhitespaceSetting, whitespace_setting: ShowWhitespaceSetting,
selection_ranges: &[Range<DisplayPoint>], selection_ranges: &[Range<DisplayPoint>],
cx: &mut ViewContext<Editor>, cx: &mut WindowContext,
) { ) {
let line_height = layout.position_map.line_height; let line_height = layout.position_map.line_height;
let line_y = line_height * row as f32 - layout.position_map.scroll_position.y; let line_y = line_height * row as f32 - layout.position_map.scroll_position.y;
@ -2345,7 +2369,7 @@ impl LineWithInvisibles {
row: u32, row: u32,
line_height: Pixels, line_height: Pixels,
whitespace_setting: ShowWhitespaceSetting, whitespace_setting: ShowWhitespaceSetting,
cx: &mut ViewContext<Editor>, cx: &mut WindowContext,
) { ) {
let allowed_invisibles_regions = match whitespace_setting { let allowed_invisibles_regions = match whitespace_setting {
ShowWhitespaceSetting::None => return, ShowWhitespaceSetting::None => return,
@ -2388,87 +2412,102 @@ enum Invisible {
Whitespace { line_offset: usize }, Whitespace { line_offset: usize },
} }
impl Element<Editor> for EditorElement { impl Element for EditorElement {
type ElementState = (); type State = ();
fn element_id(&self) -> Option<gpui::ElementId> {
Some(self.editor_id.into())
}
fn layout( fn layout(
&mut self, &mut self,
editor: &mut Editor, element_state: Option<Self::State>,
element_state: Option<Self::ElementState>, cx: &mut gpui::WindowContext,
cx: &mut gpui::ViewContext<Editor>, ) -> (gpui::LayoutId, Self::State) {
) -> (gpui::LayoutId, Self::ElementState) { self.editor.update(cx, |editor, cx| {
editor.style = Some(self.style.clone()); // Long-term, we'd like to eliminate this. editor.style = Some(self.style.clone()); // Long-term, we'd like to eliminate this.
let rem_size = cx.rem_size(); let rem_size = cx.rem_size();
let mut style = Style::default(); let mut style = Style::default();
style.size.width = relative(1.).into(); style.size.width = relative(1.).into();
style.size.height = match editor.mode { style.size.height = match editor.mode {
EditorMode::SingleLine => self.style.text.line_height_in_pixels(cx.rem_size()).into(), EditorMode::SingleLine => {
EditorMode::AutoHeight { .. } => todo!(), self.style.text.line_height_in_pixels(cx.rem_size()).into()
EditorMode::Full => relative(1.).into(), }
}; EditorMode::AutoHeight { .. } => todo!(),
let layout_id = cx.request_layout(&style, None); EditorMode::Full => relative(1.).into(),
(layout_id, ()) };
let layout_id = cx.request_layout(&style, None);
(layout_id, ())
})
} }
fn paint( fn paint(
&mut self, mut self,
bounds: Bounds<gpui::Pixels>, bounds: Bounds<gpui::Pixels>,
editor: &mut Editor, element_state: &mut Self::State,
element_state: &mut Self::ElementState, cx: &mut gpui::WindowContext,
cx: &mut gpui::ViewContext<Editor>,
) { ) {
let mut layout = self.compute_layout(editor, cx, bounds); let editor = self.editor.clone();
let gutter_bounds = Bounds { editor.update(cx, |editor, cx| {
origin: bounds.origin, let mut layout = self.compute_layout(editor, cx, bounds);
size: layout.gutter_size, let gutter_bounds = Bounds {
}; origin: bounds.origin,
let text_bounds = Bounds { size: layout.gutter_size,
origin: gutter_bounds.upper_right(), };
size: layout.text_size, let text_bounds = Bounds {
}; origin: gutter_bounds.upper_right(),
size: layout.text_size,
};
let dispatch_context = editor.dispatch_context(cx); let dispatch_context = editor.dispatch_context(cx);
cx.with_key_dispatch( let editor_handle = cx.view().clone();
dispatch_context, cx.with_key_dispatch(
Some(editor.focus_handle.clone()), dispatch_context,
|_, cx| { Some(editor.focus_handle.clone()),
register_actions(cx); |_, cx| {
register_actions(&editor_handle, cx);
// We call with_z_index to establish a new stacking context. // We call with_z_index to establish a new stacking context.
cx.with_z_index(0, |cx| { cx.with_z_index(0, |cx| {
cx.with_content_mask(Some(ContentMask { bounds }), |cx| { cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
// Paint mouse listeners first, so any elements we paint on top of the editor // Paint mouse listeners first, so any elements we paint on top of the editor
// take precedence. // take precedence.
self.paint_mouse_listeners(bounds, gutter_bounds, text_bounds, &layout, cx); self.paint_mouse_listeners(
let input_handler = ElementInputHandler::new(bounds, cx); bounds,
cx.handle_input(&editor.focus_handle, input_handler); gutter_bounds,
text_bounds,
&layout,
cx,
);
let input_handler = ElementInputHandler::new(bounds, editor_handle, cx);
cx.handle_input(&editor.focus_handle, input_handler);
self.paint_background(gutter_bounds, text_bounds, &layout, cx); self.paint_background(gutter_bounds, text_bounds, &layout, cx);
if layout.gutter_size.width > Pixels::ZERO { if layout.gutter_size.width > Pixels::ZERO {
self.paint_gutter(gutter_bounds, &mut layout, editor, cx); self.paint_gutter(gutter_bounds, &mut layout, editor, cx);
} }
self.paint_text(text_bounds, &mut layout, editor, cx); self.paint_text(text_bounds, &mut layout, editor, cx);
if !layout.blocks.is_empty() { if !layout.blocks.is_empty() {
cx.with_element_id(Some("editor_blocks"), |cx| { cx.with_element_id(Some("editor_blocks"), |cx| {
self.paint_blocks(bounds, &mut layout, editor, cx); self.paint_blocks(bounds, &mut layout, editor, cx);
}) })
} }
});
}); });
}); },
}, )
) })
} }
} }
impl Component<Editor> for EditorElement { impl RenderOnce for EditorElement {
fn render(self) -> AnyElement<Editor> { type Element = Self;
AnyElement::new(self)
fn element_id(&self) -> Option<gpui::ElementId> {
self.editor.element_id()
}
fn render_once(self) -> Self::Element {
self
} }
} }
@ -3093,17 +3132,17 @@ pub struct LayoutState {
show_scrollbars: bool, show_scrollbars: bool,
is_singleton: bool, is_singleton: bool,
max_row: u32, max_row: u32,
context_menu: Option<(DisplayPoint, AnyElement<Editor>)>, context_menu: Option<(DisplayPoint, AnyElement)>,
code_actions_indicator: Option<CodeActionsIndicator>, code_actions_indicator: Option<CodeActionsIndicator>,
// hover_popovers: Option<(DisplayPoint, Vec<AnyElement<Editor>>)>, // hover_popovers: Option<(DisplayPoint, Vec<AnyElement>)>,
fold_indicators: Vec<Option<AnyElement<Editor>>>, fold_indicators: Vec<Option<IconButton>>,
tab_invisible: ShapedLine, tab_invisible: ShapedLine,
space_invisible: ShapedLine, space_invisible: ShapedLine,
} }
struct CodeActionsIndicator { struct CodeActionsIndicator {
row: u32, row: u32,
element: AnyElement<Editor>, button: IconButton,
} }
struct PositionMap { struct PositionMap {
@ -3188,7 +3227,7 @@ impl PositionMap {
struct BlockLayout { struct BlockLayout {
row: u32, row: u32,
element: AnyElement<Editor>, element: AnyElement,
available_space: Size<AvailableSpace>, available_space: Size<AvailableSpace>,
style: BlockStyle, style: BlockStyle,
} }
@ -3893,187 +3932,191 @@ fn scale_horizontal_mouse_autoscroll_delta(delta: Pixels) -> f32 {
// } // }
// } // }
fn register_actions(cx: &mut ViewContext<Editor>) { fn register_actions(view: &View<Editor>, cx: &mut WindowContext) {
register_action(cx, Editor::move_left); register_action(view, cx, Editor::move_left);
register_action(cx, Editor::move_right); register_action(view, cx, Editor::move_right);
register_action(cx, Editor::move_down); register_action(view, cx, Editor::move_down);
register_action(cx, Editor::move_up); register_action(view, cx, Editor::move_up);
// on_action(cx, Editor::new_file); todo!() // on_action(cx, Editor::new_file); todo!()
// on_action(cx, Editor::new_file_in_direction); todo!() // on_action(cx, Editor::new_file_in_direction); todo!()
register_action(cx, Editor::cancel); register_action(view, cx, Editor::cancel);
register_action(cx, Editor::newline); register_action(view, cx, Editor::newline);
register_action(cx, Editor::newline_above); register_action(view, cx, Editor::newline_above);
register_action(cx, Editor::newline_below); register_action(view, cx, Editor::newline_below);
register_action(cx, Editor::backspace); register_action(view, cx, Editor::backspace);
register_action(cx, Editor::delete); register_action(view, cx, Editor::delete);
register_action(cx, Editor::tab); register_action(view, cx, Editor::tab);
register_action(cx, Editor::tab_prev); register_action(view, cx, Editor::tab_prev);
register_action(cx, Editor::indent); register_action(view, cx, Editor::indent);
register_action(cx, Editor::outdent); register_action(view, cx, Editor::outdent);
register_action(cx, Editor::delete_line); register_action(view, cx, Editor::delete_line);
register_action(cx, Editor::join_lines); register_action(view, cx, Editor::join_lines);
register_action(cx, Editor::sort_lines_case_sensitive); register_action(view, cx, Editor::sort_lines_case_sensitive);
register_action(cx, Editor::sort_lines_case_insensitive); register_action(view, cx, Editor::sort_lines_case_insensitive);
register_action(cx, Editor::reverse_lines); register_action(view, cx, Editor::reverse_lines);
register_action(cx, Editor::shuffle_lines); register_action(view, cx, Editor::shuffle_lines);
register_action(cx, Editor::convert_to_upper_case); register_action(view, cx, Editor::convert_to_upper_case);
register_action(cx, Editor::convert_to_lower_case); register_action(view, cx, Editor::convert_to_lower_case);
register_action(cx, Editor::convert_to_title_case); register_action(view, cx, Editor::convert_to_title_case);
register_action(cx, Editor::convert_to_snake_case); register_action(view, cx, Editor::convert_to_snake_case);
register_action(cx, Editor::convert_to_kebab_case); register_action(view, cx, Editor::convert_to_kebab_case);
register_action(cx, Editor::convert_to_upper_camel_case); register_action(view, cx, Editor::convert_to_upper_camel_case);
register_action(cx, Editor::convert_to_lower_camel_case); register_action(view, cx, Editor::convert_to_lower_camel_case);
register_action(cx, Editor::delete_to_previous_word_start); register_action(view, cx, Editor::delete_to_previous_word_start);
register_action(cx, Editor::delete_to_previous_subword_start); register_action(view, cx, Editor::delete_to_previous_subword_start);
register_action(cx, Editor::delete_to_next_word_end); register_action(view, cx, Editor::delete_to_next_word_end);
register_action(cx, Editor::delete_to_next_subword_end); register_action(view, cx, Editor::delete_to_next_subword_end);
register_action(cx, Editor::delete_to_beginning_of_line); register_action(view, cx, Editor::delete_to_beginning_of_line);
register_action(cx, Editor::delete_to_end_of_line); register_action(view, cx, Editor::delete_to_end_of_line);
register_action(cx, Editor::cut_to_end_of_line); register_action(view, cx, Editor::cut_to_end_of_line);
register_action(cx, Editor::duplicate_line); register_action(view, cx, Editor::duplicate_line);
register_action(cx, Editor::move_line_up); register_action(view, cx, Editor::move_line_up);
register_action(cx, Editor::move_line_down); register_action(view, cx, Editor::move_line_down);
register_action(cx, Editor::transpose); register_action(view, cx, Editor::transpose);
register_action(cx, Editor::cut); register_action(view, cx, Editor::cut);
register_action(cx, Editor::copy); register_action(view, cx, Editor::copy);
register_action(cx, Editor::paste); register_action(view, cx, Editor::paste);
register_action(cx, Editor::undo); register_action(view, cx, Editor::undo);
register_action(cx, Editor::redo); register_action(view, cx, Editor::redo);
register_action(cx, Editor::move_page_up); register_action(view, cx, Editor::move_page_up);
register_action(cx, Editor::move_page_down); register_action(view, cx, Editor::move_page_down);
register_action(cx, Editor::next_screen); register_action(view, cx, Editor::next_screen);
register_action(cx, Editor::scroll_cursor_top); register_action(view, cx, Editor::scroll_cursor_top);
register_action(cx, Editor::scroll_cursor_center); register_action(view, cx, Editor::scroll_cursor_center);
register_action(cx, Editor::scroll_cursor_bottom); register_action(view, cx, Editor::scroll_cursor_bottom);
register_action(cx, |editor, _: &LineDown, cx| { register_action(view, cx, |editor, _: &LineDown, cx| {
editor.scroll_screen(&ScrollAmount::Line(1.), cx) editor.scroll_screen(&ScrollAmount::Line(1.), cx)
}); });
register_action(cx, |editor, _: &LineUp, cx| { register_action(view, cx, |editor, _: &LineUp, cx| {
editor.scroll_screen(&ScrollAmount::Line(-1.), cx) editor.scroll_screen(&ScrollAmount::Line(-1.), cx)
}); });
register_action(cx, |editor, _: &HalfPageDown, cx| { register_action(view, cx, |editor, _: &HalfPageDown, cx| {
editor.scroll_screen(&ScrollAmount::Page(0.5), cx) editor.scroll_screen(&ScrollAmount::Page(0.5), cx)
}); });
register_action(cx, |editor, _: &HalfPageUp, cx| { register_action(view, cx, |editor, _: &HalfPageUp, cx| {
editor.scroll_screen(&ScrollAmount::Page(-0.5), cx) editor.scroll_screen(&ScrollAmount::Page(-0.5), cx)
}); });
register_action(cx, |editor, _: &PageDown, cx| { register_action(view, cx, |editor, _: &PageDown, cx| {
editor.scroll_screen(&ScrollAmount::Page(1.), cx) editor.scroll_screen(&ScrollAmount::Page(1.), cx)
}); });
register_action(cx, |editor, _: &PageUp, cx| { register_action(view, cx, |editor, _: &PageUp, cx| {
editor.scroll_screen(&ScrollAmount::Page(-1.), cx) editor.scroll_screen(&ScrollAmount::Page(-1.), cx)
}); });
register_action(cx, Editor::move_to_previous_word_start); register_action(view, cx, Editor::move_to_previous_word_start);
register_action(cx, Editor::move_to_previous_subword_start); register_action(view, cx, Editor::move_to_previous_subword_start);
register_action(cx, Editor::move_to_next_word_end); register_action(view, cx, Editor::move_to_next_word_end);
register_action(cx, Editor::move_to_next_subword_end); register_action(view, cx, Editor::move_to_next_subword_end);
register_action(cx, Editor::move_to_beginning_of_line); register_action(view, cx, Editor::move_to_beginning_of_line);
register_action(cx, Editor::move_to_end_of_line); register_action(view, cx, Editor::move_to_end_of_line);
register_action(cx, Editor::move_to_start_of_paragraph); register_action(view, cx, Editor::move_to_start_of_paragraph);
register_action(cx, Editor::move_to_end_of_paragraph); register_action(view, cx, Editor::move_to_end_of_paragraph);
register_action(cx, Editor::move_to_beginning); register_action(view, cx, Editor::move_to_beginning);
register_action(cx, Editor::move_to_end); register_action(view, cx, Editor::move_to_end);
register_action(cx, Editor::select_up); register_action(view, cx, Editor::select_up);
register_action(cx, Editor::select_down); register_action(view, cx, Editor::select_down);
register_action(cx, Editor::select_left); register_action(view, cx, Editor::select_left);
register_action(cx, Editor::select_right); register_action(view, cx, Editor::select_right);
register_action(cx, Editor::select_to_previous_word_start); register_action(view, cx, Editor::select_to_previous_word_start);
register_action(cx, Editor::select_to_previous_subword_start); register_action(view, cx, Editor::select_to_previous_subword_start);
register_action(cx, Editor::select_to_next_word_end); register_action(view, cx, Editor::select_to_next_word_end);
register_action(cx, Editor::select_to_next_subword_end); register_action(view, cx, Editor::select_to_next_subword_end);
register_action(cx, Editor::select_to_beginning_of_line); register_action(view, cx, Editor::select_to_beginning_of_line);
register_action(cx, Editor::select_to_end_of_line); register_action(view, cx, Editor::select_to_end_of_line);
register_action(cx, Editor::select_to_start_of_paragraph); register_action(view, cx, Editor::select_to_start_of_paragraph);
register_action(cx, Editor::select_to_end_of_paragraph); register_action(view, cx, Editor::select_to_end_of_paragraph);
register_action(cx, Editor::select_to_beginning); register_action(view, cx, Editor::select_to_beginning);
register_action(cx, Editor::select_to_end); register_action(view, cx, Editor::select_to_end);
register_action(cx, Editor::select_all); register_action(view, cx, Editor::select_all);
register_action(cx, |editor, action, cx| { register_action(view, cx, |editor, action, cx| {
editor.select_all_matches(action, cx).log_err(); editor.select_all_matches(action, cx).log_err();
}); });
register_action(cx, Editor::select_line); register_action(view, cx, Editor::select_line);
register_action(cx, Editor::split_selection_into_lines); register_action(view, cx, Editor::split_selection_into_lines);
register_action(cx, Editor::add_selection_above); register_action(view, cx, Editor::add_selection_above);
register_action(cx, Editor::add_selection_below); register_action(view, cx, Editor::add_selection_below);
register_action(cx, |editor, action, cx| { register_action(view, cx, |editor, action, cx| {
editor.select_next(action, cx).log_err(); editor.select_next(action, cx).log_err();
}); });
register_action(cx, |editor, action, cx| { register_action(view, cx, |editor, action, cx| {
editor.select_previous(action, cx).log_err(); editor.select_previous(action, cx).log_err();
}); });
register_action(cx, Editor::toggle_comments); register_action(view, cx, Editor::toggle_comments);
register_action(cx, Editor::select_larger_syntax_node); register_action(view, cx, Editor::select_larger_syntax_node);
register_action(cx, Editor::select_smaller_syntax_node); register_action(view, cx, Editor::select_smaller_syntax_node);
register_action(cx, Editor::move_to_enclosing_bracket); register_action(view, cx, Editor::move_to_enclosing_bracket);
register_action(cx, Editor::undo_selection); register_action(view, cx, Editor::undo_selection);
register_action(cx, Editor::redo_selection); register_action(view, cx, Editor::redo_selection);
register_action(cx, Editor::go_to_diagnostic); register_action(view, cx, Editor::go_to_diagnostic);
register_action(cx, Editor::go_to_prev_diagnostic); register_action(view, cx, Editor::go_to_prev_diagnostic);
register_action(cx, Editor::go_to_hunk); register_action(view, cx, Editor::go_to_hunk);
register_action(cx, Editor::go_to_prev_hunk); register_action(view, cx, Editor::go_to_prev_hunk);
register_action(cx, Editor::go_to_definition); register_action(view, cx, Editor::go_to_definition);
register_action(cx, Editor::go_to_definition_split); register_action(view, cx, Editor::go_to_definition_split);
register_action(cx, Editor::go_to_type_definition); register_action(view, cx, Editor::go_to_type_definition);
register_action(cx, Editor::go_to_type_definition_split); register_action(view, cx, Editor::go_to_type_definition_split);
register_action(cx, Editor::fold); register_action(view, cx, Editor::fold);
register_action(cx, Editor::fold_at); register_action(view, cx, Editor::fold_at);
register_action(cx, Editor::unfold_lines); register_action(view, cx, Editor::unfold_lines);
register_action(cx, Editor::unfold_at); register_action(view, cx, Editor::unfold_at);
register_action(cx, Editor::fold_selected_ranges); register_action(view, cx, Editor::fold_selected_ranges);
register_action(cx, Editor::show_completions); register_action(view, cx, Editor::show_completions);
register_action(cx, Editor::toggle_code_actions); register_action(view, cx, Editor::toggle_code_actions);
// on_action(cx, Editor::open_excerpts); todo!() // on_action(cx, Editor::open_excerpts); todo!()
register_action(cx, Editor::toggle_soft_wrap); register_action(view, cx, Editor::toggle_soft_wrap);
register_action(cx, Editor::toggle_inlay_hints); register_action(view, cx, Editor::toggle_inlay_hints);
register_action(cx, Editor::reveal_in_finder); register_action(view, cx, Editor::reveal_in_finder);
register_action(cx, Editor::copy_path); register_action(view, cx, Editor::copy_path);
register_action(cx, Editor::copy_relative_path); register_action(view, cx, Editor::copy_relative_path);
register_action(cx, Editor::copy_highlight_json); register_action(view, cx, Editor::copy_highlight_json);
register_action(cx, |editor, action, cx| { register_action(view, cx, |editor, action, cx| {
editor editor
.format(action, cx) .format(action, cx)
.map(|task| task.detach_and_log_err(cx)); .map(|task| task.detach_and_log_err(cx));
}); });
register_action(cx, Editor::restart_language_server); register_action(view, cx, Editor::restart_language_server);
register_action(cx, Editor::show_character_palette); register_action(view, cx, Editor::show_character_palette);
// on_action(cx, Editor::confirm_completion); todo!() // on_action(cx, Editor::confirm_completion); todo!()
register_action(cx, |editor, action, cx| { register_action(view, cx, |editor, action, cx| {
editor editor
.confirm_code_action(action, cx) .confirm_code_action(action, cx)
.map(|task| task.detach_and_log_err(cx)); .map(|task| task.detach_and_log_err(cx));
}); });
register_action(cx, |editor, action, cx| { register_action(view, cx, |editor, action, cx| {
editor editor
.rename(action, cx) .rename(action, cx)
.map(|task| task.detach_and_log_err(cx)); .map(|task| task.detach_and_log_err(cx));
}); });
register_action(cx, |editor, action, cx| { register_action(view, cx, |editor, action, cx| {
editor editor
.confirm_rename(action, cx) .confirm_rename(action, cx)
.map(|task| task.detach_and_log_err(cx)); .map(|task| task.detach_and_log_err(cx));
}); });
register_action(cx, |editor, action, cx| { register_action(view, cx, |editor, action, cx| {
editor editor
.find_all_references(action, cx) .find_all_references(action, cx)
.map(|task| task.detach_and_log_err(cx)); .map(|task| task.detach_and_log_err(cx));
}); });
register_action(cx, Editor::next_copilot_suggestion); register_action(view, cx, Editor::next_copilot_suggestion);
register_action(cx, Editor::previous_copilot_suggestion); register_action(view, cx, Editor::previous_copilot_suggestion);
register_action(cx, Editor::copilot_suggest); register_action(view, cx, Editor::copilot_suggest);
register_action(cx, Editor::context_menu_first); register_action(view, cx, Editor::context_menu_first);
register_action(cx, Editor::context_menu_prev); register_action(view, cx, Editor::context_menu_prev);
register_action(cx, Editor::context_menu_next); register_action(view, cx, Editor::context_menu_next);
register_action(cx, Editor::context_menu_last); register_action(view, cx, Editor::context_menu_last);
} }
fn register_action<T: Action>( fn register_action<T: Action>(
cx: &mut ViewContext<Editor>, view: &View<Editor>,
cx: &mut WindowContext,
listener: impl Fn(&mut Editor, &T, &mut ViewContext<Editor>) + 'static, listener: impl Fn(&mut Editor, &T, &mut ViewContext<Editor>) + 'static,
) { ) {
cx.on_action(TypeId::of::<T>(), move |editor, action, phase, cx| { let view = view.clone();
cx.on_action(TypeId::of::<T>(), move |action, phase, cx| {
let action = action.downcast_ref().unwrap(); let action = action.downcast_ref().unwrap();
if phase == DispatchPhase::Bubble { if phase == DispatchPhase::Bubble {
listener(editor, action, cx); view.update(cx, |editor, cx| {
listener(editor, action, cx);
})
} }
}) })
} }

View file

@ -422,7 +422,7 @@ impl HoverState {
visible_rows: Range<u32>, visible_rows: Range<u32>,
workspace: Option<WeakView<Workspace>>, workspace: Option<WeakView<Workspace>>,
cx: &mut ViewContext<Editor>, cx: &mut ViewContext<Editor>,
) -> Option<(DisplayPoint, Vec<AnyElement<Editor>>)> { ) -> Option<(DisplayPoint, Vec<AnyElement>)> {
todo!("old version below") todo!("old version below")
} }
// // If there is a diagnostic, position the popovers based on that. // // If there is a diagnostic, position the popovers based on that.
@ -504,7 +504,7 @@ pub struct DiagnosticPopover {
} }
impl DiagnosticPopover { impl DiagnosticPopover {
pub fn render(&self, style: &EditorStyle, cx: &mut ViewContext<Editor>) -> AnyElement<Editor> { pub fn render(&self, style: &EditorStyle, cx: &mut ViewContext<Editor>) -> AnyElement {
todo!() todo!()
// enum PrimaryDiagnostic {} // enum PrimaryDiagnostic {}

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};
@ -9,8 +9,8 @@ use collections::HashSet;
use futures::future::try_join_all; use futures::future::try_join_all;
use gpui::{ use gpui::{
div, point, AnyElement, AppContext, AsyncAppContext, Entity, EntityId, EventEmitter, div, point, AnyElement, AppContext, AsyncAppContext, Entity, EntityId, EventEmitter,
FocusHandle, Model, ParentComponent, Pixels, SharedString, Styled, Subscription, Task, View, FocusHandle, Model, ParentElement, Pixels, SharedString, Styled, Subscription, Task, View,
ViewContext, VisualContext, WeakView, ViewContext, VisualContext, WeakView, WindowContext,
}; };
use language::{ use language::{
proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, CharKind, OffsetRangeExt, proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, CharKind, OffsetRangeExt,
@ -30,7 +30,7 @@ use std::{
}; };
use text::Selection; use text::Selection;
use theme::{ActiveTheme, Theme}; use theme::{ActiveTheme, Theme};
use ui::{Label, TextColor}; use ui::{Color, Label};
use util::{paths::PathExt, ResultExt, TryFutureExt}; use util::{paths::PathExt, ResultExt, TryFutureExt};
use workspace::item::{BreadcrumbText, FollowEvent, FollowableEvents, FollowableItemHandle}; use workspace::item::{BreadcrumbText, FollowEvent, FollowableEvents, FollowableItemHandle};
use workspace::{ use workspace::{
@ -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()
@ -583,7 +584,7 @@ impl Item for Editor {
Some(path.to_string_lossy().to_string().into()) Some(path.to_string_lossy().to_string().into())
} }
fn tab_content<T: 'static>(&self, detail: Option<usize>, cx: &AppContext) -> AnyElement<T> { fn tab_content(&self, detail: Option<usize>, cx: &WindowContext) -> AnyElement {
let theme = cx.theme(); let theme = cx.theme();
AnyElement::new( AnyElement::new(
@ -603,7 +604,7 @@ impl Item for Editor {
&description, &description,
MAX_TAB_TITLE_LEN, MAX_TAB_TITLE_LEN,
)) ))
.color(TextColor::Muted), .color(Color::Muted),
), ),
) )
})), })),
@ -760,7 +761,7 @@ impl Item for Editor {
} }
fn breadcrumb_location(&self) -> ToolbarItemLocation { fn breadcrumb_location(&self) -> ToolbarItemLocation {
ToolbarItemLocation::PrimaryLeft { flex: None } ToolbarItemLocation::PrimaryLeft
} }
fn breadcrumbs(&self, variant: &Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> { fn breadcrumbs(&self, variant: &Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
@ -906,17 +907,15 @@ impl SearchableItem for Editor {
type Match = Range<Anchor>; type Match = Range<Anchor>;
fn clear_matches(&mut self, cx: &mut ViewContext<Self>) { fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
todo!() self.clear_background_highlights::<BufferSearchHighlights>(cx);
// self.clear_background_highlights::<BufferSearchHighlights>(cx);
} }
fn update_matches(&mut self, matches: Vec<Range<Anchor>>, cx: &mut ViewContext<Self>) { fn update_matches(&mut self, matches: Vec<Range<Anchor>>, cx: &mut ViewContext<Self>) {
todo!() self.highlight_background::<BufferSearchHighlights>(
// self.highlight_background::<BufferSearchHighlights>( matches,
// matches, |theme| theme.title_bar_background, // todo: update theme
// |theme| theme.search.match_background, cx,
// cx, );
// );
} }
fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String { fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
@ -951,22 +950,20 @@ impl SearchableItem for Editor {
matches: Vec<Range<Anchor>>, matches: Vec<Range<Anchor>>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) {
todo!() self.unfold_ranges([matches[index].clone()], false, true, cx);
// self.unfold_ranges([matches[index].clone()], false, true, cx); let range = self.range_for_match(&matches[index]);
// let range = self.range_for_match(&matches[index]); self.change_selections(Some(Autoscroll::fit()), cx, |s| {
// self.change_selections(Some(Autoscroll::fit()), cx, |s| { s.select_ranges([range]);
// s.select_ranges([range]); })
// })
} }
fn select_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>) { fn select_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>) {
todo!() self.unfold_ranges(matches.clone(), false, false, cx);
// self.unfold_ranges(matches.clone(), false, false, cx); let mut ranges = Vec::new();
// let mut ranges = Vec::new(); for m in &matches {
// for m in &matches { ranges.push(self.range_for_match(&m))
// ranges.push(self.range_for_match(&m)) }
// } self.change_selections(None, cx, |s| s.select_ranges(ranges));
// self.change_selections(None, cx, |s| s.select_ranges(ranges));
} }
fn replace( fn replace(
&mut self, &mut self,

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,9 +2,9 @@ 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, Component, Dismiss, Div, FocusHandle, InteractiveComponent, actions, div, AppContext, Div, EventEmitter, FocusHandle, FocusableView, InteractiveElement,
ManagedView, Model, ParentComponent, Render, Styled, Task, View, ViewContext, VisualContext, Manager, Model, ParentElement, Render, RenderOnce, Styled, Task, View, ViewContext,
WeakView, VisualContext, WeakView,
}; };
use picker::{Picker, PickerDelegate}; use picker::{Picker, PickerDelegate};
use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId}; use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
@ -111,13 +111,14 @@ impl FileFinder {
} }
} }
impl ManagedView for FileFinder { impl EventEmitter<Manager> for FileFinder {}
impl FocusableView for FileFinder {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle { fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.picker.focus_handle(cx) self.picker.focus_handle(cx)
} }
} }
impl Render for FileFinder { impl Render for FileFinder {
type Element = Div<Self>; type Element = Div;
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element { fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
v_stack().w_96().child(self.picker.clone()) v_stack().w_96().child(self.picker.clone())
@ -529,7 +530,7 @@ impl FileFinderDelegate {
} }
impl PickerDelegate for FileFinderDelegate { impl PickerDelegate for FileFinderDelegate {
type ListItem = Div<Picker<Self>>; type ListItem = Div;
fn placeholder_text(&self) -> Arc<str> { fn placeholder_text(&self) -> Arc<str> {
"Search project files...".into() "Search project files...".into()
@ -688,7 +689,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(())
}) })
@ -699,7 +702,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,11 +1,11 @@
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, ParentComponent, actions, div, prelude::*, AppContext, Div, EventEmitter, FocusHandle, FocusableView, Manager,
Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext, WindowContext, Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext, WindowContext,
}; };
use text::{Bias, Point}; use text::{Bias, Point};
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::{h_stack, v_stack, Label, StyledExt, TextColor}; use ui::{h_stack, v_stack, Color, Label, StyledExt};
use util::paths::FILE_ROW_COLUMN_DELIMITER; use util::paths::FILE_ROW_COLUMN_DELIMITER;
use workspace::Workspace; use workspace::Workspace;
@ -23,11 +23,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 +83,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 +123,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,19 +140,19 @@ impl GoToLine {
self.prev_scroll_position.take(); self.prev_scroll_position.take();
} }
cx.emit(Dismiss); cx.emit(Manager::Dismiss);
} }
} }
impl Render for GoToLine { impl Render for GoToLine {
type Element = Div<Self>; type Element = Div;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element { fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
div() div()
.elevation_2(cx) .elevation_2(cx)
.key_context("GoToLine") .key_context("GoToLine")
.on_action(Self::cancel) .on_action(cx.listener(Self::cancel))
.on_action(Self::confirm) .on_action(cx.listener(Self::confirm))
.w_96() .w_96()
.child( .child(
v_stack() v_stack()
@ -175,7 +176,7 @@ impl Render for GoToLine {
.justify_between() .justify_between()
.px_2() .px_2()
.py_1() .py_1()
.child(Label::new(self.current_text.clone()).color(TextColor::Muted)), .child(Label::new(self.current_text.clone()).color(Color::Muted)),
), ),
) )
} }

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

@ -14,7 +14,7 @@ use smallvec::SmallVec;
pub use test_context::*; pub use test_context::*;
use crate::{ use crate::{
current_platform, image_cache::ImageCache, Action, ActionRegistry, AnyBox, AnyView, current_platform, image_cache::ImageCache, Action, ActionRegistry, Any, AnyView,
AnyWindowHandle, AppMetadata, AssetSource, BackgroundExecutor, ClipboardItem, Context, AnyWindowHandle, AppMetadata, AssetSource, BackgroundExecutor, ClipboardItem, Context,
DispatchPhase, DisplayId, Entity, EventEmitter, FocusEvent, FocusHandle, FocusId, DispatchPhase, DisplayId, Entity, EventEmitter, FocusEvent, FocusHandle, FocusId,
ForegroundExecutor, KeyBinding, Keymap, LayoutId, PathPromptOptions, Pixels, Platform, ForegroundExecutor, KeyBinding, Keymap, LayoutId, PathPromptOptions, Pixels, Platform,
@ -28,7 +28,7 @@ use futures::{channel::oneshot, future::LocalBoxFuture, Future};
use parking_lot::Mutex; use parking_lot::Mutex;
use slotmap::SlotMap; use slotmap::SlotMap;
use std::{ use std::{
any::{type_name, Any, TypeId}, any::{type_name, TypeId},
cell::{Ref, RefCell, RefMut}, cell::{Ref, RefCell, RefMut},
marker::PhantomData, marker::PhantomData,
mem, mem,
@ -194,7 +194,7 @@ pub struct AppContext {
asset_source: Arc<dyn AssetSource>, asset_source: Arc<dyn AssetSource>,
pub(crate) image_cache: ImageCache, pub(crate) image_cache: ImageCache,
pub(crate) text_style_stack: Vec<TextStyleRefinement>, pub(crate) text_style_stack: Vec<TextStyleRefinement>,
pub(crate) globals_by_type: HashMap<TypeId, AnyBox>, pub(crate) globals_by_type: HashMap<TypeId, Box<dyn Any>>,
pub(crate) entities: EntityMap, pub(crate) entities: EntityMap,
pub(crate) new_view_observers: SubscriberSet<TypeId, NewViewListener>, pub(crate) new_view_observers: SubscriberSet<TypeId, NewViewListener>,
pub(crate) windows: SlotMap<WindowId, Option<Window>>, pub(crate) windows: SlotMap<WindowId, Option<Window>>,
@ -424,7 +424,7 @@ impl AppContext {
/// Opens a new window with the given option and the root view returned by the given function. /// Opens a new window with the given option and the root view returned by the given function.
/// The function is invoked with a `WindowContext`, which can be used to interact with window-specific /// The function is invoked with a `WindowContext`, which can be used to interact with window-specific
/// functionality. /// functionality.
pub fn open_window<V: Render>( pub fn open_window<V: 'static + Render>(
&mut self, &mut self,
options: crate::WindowOptions, options: crate::WindowOptions,
build_root_view: impl FnOnce(&mut WindowContext) -> View<V>, build_root_view: impl FnOnce(&mut WindowContext) -> View<V>,
@ -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)
} }
@ -1100,12 +1104,12 @@ pub(crate) enum Effect {
/// Wraps a global variable value during `update_global` while the value has been moved to the stack. /// Wraps a global variable value during `update_global` while the value has been moved to the stack.
pub(crate) struct GlobalLease<G: 'static> { pub(crate) struct GlobalLease<G: 'static> {
global: AnyBox, global: Box<dyn Any>,
global_type: PhantomData<G>, global_type: PhantomData<G>,
} }
impl<G: 'static> GlobalLease<G> { impl<G: 'static> GlobalLease<G> {
fn new(global: AnyBox) -> Self { fn new(global: Box<dyn Any>) -> Self {
GlobalLease { GlobalLease {
global, global,
global_type: PhantomData, global_type: PhantomData,

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 _};
@ -115,7 +115,7 @@ impl AsyncAppContext {
build_root_view: impl FnOnce(&mut WindowContext) -> View<V>, build_root_view: impl FnOnce(&mut WindowContext) -> View<V>,
) -> Result<WindowHandle<V>> ) -> Result<WindowHandle<V>>
where where
V: Render, V: 'static + Render,
{ {
let app = self let app = self
.app .app
@ -306,7 +306,7 @@ impl VisualContext for AsyncWindowContext {
build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V, build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V,
) -> Self::Result<View<V>> ) -> Self::Result<View<V>>
where where
V: Render, V: 'static + Render,
{ {
self.window self.window
.update(self, |_, cx| cx.replace_root_view(build_view)) .update(self, |_, cx| cx.replace_root_view(build_view))
@ -320,4 +320,13 @@ impl VisualContext for AsyncWindowContext {
view.read(cx).focus_handle(cx).clone().focus(cx); 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

@ -1,10 +1,10 @@
use crate::{private::Sealed, AnyBox, AppContext, Context, Entity, ModelContext}; use crate::{private::Sealed, AppContext, Context, Entity, ModelContext};
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use derive_more::{Deref, DerefMut}; use derive_more::{Deref, DerefMut};
use parking_lot::{RwLock, RwLockUpgradableReadGuard}; use parking_lot::{RwLock, RwLockUpgradableReadGuard};
use slotmap::{SecondaryMap, SlotMap}; use slotmap::{SecondaryMap, SlotMap};
use std::{ use std::{
any::{type_name, TypeId}, any::{type_name, Any, TypeId},
fmt::{self, Display}, fmt::{self, Display},
hash::{Hash, Hasher}, hash::{Hash, Hasher},
marker::PhantomData, marker::PhantomData,
@ -31,7 +31,7 @@ impl Display for EntityId {
} }
pub(crate) struct EntityMap { pub(crate) struct EntityMap {
entities: SecondaryMap<EntityId, AnyBox>, entities: SecondaryMap<EntityId, Box<dyn Any>>,
ref_counts: Arc<RwLock<EntityRefCounts>>, ref_counts: Arc<RwLock<EntityRefCounts>>,
} }
@ -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,
@ -101,7 +102,7 @@ impl EntityMap {
); );
} }
pub fn take_dropped(&mut self) -> Vec<(EntityId, AnyBox)> { pub fn take_dropped(&mut self) -> Vec<(EntityId, Box<dyn Any>)> {
let mut ref_counts = self.ref_counts.write(); let mut ref_counts = self.ref_counts.write();
let dropped_entity_ids = mem::take(&mut ref_counts.dropped_entity_ids); let dropped_entity_ids = mem::take(&mut ref_counts.dropped_entity_ids);
@ -121,7 +122,7 @@ impl EntityMap {
} }
pub struct Lease<'a, T> { pub struct Lease<'a, T> {
entity: Option<AnyBox>, entity: Option<Box<dyn Any>>,
pub model: &'a Model<T>, pub model: &'a Model<T>,
entity_type: PhantomData<T>, entity_type: PhantomData<T>,
} }

View file

@ -1,8 +1,9 @@
use crate::{ use crate::{
div, Action, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext, div, Action, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext,
BackgroundExecutor, Context, Div, EventEmitter, ForegroundExecutor, InputEvent, KeyDownEvent, BackgroundExecutor, Context, Div, Entity, EventEmitter, ForegroundExecutor, InputEvent,
Keystroke, Model, ModelContext, Render, Result, Task, TestDispatcher, TestPlatform, TestWindow, KeyDownEvent, Keystroke, Model, ModelContext, Render, Result, Task, TestDispatcher,
View, ViewContext, VisualContext, WindowContext, WindowHandle, WindowOptions, TestPlatform, TestWindow, View, ViewContext, VisualContext, WindowContext, WindowHandle,
WindowOptions,
}; };
use anyhow::{anyhow, bail}; use anyhow::{anyhow, bail};
use futures::{Stream, StreamExt}; use futures::{Stream, StreamExt};
@ -126,7 +127,7 @@ impl TestAppContext {
pub fn add_window<F, V>(&mut self, build_window: F) -> WindowHandle<V> pub fn add_window<F, V>(&mut self, build_window: F) -> WindowHandle<V>
where where
F: FnOnce(&mut ViewContext<V>) -> V, F: FnOnce(&mut ViewContext<V>) -> V,
V: Render, V: 'static + Render,
{ {
let mut cx = self.app.borrow_mut(); let mut cx = self.app.borrow_mut();
cx.open_window(WindowOptions::default(), |cx| cx.build_view(build_window)) cx.open_window(WindowOptions::default(), |cx| cx.build_view(build_window))
@ -143,7 +144,7 @@ impl TestAppContext {
pub fn add_window_view<F, V>(&mut self, build_window: F) -> (View<V>, &mut VisualTestContext) pub fn add_window_view<F, V>(&mut self, build_window: F) -> (View<V>, &mut VisualTestContext)
where where
F: FnOnce(&mut ViewContext<V>) -> V, F: FnOnce(&mut ViewContext<V>) -> V,
V: Render, V: 'static + Render,
{ {
let mut cx = self.app.borrow_mut(); let mut cx = self.app.borrow_mut();
let window = cx.open_window(WindowOptions::default(), |cx| cx.build_view(build_window)); let window = cx.open_window(WindowOptions::default(), |cx| cx.build_view(build_window));
@ -296,21 +297,19 @@ impl TestAppContext {
.unwrap() .unwrap()
} }
pub fn notifications<T: 'static>(&mut self, entity: &Model<T>) -> impl Stream<Item = ()> { pub fn notifications<T: 'static>(&mut self, entity: &impl Entity<T>) -> impl Stream<Item = ()> {
let (tx, rx) = futures::channel::mpsc::unbounded(); let (tx, rx) = futures::channel::mpsc::unbounded();
self.update(|cx| {
entity.update(self, move |_, cx: &mut ModelContext<T>| {
cx.observe(entity, { cx.observe(entity, {
let tx = tx.clone(); let tx = tx.clone();
move |_, _, _| { move |_, _| {
let _ = tx.unbounded_send(()); let _ = tx.unbounded_send(());
} }
}) })
.detach(); .detach();
cx.observe_release(entity, move |_, _| tx.close_channel())
cx.on_release(move |_, _| tx.close_channel()).detach(); .detach()
}); });
rx rx
} }
@ -386,6 +385,32 @@ impl<T: Send> Model<T> {
} }
} }
impl<V: 'static> View<V> {
pub fn next_notification(&self, cx: &TestAppContext) -> impl Future<Output = ()> {
use postage::prelude::{Sink as _, Stream as _};
let (mut tx, mut rx) = postage::mpsc::channel(1);
let mut cx = cx.app.app.borrow_mut();
let subscription = cx.observe(self, move |_, _| {
tx.try_send(()).ok();
});
let duration = if std::env::var("CI").is_ok() {
Duration::from_secs(5)
} else {
Duration::from_secs(1)
};
async move {
let notification = crate::util::timeout(duration, rx.recv())
.await
.expect("next notification timed out");
drop(subscription);
notification.expect("model dropped while test was waiting for its next notification")
}
}
}
impl<V> View<V> { impl<V> View<V> {
pub fn condition<Evt>( pub fn condition<Evt>(
&self, &self,
@ -565,7 +590,7 @@ impl<'a> VisualContext for VisualTestContext<'a> {
build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V, build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V,
) -> Self::Result<View<V>> ) -> Self::Result<View<V>>
where where
V: Render, V: 'static + Render,
{ {
self.window self.window
.update(self.cx, |_, cx| cx.replace_root_view(build_view)) .update(self.cx, |_, cx| cx.replace_root_view(build_view))
@ -579,6 +604,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 {
@ -594,7 +630,7 @@ impl AnyWindowHandle {
pub struct EmptyView {} pub struct EmptyView {}
impl Render for EmptyView { impl Render for EmptyView {
type Element = Div<Self>; type Element = Div;
fn render(&mut self, _cx: &mut crate::ViewContext<Self>) -> Self::Element { fn render(&mut self, _cx: &mut crate::ViewContext<Self>) -> Self::Element {
div() div()

View file

@ -1,308 +1,63 @@
use crate::{ use crate::{
AvailableSpace, BorrowWindow, Bounds, ElementId, LayoutId, Pixels, Point, Size, ViewContext, AvailableSpace, BorrowWindow, Bounds, ElementId, LayoutId, Pixels, Point, Size, ViewContext,
WindowContext,
}; };
use derive_more::{Deref, DerefMut}; use derive_more::{Deref, DerefMut};
pub(crate) use smallvec::SmallVec; pub(crate) use smallvec::SmallVec;
use std::{any::Any, fmt::Debug, mem}; use std::{any::Any, fmt::Debug};
pub trait Element<V: 'static> { pub trait Render: 'static + Sized {
type ElementState: 'static; type Element: Element + 'static;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element;
}
pub trait RenderOnce: Sized {
type Element: Element + 'static;
fn element_id(&self) -> Option<ElementId>; fn element_id(&self) -> Option<ElementId>;
fn layout( fn render_once(self) -> Self::Element;
&mut self,
view_state: &mut V,
element_state: Option<Self::ElementState>,
cx: &mut ViewContext<V>,
) -> (LayoutId, Self::ElementState);
fn paint( fn render_into_any(self) -> AnyElement {
&mut self, self.render_once().into_any()
bounds: Bounds<Pixels>, }
view_state: &mut V,
element_state: &mut Self::ElementState,
cx: &mut ViewContext<V>,
);
fn draw<T, R>( fn draw<T, R>(
self, self,
origin: Point<Pixels>, origin: Point<Pixels>,
available_space: Size<T>, available_space: Size<T>,
view_state: &mut V, cx: &mut WindowContext,
cx: &mut ViewContext<V>, f: impl FnOnce(&mut <Self::Element as Element>::State, &mut WindowContext) -> R,
f: impl FnOnce(&Self::ElementState, &mut ViewContext<V>) -> R,
) -> R ) -> R
where where
Self: Sized,
T: Clone + Default + Debug + Into<AvailableSpace>, T: Clone + Default + Debug + Into<AvailableSpace>,
{ {
let mut element = RenderedElement { let element = self.render_once();
element: self, let element_id = element.element_id();
phase: ElementRenderPhase::Start, let element = DrawableElement {
element: Some(element),
phase: ElementDrawPhase::Start,
}; };
element.draw(origin, available_space.map(Into::into), view_state, cx);
if let ElementRenderPhase::Painted { frame_state } = &element.phase { let frame_state =
if let Some(frame_state) = frame_state.as_ref() { DrawableElement::draw(element, origin, available_space.map(Into::into), cx);
f(&frame_state, cx)
} else { if let Some(mut frame_state) = frame_state {
let element_id = element f(&mut frame_state, cx)
.element
.element_id()
.expect("we either have some frame_state or some element_id");
cx.with_element_state(element_id, |element_state, cx| {
let element_state = element_state.unwrap();
let result = f(&element_state, cx);
(result, element_state)
})
}
} else { } else {
unreachable!() cx.with_element_state(element_id.unwrap(), |element_state, cx| {
let mut element_state = element_state.unwrap();
let result = f(&mut element_state, cx);
(result, element_state)
})
} }
} }
}
#[derive(Deref, DerefMut, Default, Clone, Debug, Eq, PartialEq, Hash)]
pub struct GlobalElementId(SmallVec<[ElementId; 32]>);
pub trait ParentComponent<V: 'static> {
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]>;
fn child(mut self, child: impl Component<V>) -> Self
where
Self: Sized,
{
self.children_mut().push(child.render());
self
}
fn children(mut self, iter: impl IntoIterator<Item = impl Component<V>>) -> Self
where
Self: Sized,
{
self.children_mut()
.extend(iter.into_iter().map(|item| item.render()));
self
}
}
trait ElementObject<V> {
fn layout(&mut self, view_state: &mut V, cx: &mut ViewContext<V>) -> LayoutId;
fn paint(&mut self, view_state: &mut V, cx: &mut ViewContext<V>);
fn measure(
&mut self,
available_space: Size<AvailableSpace>,
view_state: &mut V,
cx: &mut ViewContext<V>,
) -> Size<Pixels>;
fn draw(
&mut self,
origin: Point<Pixels>,
available_space: Size<AvailableSpace>,
view_state: &mut V,
cx: &mut ViewContext<V>,
);
}
struct RenderedElement<V: 'static, E: Element<V>> {
element: E,
phase: ElementRenderPhase<E::ElementState>,
}
#[derive(Default)]
enum ElementRenderPhase<V> {
#[default]
Start,
LayoutRequested {
layout_id: LayoutId,
frame_state: Option<V>,
},
LayoutComputed {
layout_id: LayoutId,
available_space: Size<AvailableSpace>,
frame_state: Option<V>,
},
Painted {
frame_state: Option<V>,
},
}
/// Internal struct that wraps an element to store Layout and ElementState after the element is rendered.
/// It's allocated as a trait object to erase the element type and wrapped in AnyElement<E::State> for
/// improved usability.
impl<V, E: Element<V>> RenderedElement<V, E> {
fn new(element: E) -> Self {
RenderedElement {
element,
phase: ElementRenderPhase::Start,
}
}
}
impl<V, E> ElementObject<V> for RenderedElement<V, E>
where
E: Element<V>,
E::ElementState: 'static,
{
fn layout(&mut self, state: &mut V, cx: &mut ViewContext<V>) -> LayoutId {
let (layout_id, frame_state) = match mem::take(&mut self.phase) {
ElementRenderPhase::Start => {
if let Some(id) = self.element.element_id() {
let layout_id = cx.with_element_state(id, |element_state, cx| {
self.element.layout(state, element_state, cx)
});
(layout_id, None)
} else {
let (layout_id, frame_state) = self.element.layout(state, None, cx);
(layout_id, Some(frame_state))
}
}
ElementRenderPhase::LayoutRequested { .. }
| ElementRenderPhase::LayoutComputed { .. }
| ElementRenderPhase::Painted { .. } => {
panic!("element rendered twice")
}
};
self.phase = ElementRenderPhase::LayoutRequested {
layout_id,
frame_state,
};
layout_id
}
fn paint(&mut self, view_state: &mut V, cx: &mut ViewContext<V>) {
self.phase = match mem::take(&mut self.phase) {
ElementRenderPhase::LayoutRequested {
layout_id,
mut frame_state,
}
| ElementRenderPhase::LayoutComputed {
layout_id,
mut frame_state,
..
} => {
let bounds = cx.layout_bounds(layout_id);
if let Some(id) = self.element.element_id() {
cx.with_element_state(id, |element_state, cx| {
let mut element_state = element_state.unwrap();
self.element
.paint(bounds, view_state, &mut element_state, cx);
((), element_state)
});
} else {
self.element
.paint(bounds, view_state, frame_state.as_mut().unwrap(), cx);
}
ElementRenderPhase::Painted { frame_state }
}
_ => panic!("must call layout before paint"),
};
}
fn measure(
&mut self,
available_space: Size<AvailableSpace>,
view_state: &mut V,
cx: &mut ViewContext<V>,
) -> Size<Pixels> {
if matches!(&self.phase, ElementRenderPhase::Start) {
self.layout(view_state, cx);
}
let layout_id = match &mut self.phase {
ElementRenderPhase::LayoutRequested {
layout_id,
frame_state,
} => {
cx.compute_layout(*layout_id, available_space);
let layout_id = *layout_id;
self.phase = ElementRenderPhase::LayoutComputed {
layout_id,
available_space,
frame_state: frame_state.take(),
};
layout_id
}
ElementRenderPhase::LayoutComputed {
layout_id,
available_space: prev_available_space,
..
} => {
if available_space != *prev_available_space {
cx.compute_layout(*layout_id, available_space);
*prev_available_space = available_space;
}
*layout_id
}
_ => panic!("cannot measure after painting"),
};
cx.layout_bounds(layout_id).size
}
fn draw(
&mut self,
origin: Point<Pixels>,
available_space: Size<AvailableSpace>,
view_state: &mut V,
cx: &mut ViewContext<V>,
) {
self.measure(available_space, view_state, cx);
cx.with_absolute_element_offset(origin, |cx| self.paint(view_state, cx))
}
}
pub struct AnyElement<V>(Box<dyn ElementObject<V>>);
impl<V> AnyElement<V> {
pub fn new<E>(element: E) -> Self
where
V: 'static,
E: 'static + Element<V>,
E::ElementState: Any,
{
AnyElement(Box::new(RenderedElement::new(element)))
}
pub fn layout(&mut self, view_state: &mut V, cx: &mut ViewContext<V>) -> LayoutId {
self.0.layout(view_state, cx)
}
pub fn paint(&mut self, view_state: &mut V, cx: &mut ViewContext<V>) {
self.0.paint(view_state, cx)
}
/// Initializes this element and performs layout within the given available space to determine its size.
pub fn measure(
&mut self,
available_space: Size<AvailableSpace>,
view_state: &mut V,
cx: &mut ViewContext<V>,
) -> Size<Pixels> {
self.0.measure(available_space, view_state, cx)
}
/// Initializes this element and performs layout in the available space, then paints it at the given origin.
pub fn draw(
&mut self,
origin: Point<Pixels>,
available_space: Size<AvailableSpace>,
view_state: &mut V,
cx: &mut ViewContext<V>,
) {
self.0.draw(origin, available_space, view_state, cx)
}
}
pub trait Component<V> {
fn render(self) -> AnyElement<V>;
fn map<U>(self, f: impl FnOnce(Self) -> U) -> U fn map<U>(self, f: impl FnOnce(Self) -> U) -> U
where where
Self: Sized, Self: Sized,
U: Component<V>, U: RenderOnce,
{ {
f(self) f(self)
} }
@ -328,65 +83,463 @@ pub trait Component<V> {
} }
} }
impl<V> Component<V> for AnyElement<V> { pub trait Element: 'static + RenderOnce {
fn render(self) -> AnyElement<V> { type State: 'static;
self
fn layout(
&mut self,
state: Option<Self::State>,
cx: &mut WindowContext,
) -> (LayoutId, Self::State);
fn paint(self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext);
fn into_any(self) -> AnyElement {
AnyElement::new(self)
} }
} }
impl<V, E, F> Element<V> for Option<F> pub trait Component: 'static {
where type Rendered: RenderOnce;
V: 'static,
E: 'static + Component<V>, fn render(self, cx: &mut WindowContext) -> Self::Rendered;
F: FnOnce(&mut V, &mut ViewContext<'_, V>) -> E + 'static, }
{
type ElementState = AnyElement<V>; pub struct CompositeElement<C> {
component: Option<C>,
}
pub struct CompositeElementState<C: Component> {
rendered_element: Option<<C::Rendered as RenderOnce>::Element>,
rendered_element_state: <<C::Rendered as RenderOnce>::Element as Element>::State,
}
impl<C> CompositeElement<C> {
pub fn new(component: C) -> Self {
CompositeElement {
component: Some(component),
}
}
}
impl<C: Component> Element for CompositeElement<C> {
type State = CompositeElementState<C>;
fn layout(
&mut self,
state: Option<Self::State>,
cx: &mut WindowContext,
) -> (LayoutId, Self::State) {
let mut element = self.component.take().unwrap().render(cx).render_once();
let (layout_id, state) = element.layout(state.map(|s| s.rendered_element_state), cx);
let state = CompositeElementState {
rendered_element: Some(element),
rendered_element_state: state,
};
(layout_id, state)
}
fn paint(self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
state
.rendered_element
.take()
.unwrap()
.paint(bounds, &mut state.rendered_element_state, cx);
}
}
impl<C: Component> RenderOnce for CompositeElement<C> {
type Element = Self;
fn element_id(&self) -> Option<ElementId> { fn element_id(&self) -> Option<ElementId> {
None None
} }
fn layout( fn render_once(self) -> Self::Element {
&mut self, self
view_state: &mut V,
_: Option<Self::ElementState>,
cx: &mut ViewContext<V>,
) -> (LayoutId, Self::ElementState) {
let render = self.take().unwrap();
let mut rendered_element = (render)(view_state, cx).render();
let layout_id = rendered_element.layout(view_state, cx);
(layout_id, rendered_element)
}
fn paint(
&mut self,
_bounds: Bounds<Pixels>,
view_state: &mut V,
rendered_element: &mut Self::ElementState,
cx: &mut ViewContext<V>,
) {
rendered_element.paint(view_state, cx)
} }
} }
impl<V, E, F> Component<V> for Option<F> #[derive(Deref, DerefMut, Default, Clone, Debug, Eq, PartialEq, Hash)]
pub struct GlobalElementId(SmallVec<[ElementId; 32]>);
pub trait ParentElement {
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]>;
fn child(mut self, child: impl RenderOnce) -> Self
where
Self: Sized,
{
self.children_mut().push(child.render_once().into_any());
self
}
fn children(mut self, children: impl IntoIterator<Item = impl RenderOnce>) -> Self
where
Self: Sized,
{
self.children_mut().extend(
children
.into_iter()
.map(|child| child.render_once().into_any()),
);
self
}
}
trait ElementObject {
fn element_id(&self) -> Option<ElementId>;
fn layout(&mut self, cx: &mut WindowContext) -> LayoutId;
fn paint(&mut self, cx: &mut WindowContext);
fn measure(
&mut self,
available_space: Size<AvailableSpace>,
cx: &mut WindowContext,
) -> Size<Pixels>;
fn draw(
&mut self,
origin: Point<Pixels>,
available_space: Size<AvailableSpace>,
cx: &mut WindowContext,
);
}
pub struct DrawableElement<E: Element> {
element: Option<E>,
phase: ElementDrawPhase<E::State>,
}
#[derive(Default)]
enum ElementDrawPhase<S> {
#[default]
Start,
LayoutRequested {
layout_id: LayoutId,
frame_state: Option<S>,
},
LayoutComputed {
layout_id: LayoutId,
available_space: Size<AvailableSpace>,
frame_state: Option<S>,
},
}
/// A wrapper around an implementer of [Element] that allows it to be drawn in a window.
impl<E: Element> DrawableElement<E> {
fn new(element: E) -> Self {
DrawableElement {
element: Some(element),
phase: ElementDrawPhase::Start,
}
}
fn element_id(&self) -> Option<ElementId> {
self.element.as_ref()?.element_id()
}
fn layout(&mut self, cx: &mut WindowContext) -> LayoutId {
let (layout_id, frame_state) = if let Some(id) = self.element.as_ref().unwrap().element_id()
{
let layout_id = cx.with_element_state(id, |element_state, cx| {
self.element.as_mut().unwrap().layout(element_state, cx)
});
(layout_id, None)
} else {
let (layout_id, frame_state) = self.element.as_mut().unwrap().layout(None, cx);
(layout_id, Some(frame_state))
};
self.phase = ElementDrawPhase::LayoutRequested {
layout_id,
frame_state,
};
layout_id
}
fn paint(mut self, cx: &mut WindowContext) -> Option<E::State> {
match self.phase {
ElementDrawPhase::LayoutRequested {
layout_id,
frame_state,
}
| ElementDrawPhase::LayoutComputed {
layout_id,
frame_state,
..
} => {
let bounds = cx.layout_bounds(layout_id);
if let Some(mut frame_state) = frame_state {
self.element
.take()
.unwrap()
.paint(bounds, &mut frame_state, cx);
Some(frame_state)
} else {
let element_id = self
.element
.as_ref()
.unwrap()
.element_id()
.expect("if we don't have frame state, we should have element state");
cx.with_element_state(element_id, |element_state, cx| {
let mut element_state = element_state.unwrap();
self.element
.take()
.unwrap()
.paint(bounds, &mut element_state, cx);
((), element_state)
});
None
}
}
_ => panic!("must call layout before paint"),
}
}
fn measure(
&mut self,
available_space: Size<AvailableSpace>,
cx: &mut WindowContext,
) -> Size<Pixels> {
if matches!(&self.phase, ElementDrawPhase::Start) {
self.layout(cx);
}
let layout_id = match &mut self.phase {
ElementDrawPhase::LayoutRequested {
layout_id,
frame_state,
} => {
cx.compute_layout(*layout_id, available_space);
let layout_id = *layout_id;
self.phase = ElementDrawPhase::LayoutComputed {
layout_id,
available_space,
frame_state: frame_state.take(),
};
layout_id
}
ElementDrawPhase::LayoutComputed {
layout_id,
available_space: prev_available_space,
..
} => {
if available_space != *prev_available_space {
cx.compute_layout(*layout_id, available_space);
*prev_available_space = available_space;
}
*layout_id
}
_ => panic!("cannot measure after painting"),
};
cx.layout_bounds(layout_id).size
}
fn draw(
mut self,
origin: Point<Pixels>,
available_space: Size<AvailableSpace>,
cx: &mut WindowContext,
) -> Option<E::State> {
self.measure(available_space, cx);
cx.with_absolute_element_offset(origin, |cx| self.paint(cx))
}
}
// impl<V: 'static, E: Element> Element for DrawableElement<V, E> {
// type State = <E::Element as Element>::State;
// fn layout(
// &mut self,
// element_state: Option<Self::State>,
// cx: &mut WindowContext,
// ) -> (LayoutId, Self::State) {
// }
// fn paint(
// self,
// bounds: Bounds<Pixels>,
// element_state: &mut Self::State,
// cx: &mut WindowContext,
// ) {
// todo!()
// }
// }
// impl<V: 'static, E: 'static + Element> RenderOnce for DrawableElement<V, E> {
// type Element = Self;
// fn element_id(&self) -> Option<ElementId> {
// self.element.as_ref()?.element_id()
// }
// fn render_once(self) -> Self::Element {
// self
// }
// }
impl<E> ElementObject for Option<DrawableElement<E>>
where where
V: 'static, E: Element,
E: 'static + Component<V>, E::State: 'static,
F: FnOnce(&mut V, &mut ViewContext<'_, V>) -> E + 'static,
{ {
fn render(self) -> AnyElement<V> { fn element_id(&self) -> Option<ElementId> {
self.as_ref().unwrap().element_id()
}
fn layout(&mut self, cx: &mut WindowContext) -> LayoutId {
DrawableElement::layout(self.as_mut().unwrap(), cx)
}
fn paint(&mut self, cx: &mut WindowContext) {
DrawableElement::paint(self.take().unwrap(), cx);
}
fn measure(
&mut self,
available_space: Size<AvailableSpace>,
cx: &mut WindowContext,
) -> Size<Pixels> {
DrawableElement::measure(self.as_mut().unwrap(), available_space, cx)
}
fn draw(
&mut self,
origin: Point<Pixels>,
available_space: Size<AvailableSpace>,
cx: &mut WindowContext,
) {
DrawableElement::draw(self.take().unwrap(), origin, available_space, cx);
}
}
pub struct AnyElement(Box<dyn ElementObject>);
impl AnyElement {
pub fn new<E>(element: E) -> Self
where
E: 'static + Element,
E::State: Any,
{
AnyElement(Box::new(Some(DrawableElement::new(element))) as Box<dyn ElementObject>)
}
pub fn element_id(&self) -> Option<ElementId> {
self.0.element_id()
}
pub fn layout(&mut self, cx: &mut WindowContext) -> LayoutId {
self.0.layout(cx)
}
pub fn paint(mut self, cx: &mut WindowContext) {
self.0.paint(cx)
}
/// Initializes this element and performs layout within the given available space to determine its size.
pub fn measure(
&mut self,
available_space: Size<AvailableSpace>,
cx: &mut WindowContext,
) -> Size<Pixels> {
self.0.measure(available_space, cx)
}
/// Initializes this element and performs layout in the available space, then paints it at the given origin.
pub fn draw(
mut self,
origin: Point<Pixels>,
available_space: Size<AvailableSpace>,
cx: &mut WindowContext,
) {
self.0.draw(origin, available_space, cx)
}
/// Converts this `AnyElement` into a trait object that can be stored and manipulated.
pub fn into_any(self) -> AnyElement {
AnyElement::new(self) AnyElement::new(self)
} }
} }
impl<V, E, F> Component<V> for F impl Element for AnyElement {
where type State = ();
V: 'static,
E: 'static + Component<V>, fn layout(
F: FnOnce(&mut V, &mut ViewContext<'_, V>) -> E + 'static, &mut self,
{ _: Option<Self::State>,
fn render(self) -> AnyElement<V> { cx: &mut WindowContext,
AnyElement::new(Some(self)) ) -> (LayoutId, Self::State) {
let layout_id = self.layout(cx);
(layout_id, ())
}
fn paint(self, _: Bounds<Pixels>, _: &mut Self::State, cx: &mut WindowContext) {
self.paint(cx);
} }
} }
impl RenderOnce for AnyElement {
type Element = Self;
fn element_id(&self) -> Option<ElementId> {
AnyElement::element_id(self)
}
fn render_once(self) -> Self::Element {
self
}
}
// impl<V, E, F> Element for Option<F>
// where
// V: 'static,
// E: Element,
// F: FnOnce(&mut V, &mut WindowContext<'_, V>) -> E + 'static,
// {
// type State = Option<AnyElement>;
// fn element_id(&self) -> Option<ElementId> {
// None
// }
// fn layout(
// &mut self,
// _: Option<Self::State>,
// cx: &mut WindowContext,
// ) -> (LayoutId, Self::State) {
// let render = self.take().unwrap();
// let mut element = (render)(view_state, cx).into_any();
// let layout_id = element.layout(view_state, cx);
// (layout_id, Some(element))
// }
// fn paint(
// self,
// _bounds: Bounds<Pixels>,
// rendered_element: &mut Self::State,
// cx: &mut WindowContext,
// ) {
// rendered_element.take().unwrap().paint(view_state, cx);
// }
// }
// impl<V, E, F> RenderOnce for Option<F>
// where
// V: 'static,
// E: Element,
// F: FnOnce(&mut V, &mut WindowContext) -> E + 'static,
// {
// type Element = Self;
// fn render(self) -> Self::Element {
// self
// }
// }

File diff suppressed because it is too large Load diff

View file

@ -1,18 +1,17 @@
use crate::{ use crate::{
AnyElement, BorrowWindow, Bounds, Component, Element, InteractiveComponent, Bounds, Element, InteractiveElement, InteractiveElementState, Interactivity, LayoutId, Pixels,
InteractiveElementState, Interactivity, LayoutId, Pixels, SharedString, StyleRefinement, RenderOnce, SharedString, StyleRefinement, Styled, WindowContext,
Styled, ViewContext,
}; };
use futures::FutureExt; use futures::FutureExt;
use util::ResultExt; use util::ResultExt;
pub struct Img<V: 'static> { pub struct Img {
interactivity: Interactivity<V>, interactivity: Interactivity,
uri: Option<SharedString>, uri: Option<SharedString>,
grayscale: bool, grayscale: bool,
} }
pub fn img<V: 'static>() -> Img<V> { pub fn img() -> Img {
Img { Img {
interactivity: Interactivity::default(), interactivity: Interactivity::default(),
uri: None, uri: None,
@ -20,10 +19,7 @@ pub fn img<V: 'static>() -> Img<V> {
} }
} }
impl<V> Img<V> impl Img {
where
V: 'static,
{
pub fn uri(mut self, uri: impl Into<SharedString>) -> Self { pub fn uri(mut self, uri: impl Into<SharedString>) -> Self {
self.uri = Some(uri.into()); self.uri = Some(uri.into());
self self
@ -35,36 +31,24 @@ where
} }
} }
impl<V> Component<V> for Img<V> { impl Element for Img {
fn render(self) -> AnyElement<V> { type State = InteractiveElementState;
AnyElement::new(self)
}
}
impl<V> Element<V> for Img<V> {
type ElementState = InteractiveElementState;
fn element_id(&self) -> Option<crate::ElementId> {
self.interactivity.element_id.clone()
}
fn layout( fn layout(
&mut self, &mut self,
_view_state: &mut V, element_state: Option<Self::State>,
element_state: Option<Self::ElementState>, cx: &mut WindowContext,
cx: &mut ViewContext<V>, ) -> (LayoutId, Self::State) {
) -> (LayoutId, Self::ElementState) {
self.interactivity.layout(element_state, cx, |style, cx| { self.interactivity.layout(element_state, cx, |style, cx| {
cx.request_layout(&style, None) cx.request_layout(&style, None)
}) })
} }
fn paint( fn paint(
&mut self, self,
bounds: Bounds<Pixels>, bounds: Bounds<Pixels>,
_view_state: &mut V, element_state: &mut Self::State,
element_state: &mut Self::ElementState, cx: &mut WindowContext,
cx: &mut ViewContext<V>,
) { ) {
self.interactivity.paint( self.interactivity.paint(
bounds, bounds,
@ -81,7 +65,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| {
@ -89,8 +73,8 @@ impl<V> Element<V> for Img<V> {
.log_err() .log_err()
}); });
} 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());
} }
}) })
@ -102,14 +86,26 @@ impl<V> Element<V> for Img<V> {
} }
} }
impl<V> Styled for Img<V> { impl RenderOnce for Img {
type Element = Self;
fn element_id(&self) -> Option<crate::ElementId> {
self.interactivity.element_id.clone()
}
fn render_once(self) -> Self::Element {
self
}
}
impl Styled for Img {
fn style(&mut self) -> &mut StyleRefinement { fn style(&mut self) -> &mut StyleRefinement {
&mut self.interactivity.base_style &mut self.interactivity.base_style
} }
} }
impl<V> InteractiveComponent<V> for Img<V> { impl InteractiveElement for Img {
fn interactivity(&mut self) -> &mut Interactivity<V> { fn interactivity(&mut self) -> &mut Interactivity {
&mut self.interactivity &mut self.interactivity
} }
} }

View file

@ -2,16 +2,16 @@ use smallvec::SmallVec;
use taffy::style::{Display, Position}; use taffy::style::{Display, Position};
use crate::{ use crate::{
point, AnyElement, BorrowWindow, Bounds, Component, Element, LayoutId, ParentComponent, Pixels, point, AnyElement, BorrowWindow, Bounds, Element, LayoutId, ParentElement, Pixels, Point,
Point, Size, Style, RenderOnce, Size, Style, WindowContext,
}; };
pub struct OverlayState { pub struct OverlayState {
child_layout_ids: SmallVec<[LayoutId; 4]>, child_layout_ids: SmallVec<[LayoutId; 4]>,
} }
pub struct Overlay<V> { pub struct Overlay {
children: SmallVec<[AnyElement<V>; 2]>, children: SmallVec<[AnyElement; 2]>,
anchor_corner: AnchorCorner, anchor_corner: AnchorCorner,
fit_mode: OverlayFitMode, fit_mode: OverlayFitMode,
// todo!(); // todo!();
@ -21,7 +21,7 @@ pub struct Overlay<V> {
/// overlay gives you a floating element that will avoid overflowing the window bounds. /// overlay gives you a floating element that will avoid overflowing the window bounds.
/// Its children should have no margin to avoid measurement issues. /// Its children should have no margin to avoid measurement issues.
pub fn overlay<V: 'static>() -> Overlay<V> { pub fn overlay() -> Overlay {
Overlay { Overlay {
children: SmallVec::new(), children: SmallVec::new(),
anchor_corner: AnchorCorner::TopLeft, anchor_corner: AnchorCorner::TopLeft,
@ -30,7 +30,7 @@ pub fn overlay<V: 'static>() -> Overlay<V> {
} }
} }
impl<V> Overlay<V> { impl Overlay {
/// Sets which corner of the overlay should be anchored to the current position. /// Sets which corner of the overlay should be anchored to the current position.
pub fn anchor(mut self, anchor: AnchorCorner) -> Self { pub fn anchor(mut self, anchor: AnchorCorner) -> Self {
self.anchor_corner = anchor; self.anchor_corner = anchor;
@ -51,35 +51,24 @@ impl<V> Overlay<V> {
} }
} }
impl<V: 'static> ParentComponent<V> for Overlay<V> { impl ParentElement for Overlay {
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> { fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
&mut self.children &mut self.children
} }
} }
impl<V: 'static> Component<V> for Overlay<V> { impl Element for Overlay {
fn render(self) -> AnyElement<V> { type State = OverlayState;
AnyElement::new(self)
}
}
impl<V: 'static> Element<V> for Overlay<V> {
type ElementState = OverlayState;
fn element_id(&self) -> Option<crate::ElementId> {
None
}
fn layout( fn layout(
&mut self, &mut self,
view_state: &mut V, _: Option<Self::State>,
_: Option<Self::ElementState>, cx: &mut WindowContext,
cx: &mut crate::ViewContext<V>, ) -> (crate::LayoutId, Self::State) {
) -> (crate::LayoutId, Self::ElementState) {
let child_layout_ids = self let child_layout_ids = self
.children .children
.iter_mut() .iter_mut()
.map(|child| child.layout(view_state, cx)) .map(|child| child.layout(cx))
.collect::<SmallVec<_>>(); .collect::<SmallVec<_>>();
let mut overlay_style = Style::default(); let mut overlay_style = Style::default();
@ -92,11 +81,10 @@ impl<V: 'static> Element<V> for Overlay<V> {
} }
fn paint( fn paint(
&mut self, self,
bounds: crate::Bounds<crate::Pixels>, bounds: crate::Bounds<crate::Pixels>,
view_state: &mut V, element_state: &mut Self::State,
element_state: &mut Self::ElementState, cx: &mut WindowContext,
cx: &mut crate::ViewContext<V>,
) { ) {
if element_state.child_layout_ids.is_empty() { if element_state.child_layout_ids.is_empty() {
return; return;
@ -156,13 +144,25 @@ impl<V: 'static> Element<V> for Overlay<V> {
} }
cx.with_element_offset(desired.origin - bounds.origin, |cx| { cx.with_element_offset(desired.origin - bounds.origin, |cx| {
for child in &mut self.children { for child in self.children {
child.paint(view_state, cx); child.paint(cx);
} }
}) })
} }
} }
impl RenderOnce for Overlay {
type Element = Self;
fn element_id(&self) -> Option<crate::ElementId> {
None
}
fn render_once(self) -> Self::Element {
self
}
}
enum Axis { enum Axis {
Horizontal, Horizontal,
Vertical, Vertical,

View file

@ -1,60 +1,43 @@
use crate::{ use crate::{
AnyElement, Bounds, Component, Element, ElementId, InteractiveComponent, Bounds, Element, ElementId, InteractiveElement, InteractiveElementState, Interactivity,
InteractiveElementState, Interactivity, LayoutId, Pixels, SharedString, StyleRefinement, LayoutId, Pixels, RenderOnce, SharedString, StyleRefinement, Styled, WindowContext,
Styled, ViewContext,
}; };
use util::ResultExt; use util::ResultExt;
pub struct Svg<V: 'static> { pub struct Svg {
interactivity: Interactivity<V>, interactivity: Interactivity,
path: Option<SharedString>, path: Option<SharedString>,
} }
pub fn svg<V: 'static>() -> Svg<V> { pub fn svg() -> Svg {
Svg { Svg {
interactivity: Interactivity::default(), interactivity: Interactivity::default(),
path: None, path: None,
} }
} }
impl<V> Svg<V> { impl Svg {
pub fn path(mut self, path: impl Into<SharedString>) -> Self { pub fn path(mut self, path: impl Into<SharedString>) -> Self {
self.path = Some(path.into()); self.path = Some(path.into());
self self
} }
} }
impl<V> Component<V> for Svg<V> { impl Element for Svg {
fn render(self) -> AnyElement<V> { type State = InteractiveElementState;
AnyElement::new(self)
}
}
impl<V> Element<V> for Svg<V> {
type ElementState = InteractiveElementState;
fn element_id(&self) -> Option<ElementId> {
self.interactivity.element_id.clone()
}
fn layout( fn layout(
&mut self, &mut self,
_view_state: &mut V, element_state: Option<Self::State>,
element_state: Option<Self::ElementState>, cx: &mut WindowContext,
cx: &mut ViewContext<V>, ) -> (LayoutId, Self::State) {
) -> (LayoutId, Self::ElementState) {
self.interactivity.layout(element_state, cx, |style, cx| { self.interactivity.layout(element_state, cx, |style, cx| {
cx.request_layout(&style, None) cx.request_layout(&style, None)
}) })
} }
fn paint( fn paint(self, bounds: Bounds<Pixels>, element_state: &mut Self::State, cx: &mut WindowContext)
&mut self, where
bounds: Bounds<Pixels>,
_view_state: &mut V,
element_state: &mut Self::ElementState,
cx: &mut ViewContext<V>,
) where
Self: Sized, Self: Sized,
{ {
self.interactivity self.interactivity
@ -66,14 +49,26 @@ impl<V> Element<V> for Svg<V> {
} }
} }
impl<V> Styled for Svg<V> { impl RenderOnce for Svg {
type Element = Self;
fn element_id(&self) -> Option<ElementId> {
self.interactivity.element_id.clone()
}
fn render_once(self) -> Self::Element {
self
}
}
impl Styled for Svg {
fn style(&mut self) -> &mut StyleRefinement { fn style(&mut self) -> &mut StyleRefinement {
&mut self.interactivity.base_style &mut self.interactivity.base_style
} }
} }
impl<V> InteractiveComponent<V> for Svg<V> { impl InteractiveElement for Svg {
fn interactivity(&mut self) -> &mut Interactivity<V> { fn interactivity(&mut self) -> &mut Interactivity {
&mut self.interactivity &mut self.interactivity
} }
} }

View file

@ -1,82 +1,191 @@
use crate::{ use crate::{
AnyElement, BorrowWindow, Bounds, Component, Element, ElementId, LayoutId, Pixels, Bounds, Element, ElementId, LayoutId, Pixels, RenderOnce, SharedString, Size, TextRun,
SharedString, Size, TextRun, ViewContext, WrappedLine, WindowContext, WrappedLine,
}; };
use anyhow::anyhow;
use parking_lot::{Mutex, MutexGuard}; use parking_lot::{Mutex, MutexGuard};
use smallvec::SmallVec; use smallvec::SmallVec;
use std::{cell::Cell, rc::Rc, sync::Arc}; use std::{cell::Cell, rc::Rc, sync::Arc};
use util::ResultExt; use util::ResultExt;
pub struct Text { impl Element for &'static str {
type State = TextState;
fn layout(
&mut self,
_: Option<Self::State>,
cx: &mut WindowContext,
) -> (LayoutId, Self::State) {
let mut state = TextState::default();
let layout_id = state.layout(SharedString::from(*self), None, cx);
(layout_id, state)
}
fn paint(self, bounds: Bounds<Pixels>, state: &mut TextState, cx: &mut WindowContext) {
state.paint(bounds, self, cx)
}
}
impl RenderOnce for &'static str {
type Element = Self;
fn element_id(&self) -> Option<ElementId> {
None
}
fn render_once(self) -> Self::Element {
self
}
}
impl Element for SharedString {
type State = TextState;
fn layout(
&mut self,
_: Option<Self::State>,
cx: &mut WindowContext,
) -> (LayoutId, Self::State) {
let mut state = TextState::default();
let layout_id = state.layout(self.clone(), None, cx);
(layout_id, state)
}
fn paint(self, bounds: Bounds<Pixels>, state: &mut TextState, cx: &mut WindowContext) {
let text_str: &str = self.as_ref();
state.paint(bounds, text_str, cx)
}
}
impl RenderOnce for SharedString {
type Element = Self;
fn element_id(&self) -> Option<ElementId> {
None
}
fn render_once(self) -> Self::Element {
self
}
}
pub struct StyledText {
text: SharedString, text: SharedString,
runs: Option<Vec<TextRun>>, runs: Option<Vec<TextRun>>,
} }
impl Text { impl StyledText {
/// Renders text with runs of different styles. /// Renders text with runs of different styles.
/// ///
/// Callers are responsible for setting the correct style for each run. /// Callers are responsible for setting the correct style for each run.
/// For text with a uniform style, you can usually avoid calling this constructor /// For text with a uniform style, you can usually avoid calling this constructor
/// and just pass text directly. /// and just pass text directly.
pub fn styled(text: SharedString, runs: Vec<TextRun>) -> Self { pub fn new(text: SharedString, runs: Vec<TextRun>) -> Self {
Text { StyledText {
text, text,
runs: Some(runs), runs: Some(runs),
} }
} }
} }
impl<V: 'static> Component<V> for Text { impl Element for StyledText {
fn render(self) -> AnyElement<V> { type State = TextState;
AnyElement::new(self)
fn layout(
&mut self,
_: Option<Self::State>,
cx: &mut WindowContext,
) -> (LayoutId, Self::State) {
let mut state = TextState::default();
let layout_id = state.layout(self.text.clone(), self.runs.take(), cx);
(layout_id, state)
}
fn paint(self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
state.paint(bounds, &self.text, cx)
} }
} }
impl<V: 'static> Element<V> for Text { impl RenderOnce for StyledText {
type ElementState = TextState; type Element = Self;
fn element_id(&self) -> Option<crate::ElementId> { fn element_id(&self) -> Option<crate::ElementId> {
None None
} }
fn render_once(self) -> Self::Element {
self
}
}
#[derive(Default, Clone)]
pub struct TextState(Arc<Mutex<Option<TextStateInner>>>);
struct TextStateInner {
lines: SmallVec<[WrappedLine; 1]>,
line_height: Pixels,
wrap_width: Option<Pixels>,
size: Option<Size<Pixels>>,
}
impl TextState {
fn lock(&self) -> MutexGuard<Option<TextStateInner>> {
self.0.lock()
}
fn layout( fn layout(
&mut self, &mut self,
_view: &mut V, text: SharedString,
element_state: Option<Self::ElementState>, runs: Option<Vec<TextRun>>,
cx: &mut ViewContext<V>, cx: &mut WindowContext,
) -> (LayoutId, Self::ElementState) { ) -> LayoutId {
let element_state = element_state.unwrap_or_default();
let text_system = cx.text_system().clone(); let text_system = cx.text_system().clone();
let text_style = cx.text_style(); let text_style = cx.text_style();
let font_size = text_style.font_size.to_pixels(cx.rem_size()); let font_size = text_style.font_size.to_pixels(cx.rem_size());
let line_height = text_style let line_height = text_style
.line_height .line_height
.to_pixels(font_size.into(), cx.rem_size()); .to_pixels(font_size.into(), cx.rem_size());
let text = self.text.clone(); let text = SharedString::from(text);
let rem_size = cx.rem_size(); let rem_size = cx.rem_size();
let runs = if let Some(runs) = self.runs.take() { let runs = if let Some(runs) = runs {
runs runs
} else { } else {
vec![text_style.to_run(text.len())] vec![text_style.to_run(text.len())]
}; };
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 = self.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();
}; };
@ -88,28 +197,25 @@ impl<V: 'static> Element<V> for Text {
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
} }
}); });
(layout_id, element_state) layout_id
} }
fn paint( fn paint(&mut self, bounds: Bounds<Pixels>, text: &str, cx: &mut WindowContext) {
&mut self, let element_state = self.lock();
bounds: Bounds<Pixels>,
_: &mut V,
element_state: &mut Self::ElementState,
cx: &mut ViewContext<V>,
) {
let element_state = element_state.lock();
let element_state = element_state let element_state = element_state
.as_ref() .as_ref()
.ok_or_else(|| anyhow::anyhow!("measurement has not been performed on {}", &self.text)) .ok_or_else(|| anyhow!("measurement has not been performed on {}", text))
.unwrap(); .unwrap();
let line_height = element_state.line_height; let line_height = element_state.line_height;
@ -121,23 +227,9 @@ impl<V: 'static> Element<V> for Text {
} }
} }
#[derive(Default, Clone)]
pub struct TextState(Arc<Mutex<Option<TextStateInner>>>);
impl TextState {
fn lock(&self) -> MutexGuard<Option<TextStateInner>> {
self.0.lock()
}
}
struct TextStateInner {
lines: SmallVec<[WrappedLine; 1]>,
line_height: Pixels,
}
struct InteractiveText { struct InteractiveText {
id: ElementId, element_id: ElementId,
text: Text, text: StyledText,
} }
struct InteractiveTextState { struct InteractiveTextState {
@ -145,32 +237,27 @@ struct InteractiveTextState {
clicked_range_ixs: Rc<Cell<SmallVec<[usize; 1]>>>, clicked_range_ixs: Rc<Cell<SmallVec<[usize; 1]>>>,
} }
impl<V: 'static> Element<V> for InteractiveText { impl Element for InteractiveText {
type ElementState = InteractiveTextState; type State = InteractiveTextState;
fn element_id(&self) -> Option<ElementId> {
Some(self.id.clone())
}
fn layout( fn layout(
&mut self, &mut self,
view_state: &mut V, state: Option<Self::State>,
element_state: Option<Self::ElementState>, cx: &mut WindowContext,
cx: &mut ViewContext<V>, ) -> (LayoutId, Self::State) {
) -> (LayoutId, Self::ElementState) {
if let Some(InteractiveTextState { if let Some(InteractiveTextState {
text_state, text_state,
clicked_range_ixs, clicked_range_ixs,
}) = element_state }) = state
{ {
let (layout_id, text_state) = self.text.layout(view_state, Some(text_state), cx); let (layout_id, text_state) = self.text.layout(Some(text_state), cx);
let element_state = InteractiveTextState { let element_state = InteractiveTextState {
text_state, text_state,
clicked_range_ixs, clicked_range_ixs,
}; };
(layout_id, element_state) (layout_id, element_state)
} else { } else {
let (layout_id, text_state) = self.text.layout(view_state, None, cx); let (layout_id, text_state) = self.text.layout(None, cx);
let element_state = InteractiveTextState { let element_state = InteractiveTextState {
text_state, text_state,
clicked_range_ixs: Rc::default(), clicked_range_ixs: Rc::default(),
@ -179,46 +266,19 @@ impl<V: 'static> Element<V> for InteractiveText {
} }
} }
fn paint( fn paint(self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
&mut self, self.text.paint(bounds, &mut state.text_state, cx)
bounds: Bounds<Pixels>,
view_state: &mut V,
element_state: &mut Self::ElementState,
cx: &mut ViewContext<V>,
) {
self.text
.paint(bounds, view_state, &mut element_state.text_state, cx)
} }
} }
impl<V: 'static> Component<V> for SharedString { impl RenderOnce for InteractiveText {
fn render(self) -> AnyElement<V> { type Element = Self;
Text {
text: self,
runs: None,
}
.render()
}
}
impl<V: 'static> Component<V> for &'static str { fn element_id(&self) -> Option<ElementId> {
fn render(self) -> AnyElement<V> { Some(self.element_id.clone())
Text {
text: self.into(),
runs: None,
}
.render()
} }
}
// TODO: Figure out how to pass `String` to `child` without this. fn render_once(self) -> Self::Element {
// This impl doesn't exist in the `gpui2` crate. self
impl<V: 'static> Component<V> for String {
fn render(self) -> AnyElement<V> {
Text {
text: self.into(),
runs: None,
}
.render()
} }
} }

View file

@ -1,40 +1,45 @@
use crate::{ use crate::{
point, px, size, AnyElement, AvailableSpace, BorrowWindow, Bounds, Component, Element, point, px, size, AnyElement, AvailableSpace, Bounds, Element, ElementId, InteractiveElement,
ElementId, InteractiveComponent, InteractiveElementState, Interactivity, LayoutId, Pixels, InteractiveElementState, Interactivity, LayoutId, Pixels, Point, Render, RenderOnce, Size,
Point, Size, StyleRefinement, Styled, ViewContext, StyleRefinement, Styled, View, ViewContext, WindowContext,
}; };
use smallvec::SmallVec; use smallvec::SmallVec;
use std::{cell::RefCell, cmp, mem, ops::Range, rc::Rc}; use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
use taffy::style::Overflow; use taffy::style::Overflow;
/// uniform_list provides lazy rendering for a set of items that are of uniform height. /// uniform_list provides lazy rendering for a set of items that are of uniform height.
/// When rendered into a container with overflow-y: hidden and a fixed (or max) height, /// When rendered into a container with overflow-y: hidden and a fixed (or max) height,
/// uniform_list will only render the visibile subset of items. /// uniform_list will only render the visibile subset of items.
pub fn uniform_list<I, V, C>( pub fn uniform_list<I, R, V>(
view: View<V>,
id: I, id: I,
item_count: usize, item_count: usize,
f: impl 'static + Fn(&mut V, Range<usize>, &mut ViewContext<V>) -> Vec<C>, f: impl 'static + Fn(&mut V, Range<usize>, &mut ViewContext<V>) -> Vec<R>,
) -> UniformList<V> ) -> UniformList
where where
I: Into<ElementId>, I: Into<ElementId>,
V: 'static, R: RenderOnce,
C: Component<V>, V: Render,
{ {
let id = id.into(); let id = id.into();
let mut style = StyleRefinement::default(); let mut style = StyleRefinement::default();
style.overflow.y = Some(Overflow::Hidden); style.overflow.y = Some(Overflow::Hidden);
let render_range = move |range, cx: &mut WindowContext| {
view.update(cx, |this, cx| {
f(this, range, cx)
.into_iter()
.map(|component| component.render_into_any())
.collect()
})
};
UniformList { UniformList {
id: id.clone(), id: id.clone(),
style, style,
item_count, item_count,
item_to_measure_index: 0, item_to_measure_index: 0,
render_items: Box::new(move |view, visible_range, cx| { render_items: Box::new(render_range),
f(view, visible_range, cx)
.into_iter()
.map(|component| component.render())
.collect()
}),
interactivity: Interactivity { interactivity: Interactivity {
element_id: Some(id.into()), element_id: Some(id.into()),
..Default::default() ..Default::default()
@ -43,19 +48,14 @@ where
} }
} }
pub struct UniformList<V: 'static> { pub struct UniformList {
id: ElementId, id: ElementId,
style: StyleRefinement, style: StyleRefinement,
item_count: usize, item_count: usize,
item_to_measure_index: usize, item_to_measure_index: usize,
render_items: Box< render_items:
dyn for<'a> Fn( Box<dyn for<'a> Fn(Range<usize>, &'a mut WindowContext) -> SmallVec<[AnyElement; 64]>>,
&'a mut V, interactivity: Interactivity,
Range<usize>,
&'a mut ViewContext<V>,
) -> SmallVec<[AnyElement<V>; 64]>,
>,
interactivity: Interactivity<V>,
scroll_handle: Option<UniformListScrollHandle>, scroll_handle: Option<UniformListScrollHandle>,
} }
@ -89,7 +89,7 @@ impl UniformListScrollHandle {
} }
} }
impl<V: 'static> Styled for UniformList<V> { impl Styled for UniformList {
fn style(&mut self) -> &mut StyleRefinement { fn style(&mut self) -> &mut StyleRefinement {
&mut self.style &mut self.style
} }
@ -101,29 +101,24 @@ pub struct UniformListState {
item_size: Size<Pixels>, item_size: Size<Pixels>,
} }
impl<V: 'static> Element<V> for UniformList<V> { impl Element for UniformList {
type ElementState = UniformListState; type State = UniformListState;
fn element_id(&self) -> Option<crate::ElementId> {
Some(self.id.clone())
}
fn layout( fn layout(
&mut self, &mut self,
view_state: &mut V, state: Option<Self::State>,
element_state: Option<Self::ElementState>, cx: &mut WindowContext,
cx: &mut ViewContext<V>, ) -> (LayoutId, Self::State) {
) -> (LayoutId, Self::ElementState) {
let max_items = self.item_count; let max_items = self.item_count;
let rem_size = cx.rem_size(); let rem_size = cx.rem_size();
let item_size = element_state let item_size = state
.as_ref() .as_ref()
.map(|s| s.item_size) .map(|s| s.item_size)
.unwrap_or_else(|| self.measure_item(view_state, None, cx)); .unwrap_or_else(|| self.measure_item(None, cx));
let (layout_id, interactive) = let (layout_id, interactive) =
self.interactivity self.interactivity
.layout(element_state.map(|s| s.interactive), cx, |style, cx| { .layout(state.map(|s| s.interactive), cx, |style, cx| {
cx.request_measured_layout( cx.request_measured_layout(
style, style,
rem_size, rem_size,
@ -159,11 +154,10 @@ impl<V: 'static> Element<V> for UniformList<V> {
} }
fn paint( fn paint(
&mut self, self,
bounds: Bounds<crate::Pixels>, bounds: Bounds<crate::Pixels>,
view_state: &mut V, element_state: &mut Self::State,
element_state: &mut Self::ElementState, cx: &mut WindowContext,
cx: &mut ViewContext<V>,
) { ) {
let style = let style =
self.interactivity self.interactivity
@ -183,14 +177,15 @@ impl<V: 'static> Element<V> for UniformList<V> {
height: item_size.height * self.item_count, height: item_size.height * self.item_count,
}; };
let mut interactivity = mem::take(&mut self.interactivity);
let shared_scroll_offset = element_state let shared_scroll_offset = element_state
.interactive .interactive
.scroll_offset .scroll_offset
.get_or_insert_with(Rc::default) .get_or_insert_with(Rc::default)
.clone(); .clone();
interactivity.paint( let item_height = self.measure_item(Some(padded_bounds.size.width), cx).height;
self.interactivity.paint(
bounds, bounds,
content_size, content_size,
&mut element_state.interactive, &mut element_state.interactive,
@ -209,9 +204,6 @@ impl<V: 'static> Element<V> for UniformList<V> {
style.paint(bounds, cx); style.paint(bounds, cx);
if self.item_count > 0 { if self.item_count > 0 {
let item_height = self
.measure_item(view_state, Some(padded_bounds.size.width), cx)
.height;
if let Some(scroll_handle) = self.scroll_handle.clone() { if let Some(scroll_handle) = self.scroll_handle.clone() {
scroll_handle.0.borrow_mut().replace(ScrollHandleState { scroll_handle.0.borrow_mut().replace(ScrollHandleState {
item_height, item_height,
@ -233,44 +225,50 @@ impl<V: 'static> Element<V> for UniformList<V> {
self.item_count, self.item_count,
); );
let mut items = (self.render_items)(view_state, visible_range.clone(), cx); let items = (self.render_items)(visible_range.clone(), cx);
cx.with_z_index(1, |cx| { cx.with_z_index(1, |cx| {
for (item, ix) in items.iter_mut().zip(visible_range) { for (item, ix) in items.into_iter().zip(visible_range) {
let item_origin = padded_bounds.origin let item_origin = padded_bounds.origin
+ point(px(0.), item_height * ix + scroll_offset.y); + point(px(0.), item_height * ix + scroll_offset.y);
let available_space = size( let available_space = size(
AvailableSpace::Definite(padded_bounds.size.width), AvailableSpace::Definite(padded_bounds.size.width),
AvailableSpace::Definite(item_height), AvailableSpace::Definite(item_height),
); );
item.draw(item_origin, available_space, view_state, cx); item.draw(item_origin, available_space, cx);
} }
}); });
} }
}) })
}, },
); );
self.interactivity = interactivity;
} }
} }
impl<V> UniformList<V> { impl RenderOnce for UniformList {
type Element = Self;
fn element_id(&self) -> Option<crate::ElementId> {
Some(self.id.clone())
}
fn render_once(self) -> Self::Element {
self
}
}
impl UniformList {
pub fn with_width_from_item(mut self, item_index: Option<usize>) -> Self { pub fn with_width_from_item(mut self, item_index: Option<usize>) -> Self {
self.item_to_measure_index = item_index.unwrap_or(0); self.item_to_measure_index = item_index.unwrap_or(0);
self self
} }
fn measure_item( fn measure_item(&self, list_width: Option<Pixels>, cx: &mut WindowContext) -> Size<Pixels> {
&self,
view_state: &mut V,
list_width: Option<Pixels>,
cx: &mut ViewContext<V>,
) -> Size<Pixels> {
if self.item_count == 0 { if self.item_count == 0 {
return Size::default(); return Size::default();
} }
let item_ix = cmp::min(self.item_to_measure_index, self.item_count - 1); let item_ix = cmp::min(self.item_to_measure_index, self.item_count - 1);
let mut items = (self.render_items)(view_state, item_ix..item_ix + 1, cx); let mut items = (self.render_items)(item_ix..item_ix + 1, cx);
let mut item_to_measure = items.pop().unwrap(); let mut item_to_measure = items.pop().unwrap();
let available_space = size( let available_space = size(
list_width.map_or(AvailableSpace::MinContent, |width| { list_width.map_or(AvailableSpace::MinContent, |width| {
@ -278,7 +276,7 @@ impl<V> UniformList<V> {
}), }),
AvailableSpace::MinContent, AvailableSpace::MinContent,
); );
item_to_measure.measure(available_space, view_state, cx) item_to_measure.measure(available_space, cx)
} }
pub fn track_scroll(mut self, handle: UniformListScrollHandle) -> Self { pub fn track_scroll(mut self, handle: UniformListScrollHandle) -> Self {
@ -287,14 +285,8 @@ impl<V> UniformList<V> {
} }
} }
impl<V> InteractiveComponent<V> for UniformList<V> { impl InteractiveElement for UniformList {
fn interactivity(&mut self) -> &mut crate::Interactivity<V> { fn interactivity(&mut self) -> &mut crate::Interactivity {
&mut self.interactivity &mut self.interactivity
} }
} }
impl<V: 'static> Component<V> for UniformList<V> {
fn render(self) -> AnyElement<V> {
AnyElement::new(self)
}
}

View file

@ -78,8 +78,6 @@ use std::{
}; };
use taffy::TaffyLayoutEngine; use taffy::TaffyLayoutEngine;
type AnyBox = Box<dyn Any>;
pub trait Context { pub trait Context {
type Result<T>; type Result<T>;
@ -136,11 +134,15 @@ pub trait VisualContext: Context {
build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V, build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V,
) -> Self::Result<View<V>> ) -> Self::Result<View<V>>
where where
V: Render; V: 'static + Render;
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

@ -1,4 +1,6 @@
use crate::{AsyncWindowContext, Bounds, Pixels, PlatformInputHandler, View, ViewContext}; use crate::{
AsyncWindowContext, Bounds, Pixels, PlatformInputHandler, View, ViewContext, WindowContext,
};
use std::ops::Range; use std::ops::Range;
/// Implement this trait to allow views to handle textual input when implementing an editor, field, etc. /// Implement this trait to allow views to handle textual input when implementing an editor, field, etc.
@ -43,9 +45,9 @@ pub struct ElementInputHandler<V> {
impl<V: 'static> ElementInputHandler<V> { impl<V: 'static> ElementInputHandler<V> {
/// Used in [Element::paint] with the element's bounds and a view context for its /// Used in [Element::paint] with the element's bounds and a view context for its
/// containing view. /// containing view.
pub fn new(element_bounds: Bounds<Pixels>, cx: &mut ViewContext<V>) -> Self { pub fn new(element_bounds: Bounds<Pixels>, view: View<V>, cx: &mut WindowContext) -> Self {
ElementInputHandler { ElementInputHandler {
view: cx.view().clone(), view,
element_bounds, element_bounds,
cx: cx.to_async(), cx: cx.to_async(),
} }

View file

@ -1,5 +1,5 @@
use crate::{ use crate::{
div, point, Component, Div, FocusHandle, Keystroke, Modifiers, Pixels, Point, Render, div, point, Div, Element, FocusHandle, Keystroke, Modifiers, Pixels, Point, Render, RenderOnce,
ViewContext, ViewContext,
}; };
use smallvec::SmallVec; use smallvec::SmallVec;
@ -64,24 +64,24 @@ pub struct Drag<S, R, V, E>
where where
R: Fn(&mut V, &mut ViewContext<V>) -> E, R: Fn(&mut V, &mut ViewContext<V>) -> E,
V: 'static, V: 'static,
E: Component<()>, E: RenderOnce,
{ {
pub state: S, pub state: S,
pub render_drag_handle: R, pub render_drag_handle: R,
view_type: PhantomData<V>, view_element_types: PhantomData<(V, E)>,
} }
impl<S, R, V, E> Drag<S, R, V, E> impl<S, R, V, E> Drag<S, R, V, E>
where where
R: Fn(&mut V, &mut ViewContext<V>) -> E, R: Fn(&mut V, &mut ViewContext<V>) -> E,
V: 'static, V: 'static,
E: Component<()>, E: Element,
{ {
pub fn new(state: S, render_drag_handle: R) -> Self { pub fn new(state: S, render_drag_handle: R) -> Self {
Drag { Drag {
state, state,
render_drag_handle, render_drag_handle,
view_type: PhantomData, view_element_types: Default::default(),
} }
} }
} }
@ -194,7 +194,7 @@ impl Deref for MouseExitEvent {
pub struct ExternalPaths(pub(crate) SmallVec<[PathBuf; 2]>); pub struct ExternalPaths(pub(crate) SmallVec<[PathBuf; 2]>);
impl Render for ExternalPaths { impl Render for ExternalPaths {
type Element = Div<Self>; type Element = Div;
fn render(&mut self, _: &mut ViewContext<Self>) -> Self::Element { fn render(&mut self, _: &mut ViewContext<Self>) -> Self::Element {
div() // Intentionally left empty because the platform will render icons for the dragged files div() // Intentionally left empty because the platform will render icons for the dragged files
@ -286,8 +286,8 @@ pub struct FocusEvent {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::{ use crate::{
self as gpui, div, Component, Div, FocusHandle, InteractiveComponent, KeyBinding, self as gpui, div, Div, FocusHandle, InteractiveElement, KeyBinding, Keystroke,
Keystroke, ParentComponent, Render, Stateful, TestAppContext, ViewContext, VisualContext, ParentElement, Render, RenderOnce, Stateful, TestAppContext, VisualContext,
}; };
struct TestView { struct TestView {
@ -299,20 +299,24 @@ mod test {
actions!(TestAction); actions!(TestAction);
impl Render for TestView { impl Render for TestView {
type Element = Stateful<Self, Div<Self>>; type Element = Stateful<Div>;
fn render(&mut self, _: &mut gpui::ViewContext<Self>) -> Self::Element { fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element {
div().id("testview").child( div().id("testview").child(
div() div()
.key_context("parent") .key_context("parent")
.on_key_down(|this: &mut TestView, _, _, _| this.saw_key_down = true) .on_key_down(cx.listener(|this, _, _| this.saw_key_down = true))
.on_action(|this: &mut TestView, _: &TestAction, _| this.saw_action = true) .on_action(
.child(|this: &mut Self, _cx: &mut ViewContext<Self>| { cx.listener(|this: &mut TestView, _: &TestAction, _| {
this.saw_action = true
}),
)
.child(
div() div()
.key_context("nested") .key_context("nested")
.track_focus(&this.focus_handle) .track_focus(&self.focus_handle)
.render() .render_once(),
}), ),
) )
} }
} }

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

@ -1,4 +1,5 @@
pub use crate::{ pub use crate::{
BorrowAppContext, BorrowWindow, Component, Context, FocusableComponent, InteractiveComponent, BorrowAppContext, BorrowWindow, Component, Context, Element, FocusableElement,
ParentComponent, Refineable, Render, StatefulInteractiveComponent, Styled, VisualContext, InteractiveElement, ParentElement, Refineable, Render, RenderOnce, StatefulInteractiveElement,
Styled, VisualContext,
}; };

View file

@ -2,7 +2,7 @@ use crate::{
black, phi, point, rems, AbsoluteLength, BorrowAppContext, BorrowWindow, Bounds, ContentMask, black, phi, point, rems, AbsoluteLength, BorrowAppContext, BorrowWindow, Bounds, ContentMask,
Corners, CornersRefinement, CursorStyle, DefiniteLength, Edges, EdgesRefinement, Font, Corners, CornersRefinement, CursorStyle, DefiniteLength, Edges, EdgesRefinement, Font,
FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Rgba, FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Rgba,
SharedString, Size, SizeRefinement, Styled, TextRun, ViewContext, SharedString, Size, SizeRefinement, Styled, TextRun, WindowContext,
}; };
use refineable::{Cascade, Refineable}; use refineable::{Cascade, Refineable};
use smallvec::SmallVec; use smallvec::SmallVec;
@ -313,7 +313,7 @@ impl Style {
} }
/// Paints the background of an element styled with this style. /// Paints the background of an element styled with this style.
pub fn paint<V: 'static>(&self, bounds: Bounds<Pixels>, cx: &mut ViewContext<V>) { pub fn paint(&self, bounds: Bounds<Pixels>, cx: &mut WindowContext) {
let rem_size = cx.rem_size(); let rem_size = cx.rem_size();
cx.with_z_index(0, |cx| { cx.with_z_index(0, |cx| {

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

@ -1,23 +1,17 @@
use crate::{ use crate::{
private::Sealed, AnyBox, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace, private::Sealed, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace, BorrowWindow,
BorrowWindow, Bounds, Component, Element, ElementId, Entity, EntityId, Flatten, FocusHandle, Bounds, Element, ElementId, Entity, EntityId, Flatten, FocusHandle, FocusableView, LayoutId,
FocusableView, LayoutId, Model, Pixels, Point, Size, ViewContext, VisualContext, WeakModel, Model, Pixels, Point, Render, RenderOnce, Size, ViewContext, VisualContext, WeakModel,
WindowContext, WindowContext,
}; };
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use std::{ use std::{
any::{Any, TypeId}, any::TypeId,
hash::{Hash, Hasher}, hash::{Hash, Hasher},
}; };
pub trait Render: 'static + Sized {
type Element: Element<Self> + 'static;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element;
}
pub struct View<V> { pub struct View<V> {
pub(crate) model: Model<V>, pub model: Model<V>,
} }
impl<V> Sealed for View<V> {} impl<V> Sealed for View<V> {}
@ -65,15 +59,15 @@ impl<V: 'static> View<V> {
self.model.read(cx) self.model.read(cx)
} }
pub fn render_with<C>(&self, component: C) -> RenderViewWith<C, V> // pub fn render_with<E>(&self, component: E) -> RenderViewWith<E, V>
where // where
C: 'static + Component<V>, // E: 'static + Element,
{ // {
RenderViewWith { // RenderViewWith {
view: self.clone(), // view: self.clone(),
component: Some(component), // element: Some(component),
} // }
} // }
pub fn focus_handle(&self, cx: &AppContext) -> FocusHandle pub fn focus_handle(&self, cx: &AppContext) -> FocusHandle
where where
@ -83,6 +77,24 @@ impl<V: 'static> View<V> {
} }
} }
impl<V: Render> Element for View<V> {
type State = Option<AnyElement>;
fn layout(
&mut self,
_state: Option<Self::State>,
cx: &mut WindowContext,
) -> (LayoutId, Self::State) {
let mut element = self.update(cx, |view, cx| view.render(cx).into_any());
let layout_id = element.layout(cx);
(layout_id, Some(element))
}
fn paint(self, _: Bounds<Pixels>, element: &mut Self::State, cx: &mut WindowContext) {
element.take().unwrap().paint(cx);
}
}
impl<V> Clone for View<V> { impl<V> Clone for View<V> {
fn clone(&self) -> Self { fn clone(&self) -> Self {
Self { Self {
@ -105,12 +117,6 @@ impl<V> PartialEq for View<V> {
impl<V> Eq for View<V> {} impl<V> Eq for View<V> {}
impl<V: Render, ParentViewState: 'static> Component<ParentViewState> for View<V> {
fn render(self) -> AnyElement<ParentViewState> {
AnyElement::new(AnyView::from(self))
}
}
pub struct WeakView<V> { pub struct WeakView<V> {
pub(crate) model: WeakModel<V>, pub(crate) model: WeakModel<V>,
} }
@ -163,8 +169,8 @@ impl<V> Eq for WeakView<V> {}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct AnyView { pub struct AnyView {
model: AnyModel, model: AnyModel,
layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, Box<dyn Any>), layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, AnyElement),
paint: fn(&AnyView, &mut AnyBox, &mut WindowContext), paint: fn(&AnyView, AnyElement, &mut WindowContext),
} }
impl AnyView { impl AnyView {
@ -191,6 +197,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>,
@ -198,21 +208,15 @@ impl AnyView {
cx: &mut WindowContext, cx: &mut WindowContext,
) { ) {
cx.with_absolute_element_offset(origin, |cx| { cx.with_absolute_element_offset(origin, |cx| {
let (layout_id, mut rendered_element) = (self.layout)(self, cx); let (layout_id, rendered_element) = (self.layout)(self, cx);
cx.window cx.window
.layout_engine .layout_engine
.compute_layout(layout_id, available_space); .compute_layout(layout_id, available_space);
(self.paint)(self, &mut rendered_element, cx); (self.paint)(self, rendered_element, cx);
}) })
} }
} }
impl<V: 'static> Component<V> for AnyView {
fn render(self) -> AnyElement<V> {
AnyElement::new(self)
}
}
impl<V: Render> From<View<V>> for AnyView { impl<V: Render> From<View<V>> for AnyView {
fn from(value: View<V>) -> Self { fn from(value: View<V>) -> Self {
AnyView { AnyView {
@ -223,37 +227,51 @@ impl<V: Render> From<View<V>> for AnyView {
} }
} }
impl<ParentViewState: 'static> Element<ParentViewState> for AnyView { impl Element for AnyView {
type ElementState = Box<dyn Any>; type State = Option<AnyElement>;
fn layout(
&mut self,
_state: Option<Self::State>,
cx: &mut WindowContext,
) -> (LayoutId, Self::State) {
let (layout_id, state) = (self.layout)(self, cx);
(layout_id, Some(state))
}
fn paint(self, _: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
(self.paint)(&self, state.take().unwrap(), cx)
}
}
impl<V: 'static + Render> RenderOnce for View<V> {
type Element = View<V>;
fn element_id(&self) -> Option<ElementId> { fn element_id(&self) -> Option<ElementId> {
Some(self.model.entity_id.into()) Some(self.model.entity_id.into())
} }
fn layout( fn render_once(self) -> Self::Element {
&mut self, self
_view_state: &mut ParentViewState, }
_element_state: Option<Self::ElementState>, }
cx: &mut ViewContext<ParentViewState>,
) -> (LayoutId, Self::ElementState) { impl RenderOnce for AnyView {
(self.layout)(self, cx) type Element = Self;
fn element_id(&self) -> Option<ElementId> {
Some(self.model.entity_id.into())
} }
fn paint( fn render_once(self) -> Self::Element {
&mut self, self
_bounds: Bounds<Pixels>,
_view_state: &mut ParentViewState,
rendered_element: &mut Self::ElementState,
cx: &mut ViewContext<ParentViewState>,
) {
(self.paint)(self, rendered_element, cx)
} }
} }
pub struct AnyWeakView { pub struct AnyWeakView {
model: AnyWeakModel, model: AnyWeakModel,
layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, Box<dyn Any>), layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, AnyElement),
paint: fn(&AnyView, &mut AnyBox, &mut WindowContext), paint: fn(&AnyView, AnyElement, &mut WindowContext),
} }
impl AnyWeakView { impl AnyWeakView {
@ -267,7 +285,7 @@ impl AnyWeakView {
} }
} }
impl<V: Render> From<WeakView<V>> for AnyWeakView { impl<V: 'static + Render> From<WeakView<V>> for AnyWeakView {
fn from(view: WeakView<V>) -> Self { fn from(view: WeakView<V>) -> Self {
Self { Self {
model: view.model.into(), model: view.model.into(),
@ -280,7 +298,7 @@ impl<V: Render> From<WeakView<V>> for AnyWeakView {
impl<T, E> Render for T impl<T, E> Render for T
where where
T: 'static + FnMut(&mut WindowContext) -> E, T: 'static + FnMut(&mut WindowContext) -> E,
E: 'static + Send + Element<T>, E: 'static + Send + Element,
{ {
type Element = E; type Element = E;
@ -289,85 +307,28 @@ where
} }
} }
pub struct RenderViewWith<C, V> { mod any_view {
view: View<V>, use crate::{AnyElement, AnyView, BorrowWindow, Element, LayoutId, Render, WindowContext};
component: Option<C>,
}
impl<C, ParentViewState, ViewState> Component<ParentViewState> for RenderViewWith<C, ViewState> pub(crate) fn layout<V: 'static + Render>(
where view: &AnyView,
C: 'static + Component<ViewState>, cx: &mut WindowContext,
ParentViewState: 'static, ) -> (LayoutId, AnyElement) {
ViewState: 'static, cx.with_element_id(Some(view.model.entity_id), |cx| {
{ let view = view.clone().downcast::<V>().unwrap();
fn render(self) -> AnyElement<ParentViewState> { let mut element = view.update(cx, |view, cx| view.render(cx).into_any());
AnyElement::new(self) let layout_id = element.layout(cx);
}
}
impl<C, ParentViewState, ViewState> Element<ParentViewState> for RenderViewWith<C, ViewState>
where
C: 'static + Component<ViewState>,
ParentViewState: 'static,
ViewState: 'static,
{
type ElementState = AnyElement<ViewState>;
fn element_id(&self) -> Option<ElementId> {
Some(self.view.entity_id().into())
}
fn layout(
&mut self,
_: &mut ParentViewState,
_: Option<Self::ElementState>,
cx: &mut ViewContext<ParentViewState>,
) -> (LayoutId, Self::ElementState) {
self.view.update(cx, |view, cx| {
let mut element = self.component.take().unwrap().render();
let layout_id = element.layout(view, cx);
(layout_id, element) (layout_id, element)
}) })
} }
fn paint( pub(crate) fn paint<V: 'static + Render>(
&mut self,
_: Bounds<Pixels>,
_: &mut ParentViewState,
element: &mut Self::ElementState,
cx: &mut ViewContext<ParentViewState>,
) {
self.view.update(cx, |view, cx| element.paint(view, cx))
}
}
mod any_view {
use crate::{AnyElement, AnyView, BorrowWindow, LayoutId, Render, WindowContext};
use std::any::Any;
pub(crate) fn layout<V: Render>(
view: &AnyView, view: &AnyView,
cx: &mut WindowContext, element: AnyElement,
) -> (LayoutId, Box<dyn Any>) {
cx.with_element_id(Some(view.model.entity_id), |cx| {
let view = view.clone().downcast::<V>().unwrap();
view.update(cx, |view, cx| {
let mut element = AnyElement::new(view.render(cx));
let layout_id = element.layout(view, cx);
(layout_id, Box::new(element) as Box<dyn Any>)
})
})
}
pub(crate) fn paint<V: Render>(
view: &AnyView,
element: &mut Box<dyn Any>,
cx: &mut WindowContext, cx: &mut WindowContext,
) { ) {
cx.with_element_id(Some(view.model.entity_id), |cx| { cx.with_element_id(Some(view.model.entity_id), |cx| {
let view = view.clone().downcast::<V>().unwrap(); element.paint(cx);
let element = element.downcast_mut::<AnyElement<V>>().unwrap();
view.update(cx, |view, cx| element.paint(view, cx))
}) })
} }
} }

View file

@ -1,15 +1,15 @@
use crate::{ use crate::{
key_dispatch::DispatchActionListener, px, size, Action, AnyBox, AnyDrag, AnyView, AppContext, key_dispatch::DispatchActionListener, px, size, Action, AnyDrag, AnyView, AppContext,
AsyncWindowContext, AvailableSpace, Bounds, BoxShadow, Context, Corners, CursorStyle, AsyncWindowContext, AvailableSpace, Bounds, BoxShadow, Context, Corners, CursorStyle,
DevicePixels, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, DevicePixels, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId,
EventEmitter, FileDropEvent, FocusEvent, FontId, GlobalElementId, GlyphId, Hsla, ImageData, EventEmitter, FileDropEvent, Flatten, FocusEvent, FontId, GlobalElementId, GlyphId, Hsla,
InputEvent, IsZero, KeyBinding, KeyContext, KeyDownEvent, LayoutId, Model, ModelContext, ImageData, InputEvent, IsZero, KeyBinding, KeyContext, KeyDownEvent, LayoutId, Model,
Modifiers, MonochromeSprite, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Path, ModelContext, Modifiers, MonochromeSprite, MouseButton, MouseDownEvent, MouseMoveEvent,
Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point, MouseUpEvent, Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler,
PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams, PlatformWindow, Point, PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams,
RenderSvgParams, ScaledPixels, SceneBuilder, Shadow, SharedString, Size, Style, SubscriberSet, RenderImageParams, RenderSvgParams, ScaledPixels, SceneBuilder, Shadow, SharedString, Size,
Subscription, TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, VisualContext, Style, SubscriberSet, Subscription, TaffyLayoutEngine, Task, Underline, UnderlineStyle, View,
WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS, VisualContext, WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS,
}; };
use anyhow::{anyhow, Context as _, Result}; use anyhow::{anyhow, Context as _, Result};
use collections::HashMap; use collections::HashMap;
@ -187,23 +187,18 @@ impl Drop for FocusHandle {
/// FocusableView allows users of your view to easily /// FocusableView allows users of your view to easily
/// focus it (using cx.focus_view(view)) /// focus it (using cx.focus_view(view))
pub trait FocusableView: Render { pub trait FocusableView: 'static + Render {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle; fn focus_handle(&self, cx: &AppContext) -> FocusHandle;
} }
/// 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: Render { 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.
@ -237,7 +232,7 @@ pub struct Window {
// #[derive(Default)] // #[derive(Default)]
pub(crate) struct Frame { pub(crate) struct Frame {
pub(crate) element_states: HashMap<GlobalElementId, AnyBox>, pub(crate) element_states: HashMap<GlobalElementId, Box<dyn Any>>,
mouse_listeners: HashMap<TypeId, Vec<(StackingOrder, AnyMouseListener)>>, mouse_listeners: HashMap<TypeId, Vec<(StackingOrder, AnyMouseListener)>>,
pub(crate) dispatch_tree: DispatchTree, pub(crate) dispatch_tree: DispatchTree,
pub(crate) focus_listeners: Vec<AnyFocusListener>, pub(crate) focus_listeners: Vec<AnyFocusListener>,
@ -1441,6 +1436,82 @@ impl<'a> WindowContext<'a> {
.dispatch_tree .dispatch_tree
.bindings_for_action(action) .bindings_for_action(action)
} }
pub fn listener_for<V: Render, E>(
&self,
view: &View<V>,
f: impl Fn(&mut V, &E, &mut ViewContext<V>) + 'static,
) -> impl Fn(&E, &mut WindowContext) + 'static {
let view = view.downgrade();
move |e: &E, cx: &mut WindowContext| {
view.update(cx, |view, cx| f(view, e, cx)).ok();
}
}
pub fn constructor_for<V: Render, R>(
&self,
view: &View<V>,
f: impl Fn(&mut V, &mut ViewContext<V>) -> R + 'static,
) -> impl Fn(&mut WindowContext) -> R + 'static {
let view = view.clone();
move |cx: &mut WindowContext| view.update(cx, |view, cx| f(view, cx))
}
//========== ELEMENT RELATED FUNCTIONS ===========
pub fn with_key_dispatch<R>(
&mut self,
context: KeyContext,
focus_handle: Option<FocusHandle>,
f: impl FnOnce(Option<FocusHandle>, &mut Self) -> R,
) -> R {
let window = &mut self.window;
window
.current_frame
.dispatch_tree
.push_node(context.clone());
if let Some(focus_handle) = focus_handle.as_ref() {
window
.current_frame
.dispatch_tree
.make_focusable(focus_handle.id);
}
let result = f(focus_handle, self);
self.window.current_frame.dispatch_tree.pop_node();
result
}
/// Register a focus listener for the current frame only. It will be cleared
/// on the next frame render. You should use this method only from within elements,
/// and we may want to enforce that better via a different context type.
// todo!() Move this to `FrameContext` to emphasize its individuality?
pub fn on_focus_changed(
&mut self,
listener: impl Fn(&FocusEvent, &mut WindowContext) + 'static,
) {
self.window
.current_frame
.focus_listeners
.push(Box::new(move |event, cx| {
listener(event, cx);
}));
}
/// Set an input handler, such as [ElementInputHandler], which interfaces with the
/// platform to receive textual input with proper integration with concerns such
/// as IME interactions.
pub fn handle_input(
&mut self,
focus_handle: &FocusHandle,
input_handler: impl PlatformInputHandler,
) {
if focus_handle.is_focused(self) {
self.window
.platform_window
.set_input_handler(Box::new(input_handler));
}
}
} }
impl Context for WindowContext<'_> { impl Context for WindowContext<'_> {
@ -1564,7 +1635,7 @@ impl VisualContext for WindowContext<'_> {
build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V, build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V,
) -> Self::Result<View<V>> ) -> Self::Result<View<V>>
where where
V: Render, V: 'static + Render,
{ {
let slot = self.app.entities.reserve(); let slot = self.app.entities.reserve();
let view = View { let view = View {
@ -1582,6 +1653,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> {
@ -1615,6 +1693,10 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
self.borrow_mut() self.borrow_mut()
} }
fn app(&self) -> &AppContext {
self.borrow()
}
fn window(&self) -> &Window { fn window(&self) -> &Window {
self.borrow() self.borrow()
} }
@ -2122,49 +2204,6 @@ impl<'a, V: 'static> ViewContext<'a, V> {
) )
} }
/// Register a focus listener for the current frame only. It will be cleared
/// on the next frame render. You should use this method only from within elements,
/// and we may want to enforce that better via a different context type.
// todo!() Move this to `FrameContext` to emphasize its individuality?
pub fn on_focus_changed(
&mut self,
listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + 'static,
) {
let handle = self.view().downgrade();
self.window
.current_frame
.focus_listeners
.push(Box::new(move |event, cx| {
handle
.update(cx, |view, cx| listener(view, event, cx))
.log_err();
}));
}
pub fn with_key_dispatch<R>(
&mut self,
context: KeyContext,
focus_handle: Option<FocusHandle>,
f: impl FnOnce(Option<FocusHandle>, &mut Self) -> R,
) -> R {
let window = &mut self.window;
window
.current_frame
.dispatch_tree
.push_node(context.clone());
if let Some(focus_handle) = focus_handle.as_ref() {
window
.current_frame
.dispatch_tree
.make_focusable(focus_handle.id);
}
let result = f(focus_handle, self);
self.window.current_frame.dispatch_tree.pop_node();
result
}
pub fn spawn<Fut, R>( pub fn spawn<Fut, R>(
&mut self, &mut self,
f: impl FnOnce(WeakView<V>, AsyncWindowContext) -> Fut, f: impl FnOnce(WeakView<V>, AsyncWindowContext) -> Fut,
@ -2241,21 +2280,6 @@ impl<'a, V: 'static> ViewContext<'a, V> {
}); });
} }
/// Set an input handler, such as [ElementInputHandler], which interfaces with the
/// platform to receive textual input with proper integration with concerns such
/// as IME interactions.
pub fn handle_input(
&mut self,
focus_handle: &FocusHandle,
input_handler: impl PlatformInputHandler,
) {
if focus_handle.is_focused(self) {
self.window
.platform_window
.set_input_handler(Box::new(input_handler));
}
}
pub fn emit<Evt>(&mut self, event: Evt) pub fn emit<Evt>(&mut self, event: Evt)
where where
Evt: 'static, Evt: 'static,
@ -2275,6 +2299,23 @@ 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))
}
pub fn listener<E>(
&self,
f: impl Fn(&mut V, &E, &mut ViewContext<V>) + 'static,
) -> impl Fn(&E, &mut WindowContext) + 'static {
let view = self.view().downgrade();
move |e: &E, cx: &mut WindowContext| {
view.update(cx, |view, cx| f(view, e, cx)).ok();
}
}
} }
impl<V> Context for ViewContext<'_, V> { impl<V> Context for ViewContext<'_, V> {
@ -2346,7 +2387,7 @@ impl<V: 'static> VisualContext for ViewContext<'_, V> {
build_view: impl FnOnce(&mut ViewContext<'_, W>) -> W, build_view: impl FnOnce(&mut ViewContext<'_, W>) -> W,
) -> Self::Result<View<W>> ) -> Self::Result<View<W>>
where where
W: Render, W: 'static + Render,
{ {
self.window_cx.replace_root_view(build_view) self.window_cx.replace_root_view(build_view)
} }
@ -2354,6 +2395,10 @@ impl<V: 'static> VisualContext for ViewContext<'_, V> {
fn focus_view<W: FocusableView>(&mut self, view: &View<W>) -> Self::Result<()> { 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 +2443,17 @@ impl<V: 'static + Render> WindowHandle<V> {
} }
} }
pub fn root<C>(&self, cx: &mut C) -> Result<View<V>>
where
C: Context,
{
Flatten::flatten(cx.update_window(self.any_handle, |root_view, _| {
root_view
.downcast::<V>()
.map_err(|_| anyhow!("the type of the window's root view has changed"))
}))
}
pub fn update<C, R>( pub fn update<C, R>(
&self, &self,
cx: &mut C, cx: &mut C,
@ -2543,6 +2599,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

@ -0,0 +1,27 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
pub fn derive_render_once(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
let type_name = &ast.ident;
let (impl_generics, type_generics, where_clause) = ast.generics.split_for_impl();
let gen = quote! {
impl #impl_generics gpui::RenderOnce for #type_name #type_generics
#where_clause
{
type Element = gpui::CompositeElement<Self>;
fn element_id(&self) -> Option<ElementId> {
None
}
fn render_once(self) -> Self::Element {
gpui::CompositeElement::new(self)
}
}
};
gen.into()
}

View file

@ -1,16 +1,12 @@
mod action; mod action;
mod derive_component; mod derive_component;
mod derive_render_once;
mod register_action; mod register_action;
mod style_helpers; mod style_helpers;
mod test; mod test;
use proc_macro::TokenStream; use proc_macro::TokenStream;
#[proc_macro]
pub fn style_helpers(args: TokenStream) -> TokenStream {
style_helpers::style_helpers(args)
}
#[proc_macro_derive(Action)] #[proc_macro_derive(Action)]
pub fn action(input: TokenStream) -> TokenStream { pub fn action(input: TokenStream) -> TokenStream {
action::action(input) action::action(input)
@ -26,6 +22,16 @@ pub fn derive_component(input: TokenStream) -> TokenStream {
derive_component::derive_component(input) derive_component::derive_component(input)
} }
#[proc_macro_derive(RenderOnce, attributes(view))]
pub fn derive_render_once(input: TokenStream) -> TokenStream {
derive_render_once::derive_render_once(input)
}
#[proc_macro]
pub fn style_helpers(input: TokenStream) -> TokenStream {
style_helpers::style_helpers(input)
}
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn test(args: TokenStream, function: TokenStream) -> TokenStream { pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
test::test(args, function) test::test(args, function)

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

@ -61,12 +61,14 @@ fn build_bridge(swift_target: &SwiftTarget) {
let swift_package_root = swift_package_root(); let swift_package_root = swift_package_root();
let swift_target_folder = swift_target_folder(); let swift_target_folder = swift_target_folder();
let swift_cache_folder = swift_cache_folder();
if !Command::new("swift") if !Command::new("swift")
.arg("build") .arg("build")
.arg("--disable-automatic-resolution") .arg("--disable-automatic-resolution")
.args(["--configuration", &env::var("PROFILE").unwrap()]) .args(["--configuration", &env::var("PROFILE").unwrap()])
.args(["--triple", &swift_target.target.triple]) .args(["--triple", &swift_target.target.triple])
.args(["--build-path".into(), swift_target_folder]) .args(["--build-path".into(), swift_target_folder])
.args(["--cache-path".into(), swift_cache_folder])
.current_dir(&swift_package_root) .current_dir(&swift_package_root)
.status() .status()
.unwrap() .unwrap()
@ -133,9 +135,17 @@ fn swift_package_root() -> PathBuf {
} }
fn swift_target_folder() -> PathBuf { fn swift_target_folder() -> PathBuf {
let target = env::var("TARGET").unwrap();
env::current_dir() env::current_dir()
.unwrap() .unwrap()
.join(format!("../../target/{SWIFT_PACKAGE_NAME}")) .join(format!("../../target/{target}/{SWIFT_PACKAGE_NAME}_target"))
}
fn swift_cache_folder() -> PathBuf {
let target = env::var("TARGET").unwrap();
env::current_dir()
.unwrap()
.join(format!("../../target/{target}/{SWIFT_PACKAGE_NAME}_cache"))
} }
fn copy_dir(source: &Path, destination: &Path) { fn copy_dir(source: &Path, destination: &Path) {

View file

@ -1,10 +1,10 @@
use editor::Editor; use editor::Editor;
use gpui::{ use gpui::{
div, prelude::*, uniform_list, AppContext, Component, Div, FocusHandle, FocusableView, div, prelude::*, uniform_list, AppContext, Div, FocusHandle, FocusableView, MouseButton,
MouseButton, Render, Task, UniformListScrollHandle, View, ViewContext, WindowContext, MouseDownEvent, Render, Task, UniformListScrollHandle, View, ViewContext, WindowContext,
}; };
use std::{cmp, sync::Arc}; use std::{cmp, sync::Arc};
use ui::{prelude::*, v_stack, Divider, Label, TextColor}; use ui::{prelude::*, v_stack, Color, Divider, Label};
pub struct Picker<D: PickerDelegate> { pub struct Picker<D: PickerDelegate> {
pub delegate: D, pub delegate: D,
@ -15,7 +15,7 @@ pub struct Picker<D: PickerDelegate> {
} }
pub trait PickerDelegate: Sized + 'static { pub trait PickerDelegate: Sized + 'static {
type ListItem: Component<Picker<Self>>; type ListItem: RenderOnce;
fn match_count(&self) -> usize; fn match_count(&self) -> usize;
fn selected_index(&self) -> usize; fn selected_index(&self) -> usize;
@ -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);
} }
@ -181,20 +181,20 @@ impl<D: PickerDelegate> Picker<D> {
} }
impl<D: PickerDelegate> Render for Picker<D> { impl<D: PickerDelegate> Render for Picker<D> {
type Element = Div<Self>; type Element = Div;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element { fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
div() div()
.key_context("picker") .key_context("picker")
.size_full() .size_full()
.elevation_2(cx) .elevation_2(cx)
.on_action(Self::select_next) .on_action(cx.listener(Self::select_next))
.on_action(Self::select_prev) .on_action(cx.listener(Self::select_prev))
.on_action(Self::select_first) .on_action(cx.listener(Self::select_first))
.on_action(Self::select_last) .on_action(cx.listener(Self::select_last))
.on_action(Self::cancel) .on_action(cx.listener(Self::cancel))
.on_action(Self::confirm) .on_action(cx.listener(Self::confirm))
.on_action(Self::secondary_confirm) .on_action(cx.listener(Self::secondary_confirm))
.child( .child(
v_stack() v_stack()
.py_0p5() .py_0p5()
@ -208,31 +208,37 @@ impl<D: PickerDelegate> Render for Picker<D> {
.p_1() .p_1()
.grow() .grow()
.child( .child(
uniform_list("candidates", self.delegate.match_count(), { uniform_list(
move |this: &mut Self, visible_range, cx| { cx.view().clone(),
let selected_ix = this.delegate.selected_index(); "candidates",
visible_range self.delegate.match_count(),
.map(|ix| { {
div() let selected_index = self.delegate.selected_index();
.on_mouse_down(
MouseButton::Left, move |picker, visible_range, cx| {
move |this: &mut Self, event, cx| { visible_range
this.handle_click( .map(|ix| {
ix, div()
event.modifiers.command, .on_mouse_down(
cx, MouseButton::Left,
) cx.listener(move |this, event: &MouseDownEvent, cx| {
}, this.handle_click(
) ix,
.child(this.delegate.render_match( event.modifiers.command,
ix, cx,
ix == selected_ix, )
cx, }),
)) )
}) .child(picker.delegate.render_match(
.collect() ix,
} ix == selected_index,
}) cx,
))
})
.collect()
}
},
)
.track_scroll(self.scroll_handle.clone()), .track_scroll(self.scroll_handle.clone()),
) )
.max_h_72() .max_h_72()
@ -244,7 +250,7 @@ impl<D: PickerDelegate> Render for Picker<D> {
v_stack().p_1().grow().child( v_stack().p_1().grow().child(
div() div()
.px_1() .px_1()
.child(Label::new("No matches").color(TextColor::Muted)), .child(Label::new("No matches").color(Color::Muted)),
), ),
) )
}) })

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

@ -2633,6 +2633,60 @@ async fn test_file_changes_multiple_times_on_disk(cx: &mut gpui::TestAppContext)
.unwrap(); .unwrap();
worktree.next_event(cx); worktree.next_event(cx);
cx.executor().run_until_parked();
let on_disk_text = fs.load(Path::new("/dir/file1")).await.unwrap();
buffer.read_with(cx, |buffer, _| {
assert_eq!(buffer.text(), on_disk_text);
assert!(!buffer.is_dirty(), "buffer should not be dirty");
assert!(!buffer.has_conflict(), "buffer should not be dirty");
});
}
#[gpui::test(iterations = 30)]
async fn test_edit_buffer_while_it_reloads(cx: &mut gpui::TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor().clone());
fs.insert_tree(
"/dir",
json!({
"file1": "the original contents",
}),
)
.await;
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
let worktree = project.read_with(cx, |project, _| project.worktrees().next().unwrap());
let buffer = project
.update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
.await
.unwrap();
// Simulate buffer diffs being slow, so that they don't complete before
// the next file change occurs.
cx.executor().deprioritize(*language::BUFFER_DIFF_TASK);
// Change the buffer's file on disk, and then wait for the file change
// to be detected by the worktree, so that the buffer starts reloading.
fs.save(
"/dir/file1".as_ref(),
&"the first contents".into(),
Default::default(),
)
.await
.unwrap();
worktree.next_event(cx);
cx.executor()
.spawn(cx.executor().simulate_random_delay())
.await;
// Perform a noop edit, causing the buffer's version to increase.
buffer.update(cx, |buffer, cx| {
buffer.edit([(0..0, " ")], None, cx);
buffer.undo(cx);
});
cx.executor().run_until_parked(); cx.executor().run_until_parked();
let on_disk_text = fs.load(Path::new("/dir/file1")).await.unwrap(); let on_disk_text = fs.load(Path::new("/dir/file1")).await.unwrap();
buffer.read_with(cx, |buffer, _| { buffer.read_with(cx, |buffer, _| {
@ -2646,10 +2700,8 @@ async fn test_file_changes_multiple_times_on_disk(cx: &mut gpui::TestAppContext)
// If the file change occurred while the buffer was processing the first // If the file change occurred while the buffer was processing the first
// change, the buffer will be in a conflicting state. // change, the buffer will be in a conflicting state.
else { else {
assert!( assert!(buffer.is_dirty(), "buffer should report that it is dirty. text: {buffer_text:?}, disk text: {on_disk_text:?}");
buffer.is_dirty() && buffer.has_conflict(), assert!(buffer.has_conflict(), "buffer should report that it is dirty. text: {buffer_text:?}, disk text: {on_disk_text:?}");
"buffer should report that it has a conflict. text: {buffer_text:?}, disk text: {on_disk_text:?}"
);
} }
}); });
} }
@ -3678,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
@ -3703,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
@ -3742,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()
) )
@ -3761,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()
) )
@ -3783,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(),
@ -3807,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(),
@ -3854,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()],
) )
@ -3878,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()],
) )
@ -3900,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(),
@ -3924,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(),
@ -3965,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()],
) )
@ -3984,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(),
@ -4002,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()
@ -4027,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()
@ -4084,7 +4148,7 @@ async fn search(
fn init_test(cx: &mut gpui::TestAppContext) { fn init_test(cx: &mut gpui::TestAppContext) {
if std::env::var("RUST_LOG").is_ok() { if std::env::var("RUST_LOG").is_ok() {
env_logger::init(); env_logger::try_init().ok();
} }
cx.update(|cx| { cx.update(|cx| {

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

@ -9,7 +9,6 @@ path = "src/project_panel.rs"
doctest = false doctest = false
[dependencies] [dependencies]
context_menu = { path = "../context_menu" }
collections = { path = "../collections" } collections = { path = "../collections" }
db = { path = "../db2", package = "db2" } db = { path = "../db2", package = "db2" }
editor = { path = "../editor2", package = "editor2" } editor = { path = "../editor2", package = "editor2" }

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};
@ -9,9 +9,9 @@ use file_associations::FileAssociations;
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use gpui::{ use gpui::{
actions, div, px, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext, actions, div, px, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext,
ClipboardItem, Component, Div, EventEmitter, FocusHandle, Focusable, FocusableView, ClipboardItem, Div, EventEmitter, FocusHandle, Focusable, FocusableView, InteractiveElement,
InteractiveComponent, Model, MouseButton, ParentComponent, Pixels, Point, PromptLevel, Render, Model, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render,
Stateful, StatefulInteractiveComponent, Styled, Task, UniformListScrollHandle, View, RenderOnce, Stateful, StatefulInteractiveElement, Styled, Task, UniformListScrollHandle, View,
ViewContext, VisualContext as _, WeakView, WindowContext, ViewContext, VisualContext as _, WeakView, WindowContext,
}; };
use menu::{Confirm, SelectNext, SelectPrev}; use menu::{Confirm, SelectNext, SelectPrev};
@ -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,16 @@ 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); let new_dock_position = this.position(cx);
// if new_dock_position != old_dock_position { if new_dock_position != old_dock_position {
// old_dock_position = new_dock_position; old_dock_position = new_dock_position;
// cx.emit(Event::DockPositionChanged); cx.emit(PanelEvent::ChangePosition);
// } }
// }) })
// .detach(); .detach();
this this
}); });
@ -1339,7 +1339,7 @@ impl ProjectPanel {
editor: Option<&View<Editor>>, editor: Option<&View<Editor>>,
padding: Pixels, padding: Pixels,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Div<Self> { ) -> Div {
let show_editor = details.is_editing && !details.is_processing; let show_editor = details.is_editing && !details.is_processing;
let theme = cx.theme(); let theme = cx.theme();
@ -1378,7 +1378,7 @@ impl ProjectPanel {
details: EntryDetails, details: EntryDetails,
// dragged_entry_destination: &mut Option<Arc<Path>>, // dragged_entry_destination: &mut Option<Arc<Path>>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Stateful<Self, Div<Self>> { ) -> Stateful<Div> {
let kind = details.kind; let kind = details.kind;
let settings = ProjectPanelSettings::get_global(cx); let settings = ProjectPanelSettings::get_global(cx);
const INDENT_SIZE: Pixels = px(16.0); const INDENT_SIZE: Pixels = px(16.0);
@ -1396,7 +1396,7 @@ impl ProjectPanel {
this.bg(cx.theme().colors().element_selected) this.bg(cx.theme().colors().element_selected)
}) })
.hover(|style| style.bg(cx.theme().colors().element_hover)) .hover(|style| style.bg(cx.theme().colors().element_hover))
.on_click(move |this, event, cx| { .on_click(cx.listener(move |this, event: &gpui::ClickEvent, cx| {
if !show_editor { if !show_editor {
if kind.is_dir() { if kind.is_dir() {
this.toggle_expanded(entry_id, cx); this.toggle_expanded(entry_id, cx);
@ -1408,10 +1408,13 @@ impl ProjectPanel {
} }
} }
} }
}) }))
.on_mouse_down(MouseButton::Right, move |this, event, cx| { .on_mouse_down(
this.deploy_context_menu(event.position, entry_id, cx); MouseButton::Right,
}) cx.listener(move |this, event: &MouseDownEvent, cx| {
this.deploy_context_menu(event.position, entry_id, cx);
}),
)
// .on_drop::<ProjectEntryId>(|this, event, cx| { // .on_drop::<ProjectEntryId>(|this, event, cx| {
// this.move_entry( // this.move_entry(
// *dragged_entry, // *dragged_entry,
@ -1424,9 +1427,9 @@ impl ProjectPanel {
} }
impl Render for ProjectPanel { impl Render for ProjectPanel {
type Element = Focusable<Self, Stateful<Self, Div<Self>>>; type Element = Focusable<Stateful<Div>>;
fn render(&mut self, _cx: &mut gpui::ViewContext<Self>) -> Self::Element { fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element {
let has_worktree = self.visible_entries.len() != 0; let has_worktree = self.visible_entries.len() != 0;
if has_worktree { if has_worktree {
@ -1434,40 +1437,43 @@ impl Render for ProjectPanel {
.id("project-panel") .id("project-panel")
.size_full() .size_full()
.key_context("ProjectPanel") .key_context("ProjectPanel")
.on_action(Self::select_next) .on_action(cx.listener(Self::select_next))
.on_action(Self::select_prev) .on_action(cx.listener(Self::select_prev))
.on_action(Self::expand_selected_entry) .on_action(cx.listener(Self::expand_selected_entry))
.on_action(Self::collapse_selected_entry) .on_action(cx.listener(Self::collapse_selected_entry))
.on_action(Self::collapse_all_entries) .on_action(cx.listener(Self::collapse_all_entries))
.on_action(Self::new_file) .on_action(cx.listener(Self::new_file))
.on_action(Self::new_directory) .on_action(cx.listener(Self::new_directory))
.on_action(Self::rename) .on_action(cx.listener(Self::rename))
.on_action(Self::delete) .on_action(cx.listener(Self::delete))
.on_action(Self::confirm) .on_action(cx.listener(Self::confirm))
.on_action(Self::open_file) .on_action(cx.listener(Self::open_file))
.on_action(Self::cancel) .on_action(cx.listener(Self::cancel))
.on_action(Self::cut) .on_action(cx.listener(Self::cut))
.on_action(Self::copy) .on_action(cx.listener(Self::copy))
.on_action(Self::copy_path) .on_action(cx.listener(Self::copy_path))
.on_action(Self::copy_relative_path) .on_action(cx.listener(Self::copy_relative_path))
.on_action(Self::paste) .on_action(cx.listener(Self::paste))
.on_action(Self::reveal_in_finder) .on_action(cx.listener(Self::reveal_in_finder))
.on_action(Self::open_in_terminal) .on_action(cx.listener(Self::open_in_terminal))
.on_action(Self::new_search_in_directory) .on_action(cx.listener(Self::new_search_in_directory))
.track_focus(&self.focus_handle) .track_focus(&self.focus_handle)
.child( .child(
uniform_list( uniform_list(
cx.view().clone(),
"entries", "entries",
self.visible_entries self.visible_entries
.iter() .iter()
.map(|(_, worktree_entries)| worktree_entries.len()) .map(|(_, worktree_entries)| worktree_entries.len())
.sum(), .sum(),
|this: &mut Self, range, cx| { {
let mut items = Vec::new(); |this, range, cx| {
this.for_each_visible_entry(range, cx, |id, details, cx| { let mut items = Vec::new();
items.push(this.render_entry(id, details, cx)); this.for_each_visible_entry(range, cx, |id, details, cx| {
}); items.push(this.render_entry(id, details, cx));
items });
items
}
}, },
) )
.size_full() .size_full()
@ -1485,7 +1491,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 +1577,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 +1678,124 @@ mod tests {
); );
} }
#[gpui::test]
async fn test_exclusions_in_visible_list(cx: &mut gpui::TestAppContext) {
init_test(cx);
cx.update(|cx| {
cx.update_global::<SettingsStore, _>(|store, cx| {
store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
project_settings.file_scan_exclusions =
Some(vec!["**/.git".to_string(), "**/4/**".to_string()]);
});
});
});
let fs = FakeFs::new(cx.background_executor.clone());
fs.insert_tree(
"/root1",
json!({
".dockerignore": "",
".git": {
"HEAD": "",
},
"a": {
"0": { "q": "", "r": "", "s": "" },
"1": { "t": "", "u": "" },
"2": { "v": "", "w": "", "x": "", "y": "" },
},
"b": {
"3": { "Q": "" },
"4": { "R": "", "S": "", "T": "", "U": "" },
},
"C": {
"5": {},
"6": { "V": "", "W": "" },
"7": { "X": "" },
"8": { "Y": {}, "Z": "" }
}
}),
)
.await;
fs.insert_tree(
"/root2",
json!({
"d": {
"4": ""
},
"e": {}
}),
)
.await;
let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let panel = workspace
.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx))
.unwrap();
assert_eq!(
visible_entries_as_strings(&panel, 0..50, cx),
&[
"v root1",
" > a",
" > b",
" > C",
" .dockerignore",
"v root2",
" > d",
" > e",
]
);
toggle_expand_dir(&panel, "root1/b", cx);
assert_eq!(
visible_entries_as_strings(&panel, 0..50, cx),
&[
"v root1",
" > a",
" v b <== selected",
" > 3",
" > C",
" .dockerignore",
"v root2",
" > d",
" > e",
]
);
toggle_expand_dir(&panel, "root2/d", cx);
assert_eq!(
visible_entries_as_strings(&panel, 0..50, cx),
&[
"v root1",
" > a",
" v b",
" > 3",
" > C",
" .dockerignore",
"v root2",
" v d <== selected",
" > e",
]
);
toggle_expand_dir(&panel, "root2/e", cx);
assert_eq!(
visible_entries_as_strings(&panel, 0..50, cx),
&[
"v root1",
" > a",
" v b",
" > 3",
" > C",
" .dockerignore",
"v root2",
" v d",
" v e <== selected",
]
);
}
#[gpui::test(iterations = 30)] #[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 +2916,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

@ -56,12 +56,12 @@ pub struct Mention {
} }
impl RichText { impl RichText {
pub fn element<V: 'static>( pub fn element(
&self, &self,
// syntax: Arc<SyntaxTheme>, // syntax: Arc<SyntaxTheme>,
// style: RichTextStyle, // style: RichTextStyle,
// cx: &mut ViewContext<V>, // cx: &mut ViewContext<V>,
) -> AnyElement<V> { ) -> AnyElement {
todo!(); todo!();
// let mut region_id = 0; // let mut region_id = 0;

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

40
crates/search2/Cargo.toml Normal file
View file

@ -0,0 +1,40 @@
[package]
name = "search2"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/search.rs"
doctest = false
[dependencies]
bitflags = "1"
collections = { path = "../collections" }
editor = { package = "editor2", path = "../editor2" }
gpui = { package = "gpui2", path = "../gpui2" }
language = { package = "language2", path = "../language2" }
menu = { package = "menu2", path = "../menu2" }
project = { package = "project2", path = "../project2" }
settings = { package = "settings2", path = "../settings2" }
theme = { package = "theme2", path = "../theme2" }
util = { path = "../util" }
ui = {package = "ui2", path = "../ui2"}
workspace = { package = "workspace2", path = "../workspace2" }
#semantic_index = { path = "../semantic_index" }
anyhow.workspace = true
futures.workspace = true
log.workspace = true
postage.workspace = true
serde.workspace = true
serde_derive.workspace = true
smallvec.workspace = true
smol.workspace = true
serde_json.workspace = true
[dev-dependencies]
client = { package = "client2", path = "../client2", features = ["test-support"] }
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
unindent.workspace = true

File diff suppressed because it is too large Load diff

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