Merge branch 'main' into editor2-autocomplete
# Conflicts: # crates/editor2/src/editor.rs
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
name: 'Check formatting'
|
||||
description: 'Checks code formatting use cargo fmt'
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Install Rust
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: |
|
||||
rustup set profile minimal
|
||||
rustup update stable
|
||||
|
||||
- name: cargo fmt
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: cargo fmt --all -- --check
|
||||
@@ -0,0 +1,34 @@
|
||||
name: "Run tests"
|
||||
description: "Runs the tests"
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Install Rust
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: |
|
||||
rustup set profile minimal
|
||||
rustup update stable
|
||||
rustup target add wasm32-wasi
|
||||
cargo install cargo-nextest
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
- name: Limit target directory size
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: script/clear-target-dir-if-larger-than 70
|
||||
|
||||
- name: Run check
|
||||
env:
|
||||
RUSTFLAGS: -D warnings
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: cargo check --tests --workspace
|
||||
|
||||
- name: Run tests
|
||||
env:
|
||||
RUSTFLAGS: -D warnings
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: cargo nextest run --workspace --no-fail-fast
|
||||
@@ -23,19 +23,14 @@ jobs:
|
||||
- self-hosted
|
||||
- test
|
||||
steps:
|
||||
- name: Install Rust
|
||||
run: |
|
||||
rustup set profile minimal
|
||||
rustup update stable
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
- name: cargo fmt
|
||||
run: cargo fmt --all -- --check
|
||||
- name: Run rustfmt
|
||||
uses: ./.github/actions/check_formatting
|
||||
|
||||
tests:
|
||||
name: Run tests
|
||||
@@ -43,35 +38,15 @@ jobs:
|
||||
- self-hosted
|
||||
- test
|
||||
needs: rustfmt
|
||||
env:
|
||||
RUSTFLAGS: -D warnings
|
||||
steps:
|
||||
- name: Install Rust
|
||||
run: |
|
||||
rustup set profile minimal
|
||||
rustup update stable
|
||||
rustup target add wasm32-wasi
|
||||
cargo install cargo-nextest
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Limit target directory size
|
||||
run: script/clear-target-dir-if-larger-than 70
|
||||
|
||||
- name: Run check
|
||||
run: cargo check --workspace
|
||||
|
||||
- name: Run tests
|
||||
run: cargo nextest run --workspace --no-fail-fast
|
||||
uses: ./.github/actions/run_tests
|
||||
|
||||
- name: Build collab
|
||||
run: cargo build -p collab
|
||||
@@ -130,6 +105,8 @@ jobs:
|
||||
expected_tag_name="v${version}";;
|
||||
preview)
|
||||
expected_tag_name="v${version}-pre";;
|
||||
nightly)
|
||||
expected_tag_name="v${version}-nightly";;
|
||||
*)
|
||||
echo "can't publish a release on channel ${channel}"
|
||||
exit 1;;
|
||||
@@ -154,7 +131,9 @@ jobs:
|
||||
|
||||
- uses: softprops/action-gh-release@v1
|
||||
name: Upload app bundle to release
|
||||
if: ${{ env.RELEASE_CHANNEL }}
|
||||
# TODO kb seems that zed.dev relies on GitHub releases for release version tracking.
|
||||
# Find alternatives for `nightly` or just go on with more releases?
|
||||
if: ${{ env.RELEASE_CHANNEL == 'preview' || env.RELEASE_CHANNEL == 'stable' }}
|
||||
with:
|
||||
draft: true
|
||||
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
name: Release Nightly
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Fire every night at 1:00am
|
||||
- cron: "0 1 * * *"
|
||||
push:
|
||||
tags:
|
||||
- "nightly"
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_INCREMENTAL: 0
|
||||
RUST_BACKTRACE: 1
|
||||
|
||||
jobs:
|
||||
rustfmt:
|
||||
name: Check formatting
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Run rustfmt
|
||||
uses: ./.github/actions/check_formatting
|
||||
|
||||
tests:
|
||||
name: Run tests
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
needs: rustfmt
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Run tests
|
||||
uses: ./.github/actions/run_tests
|
||||
|
||||
bundle:
|
||||
name: Bundle app
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- bundle
|
||||
needs: tests
|
||||
env:
|
||||
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
|
||||
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
|
||||
APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
|
||||
APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
|
||||
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
|
||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||
steps:
|
||||
- name: Install Rust
|
||||
run: |
|
||||
rustup set profile minimal
|
||||
rustup update stable
|
||||
rustup target add aarch64-apple-darwin
|
||||
rustup target add x86_64-apple-darwin
|
||||
rustup target add wasm32-wasi
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Limit target directory size
|
||||
run: script/clear-target-dir-if-larger-than 70
|
||||
|
||||
- name: Set release channel to nightly
|
||||
run: |
|
||||
set -eu
|
||||
version=$(git rev-parse --short HEAD)
|
||||
echo "Publishing version: ${version} on release channel nightly"
|
||||
echo "nightly" > crates/zed/RELEASE_CHANNEL
|
||||
|
||||
- name: Generate license file
|
||||
run: script/generate-licenses
|
||||
|
||||
- name: Create app bundle
|
||||
run: script/bundle -2
|
||||
|
||||
- name: Upload Zed Nightly
|
||||
run: script/upload-nightly
|
||||
Generated
+109
-72
@@ -724,6 +724,30 @@ dependencies = [
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "auto_update2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"client2",
|
||||
"db2",
|
||||
"gpui2",
|
||||
"isahc",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"menu2",
|
||||
"project2",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"settings2",
|
||||
"smol",
|
||||
"tempdir",
|
||||
"theme2",
|
||||
"util",
|
||||
"workspace2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.1.0"
|
||||
@@ -817,17 +841,6 @@ dependencies = [
|
||||
"rustc-demangle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "backtrace-on-stack-overflow"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fd2d70527f3737a1ad17355e260706c1badebabd1fa06a7a053407380df841b"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"libc",
|
||||
"nix 0.23.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.13.1"
|
||||
@@ -1374,7 +1387,7 @@ dependencies = [
|
||||
"smol",
|
||||
"sum_tree",
|
||||
"tempfile",
|
||||
"text",
|
||||
"text2",
|
||||
"thiserror",
|
||||
"time",
|
||||
"tiny_http",
|
||||
@@ -1526,6 +1539,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion 0.3.2",
|
||||
"async-tungstenite",
|
||||
"chrono",
|
||||
"collections",
|
||||
"db",
|
||||
"feature_flags",
|
||||
@@ -1562,6 +1576,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion 0.3.2",
|
||||
"async-tungstenite",
|
||||
"chrono",
|
||||
"collections",
|
||||
"db2",
|
||||
"feature_flags2",
|
||||
@@ -1843,7 +1858,7 @@ dependencies = [
|
||||
"editor2",
|
||||
"feature_flags2",
|
||||
"futures 0.3.28",
|
||||
"fuzzy",
|
||||
"fuzzy2",
|
||||
"gpui2",
|
||||
"language2",
|
||||
"lazy_static",
|
||||
@@ -2614,6 +2629,34 @@ dependencies = [
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "diagnostics2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"client2",
|
||||
"collections",
|
||||
"editor2",
|
||||
"futures 0.3.28",
|
||||
"gpui2",
|
||||
"language2",
|
||||
"log",
|
||||
"lsp2",
|
||||
"postage",
|
||||
"project2",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"settings2",
|
||||
"smallvec",
|
||||
"theme2",
|
||||
"ui2",
|
||||
"unindent",
|
||||
"util",
|
||||
"workspace2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "diff"
|
||||
version = "0.1.13"
|
||||
@@ -3759,7 +3802,7 @@ dependencies = [
|
||||
"smol",
|
||||
"sqlez",
|
||||
"sum_tree",
|
||||
"taffy",
|
||||
"taffy 0.3.11 (git+https://github.com/DioxusLabs/taffy?rev=4fb530bdd71609bb1d3f76c6a8bde1ba82805d5e)",
|
||||
"thiserror",
|
||||
"time",
|
||||
"tiny-skia",
|
||||
@@ -3824,7 +3867,7 @@ dependencies = [
|
||||
"smol",
|
||||
"sqlez",
|
||||
"sum_tree",
|
||||
"taffy",
|
||||
"taffy 0.3.11 (git+https://github.com/DioxusLabs/taffy?rev=1876f72bee5e376023eaa518aa7b8a34c769bd1b)",
|
||||
"thiserror",
|
||||
"time",
|
||||
"tiny-skia",
|
||||
@@ -3859,6 +3902,12 @@ version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eec1c01eb1de97451ee0d60de7d81cf1e72aabefb021616027f3d1c3ec1c723c"
|
||||
|
||||
[[package]]
|
||||
name = "grid"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1df00eed8d1f0db937f6be10e46e8072b0671accb504cf0f959c5c52c679f5b9"
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.3.21"
|
||||
@@ -4486,7 +4535,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"dirs 4.0.0",
|
||||
"editor",
|
||||
"editor2",
|
||||
"gpui2",
|
||||
"log",
|
||||
"schemars",
|
||||
@@ -5510,19 +5559,6 @@ dependencies = [
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.23.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"cc",
|
||||
"cfg-if 1.0.0",
|
||||
"libc",
|
||||
"memoffset 0.6.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.24.3"
|
||||
@@ -6703,7 +6739,6 @@ dependencies = [
|
||||
"anyhow",
|
||||
"client2",
|
||||
"collections",
|
||||
"context_menu",
|
||||
"db2",
|
||||
"editor2",
|
||||
"futures 0.3.28",
|
||||
@@ -7972,6 +8007,35 @@ dependencies = [
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "search2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 1.3.2",
|
||||
"client2",
|
||||
"collections",
|
||||
"editor2",
|
||||
"futures 0.3.28",
|
||||
"gpui2",
|
||||
"language2",
|
||||
"log",
|
||||
"menu2",
|
||||
"postage",
|
||||
"project2",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"settings2",
|
||||
"smallvec",
|
||||
"smol",
|
||||
"theme2",
|
||||
"ui2",
|
||||
"unindent",
|
||||
"util",
|
||||
"workspace2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "2.9.2"
|
||||
@@ -8795,45 +8859,6 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "storybook2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"backtrace-on-stack-overflow",
|
||||
"chrono",
|
||||
"clap 4.4.4",
|
||||
"editor2",
|
||||
"fuzzy2",
|
||||
"gpui2",
|
||||
"itertools 0.11.0",
|
||||
"language2",
|
||||
"log",
|
||||
"menu2",
|
||||
"picker2",
|
||||
"rust-embed",
|
||||
"serde",
|
||||
"settings2",
|
||||
"simplelog",
|
||||
"smallvec",
|
||||
"strum",
|
||||
"theme",
|
||||
"theme2",
|
||||
"ui2",
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "storybook3"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"gpui2",
|
||||
"settings2",
|
||||
"theme2",
|
||||
"ui2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stringprep"
|
||||
version = "0.1.4"
|
||||
@@ -9053,13 +9078,24 @@ dependencies = [
|
||||
"winx",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "taffy"
|
||||
version = "0.3.11"
|
||||
source = "git+https://github.com/DioxusLabs/taffy?rev=1876f72bee5e376023eaa518aa7b8a34c769bd1b#1876f72bee5e376023eaa518aa7b8a34c769bd1b"
|
||||
dependencies = [
|
||||
"arrayvec 0.7.4",
|
||||
"grid 0.11.0",
|
||||
"num-traits",
|
||||
"slotmap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "taffy"
|
||||
version = "0.3.11"
|
||||
source = "git+https://github.com/DioxusLabs/taffy?rev=4fb530bdd71609bb1d3f76c6a8bde1ba82805d5e#4fb530bdd71609bb1d3f76c6a8bde1ba82805d5e"
|
||||
dependencies = [
|
||||
"arrayvec 0.7.4",
|
||||
"grid",
|
||||
"grid 0.10.0",
|
||||
"num-traits",
|
||||
"slotmap",
|
||||
]
|
||||
@@ -11543,6 +11579,7 @@ dependencies = [
|
||||
"async-recursion 0.3.2",
|
||||
"async-tar",
|
||||
"async-trait",
|
||||
"auto_update2",
|
||||
"backtrace",
|
||||
"call2",
|
||||
"chrono",
|
||||
@@ -11554,6 +11591,7 @@ dependencies = [
|
||||
"copilot2",
|
||||
"ctor",
|
||||
"db2",
|
||||
"diagnostics2",
|
||||
"editor2",
|
||||
"env_logger 0.9.3",
|
||||
"feature_flags2",
|
||||
@@ -11561,7 +11599,6 @@ dependencies = [
|
||||
"fs2",
|
||||
"fsevent",
|
||||
"futures 0.3.28",
|
||||
"fuzzy",
|
||||
"go_to_line2",
|
||||
"gpui2",
|
||||
"ignore",
|
||||
@@ -11571,7 +11608,6 @@ dependencies = [
|
||||
"isahc",
|
||||
"journal2",
|
||||
"language2",
|
||||
"language_tools",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"log",
|
||||
@@ -11590,6 +11626,7 @@ dependencies = [
|
||||
"rsa 0.4.0",
|
||||
"rust-embed",
|
||||
"schemars",
|
||||
"search2",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
|
||||
+5
-2
@@ -6,6 +6,7 @@ members = [
|
||||
"crates/audio",
|
||||
"crates/audio2",
|
||||
"crates/auto_update",
|
||||
"crates/auto_update2",
|
||||
"crates/breadcrumbs",
|
||||
"crates/call",
|
||||
"crates/call2",
|
||||
@@ -32,6 +33,7 @@ members = [
|
||||
"crates/refineable",
|
||||
"crates/refineable/derive_refineable",
|
||||
"crates/diagnostics",
|
||||
"crates/diagnostics2",
|
||||
"crates/drag_and_drop",
|
||||
"crates/editor",
|
||||
"crates/feature_flags",
|
||||
@@ -88,14 +90,15 @@ members = [
|
||||
"crates/rpc",
|
||||
"crates/rpc2",
|
||||
"crates/search",
|
||||
"crates/search2",
|
||||
"crates/settings",
|
||||
"crates/settings2",
|
||||
"crates/snippet",
|
||||
"crates/sqlez",
|
||||
"crates/sqlez_macros",
|
||||
"crates/rich_text",
|
||||
"crates/storybook2",
|
||||
"crates/storybook3",
|
||||
# "crates/storybook2",
|
||||
# "crates/storybook3",
|
||||
"crates/sum_tree",
|
||||
"crates/terminal",
|
||||
"crates/terminal2",
|
||||
|
||||
@@ -1,6 +1 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.45563 12.3438H11.5444C11.9137 12.3438 12.1556 11.9571 11.994 11.625L10.2346 8.00952C9.77174 7.05841 8.89104 6.37821 7.85383 6.17077C7.29019 6.05804 6.70981 6.05804 6.14617 6.17077C5.10896 6.37821 4.22826 7.05841 3.76542 8.00952L2.00603 11.625C1.84442 11.9571 2.08628 12.3438 2.45563 12.3438Z" fill="#001A33" fill-opacity="0.157"/>
|
||||
<path d="M9.5 6.5L11.994 11.625C12.1556 11.9571 11.9137 12.3438 11.5444 12.3438H2.45563C2.08628 12.3438 1.84442 11.9571 2.00603 11.625L4.5 6.5" stroke="#11181C" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M7 7L7 2" stroke="#11181C" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="7" cy="9.24219" r="0.75" fill="#11181C"/>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-alert-triangle"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 835 B After Width: | Height: | Size: 351 B |
@@ -268,6 +268,19 @@
|
||||
// Whether to show warnings or not by default.
|
||||
"include_warnings": true
|
||||
},
|
||||
// Add files or globs of files that will be excluded by Zed entirely:
|
||||
// they will be skipped during FS scan(s), file tree and file search
|
||||
// will lack the corresponding file entries.
|
||||
"file_scan_exclusions": [
|
||||
"**/.git",
|
||||
"**/.svn",
|
||||
"**/.hg",
|
||||
"**/CVS",
|
||||
"**/.DS_Store",
|
||||
"**/Thumbs.db",
|
||||
"**/.classpath",
|
||||
"**/.settings"
|
||||
],
|
||||
// Git gutter behavior configuration.
|
||||
"git": {
|
||||
// Control whether the git gutter is shown. May take 2 values:
|
||||
|
||||
@@ -15,7 +15,7 @@ use ai::{
|
||||
use ai::prompts::repository_context::PromptCodeSnippet;
|
||||
use anyhow::{anyhow, Result};
|
||||
use chrono::{DateTime, Local};
|
||||
use client::{telemetry::AssistantKind, ClickhouseEvent, TelemetrySettings};
|
||||
use client::{telemetry::AssistantKind, TelemetrySettings};
|
||||
use collections::{hash_map, HashMap, HashSet, VecDeque};
|
||||
use editor::{
|
||||
display_map::{
|
||||
@@ -3803,12 +3803,12 @@ fn report_assistant_event(
|
||||
.default_open_ai_model
|
||||
.clone();
|
||||
|
||||
let event = ClickhouseEvent::Assistant {
|
||||
conversation_id,
|
||||
kind: assistant_kind,
|
||||
model: model.full_name(),
|
||||
};
|
||||
let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
|
||||
|
||||
telemetry.report_clickhouse_event(event, telemetry_settings)
|
||||
telemetry.report_assistant_event(
|
||||
telemetry_settings,
|
||||
conversation_id,
|
||||
assistant_kind,
|
||||
model.full_name(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -118,14 +118,18 @@ fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) {
|
||||
let auto_updater = auto_updater.read(cx);
|
||||
let server_url = &auto_updater.server_url;
|
||||
let current_version = auto_updater.current_version;
|
||||
let latest_release_url = if cx.has_global::<ReleaseChannel>()
|
||||
&& *cx.global::<ReleaseChannel>() == ReleaseChannel::Preview
|
||||
{
|
||||
format!("{server_url}/releases/preview/{current_version}")
|
||||
} else {
|
||||
format!("{server_url}/releases/stable/{current_version}")
|
||||
};
|
||||
cx.platform().open_url(&latest_release_url);
|
||||
if cx.has_global::<ReleaseChannel>() {
|
||||
match cx.global::<ReleaseChannel>() {
|
||||
ReleaseChannel::Dev => {}
|
||||
ReleaseChannel::Nightly => {}
|
||||
ReleaseChannel::Preview => cx
|
||||
.platform()
|
||||
.open_url(&format!("{server_url}/releases/preview/{current_version}")),
|
||||
ReleaseChannel::Stable => cx
|
||||
.platform()
|
||||
.open_url(&format!("{server_url}/releases/stable/{current_version}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,22 +228,19 @@ impl AutoUpdater {
|
||||
)
|
||||
});
|
||||
|
||||
let preview_param = cx.read(|cx| {
|
||||
let mut url_string = format!(
|
||||
"{server_url}/api/releases/latest?token={ZED_SECRET_CLIENT_TOKEN}&asset=Zed.dmg"
|
||||
);
|
||||
cx.read(|cx| {
|
||||
if cx.has_global::<ReleaseChannel>() {
|
||||
if *cx.global::<ReleaseChannel>() == ReleaseChannel::Preview {
|
||||
return "&preview=1";
|
||||
if let Some(param) = cx.global::<ReleaseChannel>().release_query_param() {
|
||||
url_string += "&";
|
||||
url_string += param;
|
||||
}
|
||||
}
|
||||
""
|
||||
});
|
||||
|
||||
let mut response = client
|
||||
.get(
|
||||
&format!("{server_url}/api/releases/latest?token={ZED_SECRET_CLIENT_TOKEN}&asset=Zed.dmg{preview_param}"),
|
||||
Default::default(),
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
let mut response = client.get(&url_string, Default::default(), true).await?;
|
||||
|
||||
let mut body = Vec::new();
|
||||
response
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
[package]
|
||||
name = "auto_update2"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/auto_update.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
db = { package = "db2", path = "../db2" }
|
||||
client = { package = "client2", path = "../client2" }
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
menu = { package = "menu2", path = "../menu2" }
|
||||
project = { package = "project2", path = "../project2" }
|
||||
settings = { package = "settings2", path = "../settings2" }
|
||||
theme = { package = "theme2", path = "../theme2" }
|
||||
workspace = { package = "workspace2", path = "../workspace2" }
|
||||
util = { path = "../util" }
|
||||
anyhow.workspace = true
|
||||
isahc.workspace = true
|
||||
lazy_static.workspace = true
|
||||
log.workspace = true
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
serde_json.workspace = true
|
||||
smol.workspace = true
|
||||
tempdir.workspace = true
|
||||
@@ -0,0 +1,406 @@
|
||||
mod update_notification;
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use client::{Client, TelemetrySettings, ZED_APP_PATH, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN};
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use db::RELEASE_CHANNEL;
|
||||
use gpui::{
|
||||
actions, AppContext, AsyncAppContext, Context as _, Model, ModelContext, SemanticVersion, Task,
|
||||
ViewContext, VisualContext,
|
||||
};
|
||||
use isahc::AsyncBody;
|
||||
use serde::Deserialize;
|
||||
use serde_derive::Serialize;
|
||||
use smol::io::AsyncReadExt;
|
||||
|
||||
use settings::{Settings, SettingsStore};
|
||||
use smol::{fs::File, process::Command};
|
||||
use std::{ffi::OsString, sync::Arc, time::Duration};
|
||||
use update_notification::UpdateNotification;
|
||||
use util::channel::{AppCommitSha, ReleaseChannel};
|
||||
use util::http::HttpClient;
|
||||
use workspace::Workspace;
|
||||
|
||||
const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &str = "auto-updater-should-show-updated-notification";
|
||||
const POLL_INTERVAL: Duration = Duration::from_secs(60 * 60);
|
||||
|
||||
//todo!(remove CheckThatAutoUpdaterWorks)
|
||||
actions!(
|
||||
Check,
|
||||
DismissErrorMessage,
|
||||
ViewReleaseNotes,
|
||||
CheckThatAutoUpdaterWorks
|
||||
);
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct UpdateRequestBody {
|
||||
installation_id: Option<Arc<str>>,
|
||||
release_channel: Option<&'static str>,
|
||||
telemetry: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub enum AutoUpdateStatus {
|
||||
Idle,
|
||||
Checking,
|
||||
Downloading,
|
||||
Installing,
|
||||
Updated,
|
||||
Errored,
|
||||
}
|
||||
|
||||
pub struct AutoUpdater {
|
||||
status: AutoUpdateStatus,
|
||||
current_version: SemanticVersion,
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
pending_poll: Option<Task<Option<()>>>,
|
||||
server_url: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct JsonRelease {
|
||||
version: String,
|
||||
url: String,
|
||||
}
|
||||
|
||||
struct AutoUpdateSetting(bool);
|
||||
|
||||
impl Settings for AutoUpdateSetting {
|
||||
const KEY: Option<&'static str> = Some("auto_update");
|
||||
|
||||
type FileContent = Option<bool>;
|
||||
|
||||
fn load(
|
||||
default_value: &Option<bool>,
|
||||
user_values: &[&Option<bool>],
|
||||
_: &mut AppContext,
|
||||
) -> Result<Self> {
|
||||
Ok(Self(
|
||||
Self::json_merge(default_value, user_values)?.ok_or_else(Self::missing_default)?,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut AppContext) {
|
||||
AutoUpdateSetting::register(cx);
|
||||
|
||||
cx.observe_new_views(|wokrspace: &mut Workspace, _cx| {
|
||||
wokrspace
|
||||
.register_action(|_, action: &Check, cx| check(action, cx))
|
||||
.register_action(|_, _action: &CheckThatAutoUpdaterWorks, cx| {
|
||||
let prompt = cx.prompt(gpui::PromptLevel::Info, "It does!", &["Ok"]);
|
||||
cx.spawn(|_, _cx| async move {
|
||||
prompt.await.ok();
|
||||
})
|
||||
.detach();
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
|
||||
if let Some(version) = *ZED_APP_VERSION {
|
||||
let auto_updater = cx.build_model(|cx| {
|
||||
let updater = AutoUpdater::new(version, http_client, server_url);
|
||||
|
||||
let mut update_subscription = AutoUpdateSetting::get_global(cx)
|
||||
.0
|
||||
.then(|| updater.start_polling(cx));
|
||||
|
||||
cx.observe_global::<SettingsStore>(move |updater, cx| {
|
||||
if AutoUpdateSetting::get_global(cx).0 {
|
||||
if update_subscription.is_none() {
|
||||
update_subscription = Some(updater.start_polling(cx))
|
||||
}
|
||||
} else {
|
||||
update_subscription.take();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
updater
|
||||
});
|
||||
cx.set_global(Some(auto_updater));
|
||||
//todo!(action)
|
||||
// cx.add_global_action(view_release_notes);
|
||||
// cx.add_action(UpdateNotification::dismiss);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check(_: &Check, cx: &mut AppContext) {
|
||||
if let Some(updater) = AutoUpdater::get(cx) {
|
||||
updater.update(cx, |updater, cx| updater.poll(cx));
|
||||
}
|
||||
}
|
||||
|
||||
fn _view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) {
|
||||
if let Some(auto_updater) = AutoUpdater::get(cx) {
|
||||
let auto_updater = auto_updater.read(cx);
|
||||
let server_url = &auto_updater.server_url;
|
||||
let current_version = auto_updater.current_version;
|
||||
if cx.has_global::<ReleaseChannel>() {
|
||||
match cx.global::<ReleaseChannel>() {
|
||||
ReleaseChannel::Dev => {}
|
||||
ReleaseChannel::Nightly => {}
|
||||
ReleaseChannel::Preview => {
|
||||
cx.open_url(&format!("{server_url}/releases/preview/{current_version}"))
|
||||
}
|
||||
ReleaseChannel::Stable => {
|
||||
cx.open_url(&format!("{server_url}/releases/stable/{current_version}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn notify_of_any_new_update(cx: &mut ViewContext<Workspace>) -> Option<()> {
|
||||
let updater = AutoUpdater::get(cx)?;
|
||||
let version = updater.read(cx).current_version;
|
||||
let should_show_notification = updater.read(cx).should_show_update_notification(cx);
|
||||
|
||||
cx.spawn(|workspace, mut cx| async move {
|
||||
let should_show_notification = should_show_notification.await?;
|
||||
if should_show_notification {
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
workspace.show_notification(0, cx, |cx| {
|
||||
cx.build_view(|_| UpdateNotification::new(version))
|
||||
});
|
||||
updater
|
||||
.read(cx)
|
||||
.set_should_show_update_notification(false, cx)
|
||||
.detach_and_log_err(cx);
|
||||
})?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
impl AutoUpdater {
|
||||
pub fn get(cx: &mut AppContext) -> Option<Model<Self>> {
|
||||
cx.default_global::<Option<Model<Self>>>().clone()
|
||||
}
|
||||
|
||||
fn new(
|
||||
current_version: SemanticVersion,
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
server_url: String,
|
||||
) -> Self {
|
||||
Self {
|
||||
status: AutoUpdateStatus::Idle,
|
||||
current_version,
|
||||
http_client,
|
||||
server_url,
|
||||
pending_poll: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_polling(&self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
loop {
|
||||
this.update(&mut cx, |this, cx| this.poll(cx))?;
|
||||
cx.background_executor().timer(POLL_INTERVAL).await;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn poll(&mut self, cx: &mut ModelContext<Self>) {
|
||||
if self.pending_poll.is_some() || self.status == AutoUpdateStatus::Updated {
|
||||
return;
|
||||
}
|
||||
|
||||
self.status = AutoUpdateStatus::Checking;
|
||||
cx.notify();
|
||||
|
||||
self.pending_poll = Some(cx.spawn(|this, mut cx| async move {
|
||||
let result = Self::update(this.upgrade()?, cx.clone()).await;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.pending_poll = None;
|
||||
if let Err(error) = result {
|
||||
log::error!("auto-update failed: error:{:?}", error);
|
||||
this.status = AutoUpdateStatus::Errored;
|
||||
cx.notify();
|
||||
}
|
||||
})
|
||||
.ok()
|
||||
}));
|
||||
}
|
||||
|
||||
pub fn status(&self) -> AutoUpdateStatus {
|
||||
self.status
|
||||
}
|
||||
|
||||
pub fn dismiss_error(&mut self, cx: &mut ModelContext<Self>) {
|
||||
self.status = AutoUpdateStatus::Idle;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
async fn update(this: Model<Self>, mut cx: AsyncAppContext) -> Result<()> {
|
||||
let (client, server_url, current_version) = this.read_with(&cx, |this, _| {
|
||||
(
|
||||
this.http_client.clone(),
|
||||
this.server_url.clone(),
|
||||
this.current_version,
|
||||
)
|
||||
})?;
|
||||
|
||||
let mut url_string = format!(
|
||||
"{server_url}/api/releases/latest?token={ZED_SECRET_CLIENT_TOKEN}&asset=Zed.dmg"
|
||||
);
|
||||
cx.update(|cx| {
|
||||
if cx.has_global::<ReleaseChannel>() {
|
||||
if let Some(param) = cx.global::<ReleaseChannel>().release_query_param() {
|
||||
url_string += "&";
|
||||
url_string += param;
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
let mut response = client.get(&url_string, Default::default(), true).await?;
|
||||
|
||||
let mut body = Vec::new();
|
||||
response
|
||||
.body_mut()
|
||||
.read_to_end(&mut body)
|
||||
.await
|
||||
.context("error reading release")?;
|
||||
let release: JsonRelease =
|
||||
serde_json::from_slice(body.as_slice()).context("error deserializing release")?;
|
||||
|
||||
let should_download = match *RELEASE_CHANNEL {
|
||||
ReleaseChannel::Nightly => cx
|
||||
.try_read_global::<AppCommitSha, _>(|sha, _| release.version != sha.0)
|
||||
.unwrap_or(true),
|
||||
_ => release.version.parse::<SemanticVersion>()? <= current_version,
|
||||
};
|
||||
|
||||
if !should_download {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.status = AutoUpdateStatus::Idle;
|
||||
cx.notify();
|
||||
})?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.status = AutoUpdateStatus::Downloading;
|
||||
cx.notify();
|
||||
})?;
|
||||
|
||||
let temp_dir = tempdir::TempDir::new("zed-auto-update")?;
|
||||
let dmg_path = temp_dir.path().join("Zed.dmg");
|
||||
let mount_path = temp_dir.path().join("Zed");
|
||||
let running_app_path = ZED_APP_PATH
|
||||
.clone()
|
||||
.map_or_else(|| cx.update(|cx| cx.app_path())?, Ok)?;
|
||||
let running_app_filename = running_app_path
|
||||
.file_name()
|
||||
.ok_or_else(|| anyhow!("invalid running app path"))?;
|
||||
let mut mounted_app_path: OsString = mount_path.join(running_app_filename).into();
|
||||
mounted_app_path.push("/");
|
||||
|
||||
let mut dmg_file = File::create(&dmg_path).await?;
|
||||
|
||||
let (installation_id, release_channel, telemetry) = cx.update(|cx| {
|
||||
let installation_id = cx.global::<Arc<Client>>().telemetry().installation_id();
|
||||
let release_channel = cx
|
||||
.has_global::<ReleaseChannel>()
|
||||
.then(|| cx.global::<ReleaseChannel>().display_name());
|
||||
let telemetry = TelemetrySettings::get_global(cx).metrics;
|
||||
|
||||
(installation_id, release_channel, telemetry)
|
||||
})?;
|
||||
|
||||
let request_body = AsyncBody::from(serde_json::to_string(&UpdateRequestBody {
|
||||
installation_id,
|
||||
release_channel,
|
||||
telemetry,
|
||||
})?);
|
||||
|
||||
let mut response = client.get(&release.url, request_body, true).await?;
|
||||
smol::io::copy(response.body_mut(), &mut dmg_file).await?;
|
||||
log::info!("downloaded update. path:{:?}", dmg_path);
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.status = AutoUpdateStatus::Installing;
|
||||
cx.notify();
|
||||
})?;
|
||||
|
||||
let output = Command::new("hdiutil")
|
||||
.args(&["attach", "-nobrowse"])
|
||||
.arg(&dmg_path)
|
||||
.arg("-mountroot")
|
||||
.arg(&temp_dir.path())
|
||||
.output()
|
||||
.await?;
|
||||
if !output.status.success() {
|
||||
Err(anyhow!(
|
||||
"failed to mount: {:?}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
))?;
|
||||
}
|
||||
|
||||
let output = Command::new("rsync")
|
||||
.args(&["-av", "--delete"])
|
||||
.arg(&mounted_app_path)
|
||||
.arg(&running_app_path)
|
||||
.output()
|
||||
.await?;
|
||||
if !output.status.success() {
|
||||
Err(anyhow!(
|
||||
"failed to copy app: {:?}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
))?;
|
||||
}
|
||||
|
||||
let output = Command::new("hdiutil")
|
||||
.args(&["detach"])
|
||||
.arg(&mount_path)
|
||||
.output()
|
||||
.await?;
|
||||
if !output.status.success() {
|
||||
Err(anyhow!(
|
||||
"failed to unmount: {:?}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
))?;
|
||||
}
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.set_should_show_update_notification(true, cx)
|
||||
.detach_and_log_err(cx);
|
||||
this.status = AutoUpdateStatus::Updated;
|
||||
cx.notify();
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_should_show_update_notification(
|
||||
&self,
|
||||
should_show: bool,
|
||||
cx: &AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
cx.background_executor().spawn(async move {
|
||||
if should_show {
|
||||
KEY_VALUE_STORE
|
||||
.write_kvp(
|
||||
SHOULD_SHOW_UPDATE_NOTIFICATION_KEY.to_string(),
|
||||
"".to_string(),
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
KEY_VALUE_STORE
|
||||
.delete_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY.to_string())
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn should_show_update_notification(&self, cx: &AppContext) -> Task<Result<bool>> {
|
||||
cx.background_executor().spawn(async move {
|
||||
Ok(KEY_VALUE_STORE
|
||||
.read_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY)?
|
||||
.is_some())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
use gpui::{div, Div, EventEmitter, ParentElement, Render, SemanticVersion, ViewContext};
|
||||
use menu::Cancel;
|
||||
use workspace::notifications::NotificationEvent;
|
||||
|
||||
pub struct UpdateNotification {
|
||||
_version: SemanticVersion,
|
||||
}
|
||||
|
||||
impl EventEmitter<NotificationEvent> for UpdateNotification {}
|
||||
|
||||
impl Render for UpdateNotification {
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, _cx: &mut gpui::ViewContext<Self>) -> Self::Element {
|
||||
div().child("Updated zed!")
|
||||
// let theme = theme::current(cx).clone();
|
||||
// let theme = &theme.update_notification;
|
||||
|
||||
// let app_name = cx.global::<ReleaseChannel>().display_name();
|
||||
|
||||
// MouseEventHandler::new::<ViewReleaseNotes, _>(0, cx, |state, cx| {
|
||||
// Flex::column()
|
||||
// .with_child(
|
||||
// Flex::row()
|
||||
// .with_child(
|
||||
// Text::new(
|
||||
// format!("Updated to {app_name} {}", self.version),
|
||||
// theme.message.text.clone(),
|
||||
// )
|
||||
// .contained()
|
||||
// .with_style(theme.message.container)
|
||||
// .aligned()
|
||||
// .top()
|
||||
// .left()
|
||||
// .flex(1., true),
|
||||
// )
|
||||
// .with_child(
|
||||
// MouseEventHandler::new::<Cancel, _>(0, cx, |state, _| {
|
||||
// let style = theme.dismiss_button.style_for(state);
|
||||
// Svg::new("icons/x.svg")
|
||||
// .with_color(style.color)
|
||||
// .constrained()
|
||||
// .with_width(style.icon_width)
|
||||
// .aligned()
|
||||
// .contained()
|
||||
// .with_style(style.container)
|
||||
// .constrained()
|
||||
// .with_width(style.button_width)
|
||||
// .with_height(style.button_width)
|
||||
// })
|
||||
// .with_padding(Padding::uniform(5.))
|
||||
// .on_click(MouseButton::Left, move |_, this, cx| {
|
||||
// this.dismiss(&Default::default(), cx)
|
||||
// })
|
||||
// .aligned()
|
||||
// .constrained()
|
||||
// .with_height(cx.font_cache().line_height(theme.message.text.font_size))
|
||||
// .aligned()
|
||||
// .top()
|
||||
// .flex_float(),
|
||||
// ),
|
||||
// )
|
||||
// .with_child({
|
||||
// let style = theme.action_message.style_for(state);
|
||||
// Text::new("View the release notes", style.text.clone())
|
||||
// .contained()
|
||||
// .with_style(style.container)
|
||||
// })
|
||||
// .contained()
|
||||
// })
|
||||
// .with_cursor_style(CursorStyle::PointingHand)
|
||||
// .on_click(MouseButton::Left, |_, _, cx| {
|
||||
// crate::view_release_notes(&Default::default(), cx)
|
||||
// })
|
||||
// .into_any_named("update notification")
|
||||
}
|
||||
}
|
||||
|
||||
impl UpdateNotification {
|
||||
pub fn new(version: SemanticVersion) -> Self {
|
||||
Self { _version: version }
|
||||
}
|
||||
|
||||
pub fn _dismiss(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(NotificationEvent::Dismiss);
|
||||
}
|
||||
}
|
||||
+8
-15
@@ -5,10 +5,7 @@ pub mod room;
|
||||
use anyhow::{anyhow, Result};
|
||||
use audio::Audio;
|
||||
use call_settings::CallSettings;
|
||||
use client::{
|
||||
proto, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore,
|
||||
ZED_ALWAYS_ACTIVE,
|
||||
};
|
||||
use client::{proto, Client, TelemetrySettings, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE};
|
||||
use collections::HashSet;
|
||||
use futures::{channel::oneshot, future::Shared, Future, FutureExt};
|
||||
use gpui::{
|
||||
@@ -485,12 +482,8 @@ pub fn report_call_event_for_room(
|
||||
) {
|
||||
let telemetry = client.telemetry();
|
||||
let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
|
||||
let event = ClickhouseEvent::Call {
|
||||
operation,
|
||||
room_id: Some(room_id),
|
||||
channel_id,
|
||||
};
|
||||
telemetry.report_clickhouse_event(event, telemetry_settings);
|
||||
|
||||
telemetry.report_call_event(telemetry_settings, operation, Some(room_id), channel_id)
|
||||
}
|
||||
|
||||
pub fn report_call_event_for_channel(
|
||||
@@ -504,12 +497,12 @@ pub fn report_call_event_for_channel(
|
||||
let telemetry = client.telemetry();
|
||||
let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
|
||||
|
||||
let event = ClickhouseEvent::Call {
|
||||
telemetry.report_call_event(
|
||||
telemetry_settings,
|
||||
operation,
|
||||
room_id: room.map(|r| r.read(cx).id()),
|
||||
channel_id: Some(channel_id),
|
||||
};
|
||||
telemetry.report_clickhouse_event(event, telemetry_settings);
|
||||
room.map(|r| r.read(cx).id()),
|
||||
Some(channel_id),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -5,10 +5,7 @@ pub mod room;
|
||||
use anyhow::{anyhow, Result};
|
||||
use audio::Audio;
|
||||
use call_settings::CallSettings;
|
||||
use client::{
|
||||
proto, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore,
|
||||
ZED_ALWAYS_ACTIVE,
|
||||
};
|
||||
use client::{proto, Client, TelemetrySettings, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE};
|
||||
use collections::HashSet;
|
||||
use futures::{channel::oneshot, future::Shared, Future, FutureExt};
|
||||
use gpui::{
|
||||
@@ -484,12 +481,8 @@ pub fn report_call_event_for_room(
|
||||
) {
|
||||
let telemetry = client.telemetry();
|
||||
let telemetry_settings = *TelemetrySettings::get_global(cx);
|
||||
let event = ClickhouseEvent::Call {
|
||||
operation,
|
||||
room_id: Some(room_id),
|
||||
channel_id,
|
||||
};
|
||||
telemetry.report_clickhouse_event(event, telemetry_settings);
|
||||
|
||||
telemetry.report_call_event(telemetry_settings, operation, Some(room_id), channel_id)
|
||||
}
|
||||
|
||||
pub fn report_call_event_for_channel(
|
||||
@@ -504,12 +497,12 @@ pub fn report_call_event_for_channel(
|
||||
|
||||
let telemetry_settings = *TelemetrySettings::get_global(cx);
|
||||
|
||||
let event = ClickhouseEvent::Call {
|
||||
telemetry.report_call_event(
|
||||
telemetry_settings,
|
||||
operation,
|
||||
room_id: room.map(|r| r.read(cx).id()),
|
||||
channel_id: Some(channel_id),
|
||||
};
|
||||
telemetry.report_clickhouse_event(event, telemetry_settings);
|
||||
room.map(|r| r.read(cx).id()),
|
||||
Some(channel_id),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -18,7 +18,7 @@ db = { package = "db2", path = "../db2" }
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
util = { path = "../util" }
|
||||
rpc = { package = "rpc2", path = "../rpc2" }
|
||||
text = { path = "../text" }
|
||||
text = { package = "text2", path = "../text2" }
|
||||
language = { package = "language2", path = "../language2" }
|
||||
settings = { package = "settings2", path = "../settings2" }
|
||||
feature_flags = { package = "feature_flags2", path = "../feature_flags2" }
|
||||
|
||||
@@ -12,6 +12,7 @@ doctest = false
|
||||
test-support = ["collections/test-support", "gpui/test-support", "rpc/test-support"]
|
||||
|
||||
[dependencies]
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
collections = { path = "../collections" }
|
||||
db = { path = "../db" }
|
||||
gpui = { path = "../gpui" }
|
||||
|
||||
@@ -987,9 +987,17 @@ impl Client {
|
||||
self.establish_websocket_connection(credentials, cx)
|
||||
}
|
||||
|
||||
async fn get_rpc_url(http: Arc<dyn HttpClient>, is_preview: bool) -> Result<Url> {
|
||||
let preview_param = if is_preview { "?preview=1" } else { "" };
|
||||
let url = format!("{}/rpc{preview_param}", *ZED_SERVER_URL);
|
||||
async fn get_rpc_url(
|
||||
http: Arc<dyn HttpClient>,
|
||||
release_channel: Option<ReleaseChannel>,
|
||||
) -> Result<Url> {
|
||||
let mut url = format!("{}/rpc", *ZED_SERVER_URL);
|
||||
if let Some(preview_param) =
|
||||
release_channel.and_then(|channel| channel.release_query_param())
|
||||
{
|
||||
url += "?";
|
||||
url += preview_param;
|
||||
}
|
||||
let response = http.get(&url, Default::default(), false).await?;
|
||||
|
||||
// Normally, ZED_SERVER_URL is set to the URL of zed.dev website.
|
||||
@@ -1024,11 +1032,11 @@ impl Client {
|
||||
credentials: &Credentials,
|
||||
cx: &AsyncAppContext,
|
||||
) -> Task<Result<Connection, EstablishConnectionError>> {
|
||||
let use_preview_server = cx.read(|cx| {
|
||||
let release_channel = cx.read(|cx| {
|
||||
if cx.has_global::<ReleaseChannel>() {
|
||||
*cx.global::<ReleaseChannel>() != ReleaseChannel::Stable
|
||||
Some(*cx.global::<ReleaseChannel>())
|
||||
} else {
|
||||
false
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1041,7 +1049,7 @@ impl Client {
|
||||
|
||||
let http = self.http.clone();
|
||||
cx.background().spawn(async move {
|
||||
let mut rpc_url = Self::get_rpc_url(http, use_preview_server).await?;
|
||||
let mut rpc_url = Self::get_rpc_url(http, release_channel).await?;
|
||||
let rpc_host = rpc_url
|
||||
.host_str()
|
||||
.zip(rpc_url.port_or_known_default())
|
||||
@@ -1191,7 +1199,7 @@ impl Client {
|
||||
|
||||
// Use the collab server's admin API to retrieve the id
|
||||
// of the impersonated user.
|
||||
let mut url = Self::get_rpc_url(http.clone(), false).await?;
|
||||
let mut url = Self::get_rpc_url(http.clone(), None).await?;
|
||||
url.set_path("/user");
|
||||
url.set_query(Some(&format!("github_login={login}")));
|
||||
let request = Request::get(url.as_str())
|
||||
|
||||
+138
-14
@@ -1,4 +1,5 @@
|
||||
use crate::{TelemetrySettings, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
|
||||
use chrono::{DateTime, Utc};
|
||||
use gpui::{executor::Background, serde_json, AppContext, Task};
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::Mutex;
|
||||
@@ -20,7 +21,7 @@ pub struct Telemetry {
|
||||
#[derive(Default)]
|
||||
struct TelemetryState {
|
||||
metrics_id: Option<Arc<str>>, // Per logged-in user
|
||||
installation_id: Option<Arc<str>>, // Per app installation (different for dev, preview, and stable)
|
||||
installation_id: Option<Arc<str>>, // Per app installation (different for dev, nightly, preview, and stable)
|
||||
session_id: Option<Arc<str>>, // Per app launch
|
||||
app_version: Option<Arc<str>>,
|
||||
release_channel: Option<&'static str>,
|
||||
@@ -31,6 +32,7 @@ struct TelemetryState {
|
||||
flush_clickhouse_events_task: Option<Task<()>>,
|
||||
log_file: Option<NamedTempFile>,
|
||||
is_staff: Option<bool>,
|
||||
first_event_datetime: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
const CLICKHOUSE_EVENTS_URL_PATH: &'static str = "/api/events";
|
||||
@@ -77,29 +79,35 @@ pub enum ClickhouseEvent {
|
||||
vim_mode: bool,
|
||||
copilot_enabled: bool,
|
||||
copilot_enabled_for_language: bool,
|
||||
milliseconds_since_first_event: i64,
|
||||
},
|
||||
Copilot {
|
||||
suggestion_id: Option<String>,
|
||||
suggestion_accepted: bool,
|
||||
file_extension: Option<String>,
|
||||
milliseconds_since_first_event: i64,
|
||||
},
|
||||
Call {
|
||||
operation: &'static str,
|
||||
room_id: Option<u64>,
|
||||
channel_id: Option<u64>,
|
||||
milliseconds_since_first_event: i64,
|
||||
},
|
||||
Assistant {
|
||||
conversation_id: Option<String>,
|
||||
kind: AssistantKind,
|
||||
model: &'static str,
|
||||
milliseconds_since_first_event: i64,
|
||||
},
|
||||
Cpu {
|
||||
usage_as_percentage: f32,
|
||||
core_count: u32,
|
||||
milliseconds_since_first_event: i64,
|
||||
},
|
||||
Memory {
|
||||
memory_in_bytes: u64,
|
||||
virtual_memory_in_bytes: u64,
|
||||
milliseconds_since_first_event: i64,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -140,6 +148,7 @@ impl Telemetry {
|
||||
flush_clickhouse_events_task: Default::default(),
|
||||
log_file: None,
|
||||
is_staff: None,
|
||||
first_event_datetime: None,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -195,20 +204,18 @@ impl Telemetry {
|
||||
return;
|
||||
};
|
||||
|
||||
let memory_event = ClickhouseEvent::Memory {
|
||||
memory_in_bytes: process.memory(),
|
||||
virtual_memory_in_bytes: process.virtual_memory(),
|
||||
};
|
||||
|
||||
let cpu_event = ClickhouseEvent::Cpu {
|
||||
usage_as_percentage: process.cpu_usage(),
|
||||
core_count: system.cpus().len() as u32,
|
||||
};
|
||||
|
||||
let telemetry_settings = cx.update(|cx| *settings::get::<TelemetrySettings>(cx));
|
||||
|
||||
this.report_clickhouse_event(memory_event, telemetry_settings);
|
||||
this.report_clickhouse_event(cpu_event, telemetry_settings);
|
||||
this.report_memory_event(
|
||||
telemetry_settings,
|
||||
process.memory(),
|
||||
process.virtual_memory(),
|
||||
);
|
||||
this.report_cpu_event(
|
||||
telemetry_settings,
|
||||
process.cpu_usage(),
|
||||
system.cpus().len() as u32,
|
||||
);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
@@ -231,7 +238,123 @@ impl Telemetry {
|
||||
drop(state);
|
||||
}
|
||||
|
||||
pub fn report_clickhouse_event(
|
||||
pub fn report_editor_event(
|
||||
self: &Arc<Self>,
|
||||
telemetry_settings: TelemetrySettings,
|
||||
file_extension: Option<String>,
|
||||
vim_mode: bool,
|
||||
operation: &'static str,
|
||||
copilot_enabled: bool,
|
||||
copilot_enabled_for_language: bool,
|
||||
) {
|
||||
let event = ClickhouseEvent::Editor {
|
||||
file_extension,
|
||||
vim_mode,
|
||||
operation,
|
||||
copilot_enabled,
|
||||
copilot_enabled_for_language,
|
||||
milliseconds_since_first_event: self.milliseconds_since_first_event(),
|
||||
};
|
||||
|
||||
self.report_clickhouse_event(event, telemetry_settings)
|
||||
}
|
||||
|
||||
pub fn report_copilot_event(
|
||||
self: &Arc<Self>,
|
||||
telemetry_settings: TelemetrySettings,
|
||||
suggestion_id: Option<String>,
|
||||
suggestion_accepted: bool,
|
||||
file_extension: Option<String>,
|
||||
) {
|
||||
let event = ClickhouseEvent::Copilot {
|
||||
suggestion_id,
|
||||
suggestion_accepted,
|
||||
file_extension,
|
||||
milliseconds_since_first_event: self.milliseconds_since_first_event(),
|
||||
};
|
||||
|
||||
self.report_clickhouse_event(event, telemetry_settings)
|
||||
}
|
||||
|
||||
pub fn report_assistant_event(
|
||||
self: &Arc<Self>,
|
||||
telemetry_settings: TelemetrySettings,
|
||||
conversation_id: Option<String>,
|
||||
kind: AssistantKind,
|
||||
model: &'static str,
|
||||
) {
|
||||
let event = ClickhouseEvent::Assistant {
|
||||
conversation_id,
|
||||
kind,
|
||||
model,
|
||||
milliseconds_since_first_event: self.milliseconds_since_first_event(),
|
||||
};
|
||||
|
||||
self.report_clickhouse_event(event, telemetry_settings)
|
||||
}
|
||||
|
||||
pub fn report_call_event(
|
||||
self: &Arc<Self>,
|
||||
telemetry_settings: TelemetrySettings,
|
||||
operation: &'static str,
|
||||
room_id: Option<u64>,
|
||||
channel_id: Option<u64>,
|
||||
) {
|
||||
let event = ClickhouseEvent::Call {
|
||||
operation,
|
||||
room_id,
|
||||
channel_id,
|
||||
milliseconds_since_first_event: self.milliseconds_since_first_event(),
|
||||
};
|
||||
|
||||
self.report_clickhouse_event(event, telemetry_settings)
|
||||
}
|
||||
|
||||
pub fn report_cpu_event(
|
||||
self: &Arc<Self>,
|
||||
telemetry_settings: TelemetrySettings,
|
||||
usage_as_percentage: f32,
|
||||
core_count: u32,
|
||||
) {
|
||||
let event = ClickhouseEvent::Cpu {
|
||||
usage_as_percentage,
|
||||
core_count,
|
||||
milliseconds_since_first_event: self.milliseconds_since_first_event(),
|
||||
};
|
||||
|
||||
self.report_clickhouse_event(event, telemetry_settings)
|
||||
}
|
||||
|
||||
pub fn report_memory_event(
|
||||
self: &Arc<Self>,
|
||||
telemetry_settings: TelemetrySettings,
|
||||
memory_in_bytes: u64,
|
||||
virtual_memory_in_bytes: u64,
|
||||
) {
|
||||
let event = ClickhouseEvent::Memory {
|
||||
memory_in_bytes,
|
||||
virtual_memory_in_bytes,
|
||||
milliseconds_since_first_event: self.milliseconds_since_first_event(),
|
||||
};
|
||||
|
||||
self.report_clickhouse_event(event, telemetry_settings)
|
||||
}
|
||||
|
||||
fn milliseconds_since_first_event(&self) -> i64 {
|
||||
let mut state = self.state.lock();
|
||||
match state.first_event_datetime {
|
||||
Some(first_event_datetime) => {
|
||||
let now: DateTime<Utc> = Utc::now();
|
||||
now.timestamp_millis() - first_event_datetime.timestamp_millis()
|
||||
}
|
||||
None => {
|
||||
state.first_event_datetime = Some(Utc::now());
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn report_clickhouse_event(
|
||||
self: &Arc<Self>,
|
||||
event: ClickhouseEvent,
|
||||
telemetry_settings: TelemetrySettings,
|
||||
@@ -275,6 +398,7 @@ impl Telemetry {
|
||||
|
||||
fn flush_clickhouse_events(self: &Arc<Self>) {
|
||||
let mut state = self.state.lock();
|
||||
state.first_event_datetime = None;
|
||||
let mut events = mem::take(&mut state.clickhouse_events_queue);
|
||||
state.flush_clickhouse_events_task.take();
|
||||
drop(state);
|
||||
|
||||
@@ -12,6 +12,7 @@ doctest = false
|
||||
test-support = ["collections/test-support", "gpui/test-support", "rpc/test-support"]
|
||||
|
||||
[dependencies]
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
collections = { path = "../collections" }
|
||||
db = { package = "db2", path = "../db2" }
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
|
||||
@@ -923,9 +923,17 @@ impl Client {
|
||||
self.establish_websocket_connection(credentials, cx)
|
||||
}
|
||||
|
||||
async fn get_rpc_url(http: Arc<dyn HttpClient>, is_preview: bool) -> Result<Url> {
|
||||
let preview_param = if is_preview { "?preview=1" } else { "" };
|
||||
let url = format!("{}/rpc{preview_param}", *ZED_SERVER_URL);
|
||||
async fn get_rpc_url(
|
||||
http: Arc<dyn HttpClient>,
|
||||
release_channel: Option<ReleaseChannel>,
|
||||
) -> Result<Url> {
|
||||
let mut url = format!("{}/rpc", *ZED_SERVER_URL);
|
||||
if let Some(preview_param) =
|
||||
release_channel.and_then(|channel| channel.release_query_param())
|
||||
{
|
||||
url += "?";
|
||||
url += preview_param;
|
||||
}
|
||||
let response = http.get(&url, Default::default(), false).await?;
|
||||
|
||||
// Normally, ZED_SERVER_URL is set to the URL of zed.dev website.
|
||||
@@ -960,9 +968,7 @@ impl Client {
|
||||
credentials: &Credentials,
|
||||
cx: &AsyncAppContext,
|
||||
) -> Task<Result<Connection, EstablishConnectionError>> {
|
||||
let use_preview_server = cx
|
||||
.try_read_global(|channel: &ReleaseChannel, _| *channel != ReleaseChannel::Stable)
|
||||
.unwrap_or(false);
|
||||
let release_channel = cx.try_read_global(|channel: &ReleaseChannel, _| *channel);
|
||||
|
||||
let request = Request::builder()
|
||||
.header(
|
||||
@@ -973,7 +979,7 @@ impl Client {
|
||||
|
||||
let http = self.http.clone();
|
||||
cx.background_executor().spawn(async move {
|
||||
let mut rpc_url = Self::get_rpc_url(http, use_preview_server).await?;
|
||||
let mut rpc_url = Self::get_rpc_url(http, release_channel).await?;
|
||||
let rpc_host = rpc_url
|
||||
.host_str()
|
||||
.zip(rpc_url.port_or_known_default())
|
||||
@@ -1120,7 +1126,7 @@ impl Client {
|
||||
|
||||
// Use the collab server's admin API to retrieve the id
|
||||
// of the impersonated user.
|
||||
let mut url = Self::get_rpc_url(http.clone(), false).await?;
|
||||
let mut url = Self::get_rpc_url(http.clone(), None).await?;
|
||||
url.set_path("/user");
|
||||
url.set_query(Some(&format!("github_login={login}")));
|
||||
let request = Request::get(url.as_str())
|
||||
|
||||
+138
-14
@@ -1,4 +1,5 @@
|
||||
use crate::{TelemetrySettings, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
|
||||
use chrono::{DateTime, Utc};
|
||||
use gpui::{serde_json, AppContext, AppMetadata, BackgroundExecutor, Task};
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::Mutex;
|
||||
@@ -20,7 +21,7 @@ pub struct Telemetry {
|
||||
|
||||
struct TelemetryState {
|
||||
metrics_id: Option<Arc<str>>, // Per logged-in user
|
||||
installation_id: Option<Arc<str>>, // Per app installation (different for dev, preview, and stable)
|
||||
installation_id: Option<Arc<str>>, // Per app installation (different for dev, nightly, preview, and stable)
|
||||
session_id: Option<Arc<str>>, // Per app launch
|
||||
release_channel: Option<&'static str>,
|
||||
app_metadata: AppMetadata,
|
||||
@@ -29,6 +30,7 @@ struct TelemetryState {
|
||||
flush_clickhouse_events_task: Option<Task<()>>,
|
||||
log_file: Option<NamedTempFile>,
|
||||
is_staff: Option<bool>,
|
||||
first_event_datetime: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
const CLICKHOUSE_EVENTS_URL_PATH: &'static str = "/api/events";
|
||||
@@ -75,29 +77,35 @@ pub enum ClickhouseEvent {
|
||||
vim_mode: bool,
|
||||
copilot_enabled: bool,
|
||||
copilot_enabled_for_language: bool,
|
||||
milliseconds_since_first_event: i64,
|
||||
},
|
||||
Copilot {
|
||||
suggestion_id: Option<String>,
|
||||
suggestion_accepted: bool,
|
||||
file_extension: Option<String>,
|
||||
milliseconds_since_first_event: i64,
|
||||
},
|
||||
Call {
|
||||
operation: &'static str,
|
||||
room_id: Option<u64>,
|
||||
channel_id: Option<u64>,
|
||||
milliseconds_since_first_event: i64,
|
||||
},
|
||||
Assistant {
|
||||
conversation_id: Option<String>,
|
||||
kind: AssistantKind,
|
||||
model: &'static str,
|
||||
milliseconds_since_first_event: i64,
|
||||
},
|
||||
Cpu {
|
||||
usage_as_percentage: f32,
|
||||
core_count: u32,
|
||||
milliseconds_since_first_event: i64,
|
||||
},
|
||||
Memory {
|
||||
memory_in_bytes: u64,
|
||||
virtual_memory_in_bytes: u64,
|
||||
milliseconds_since_first_event: i64,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -135,6 +143,7 @@ impl Telemetry {
|
||||
flush_clickhouse_events_task: Default::default(),
|
||||
log_file: None,
|
||||
is_staff: None,
|
||||
first_event_datetime: None,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -190,16 +199,6 @@ impl Telemetry {
|
||||
return;
|
||||
};
|
||||
|
||||
let memory_event = ClickhouseEvent::Memory {
|
||||
memory_in_bytes: process.memory(),
|
||||
virtual_memory_in_bytes: process.virtual_memory(),
|
||||
};
|
||||
|
||||
let cpu_event = ClickhouseEvent::Cpu {
|
||||
usage_as_percentage: process.cpu_usage(),
|
||||
core_count: system.cpus().len() as u32,
|
||||
};
|
||||
|
||||
let telemetry_settings = if let Ok(telemetry_settings) =
|
||||
cx.update(|cx| *TelemetrySettings::get_global(cx))
|
||||
{
|
||||
@@ -208,8 +207,16 @@ impl Telemetry {
|
||||
break;
|
||||
};
|
||||
|
||||
this.report_clickhouse_event(memory_event, telemetry_settings);
|
||||
this.report_clickhouse_event(cpu_event, telemetry_settings);
|
||||
this.report_memory_event(
|
||||
telemetry_settings,
|
||||
process.memory(),
|
||||
process.virtual_memory(),
|
||||
);
|
||||
this.report_cpu_event(
|
||||
telemetry_settings,
|
||||
process.cpu_usage(),
|
||||
system.cpus().len() as u32,
|
||||
);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
@@ -232,7 +239,123 @@ impl Telemetry {
|
||||
drop(state);
|
||||
}
|
||||
|
||||
pub fn report_clickhouse_event(
|
||||
pub fn report_editor_event(
|
||||
self: &Arc<Self>,
|
||||
telemetry_settings: TelemetrySettings,
|
||||
file_extension: Option<String>,
|
||||
vim_mode: bool,
|
||||
operation: &'static str,
|
||||
copilot_enabled: bool,
|
||||
copilot_enabled_for_language: bool,
|
||||
) {
|
||||
let event = ClickhouseEvent::Editor {
|
||||
file_extension,
|
||||
vim_mode,
|
||||
operation,
|
||||
copilot_enabled,
|
||||
copilot_enabled_for_language,
|
||||
milliseconds_since_first_event: self.milliseconds_since_first_event(),
|
||||
};
|
||||
|
||||
self.report_clickhouse_event(event, telemetry_settings)
|
||||
}
|
||||
|
||||
pub fn report_copilot_event(
|
||||
self: &Arc<Self>,
|
||||
telemetry_settings: TelemetrySettings,
|
||||
suggestion_id: Option<String>,
|
||||
suggestion_accepted: bool,
|
||||
file_extension: Option<String>,
|
||||
) {
|
||||
let event = ClickhouseEvent::Copilot {
|
||||
suggestion_id,
|
||||
suggestion_accepted,
|
||||
file_extension,
|
||||
milliseconds_since_first_event: self.milliseconds_since_first_event(),
|
||||
};
|
||||
|
||||
self.report_clickhouse_event(event, telemetry_settings)
|
||||
}
|
||||
|
||||
pub fn report_assistant_event(
|
||||
self: &Arc<Self>,
|
||||
telemetry_settings: TelemetrySettings,
|
||||
conversation_id: Option<String>,
|
||||
kind: AssistantKind,
|
||||
model: &'static str,
|
||||
) {
|
||||
let event = ClickhouseEvent::Assistant {
|
||||
conversation_id,
|
||||
kind,
|
||||
model,
|
||||
milliseconds_since_first_event: self.milliseconds_since_first_event(),
|
||||
};
|
||||
|
||||
self.report_clickhouse_event(event, telemetry_settings)
|
||||
}
|
||||
|
||||
pub fn report_call_event(
|
||||
self: &Arc<Self>,
|
||||
telemetry_settings: TelemetrySettings,
|
||||
operation: &'static str,
|
||||
room_id: Option<u64>,
|
||||
channel_id: Option<u64>,
|
||||
) {
|
||||
let event = ClickhouseEvent::Call {
|
||||
operation,
|
||||
room_id,
|
||||
channel_id,
|
||||
milliseconds_since_first_event: self.milliseconds_since_first_event(),
|
||||
};
|
||||
|
||||
self.report_clickhouse_event(event, telemetry_settings)
|
||||
}
|
||||
|
||||
pub fn report_cpu_event(
|
||||
self: &Arc<Self>,
|
||||
telemetry_settings: TelemetrySettings,
|
||||
usage_as_percentage: f32,
|
||||
core_count: u32,
|
||||
) {
|
||||
let event = ClickhouseEvent::Cpu {
|
||||
usage_as_percentage,
|
||||
core_count,
|
||||
milliseconds_since_first_event: self.milliseconds_since_first_event(),
|
||||
};
|
||||
|
||||
self.report_clickhouse_event(event, telemetry_settings)
|
||||
}
|
||||
|
||||
pub fn report_memory_event(
|
||||
self: &Arc<Self>,
|
||||
telemetry_settings: TelemetrySettings,
|
||||
memory_in_bytes: u64,
|
||||
virtual_memory_in_bytes: u64,
|
||||
) {
|
||||
let event = ClickhouseEvent::Memory {
|
||||
memory_in_bytes,
|
||||
virtual_memory_in_bytes,
|
||||
milliseconds_since_first_event: self.milliseconds_since_first_event(),
|
||||
};
|
||||
|
||||
self.report_clickhouse_event(event, telemetry_settings)
|
||||
}
|
||||
|
||||
fn milliseconds_since_first_event(&self) -> i64 {
|
||||
let mut state = self.state.lock();
|
||||
match state.first_event_datetime {
|
||||
Some(first_event_datetime) => {
|
||||
let now: DateTime<Utc> = Utc::now();
|
||||
now.timestamp_millis() - first_event_datetime.timestamp_millis()
|
||||
}
|
||||
None => {
|
||||
state.first_event_datetime = Some(Utc::now());
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn report_clickhouse_event(
|
||||
self: &Arc<Self>,
|
||||
event: ClickhouseEvent,
|
||||
telemetry_settings: TelemetrySettings,
|
||||
@@ -276,6 +399,7 @@ impl Telemetry {
|
||||
|
||||
fn flush_clickhouse_events(self: &Arc<Self>) {
|
||||
let mut state = self.state.lock();
|
||||
state.first_event_datetime = None;
|
||||
let mut events = mem::take(&mut state.clickhouse_events_queue);
|
||||
state.flush_clickhouse_events_task.take();
|
||||
drop(state);
|
||||
|
||||
@@ -5052,7 +5052,7 @@ async fn test_project_search(
|
||||
let mut results = HashMap::default();
|
||||
let mut search_rx = project_b.update(cx_b, |project, cx| {
|
||||
project.search(
|
||||
SearchQuery::text("world", false, false, Vec::new(), Vec::new()).unwrap(),
|
||||
SearchQuery::text("world", false, false, false, Vec::new(), Vec::new()).unwrap(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
@@ -869,7 +869,8 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
|
||||
let mut search = project.update(cx, |project, cx| {
|
||||
project.search(
|
||||
SearchQuery::text(query, false, false, Vec::new(), Vec::new()).unwrap(),
|
||||
SearchQuery::text(query, false, false, false, Vec::new(), Vec::new())
|
||||
.unwrap(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
@@ -4599,7 +4599,7 @@ async fn test_project_search(
|
||||
let mut results = HashMap::default();
|
||||
let mut search_rx = project_b.update(cx_b, |project, cx| {
|
||||
project.search(
|
||||
SearchQuery::text("world", false, false, Vec::new(), Vec::new()).unwrap(),
|
||||
SearchQuery::text("world", false, false, false, Vec::new(), Vec::new()).unwrap(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
@@ -870,7 +870,8 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
|
||||
let mut search = project.update(cx, |project, cx| {
|
||||
project.search(
|
||||
SearchQuery::text(query, false, false, Vec::new(), Vec::new()).unwrap(),
|
||||
SearchQuery::text(query, false, false, false, Vec::new(), Vec::new())
|
||||
.unwrap(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
@@ -14,14 +14,8 @@ use std::{sync::Arc, time::Duration};
|
||||
const MENTIONS_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(50);
|
||||
|
||||
lazy_static! {
|
||||
static ref MENTIONS_SEARCH: SearchQuery = SearchQuery::regex(
|
||||
"@[-_\\w]+",
|
||||
false,
|
||||
false,
|
||||
Default::default(),
|
||||
Default::default()
|
||||
)
|
||||
.unwrap();
|
||||
static ref MENTIONS_SEARCH: SearchQuery =
|
||||
SearchQuery::regex("@[-_\\w]+", false, false, false, Vec::new(), Vec::new()).unwrap();
|
||||
}
|
||||
|
||||
pub struct MessageEditor {
|
||||
|
||||
@@ -33,7 +33,7 @@ collections = { path = "../collections" }
|
||||
# drag_and_drop = { path = "../drag_and_drop" }
|
||||
editor = { package="editor2", path = "../editor2" }
|
||||
#feedback = { path = "../feedback" }
|
||||
fuzzy = { path = "../fuzzy" }
|
||||
fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
language = { package = "language2", path = "../language2" }
|
||||
menu = { package = "menu2", path = "../menu2" }
|
||||
|
||||
@@ -14,14 +14,8 @@ use std::{sync::Arc, time::Duration};
|
||||
const MENTIONS_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(50);
|
||||
|
||||
lazy_static! {
|
||||
static ref MENTIONS_SEARCH: SearchQuery = SearchQuery::regex(
|
||||
"@[-_\\w]+",
|
||||
false,
|
||||
false,
|
||||
Default::default(),
|
||||
Default::default()
|
||||
)
|
||||
.unwrap();
|
||||
static ref MENTIONS_SEARCH: SearchQuery =
|
||||
SearchQuery::regex("@[-_\\w]+", false, false, false, Vec::new(), Vec::new()).unwrap();
|
||||
}
|
||||
|
||||
pub struct MessageEditor {
|
||||
|
||||
@@ -160,7 +160,7 @@ use std::sync::Arc;
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use gpui::{
|
||||
actions, div, serde_json, AppContext, AsyncWindowContext, Div, EventEmitter, FocusHandle,
|
||||
Focusable, FocusableView, InteractiveComponent, ParentComponent, Render, View, ViewContext,
|
||||
Focusable, FocusableView, InteractiveElement, ParentElement, Render, View, ViewContext,
|
||||
VisualContext, WeakView,
|
||||
};
|
||||
use project::Fs;
|
||||
@@ -3295,7 +3295,7 @@ impl CollabPanel {
|
||||
// }
|
||||
|
||||
impl Render for CollabPanel {
|
||||
type Element = Focusable<Self, Div<Self>>;
|
||||
type Element = Focusable<Div>;
|
||||
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
div()
|
||||
|
||||
@@ -31,13 +31,13 @@ use std::sync::Arc;
|
||||
use call::ActiveCall;
|
||||
use client::{Client, UserStore};
|
||||
use gpui::{
|
||||
div, px, rems, AppContext, Component, Div, InteractiveComponent, Model, ParentComponent,
|
||||
Render, Stateful, StatefulInteractiveComponent, Styled, Subscription, ViewContext,
|
||||
VisualContext, WeakView, WindowBounds,
|
||||
div, px, rems, AppContext, Div, InteractiveElement, Model, ParentElement, Render, RenderOnce,
|
||||
Stateful, StatefulInteractiveElement, Styled, Subscription, ViewContext, VisualContext,
|
||||
WeakView, WindowBounds,
|
||||
};
|
||||
use project::Project;
|
||||
use theme::ActiveTheme;
|
||||
use ui::{h_stack, Button, ButtonVariant, KeyBinding, Label, TextColor, Tooltip};
|
||||
use ui::{h_stack, Button, ButtonVariant, Color, KeyBinding, Label, Tooltip};
|
||||
use workspace::Workspace;
|
||||
|
||||
// const MAX_PROJECT_NAME_LENGTH: usize = 40;
|
||||
@@ -82,7 +82,7 @@ pub struct CollabTitlebarItem {
|
||||
}
|
||||
|
||||
impl Render for CollabTitlebarItem {
|
||||
type Element = Stateful<Self, Div<Self>>;
|
||||
type Element = Stateful<Div>;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
h_stack()
|
||||
@@ -100,7 +100,7 @@ impl Render for CollabTitlebarItem {
|
||||
|s| s.pl(px(68.)),
|
||||
)
|
||||
.bg(cx.theme().colors().title_bar_background)
|
||||
.on_click(|_, event, cx| {
|
||||
.on_click(|event, cx| {
|
||||
if event.up.click_count == 2 {
|
||||
cx.zoom_window();
|
||||
}
|
||||
@@ -115,16 +115,16 @@ impl Render for CollabTitlebarItem {
|
||||
.child(
|
||||
Button::new("player")
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.color(Some(TextColor::Player(0))),
|
||||
.color(Some(Color::Player(0))),
|
||||
)
|
||||
.tooltip(move |_, cx| Tooltip::text("Toggle following", cx)),
|
||||
.tooltip(move |cx| Tooltip::text("Toggle following", cx)),
|
||||
)
|
||||
// TODO - Add project menu
|
||||
.child(
|
||||
div()
|
||||
.id("titlebar_project_menu_button")
|
||||
.child(Button::new("project_name").variant(ButtonVariant::Ghost))
|
||||
.tooltip(move |_, cx| Tooltip::text("Recent Projects", cx)),
|
||||
.tooltip(move |cx| Tooltip::text("Recent Projects", cx)),
|
||||
)
|
||||
// TODO - Add git menu
|
||||
.child(
|
||||
@@ -133,9 +133,9 @@ impl Render for CollabTitlebarItem {
|
||||
.child(
|
||||
Button::new("branch_name")
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.color(Some(TextColor::Muted)),
|
||||
.color(Some(Color::Muted)),
|
||||
)
|
||||
.tooltip(move |_, cx| {
|
||||
.tooltip(move |cx| {
|
||||
cx.build_view(|_| {
|
||||
Tooltip::new("Recent Branches")
|
||||
.key_binding(KeyBinding::new(gpui::KeyBinding::new(
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use collections::{CommandPaletteFilter, HashMap};
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
actions, div, prelude::*, Action, AppContext, Component, Dismiss, Div, FocusHandle, Keystroke,
|
||||
ManagedView, ParentComponent, Render, Styled, View, ViewContext, VisualContext, WeakView,
|
||||
actions, div, prelude::*, Action, AppContext, Div, EventEmitter, FocusHandle, FocusableView,
|
||||
Keystroke, Manager, ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use std::{
|
||||
@@ -68,14 +68,16 @@ impl CommandPalette {
|
||||
}
|
||||
}
|
||||
|
||||
impl ManagedView for CommandPalette {
|
||||
impl EventEmitter<Manager> for CommandPalette {}
|
||||
|
||||
impl FocusableView for CommandPalette {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
self.picker.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for CommandPalette {
|
||||
type Element = Div<Self>;
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
v_stack().w_96().child(self.picker.clone())
|
||||
@@ -114,6 +116,7 @@ impl Clone for Command {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Hit count for each command in the palette.
|
||||
/// We only account for commands triggered directly via command palette and not by e.g. keystrokes because
|
||||
/// if an user already knows a keystroke for a command, they are unlikely to use a command palette to look for it.
|
||||
@@ -137,7 +140,7 @@ impl CommandPaletteDelegate {
|
||||
}
|
||||
|
||||
impl PickerDelegate for CommandPaletteDelegate {
|
||||
type ListItem = Div<Picker<Self>>;
|
||||
type ListItem = Div;
|
||||
|
||||
fn placeholder_text(&self) -> Arc<str> {
|
||||
"Execute a command...".into()
|
||||
@@ -265,7 +268,7 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
|
||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.command_palette
|
||||
.update(cx, |_, cx| cx.emit(Dismiss))
|
||||
.update(cx, |_, cx| cx.emit(Manager::Dismiss))
|
||||
.log_err();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
[package]
|
||||
name = "diagnostics2"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/diagnostics.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
collections = { path = "../collections" }
|
||||
editor = { package = "editor2", path = "../editor2" }
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
ui = { package = "ui2", path = "../ui2" }
|
||||
language = { package = "language2", path = "../language2" }
|
||||
lsp = { package = "lsp2", path = "../lsp2" }
|
||||
project = { package = "project2", path = "../project2" }
|
||||
settings = { package = "settings2", path = "../settings2" }
|
||||
theme = { package = "theme2", path = "../theme2" }
|
||||
util = { path = "../util" }
|
||||
workspace = { package = "workspace2", path = "../workspace2" }
|
||||
|
||||
log.workspace = true
|
||||
anyhow.workspace = true
|
||||
futures.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
smallvec.workspace = true
|
||||
postage.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
client = { package = "client2", path = "../client2", features = ["test-support"] }
|
||||
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
|
||||
language = { package = "language2", path = "../language2", features = ["test-support"] }
|
||||
lsp = { package = "lsp2", path = "../lsp2", features = ["test-support"] }
|
||||
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
|
||||
workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
|
||||
theme = { package = "theme2", path = "../theme2", features = ["test-support"] }
|
||||
|
||||
serde_json.workspace = true
|
||||
unindent.workspace = true
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,151 @@
|
||||
use collections::HashSet;
|
||||
use editor::{Editor, GoToDiagnostic};
|
||||
use gpui::{
|
||||
rems, Div, EventEmitter, InteractiveElement, ParentElement, Render, Stateful,
|
||||
StatefulInteractiveElement, Styled, Subscription, View, ViewContext, WeakView,
|
||||
};
|
||||
use language::Diagnostic;
|
||||
use lsp::LanguageServerId;
|
||||
use theme::ActiveTheme;
|
||||
use ui::{h_stack, Color, Icon, IconElement, Label, Tooltip};
|
||||
use workspace::{item::ItemHandle, StatusItemView, ToolbarItemEvent, Workspace};
|
||||
|
||||
use crate::ProjectDiagnosticsEditor;
|
||||
|
||||
pub struct DiagnosticIndicator {
|
||||
summary: project::DiagnosticSummary,
|
||||
active_editor: Option<WeakView<Editor>>,
|
||||
workspace: WeakView<Workspace>,
|
||||
current_diagnostic: Option<Diagnostic>,
|
||||
in_progress_checks: HashSet<LanguageServerId>,
|
||||
_observe_active_editor: Option<Subscription>,
|
||||
}
|
||||
|
||||
impl Render for DiagnosticIndicator {
|
||||
type Element = Stateful<Div>;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
let diagnostic_indicator = match (self.summary.error_count, self.summary.warning_count) {
|
||||
(0, 0) => h_stack().child(IconElement::new(Icon::Check).color(Color::Success)),
|
||||
(0, warning_count) => h_stack()
|
||||
.gap_1()
|
||||
.child(IconElement::new(Icon::ExclamationTriangle).color(Color::Warning))
|
||||
.child(Label::new(warning_count.to_string())),
|
||||
(error_count, 0) => h_stack()
|
||||
.gap_1()
|
||||
.child(IconElement::new(Icon::XCircle).color(Color::Error))
|
||||
.child(Label::new(error_count.to_string())),
|
||||
(error_count, warning_count) => h_stack()
|
||||
.gap_1()
|
||||
.child(IconElement::new(Icon::XCircle).color(Color::Error))
|
||||
.child(Label::new(error_count.to_string()))
|
||||
.child(IconElement::new(Icon::ExclamationTriangle).color(Color::Warning))
|
||||
.child(Label::new(warning_count.to_string())),
|
||||
};
|
||||
|
||||
h_stack()
|
||||
.id(cx.entity_id())
|
||||
.on_action(cx.listener(Self::go_to_next_diagnostic))
|
||||
.rounded_md()
|
||||
.flex_none()
|
||||
.h(rems(1.375))
|
||||
.px_1()
|
||||
.cursor_pointer()
|
||||
.bg(cx.theme().colors().ghost_element_background)
|
||||
.hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
|
||||
.active(|style| style.bg(cx.theme().colors().ghost_element_active))
|
||||
.tooltip(|cx| Tooltip::text("Project Diagnostics", cx))
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
if let Some(workspace) = this.workspace.upgrade() {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
ProjectDiagnosticsEditor::deploy(workspace, &Default::default(), cx)
|
||||
})
|
||||
}
|
||||
}))
|
||||
.child(diagnostic_indicator)
|
||||
}
|
||||
}
|
||||
|
||||
impl DiagnosticIndicator {
|
||||
pub fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
|
||||
let project = workspace.project();
|
||||
cx.subscribe(project, |this, project, event, cx| match event {
|
||||
project::Event::DiskBasedDiagnosticsStarted { language_server_id } => {
|
||||
this.in_progress_checks.insert(*language_server_id);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
project::Event::DiskBasedDiagnosticsFinished { language_server_id }
|
||||
| project::Event::LanguageServerRemoved(language_server_id) => {
|
||||
this.summary = project.read(cx).diagnostic_summary(cx);
|
||||
this.in_progress_checks.remove(language_server_id);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
project::Event::DiagnosticsUpdated { .. } => {
|
||||
this.summary = project.read(cx).diagnostic_summary(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
_ => {}
|
||||
})
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
summary: project.read(cx).diagnostic_summary(cx),
|
||||
in_progress_checks: project
|
||||
.read(cx)
|
||||
.language_servers_running_disk_based_diagnostics()
|
||||
.collect(),
|
||||
active_editor: None,
|
||||
workspace: workspace.weak_handle(),
|
||||
current_diagnostic: None,
|
||||
_observe_active_editor: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn go_to_next_diagnostic(&mut self, _: &GoToDiagnostic, cx: &mut ViewContext<Self>) {
|
||||
if let Some(editor) = self.active_editor.as_ref().and_then(|e| e.upgrade()) {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.go_to_diagnostic_impl(editor::Direction::Next, cx);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
|
||||
let editor = editor.read(cx);
|
||||
let buffer = editor.buffer().read(cx);
|
||||
let cursor_position = editor.selections.newest::<usize>(cx).head();
|
||||
let new_diagnostic = buffer
|
||||
.snapshot(cx)
|
||||
.diagnostics_in_range::<_, usize>(cursor_position..cursor_position, false)
|
||||
.filter(|entry| !entry.range.is_empty())
|
||||
.min_by_key(|entry| (entry.diagnostic.severity, entry.range.len()))
|
||||
.map(|entry| entry.diagnostic);
|
||||
if new_diagnostic != self.current_diagnostic {
|
||||
self.current_diagnostic = new_diagnostic;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<ToolbarItemEvent> for DiagnosticIndicator {}
|
||||
|
||||
impl StatusItemView for DiagnosticIndicator {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
active_pane_item: Option<&dyn ItemHandle>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
|
||||
self.active_editor = Some(editor.downgrade());
|
||||
self._observe_active_editor = Some(cx.observe(&editor, Self::update));
|
||||
self.update(editor, cx);
|
||||
} else {
|
||||
self.active_editor = None;
|
||||
self.current_diagnostic = None;
|
||||
self._observe_active_editor = None;
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct ProjectDiagnosticsSettings {
|
||||
pub include_warnings: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
pub struct ProjectDiagnosticsSettingsContent {
|
||||
include_warnings: Option<bool>,
|
||||
}
|
||||
|
||||
impl settings::Settings for ProjectDiagnosticsSettings {
|
||||
const KEY: Option<&'static str> = Some("diagnostics");
|
||||
type FileContent = ProjectDiagnosticsSettingsContent;
|
||||
|
||||
fn load(
|
||||
default_value: &Self::FileContent,
|
||||
user_values: &[&Self::FileContent],
|
||||
_cx: &mut gpui::AppContext,
|
||||
) -> anyhow::Result<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Self::load_via_json_merge(default_value, user_values)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
use crate::ProjectDiagnosticsEditor;
|
||||
use gpui::{div, Div, EventEmitter, ParentElement, Render, ViewContext, WeakView};
|
||||
use ui::{Icon, IconButton, Tooltip};
|
||||
use workspace::{item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
|
||||
|
||||
pub struct ToolbarControls {
|
||||
editor: Option<WeakView<ProjectDiagnosticsEditor>>,
|
||||
}
|
||||
|
||||
impl Render for ToolbarControls {
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
let include_warnings = self
|
||||
.editor
|
||||
.as_ref()
|
||||
.and_then(|editor| editor.upgrade())
|
||||
.map(|editor| editor.read(cx).include_warnings)
|
||||
.unwrap_or(false);
|
||||
|
||||
let tooltip = if include_warnings {
|
||||
"Exclude Warnings"
|
||||
} else {
|
||||
"Include Warnings"
|
||||
};
|
||||
|
||||
div().child(
|
||||
IconButton::new("toggle-warnings", Icon::ExclamationTriangle)
|
||||
.tooltip(move |cx| Tooltip::text(tooltip, cx))
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
if let Some(editor) = this.editor.as_ref().and_then(|editor| editor.upgrade()) {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.toggle_warnings(&Default::default(), cx);
|
||||
});
|
||||
}
|
||||
})),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<ToolbarItemEvent> for ToolbarControls {}
|
||||
|
||||
impl ToolbarItemView for ToolbarControls {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
active_pane_item: Option<&dyn ItemHandle>,
|
||||
_: &mut ViewContext<Self>,
|
||||
) -> ToolbarItemLocation {
|
||||
if let Some(pane_item) = active_pane_item.as_ref() {
|
||||
if let Some(editor) = pane_item.downcast::<ProjectDiagnosticsEditor>() {
|
||||
self.editor = Some(editor.downgrade());
|
||||
ToolbarItemLocation::PrimaryRight
|
||||
} else {
|
||||
ToolbarItemLocation::Hidden
|
||||
}
|
||||
} else {
|
||||
ToolbarItemLocation::Hidden
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolbarControls {
|
||||
pub fn new() -> Self {
|
||||
ToolbarControls { editor: None }
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ use ::git::diff::DiffHunk;
|
||||
use aho_corasick::AhoCorasick;
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use blink_manager::BlinkManager;
|
||||
use client::{ClickhouseEvent, Client, Collaborator, ParticipantIndex, TelemetrySettings};
|
||||
use client::{Client, Collaborator, ParticipantIndex, TelemetrySettings};
|
||||
use clock::{Global, ReplicaId};
|
||||
use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
|
||||
use convert_case::{Case, Casing};
|
||||
@@ -8946,12 +8946,12 @@ impl Editor {
|
||||
let telemetry = project.read(cx).client().telemetry().clone();
|
||||
let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
|
||||
|
||||
let event = ClickhouseEvent::Copilot {
|
||||
telemetry.report_copilot_event(
|
||||
telemetry_settings,
|
||||
suggestion_id,
|
||||
suggestion_accepted,
|
||||
file_extension,
|
||||
};
|
||||
telemetry.report_clickhouse_event(event, telemetry_settings);
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
@@ -8998,14 +8998,14 @@ impl Editor {
|
||||
.show_copilot_suggestions;
|
||||
|
||||
let telemetry = project.read(cx).client().telemetry().clone();
|
||||
let event = ClickhouseEvent::Editor {
|
||||
telemetry.report_editor_event(
|
||||
telemetry_settings,
|
||||
file_extension,
|
||||
vim_mode,
|
||||
operation,
|
||||
copilot_enabled,
|
||||
copilot_enabled_for_language,
|
||||
};
|
||||
telemetry.report_clickhouse_event(event, telemetry_settings)
|
||||
)
|
||||
}
|
||||
|
||||
/// Copy the highlighted chunks to the clipboard as JSON. The format is an array of lines,
|
||||
|
||||
@@ -50,7 +50,7 @@ struct BlockRow(u32);
|
||||
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
|
||||
struct WrapRow(u32);
|
||||
|
||||
pub type RenderBlock = Arc<dyn Fn(&mut BlockContext) -> AnyElement<Editor>>;
|
||||
pub type RenderBlock = Arc<dyn Fn(&mut BlockContext) -> AnyElement>;
|
||||
|
||||
pub struct Block {
|
||||
id: BlockId,
|
||||
@@ -69,7 +69,7 @@ where
|
||||
pub position: P,
|
||||
pub height: u8,
|
||||
pub style: BlockStyle,
|
||||
pub render: Arc<dyn Fn(&mut BlockContext) -> AnyElement<Editor>>,
|
||||
pub render: Arc<dyn Fn(&mut BlockContext) -> AnyElement>,
|
||||
pub disposition: BlockDisposition,
|
||||
}
|
||||
|
||||
@@ -947,7 +947,7 @@ impl DerefMut for BlockContext<'_, '_> {
|
||||
}
|
||||
|
||||
impl Block {
|
||||
pub fn render(&self, cx: &mut BlockContext) -> AnyElement<Editor> {
|
||||
pub fn render(&self, cx: &mut BlockContext) -> AnyElement {
|
||||
self.render.lock()(cx)
|
||||
}
|
||||
|
||||
|
||||
+190
-173
@@ -24,7 +24,7 @@ use ::git::diff::DiffHunk;
|
||||
use aho_corasick::AhoCorasick;
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use blink_manager::BlinkManager;
|
||||
use client::{ClickhouseEvent, Client, Collaborator, ParticipantIndex, TelemetrySettings};
|
||||
use client::{Client, Collaborator, ParticipantIndex, TelemetrySettings};
|
||||
use clock::ReplicaId;
|
||||
use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
|
||||
use convert_case::{Case, Casing};
|
||||
@@ -42,9 +42,9 @@ use gpui::{
|
||||
actions, div, point, prelude::*, px, relative, rems, size, uniform_list, Action, AnyElement,
|
||||
AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Component, Context,
|
||||
EventEmitter, FocusHandle, FocusableView, FontFeatures, FontStyle, FontWeight, HighlightStyle,
|
||||
Hsla, InputHandler, KeyContext, Model, MouseButton, ParentComponent, Pixels, Render, Styled,
|
||||
Subscription, Task, TextStyle, UniformListScrollHandle, View, ViewContext, VisualContext,
|
||||
WeakView, WindowContext,
|
||||
Hsla, InputHandler, KeyContext, Model, MouseButton, ParentElement, Pixels, Render,
|
||||
SharedString, Styled, Subscription, Task, TextStyle, UniformListScrollHandle, View,
|
||||
ViewContext, VisualContext, WeakView, WindowContext,
|
||||
};
|
||||
use highlight_matching_bracket::refresh_matching_bracket_highlights;
|
||||
use hover_popover::{hide_hover, HoverState};
|
||||
@@ -585,7 +585,7 @@ pub enum SoftWrap {
|
||||
Column(u32),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Default)]
|
||||
pub struct EditorStyle {
|
||||
pub background: Hsla,
|
||||
pub local_player: PlayerColor,
|
||||
@@ -907,7 +907,7 @@ impl ContextMenu {
|
||||
style: &EditorStyle,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> (DisplayPoint, AnyElement<Editor>) {
|
||||
) -> (DisplayPoint, AnyElement) {
|
||||
match self {
|
||||
ContextMenu::Completions(menu) => (cursor_position, menu.render(style, workspace, cx)),
|
||||
ContextMenu::CodeActions(menu) => menu.render(cursor_position, style, cx),
|
||||
@@ -1223,9 +1223,7 @@ impl CompletionsMenu {
|
||||
style: &EditorStyle,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> AnyElement<Editor> {
|
||||
// enum CompletionTag {}
|
||||
|
||||
) -> AnyElement {
|
||||
let settings = EditorSettings::get_global(cx);
|
||||
let show_completion_documentation = settings.show_completion_documentation;
|
||||
|
||||
@@ -1253,121 +1251,126 @@ impl CompletionsMenu {
|
||||
let matches = self.matches.clone();
|
||||
let selected_item = self.selected_item;
|
||||
|
||||
let list = uniform_list("completions", matches.len(), move |editor, range, cx| {
|
||||
let start_ix = range.start;
|
||||
let completions_guard = completions.read();
|
||||
let list = uniform_list(
|
||||
cx.view().clone(),
|
||||
"completions",
|
||||
matches.len(),
|
||||
move |editor, range, cx| {
|
||||
let start_ix = range.start;
|
||||
let completions_guard = completions.read();
|
||||
|
||||
matches[range]
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, mat)| {
|
||||
let item_ix = start_ix + ix;
|
||||
let candidate_id = mat.candidate_id;
|
||||
let completion = &completions_guard[candidate_id];
|
||||
matches[range]
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, mat)| {
|
||||
let item_ix = start_ix + ix;
|
||||
let candidate_id = mat.candidate_id;
|
||||
let completion = &completions_guard[candidate_id];
|
||||
|
||||
let documentation = if show_completion_documentation {
|
||||
&completion.documentation
|
||||
} else {
|
||||
&None
|
||||
};
|
||||
let documentation = if show_completion_documentation {
|
||||
&completion.documentation
|
||||
} else {
|
||||
&None
|
||||
};
|
||||
|
||||
// todo!("highlights")
|
||||
// let highlights = combine_syntax_and_fuzzy_match_highlights(
|
||||
// &completion.label.text,
|
||||
// style.text.color.into(),
|
||||
// styled_runs_for_code_label(&completion.label, &style.syntax),
|
||||
// &mat.positions,
|
||||
// )
|
||||
// todo!("highlights")
|
||||
// let highlights = combine_syntax_and_fuzzy_match_highlights(
|
||||
// &completion.label.text,
|
||||
// style.text.color.into(),
|
||||
// styled_runs_for_code_label(&completion.label, &style.syntax),
|
||||
// &mat.positions,
|
||||
// )
|
||||
|
||||
// todo!("documentation")
|
||||
// MouseEventHandler::new::<CompletionTag, _>(mat.candidate_id, cx, |state, _| {
|
||||
// let completion_label = HighlightedLabel::new(
|
||||
// completion.label.text.clone(),
|
||||
// combine_syntax_and_fuzzy_match_highlights(
|
||||
// &completion.label.text,
|
||||
// style.text.color.into(),
|
||||
// styled_runs_for_code_label(&completion.label, &style.syntax),
|
||||
// &mat.positions,
|
||||
// ),
|
||||
// );
|
||||
// Text::new(completion.label.text.clone(), style.text.clone())
|
||||
// .with_soft_wrap(false)
|
||||
// .with_highlights();
|
||||
// todo!("documentation")
|
||||
// MouseEventHandler::new::<CompletionTag, _>(mat.candidate_id, cx, |state, _| {
|
||||
// let completion_label = HighlightedLabel::new(
|
||||
// completion.label.text.clone(),
|
||||
// combine_syntax_and_fuzzy_match_highlights(
|
||||
// &completion.label.text,
|
||||
// style.text.color.into(),
|
||||
// styled_runs_for_code_label(&completion.label, &style.syntax),
|
||||
// &mat.positions,
|
||||
// ),
|
||||
// );
|
||||
// Text::new(completion.label.text.clone(), style.text.clone())
|
||||
// .with_soft_wrap(false)
|
||||
// .with_highlights();
|
||||
|
||||
// if let Some(Documentation::SingleLine(text)) = documentation {
|
||||
// h_stack()
|
||||
// .child(completion_label)
|
||||
// .with_children((|| {
|
||||
// let text_style = TextStyle {
|
||||
// color: style.autocomplete.inline_docs_color,
|
||||
// font_size: style.text.font_size
|
||||
// * style.autocomplete.inline_docs_size_percent,
|
||||
// ..style.text.clone()
|
||||
// };
|
||||
// if let Some(Documentation::SingleLine(text)) = documentation {
|
||||
// h_stack()
|
||||
// .child(completion_label)
|
||||
// .with_children((|| {
|
||||
// let text_style = TextStyle {
|
||||
// color: style.autocomplete.inline_docs_color,
|
||||
// font_size: style.text.font_size
|
||||
// * style.autocomplete.inline_docs_size_percent,
|
||||
// ..style.text.clone()
|
||||
// };
|
||||
|
||||
// let label = Text::new(text.clone(), text_style)
|
||||
// .aligned()
|
||||
// .constrained()
|
||||
// .dynamically(move |constraint, _, _| gpui::SizeConstraint {
|
||||
// min: constraint.min,
|
||||
// max: vec2f(constraint.max.x(), constraint.min.y()),
|
||||
// });
|
||||
// let label = Text::new(text.clone(), text_style)
|
||||
// .aligned()
|
||||
// .constrained()
|
||||
// .dynamically(move |constraint, _, _| gpui::SizeConstraint {
|
||||
// min: constraint.min,
|
||||
// max: vec2f(constraint.max.x(), constraint.min.y()),
|
||||
// });
|
||||
|
||||
// if Some(item_ix) == widest_completion_ix {
|
||||
// Some(
|
||||
// label
|
||||
// .contained()
|
||||
// .with_style(style.autocomplete.inline_docs_container)
|
||||
// .into_any(),
|
||||
// )
|
||||
// } else {
|
||||
// Some(label.flex_float().into_any())
|
||||
// }
|
||||
// })())
|
||||
// .into_any()
|
||||
// } else {
|
||||
// completion_label.into_any()
|
||||
// }
|
||||
// .contained()
|
||||
// .with_style(item_style)
|
||||
// .constrained()
|
||||
// .dynamically(move |constraint, _, _| {
|
||||
// if Some(item_ix) == widest_completion_ix {
|
||||
// constraint
|
||||
// } else {
|
||||
// gpui::SizeConstraint {
|
||||
// min: constraint.min,
|
||||
// max: constraint.min,
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
// })
|
||||
// .with_cursor_style(CursorStyle::PointingHand)
|
||||
// .on_down(MouseButton::Left, move |_, this, cx| {
|
||||
// this.confirm_completion(
|
||||
// &ConfirmCompletion {
|
||||
// item_ix: Some(item_ix),
|
||||
// },
|
||||
// cx,
|
||||
// )
|
||||
// .map(|task| task.detach());
|
||||
// })
|
||||
// .constrained()
|
||||
//
|
||||
div()
|
||||
.id(mat.candidate_id)
|
||||
.bg(gpui::green())
|
||||
.hover(|style| style.bg(gpui::blue()))
|
||||
.when(item_ix == selected_item, |div| div.bg(gpui::blue()))
|
||||
.child(completion.label.text.clone())
|
||||
.min_w(px(300.))
|
||||
.max_w(px(700.))
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
// if Some(item_ix) == widest_completion_ix {
|
||||
// Some(
|
||||
// label
|
||||
// .contained()
|
||||
// .with_style(style.autocomplete.inline_docs_container)
|
||||
// .into_any(),
|
||||
// )
|
||||
// } else {
|
||||
// Some(label.flex_float().into_any())
|
||||
// }
|
||||
// })())
|
||||
// .into_any()
|
||||
// } else {
|
||||
// completion_label.into_any()
|
||||
// }
|
||||
// .contained()
|
||||
// .with_style(item_style)
|
||||
// .constrained()
|
||||
// .dynamically(move |constraint, _, _| {
|
||||
// if Some(item_ix) == widest_completion_ix {
|
||||
// constraint
|
||||
// } else {
|
||||
// gpui::SizeConstraint {
|
||||
// min: constraint.min,
|
||||
// max: constraint.min,
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
// })
|
||||
// .with_cursor_style(CursorStyle::PointingHand)
|
||||
// .on_down(MouseButton::Left, move |_, this, cx| {
|
||||
// this.confirm_completion(
|
||||
// &ConfirmCompletion {
|
||||
// item_ix: Some(item_ix),
|
||||
// },
|
||||
// cx,
|
||||
// )
|
||||
// .map(|task| task.detach());
|
||||
// })
|
||||
// .constrained()
|
||||
//
|
||||
div()
|
||||
.id(mat.candidate_id)
|
||||
.bg(gpui::green())
|
||||
.hover(|style| style.bg(gpui::blue()))
|
||||
.when(item_ix == selected_item, |div| div.bg(gpui::blue()))
|
||||
.child(SharedString::from(completion.label.text.clone()))
|
||||
.min_w(px(300.))
|
||||
.max_w(px(700.))
|
||||
})
|
||||
.collect()
|
||||
},
|
||||
)
|
||||
.with_width_from_item(widest_completion_ix);
|
||||
|
||||
list.render()
|
||||
list.render_into_any()
|
||||
// todo!("multiline documentation")
|
||||
// enum MultiLineDocumentation {}
|
||||
|
||||
@@ -1529,13 +1532,15 @@ impl CodeActionsMenu {
|
||||
mut cursor_position: DisplayPoint,
|
||||
style: &EditorStyle,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> (DisplayPoint, AnyElement<Editor>) {
|
||||
) -> (DisplayPoint, AnyElement) {
|
||||
let actions = self.actions.clone();
|
||||
let selected_item = self.selected_item;
|
||||
|
||||
let element = uniform_list(
|
||||
cx.view().clone(),
|
||||
"code_actions_menu",
|
||||
self.actions.len(),
|
||||
move |editor, range, cx| {
|
||||
move |this, range, cx| {
|
||||
actions[range.clone()]
|
||||
.iter()
|
||||
.enumerate()
|
||||
@@ -1557,18 +1562,22 @@ impl CodeActionsMenu {
|
||||
.bg(colors.element_hover)
|
||||
.text_color(colors.text_accent)
|
||||
})
|
||||
.on_mouse_down(MouseButton::Left, move |editor: &mut Editor, _, cx| {
|
||||
cx.stop_propagation();
|
||||
editor
|
||||
.confirm_code_action(
|
||||
&ConfirmCodeAction {
|
||||
item_ix: Some(item_ix),
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.map(|task| task.detach_and_log_err(cx));
|
||||
})
|
||||
.child(action.lsp_action.title.clone())
|
||||
.on_mouse_down(
|
||||
MouseButton::Left,
|
||||
cx.listener(move |editor, _, cx| {
|
||||
cx.stop_propagation();
|
||||
editor
|
||||
.confirm_code_action(
|
||||
&ConfirmCodeAction {
|
||||
item_ix: Some(item_ix),
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.map(|task| task.detach_and_log_err(cx));
|
||||
}),
|
||||
)
|
||||
// TASK: It would be good to make lsp_action.title a SharedString to avoid allocating here.
|
||||
.child(SharedString::from(action.lsp_action.title.clone()))
|
||||
})
|
||||
.collect()
|
||||
},
|
||||
@@ -1583,7 +1592,7 @@ impl CodeActionsMenu {
|
||||
.max_by_key(|(_, action)| action.lsp_action.title.chars().count())
|
||||
.map(|(ix, _)| ix),
|
||||
)
|
||||
.render();
|
||||
.render_into_any();
|
||||
|
||||
if self.deployed_from_indicator {
|
||||
*cursor_position.column_mut() = 0;
|
||||
@@ -2306,7 +2315,8 @@ impl Editor {
|
||||
}
|
||||
|
||||
self.blink_manager.update(cx, BlinkManager::pause_blinking);
|
||||
cx.emit(Event::SelectionsChanged { local });
|
||||
cx.emit(EditorEvent::SelectionsChanged { local });
|
||||
cx.emit(SearchEvent::MatchesInvalidated);
|
||||
|
||||
if self.selections.disjoint_anchors().len() == 1 {
|
||||
cx.emit(SearchEvent::ActiveMatchChanged)
|
||||
@@ -4230,7 +4240,7 @@ impl Editor {
|
||||
|
||||
self.report_copilot_event(Some(completion.uuid.clone()), true, cx)
|
||||
}
|
||||
cx.emit(Event::InputHandled {
|
||||
cx.emit(EditorEvent::InputHandled {
|
||||
utf16_range_to_replace: None,
|
||||
text: suggestion.text.to_string().into(),
|
||||
});
|
||||
@@ -4341,19 +4351,19 @@ impl Editor {
|
||||
style: &EditorStyle,
|
||||
is_active: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<AnyElement<Self>> {
|
||||
) -> Option<IconButton> {
|
||||
if self.available_code_actions.is_some() {
|
||||
Some(
|
||||
IconButton::new("code_actions_indicator", ui::Icon::Bolt)
|
||||
.on_click(|editor: &mut Editor, cx| {
|
||||
IconButton::new("code_actions_indicator", ui::Icon::Bolt).on_click(cx.listener(
|
||||
|editor, e, cx| {
|
||||
editor.toggle_code_actions(
|
||||
&ToggleCodeActions {
|
||||
deployed_from_indicator: true,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.render(),
|
||||
},
|
||||
)),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
@@ -4368,7 +4378,7 @@ impl Editor {
|
||||
line_height: Pixels,
|
||||
gutter_margin: Pixels,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Vec<Option<AnyElement<Self>>> {
|
||||
) -> Vec<Option<IconButton>> {
|
||||
fold_data
|
||||
.iter()
|
||||
.enumerate()
|
||||
@@ -4381,15 +4391,15 @@ impl Editor {
|
||||
FoldStatus::Foldable => ui::Icon::ChevronDown,
|
||||
};
|
||||
IconButton::new(ix as usize, icon)
|
||||
.on_click(move |editor: &mut Editor, cx| match fold_status {
|
||||
.on_click(cx.listener(move |editor, e, cx| match fold_status {
|
||||
FoldStatus::Folded => {
|
||||
editor.unfold_at(&UnfoldAt { buffer_row }, cx);
|
||||
}
|
||||
FoldStatus::Foldable => {
|
||||
editor.fold_at(&FoldAt { buffer_row }, cx);
|
||||
}
|
||||
})
|
||||
.render()
|
||||
}))
|
||||
.color(ui::Color::Muted)
|
||||
})
|
||||
})
|
||||
.flatten()
|
||||
@@ -4409,7 +4419,7 @@ impl Editor {
|
||||
cursor_position: DisplayPoint,
|
||||
style: &EditorStyle,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> Option<(DisplayPoint, AnyElement<Editor>)> {
|
||||
) -> Option<(DisplayPoint, AnyElement)> {
|
||||
self.context_menu.read().as_ref().map(|menu| {
|
||||
menu.render(
|
||||
cursor_position,
|
||||
@@ -5627,7 +5637,7 @@ impl Editor {
|
||||
self.request_autoscroll(Autoscroll::fit(), cx);
|
||||
self.unmark_text(cx);
|
||||
self.refresh_copilot_suggestions(true, cx);
|
||||
cx.emit(Event::Edited);
|
||||
cx.emit(EditorEvent::Edited);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5642,7 +5652,7 @@ impl Editor {
|
||||
self.request_autoscroll(Autoscroll::fit(), cx);
|
||||
self.unmark_text(cx);
|
||||
self.refresh_copilot_suggestions(true, cx);
|
||||
cx.emit(Event::Edited);
|
||||
cx.emit(EditorEvent::Edited);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7768,7 +7778,7 @@ impl Editor {
|
||||
}
|
||||
div()
|
||||
.pl(cx.anchor_x)
|
||||
.child(rename_editor.render_with(EditorElement::new(
|
||||
.child(EditorElement::new(
|
||||
&rename_editor,
|
||||
EditorStyle {
|
||||
background: cx.theme().system().transparent,
|
||||
@@ -7776,11 +7786,13 @@ impl Editor {
|
||||
text: text_style,
|
||||
scrollbar_width: cx.editor_style.scrollbar_width,
|
||||
syntax: cx.editor_style.syntax.clone(),
|
||||
diagnostic_style:
|
||||
cx.editor_style.diagnostic_style.clone(),
|
||||
diagnostic_style: cx
|
||||
.editor_style
|
||||
.diagnostic_style
|
||||
.clone(),
|
||||
},
|
||||
)))
|
||||
.render()
|
||||
))
|
||||
.render_into_any()
|
||||
}
|
||||
}),
|
||||
disposition: BlockDisposition::Below,
|
||||
@@ -8111,7 +8123,7 @@ impl Editor {
|
||||
log::error!("unexpectedly ended a transaction that wasn't started by this editor");
|
||||
}
|
||||
|
||||
cx.emit(Event::Edited);
|
||||
cx.emit(EditorEvent::Edited);
|
||||
Some(tx_id)
|
||||
} else {
|
||||
None
|
||||
@@ -8699,7 +8711,7 @@ impl Editor {
|
||||
if self.has_active_copilot_suggestion(cx) {
|
||||
self.update_visible_copilot_suggestion(cx);
|
||||
}
|
||||
cx.emit(Event::BufferEdited);
|
||||
cx.emit(EditorEvent::BufferEdited);
|
||||
cx.emit(ItemEvent::Edit);
|
||||
cx.emit(ItemEvent::UpdateBreadcrumbs);
|
||||
cx.emit(SearchEvent::MatchesInvalidated);
|
||||
@@ -8738,7 +8750,7 @@ impl Editor {
|
||||
predecessor,
|
||||
excerpts,
|
||||
} => {
|
||||
cx.emit(Event::ExcerptsAdded {
|
||||
cx.emit(EditorEvent::ExcerptsAdded {
|
||||
buffer: buffer.clone(),
|
||||
predecessor: *predecessor,
|
||||
excerpts: excerpts.clone(),
|
||||
@@ -8747,7 +8759,7 @@ impl Editor {
|
||||
}
|
||||
multi_buffer::Event::ExcerptsRemoved { ids } => {
|
||||
self.refresh_inlay_hints(InlayHintRefreshReason::ExcerptsRemoved(ids.clone()), cx);
|
||||
cx.emit(Event::ExcerptsRemoved { ids: ids.clone() })
|
||||
cx.emit(EditorEvent::ExcerptsRemoved { ids: ids.clone() })
|
||||
}
|
||||
multi_buffer::Event::Reparsed => {
|
||||
cx.emit(ItemEvent::UpdateBreadcrumbs);
|
||||
@@ -8761,7 +8773,7 @@ impl Editor {
|
||||
cx.emit(ItemEvent::UpdateTab);
|
||||
cx.emit(ItemEvent::UpdateBreadcrumbs);
|
||||
}
|
||||
multi_buffer::Event::DiffBaseChanged => cx.emit(Event::DiffBaseChanged),
|
||||
multi_buffer::Event::DiffBaseChanged => cx.emit(EditorEvent::DiffBaseChanged),
|
||||
multi_buffer::Event::Closed => cx.emit(ItemEvent::CloseItem),
|
||||
multi_buffer::Event::DiagnosticsUpdated => {
|
||||
self.refresh_active_diagnostics(cx);
|
||||
@@ -8955,12 +8967,12 @@ impl Editor {
|
||||
let telemetry = project.read(cx).client().telemetry().clone();
|
||||
let telemetry_settings = *TelemetrySettings::get_global(cx);
|
||||
|
||||
let event = ClickhouseEvent::Copilot {
|
||||
telemetry.report_copilot_event(
|
||||
telemetry_settings,
|
||||
suggestion_id,
|
||||
suggestion_accepted,
|
||||
file_extension,
|
||||
};
|
||||
telemetry.report_clickhouse_event(event, telemetry_settings);
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
@@ -9007,14 +9019,14 @@ impl Editor {
|
||||
.show_copilot_suggestions;
|
||||
|
||||
let telemetry = project.read(cx).client().telemetry().clone();
|
||||
let event = ClickhouseEvent::Editor {
|
||||
telemetry.report_editor_event(
|
||||
telemetry_settings,
|
||||
file_extension,
|
||||
vim_mode,
|
||||
operation,
|
||||
copilot_enabled,
|
||||
copilot_enabled_for_language,
|
||||
};
|
||||
telemetry.report_clickhouse_event(event, telemetry_settings)
|
||||
)
|
||||
}
|
||||
|
||||
/// Copy the highlighted chunks to the clipboard as JSON. The format is an array of lines,
|
||||
@@ -9101,7 +9113,7 @@ impl Editor {
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if !self.input_enabled {
|
||||
cx.emit(Event::InputIgnored { text: text.into() });
|
||||
cx.emit(EditorEvent::InputIgnored { text: text.into() });
|
||||
return;
|
||||
}
|
||||
if let Some(relative_utf16_range) = relative_utf16_range {
|
||||
@@ -9161,7 +9173,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
fn handle_focus(&mut self, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(Event::Focused);
|
||||
cx.emit(EditorEvent::Focused);
|
||||
|
||||
if let Some(rename) = self.pending_rename.as_ref() {
|
||||
let rename_editor_focus_handle = rename.editor.read(cx).focus_handle.clone();
|
||||
@@ -9191,7 +9203,7 @@ impl Editor {
|
||||
.update(cx, |buffer, cx| buffer.remove_active_selections(cx));
|
||||
self.hide_context_menu(cx);
|
||||
hide_hover(self, cx);
|
||||
cx.emit(Event::Blurred);
|
||||
cx.emit(EditorEvent::Blurred);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
@@ -9314,7 +9326,7 @@ impl Deref for EditorSnapshot {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum Event {
|
||||
pub enum EditorEvent {
|
||||
InputIgnored {
|
||||
text: Arc<str>,
|
||||
},
|
||||
@@ -9332,8 +9344,12 @@ pub enum Event {
|
||||
},
|
||||
BufferEdited,
|
||||
Edited,
|
||||
Reparsed,
|
||||
Focused,
|
||||
Blurred,
|
||||
DirtyChanged,
|
||||
Saved,
|
||||
TitleChanged,
|
||||
DiffBaseChanged,
|
||||
SelectionsChanged {
|
||||
local: bool,
|
||||
@@ -9342,6 +9358,7 @@ pub enum Event {
|
||||
local: bool,
|
||||
autoscroll: bool,
|
||||
},
|
||||
Closed,
|
||||
}
|
||||
|
||||
pub struct EditorFocused(pub View<Editor>);
|
||||
@@ -9356,7 +9373,7 @@ pub struct EditorReleased(pub WeakView<Editor>);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
impl EventEmitter<Event> for Editor {}
|
||||
impl EventEmitter<EditorEvent> for Editor {}
|
||||
|
||||
impl FocusableView for Editor {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
@@ -9559,7 +9576,7 @@ impl InputHandler for Editor {
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if !self.input_enabled {
|
||||
cx.emit(Event::InputIgnored { text: text.into() });
|
||||
cx.emit(EditorEvent::InputIgnored { text: text.into() });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -9589,7 +9606,7 @@ impl InputHandler for Editor {
|
||||
})
|
||||
});
|
||||
|
||||
cx.emit(Event::InputHandled {
|
||||
cx.emit(EditorEvent::InputHandled {
|
||||
utf16_range_to_replace: range_to_replace,
|
||||
text: text.into(),
|
||||
});
|
||||
@@ -9620,7 +9637,7 @@ impl InputHandler for Editor {
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if !self.input_enabled {
|
||||
cx.emit(Event::InputIgnored { text: text.into() });
|
||||
cx.emit(EditorEvent::InputIgnored { text: text.into() });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -9663,7 +9680,7 @@ impl InputHandler for Editor {
|
||||
})
|
||||
});
|
||||
|
||||
cx.emit(Event::InputHandled {
|
||||
cx.emit(EditorEvent::InputHandled {
|
||||
utf16_range_to_replace: range_to_replace,
|
||||
text: text.into(),
|
||||
});
|
||||
@@ -9978,11 +9995,11 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend
|
||||
.ml(cx.anchor_x)
|
||||
}))
|
||||
.cursor_pointer()
|
||||
.on_click(move |_, _, cx| {
|
||||
.on_click(cx.listener(move |_, _, cx| {
|
||||
cx.write_to_clipboard(ClipboardItem::new(message.clone()));
|
||||
})
|
||||
.tooltip(|_, cx| Tooltip::text("Copy diagnostic message", cx))
|
||||
.render()
|
||||
}))
|
||||
.tooltip(|cx| Tooltip::text("Copy diagnostic message", cx))
|
||||
.render_into_any()
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -3048,7 +3048,7 @@ fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
|
||||
position: snapshot.anchor_after(Point::new(2, 0)),
|
||||
disposition: BlockDisposition::Below,
|
||||
height: 1,
|
||||
render: Arc::new(|_| div().render()),
|
||||
render: Arc::new(|_| div().into_any()),
|
||||
}],
|
||||
Some(Autoscroll::fit()),
|
||||
cx,
|
||||
@@ -3853,7 +3853,7 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
|
||||
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let (view, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
||||
|
||||
view.condition::<crate::Event>(&cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
|
||||
view.condition::<crate::EditorEvent>(&cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
|
||||
.await;
|
||||
|
||||
view.update(cx, |view, cx| {
|
||||
@@ -4019,7 +4019,7 @@ async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) {
|
||||
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
||||
editor
|
||||
.condition::<crate::Event>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
|
||||
.condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
|
||||
.await;
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
@@ -4583,7 +4583,7 @@ async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) {
|
||||
});
|
||||
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let (view, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
||||
view.condition::<crate::Event>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
|
||||
view.condition::<crate::EditorEvent>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
|
||||
.await;
|
||||
|
||||
view.update(cx, |view, cx| {
|
||||
@@ -4734,7 +4734,7 @@ async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) {
|
||||
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
||||
editor
|
||||
.condition::<crate::Event>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
|
||||
.condition::<crate::EditorEvent>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
|
||||
.await;
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
@@ -6295,7 +6295,7 @@ async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) {
|
||||
});
|
||||
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let (view, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
||||
view.condition::<crate::Event>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
|
||||
view.condition::<crate::EditorEvent>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
|
||||
.await;
|
||||
|
||||
view.update(cx, |view, cx| {
|
||||
|
||||
+313
-270
@@ -20,10 +20,10 @@ use collections::{BTreeMap, HashMap};
|
||||
use gpui::{
|
||||
div, point, px, relative, size, transparent_black, Action, AnyElement, AvailableSpace,
|
||||
BorrowWindow, Bounds, Component, ContentMask, Corners, DispatchPhase, Edges, Element,
|
||||
ElementId, ElementInputHandler, Entity, EntityId, Hsla, InteractiveComponent, LineLayout,
|
||||
MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentComponent, Pixels,
|
||||
ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveComponent, Style, Styled,
|
||||
TextRun, TextStyle, View, ViewContext, WindowContext, WrappedLine,
|
||||
ElementId, ElementInputHandler, Entity, EntityId, Hsla, InteractiveElement, LineLayout,
|
||||
MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, RenderOnce,
|
||||
ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled,
|
||||
TextRun, TextStyle, View, ViewContext, WeakView, WindowContext, WrappedLine,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use language::language_settings::ShowWhitespaceSetting;
|
||||
@@ -112,14 +112,14 @@ impl SelectionLayout {
|
||||
}
|
||||
|
||||
pub struct EditorElement {
|
||||
editor_id: EntityId,
|
||||
editor: View<Editor>,
|
||||
style: EditorStyle,
|
||||
}
|
||||
|
||||
impl EditorElement {
|
||||
pub fn new(editor: &View<Editor>, style: EditorStyle) -> Self {
|
||||
Self {
|
||||
editor_id: editor.entity_id(),
|
||||
editor: editor.clone(),
|
||||
style,
|
||||
}
|
||||
}
|
||||
@@ -349,7 +349,7 @@ impl EditorElement {
|
||||
gutter_bounds: Bounds<Pixels>,
|
||||
text_bounds: Bounds<Pixels>,
|
||||
layout: &LayoutState,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
let bounds = gutter_bounds.union(&text_bounds);
|
||||
let scroll_top =
|
||||
@@ -460,7 +460,7 @@ impl EditorElement {
|
||||
bounds: Bounds<Pixels>,
|
||||
layout: &mut LayoutState,
|
||||
editor: &mut Editor,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
let line_height = layout.position_map.line_height;
|
||||
|
||||
@@ -488,13 +488,14 @@ impl EditorElement {
|
||||
}
|
||||
}
|
||||
|
||||
for (ix, fold_indicator) in layout.fold_indicators.iter_mut().enumerate() {
|
||||
if let Some(fold_indicator) = fold_indicator.as_mut() {
|
||||
for (ix, fold_indicator) in layout.fold_indicators.drain(..).enumerate() {
|
||||
if let Some(mut fold_indicator) = fold_indicator {
|
||||
let mut fold_indicator = fold_indicator.render_into_any();
|
||||
let available_space = size(
|
||||
AvailableSpace::MinContent,
|
||||
AvailableSpace::Definite(line_height * 0.55),
|
||||
);
|
||||
let fold_indicator_size = fold_indicator.measure(available_space, editor, cx);
|
||||
let fold_indicator_size = fold_indicator.measure(available_space, cx);
|
||||
|
||||
let position = point(
|
||||
bounds.size.width - layout.gutter_padding,
|
||||
@@ -505,32 +506,29 @@ impl EditorElement {
|
||||
(line_height - fold_indicator_size.height) / 2.,
|
||||
);
|
||||
let origin = bounds.origin + position + centering_offset;
|
||||
fold_indicator.draw(origin, available_space, editor, cx);
|
||||
fold_indicator.draw(origin, available_space, cx);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(indicator) = layout.code_actions_indicator.as_mut() {
|
||||
if let Some(indicator) = layout.code_actions_indicator.take() {
|
||||
let mut button = indicator.button.render_into_any();
|
||||
let available_space = size(
|
||||
AvailableSpace::MinContent,
|
||||
AvailableSpace::Definite(line_height),
|
||||
);
|
||||
let indicator_size = indicator.element.measure(available_space, editor, cx);
|
||||
let indicator_size = button.measure(available_space, cx);
|
||||
|
||||
let mut x = Pixels::ZERO;
|
||||
let mut y = indicator.row as f32 * line_height - scroll_top;
|
||||
// Center indicator.
|
||||
x += ((layout.gutter_padding + layout.gutter_margin) - indicator_size.width) / 2.;
|
||||
y += (line_height - indicator_size.height) / 2.;
|
||||
indicator
|
||||
.element
|
||||
.draw(bounds.origin + point(x, y), available_space, editor, cx);
|
||||
|
||||
button.draw(bounds.origin + point(x, y), available_space, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_diff_hunks(
|
||||
bounds: Bounds<Pixels>,
|
||||
layout: &LayoutState,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
fn paint_diff_hunks(bounds: Bounds<Pixels>, layout: &LayoutState, cx: &mut WindowContext) {
|
||||
// todo!()
|
||||
// let diff_style = &theme::current(cx).editor.diff.clone();
|
||||
// let line_height = layout.position_map.line_height;
|
||||
@@ -619,7 +617,7 @@ impl EditorElement {
|
||||
text_bounds: Bounds<Pixels>,
|
||||
layout: &mut LayoutState,
|
||||
editor: &mut Editor,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
let scroll_position = layout.position_map.snapshot.scroll_position();
|
||||
let start_row = layout.visible_display_row_range.start;
|
||||
@@ -674,20 +672,22 @@ impl EditorElement {
|
||||
div()
|
||||
.id(fold.id)
|
||||
.size_full()
|
||||
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
|
||||
.on_click(move |editor: &mut Editor, _, cx| {
|
||||
editor.unfold_ranges(
|
||||
[fold_range.start..fold_range.end],
|
||||
true,
|
||||
false,
|
||||
cx,
|
||||
);
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
|
||||
.on_click(cx.listener_for(
|
||||
&self.editor,
|
||||
move |editor: &mut Editor, _, cx| {
|
||||
editor.unfold_ranges(
|
||||
[fold_range.start..fold_range.end],
|
||||
true,
|
||||
false,
|
||||
cx,
|
||||
);
|
||||
cx.stop_propagation();
|
||||
},
|
||||
))
|
||||
.draw(
|
||||
fold_bounds.origin,
|
||||
fold_bounds.size,
|
||||
editor,
|
||||
cx,
|
||||
|fold_element_state, cx| {
|
||||
if fold_element_state.is_active() {
|
||||
@@ -840,7 +840,7 @@ impl EditorElement {
|
||||
}
|
||||
});
|
||||
|
||||
if let Some((position, context_menu)) = layout.context_menu.as_mut() {
|
||||
if let Some((position, mut context_menu)) = layout.context_menu.take() {
|
||||
cx.with_z_index(1, |cx| {
|
||||
let line_height = self.style.text.line_height_in_pixels(cx.rem_size());
|
||||
let available_space = size(
|
||||
@@ -850,7 +850,7 @@ impl EditorElement {
|
||||
.min((text_bounds.size.height - line_height) / 2.),
|
||||
),
|
||||
);
|
||||
let context_menu_size = context_menu.measure(available_space, editor, cx);
|
||||
let context_menu_size = context_menu.measure(available_space, cx);
|
||||
|
||||
let cursor_row_layout = &layout.position_map.line_layouts
|
||||
[(position.row() - start_row) as usize]
|
||||
@@ -874,7 +874,7 @@ impl EditorElement {
|
||||
list_origin.y -= layout.position_map.line_height - list_height;
|
||||
}
|
||||
|
||||
context_menu.draw(list_origin, available_space, editor, cx);
|
||||
context_menu.draw(list_origin, available_space, cx);
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1165,7 +1165,7 @@ impl EditorElement {
|
||||
layout: &LayoutState,
|
||||
content_origin: gpui::Point<Pixels>,
|
||||
bounds: Bounds<Pixels>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
let start_row = layout.visible_display_row_range.start;
|
||||
let end_row = layout.visible_display_row_range.end;
|
||||
@@ -1218,13 +1218,13 @@ impl EditorElement {
|
||||
bounds: Bounds<Pixels>,
|
||||
layout: &mut LayoutState,
|
||||
editor: &mut Editor,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
let scroll_position = layout.position_map.snapshot.scroll_position();
|
||||
let scroll_left = scroll_position.x * layout.position_map.em_width;
|
||||
let scroll_top = scroll_position.y * layout.position_map.line_height;
|
||||
|
||||
for block in &mut layout.blocks {
|
||||
for block in layout.blocks.drain(..) {
|
||||
let mut origin = bounds.origin
|
||||
+ point(
|
||||
Pixels::ZERO,
|
||||
@@ -1233,9 +1233,7 @@ impl EditorElement {
|
||||
if !matches!(block.style, BlockStyle::Sticky) {
|
||||
origin += point(-scroll_left, Pixels::ZERO);
|
||||
}
|
||||
block
|
||||
.element
|
||||
.draw(origin, block.available_space, editor, cx);
|
||||
block.element.draw(origin, block.available_space, cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1810,7 +1808,7 @@ impl EditorElement {
|
||||
.render_code_actions_indicator(&style, active, cx)
|
||||
.map(|element| CodeActionsIndicator {
|
||||
row: newest_selection_head.row(),
|
||||
element,
|
||||
button: element,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1970,6 +1968,7 @@ impl EditorElement {
|
||||
TransformBlock::ExcerptHeader { .. } => false,
|
||||
TransformBlock::Custom(block) => block.style() == BlockStyle::Fixed,
|
||||
});
|
||||
|
||||
let mut render_block = |block: &TransformBlock,
|
||||
available_space: Size<AvailableSpace>,
|
||||
block_id: usize,
|
||||
@@ -2003,6 +2002,7 @@ impl EditorElement {
|
||||
editor_style: &self.style,
|
||||
})
|
||||
}
|
||||
|
||||
TransformBlock::ExcerptHeader {
|
||||
buffer,
|
||||
range,
|
||||
@@ -2026,12 +2026,10 @@ impl EditorElement {
|
||||
let jump_position = language::ToPoint::to_point(&jump_anchor, buffer);
|
||||
|
||||
IconButton::new(block_id, ui::Icon::ArrowUpRight)
|
||||
.on_click(move |editor: &mut Editor, cx| {
|
||||
.on_click(cx.listener_for(&self.editor, move |editor, e, cx| {
|
||||
editor.jump(jump_path.clone(), jump_position, jump_anchor, cx);
|
||||
})
|
||||
.tooltip(move |_, cx| {
|
||||
Tooltip::for_action("Jump to Buffer", &OpenExcerpts, cx)
|
||||
})
|
||||
}))
|
||||
.tooltip(|cx| Tooltip::for_action("Jump to Buffer", &OpenExcerpts, cx))
|
||||
});
|
||||
|
||||
let element = if *starts_new_buffer {
|
||||
@@ -2041,29 +2039,36 @@ impl EditorElement {
|
||||
// Can't use .and_then() because `.file_name()` and `.parent()` return references :(
|
||||
if let Some(path) = path {
|
||||
filename = path.file_name().map(|f| f.to_string_lossy().to_string());
|
||||
parent_path =
|
||||
path.parent().map(|p| p.to_string_lossy().to_string() + "/");
|
||||
parent_path = path
|
||||
.parent()
|
||||
.map(|p| SharedString::from(p.to_string_lossy().to_string() + "/"));
|
||||
}
|
||||
|
||||
h_stack()
|
||||
.id("path header block")
|
||||
.size_full()
|
||||
.bg(gpui::red())
|
||||
.child(filename.unwrap_or_else(|| "untitled".to_string()))
|
||||
.child(
|
||||
filename
|
||||
.map(SharedString::from)
|
||||
.unwrap_or_else(|| "untitled".into()),
|
||||
)
|
||||
.children(parent_path)
|
||||
.children(jump_icon) // .p_x(gutter_padding)
|
||||
} else {
|
||||
let text_style = style.text.clone();
|
||||
h_stack()
|
||||
.id("collapsed context")
|
||||
.size_full()
|
||||
.bg(gpui::red())
|
||||
.child("⋯")
|
||||
.children(jump_icon) // .p_x(gutter_padding)
|
||||
};
|
||||
element.render()
|
||||
element.into_any()
|
||||
}
|
||||
};
|
||||
|
||||
let size = element.measure(available_space, editor, cx);
|
||||
let size = element.measure(available_space, cx);
|
||||
(element, size)
|
||||
};
|
||||
|
||||
@@ -2122,47 +2127,61 @@ impl EditorElement {
|
||||
gutter_bounds: Bounds<Pixels>,
|
||||
text_bounds: Bounds<Pixels>,
|
||||
layout: &LayoutState,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
let content_origin = text_bounds.origin + point(layout.gutter_margin, Pixels::ZERO);
|
||||
|
||||
cx.on_mouse_event({
|
||||
let position_map = layout.position_map.clone();
|
||||
move |editor, event: &ScrollWheelEvent, phase, cx| {
|
||||
let editor = self.editor.clone();
|
||||
|
||||
move |event: &ScrollWheelEvent, phase, cx| {
|
||||
if phase != DispatchPhase::Bubble {
|
||||
return;
|
||||
}
|
||||
|
||||
if Self::scroll(editor, event, &position_map, bounds, cx) {
|
||||
let should_cancel = editor.update(cx, |editor, cx| {
|
||||
Self::scroll(editor, event, &position_map, bounds, cx)
|
||||
});
|
||||
if should_cancel {
|
||||
cx.stop_propagation();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
cx.on_mouse_event({
|
||||
let position_map = layout.position_map.clone();
|
||||
move |editor, event: &MouseDownEvent, phase, cx| {
|
||||
let editor = self.editor.clone();
|
||||
|
||||
move |event: &MouseDownEvent, phase, cx| {
|
||||
if phase != DispatchPhase::Bubble {
|
||||
return;
|
||||
}
|
||||
|
||||
if Self::mouse_down(editor, event, &position_map, text_bounds, gutter_bounds, cx) {
|
||||
let should_cancel = editor.update(cx, |editor, cx| {
|
||||
Self::mouse_down(editor, event, &position_map, text_bounds, gutter_bounds, cx)
|
||||
});
|
||||
|
||||
if should_cancel {
|
||||
cx.stop_propagation()
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
cx.on_mouse_event({
|
||||
let position_map = layout.position_map.clone();
|
||||
move |editor, event: &MouseUpEvent, phase, cx| {
|
||||
if phase != DispatchPhase::Bubble {
|
||||
return;
|
||||
}
|
||||
let editor = self.editor.clone();
|
||||
move |event: &MouseUpEvent, phase, cx| {
|
||||
let should_cancel = editor.update(cx, |editor, cx| {
|
||||
Self::mouse_up(editor, event, &position_map, text_bounds, cx)
|
||||
});
|
||||
|
||||
if Self::mouse_up(editor, event, &position_map, text_bounds, cx) {
|
||||
if should_cancel {
|
||||
cx.stop_propagation()
|
||||
}
|
||||
}
|
||||
});
|
||||
// todo!()
|
||||
//todo!()
|
||||
// on_down(MouseButton::Right, {
|
||||
// let position_map = layout.position_map.clone();
|
||||
// move |event, editor, cx| {
|
||||
@@ -2179,12 +2198,17 @@ impl EditorElement {
|
||||
// });
|
||||
cx.on_mouse_event({
|
||||
let position_map = layout.position_map.clone();
|
||||
move |editor, event: &MouseMoveEvent, phase, cx| {
|
||||
let editor = self.editor.clone();
|
||||
move |event: &MouseMoveEvent, phase, cx| {
|
||||
if phase != DispatchPhase::Bubble {
|
||||
return;
|
||||
}
|
||||
|
||||
if Self::mouse_moved(editor, event, &position_map, text_bounds, gutter_bounds, cx) {
|
||||
let stop_propogating = editor.update(cx, |editor, cx| {
|
||||
Self::mouse_moved(editor, event, &position_map, text_bounds, gutter_bounds, cx)
|
||||
});
|
||||
|
||||
if stop_propogating {
|
||||
cx.stop_propagation()
|
||||
}
|
||||
}
|
||||
@@ -2313,7 +2337,7 @@ impl LineWithInvisibles {
|
||||
content_origin: gpui::Point<Pixels>,
|
||||
whitespace_setting: ShowWhitespaceSetting,
|
||||
selection_ranges: &[Range<DisplayPoint>],
|
||||
cx: &mut ViewContext<Editor>,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
let line_height = layout.position_map.line_height;
|
||||
let line_y = line_height * row as f32 - layout.position_map.scroll_position.y;
|
||||
@@ -2345,7 +2369,7 @@ impl LineWithInvisibles {
|
||||
row: u32,
|
||||
line_height: Pixels,
|
||||
whitespace_setting: ShowWhitespaceSetting,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
let allowed_invisibles_regions = match whitespace_setting {
|
||||
ShowWhitespaceSetting::None => return,
|
||||
@@ -2388,87 +2412,102 @@ enum Invisible {
|
||||
Whitespace { line_offset: usize },
|
||||
}
|
||||
|
||||
impl Element<Editor> for EditorElement {
|
||||
type ElementState = ();
|
||||
|
||||
fn element_id(&self) -> Option<gpui::ElementId> {
|
||||
Some(self.editor_id.into())
|
||||
}
|
||||
impl Element for EditorElement {
|
||||
type State = ();
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
editor: &mut Editor,
|
||||
element_state: Option<Self::ElementState>,
|
||||
cx: &mut gpui::ViewContext<Editor>,
|
||||
) -> (gpui::LayoutId, Self::ElementState) {
|
||||
editor.style = Some(self.style.clone()); // Long-term, we'd like to eliminate this.
|
||||
element_state: Option<Self::State>,
|
||||
cx: &mut gpui::WindowContext,
|
||||
) -> (gpui::LayoutId, Self::State) {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.style = Some(self.style.clone()); // Long-term, we'd like to eliminate this.
|
||||
|
||||
let rem_size = cx.rem_size();
|
||||
let mut style = Style::default();
|
||||
style.size.width = relative(1.).into();
|
||||
style.size.height = match editor.mode {
|
||||
EditorMode::SingleLine => self.style.text.line_height_in_pixels(cx.rem_size()).into(),
|
||||
EditorMode::AutoHeight { .. } => todo!(),
|
||||
EditorMode::Full => relative(1.).into(),
|
||||
};
|
||||
let layout_id = cx.request_layout(&style, None);
|
||||
(layout_id, ())
|
||||
let rem_size = cx.rem_size();
|
||||
let mut style = Style::default();
|
||||
style.size.width = relative(1.).into();
|
||||
style.size.height = match editor.mode {
|
||||
EditorMode::SingleLine => {
|
||||
self.style.text.line_height_in_pixels(cx.rem_size()).into()
|
||||
}
|
||||
EditorMode::AutoHeight { .. } => todo!(),
|
||||
EditorMode::Full => relative(1.).into(),
|
||||
};
|
||||
let layout_id = cx.request_layout(&style, None);
|
||||
|
||||
(layout_id, ())
|
||||
})
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
mut self,
|
||||
bounds: Bounds<gpui::Pixels>,
|
||||
editor: &mut Editor,
|
||||
element_state: &mut Self::ElementState,
|
||||
cx: &mut gpui::ViewContext<Editor>,
|
||||
element_state: &mut Self::State,
|
||||
cx: &mut gpui::WindowContext,
|
||||
) {
|
||||
let mut layout = self.compute_layout(editor, cx, bounds);
|
||||
let gutter_bounds = Bounds {
|
||||
origin: bounds.origin,
|
||||
size: layout.gutter_size,
|
||||
};
|
||||
let text_bounds = Bounds {
|
||||
origin: gutter_bounds.upper_right(),
|
||||
size: layout.text_size,
|
||||
};
|
||||
let editor = self.editor.clone();
|
||||
editor.update(cx, |editor, cx| {
|
||||
let mut layout = self.compute_layout(editor, cx, bounds);
|
||||
let gutter_bounds = Bounds {
|
||||
origin: bounds.origin,
|
||||
size: layout.gutter_size,
|
||||
};
|
||||
let text_bounds = Bounds {
|
||||
origin: gutter_bounds.upper_right(),
|
||||
size: layout.text_size,
|
||||
};
|
||||
|
||||
let dispatch_context = editor.dispatch_context(cx);
|
||||
cx.with_key_dispatch(
|
||||
dispatch_context,
|
||||
Some(editor.focus_handle.clone()),
|
||||
|_, cx| {
|
||||
register_actions(cx);
|
||||
let dispatch_context = editor.dispatch_context(cx);
|
||||
let editor_handle = cx.view().clone();
|
||||
cx.with_key_dispatch(
|
||||
dispatch_context,
|
||||
Some(editor.focus_handle.clone()),
|
||||
|_, cx| {
|
||||
register_actions(&editor_handle, cx);
|
||||
|
||||
// We call with_z_index to establish a new stacking context.
|
||||
cx.with_z_index(0, |cx| {
|
||||
cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
|
||||
// Paint mouse listeners first, so any elements we paint on top of the editor
|
||||
// take precedence.
|
||||
self.paint_mouse_listeners(bounds, gutter_bounds, text_bounds, &layout, cx);
|
||||
let input_handler = ElementInputHandler::new(bounds, cx);
|
||||
cx.handle_input(&editor.focus_handle, input_handler);
|
||||
// We call with_z_index to establish a new stacking context.
|
||||
cx.with_z_index(0, |cx| {
|
||||
cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
|
||||
// Paint mouse listeners first, so any elements we paint on top of the editor
|
||||
// take precedence.
|
||||
self.paint_mouse_listeners(
|
||||
bounds,
|
||||
gutter_bounds,
|
||||
text_bounds,
|
||||
&layout,
|
||||
cx,
|
||||
);
|
||||
let input_handler = ElementInputHandler::new(bounds, editor_handle, cx);
|
||||
cx.handle_input(&editor.focus_handle, input_handler);
|
||||
|
||||
self.paint_background(gutter_bounds, text_bounds, &layout, cx);
|
||||
if layout.gutter_size.width > Pixels::ZERO {
|
||||
self.paint_gutter(gutter_bounds, &mut layout, editor, cx);
|
||||
}
|
||||
self.paint_text(text_bounds, &mut layout, editor, cx);
|
||||
self.paint_background(gutter_bounds, text_bounds, &layout, cx);
|
||||
if layout.gutter_size.width > Pixels::ZERO {
|
||||
self.paint_gutter(gutter_bounds, &mut layout, editor, cx);
|
||||
}
|
||||
self.paint_text(text_bounds, &mut layout, editor, cx);
|
||||
|
||||
if !layout.blocks.is_empty() {
|
||||
cx.with_element_id(Some("editor_blocks"), |cx| {
|
||||
self.paint_blocks(bounds, &mut layout, editor, cx);
|
||||
})
|
||||
}
|
||||
if !layout.blocks.is_empty() {
|
||||
cx.with_element_id(Some("editor_blocks"), |cx| {
|
||||
self.paint_blocks(bounds, &mut layout, editor, cx);
|
||||
})
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Editor> for EditorElement {
|
||||
fn render(self) -> AnyElement<Editor> {
|
||||
AnyElement::new(self)
|
||||
impl RenderOnce for EditorElement {
|
||||
type Element = Self;
|
||||
|
||||
fn element_id(&self) -> Option<gpui::ElementId> {
|
||||
self.editor.element_id()
|
||||
}
|
||||
|
||||
fn render_once(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3093,17 +3132,17 @@ pub struct LayoutState {
|
||||
show_scrollbars: bool,
|
||||
is_singleton: bool,
|
||||
max_row: u32,
|
||||
context_menu: Option<(DisplayPoint, AnyElement<Editor>)>,
|
||||
context_menu: Option<(DisplayPoint, AnyElement)>,
|
||||
code_actions_indicator: Option<CodeActionsIndicator>,
|
||||
// hover_popovers: Option<(DisplayPoint, Vec<AnyElement<Editor>>)>,
|
||||
fold_indicators: Vec<Option<AnyElement<Editor>>>,
|
||||
// hover_popovers: Option<(DisplayPoint, Vec<AnyElement>)>,
|
||||
fold_indicators: Vec<Option<IconButton>>,
|
||||
tab_invisible: ShapedLine,
|
||||
space_invisible: ShapedLine,
|
||||
}
|
||||
|
||||
struct CodeActionsIndicator {
|
||||
row: u32,
|
||||
element: AnyElement<Editor>,
|
||||
button: IconButton,
|
||||
}
|
||||
|
||||
struct PositionMap {
|
||||
@@ -3188,7 +3227,7 @@ impl PositionMap {
|
||||
|
||||
struct BlockLayout {
|
||||
row: u32,
|
||||
element: AnyElement<Editor>,
|
||||
element: AnyElement,
|
||||
available_space: Size<AvailableSpace>,
|
||||
style: BlockStyle,
|
||||
}
|
||||
@@ -3893,187 +3932,191 @@ fn scale_horizontal_mouse_autoscroll_delta(delta: Pixels) -> f32 {
|
||||
// }
|
||||
// }
|
||||
|
||||
fn register_actions(cx: &mut ViewContext<Editor>) {
|
||||
register_action(cx, Editor::move_left);
|
||||
register_action(cx, Editor::move_right);
|
||||
register_action(cx, Editor::move_down);
|
||||
register_action(cx, Editor::move_up);
|
||||
fn register_actions(view: &View<Editor>, cx: &mut WindowContext) {
|
||||
register_action(view, cx, Editor::move_left);
|
||||
register_action(view, cx, Editor::move_right);
|
||||
register_action(view, cx, Editor::move_down);
|
||||
register_action(view, cx, Editor::move_up);
|
||||
// on_action(cx, Editor::new_file); todo!()
|
||||
// on_action(cx, Editor::new_file_in_direction); todo!()
|
||||
register_action(cx, Editor::cancel);
|
||||
register_action(cx, Editor::newline);
|
||||
register_action(cx, Editor::newline_above);
|
||||
register_action(cx, Editor::newline_below);
|
||||
register_action(cx, Editor::backspace);
|
||||
register_action(cx, Editor::delete);
|
||||
register_action(cx, Editor::tab);
|
||||
register_action(cx, Editor::tab_prev);
|
||||
register_action(cx, Editor::indent);
|
||||
register_action(cx, Editor::outdent);
|
||||
register_action(cx, Editor::delete_line);
|
||||
register_action(cx, Editor::join_lines);
|
||||
register_action(cx, Editor::sort_lines_case_sensitive);
|
||||
register_action(cx, Editor::sort_lines_case_insensitive);
|
||||
register_action(cx, Editor::reverse_lines);
|
||||
register_action(cx, Editor::shuffle_lines);
|
||||
register_action(cx, Editor::convert_to_upper_case);
|
||||
register_action(cx, Editor::convert_to_lower_case);
|
||||
register_action(cx, Editor::convert_to_title_case);
|
||||
register_action(cx, Editor::convert_to_snake_case);
|
||||
register_action(cx, Editor::convert_to_kebab_case);
|
||||
register_action(cx, Editor::convert_to_upper_camel_case);
|
||||
register_action(cx, Editor::convert_to_lower_camel_case);
|
||||
register_action(cx, Editor::delete_to_previous_word_start);
|
||||
register_action(cx, Editor::delete_to_previous_subword_start);
|
||||
register_action(cx, Editor::delete_to_next_word_end);
|
||||
register_action(cx, Editor::delete_to_next_subword_end);
|
||||
register_action(cx, Editor::delete_to_beginning_of_line);
|
||||
register_action(cx, Editor::delete_to_end_of_line);
|
||||
register_action(cx, Editor::cut_to_end_of_line);
|
||||
register_action(cx, Editor::duplicate_line);
|
||||
register_action(cx, Editor::move_line_up);
|
||||
register_action(cx, Editor::move_line_down);
|
||||
register_action(cx, Editor::transpose);
|
||||
register_action(cx, Editor::cut);
|
||||
register_action(cx, Editor::copy);
|
||||
register_action(cx, Editor::paste);
|
||||
register_action(cx, Editor::undo);
|
||||
register_action(cx, Editor::redo);
|
||||
register_action(cx, Editor::move_page_up);
|
||||
register_action(cx, Editor::move_page_down);
|
||||
register_action(cx, Editor::next_screen);
|
||||
register_action(cx, Editor::scroll_cursor_top);
|
||||
register_action(cx, Editor::scroll_cursor_center);
|
||||
register_action(cx, Editor::scroll_cursor_bottom);
|
||||
register_action(cx, |editor, _: &LineDown, cx| {
|
||||
register_action(view, cx, Editor::cancel);
|
||||
register_action(view, cx, Editor::newline);
|
||||
register_action(view, cx, Editor::newline_above);
|
||||
register_action(view, cx, Editor::newline_below);
|
||||
register_action(view, cx, Editor::backspace);
|
||||
register_action(view, cx, Editor::delete);
|
||||
register_action(view, cx, Editor::tab);
|
||||
register_action(view, cx, Editor::tab_prev);
|
||||
register_action(view, cx, Editor::indent);
|
||||
register_action(view, cx, Editor::outdent);
|
||||
register_action(view, cx, Editor::delete_line);
|
||||
register_action(view, cx, Editor::join_lines);
|
||||
register_action(view, cx, Editor::sort_lines_case_sensitive);
|
||||
register_action(view, cx, Editor::sort_lines_case_insensitive);
|
||||
register_action(view, cx, Editor::reverse_lines);
|
||||
register_action(view, cx, Editor::shuffle_lines);
|
||||
register_action(view, cx, Editor::convert_to_upper_case);
|
||||
register_action(view, cx, Editor::convert_to_lower_case);
|
||||
register_action(view, cx, Editor::convert_to_title_case);
|
||||
register_action(view, cx, Editor::convert_to_snake_case);
|
||||
register_action(view, cx, Editor::convert_to_kebab_case);
|
||||
register_action(view, cx, Editor::convert_to_upper_camel_case);
|
||||
register_action(view, cx, Editor::convert_to_lower_camel_case);
|
||||
register_action(view, cx, Editor::delete_to_previous_word_start);
|
||||
register_action(view, cx, Editor::delete_to_previous_subword_start);
|
||||
register_action(view, cx, Editor::delete_to_next_word_end);
|
||||
register_action(view, cx, Editor::delete_to_next_subword_end);
|
||||
register_action(view, cx, Editor::delete_to_beginning_of_line);
|
||||
register_action(view, cx, Editor::delete_to_end_of_line);
|
||||
register_action(view, cx, Editor::cut_to_end_of_line);
|
||||
register_action(view, cx, Editor::duplicate_line);
|
||||
register_action(view, cx, Editor::move_line_up);
|
||||
register_action(view, cx, Editor::move_line_down);
|
||||
register_action(view, cx, Editor::transpose);
|
||||
register_action(view, cx, Editor::cut);
|
||||
register_action(view, cx, Editor::copy);
|
||||
register_action(view, cx, Editor::paste);
|
||||
register_action(view, cx, Editor::undo);
|
||||
register_action(view, cx, Editor::redo);
|
||||
register_action(view, cx, Editor::move_page_up);
|
||||
register_action(view, cx, Editor::move_page_down);
|
||||
register_action(view, cx, Editor::next_screen);
|
||||
register_action(view, cx, Editor::scroll_cursor_top);
|
||||
register_action(view, cx, Editor::scroll_cursor_center);
|
||||
register_action(view, cx, Editor::scroll_cursor_bottom);
|
||||
register_action(view, cx, |editor, _: &LineDown, cx| {
|
||||
editor.scroll_screen(&ScrollAmount::Line(1.), cx)
|
||||
});
|
||||
register_action(cx, |editor, _: &LineUp, cx| {
|
||||
register_action(view, cx, |editor, _: &LineUp, cx| {
|
||||
editor.scroll_screen(&ScrollAmount::Line(-1.), cx)
|
||||
});
|
||||
register_action(cx, |editor, _: &HalfPageDown, cx| {
|
||||
register_action(view, cx, |editor, _: &HalfPageDown, cx| {
|
||||
editor.scroll_screen(&ScrollAmount::Page(0.5), cx)
|
||||
});
|
||||
register_action(cx, |editor, _: &HalfPageUp, cx| {
|
||||
register_action(view, cx, |editor, _: &HalfPageUp, cx| {
|
||||
editor.scroll_screen(&ScrollAmount::Page(-0.5), cx)
|
||||
});
|
||||
register_action(cx, |editor, _: &PageDown, cx| {
|
||||
register_action(view, cx, |editor, _: &PageDown, cx| {
|
||||
editor.scroll_screen(&ScrollAmount::Page(1.), cx)
|
||||
});
|
||||
register_action(cx, |editor, _: &PageUp, cx| {
|
||||
register_action(view, cx, |editor, _: &PageUp, cx| {
|
||||
editor.scroll_screen(&ScrollAmount::Page(-1.), cx)
|
||||
});
|
||||
register_action(cx, Editor::move_to_previous_word_start);
|
||||
register_action(cx, Editor::move_to_previous_subword_start);
|
||||
register_action(cx, Editor::move_to_next_word_end);
|
||||
register_action(cx, Editor::move_to_next_subword_end);
|
||||
register_action(cx, Editor::move_to_beginning_of_line);
|
||||
register_action(cx, Editor::move_to_end_of_line);
|
||||
register_action(cx, Editor::move_to_start_of_paragraph);
|
||||
register_action(cx, Editor::move_to_end_of_paragraph);
|
||||
register_action(cx, Editor::move_to_beginning);
|
||||
register_action(cx, Editor::move_to_end);
|
||||
register_action(cx, Editor::select_up);
|
||||
register_action(cx, Editor::select_down);
|
||||
register_action(cx, Editor::select_left);
|
||||
register_action(cx, Editor::select_right);
|
||||
register_action(cx, Editor::select_to_previous_word_start);
|
||||
register_action(cx, Editor::select_to_previous_subword_start);
|
||||
register_action(cx, Editor::select_to_next_word_end);
|
||||
register_action(cx, Editor::select_to_next_subword_end);
|
||||
register_action(cx, Editor::select_to_beginning_of_line);
|
||||
register_action(cx, Editor::select_to_end_of_line);
|
||||
register_action(cx, Editor::select_to_start_of_paragraph);
|
||||
register_action(cx, Editor::select_to_end_of_paragraph);
|
||||
register_action(cx, Editor::select_to_beginning);
|
||||
register_action(cx, Editor::select_to_end);
|
||||
register_action(cx, Editor::select_all);
|
||||
register_action(cx, |editor, action, cx| {
|
||||
register_action(view, cx, Editor::move_to_previous_word_start);
|
||||
register_action(view, cx, Editor::move_to_previous_subword_start);
|
||||
register_action(view, cx, Editor::move_to_next_word_end);
|
||||
register_action(view, cx, Editor::move_to_next_subword_end);
|
||||
register_action(view, cx, Editor::move_to_beginning_of_line);
|
||||
register_action(view, cx, Editor::move_to_end_of_line);
|
||||
register_action(view, cx, Editor::move_to_start_of_paragraph);
|
||||
register_action(view, cx, Editor::move_to_end_of_paragraph);
|
||||
register_action(view, cx, Editor::move_to_beginning);
|
||||
register_action(view, cx, Editor::move_to_end);
|
||||
register_action(view, cx, Editor::select_up);
|
||||
register_action(view, cx, Editor::select_down);
|
||||
register_action(view, cx, Editor::select_left);
|
||||
register_action(view, cx, Editor::select_right);
|
||||
register_action(view, cx, Editor::select_to_previous_word_start);
|
||||
register_action(view, cx, Editor::select_to_previous_subword_start);
|
||||
register_action(view, cx, Editor::select_to_next_word_end);
|
||||
register_action(view, cx, Editor::select_to_next_subword_end);
|
||||
register_action(view, cx, Editor::select_to_beginning_of_line);
|
||||
register_action(view, cx, Editor::select_to_end_of_line);
|
||||
register_action(view, cx, Editor::select_to_start_of_paragraph);
|
||||
register_action(view, cx, Editor::select_to_end_of_paragraph);
|
||||
register_action(view, cx, Editor::select_to_beginning);
|
||||
register_action(view, cx, Editor::select_to_end);
|
||||
register_action(view, cx, Editor::select_all);
|
||||
register_action(view, cx, |editor, action, cx| {
|
||||
editor.select_all_matches(action, cx).log_err();
|
||||
});
|
||||
register_action(cx, Editor::select_line);
|
||||
register_action(cx, Editor::split_selection_into_lines);
|
||||
register_action(cx, Editor::add_selection_above);
|
||||
register_action(cx, Editor::add_selection_below);
|
||||
register_action(cx, |editor, action, cx| {
|
||||
register_action(view, cx, Editor::select_line);
|
||||
register_action(view, cx, Editor::split_selection_into_lines);
|
||||
register_action(view, cx, Editor::add_selection_above);
|
||||
register_action(view, cx, Editor::add_selection_below);
|
||||
register_action(view, cx, |editor, action, cx| {
|
||||
editor.select_next(action, cx).log_err();
|
||||
});
|
||||
register_action(cx, |editor, action, cx| {
|
||||
register_action(view, cx, |editor, action, cx| {
|
||||
editor.select_previous(action, cx).log_err();
|
||||
});
|
||||
register_action(cx, Editor::toggle_comments);
|
||||
register_action(cx, Editor::select_larger_syntax_node);
|
||||
register_action(cx, Editor::select_smaller_syntax_node);
|
||||
register_action(cx, Editor::move_to_enclosing_bracket);
|
||||
register_action(cx, Editor::undo_selection);
|
||||
register_action(cx, Editor::redo_selection);
|
||||
register_action(cx, Editor::go_to_diagnostic);
|
||||
register_action(cx, Editor::go_to_prev_diagnostic);
|
||||
register_action(cx, Editor::go_to_hunk);
|
||||
register_action(cx, Editor::go_to_prev_hunk);
|
||||
register_action(cx, Editor::go_to_definition);
|
||||
register_action(cx, Editor::go_to_definition_split);
|
||||
register_action(cx, Editor::go_to_type_definition);
|
||||
register_action(cx, Editor::go_to_type_definition_split);
|
||||
register_action(cx, Editor::fold);
|
||||
register_action(cx, Editor::fold_at);
|
||||
register_action(cx, Editor::unfold_lines);
|
||||
register_action(cx, Editor::unfold_at);
|
||||
register_action(cx, Editor::fold_selected_ranges);
|
||||
register_action(cx, Editor::show_completions);
|
||||
register_action(cx, Editor::toggle_code_actions);
|
||||
register_action(view, cx, Editor::toggle_comments);
|
||||
register_action(view, cx, Editor::select_larger_syntax_node);
|
||||
register_action(view, cx, Editor::select_smaller_syntax_node);
|
||||
register_action(view, cx, Editor::move_to_enclosing_bracket);
|
||||
register_action(view, cx, Editor::undo_selection);
|
||||
register_action(view, cx, Editor::redo_selection);
|
||||
register_action(view, cx, Editor::go_to_diagnostic);
|
||||
register_action(view, cx, Editor::go_to_prev_diagnostic);
|
||||
register_action(view, cx, Editor::go_to_hunk);
|
||||
register_action(view, cx, Editor::go_to_prev_hunk);
|
||||
register_action(view, cx, Editor::go_to_definition);
|
||||
register_action(view, cx, Editor::go_to_definition_split);
|
||||
register_action(view, cx, Editor::go_to_type_definition);
|
||||
register_action(view, cx, Editor::go_to_type_definition_split);
|
||||
register_action(view, cx, Editor::fold);
|
||||
register_action(view, cx, Editor::fold_at);
|
||||
register_action(view, cx, Editor::unfold_lines);
|
||||
register_action(view, cx, Editor::unfold_at);
|
||||
register_action(view, cx, Editor::fold_selected_ranges);
|
||||
register_action(view, cx, Editor::show_completions);
|
||||
register_action(view, cx, Editor::toggle_code_actions);
|
||||
// on_action(cx, Editor::open_excerpts); todo!()
|
||||
register_action(cx, Editor::toggle_soft_wrap);
|
||||
register_action(cx, Editor::toggle_inlay_hints);
|
||||
register_action(cx, Editor::reveal_in_finder);
|
||||
register_action(cx, Editor::copy_path);
|
||||
register_action(cx, Editor::copy_relative_path);
|
||||
register_action(cx, Editor::copy_highlight_json);
|
||||
register_action(cx, |editor, action, cx| {
|
||||
register_action(view, cx, Editor::toggle_soft_wrap);
|
||||
register_action(view, cx, Editor::toggle_inlay_hints);
|
||||
register_action(view, cx, Editor::reveal_in_finder);
|
||||
register_action(view, cx, Editor::copy_path);
|
||||
register_action(view, cx, Editor::copy_relative_path);
|
||||
register_action(view, cx, Editor::copy_highlight_json);
|
||||
register_action(view, cx, |editor, action, cx| {
|
||||
editor
|
||||
.format(action, cx)
|
||||
.map(|task| task.detach_and_log_err(cx));
|
||||
});
|
||||
register_action(cx, Editor::restart_language_server);
|
||||
register_action(cx, Editor::show_character_palette);
|
||||
register_action(view, cx, Editor::restart_language_server);
|
||||
register_action(view, cx, Editor::show_character_palette);
|
||||
// on_action(cx, Editor::confirm_completion); todo!()
|
||||
register_action(cx, |editor, action, cx| {
|
||||
register_action(view, cx, |editor, action, cx| {
|
||||
editor
|
||||
.confirm_code_action(action, cx)
|
||||
.map(|task| task.detach_and_log_err(cx));
|
||||
});
|
||||
register_action(cx, |editor, action, cx| {
|
||||
register_action(view, cx, |editor, action, cx| {
|
||||
editor
|
||||
.rename(action, cx)
|
||||
.map(|task| task.detach_and_log_err(cx));
|
||||
});
|
||||
register_action(cx, |editor, action, cx| {
|
||||
register_action(view, cx, |editor, action, cx| {
|
||||
editor
|
||||
.confirm_rename(action, cx)
|
||||
.map(|task| task.detach_and_log_err(cx));
|
||||
});
|
||||
register_action(cx, |editor, action, cx| {
|
||||
register_action(view, cx, |editor, action, cx| {
|
||||
editor
|
||||
.find_all_references(action, cx)
|
||||
.map(|task| task.detach_and_log_err(cx));
|
||||
});
|
||||
register_action(cx, Editor::next_copilot_suggestion);
|
||||
register_action(cx, Editor::previous_copilot_suggestion);
|
||||
register_action(cx, Editor::copilot_suggest);
|
||||
register_action(cx, Editor::context_menu_first);
|
||||
register_action(cx, Editor::context_menu_prev);
|
||||
register_action(cx, Editor::context_menu_next);
|
||||
register_action(cx, Editor::context_menu_last);
|
||||
register_action(view, cx, Editor::next_copilot_suggestion);
|
||||
register_action(view, cx, Editor::previous_copilot_suggestion);
|
||||
register_action(view, cx, Editor::copilot_suggest);
|
||||
register_action(view, cx, Editor::context_menu_first);
|
||||
register_action(view, cx, Editor::context_menu_prev);
|
||||
register_action(view, cx, Editor::context_menu_next);
|
||||
register_action(view, cx, Editor::context_menu_last);
|
||||
}
|
||||
|
||||
fn register_action<T: Action>(
|
||||
cx: &mut ViewContext<Editor>,
|
||||
view: &View<Editor>,
|
||||
cx: &mut WindowContext,
|
||||
listener: impl Fn(&mut Editor, &T, &mut ViewContext<Editor>) + 'static,
|
||||
) {
|
||||
cx.on_action(TypeId::of::<T>(), move |editor, action, phase, cx| {
|
||||
let view = view.clone();
|
||||
cx.on_action(TypeId::of::<T>(), move |action, phase, cx| {
|
||||
let action = action.downcast_ref().unwrap();
|
||||
if phase == DispatchPhase::Bubble {
|
||||
listener(editor, action, cx);
|
||||
view.update(cx, |editor, cx| {
|
||||
listener(editor, action, cx);
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -422,7 +422,7 @@ impl HoverState {
|
||||
visible_rows: Range<u32>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> Option<(DisplayPoint, Vec<AnyElement<Editor>>)> {
|
||||
) -> Option<(DisplayPoint, Vec<AnyElement>)> {
|
||||
todo!("old version below")
|
||||
}
|
||||
// // If there is a diagnostic, position the popovers based on that.
|
||||
@@ -504,7 +504,7 @@ pub struct DiagnosticPopover {
|
||||
}
|
||||
|
||||
impl DiagnosticPopover {
|
||||
pub fn render(&self, style: &EditorStyle, cx: &mut ViewContext<Editor>) -> AnyElement<Editor> {
|
||||
pub fn render(&self, style: &EditorStyle, cx: &mut ViewContext<Editor>) -> AnyElement {
|
||||
todo!()
|
||||
// enum PrimaryDiagnostic {}
|
||||
|
||||
|
||||
+33
-36
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
editor_settings::SeedQuerySetting, link_go_to_definition::hide_link_definition,
|
||||
movement::surrounding_word, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor,
|
||||
EditorSettings, Event, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot,
|
||||
EditorEvent, EditorSettings, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot,
|
||||
NavigationData, ToPoint as _,
|
||||
};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
@@ -9,8 +9,8 @@ use collections::HashSet;
|
||||
use futures::future::try_join_all;
|
||||
use gpui::{
|
||||
div, point, AnyElement, AppContext, AsyncAppContext, Entity, EntityId, EventEmitter,
|
||||
FocusHandle, Model, ParentComponent, Pixels, SharedString, Styled, Subscription, Task, View,
|
||||
ViewContext, VisualContext, WeakView,
|
||||
FocusHandle, Model, ParentElement, Pixels, SharedString, Styled, Subscription, Task, View,
|
||||
ViewContext, VisualContext, WeakView, WindowContext,
|
||||
};
|
||||
use language::{
|
||||
proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, CharKind, OffsetRangeExt,
|
||||
@@ -30,7 +30,7 @@ use std::{
|
||||
};
|
||||
use text::Selection;
|
||||
use theme::{ActiveTheme, Theme};
|
||||
use ui::{Label, TextColor};
|
||||
use ui::{Color, Label};
|
||||
use util::{paths::PathExt, ResultExt, TryFutureExt};
|
||||
use workspace::item::{BreadcrumbText, FollowEvent, FollowableEvents, FollowableItemHandle};
|
||||
use workspace::{
|
||||
@@ -41,11 +41,12 @@ use workspace::{
|
||||
|
||||
pub const MAX_TAB_TITLE_LEN: usize = 24;
|
||||
|
||||
impl FollowableEvents for Event {
|
||||
impl FollowableEvents for EditorEvent {
|
||||
fn to_follow_event(&self) -> Option<workspace::item::FollowEvent> {
|
||||
match self {
|
||||
Event::Edited => Some(FollowEvent::Unfollow),
|
||||
Event::SelectionsChanged { local } | Event::ScrollPositionChanged { local, .. } => {
|
||||
EditorEvent::Edited => Some(FollowEvent::Unfollow),
|
||||
EditorEvent::SelectionsChanged { local }
|
||||
| EditorEvent::ScrollPositionChanged { local, .. } => {
|
||||
if *local {
|
||||
Some(FollowEvent::Unfollow)
|
||||
} else {
|
||||
@@ -60,7 +61,7 @@ impl FollowableEvents for Event {
|
||||
impl EventEmitter<ItemEvent> for Editor {}
|
||||
|
||||
impl FollowableItem for Editor {
|
||||
type FollowableEvent = Event;
|
||||
type FollowableEvent = EditorEvent;
|
||||
fn remote_id(&self) -> Option<ViewId> {
|
||||
self.remote_id
|
||||
}
|
||||
@@ -248,7 +249,7 @@ impl FollowableItem for Editor {
|
||||
|
||||
match update {
|
||||
proto::update_view::Variant::Editor(update) => match event {
|
||||
Event::ExcerptsAdded {
|
||||
EditorEvent::ExcerptsAdded {
|
||||
buffer,
|
||||
predecessor,
|
||||
excerpts,
|
||||
@@ -269,20 +270,20 @@ impl FollowableItem for Editor {
|
||||
}
|
||||
true
|
||||
}
|
||||
Event::ExcerptsRemoved { ids } => {
|
||||
EditorEvent::ExcerptsRemoved { ids } => {
|
||||
update
|
||||
.deleted_excerpts
|
||||
.extend(ids.iter().map(ExcerptId::to_proto));
|
||||
true
|
||||
}
|
||||
Event::ScrollPositionChanged { .. } => {
|
||||
EditorEvent::ScrollPositionChanged { .. } => {
|
||||
let scroll_anchor = self.scroll_manager.anchor();
|
||||
update.scroll_top_anchor = Some(serialize_anchor(&scroll_anchor.anchor));
|
||||
update.scroll_x = scroll_anchor.offset.x;
|
||||
update.scroll_y = scroll_anchor.offset.y;
|
||||
true
|
||||
}
|
||||
Event::SelectionsChanged { .. } => {
|
||||
EditorEvent::SelectionsChanged { .. } => {
|
||||
update.selections = self
|
||||
.selections
|
||||
.disjoint_anchors()
|
||||
@@ -583,7 +584,7 @@ impl Item for Editor {
|
||||
Some(path.to_string_lossy().to_string().into())
|
||||
}
|
||||
|
||||
fn tab_content<T: 'static>(&self, detail: Option<usize>, cx: &AppContext) -> AnyElement<T> {
|
||||
fn tab_content(&self, detail: Option<usize>, cx: &WindowContext) -> AnyElement {
|
||||
let theme = cx.theme();
|
||||
|
||||
AnyElement::new(
|
||||
@@ -603,7 +604,7 @@ impl Item for Editor {
|
||||
&description,
|
||||
MAX_TAB_TITLE_LEN,
|
||||
))
|
||||
.color(TextColor::Muted),
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
})),
|
||||
@@ -760,7 +761,7 @@ impl Item for Editor {
|
||||
}
|
||||
|
||||
fn breadcrumb_location(&self) -> ToolbarItemLocation {
|
||||
ToolbarItemLocation::PrimaryLeft { flex: None }
|
||||
ToolbarItemLocation::PrimaryLeft
|
||||
}
|
||||
|
||||
fn breadcrumbs(&self, variant: &Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
|
||||
@@ -906,17 +907,15 @@ impl SearchableItem for Editor {
|
||||
type Match = Range<Anchor>;
|
||||
|
||||
fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
|
||||
todo!()
|
||||
// self.clear_background_highlights::<BufferSearchHighlights>(cx);
|
||||
self.clear_background_highlights::<BufferSearchHighlights>(cx);
|
||||
}
|
||||
|
||||
fn update_matches(&mut self, matches: Vec<Range<Anchor>>, cx: &mut ViewContext<Self>) {
|
||||
todo!()
|
||||
// self.highlight_background::<BufferSearchHighlights>(
|
||||
// matches,
|
||||
// |theme| theme.search.match_background,
|
||||
// cx,
|
||||
// );
|
||||
self.highlight_background::<BufferSearchHighlights>(
|
||||
matches,
|
||||
|theme| theme.title_bar_background, // todo: update theme
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
|
||||
@@ -951,22 +950,20 @@ impl SearchableItem for Editor {
|
||||
matches: Vec<Range<Anchor>>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
todo!()
|
||||
// self.unfold_ranges([matches[index].clone()], false, true, cx);
|
||||
// let range = self.range_for_match(&matches[index]);
|
||||
// self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
// s.select_ranges([range]);
|
||||
// })
|
||||
self.unfold_ranges([matches[index].clone()], false, true, cx);
|
||||
let range = self.range_for_match(&matches[index]);
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.select_ranges([range]);
|
||||
})
|
||||
}
|
||||
|
||||
fn select_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>) {
|
||||
todo!()
|
||||
// self.unfold_ranges(matches.clone(), false, false, cx);
|
||||
// let mut ranges = Vec::new();
|
||||
// for m in &matches {
|
||||
// ranges.push(self.range_for_match(&m))
|
||||
// }
|
||||
// self.change_selections(None, cx, |s| s.select_ranges(ranges));
|
||||
self.unfold_ranges(matches.clone(), false, false, cx);
|
||||
let mut ranges = Vec::new();
|
||||
for m in &matches {
|
||||
ranges.push(self.range_for_match(&m))
|
||||
}
|
||||
self.change_selections(None, cx, |s| s.select_ranges(ranges));
|
||||
}
|
||||
fn replace(
|
||||
&mut self,
|
||||
|
||||
@@ -6,8 +6,8 @@ use crate::{
|
||||
display_map::{DisplaySnapshot, ToDisplayPoint},
|
||||
hover_popover::hide_hover,
|
||||
persistence::DB,
|
||||
Anchor, DisplayPoint, Editor, EditorMode, Event, InlayHintRefreshReason, MultiBufferSnapshot,
|
||||
ToPoint,
|
||||
Anchor, DisplayPoint, Editor, EditorEvent, EditorMode, InlayHintRefreshReason,
|
||||
MultiBufferSnapshot, ToPoint,
|
||||
};
|
||||
use gpui::{point, px, AppContext, Entity, Pixels, Styled, Task, ViewContext};
|
||||
use language::{Bias, Point};
|
||||
@@ -224,7 +224,7 @@ impl ScrollManager {
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
self.anchor = anchor;
|
||||
cx.emit(Event::ScrollPositionChanged { local, autoscroll });
|
||||
cx.emit(EditorEvent::ScrollPositionChanged { local, autoscroll });
|
||||
self.show_scrollbar(cx);
|
||||
self.autoscroll_request.take();
|
||||
if let Some(workspace_id) = workspace_id {
|
||||
|
||||
@@ -71,7 +71,8 @@ impl<'a> EditorTestContext<'a> {
|
||||
&self,
|
||||
predicate: impl FnMut(&Editor, &AppContext) -> bool,
|
||||
) -> impl Future<Output = ()> {
|
||||
self.editor.condition::<crate::Event>(&self.cx, predicate)
|
||||
self.editor
|
||||
.condition::<crate::EditorEvent>(&self.cx, predicate)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
|
||||
@@ -2,9 +2,9 @@ use collections::HashMap;
|
||||
use editor::{scroll::autoscroll::Autoscroll, Bias, Editor};
|
||||
use fuzzy::{CharBag, PathMatch, PathMatchCandidate};
|
||||
use gpui::{
|
||||
actions, div, AppContext, Component, Dismiss, Div, FocusHandle, InteractiveComponent,
|
||||
ManagedView, Model, ParentComponent, Render, Styled, Task, View, ViewContext, VisualContext,
|
||||
WeakView,
|
||||
actions, div, AppContext, Div, EventEmitter, FocusHandle, FocusableView, InteractiveElement,
|
||||
Manager, Model, ParentElement, Render, RenderOnce, Styled, Task, View, ViewContext,
|
||||
VisualContext, WeakView,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
|
||||
@@ -111,13 +111,14 @@ impl FileFinder {
|
||||
}
|
||||
}
|
||||
|
||||
impl ManagedView for FileFinder {
|
||||
impl EventEmitter<Manager> for FileFinder {}
|
||||
impl FocusableView for FileFinder {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
self.picker.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
impl Render for FileFinder {
|
||||
type Element = Div<Self>;
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
v_stack().w_96().child(self.picker.clone())
|
||||
@@ -529,7 +530,7 @@ impl FileFinderDelegate {
|
||||
}
|
||||
|
||||
impl PickerDelegate for FileFinderDelegate {
|
||||
type ListItem = Div<Picker<Self>>;
|
||||
type ListItem = Div;
|
||||
|
||||
fn placeholder_text(&self) -> Arc<str> {
|
||||
"Search project files...".into()
|
||||
@@ -688,7 +689,9 @@ impl PickerDelegate for FileFinderDelegate {
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
finder.update(&mut cx, |_, cx| cx.emit(Dismiss)).ok()?;
|
||||
finder
|
||||
.update(&mut cx, |_, cx| cx.emit(Manager::Dismiss))
|
||||
.ok()?;
|
||||
|
||||
Some(())
|
||||
})
|
||||
@@ -699,7 +702,7 @@ impl PickerDelegate for FileFinderDelegate {
|
||||
|
||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<FileFinderDelegate>>) {
|
||||
self.file_finder
|
||||
.update(cx, |_, cx| cx.emit(Dismiss))
|
||||
.update(cx, |_, cx| cx.emit(Manager::Dismiss))
|
||||
.log_err();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Editor};
|
||||
use gpui::{
|
||||
actions, div, prelude::*, AppContext, Dismiss, Div, FocusHandle, ManagedView, ParentComponent,
|
||||
actions, div, prelude::*, AppContext, Div, EventEmitter, FocusHandle, FocusableView, Manager,
|
||||
Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext, WindowContext,
|
||||
};
|
||||
use text::{Bias, Point};
|
||||
use theme::ActiveTheme;
|
||||
use ui::{h_stack, v_stack, Label, StyledExt, TextColor};
|
||||
use ui::{h_stack, v_stack, Color, Label, StyledExt};
|
||||
use util::paths::FILE_ROW_COLUMN_DELIMITER;
|
||||
use workspace::Workspace;
|
||||
|
||||
@@ -23,11 +23,12 @@ pub struct GoToLine {
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl ManagedView for GoToLine {
|
||||
impl FocusableView for GoToLine {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
self.line_editor.focus_handle(cx)
|
||||
self.active_editor.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
impl EventEmitter<Manager> for GoToLine {}
|
||||
|
||||
impl GoToLine {
|
||||
fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
|
||||
@@ -82,13 +83,13 @@ impl GoToLine {
|
||||
fn on_line_editor_event(
|
||||
&mut self,
|
||||
_: View<Editor>,
|
||||
event: &editor::Event,
|
||||
event: &editor::EditorEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
// todo!() this isn't working...
|
||||
editor::Event::Blurred => cx.emit(Dismiss),
|
||||
editor::Event::BufferEdited { .. } => self.highlight_current_line(cx),
|
||||
editor::EditorEvent::Blurred => cx.emit(Manager::Dismiss),
|
||||
editor::EditorEvent::BufferEdited { .. } => self.highlight_current_line(cx),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -122,7 +123,7 @@ impl GoToLine {
|
||||
}
|
||||
|
||||
fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(Dismiss);
|
||||
cx.emit(Manager::Dismiss);
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
|
||||
@@ -139,19 +140,19 @@ impl GoToLine {
|
||||
self.prev_scroll_position.take();
|
||||
}
|
||||
|
||||
cx.emit(Dismiss);
|
||||
cx.emit(Manager::Dismiss);
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for GoToLine {
|
||||
type Element = Div<Self>;
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
div()
|
||||
.elevation_2(cx)
|
||||
.key_context("GoToLine")
|
||||
.on_action(Self::cancel)
|
||||
.on_action(Self::confirm)
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.on_action(cx.listener(Self::confirm))
|
||||
.w_96()
|
||||
.child(
|
||||
v_stack()
|
||||
@@ -175,7 +176,7 @@ impl Render for GoToLine {
|
||||
.justify_between()
|
||||
.px_2()
|
||||
.py_1()
|
||||
.child(Label::new(self.current_text.clone()).color(TextColor::Muted)),
|
||||
.child(Label::new(self.current_text.clone()).color(Color::Muted)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ serde_derive.workspace = true
|
||||
serde_json.workspace = true
|
||||
smallvec.workspace = true
|
||||
smol.workspace = true
|
||||
taffy = { git = "https://github.com/DioxusLabs/taffy", rev = "4fb530bdd71609bb1d3f76c6a8bde1ba82805d5e" }
|
||||
taffy = { git = "https://github.com/DioxusLabs/taffy", rev = "1876f72bee5e376023eaa518aa7b8a34c769bd1b" }
|
||||
thiserror.workspace = true
|
||||
time.workspace = true
|
||||
tiny-skia = "0.5"
|
||||
|
||||
+10
-6
@@ -14,7 +14,7 @@ use smallvec::SmallVec;
|
||||
pub use test_context::*;
|
||||
|
||||
use crate::{
|
||||
current_platform, image_cache::ImageCache, Action, ActionRegistry, AnyBox, AnyView,
|
||||
current_platform, image_cache::ImageCache, Action, ActionRegistry, Any, AnyView,
|
||||
AnyWindowHandle, AppMetadata, AssetSource, BackgroundExecutor, ClipboardItem, Context,
|
||||
DispatchPhase, DisplayId, Entity, EventEmitter, FocusEvent, FocusHandle, FocusId,
|
||||
ForegroundExecutor, KeyBinding, Keymap, LayoutId, PathPromptOptions, Pixels, Platform,
|
||||
@@ -28,7 +28,7 @@ use futures::{channel::oneshot, future::LocalBoxFuture, Future};
|
||||
use parking_lot::Mutex;
|
||||
use slotmap::SlotMap;
|
||||
use std::{
|
||||
any::{type_name, Any, TypeId},
|
||||
any::{type_name, TypeId},
|
||||
cell::{Ref, RefCell, RefMut},
|
||||
marker::PhantomData,
|
||||
mem,
|
||||
@@ -194,7 +194,7 @@ pub struct AppContext {
|
||||
asset_source: Arc<dyn AssetSource>,
|
||||
pub(crate) image_cache: ImageCache,
|
||||
pub(crate) text_style_stack: Vec<TextStyleRefinement>,
|
||||
pub(crate) globals_by_type: HashMap<TypeId, AnyBox>,
|
||||
pub(crate) globals_by_type: HashMap<TypeId, Box<dyn Any>>,
|
||||
pub(crate) entities: EntityMap,
|
||||
pub(crate) new_view_observers: SubscriberSet<TypeId, NewViewListener>,
|
||||
pub(crate) windows: SlotMap<WindowId, Option<Window>>,
|
||||
@@ -424,7 +424,7 @@ impl AppContext {
|
||||
/// Opens a new window with the given option and the root view returned by the given function.
|
||||
/// The function is invoked with a `WindowContext`, which can be used to interact with window-specific
|
||||
/// functionality.
|
||||
pub fn open_window<V: Render>(
|
||||
pub fn open_window<V: 'static + Render>(
|
||||
&mut self,
|
||||
options: crate::WindowOptions,
|
||||
build_root_view: impl FnOnce(&mut WindowContext) -> View<V>,
|
||||
@@ -492,6 +492,10 @@ impl AppContext {
|
||||
self.platform.open_url(url);
|
||||
}
|
||||
|
||||
pub fn app_path(&self) -> Result<PathBuf> {
|
||||
self.platform.app_path()
|
||||
}
|
||||
|
||||
pub fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf> {
|
||||
self.platform.path_for_auxiliary_executable(name)
|
||||
}
|
||||
@@ -1100,12 +1104,12 @@ pub(crate) enum Effect {
|
||||
|
||||
/// Wraps a global variable value during `update_global` while the value has been moved to the stack.
|
||||
pub(crate) struct GlobalLease<G: 'static> {
|
||||
global: AnyBox,
|
||||
global: Box<dyn Any>,
|
||||
global_type: PhantomData<G>,
|
||||
}
|
||||
|
||||
impl<G: 'static> GlobalLease<G> {
|
||||
fn new(global: AnyBox) -> Self {
|
||||
fn new(global: Box<dyn Any>) -> Self {
|
||||
GlobalLease {
|
||||
global,
|
||||
global_type: PhantomData,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
AnyView, AnyWindowHandle, AppCell, AppContext, BackgroundExecutor, Context, FocusableView,
|
||||
ForegroundExecutor, Model, ModelContext, Render, Result, Task, View, ViewContext,
|
||||
ForegroundExecutor, Manager, Model, ModelContext, Render, Result, Task, View, ViewContext,
|
||||
VisualContext, WindowContext, WindowHandle,
|
||||
};
|
||||
use anyhow::{anyhow, Context as _};
|
||||
@@ -115,7 +115,7 @@ impl AsyncAppContext {
|
||||
build_root_view: impl FnOnce(&mut WindowContext) -> View<V>,
|
||||
) -> Result<WindowHandle<V>>
|
||||
where
|
||||
V: Render,
|
||||
V: 'static + Render,
|
||||
{
|
||||
let app = self
|
||||
.app
|
||||
@@ -306,7 +306,7 @@ impl VisualContext for AsyncWindowContext {
|
||||
build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V,
|
||||
) -> Self::Result<View<V>>
|
||||
where
|
||||
V: Render,
|
||||
V: 'static + Render,
|
||||
{
|
||||
self.window
|
||||
.update(self, |_, cx| cx.replace_root_view(build_view))
|
||||
@@ -320,4 +320,13 @@ impl VisualContext for AsyncWindowContext {
|
||||
view.read(cx).focus_handle(cx).clone().focus(cx);
|
||||
})
|
||||
}
|
||||
|
||||
fn dismiss_view<V>(&mut self, view: &View<V>) -> Self::Result<()>
|
||||
where
|
||||
V: crate::ManagedView,
|
||||
{
|
||||
self.window.update(self, |_, cx| {
|
||||
view.update(cx, |_, cx| cx.emit(Manager::Dismiss))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use crate::{private::Sealed, AnyBox, AppContext, Context, Entity, ModelContext};
|
||||
use crate::{private::Sealed, AppContext, Context, Entity, ModelContext};
|
||||
use anyhow::{anyhow, Result};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use parking_lot::{RwLock, RwLockUpgradableReadGuard};
|
||||
use slotmap::{SecondaryMap, SlotMap};
|
||||
use std::{
|
||||
any::{type_name, TypeId},
|
||||
any::{type_name, Any, TypeId},
|
||||
fmt::{self, Display},
|
||||
hash::{Hash, Hasher},
|
||||
marker::PhantomData,
|
||||
@@ -31,7 +31,7 @@ impl Display for EntityId {
|
||||
}
|
||||
|
||||
pub(crate) struct EntityMap {
|
||||
entities: SecondaryMap<EntityId, AnyBox>,
|
||||
entities: SecondaryMap<EntityId, Box<dyn Any>>,
|
||||
ref_counts: Arc<RwLock<EntityRefCounts>>,
|
||||
}
|
||||
|
||||
@@ -71,11 +71,12 @@ impl EntityMap {
|
||||
#[track_caller]
|
||||
pub fn lease<'a, T>(&mut self, model: &'a Model<T>) -> Lease<'a, T> {
|
||||
self.assert_valid_context(model);
|
||||
let entity = Some(
|
||||
self.entities
|
||||
.remove(model.entity_id)
|
||||
.expect("Circular entity lease. Is the entity already being updated?"),
|
||||
);
|
||||
let entity = Some(self.entities.remove(model.entity_id).unwrap_or_else(|| {
|
||||
panic!(
|
||||
"Circular entity lease of {}. Is it already being updated?",
|
||||
std::any::type_name::<T>()
|
||||
)
|
||||
}));
|
||||
Lease {
|
||||
model,
|
||||
entity,
|
||||
@@ -101,7 +102,7 @@ impl EntityMap {
|
||||
);
|
||||
}
|
||||
|
||||
pub fn take_dropped(&mut self) -> Vec<(EntityId, AnyBox)> {
|
||||
pub fn take_dropped(&mut self) -> Vec<(EntityId, Box<dyn Any>)> {
|
||||
let mut ref_counts = self.ref_counts.write();
|
||||
let dropped_entity_ids = mem::take(&mut ref_counts.dropped_entity_ids);
|
||||
|
||||
@@ -121,7 +122,7 @@ impl EntityMap {
|
||||
}
|
||||
|
||||
pub struct Lease<'a, T> {
|
||||
entity: Option<AnyBox>,
|
||||
entity: Option<Box<dyn Any>>,
|
||||
pub model: &'a Model<T>,
|
||||
entity_type: PhantomData<T>,
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use crate::{
|
||||
div, Action, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext,
|
||||
BackgroundExecutor, Context, Div, EventEmitter, ForegroundExecutor, InputEvent, KeyDownEvent,
|
||||
Keystroke, Model, ModelContext, Render, Result, Task, TestDispatcher, TestPlatform, TestWindow,
|
||||
View, ViewContext, VisualContext, WindowContext, WindowHandle, WindowOptions,
|
||||
BackgroundExecutor, Context, Div, Entity, EventEmitter, ForegroundExecutor, InputEvent,
|
||||
KeyDownEvent, Keystroke, Model, ModelContext, Render, Result, Task, TestDispatcher,
|
||||
TestPlatform, TestWindow, View, ViewContext, VisualContext, WindowContext, WindowHandle,
|
||||
WindowOptions,
|
||||
};
|
||||
use anyhow::{anyhow, bail};
|
||||
use futures::{Stream, StreamExt};
|
||||
@@ -126,7 +127,7 @@ impl TestAppContext {
|
||||
pub fn add_window<F, V>(&mut self, build_window: F) -> WindowHandle<V>
|
||||
where
|
||||
F: FnOnce(&mut ViewContext<V>) -> V,
|
||||
V: Render,
|
||||
V: 'static + Render,
|
||||
{
|
||||
let mut cx = self.app.borrow_mut();
|
||||
cx.open_window(WindowOptions::default(), |cx| cx.build_view(build_window))
|
||||
@@ -143,7 +144,7 @@ impl TestAppContext {
|
||||
pub fn add_window_view<F, V>(&mut self, build_window: F) -> (View<V>, &mut VisualTestContext)
|
||||
where
|
||||
F: FnOnce(&mut ViewContext<V>) -> V,
|
||||
V: Render,
|
||||
V: 'static + Render,
|
||||
{
|
||||
let mut cx = self.app.borrow_mut();
|
||||
let window = cx.open_window(WindowOptions::default(), |cx| cx.build_view(build_window));
|
||||
@@ -296,21 +297,19 @@ impl TestAppContext {
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn notifications<T: 'static>(&mut self, entity: &Model<T>) -> impl Stream<Item = ()> {
|
||||
pub fn notifications<T: 'static>(&mut self, entity: &impl Entity<T>) -> impl Stream<Item = ()> {
|
||||
let (tx, rx) = futures::channel::mpsc::unbounded();
|
||||
|
||||
entity.update(self, move |_, cx: &mut ModelContext<T>| {
|
||||
self.update(|cx| {
|
||||
cx.observe(entity, {
|
||||
let tx = tx.clone();
|
||||
move |_, _, _| {
|
||||
move |_, _| {
|
||||
let _ = tx.unbounded_send(());
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.on_release(move |_, _| tx.close_channel()).detach();
|
||||
cx.observe_release(entity, move |_, _| tx.close_channel())
|
||||
.detach()
|
||||
});
|
||||
|
||||
rx
|
||||
}
|
||||
|
||||
@@ -386,6 +385,32 @@ impl<T: Send> Model<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: 'static> View<V> {
|
||||
pub fn next_notification(&self, cx: &TestAppContext) -> impl Future<Output = ()> {
|
||||
use postage::prelude::{Sink as _, Stream as _};
|
||||
|
||||
let (mut tx, mut rx) = postage::mpsc::channel(1);
|
||||
let mut cx = cx.app.app.borrow_mut();
|
||||
let subscription = cx.observe(self, move |_, _| {
|
||||
tx.try_send(()).ok();
|
||||
});
|
||||
|
||||
let duration = if std::env::var("CI").is_ok() {
|
||||
Duration::from_secs(5)
|
||||
} else {
|
||||
Duration::from_secs(1)
|
||||
};
|
||||
|
||||
async move {
|
||||
let notification = crate::util::timeout(duration, rx.recv())
|
||||
.await
|
||||
.expect("next notification timed out");
|
||||
drop(subscription);
|
||||
notification.expect("model dropped while test was waiting for its next notification")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> View<V> {
|
||||
pub fn condition<Evt>(
|
||||
&self,
|
||||
@@ -565,7 +590,7 @@ impl<'a> VisualContext for VisualTestContext<'a> {
|
||||
build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V,
|
||||
) -> Self::Result<View<V>>
|
||||
where
|
||||
V: Render,
|
||||
V: 'static + Render,
|
||||
{
|
||||
self.window
|
||||
.update(self.cx, |_, cx| cx.replace_root_view(build_view))
|
||||
@@ -579,6 +604,17 @@ impl<'a> VisualContext for VisualTestContext<'a> {
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn dismiss_view<V>(&mut self, view: &View<V>) -> Self::Result<()>
|
||||
where
|
||||
V: crate::ManagedView,
|
||||
{
|
||||
self.window
|
||||
.update(self.cx, |_, cx| {
|
||||
view.update(cx, |_, cx| cx.emit(crate::Manager::Dismiss))
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl AnyWindowHandle {
|
||||
@@ -594,7 +630,7 @@ impl AnyWindowHandle {
|
||||
pub struct EmptyView {}
|
||||
|
||||
impl Render for EmptyView {
|
||||
type Element = Div<Self>;
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, _cx: &mut crate::ViewContext<Self>) -> Self::Element {
|
||||
div()
|
||||
|
||||
+474
-321
@@ -1,308 +1,63 @@
|
||||
use crate::{
|
||||
AvailableSpace, BorrowWindow, Bounds, ElementId, LayoutId, Pixels, Point, Size, ViewContext,
|
||||
WindowContext,
|
||||
};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
pub(crate) use smallvec::SmallVec;
|
||||
use std::{any::Any, fmt::Debug, mem};
|
||||
use std::{any::Any, fmt::Debug};
|
||||
|
||||
pub trait Element<V: 'static> {
|
||||
type ElementState: 'static;
|
||||
pub trait Render: 'static + Sized {
|
||||
type Element: Element + 'static;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element;
|
||||
}
|
||||
|
||||
pub trait RenderOnce: Sized {
|
||||
type Element: Element + 'static;
|
||||
|
||||
fn element_id(&self) -> Option<ElementId>;
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
view_state: &mut V,
|
||||
element_state: Option<Self::ElementState>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> (LayoutId, Self::ElementState);
|
||||
fn render_once(self) -> Self::Element;
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
view_state: &mut V,
|
||||
element_state: &mut Self::ElementState,
|
||||
cx: &mut ViewContext<V>,
|
||||
);
|
||||
fn render_into_any(self) -> AnyElement {
|
||||
self.render_once().into_any()
|
||||
}
|
||||
|
||||
fn draw<T, R>(
|
||||
self,
|
||||
origin: Point<Pixels>,
|
||||
available_space: Size<T>,
|
||||
view_state: &mut V,
|
||||
cx: &mut ViewContext<V>,
|
||||
f: impl FnOnce(&Self::ElementState, &mut ViewContext<V>) -> R,
|
||||
cx: &mut WindowContext,
|
||||
f: impl FnOnce(&mut <Self::Element as Element>::State, &mut WindowContext) -> R,
|
||||
) -> R
|
||||
where
|
||||
Self: Sized,
|
||||
T: Clone + Default + Debug + Into<AvailableSpace>,
|
||||
{
|
||||
let mut element = RenderedElement {
|
||||
element: self,
|
||||
phase: ElementRenderPhase::Start,
|
||||
let element = self.render_once();
|
||||
let element_id = element.element_id();
|
||||
let element = DrawableElement {
|
||||
element: Some(element),
|
||||
phase: ElementDrawPhase::Start,
|
||||
};
|
||||
element.draw(origin, available_space.map(Into::into), view_state, cx);
|
||||
if let ElementRenderPhase::Painted { frame_state } = &element.phase {
|
||||
if let Some(frame_state) = frame_state.as_ref() {
|
||||
f(&frame_state, cx)
|
||||
} else {
|
||||
let element_id = element
|
||||
.element
|
||||
.element_id()
|
||||
.expect("we either have some frame_state or some element_id");
|
||||
cx.with_element_state(element_id, |element_state, cx| {
|
||||
let element_state = element_state.unwrap();
|
||||
let result = f(&element_state, cx);
|
||||
(result, element_state)
|
||||
})
|
||||
}
|
||||
|
||||
let frame_state =
|
||||
DrawableElement::draw(element, origin, available_space.map(Into::into), cx);
|
||||
|
||||
if let Some(mut frame_state) = frame_state {
|
||||
f(&mut frame_state, cx)
|
||||
} else {
|
||||
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
|
||||
where
|
||||
Self: Sized,
|
||||
U: Component<V>,
|
||||
U: RenderOnce,
|
||||
{
|
||||
f(self)
|
||||
}
|
||||
@@ -328,65 +83,463 @@ pub trait Component<V> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> Component<V> for AnyElement<V> {
|
||||
fn render(self) -> AnyElement<V> {
|
||||
self
|
||||
pub trait Element: 'static + RenderOnce {
|
||||
type State: 'static;
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
state: Option<Self::State>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::State);
|
||||
|
||||
fn paint(self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext);
|
||||
|
||||
fn into_any(self) -> AnyElement {
|
||||
AnyElement::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<V, E, F> Element<V> for Option<F>
|
||||
where
|
||||
V: 'static,
|
||||
E: 'static + Component<V>,
|
||||
F: FnOnce(&mut V, &mut ViewContext<'_, V>) -> E + 'static,
|
||||
{
|
||||
type ElementState = AnyElement<V>;
|
||||
pub trait Component: 'static {
|
||||
type Rendered: RenderOnce;
|
||||
|
||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered;
|
||||
}
|
||||
|
||||
pub struct CompositeElement<C> {
|
||||
component: Option<C>,
|
||||
}
|
||||
|
||||
pub struct CompositeElementState<C: Component> {
|
||||
rendered_element: Option<<C::Rendered as RenderOnce>::Element>,
|
||||
rendered_element_state: <<C::Rendered as RenderOnce>::Element as Element>::State,
|
||||
}
|
||||
|
||||
impl<C> CompositeElement<C> {
|
||||
pub fn new(component: C) -> Self {
|
||||
CompositeElement {
|
||||
component: Some(component),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Component> Element for CompositeElement<C> {
|
||||
type State = CompositeElementState<C>;
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
state: Option<Self::State>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::State) {
|
||||
let mut element = self.component.take().unwrap().render(cx).render_once();
|
||||
let (layout_id, state) = element.layout(state.map(|s| s.rendered_element_state), cx);
|
||||
let state = CompositeElementState {
|
||||
rendered_element: Some(element),
|
||||
rendered_element_state: state,
|
||||
};
|
||||
(layout_id, state)
|
||||
}
|
||||
|
||||
fn paint(self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
|
||||
state
|
||||
.rendered_element
|
||||
.take()
|
||||
.unwrap()
|
||||
.paint(bounds, &mut state.rendered_element_state, cx);
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Component> RenderOnce for CompositeElement<C> {
|
||||
type Element = Self;
|
||||
|
||||
fn element_id(&self) -> Option<ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
view_state: &mut V,
|
||||
_: Option<Self::ElementState>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> (LayoutId, Self::ElementState) {
|
||||
let render = self.take().unwrap();
|
||||
let mut rendered_element = (render)(view_state, cx).render();
|
||||
let layout_id = rendered_element.layout(view_state, cx);
|
||||
(layout_id, rendered_element)
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_bounds: Bounds<Pixels>,
|
||||
view_state: &mut V,
|
||||
rendered_element: &mut Self::ElementState,
|
||||
cx: &mut ViewContext<V>,
|
||||
) {
|
||||
rendered_element.paint(view_state, cx)
|
||||
fn render_once(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
V: 'static,
|
||||
E: 'static + Component<V>,
|
||||
F: FnOnce(&mut V, &mut ViewContext<'_, V>) -> E + 'static,
|
||||
E: Element,
|
||||
E::State: '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)
|
||||
}
|
||||
}
|
||||
|
||||
impl<V, E, F> Component<V> for F
|
||||
where
|
||||
V: 'static,
|
||||
E: 'static + Component<V>,
|
||||
F: FnOnce(&mut V, &mut ViewContext<'_, V>) -> E + 'static,
|
||||
{
|
||||
fn render(self) -> AnyElement<V> {
|
||||
AnyElement::new(Some(self))
|
||||
impl Element for AnyElement {
|
||||
type State = ();
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
_: Option<Self::State>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (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
|
||||
// }
|
||||
// }
|
||||
|
||||
+305
-352
File diff suppressed because it is too large
Load Diff
@@ -1,18 +1,17 @@
|
||||
use crate::{
|
||||
AnyElement, BorrowWindow, Bounds, Component, Element, InteractiveComponent,
|
||||
InteractiveElementState, Interactivity, LayoutId, Pixels, SharedString, StyleRefinement,
|
||||
Styled, ViewContext,
|
||||
Bounds, Element, InteractiveElement, InteractiveElementState, Interactivity, LayoutId, Pixels,
|
||||
RenderOnce, SharedString, StyleRefinement, Styled, WindowContext,
|
||||
};
|
||||
use futures::FutureExt;
|
||||
use util::ResultExt;
|
||||
|
||||
pub struct Img<V: 'static> {
|
||||
interactivity: Interactivity<V>,
|
||||
pub struct Img {
|
||||
interactivity: Interactivity,
|
||||
uri: Option<SharedString>,
|
||||
grayscale: bool,
|
||||
}
|
||||
|
||||
pub fn img<V: 'static>() -> Img<V> {
|
||||
pub fn img() -> Img {
|
||||
Img {
|
||||
interactivity: Interactivity::default(),
|
||||
uri: None,
|
||||
@@ -20,10 +19,7 @@ pub fn img<V: 'static>() -> Img<V> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> Img<V>
|
||||
where
|
||||
V: 'static,
|
||||
{
|
||||
impl Img {
|
||||
pub fn uri(mut self, uri: impl Into<SharedString>) -> Self {
|
||||
self.uri = Some(uri.into());
|
||||
self
|
||||
@@ -35,36 +31,24 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> Component<V> for Img<V> {
|
||||
fn render(self) -> AnyElement<V> {
|
||||
AnyElement::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> Element<V> for Img<V> {
|
||||
type ElementState = InteractiveElementState;
|
||||
|
||||
fn element_id(&self) -> Option<crate::ElementId> {
|
||||
self.interactivity.element_id.clone()
|
||||
}
|
||||
impl Element for Img {
|
||||
type State = InteractiveElementState;
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
_view_state: &mut V,
|
||||
element_state: Option<Self::ElementState>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> (LayoutId, Self::ElementState) {
|
||||
element_state: Option<Self::State>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::State) {
|
||||
self.interactivity.layout(element_state, cx, |style, cx| {
|
||||
cx.request_layout(&style, None)
|
||||
})
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
self,
|
||||
bounds: Bounds<Pixels>,
|
||||
_view_state: &mut V,
|
||||
element_state: &mut Self::ElementState,
|
||||
cx: &mut ViewContext<V>,
|
||||
element_state: &mut Self::State,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
self.interactivity.paint(
|
||||
bounds,
|
||||
@@ -81,7 +65,7 @@ impl<V> Element<V> for Img<V> {
|
||||
if let Some(data) = image_future
|
||||
.clone()
|
||||
.now_or_never()
|
||||
.and_then(ResultExt::log_err)
|
||||
.and_then(|result| result.ok())
|
||||
{
|
||||
let corner_radii = corner_radii.to_pixels(bounds.size, cx.rem_size());
|
||||
cx.with_z_index(1, |cx| {
|
||||
@@ -89,8 +73,8 @@ impl<V> Element<V> for Img<V> {
|
||||
.log_err()
|
||||
});
|
||||
} else {
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
if image_future.await.log_err().is_some() {
|
||||
cx.spawn(|mut cx| async move {
|
||||
if image_future.await.ok().is_some() {
|
||||
cx.on_next_frame(|cx| cx.notify());
|
||||
}
|
||||
})
|
||||
@@ -102,14 +86,26 @@ impl<V> Element<V> for Img<V> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> Styled for Img<V> {
|
||||
impl RenderOnce for Img {
|
||||
type Element = Self;
|
||||
|
||||
fn element_id(&self) -> Option<crate::ElementId> {
|
||||
self.interactivity.element_id.clone()
|
||||
}
|
||||
|
||||
fn render_once(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Styled for Img {
|
||||
fn style(&mut self) -> &mut StyleRefinement {
|
||||
&mut self.interactivity.base_style
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> InteractiveComponent<V> for Img<V> {
|
||||
fn interactivity(&mut self) -> &mut Interactivity<V> {
|
||||
impl InteractiveElement for Img {
|
||||
fn interactivity(&mut self) -> &mut Interactivity {
|
||||
&mut self.interactivity
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,16 +2,16 @@ use smallvec::SmallVec;
|
||||
use taffy::style::{Display, Position};
|
||||
|
||||
use crate::{
|
||||
point, AnyElement, BorrowWindow, Bounds, Component, Element, LayoutId, ParentComponent, Pixels,
|
||||
Point, Size, Style,
|
||||
point, AnyElement, BorrowWindow, Bounds, Element, LayoutId, ParentElement, Pixels, Point,
|
||||
RenderOnce, Size, Style, WindowContext,
|
||||
};
|
||||
|
||||
pub struct OverlayState {
|
||||
child_layout_ids: SmallVec<[LayoutId; 4]>,
|
||||
}
|
||||
|
||||
pub struct Overlay<V> {
|
||||
children: SmallVec<[AnyElement<V>; 2]>,
|
||||
pub struct Overlay {
|
||||
children: SmallVec<[AnyElement; 2]>,
|
||||
anchor_corner: AnchorCorner,
|
||||
fit_mode: OverlayFitMode,
|
||||
// todo!();
|
||||
@@ -21,7 +21,7 @@ pub struct Overlay<V> {
|
||||
|
||||
/// overlay gives you a floating element that will avoid overflowing the window bounds.
|
||||
/// Its children should have no margin to avoid measurement issues.
|
||||
pub fn overlay<V: 'static>() -> Overlay<V> {
|
||||
pub fn overlay() -> Overlay {
|
||||
Overlay {
|
||||
children: SmallVec::new(),
|
||||
anchor_corner: AnchorCorner::TopLeft,
|
||||
@@ -30,7 +30,7 @@ pub fn overlay<V: 'static>() -> Overlay<V> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> Overlay<V> {
|
||||
impl Overlay {
|
||||
/// Sets which corner of the overlay should be anchored to the current position.
|
||||
pub fn anchor(mut self, anchor: AnchorCorner) -> Self {
|
||||
self.anchor_corner = anchor;
|
||||
@@ -51,35 +51,24 @@ impl<V> Overlay<V> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: 'static> ParentComponent<V> for Overlay<V> {
|
||||
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
|
||||
impl ParentElement for Overlay {
|
||||
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
|
||||
&mut self.children
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: 'static> Component<V> for Overlay<V> {
|
||||
fn render(self) -> AnyElement<V> {
|
||||
AnyElement::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: 'static> Element<V> for Overlay<V> {
|
||||
type ElementState = OverlayState;
|
||||
|
||||
fn element_id(&self) -> Option<crate::ElementId> {
|
||||
None
|
||||
}
|
||||
impl Element for Overlay {
|
||||
type State = OverlayState;
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
view_state: &mut V,
|
||||
_: Option<Self::ElementState>,
|
||||
cx: &mut crate::ViewContext<V>,
|
||||
) -> (crate::LayoutId, Self::ElementState) {
|
||||
_: Option<Self::State>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (crate::LayoutId, Self::State) {
|
||||
let child_layout_ids = self
|
||||
.children
|
||||
.iter_mut()
|
||||
.map(|child| child.layout(view_state, cx))
|
||||
.map(|child| child.layout(cx))
|
||||
.collect::<SmallVec<_>>();
|
||||
|
||||
let mut overlay_style = Style::default();
|
||||
@@ -92,11 +81,10 @@ impl<V: 'static> Element<V> for Overlay<V> {
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
self,
|
||||
bounds: crate::Bounds<crate::Pixels>,
|
||||
view_state: &mut V,
|
||||
element_state: &mut Self::ElementState,
|
||||
cx: &mut crate::ViewContext<V>,
|
||||
element_state: &mut Self::State,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
if element_state.child_layout_ids.is_empty() {
|
||||
return;
|
||||
@@ -156,13 +144,25 @@ impl<V: 'static> Element<V> for Overlay<V> {
|
||||
}
|
||||
|
||||
cx.with_element_offset(desired.origin - bounds.origin, |cx| {
|
||||
for child in &mut self.children {
|
||||
child.paint(view_state, cx);
|
||||
for child in self.children {
|
||||
child.paint(cx);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Overlay {
|
||||
type Element = Self;
|
||||
|
||||
fn element_id(&self) -> Option<crate::ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn render_once(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
enum Axis {
|
||||
Horizontal,
|
||||
Vertical,
|
||||
|
||||
@@ -1,60 +1,43 @@
|
||||
use crate::{
|
||||
AnyElement, Bounds, Component, Element, ElementId, InteractiveComponent,
|
||||
InteractiveElementState, Interactivity, LayoutId, Pixels, SharedString, StyleRefinement,
|
||||
Styled, ViewContext,
|
||||
Bounds, Element, ElementId, InteractiveElement, InteractiveElementState, Interactivity,
|
||||
LayoutId, Pixels, RenderOnce, SharedString, StyleRefinement, Styled, WindowContext,
|
||||
};
|
||||
use util::ResultExt;
|
||||
|
||||
pub struct Svg<V: 'static> {
|
||||
interactivity: Interactivity<V>,
|
||||
pub struct Svg {
|
||||
interactivity: Interactivity,
|
||||
path: Option<SharedString>,
|
||||
}
|
||||
|
||||
pub fn svg<V: 'static>() -> Svg<V> {
|
||||
pub fn svg() -> Svg {
|
||||
Svg {
|
||||
interactivity: Interactivity::default(),
|
||||
path: None,
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> Svg<V> {
|
||||
impl Svg {
|
||||
pub fn path(mut self, path: impl Into<SharedString>) -> Self {
|
||||
self.path = Some(path.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> Component<V> for Svg<V> {
|
||||
fn render(self) -> AnyElement<V> {
|
||||
AnyElement::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> Element<V> for Svg<V> {
|
||||
type ElementState = InteractiveElementState;
|
||||
|
||||
fn element_id(&self) -> Option<ElementId> {
|
||||
self.interactivity.element_id.clone()
|
||||
}
|
||||
impl Element for Svg {
|
||||
type State = InteractiveElementState;
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
_view_state: &mut V,
|
||||
element_state: Option<Self::ElementState>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> (LayoutId, Self::ElementState) {
|
||||
element_state: Option<Self::State>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::State) {
|
||||
self.interactivity.layout(element_state, cx, |style, cx| {
|
||||
cx.request_layout(&style, None)
|
||||
})
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
_view_state: &mut V,
|
||||
element_state: &mut Self::ElementState,
|
||||
cx: &mut ViewContext<V>,
|
||||
) where
|
||||
fn paint(self, bounds: Bounds<Pixels>, element_state: &mut Self::State, cx: &mut WindowContext)
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.interactivity
|
||||
@@ -66,14 +49,26 @@ impl<V> Element<V> for Svg<V> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> Styled for Svg<V> {
|
||||
impl RenderOnce for Svg {
|
||||
type Element = Self;
|
||||
|
||||
fn element_id(&self) -> Option<ElementId> {
|
||||
self.interactivity.element_id.clone()
|
||||
}
|
||||
|
||||
fn render_once(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Styled for Svg {
|
||||
fn style(&mut self) -> &mut StyleRefinement {
|
||||
&mut self.interactivity.base_style
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> InteractiveComponent<V> for Svg<V> {
|
||||
fn interactivity(&mut self) -> &mut Interactivity<V> {
|
||||
impl InteractiveElement for Svg {
|
||||
fn interactivity(&mut self) -> &mut Interactivity {
|
||||
&mut self.interactivity
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,82 +1,191 @@
|
||||
use crate::{
|
||||
AnyElement, BorrowWindow, Bounds, Component, Element, ElementId, LayoutId, Pixels,
|
||||
SharedString, Size, TextRun, ViewContext, WrappedLine,
|
||||
Bounds, Element, ElementId, LayoutId, Pixels, RenderOnce, SharedString, Size, TextRun,
|
||||
WindowContext, WrappedLine,
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
use parking_lot::{Mutex, MutexGuard};
|
||||
use smallvec::SmallVec;
|
||||
use std::{cell::Cell, rc::Rc, sync::Arc};
|
||||
use util::ResultExt;
|
||||
|
||||
pub struct Text {
|
||||
impl Element for &'static str {
|
||||
type State = TextState;
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
_: Option<Self::State>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::State) {
|
||||
let mut state = TextState::default();
|
||||
let layout_id = state.layout(SharedString::from(*self), None, cx);
|
||||
(layout_id, state)
|
||||
}
|
||||
|
||||
fn paint(self, bounds: Bounds<Pixels>, state: &mut TextState, cx: &mut WindowContext) {
|
||||
state.paint(bounds, self, cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for &'static str {
|
||||
type Element = Self;
|
||||
|
||||
fn element_id(&self) -> Option<ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn render_once(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for SharedString {
|
||||
type State = TextState;
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
_: Option<Self::State>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::State) {
|
||||
let mut state = TextState::default();
|
||||
let layout_id = state.layout(self.clone(), None, cx);
|
||||
(layout_id, state)
|
||||
}
|
||||
|
||||
fn paint(self, bounds: Bounds<Pixels>, state: &mut TextState, cx: &mut WindowContext) {
|
||||
let text_str: &str = self.as_ref();
|
||||
state.paint(bounds, text_str, cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for SharedString {
|
||||
type Element = Self;
|
||||
|
||||
fn element_id(&self) -> Option<ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn render_once(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub struct StyledText {
|
||||
text: SharedString,
|
||||
runs: Option<Vec<TextRun>>,
|
||||
}
|
||||
|
||||
impl Text {
|
||||
impl StyledText {
|
||||
/// Renders text with runs of different styles.
|
||||
///
|
||||
/// Callers are responsible for setting the correct style for each run.
|
||||
/// For text with a uniform style, you can usually avoid calling this constructor
|
||||
/// and just pass text directly.
|
||||
pub fn styled(text: SharedString, runs: Vec<TextRun>) -> Self {
|
||||
Text {
|
||||
pub fn new(text: SharedString, runs: Vec<TextRun>) -> Self {
|
||||
StyledText {
|
||||
text,
|
||||
runs: Some(runs),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: 'static> Component<V> for Text {
|
||||
fn render(self) -> AnyElement<V> {
|
||||
AnyElement::new(self)
|
||||
impl Element for StyledText {
|
||||
type State = TextState;
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
_: Option<Self::State>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::State) {
|
||||
let mut state = TextState::default();
|
||||
let layout_id = state.layout(self.text.clone(), self.runs.take(), cx);
|
||||
(layout_id, state)
|
||||
}
|
||||
|
||||
fn paint(self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
|
||||
state.paint(bounds, &self.text, cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: 'static> Element<V> for Text {
|
||||
type ElementState = TextState;
|
||||
impl RenderOnce for StyledText {
|
||||
type Element = Self;
|
||||
|
||||
fn element_id(&self) -> Option<crate::ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn render_once(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct TextState(Arc<Mutex<Option<TextStateInner>>>);
|
||||
|
||||
struct TextStateInner {
|
||||
lines: SmallVec<[WrappedLine; 1]>,
|
||||
line_height: Pixels,
|
||||
wrap_width: Option<Pixels>,
|
||||
size: Option<Size<Pixels>>,
|
||||
}
|
||||
|
||||
impl TextState {
|
||||
fn lock(&self) -> MutexGuard<Option<TextStateInner>> {
|
||||
self.0.lock()
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
_view: &mut V,
|
||||
element_state: Option<Self::ElementState>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> (LayoutId, Self::ElementState) {
|
||||
let element_state = element_state.unwrap_or_default();
|
||||
text: SharedString,
|
||||
runs: Option<Vec<TextRun>>,
|
||||
cx: &mut WindowContext,
|
||||
) -> LayoutId {
|
||||
let text_system = cx.text_system().clone();
|
||||
let text_style = cx.text_style();
|
||||
let font_size = text_style.font_size.to_pixels(cx.rem_size());
|
||||
let line_height = text_style
|
||||
.line_height
|
||||
.to_pixels(font_size.into(), cx.rem_size());
|
||||
let text = self.text.clone();
|
||||
let text = SharedString::from(text);
|
||||
|
||||
let rem_size = cx.rem_size();
|
||||
|
||||
let runs = if let Some(runs) = self.runs.take() {
|
||||
let runs = if let Some(runs) = runs {
|
||||
runs
|
||||
} else {
|
||||
vec![text_style.to_run(text.len())]
|
||||
};
|
||||
|
||||
let layout_id = cx.request_measured_layout(Default::default(), rem_size, {
|
||||
let element_state = element_state.clone();
|
||||
move |known_dimensions, _| {
|
||||
let element_state = self.clone();
|
||||
|
||||
move |known_dimensions, available_space| {
|
||||
let wrap_width = known_dimensions.width.or(match available_space.width {
|
||||
crate::AvailableSpace::Definite(x) => Some(x),
|
||||
_ => None,
|
||||
});
|
||||
|
||||
if let Some(text_state) = element_state.0.lock().as_ref() {
|
||||
if text_state.size.is_some()
|
||||
&& (wrap_width.is_none() || wrap_width == text_state.wrap_width)
|
||||
{
|
||||
return text_state.size.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
let Some(lines) = text_system
|
||||
.shape_text(
|
||||
&text,
|
||||
font_size,
|
||||
&runs[..],
|
||||
known_dimensions.width, // Wrap if we know the width.
|
||||
wrap_width, // Wrap if we know the width.
|
||||
)
|
||||
.log_err()
|
||||
else {
|
||||
element_state.lock().replace(TextStateInner {
|
||||
lines: Default::default(),
|
||||
line_height,
|
||||
wrap_width,
|
||||
size: Some(Size::default()),
|
||||
});
|
||||
return Size::default();
|
||||
};
|
||||
@@ -88,28 +197,25 @@ impl<V: 'static> Element<V> for Text {
|
||||
size.width = size.width.max(line_size.width);
|
||||
}
|
||||
|
||||
element_state
|
||||
.lock()
|
||||
.replace(TextStateInner { lines, line_height });
|
||||
element_state.lock().replace(TextStateInner {
|
||||
lines,
|
||||
line_height,
|
||||
wrap_width,
|
||||
size: Some(size),
|
||||
});
|
||||
|
||||
size
|
||||
}
|
||||
});
|
||||
|
||||
(layout_id, element_state)
|
||||
layout_id
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
_: &mut V,
|
||||
element_state: &mut Self::ElementState,
|
||||
cx: &mut ViewContext<V>,
|
||||
) {
|
||||
let element_state = element_state.lock();
|
||||
fn paint(&mut self, bounds: Bounds<Pixels>, text: &str, cx: &mut WindowContext) {
|
||||
let element_state = self.lock();
|
||||
let element_state = element_state
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("measurement has not been performed on {}", &self.text))
|
||||
.ok_or_else(|| anyhow!("measurement has not been performed on {}", text))
|
||||
.unwrap();
|
||||
|
||||
let line_height = element_state.line_height;
|
||||
@@ -121,23 +227,9 @@ impl<V: 'static> Element<V> for Text {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct TextState(Arc<Mutex<Option<TextStateInner>>>);
|
||||
|
||||
impl TextState {
|
||||
fn lock(&self) -> MutexGuard<Option<TextStateInner>> {
|
||||
self.0.lock()
|
||||
}
|
||||
}
|
||||
|
||||
struct TextStateInner {
|
||||
lines: SmallVec<[WrappedLine; 1]>,
|
||||
line_height: Pixels,
|
||||
}
|
||||
|
||||
struct InteractiveText {
|
||||
id: ElementId,
|
||||
text: Text,
|
||||
element_id: ElementId,
|
||||
text: StyledText,
|
||||
}
|
||||
|
||||
struct InteractiveTextState {
|
||||
@@ -145,32 +237,27 @@ struct InteractiveTextState {
|
||||
clicked_range_ixs: Rc<Cell<SmallVec<[usize; 1]>>>,
|
||||
}
|
||||
|
||||
impl<V: 'static> Element<V> for InteractiveText {
|
||||
type ElementState = InteractiveTextState;
|
||||
|
||||
fn element_id(&self) -> Option<ElementId> {
|
||||
Some(self.id.clone())
|
||||
}
|
||||
impl Element for InteractiveText {
|
||||
type State = InteractiveTextState;
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
view_state: &mut V,
|
||||
element_state: Option<Self::ElementState>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> (LayoutId, Self::ElementState) {
|
||||
state: Option<Self::State>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::State) {
|
||||
if let Some(InteractiveTextState {
|
||||
text_state,
|
||||
clicked_range_ixs,
|
||||
}) = element_state
|
||||
}) = state
|
||||
{
|
||||
let (layout_id, text_state) = self.text.layout(view_state, Some(text_state), cx);
|
||||
let (layout_id, text_state) = self.text.layout(Some(text_state), cx);
|
||||
let element_state = InteractiveTextState {
|
||||
text_state,
|
||||
clicked_range_ixs,
|
||||
};
|
||||
(layout_id, element_state)
|
||||
} else {
|
||||
let (layout_id, text_state) = self.text.layout(view_state, None, cx);
|
||||
let (layout_id, text_state) = self.text.layout(None, cx);
|
||||
let element_state = InteractiveTextState {
|
||||
text_state,
|
||||
clicked_range_ixs: Rc::default(),
|
||||
@@ -179,46 +266,19 @@ impl<V: 'static> Element<V> for InteractiveText {
|
||||
}
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
view_state: &mut V,
|
||||
element_state: &mut Self::ElementState,
|
||||
cx: &mut ViewContext<V>,
|
||||
) {
|
||||
self.text
|
||||
.paint(bounds, view_state, &mut element_state.text_state, cx)
|
||||
fn paint(self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
|
||||
self.text.paint(bounds, &mut state.text_state, cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: 'static> Component<V> for SharedString {
|
||||
fn render(self) -> AnyElement<V> {
|
||||
Text {
|
||||
text: self,
|
||||
runs: None,
|
||||
}
|
||||
.render()
|
||||
}
|
||||
}
|
||||
impl RenderOnce for InteractiveText {
|
||||
type Element = Self;
|
||||
|
||||
impl<V: 'static> Component<V> for &'static str {
|
||||
fn render(self) -> AnyElement<V> {
|
||||
Text {
|
||||
text: self.into(),
|
||||
runs: None,
|
||||
}
|
||||
.render()
|
||||
fn element_id(&self) -> Option<ElementId> {
|
||||
Some(self.element_id.clone())
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Figure out how to pass `String` to `child` without this.
|
||||
// This impl doesn't exist in the `gpui2` crate.
|
||||
impl<V: 'static> Component<V> for String {
|
||||
fn render(self) -> AnyElement<V> {
|
||||
Text {
|
||||
text: self.into(),
|
||||
runs: None,
|
||||
}
|
||||
.render()
|
||||
fn render_once(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,40 +1,45 @@
|
||||
use crate::{
|
||||
point, px, size, AnyElement, AvailableSpace, BorrowWindow, Bounds, Component, Element,
|
||||
ElementId, InteractiveComponent, InteractiveElementState, Interactivity, LayoutId, Pixels,
|
||||
Point, Size, StyleRefinement, Styled, ViewContext,
|
||||
point, px, size, AnyElement, AvailableSpace, Bounds, Element, ElementId, InteractiveElement,
|
||||
InteractiveElementState, Interactivity, LayoutId, Pixels, Point, Render, RenderOnce, Size,
|
||||
StyleRefinement, Styled, View, ViewContext, WindowContext,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use std::{cell::RefCell, cmp, mem, ops::Range, rc::Rc};
|
||||
use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
|
||||
use taffy::style::Overflow;
|
||||
|
||||
/// uniform_list provides lazy rendering for a set of items that are of uniform height.
|
||||
/// When rendered into a container with overflow-y: hidden and a fixed (or max) height,
|
||||
/// uniform_list will only render the visibile subset of items.
|
||||
pub fn uniform_list<I, V, C>(
|
||||
pub fn uniform_list<I, R, V>(
|
||||
view: View<V>,
|
||||
id: I,
|
||||
item_count: usize,
|
||||
f: impl 'static + Fn(&mut V, Range<usize>, &mut ViewContext<V>) -> Vec<C>,
|
||||
) -> UniformList<V>
|
||||
f: impl 'static + Fn(&mut V, Range<usize>, &mut ViewContext<V>) -> Vec<R>,
|
||||
) -> UniformList
|
||||
where
|
||||
I: Into<ElementId>,
|
||||
V: 'static,
|
||||
C: Component<V>,
|
||||
R: RenderOnce,
|
||||
V: Render,
|
||||
{
|
||||
let id = id.into();
|
||||
let mut style = StyleRefinement::default();
|
||||
style.overflow.y = Some(Overflow::Hidden);
|
||||
|
||||
let render_range = move |range, cx: &mut WindowContext| {
|
||||
view.update(cx, |this, cx| {
|
||||
f(this, range, cx)
|
||||
.into_iter()
|
||||
.map(|component| component.render_into_any())
|
||||
.collect()
|
||||
})
|
||||
};
|
||||
|
||||
UniformList {
|
||||
id: id.clone(),
|
||||
style,
|
||||
item_count,
|
||||
item_to_measure_index: 0,
|
||||
render_items: Box::new(move |view, visible_range, cx| {
|
||||
f(view, visible_range, cx)
|
||||
.into_iter()
|
||||
.map(|component| component.render())
|
||||
.collect()
|
||||
}),
|
||||
render_items: Box::new(render_range),
|
||||
interactivity: Interactivity {
|
||||
element_id: Some(id.into()),
|
||||
..Default::default()
|
||||
@@ -43,19 +48,14 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UniformList<V: 'static> {
|
||||
pub struct UniformList {
|
||||
id: ElementId,
|
||||
style: StyleRefinement,
|
||||
item_count: usize,
|
||||
item_to_measure_index: usize,
|
||||
render_items: Box<
|
||||
dyn for<'a> Fn(
|
||||
&'a mut V,
|
||||
Range<usize>,
|
||||
&'a mut ViewContext<V>,
|
||||
) -> SmallVec<[AnyElement<V>; 64]>,
|
||||
>,
|
||||
interactivity: Interactivity<V>,
|
||||
render_items:
|
||||
Box<dyn for<'a> Fn(Range<usize>, &'a mut WindowContext) -> SmallVec<[AnyElement; 64]>>,
|
||||
interactivity: Interactivity,
|
||||
scroll_handle: Option<UniformListScrollHandle>,
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ impl UniformListScrollHandle {
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: 'static> Styled for UniformList<V> {
|
||||
impl Styled for UniformList {
|
||||
fn style(&mut self) -> &mut StyleRefinement {
|
||||
&mut self.style
|
||||
}
|
||||
@@ -101,29 +101,24 @@ pub struct UniformListState {
|
||||
item_size: Size<Pixels>,
|
||||
}
|
||||
|
||||
impl<V: 'static> Element<V> for UniformList<V> {
|
||||
type ElementState = UniformListState;
|
||||
|
||||
fn element_id(&self) -> Option<crate::ElementId> {
|
||||
Some(self.id.clone())
|
||||
}
|
||||
impl Element for UniformList {
|
||||
type State = UniformListState;
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
view_state: &mut V,
|
||||
element_state: Option<Self::ElementState>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> (LayoutId, Self::ElementState) {
|
||||
state: Option<Self::State>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::State) {
|
||||
let max_items = self.item_count;
|
||||
let rem_size = cx.rem_size();
|
||||
let item_size = element_state
|
||||
let item_size = state
|
||||
.as_ref()
|
||||
.map(|s| s.item_size)
|
||||
.unwrap_or_else(|| self.measure_item(view_state, None, cx));
|
||||
.unwrap_or_else(|| self.measure_item(None, cx));
|
||||
|
||||
let (layout_id, interactive) =
|
||||
self.interactivity
|
||||
.layout(element_state.map(|s| s.interactive), cx, |style, cx| {
|
||||
.layout(state.map(|s| s.interactive), cx, |style, cx| {
|
||||
cx.request_measured_layout(
|
||||
style,
|
||||
rem_size,
|
||||
@@ -159,11 +154,10 @@ impl<V: 'static> Element<V> for UniformList<V> {
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
self,
|
||||
bounds: Bounds<crate::Pixels>,
|
||||
view_state: &mut V,
|
||||
element_state: &mut Self::ElementState,
|
||||
cx: &mut ViewContext<V>,
|
||||
element_state: &mut Self::State,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
let style =
|
||||
self.interactivity
|
||||
@@ -183,14 +177,15 @@ impl<V: 'static> Element<V> for UniformList<V> {
|
||||
height: item_size.height * self.item_count,
|
||||
};
|
||||
|
||||
let mut interactivity = mem::take(&mut self.interactivity);
|
||||
let shared_scroll_offset = element_state
|
||||
.interactive
|
||||
.scroll_offset
|
||||
.get_or_insert_with(Rc::default)
|
||||
.clone();
|
||||
|
||||
interactivity.paint(
|
||||
let item_height = self.measure_item(Some(padded_bounds.size.width), cx).height;
|
||||
|
||||
self.interactivity.paint(
|
||||
bounds,
|
||||
content_size,
|
||||
&mut element_state.interactive,
|
||||
@@ -209,9 +204,6 @@ impl<V: 'static> Element<V> for UniformList<V> {
|
||||
style.paint(bounds, cx);
|
||||
|
||||
if self.item_count > 0 {
|
||||
let item_height = self
|
||||
.measure_item(view_state, Some(padded_bounds.size.width), cx)
|
||||
.height;
|
||||
if let Some(scroll_handle) = self.scroll_handle.clone() {
|
||||
scroll_handle.0.borrow_mut().replace(ScrollHandleState {
|
||||
item_height,
|
||||
@@ -233,44 +225,50 @@ impl<V: 'static> Element<V> for UniformList<V> {
|
||||
self.item_count,
|
||||
);
|
||||
|
||||
let mut items = (self.render_items)(view_state, visible_range.clone(), cx);
|
||||
let items = (self.render_items)(visible_range.clone(), cx);
|
||||
cx.with_z_index(1, |cx| {
|
||||
for (item, ix) in items.iter_mut().zip(visible_range) {
|
||||
for (item, ix) in items.into_iter().zip(visible_range) {
|
||||
let item_origin = padded_bounds.origin
|
||||
+ point(px(0.), item_height * ix + scroll_offset.y);
|
||||
let available_space = size(
|
||||
AvailableSpace::Definite(padded_bounds.size.width),
|
||||
AvailableSpace::Definite(item_height),
|
||||
);
|
||||
item.draw(item_origin, available_space, view_state, cx);
|
||||
item.draw(item_origin, available_space, cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
},
|
||||
);
|
||||
self.interactivity = interactivity;
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> UniformList<V> {
|
||||
impl RenderOnce for UniformList {
|
||||
type Element = Self;
|
||||
|
||||
fn element_id(&self) -> Option<crate::ElementId> {
|
||||
Some(self.id.clone())
|
||||
}
|
||||
|
||||
fn render_once(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl UniformList {
|
||||
pub fn with_width_from_item(mut self, item_index: Option<usize>) -> Self {
|
||||
self.item_to_measure_index = item_index.unwrap_or(0);
|
||||
self
|
||||
}
|
||||
|
||||
fn measure_item(
|
||||
&self,
|
||||
view_state: &mut V,
|
||||
list_width: Option<Pixels>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> Size<Pixels> {
|
||||
fn measure_item(&self, list_width: Option<Pixels>, cx: &mut WindowContext) -> Size<Pixels> {
|
||||
if self.item_count == 0 {
|
||||
return Size::default();
|
||||
}
|
||||
|
||||
let item_ix = cmp::min(self.item_to_measure_index, self.item_count - 1);
|
||||
let mut items = (self.render_items)(view_state, item_ix..item_ix + 1, cx);
|
||||
let mut items = (self.render_items)(item_ix..item_ix + 1, cx);
|
||||
let mut item_to_measure = items.pop().unwrap();
|
||||
let available_space = size(
|
||||
list_width.map_or(AvailableSpace::MinContent, |width| {
|
||||
@@ -278,7 +276,7 @@ impl<V> UniformList<V> {
|
||||
}),
|
||||
AvailableSpace::MinContent,
|
||||
);
|
||||
item_to_measure.measure(available_space, view_state, cx)
|
||||
item_to_measure.measure(available_space, cx)
|
||||
}
|
||||
|
||||
pub fn track_scroll(mut self, handle: UniformListScrollHandle) -> Self {
|
||||
@@ -287,14 +285,8 @@ impl<V> UniformList<V> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> InteractiveComponent<V> for UniformList<V> {
|
||||
fn interactivity(&mut self) -> &mut crate::Interactivity<V> {
|
||||
impl InteractiveElement for UniformList {
|
||||
fn interactivity(&mut self) -> &mut crate::Interactivity {
|
||||
&mut self.interactivity
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: 'static> Component<V> for UniformList<V> {
|
||||
fn render(self) -> AnyElement<V> {
|
||||
AnyElement::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,8 +78,6 @@ use std::{
|
||||
};
|
||||
use taffy::TaffyLayoutEngine;
|
||||
|
||||
type AnyBox = Box<dyn Any>;
|
||||
|
||||
pub trait Context {
|
||||
type Result<T>;
|
||||
|
||||
@@ -136,11 +134,15 @@ pub trait VisualContext: Context {
|
||||
build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V,
|
||||
) -> Self::Result<View<V>>
|
||||
where
|
||||
V: Render;
|
||||
V: 'static + Render;
|
||||
|
||||
fn focus_view<V>(&mut self, view: &View<V>) -> Self::Result<()>
|
||||
where
|
||||
V: FocusableView;
|
||||
|
||||
fn dismiss_view<V>(&mut self, view: &View<V>) -> Self::Result<()>
|
||||
where
|
||||
V: ManagedView;
|
||||
}
|
||||
|
||||
pub trait Entity<T>: Sealed {
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::{ImageData, ImageId, SharedString};
|
||||
use collections::HashMap;
|
||||
use futures::{
|
||||
future::{BoxFuture, Shared},
|
||||
AsyncReadExt, FutureExt,
|
||||
AsyncReadExt, FutureExt, TryFutureExt,
|
||||
};
|
||||
use image::ImageError;
|
||||
use parking_lot::Mutex;
|
||||
@@ -88,6 +88,14 @@ impl ImageCache {
|
||||
Ok(Arc::new(ImageData::new(image)))
|
||||
}
|
||||
}
|
||||
.map_err({
|
||||
let uri = uri.clone();
|
||||
|
||||
move |error| {
|
||||
log::log!(log::Level::Error, "{:?} {:?}", &uri, &error);
|
||||
error
|
||||
}
|
||||
})
|
||||
.boxed()
|
||||
.shared();
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use crate::{AsyncWindowContext, Bounds, Pixels, PlatformInputHandler, View, ViewContext};
|
||||
use crate::{
|
||||
AsyncWindowContext, Bounds, Pixels, PlatformInputHandler, View, ViewContext, WindowContext,
|
||||
};
|
||||
use std::ops::Range;
|
||||
|
||||
/// Implement this trait to allow views to handle textual input when implementing an editor, field, etc.
|
||||
@@ -43,9 +45,9 @@ pub struct ElementInputHandler<V> {
|
||||
impl<V: 'static> ElementInputHandler<V> {
|
||||
/// Used in [Element::paint] with the element's bounds and a view context for its
|
||||
/// containing view.
|
||||
pub fn new(element_bounds: Bounds<Pixels>, cx: &mut ViewContext<V>) -> Self {
|
||||
pub fn new(element_bounds: Bounds<Pixels>, view: View<V>, cx: &mut WindowContext) -> Self {
|
||||
ElementInputHandler {
|
||||
view: cx.view().clone(),
|
||||
view,
|
||||
element_bounds,
|
||||
cx: cx.to_async(),
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
div, point, Component, Div, FocusHandle, Keystroke, Modifiers, Pixels, Point, Render,
|
||||
div, point, Div, Element, FocusHandle, Keystroke, Modifiers, Pixels, Point, Render, RenderOnce,
|
||||
ViewContext,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
@@ -64,24 +64,24 @@ pub struct Drag<S, R, V, E>
|
||||
where
|
||||
R: Fn(&mut V, &mut ViewContext<V>) -> E,
|
||||
V: 'static,
|
||||
E: Component<()>,
|
||||
E: RenderOnce,
|
||||
{
|
||||
pub state: S,
|
||||
pub render_drag_handle: R,
|
||||
view_type: PhantomData<V>,
|
||||
view_element_types: PhantomData<(V, E)>,
|
||||
}
|
||||
|
||||
impl<S, R, V, E> Drag<S, R, V, E>
|
||||
where
|
||||
R: Fn(&mut V, &mut ViewContext<V>) -> E,
|
||||
V: 'static,
|
||||
E: Component<()>,
|
||||
E: Element,
|
||||
{
|
||||
pub fn new(state: S, render_drag_handle: R) -> Self {
|
||||
Drag {
|
||||
state,
|
||||
render_drag_handle,
|
||||
view_type: PhantomData,
|
||||
view_element_types: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -194,7 +194,7 @@ impl Deref for MouseExitEvent {
|
||||
pub struct ExternalPaths(pub(crate) SmallVec<[PathBuf; 2]>);
|
||||
|
||||
impl Render for ExternalPaths {
|
||||
type Element = Div<Self>;
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, _: &mut ViewContext<Self>) -> Self::Element {
|
||||
div() // Intentionally left empty because the platform will render icons for the dragged files
|
||||
@@ -286,8 +286,8 @@ pub struct FocusEvent {
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{
|
||||
self as gpui, div, Component, Div, FocusHandle, InteractiveComponent, KeyBinding,
|
||||
Keystroke, ParentComponent, Render, Stateful, TestAppContext, ViewContext, VisualContext,
|
||||
self as gpui, div, Div, FocusHandle, InteractiveElement, KeyBinding, Keystroke,
|
||||
ParentElement, Render, RenderOnce, Stateful, TestAppContext, VisualContext,
|
||||
};
|
||||
|
||||
struct TestView {
|
||||
@@ -299,20 +299,24 @@ mod test {
|
||||
actions!(TestAction);
|
||||
|
||||
impl Render for TestView {
|
||||
type Element = Stateful<Self, Div<Self>>;
|
||||
type Element = Stateful<Div>;
|
||||
|
||||
fn render(&mut self, _: &mut gpui::ViewContext<Self>) -> Self::Element {
|
||||
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element {
|
||||
div().id("testview").child(
|
||||
div()
|
||||
.key_context("parent")
|
||||
.on_key_down(|this: &mut TestView, _, _, _| this.saw_key_down = true)
|
||||
.on_action(|this: &mut TestView, _: &TestAction, _| this.saw_action = true)
|
||||
.child(|this: &mut Self, _cx: &mut ViewContext<Self>| {
|
||||
.on_key_down(cx.listener(|this, _, _| this.saw_key_down = true))
|
||||
.on_action(
|
||||
cx.listener(|this: &mut TestView, _: &TestAction, _| {
|
||||
this.saw_action = true
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.key_context("nested")
|
||||
.track_focus(&this.focus_handle)
|
||||
.render()
|
||||
}),
|
||||
.track_focus(&self.focus_handle)
|
||||
.render_once(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1205,10 +1205,7 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
|
||||
|
||||
InputEvent::MouseMove(_) if !(is_active || lock.kind == WindowKind::PopUp) => return,
|
||||
|
||||
InputEvent::MouseUp(MouseUpEvent {
|
||||
button: MouseButton::Left,
|
||||
..
|
||||
}) => {
|
||||
InputEvent::MouseUp(MouseUpEvent { .. }) => {
|
||||
lock.synthetic_drag_counter += 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pub use crate::{
|
||||
BorrowAppContext, BorrowWindow, Component, Context, FocusableComponent, InteractiveComponent,
|
||||
ParentComponent, Refineable, Render, StatefulInteractiveComponent, Styled, VisualContext,
|
||||
BorrowAppContext, BorrowWindow, Component, Context, Element, FocusableElement,
|
||||
InteractiveElement, ParentElement, Refineable, Render, RenderOnce, StatefulInteractiveElement,
|
||||
Styled, VisualContext,
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::{
|
||||
black, phi, point, rems, AbsoluteLength, BorrowAppContext, BorrowWindow, Bounds, ContentMask,
|
||||
Corners, CornersRefinement, CursorStyle, DefiniteLength, Edges, EdgesRefinement, Font,
|
||||
FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Rgba,
|
||||
SharedString, Size, SizeRefinement, Styled, TextRun, ViewContext,
|
||||
SharedString, Size, SizeRefinement, Styled, TextRun, WindowContext,
|
||||
};
|
||||
use refineable::{Cascade, Refineable};
|
||||
use smallvec::SmallVec;
|
||||
@@ -313,7 +313,7 @@ impl Style {
|
||||
}
|
||||
|
||||
/// Paints the background of an element styled with this style.
|
||||
pub fn paint<V: 'static>(&self, bounds: Bounds<Pixels>, cx: &mut ViewContext<V>) {
|
||||
pub fn paint(&self, bounds: Bounds<Pixels>, cx: &mut WindowContext) {
|
||||
let rem_size = cx.rem_size();
|
||||
|
||||
cx.with_z_index(0, |cx| {
|
||||
|
||||
+22
-24
@@ -5,12 +5,14 @@ use std::fmt::Debug;
|
||||
use taffy::{
|
||||
geometry::{Point as TaffyPoint, Rect as TaffyRect, Size as TaffySize},
|
||||
style::AvailableSpace as TaffyAvailableSpace,
|
||||
tree::{Measurable, MeasureFunc, NodeId},
|
||||
tree::NodeId,
|
||||
Taffy,
|
||||
};
|
||||
|
||||
type Measureable = dyn Fn(Size<Option<Pixels>>, Size<AvailableSpace>) -> Size<Pixels> + Send + Sync;
|
||||
|
||||
pub struct TaffyLayoutEngine {
|
||||
taffy: Taffy,
|
||||
taffy: Taffy<Box<Measureable>>,
|
||||
children_to_parents: HashMap<LayoutId, LayoutId>,
|
||||
absolute_layout_bounds: HashMap<LayoutId, Bounds<Pixels>>,
|
||||
computed_layouts: HashSet<LayoutId>,
|
||||
@@ -70,9 +72,9 @@ impl TaffyLayoutEngine {
|
||||
) -> LayoutId {
|
||||
let style = style.to_taffy(rem_size);
|
||||
|
||||
let measurable = Box::new(Measureable(measure)) as Box<dyn Measurable>;
|
||||
let measurable = Box::new(measure);
|
||||
self.taffy
|
||||
.new_leaf_with_measure(style, MeasureFunc::Boxed(measurable))
|
||||
.new_leaf_with_context(style, measurable)
|
||||
.expect(EXPECT_MESSAGE)
|
||||
.into()
|
||||
}
|
||||
@@ -154,7 +156,22 @@ impl TaffyLayoutEngine {
|
||||
|
||||
// let started_at = std::time::Instant::now();
|
||||
self.taffy
|
||||
.compute_layout(id.into(), available_space.into())
|
||||
.compute_layout_with_measure(
|
||||
id.into(),
|
||||
available_space.into(),
|
||||
|known_dimensions, available_space, _node_id, context| {
|
||||
let Some(measure) = context else {
|
||||
return taffy::geometry::Size::default();
|
||||
};
|
||||
|
||||
let known_dimensions = Size {
|
||||
width: known_dimensions.width.map(Pixels),
|
||||
height: known_dimensions.height.map(Pixels),
|
||||
};
|
||||
|
||||
measure(known_dimensions, available_space.into()).into()
|
||||
},
|
||||
)
|
||||
.expect(EXPECT_MESSAGE);
|
||||
// println!("compute_layout took {:?}", started_at.elapsed());
|
||||
}
|
||||
@@ -202,25 +219,6 @@ impl From<LayoutId> for NodeId {
|
||||
}
|
||||
}
|
||||
|
||||
struct Measureable<F>(F);
|
||||
|
||||
impl<F> taffy::tree::Measurable for Measureable<F>
|
||||
where
|
||||
F: Fn(Size<Option<Pixels>>, Size<AvailableSpace>) -> Size<Pixels> + Send + Sync,
|
||||
{
|
||||
fn measure(
|
||||
&self,
|
||||
known_dimensions: TaffySize<Option<f32>>,
|
||||
available_space: TaffySize<TaffyAvailableSpace>,
|
||||
) -> TaffySize<f32> {
|
||||
let known_dimensions: Size<Option<f32>> = known_dimensions.into();
|
||||
let known_dimensions: Size<Option<Pixels>> = known_dimensions.map(|d| d.map(Into::into));
|
||||
let available_space = available_space.into();
|
||||
let size = (self.0)(known_dimensions, available_space);
|
||||
size.into()
|
||||
}
|
||||
}
|
||||
|
||||
trait ToTaffy<Output> {
|
||||
fn to_taffy(&self, rem_size: Pixels) -> Output;
|
||||
}
|
||||
|
||||
+88
-127
@@ -1,23 +1,17 @@
|
||||
use crate::{
|
||||
private::Sealed, AnyBox, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace,
|
||||
BorrowWindow, Bounds, Component, Element, ElementId, Entity, EntityId, Flatten, FocusHandle,
|
||||
FocusableView, LayoutId, Model, Pixels, Point, Size, ViewContext, VisualContext, WeakModel,
|
||||
private::Sealed, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace, BorrowWindow,
|
||||
Bounds, Element, ElementId, Entity, EntityId, Flatten, FocusHandle, FocusableView, LayoutId,
|
||||
Model, Pixels, Point, Render, RenderOnce, Size, ViewContext, VisualContext, WeakModel,
|
||||
WindowContext,
|
||||
};
|
||||
use anyhow::{Context, Result};
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
any::TypeId,
|
||||
hash::{Hash, Hasher},
|
||||
};
|
||||
|
||||
pub trait Render: 'static + Sized {
|
||||
type Element: Element<Self> + 'static;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element;
|
||||
}
|
||||
|
||||
pub struct View<V> {
|
||||
pub(crate) model: Model<V>,
|
||||
pub model: Model<V>,
|
||||
}
|
||||
|
||||
impl<V> Sealed for View<V> {}
|
||||
@@ -65,15 +59,15 @@ impl<V: 'static> View<V> {
|
||||
self.model.read(cx)
|
||||
}
|
||||
|
||||
pub fn render_with<C>(&self, component: C) -> RenderViewWith<C, V>
|
||||
where
|
||||
C: 'static + Component<V>,
|
||||
{
|
||||
RenderViewWith {
|
||||
view: self.clone(),
|
||||
component: Some(component),
|
||||
}
|
||||
}
|
||||
// pub fn render_with<E>(&self, component: E) -> RenderViewWith<E, V>
|
||||
// where
|
||||
// E: 'static + Element,
|
||||
// {
|
||||
// RenderViewWith {
|
||||
// view: self.clone(),
|
||||
// element: Some(component),
|
||||
// }
|
||||
// }
|
||||
|
||||
pub fn focus_handle(&self, cx: &AppContext) -> FocusHandle
|
||||
where
|
||||
@@ -83,6 +77,24 @@ impl<V: 'static> View<V> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: Render> Element for View<V> {
|
||||
type State = Option<AnyElement>;
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
_state: Option<Self::State>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::State) {
|
||||
let mut element = self.update(cx, |view, cx| view.render(cx).into_any());
|
||||
let layout_id = element.layout(cx);
|
||||
(layout_id, Some(element))
|
||||
}
|
||||
|
||||
fn paint(self, _: Bounds<Pixels>, element: &mut Self::State, cx: &mut WindowContext) {
|
||||
element.take().unwrap().paint(cx);
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> Clone for View<V> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
@@ -105,12 +117,6 @@ impl<V> PartialEq for View<V> {
|
||||
|
||||
impl<V> Eq for View<V> {}
|
||||
|
||||
impl<V: Render, ParentViewState: 'static> Component<ParentViewState> for View<V> {
|
||||
fn render(self) -> AnyElement<ParentViewState> {
|
||||
AnyElement::new(AnyView::from(self))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WeakView<V> {
|
||||
pub(crate) model: WeakModel<V>,
|
||||
}
|
||||
@@ -163,8 +169,8 @@ impl<V> Eq for WeakView<V> {}
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AnyView {
|
||||
model: AnyModel,
|
||||
layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, Box<dyn Any>),
|
||||
paint: fn(&AnyView, &mut AnyBox, &mut WindowContext),
|
||||
layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, AnyElement),
|
||||
paint: fn(&AnyView, AnyElement, &mut WindowContext),
|
||||
}
|
||||
|
||||
impl AnyView {
|
||||
@@ -191,6 +197,10 @@ impl AnyView {
|
||||
self.model.entity_type
|
||||
}
|
||||
|
||||
pub fn entity_id(&self) -> EntityId {
|
||||
self.model.entity_id()
|
||||
}
|
||||
|
||||
pub(crate) fn draw(
|
||||
&self,
|
||||
origin: Point<Pixels>,
|
||||
@@ -198,21 +208,15 @@ impl AnyView {
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
cx.with_absolute_element_offset(origin, |cx| {
|
||||
let (layout_id, mut rendered_element) = (self.layout)(self, cx);
|
||||
let (layout_id, rendered_element) = (self.layout)(self, cx);
|
||||
cx.window
|
||||
.layout_engine
|
||||
.compute_layout(layout_id, available_space);
|
||||
(self.paint)(self, &mut rendered_element, cx);
|
||||
(self.paint)(self, rendered_element, cx);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: 'static> Component<V> for AnyView {
|
||||
fn render(self) -> AnyElement<V> {
|
||||
AnyElement::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: Render> From<View<V>> for AnyView {
|
||||
fn from(value: View<V>) -> Self {
|
||||
AnyView {
|
||||
@@ -223,37 +227,51 @@ impl<V: Render> From<View<V>> for AnyView {
|
||||
}
|
||||
}
|
||||
|
||||
impl<ParentViewState: 'static> Element<ParentViewState> for AnyView {
|
||||
type ElementState = Box<dyn Any>;
|
||||
impl Element for AnyView {
|
||||
type State = Option<AnyElement>;
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
_state: Option<Self::State>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::State) {
|
||||
let (layout_id, state) = (self.layout)(self, cx);
|
||||
(layout_id, Some(state))
|
||||
}
|
||||
|
||||
fn paint(self, _: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
|
||||
(self.paint)(&self, state.take().unwrap(), cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: 'static + Render> RenderOnce for View<V> {
|
||||
type Element = View<V>;
|
||||
|
||||
fn element_id(&self) -> Option<ElementId> {
|
||||
Some(self.model.entity_id.into())
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
_view_state: &mut ParentViewState,
|
||||
_element_state: Option<Self::ElementState>,
|
||||
cx: &mut ViewContext<ParentViewState>,
|
||||
) -> (LayoutId, Self::ElementState) {
|
||||
(self.layout)(self, cx)
|
||||
fn render_once(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for AnyView {
|
||||
type Element = Self;
|
||||
|
||||
fn element_id(&self) -> Option<ElementId> {
|
||||
Some(self.model.entity_id.into())
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_bounds: Bounds<Pixels>,
|
||||
_view_state: &mut ParentViewState,
|
||||
rendered_element: &mut Self::ElementState,
|
||||
cx: &mut ViewContext<ParentViewState>,
|
||||
) {
|
||||
(self.paint)(self, rendered_element, cx)
|
||||
fn render_once(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AnyWeakView {
|
||||
model: AnyWeakModel,
|
||||
layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, Box<dyn Any>),
|
||||
paint: fn(&AnyView, &mut AnyBox, &mut WindowContext),
|
||||
layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, AnyElement),
|
||||
paint: fn(&AnyView, AnyElement, &mut WindowContext),
|
||||
}
|
||||
|
||||
impl AnyWeakView {
|
||||
@@ -267,7 +285,7 @@ impl AnyWeakView {
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: Render> From<WeakView<V>> for AnyWeakView {
|
||||
impl<V: 'static + Render> From<WeakView<V>> for AnyWeakView {
|
||||
fn from(view: WeakView<V>) -> Self {
|
||||
Self {
|
||||
model: view.model.into(),
|
||||
@@ -280,7 +298,7 @@ impl<V: Render> From<WeakView<V>> for AnyWeakView {
|
||||
impl<T, E> Render for T
|
||||
where
|
||||
T: 'static + FnMut(&mut WindowContext) -> E,
|
||||
E: 'static + Send + Element<T>,
|
||||
E: 'static + Send + Element,
|
||||
{
|
||||
type Element = E;
|
||||
|
||||
@@ -289,85 +307,28 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RenderViewWith<C, V> {
|
||||
view: View<V>,
|
||||
component: Option<C>,
|
||||
}
|
||||
mod any_view {
|
||||
use crate::{AnyElement, AnyView, BorrowWindow, Element, LayoutId, Render, WindowContext};
|
||||
|
||||
impl<C, ParentViewState, ViewState> Component<ParentViewState> for RenderViewWith<C, ViewState>
|
||||
where
|
||||
C: 'static + Component<ViewState>,
|
||||
ParentViewState: 'static,
|
||||
ViewState: 'static,
|
||||
{
|
||||
fn render(self) -> AnyElement<ParentViewState> {
|
||||
AnyElement::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<C, ParentViewState, ViewState> Element<ParentViewState> for RenderViewWith<C, ViewState>
|
||||
where
|
||||
C: 'static + Component<ViewState>,
|
||||
ParentViewState: 'static,
|
||||
ViewState: 'static,
|
||||
{
|
||||
type ElementState = AnyElement<ViewState>;
|
||||
|
||||
fn element_id(&self) -> Option<ElementId> {
|
||||
Some(self.view.entity_id().into())
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
_: &mut ParentViewState,
|
||||
_: Option<Self::ElementState>,
|
||||
cx: &mut ViewContext<ParentViewState>,
|
||||
) -> (LayoutId, Self::ElementState) {
|
||||
self.view.update(cx, |view, cx| {
|
||||
let mut element = self.component.take().unwrap().render();
|
||||
let layout_id = element.layout(view, cx);
|
||||
pub(crate) fn layout<V: 'static + Render>(
|
||||
view: &AnyView,
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, AnyElement) {
|
||||
cx.with_element_id(Some(view.model.entity_id), |cx| {
|
||||
let view = view.clone().downcast::<V>().unwrap();
|
||||
let mut element = view.update(cx, |view, cx| view.render(cx).into_any());
|
||||
let layout_id = element.layout(cx);
|
||||
(layout_id, element)
|
||||
})
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_: Bounds<Pixels>,
|
||||
_: &mut ParentViewState,
|
||||
element: &mut Self::ElementState,
|
||||
cx: &mut ViewContext<ParentViewState>,
|
||||
) {
|
||||
self.view.update(cx, |view, cx| element.paint(view, cx))
|
||||
}
|
||||
}
|
||||
|
||||
mod any_view {
|
||||
use crate::{AnyElement, AnyView, BorrowWindow, LayoutId, Render, WindowContext};
|
||||
use std::any::Any;
|
||||
|
||||
pub(crate) fn layout<V: Render>(
|
||||
pub(crate) fn paint<V: 'static + Render>(
|
||||
view: &AnyView,
|
||||
cx: &mut WindowContext,
|
||||
) -> (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>,
|
||||
element: AnyElement,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
cx.with_element_id(Some(view.model.entity_id), |cx| {
|
||||
let view = view.clone().downcast::<V>().unwrap();
|
||||
let element = element.downcast_mut::<AnyElement<V>>().unwrap();
|
||||
view.update(cx, |view, cx| element.paint(view, cx))
|
||||
element.paint(cx);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+148
-80
@@ -1,15 +1,15 @@
|
||||
use crate::{
|
||||
key_dispatch::DispatchActionListener, px, size, Action, AnyBox, AnyDrag, AnyView, AppContext,
|
||||
key_dispatch::DispatchActionListener, px, size, Action, AnyDrag, AnyView, AppContext,
|
||||
AsyncWindowContext, AvailableSpace, Bounds, BoxShadow, Context, Corners, CursorStyle,
|
||||
DevicePixels, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId,
|
||||
EventEmitter, FileDropEvent, FocusEvent, FontId, GlobalElementId, GlyphId, Hsla, ImageData,
|
||||
InputEvent, IsZero, KeyBinding, KeyContext, KeyDownEvent, LayoutId, Model, ModelContext,
|
||||
Modifiers, MonochromeSprite, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Path,
|
||||
Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point,
|
||||
PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams,
|
||||
RenderSvgParams, ScaledPixels, SceneBuilder, Shadow, SharedString, Size, Style, SubscriberSet,
|
||||
Subscription, TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, VisualContext,
|
||||
WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS,
|
||||
EventEmitter, FileDropEvent, Flatten, FocusEvent, FontId, GlobalElementId, GlyphId, Hsla,
|
||||
ImageData, InputEvent, IsZero, KeyBinding, KeyContext, KeyDownEvent, LayoutId, Model,
|
||||
ModelContext, Modifiers, MonochromeSprite, MouseButton, MouseDownEvent, MouseMoveEvent,
|
||||
MouseUpEvent, Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler,
|
||||
PlatformWindow, Point, PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams,
|
||||
RenderImageParams, RenderSvgParams, ScaledPixels, SceneBuilder, Shadow, SharedString, Size,
|
||||
Style, SubscriberSet, Subscription, TaffyLayoutEngine, Task, Underline, UnderlineStyle, View,
|
||||
VisualContext, WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS,
|
||||
};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use collections::HashMap;
|
||||
@@ -187,23 +187,18 @@ impl Drop for FocusHandle {
|
||||
|
||||
/// FocusableView allows users of your view to easily
|
||||
/// focus it (using cx.focus_view(view))
|
||||
pub trait FocusableView: Render {
|
||||
pub trait FocusableView: 'static + Render {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle;
|
||||
}
|
||||
|
||||
/// ManagedView is a view (like a Modal, Popover, Menu, etc.)
|
||||
/// where the lifecycle of the view is handled by another view.
|
||||
pub trait ManagedView: Render {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle;
|
||||
}
|
||||
pub trait ManagedView: FocusableView + EventEmitter<Manager> {}
|
||||
|
||||
pub struct Dismiss;
|
||||
impl<T: ManagedView> EventEmitter<Dismiss> for T {}
|
||||
impl<M: FocusableView + EventEmitter<Manager>> ManagedView for M {}
|
||||
|
||||
impl<T: ManagedView> FocusableView for T {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
self.focus_handle(cx)
|
||||
}
|
||||
pub enum Manager {
|
||||
Dismiss,
|
||||
}
|
||||
|
||||
// Holds the state for a specific window.
|
||||
@@ -237,7 +232,7 @@ pub struct Window {
|
||||
|
||||
// #[derive(Default)]
|
||||
pub(crate) struct Frame {
|
||||
pub(crate) element_states: HashMap<GlobalElementId, AnyBox>,
|
||||
pub(crate) element_states: HashMap<GlobalElementId, Box<dyn Any>>,
|
||||
mouse_listeners: HashMap<TypeId, Vec<(StackingOrder, AnyMouseListener)>>,
|
||||
pub(crate) dispatch_tree: DispatchTree,
|
||||
pub(crate) focus_listeners: Vec<AnyFocusListener>,
|
||||
@@ -1441,6 +1436,82 @@ impl<'a> WindowContext<'a> {
|
||||
.dispatch_tree
|
||||
.bindings_for_action(action)
|
||||
}
|
||||
|
||||
pub fn listener_for<V: Render, E>(
|
||||
&self,
|
||||
view: &View<V>,
|
||||
f: impl Fn(&mut V, &E, &mut ViewContext<V>) + 'static,
|
||||
) -> impl Fn(&E, &mut WindowContext) + 'static {
|
||||
let view = view.downgrade();
|
||||
move |e: &E, cx: &mut WindowContext| {
|
||||
view.update(cx, |view, cx| f(view, e, cx)).ok();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn constructor_for<V: Render, R>(
|
||||
&self,
|
||||
view: &View<V>,
|
||||
f: impl Fn(&mut V, &mut ViewContext<V>) -> R + 'static,
|
||||
) -> impl Fn(&mut WindowContext) -> R + 'static {
|
||||
let view = view.clone();
|
||||
move |cx: &mut WindowContext| view.update(cx, |view, cx| f(view, cx))
|
||||
}
|
||||
|
||||
//========== ELEMENT RELATED FUNCTIONS ===========
|
||||
pub fn with_key_dispatch<R>(
|
||||
&mut self,
|
||||
context: KeyContext,
|
||||
focus_handle: Option<FocusHandle>,
|
||||
f: impl FnOnce(Option<FocusHandle>, &mut Self) -> R,
|
||||
) -> R {
|
||||
let window = &mut self.window;
|
||||
window
|
||||
.current_frame
|
||||
.dispatch_tree
|
||||
.push_node(context.clone());
|
||||
if let Some(focus_handle) = focus_handle.as_ref() {
|
||||
window
|
||||
.current_frame
|
||||
.dispatch_tree
|
||||
.make_focusable(focus_handle.id);
|
||||
}
|
||||
let result = f(focus_handle, self);
|
||||
|
||||
self.window.current_frame.dispatch_tree.pop_node();
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Register a focus listener for the current frame only. It will be cleared
|
||||
/// on the next frame render. You should use this method only from within elements,
|
||||
/// and we may want to enforce that better via a different context type.
|
||||
// todo!() Move this to `FrameContext` to emphasize its individuality?
|
||||
pub fn on_focus_changed(
|
||||
&mut self,
|
||||
listener: impl Fn(&FocusEvent, &mut WindowContext) + 'static,
|
||||
) {
|
||||
self.window
|
||||
.current_frame
|
||||
.focus_listeners
|
||||
.push(Box::new(move |event, cx| {
|
||||
listener(event, cx);
|
||||
}));
|
||||
}
|
||||
|
||||
/// Set an input handler, such as [ElementInputHandler], which interfaces with the
|
||||
/// platform to receive textual input with proper integration with concerns such
|
||||
/// as IME interactions.
|
||||
pub fn handle_input(
|
||||
&mut self,
|
||||
focus_handle: &FocusHandle,
|
||||
input_handler: impl PlatformInputHandler,
|
||||
) {
|
||||
if focus_handle.is_focused(self) {
|
||||
self.window
|
||||
.platform_window
|
||||
.set_input_handler(Box::new(input_handler));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Context for WindowContext<'_> {
|
||||
@@ -1564,7 +1635,7 @@ impl VisualContext for WindowContext<'_> {
|
||||
build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V,
|
||||
) -> Self::Result<View<V>>
|
||||
where
|
||||
V: Render,
|
||||
V: 'static + Render,
|
||||
{
|
||||
let slot = self.app.entities.reserve();
|
||||
let view = View {
|
||||
@@ -1582,6 +1653,13 @@ impl VisualContext for WindowContext<'_> {
|
||||
view.focus_handle(cx).clone().focus(cx);
|
||||
})
|
||||
}
|
||||
|
||||
fn dismiss_view<V>(&mut self, view: &View<V>) -> Self::Result<()>
|
||||
where
|
||||
V: ManagedView,
|
||||
{
|
||||
self.update_view(view, |_, cx| cx.emit(Manager::Dismiss))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> std::ops::Deref for WindowContext<'a> {
|
||||
@@ -1615,6 +1693,10 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
|
||||
self.borrow_mut()
|
||||
}
|
||||
|
||||
fn app(&self) -> &AppContext {
|
||||
self.borrow()
|
||||
}
|
||||
|
||||
fn window(&self) -> &Window {
|
||||
self.borrow()
|
||||
}
|
||||
@@ -2122,49 +2204,6 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
||||
)
|
||||
}
|
||||
|
||||
/// Register a focus listener for the current frame only. It will be cleared
|
||||
/// on the next frame render. You should use this method only from within elements,
|
||||
/// and we may want to enforce that better via a different context type.
|
||||
// todo!() Move this to `FrameContext` to emphasize its individuality?
|
||||
pub fn on_focus_changed(
|
||||
&mut self,
|
||||
listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + 'static,
|
||||
) {
|
||||
let handle = self.view().downgrade();
|
||||
self.window
|
||||
.current_frame
|
||||
.focus_listeners
|
||||
.push(Box::new(move |event, cx| {
|
||||
handle
|
||||
.update(cx, |view, cx| listener(view, event, cx))
|
||||
.log_err();
|
||||
}));
|
||||
}
|
||||
|
||||
pub fn with_key_dispatch<R>(
|
||||
&mut self,
|
||||
context: KeyContext,
|
||||
focus_handle: Option<FocusHandle>,
|
||||
f: impl FnOnce(Option<FocusHandle>, &mut Self) -> R,
|
||||
) -> R {
|
||||
let window = &mut self.window;
|
||||
window
|
||||
.current_frame
|
||||
.dispatch_tree
|
||||
.push_node(context.clone());
|
||||
if let Some(focus_handle) = focus_handle.as_ref() {
|
||||
window
|
||||
.current_frame
|
||||
.dispatch_tree
|
||||
.make_focusable(focus_handle.id);
|
||||
}
|
||||
let result = f(focus_handle, self);
|
||||
|
||||
self.window.current_frame.dispatch_tree.pop_node();
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
pub fn spawn<Fut, R>(
|
||||
&mut self,
|
||||
f: impl FnOnce(WeakView<V>, AsyncWindowContext) -> Fut,
|
||||
@@ -2241,21 +2280,6 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
||||
});
|
||||
}
|
||||
|
||||
/// Set an input handler, such as [ElementInputHandler], which interfaces with the
|
||||
/// platform to receive textual input with proper integration with concerns such
|
||||
/// as IME interactions.
|
||||
pub fn handle_input(
|
||||
&mut self,
|
||||
focus_handle: &FocusHandle,
|
||||
input_handler: impl PlatformInputHandler,
|
||||
) {
|
||||
if focus_handle.is_focused(self) {
|
||||
self.window
|
||||
.platform_window
|
||||
.set_input_handler(Box::new(input_handler));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn emit<Evt>(&mut self, event: Evt)
|
||||
where
|
||||
Evt: 'static,
|
||||
@@ -2275,6 +2299,23 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
||||
{
|
||||
self.defer(|view, cx| view.focus_handle(cx).focus(cx))
|
||||
}
|
||||
|
||||
pub fn dismiss_self(&mut self)
|
||||
where
|
||||
V: ManagedView,
|
||||
{
|
||||
self.defer(|_, cx| cx.emit(Manager::Dismiss))
|
||||
}
|
||||
|
||||
pub fn listener<E>(
|
||||
&self,
|
||||
f: impl Fn(&mut V, &E, &mut ViewContext<V>) + 'static,
|
||||
) -> impl Fn(&E, &mut WindowContext) + 'static {
|
||||
let view = self.view().downgrade();
|
||||
move |e: &E, cx: &mut WindowContext| {
|
||||
view.update(cx, |view, cx| f(view, e, cx)).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> Context for ViewContext<'_, V> {
|
||||
@@ -2346,7 +2387,7 @@ impl<V: 'static> VisualContext for ViewContext<'_, V> {
|
||||
build_view: impl FnOnce(&mut ViewContext<'_, W>) -> W,
|
||||
) -> Self::Result<View<W>>
|
||||
where
|
||||
W: Render,
|
||||
W: 'static + Render,
|
||||
{
|
||||
self.window_cx.replace_root_view(build_view)
|
||||
}
|
||||
@@ -2354,6 +2395,10 @@ impl<V: 'static> VisualContext for ViewContext<'_, V> {
|
||||
fn focus_view<W: FocusableView>(&mut self, view: &View<W>) -> Self::Result<()> {
|
||||
self.window_cx.focus_view(view)
|
||||
}
|
||||
|
||||
fn dismiss_view<W: ManagedView>(&mut self, view: &View<W>) -> Self::Result<()> {
|
||||
self.window_cx.dismiss_view(view)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, V> std::ops::Deref for ViewContext<'a, V> {
|
||||
@@ -2398,6 +2443,17 @@ impl<V: 'static + Render> WindowHandle<V> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn root<C>(&self, cx: &mut C) -> Result<View<V>>
|
||||
where
|
||||
C: Context,
|
||||
{
|
||||
Flatten::flatten(cx.update_window(self.any_handle, |root_view, _| {
|
||||
root_view
|
||||
.downcast::<V>()
|
||||
.map_err(|_| anyhow!("the type of the window's root view has changed"))
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn update<C, R>(
|
||||
&self,
|
||||
cx: &mut C,
|
||||
@@ -2543,6 +2599,18 @@ pub enum ElementId {
|
||||
FocusHandle(FocusId),
|
||||
}
|
||||
|
||||
impl TryInto<SharedString> for ElementId {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_into(self) -> anyhow::Result<SharedString> {
|
||||
if let ElementId::Name(name) = self {
|
||||
Ok(name)
|
||||
} else {
|
||||
Err(anyhow!("element id is not string"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EntityId> for ElementId {
|
||||
fn from(id: EntityId) -> Self {
|
||||
ElementId::View(id)
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::{parse_macro_input, DeriveInput};
|
||||
|
||||
pub fn derive_render_once(input: TokenStream) -> TokenStream {
|
||||
let ast = parse_macro_input!(input as DeriveInput);
|
||||
let type_name = &ast.ident;
|
||||
let (impl_generics, type_generics, where_clause) = ast.generics.split_for_impl();
|
||||
|
||||
let gen = quote! {
|
||||
impl #impl_generics gpui::RenderOnce for #type_name #type_generics
|
||||
#where_clause
|
||||
{
|
||||
type Element = gpui::CompositeElement<Self>;
|
||||
|
||||
fn element_id(&self) -> Option<ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn render_once(self) -> Self::Element {
|
||||
gpui::CompositeElement::new(self)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
gen.into()
|
||||
}
|
||||
@@ -1,16 +1,12 @@
|
||||
mod action;
|
||||
mod derive_component;
|
||||
mod derive_render_once;
|
||||
mod register_action;
|
||||
mod style_helpers;
|
||||
mod test;
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
|
||||
#[proc_macro]
|
||||
pub fn style_helpers(args: TokenStream) -> TokenStream {
|
||||
style_helpers::style_helpers(args)
|
||||
}
|
||||
|
||||
#[proc_macro_derive(Action)]
|
||||
pub fn action(input: TokenStream) -> TokenStream {
|
||||
action::action(input)
|
||||
@@ -26,6 +22,16 @@ pub fn derive_component(input: TokenStream) -> TokenStream {
|
||||
derive_component::derive_component(input)
|
||||
}
|
||||
|
||||
#[proc_macro_derive(RenderOnce, attributes(view))]
|
||||
pub fn derive_render_once(input: TokenStream) -> TokenStream {
|
||||
derive_render_once::derive_render_once(input)
|
||||
}
|
||||
|
||||
#[proc_macro]
|
||||
pub fn style_helpers(input: TokenStream) -> TokenStream {
|
||||
style_helpers::style_helpers(input)
|
||||
}
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
|
||||
test::test(args, function)
|
||||
|
||||
@@ -9,7 +9,7 @@ path = "src/journal2.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
editor = { path = "../editor" }
|
||||
editor = { package = "editor2", path = "../editor2" }
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
util = { path = "../util" }
|
||||
workspace2 = { path = "../workspace2" }
|
||||
@@ -24,4 +24,4 @@ log.workspace = true
|
||||
shellexpand = "2.1.0"
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { path = "../editor", features = ["test-support"] }
|
||||
editor = { package="editor2", path = "../editor2", features = ["test-support"] }
|
||||
|
||||
@@ -61,12 +61,14 @@ fn build_bridge(swift_target: &SwiftTarget) {
|
||||
|
||||
let swift_package_root = swift_package_root();
|
||||
let swift_target_folder = swift_target_folder();
|
||||
let swift_cache_folder = swift_cache_folder();
|
||||
if !Command::new("swift")
|
||||
.arg("build")
|
||||
.arg("--disable-automatic-resolution")
|
||||
.args(["--configuration", &env::var("PROFILE").unwrap()])
|
||||
.args(["--triple", &swift_target.target.triple])
|
||||
.args(["--build-path".into(), swift_target_folder])
|
||||
.args(["--cache-path".into(), swift_cache_folder])
|
||||
.current_dir(&swift_package_root)
|
||||
.status()
|
||||
.unwrap()
|
||||
@@ -133,9 +135,17 @@ fn swift_package_root() -> PathBuf {
|
||||
}
|
||||
|
||||
fn swift_target_folder() -> PathBuf {
|
||||
let target = env::var("TARGET").unwrap();
|
||||
env::current_dir()
|
||||
.unwrap()
|
||||
.join(format!("../../target/{SWIFT_PACKAGE_NAME}"))
|
||||
.join(format!("../../target/{target}/{SWIFT_PACKAGE_NAME}_target"))
|
||||
}
|
||||
|
||||
fn swift_cache_folder() -> PathBuf {
|
||||
let target = env::var("TARGET").unwrap();
|
||||
env::current_dir()
|
||||
.unwrap()
|
||||
.join(format!("../../target/{target}/{SWIFT_PACKAGE_NAME}_cache"))
|
||||
}
|
||||
|
||||
fn copy_dir(source: &Path, destination: &Path) {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use editor::Editor;
|
||||
use gpui::{
|
||||
div, prelude::*, uniform_list, AppContext, Component, Div, FocusHandle, FocusableView,
|
||||
MouseButton, Render, Task, UniformListScrollHandle, View, ViewContext, WindowContext,
|
||||
div, prelude::*, uniform_list, AppContext, Div, FocusHandle, FocusableView, MouseButton,
|
||||
MouseDownEvent, Render, Task, UniformListScrollHandle, View, ViewContext, WindowContext,
|
||||
};
|
||||
use std::{cmp, sync::Arc};
|
||||
use ui::{prelude::*, v_stack, Divider, Label, TextColor};
|
||||
use ui::{prelude::*, v_stack, Color, Divider, Label};
|
||||
|
||||
pub struct Picker<D: PickerDelegate> {
|
||||
pub delegate: D,
|
||||
@@ -15,7 +15,7 @@ pub struct Picker<D: PickerDelegate> {
|
||||
}
|
||||
|
||||
pub trait PickerDelegate: Sized + 'static {
|
||||
type ListItem: Component<Picker<Self>>;
|
||||
type ListItem: RenderOnce;
|
||||
|
||||
fn match_count(&self) -> usize;
|
||||
fn selected_index(&self) -> usize;
|
||||
@@ -143,10 +143,10 @@ impl<D: PickerDelegate> Picker<D> {
|
||||
fn on_input_editor_event(
|
||||
&mut self,
|
||||
_: View<Editor>,
|
||||
event: &editor::Event,
|
||||
event: &editor::EditorEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if let editor::Event::BufferEdited = event {
|
||||
if let editor::EditorEvent::BufferEdited = event {
|
||||
let query = self.editor.read(cx).text(cx);
|
||||
self.update_matches(query, cx);
|
||||
}
|
||||
@@ -181,20 +181,20 @@ impl<D: PickerDelegate> Picker<D> {
|
||||
}
|
||||
|
||||
impl<D: PickerDelegate> Render for Picker<D> {
|
||||
type Element = Div<Self>;
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
div()
|
||||
.key_context("picker")
|
||||
.size_full()
|
||||
.elevation_2(cx)
|
||||
.on_action(Self::select_next)
|
||||
.on_action(Self::select_prev)
|
||||
.on_action(Self::select_first)
|
||||
.on_action(Self::select_last)
|
||||
.on_action(Self::cancel)
|
||||
.on_action(Self::confirm)
|
||||
.on_action(Self::secondary_confirm)
|
||||
.on_action(cx.listener(Self::select_next))
|
||||
.on_action(cx.listener(Self::select_prev))
|
||||
.on_action(cx.listener(Self::select_first))
|
||||
.on_action(cx.listener(Self::select_last))
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.on_action(cx.listener(Self::confirm))
|
||||
.on_action(cx.listener(Self::secondary_confirm))
|
||||
.child(
|
||||
v_stack()
|
||||
.py_0p5()
|
||||
@@ -208,31 +208,37 @@ impl<D: PickerDelegate> Render for Picker<D> {
|
||||
.p_1()
|
||||
.grow()
|
||||
.child(
|
||||
uniform_list("candidates", self.delegate.match_count(), {
|
||||
move |this: &mut Self, visible_range, cx| {
|
||||
let selected_ix = this.delegate.selected_index();
|
||||
visible_range
|
||||
.map(|ix| {
|
||||
div()
|
||||
.on_mouse_down(
|
||||
MouseButton::Left,
|
||||
move |this: &mut Self, event, cx| {
|
||||
this.handle_click(
|
||||
ix,
|
||||
event.modifiers.command,
|
||||
cx,
|
||||
)
|
||||
},
|
||||
)
|
||||
.child(this.delegate.render_match(
|
||||
ix,
|
||||
ix == selected_ix,
|
||||
cx,
|
||||
))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
})
|
||||
uniform_list(
|
||||
cx.view().clone(),
|
||||
"candidates",
|
||||
self.delegate.match_count(),
|
||||
{
|
||||
let selected_index = self.delegate.selected_index();
|
||||
|
||||
move |picker, visible_range, cx| {
|
||||
visible_range
|
||||
.map(|ix| {
|
||||
div()
|
||||
.on_mouse_down(
|
||||
MouseButton::Left,
|
||||
cx.listener(move |this, event: &MouseDownEvent, cx| {
|
||||
this.handle_click(
|
||||
ix,
|
||||
event.modifiers.command,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
)
|
||||
.child(picker.delegate.render_match(
|
||||
ix,
|
||||
ix == selected_index,
|
||||
cx,
|
||||
))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
},
|
||||
)
|
||||
.track_scroll(self.scroll_handle.clone()),
|
||||
)
|
||||
.max_h_72()
|
||||
@@ -244,7 +250,7 @@ impl<D: PickerDelegate> Render for Picker<D> {
|
||||
v_stack().p_1().grow().child(
|
||||
div()
|
||||
.px_1()
|
||||
.child(Label::new("No matches").color(TextColor::Muted)),
|
||||
.child(Label::new("No matches").color(Color::Muted)),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -20,10 +20,6 @@ impl IgnoreStack {
|
||||
Arc::new(Self::All)
|
||||
}
|
||||
|
||||
pub fn is_all(&self) -> bool {
|
||||
matches!(self, IgnoreStack::All)
|
||||
}
|
||||
|
||||
pub fn append(self: Arc<Self>, abs_base_path: Arc<Path>, ignore: Arc<Gitignore>) -> Arc<Self> {
|
||||
match self.as_ref() {
|
||||
IgnoreStack::All => self,
|
||||
|
||||
@@ -5548,7 +5548,16 @@ impl Project {
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let background = cx.background().clone();
|
||||
let path_count: usize = snapshots.iter().map(|s| s.visible_file_count()).sum();
|
||||
let path_count: usize = snapshots
|
||||
.iter()
|
||||
.map(|s| {
|
||||
if query.include_ignored() {
|
||||
s.file_count()
|
||||
} else {
|
||||
s.visible_file_count()
|
||||
}
|
||||
})
|
||||
.sum();
|
||||
if path_count == 0 {
|
||||
let (_, rx) = smol::channel::bounded(1024);
|
||||
return rx;
|
||||
@@ -5561,8 +5570,16 @@ impl Project {
|
||||
.iter()
|
||||
.filter_map(|(_, b)| {
|
||||
let buffer = b.upgrade(cx)?;
|
||||
let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
|
||||
if let Some(path) = snapshot.file().map(|file| file.path()) {
|
||||
let (is_ignored, snapshot) = buffer.update(cx, |buffer, cx| {
|
||||
let is_ignored = buffer
|
||||
.project_path(cx)
|
||||
.and_then(|path| self.entry_for_path(&path, cx))
|
||||
.map_or(false, |entry| entry.is_ignored);
|
||||
(is_ignored, buffer.snapshot())
|
||||
});
|
||||
if is_ignored && !query.include_ignored() {
|
||||
return None;
|
||||
} else if let Some(path) = snapshot.file().map(|file| file.path()) {
|
||||
Some((path.clone(), (buffer, snapshot)))
|
||||
} else {
|
||||
unnamed_files.push(buffer);
|
||||
@@ -5735,7 +5752,12 @@ impl Project {
|
||||
let mut snapshot_start_ix = 0;
|
||||
let mut abs_path = PathBuf::new();
|
||||
for snapshot in snapshots {
|
||||
let snapshot_end_ix = snapshot_start_ix + snapshot.visible_file_count();
|
||||
let snapshot_end_ix = snapshot_start_ix
|
||||
+ if query.include_ignored() {
|
||||
snapshot.file_count()
|
||||
} else {
|
||||
snapshot.visible_file_count()
|
||||
};
|
||||
if worker_end_ix <= snapshot_start_ix {
|
||||
break;
|
||||
} else if worker_start_ix > snapshot_end_ix {
|
||||
@@ -5748,7 +5770,7 @@ impl Project {
|
||||
cmp::min(worker_end_ix, snapshot_end_ix) - snapshot_start_ix;
|
||||
|
||||
for entry in snapshot
|
||||
.files(false, start_in_snapshot)
|
||||
.files(query.include_ignored(), start_in_snapshot)
|
||||
.take(end_in_snapshot - start_in_snapshot)
|
||||
{
|
||||
if matching_paths_tx.is_closed() {
|
||||
|
||||
@@ -10,6 +10,8 @@ pub struct ProjectSettings {
|
||||
pub lsp: HashMap<Arc<str>, LspSettings>,
|
||||
#[serde(default)]
|
||||
pub git: GitSettings,
|
||||
#[serde(default)]
|
||||
pub file_scan_exclusions: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
|
||||
@@ -3598,7 +3598,7 @@ async fn test_search(cx: &mut gpui::TestAppContext) {
|
||||
assert_eq!(
|
||||
search(
|
||||
&project,
|
||||
SearchQuery::text("TWO", false, true, Vec::new(), Vec::new()).unwrap(),
|
||||
SearchQuery::text("TWO", false, true, false, Vec::new(), Vec::new()).unwrap(),
|
||||
cx
|
||||
)
|
||||
.await
|
||||
@@ -3623,7 +3623,7 @@ async fn test_search(cx: &mut gpui::TestAppContext) {
|
||||
assert_eq!(
|
||||
search(
|
||||
&project,
|
||||
SearchQuery::text("TWO", false, true, Vec::new(), Vec::new()).unwrap(),
|
||||
SearchQuery::text("TWO", false, true, false, Vec::new(), Vec::new()).unwrap(),
|
||||
cx
|
||||
)
|
||||
.await
|
||||
@@ -3662,6 +3662,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
|
||||
search_query,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
vec![PathMatcher::new("*.odd").unwrap()],
|
||||
Vec::new()
|
||||
)
|
||||
@@ -3681,6 +3682,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
|
||||
search_query,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
vec![PathMatcher::new("*.rs").unwrap()],
|
||||
Vec::new()
|
||||
)
|
||||
@@ -3703,6 +3705,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
|
||||
search_query,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
vec![
|
||||
PathMatcher::new("*.ts").unwrap(),
|
||||
PathMatcher::new("*.odd").unwrap(),
|
||||
@@ -3727,6 +3730,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
|
||||
search_query,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
vec![
|
||||
PathMatcher::new("*.rs").unwrap(),
|
||||
PathMatcher::new("*.ts").unwrap(),
|
||||
@@ -3774,6 +3778,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
|
||||
search_query,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
Vec::new(),
|
||||
vec![PathMatcher::new("*.odd").unwrap()],
|
||||
)
|
||||
@@ -3798,6 +3803,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
|
||||
search_query,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
Vec::new(),
|
||||
vec![PathMatcher::new("*.rs").unwrap()],
|
||||
)
|
||||
@@ -3820,6 +3826,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
|
||||
search_query,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
Vec::new(),
|
||||
vec![
|
||||
PathMatcher::new("*.ts").unwrap(),
|
||||
@@ -3844,6 +3851,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
|
||||
search_query,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
Vec::new(),
|
||||
vec![
|
||||
PathMatcher::new("*.rs").unwrap(),
|
||||
@@ -3885,6 +3893,7 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
|
||||
search_query,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
vec![PathMatcher::new("*.odd").unwrap()],
|
||||
vec![PathMatcher::new("*.odd").unwrap()],
|
||||
)
|
||||
@@ -3904,6 +3913,7 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
|
||||
search_query,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
vec![PathMatcher::new("*.ts").unwrap()],
|
||||
vec![PathMatcher::new("*.ts").unwrap()],
|
||||
).unwrap(),
|
||||
@@ -3922,6 +3932,7 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
|
||||
search_query,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
vec![
|
||||
PathMatcher::new("*.ts").unwrap(),
|
||||
PathMatcher::new("*.odd").unwrap()
|
||||
@@ -3947,6 +3958,7 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
|
||||
search_query,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
vec![
|
||||
PathMatcher::new("*.ts").unwrap(),
|
||||
PathMatcher::new("*.odd").unwrap()
|
||||
|
||||
@@ -39,6 +39,7 @@ pub enum SearchQuery {
|
||||
replacement: Option<String>,
|
||||
whole_word: bool,
|
||||
case_sensitive: bool,
|
||||
include_ignored: bool,
|
||||
inner: SearchInputs,
|
||||
},
|
||||
|
||||
@@ -48,6 +49,7 @@ pub enum SearchQuery {
|
||||
multiline: bool,
|
||||
whole_word: bool,
|
||||
case_sensitive: bool,
|
||||
include_ignored: bool,
|
||||
inner: SearchInputs,
|
||||
},
|
||||
}
|
||||
@@ -57,6 +59,7 @@ impl SearchQuery {
|
||||
query: impl ToString,
|
||||
whole_word: bool,
|
||||
case_sensitive: bool,
|
||||
include_ignored: bool,
|
||||
files_to_include: Vec<PathMatcher>,
|
||||
files_to_exclude: Vec<PathMatcher>,
|
||||
) -> Result<Self> {
|
||||
@@ -74,6 +77,7 @@ impl SearchQuery {
|
||||
replacement: None,
|
||||
whole_word,
|
||||
case_sensitive,
|
||||
include_ignored,
|
||||
inner,
|
||||
})
|
||||
}
|
||||
@@ -82,6 +86,7 @@ impl SearchQuery {
|
||||
query: impl ToString,
|
||||
whole_word: bool,
|
||||
case_sensitive: bool,
|
||||
include_ignored: bool,
|
||||
files_to_include: Vec<PathMatcher>,
|
||||
files_to_exclude: Vec<PathMatcher>,
|
||||
) -> Result<Self> {
|
||||
@@ -111,6 +116,7 @@ impl SearchQuery {
|
||||
multiline,
|
||||
whole_word,
|
||||
case_sensitive,
|
||||
include_ignored,
|
||||
inner,
|
||||
})
|
||||
}
|
||||
@@ -121,6 +127,7 @@ impl SearchQuery {
|
||||
message.query,
|
||||
message.whole_word,
|
||||
message.case_sensitive,
|
||||
message.include_ignored,
|
||||
deserialize_path_matches(&message.files_to_include)?,
|
||||
deserialize_path_matches(&message.files_to_exclude)?,
|
||||
)
|
||||
@@ -129,6 +136,7 @@ impl SearchQuery {
|
||||
message.query,
|
||||
message.whole_word,
|
||||
message.case_sensitive,
|
||||
message.include_ignored,
|
||||
deserialize_path_matches(&message.files_to_include)?,
|
||||
deserialize_path_matches(&message.files_to_exclude)?,
|
||||
)
|
||||
@@ -156,6 +164,7 @@ impl SearchQuery {
|
||||
regex: self.is_regex(),
|
||||
whole_word: self.whole_word(),
|
||||
case_sensitive: self.case_sensitive(),
|
||||
include_ignored: self.include_ignored(),
|
||||
files_to_include: self
|
||||
.files_to_include()
|
||||
.iter()
|
||||
@@ -336,6 +345,17 @@ impl SearchQuery {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn include_ignored(&self) -> bool {
|
||||
match self {
|
||||
Self::Text {
|
||||
include_ignored, ..
|
||||
} => *include_ignored,
|
||||
Self::Regex {
|
||||
include_ignored, ..
|
||||
} => *include_ignored,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_regex(&self) -> bool {
|
||||
matches!(self, Self::Regex { .. })
|
||||
}
|
||||
|
||||
+236
-110
@@ -1,5 +1,6 @@
|
||||
use crate::{
|
||||
copy_recursive, ignore::IgnoreStack, DiagnosticSummary, ProjectEntryId, RemoveOptions,
|
||||
copy_recursive, ignore::IgnoreStack, project_settings::ProjectSettings, DiagnosticSummary,
|
||||
ProjectEntryId, RemoveOptions,
|
||||
};
|
||||
use ::ignore::gitignore::{Gitignore, GitignoreBuilder};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
@@ -21,7 +22,10 @@ use futures::{
|
||||
};
|
||||
use fuzzy::CharBag;
|
||||
use git::{DOT_GIT, GITIGNORE};
|
||||
use gpui::{executor, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task};
|
||||
use gpui::{
|
||||
executor, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Subscription, Task,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use language::{
|
||||
proto::{
|
||||
deserialize_fingerprint, deserialize_version, serialize_fingerprint, serialize_line_ending,
|
||||
@@ -36,6 +40,7 @@ use postage::{
|
||||
prelude::{Sink as _, Stream as _},
|
||||
watch,
|
||||
};
|
||||
use settings::SettingsStore;
|
||||
use smol::channel::{self, Sender};
|
||||
use std::{
|
||||
any::Any,
|
||||
@@ -55,7 +60,10 @@ use std::{
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet};
|
||||
use util::{paths::HOME, ResultExt};
|
||||
use util::{
|
||||
paths::{PathMatcher, HOME},
|
||||
ResultExt,
|
||||
};
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)]
|
||||
pub struct WorktreeId(usize);
|
||||
@@ -70,7 +78,8 @@ pub struct LocalWorktree {
|
||||
scan_requests_tx: channel::Sender<ScanRequest>,
|
||||
path_prefixes_to_scan_tx: channel::Sender<Arc<Path>>,
|
||||
is_scanning: (watch::Sender<bool>, watch::Receiver<bool>),
|
||||
_background_scanner_task: Task<()>,
|
||||
_settings_subscription: Subscription,
|
||||
_background_scanner_tasks: Vec<Task<()>>,
|
||||
share: Option<ShareState>,
|
||||
diagnostics: HashMap<
|
||||
Arc<Path>,
|
||||
@@ -216,6 +225,7 @@ pub struct LocalSnapshot {
|
||||
/// All of the git repositories in the worktree, indexed by the project entry
|
||||
/// id of their parent directory.
|
||||
git_repositories: TreeMap<ProjectEntryId, LocalRepositoryEntry>,
|
||||
file_scan_exclusions: Vec<PathMatcher>,
|
||||
}
|
||||
|
||||
struct BackgroundScannerState {
|
||||
@@ -299,17 +309,54 @@ impl Worktree {
|
||||
.await
|
||||
.context("failed to stat worktree path")?;
|
||||
|
||||
let closure_fs = Arc::clone(&fs);
|
||||
let closure_next_entry_id = Arc::clone(&next_entry_id);
|
||||
let closure_abs_path = abs_path.to_path_buf();
|
||||
Ok(cx.add_model(move |cx: &mut ModelContext<Worktree>| {
|
||||
let settings_subscription = cx.observe_global::<SettingsStore, _>(move |this, cx| {
|
||||
if let Self::Local(this) = this {
|
||||
let new_file_scan_exclusions =
|
||||
file_scan_exclusions(settings::get::<ProjectSettings>(cx));
|
||||
if new_file_scan_exclusions != this.snapshot.file_scan_exclusions {
|
||||
this.snapshot.file_scan_exclusions = new_file_scan_exclusions;
|
||||
log::info!(
|
||||
"Re-scanning directories, new scan exclude files: {:?}",
|
||||
this.snapshot
|
||||
.file_scan_exclusions
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
|
||||
let (scan_requests_tx, scan_requests_rx) = channel::unbounded();
|
||||
let (path_prefixes_to_scan_tx, path_prefixes_to_scan_rx) =
|
||||
channel::unbounded();
|
||||
this.scan_requests_tx = scan_requests_tx;
|
||||
this.path_prefixes_to_scan_tx = path_prefixes_to_scan_tx;
|
||||
this._background_scanner_tasks = start_background_scan_tasks(
|
||||
&closure_abs_path,
|
||||
this.snapshot(),
|
||||
scan_requests_rx,
|
||||
path_prefixes_to_scan_rx,
|
||||
Arc::clone(&closure_next_entry_id),
|
||||
Arc::clone(&closure_fs),
|
||||
cx,
|
||||
);
|
||||
this.is_scanning = watch::channel_with(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let root_name = abs_path
|
||||
.file_name()
|
||||
.map_or(String::new(), |f| f.to_string_lossy().to_string());
|
||||
|
||||
let mut snapshot = LocalSnapshot {
|
||||
file_scan_exclusions: file_scan_exclusions(settings::get::<ProjectSettings>(cx)),
|
||||
ignores_by_parent_abs_path: Default::default(),
|
||||
git_repositories: Default::default(),
|
||||
snapshot: Snapshot {
|
||||
id: WorktreeId::from_usize(cx.model_id()),
|
||||
abs_path: abs_path.clone(),
|
||||
abs_path: abs_path.to_path_buf().into(),
|
||||
root_name: root_name.clone(),
|
||||
root_char_bag: root_name.chars().map(|c| c.to_ascii_lowercase()).collect(),
|
||||
entries_by_path: Default::default(),
|
||||
@@ -334,60 +381,23 @@ impl Worktree {
|
||||
|
||||
let (scan_requests_tx, scan_requests_rx) = channel::unbounded();
|
||||
let (path_prefixes_to_scan_tx, path_prefixes_to_scan_rx) = channel::unbounded();
|
||||
let (scan_states_tx, mut scan_states_rx) = mpsc::unbounded();
|
||||
|
||||
cx.spawn_weak(|this, mut cx| async move {
|
||||
while let Some((state, this)) = scan_states_rx.next().await.zip(this.upgrade(&cx)) {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let this = this.as_local_mut().unwrap();
|
||||
match state {
|
||||
ScanState::Started => {
|
||||
*this.is_scanning.0.borrow_mut() = true;
|
||||
}
|
||||
ScanState::Updated {
|
||||
snapshot,
|
||||
changes,
|
||||
barrier,
|
||||
scanning,
|
||||
} => {
|
||||
*this.is_scanning.0.borrow_mut() = scanning;
|
||||
this.set_snapshot(snapshot, changes, cx);
|
||||
drop(barrier);
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
let background_scanner_task = cx.background().spawn({
|
||||
let fs = fs.clone();
|
||||
let snapshot = snapshot.clone();
|
||||
let background = cx.background().clone();
|
||||
async move {
|
||||
let events = fs.watch(&abs_path, Duration::from_millis(100)).await;
|
||||
BackgroundScanner::new(
|
||||
snapshot,
|
||||
next_entry_id,
|
||||
fs,
|
||||
scan_states_tx,
|
||||
background,
|
||||
scan_requests_rx,
|
||||
path_prefixes_to_scan_rx,
|
||||
)
|
||||
.run(events)
|
||||
.await;
|
||||
}
|
||||
});
|
||||
|
||||
let task_snapshot = snapshot.clone();
|
||||
Worktree::Local(LocalWorktree {
|
||||
snapshot,
|
||||
is_scanning: watch::channel_with(true),
|
||||
share: None,
|
||||
scan_requests_tx,
|
||||
path_prefixes_to_scan_tx,
|
||||
_background_scanner_task: background_scanner_task,
|
||||
_settings_subscription: settings_subscription,
|
||||
_background_scanner_tasks: start_background_scan_tasks(
|
||||
&abs_path,
|
||||
task_snapshot,
|
||||
scan_requests_rx,
|
||||
path_prefixes_to_scan_rx,
|
||||
Arc::clone(&next_entry_id),
|
||||
Arc::clone(&fs),
|
||||
cx,
|
||||
),
|
||||
diagnostics: Default::default(),
|
||||
diagnostic_summaries: Default::default(),
|
||||
client,
|
||||
@@ -584,6 +594,76 @@ impl Worktree {
|
||||
}
|
||||
}
|
||||
|
||||
fn start_background_scan_tasks(
|
||||
abs_path: &Path,
|
||||
snapshot: LocalSnapshot,
|
||||
scan_requests_rx: channel::Receiver<ScanRequest>,
|
||||
path_prefixes_to_scan_rx: channel::Receiver<Arc<Path>>,
|
||||
next_entry_id: Arc<AtomicUsize>,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &mut ModelContext<'_, Worktree>,
|
||||
) -> Vec<Task<()>> {
|
||||
let (scan_states_tx, mut scan_states_rx) = mpsc::unbounded();
|
||||
let background_scanner = cx.background().spawn({
|
||||
let abs_path = abs_path.to_path_buf();
|
||||
let background = cx.background().clone();
|
||||
async move {
|
||||
let events = fs.watch(&abs_path, Duration::from_millis(100)).await;
|
||||
BackgroundScanner::new(
|
||||
snapshot,
|
||||
next_entry_id,
|
||||
fs,
|
||||
scan_states_tx,
|
||||
background,
|
||||
scan_requests_rx,
|
||||
path_prefixes_to_scan_rx,
|
||||
)
|
||||
.run(events)
|
||||
.await;
|
||||
}
|
||||
});
|
||||
let scan_state_updater = cx.spawn_weak(|this, mut cx| async move {
|
||||
while let Some((state, this)) = scan_states_rx.next().await.zip(this.upgrade(&cx)) {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let this = this.as_local_mut().unwrap();
|
||||
match state {
|
||||
ScanState::Started => {
|
||||
*this.is_scanning.0.borrow_mut() = true;
|
||||
}
|
||||
ScanState::Updated {
|
||||
snapshot,
|
||||
changes,
|
||||
barrier,
|
||||
scanning,
|
||||
} => {
|
||||
*this.is_scanning.0.borrow_mut() = scanning;
|
||||
this.set_snapshot(snapshot, changes, cx);
|
||||
drop(barrier);
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
});
|
||||
vec![background_scanner, scan_state_updater]
|
||||
}
|
||||
|
||||
fn file_scan_exclusions(project_settings: &ProjectSettings) -> Vec<PathMatcher> {
|
||||
project_settings.file_scan_exclusions.as_deref().unwrap_or(&[]).iter()
|
||||
.sorted()
|
||||
.filter_map(|pattern| {
|
||||
PathMatcher::new(pattern)
|
||||
.map(Some)
|
||||
.unwrap_or_else(|e| {
|
||||
log::error!(
|
||||
"Skipping pattern {pattern} in `file_scan_exclusions` project settings due to parsing error: {e:#}"
|
||||
);
|
||||
None
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
impl LocalWorktree {
|
||||
pub fn contains_abs_path(&self, path: &Path) -> bool {
|
||||
path.starts_with(&self.abs_path)
|
||||
@@ -1481,7 +1561,7 @@ impl Snapshot {
|
||||
self.entries_by_id.get(&entry_id, &()).is_some()
|
||||
}
|
||||
|
||||
pub(crate) fn insert_entry(&mut self, entry: proto::Entry) -> Result<Entry> {
|
||||
fn insert_entry(&mut self, entry: proto::Entry) -> Result<Entry> {
|
||||
let entry = Entry::try_from((&self.root_char_bag, entry))?;
|
||||
let old_entry = self.entries_by_id.insert_or_replace(
|
||||
PathEntry {
|
||||
@@ -2145,6 +2225,12 @@ impl LocalSnapshot {
|
||||
paths.sort_by(|a, b| a.0.cmp(b.0));
|
||||
paths
|
||||
}
|
||||
|
||||
fn is_abs_path_excluded(&self, abs_path: &Path) -> bool {
|
||||
self.file_scan_exclusions
|
||||
.iter()
|
||||
.any(|exclude_matcher| exclude_matcher.is_match(abs_path))
|
||||
}
|
||||
}
|
||||
|
||||
impl BackgroundScannerState {
|
||||
@@ -2167,7 +2253,7 @@ impl BackgroundScannerState {
|
||||
let ignore_stack = self.snapshot.ignore_stack_for_abs_path(&abs_path, true);
|
||||
let mut ancestor_inodes = self.snapshot.ancestor_inodes_for_path(&path);
|
||||
let mut containing_repository = None;
|
||||
if !ignore_stack.is_all() {
|
||||
if !ignore_stack.is_abs_path_ignored(&abs_path, true) {
|
||||
if let Some((workdir_path, repo)) = self.snapshot.local_repo_for_path(&path) {
|
||||
if let Ok(repo_path) = path.strip_prefix(&workdir_path.0) {
|
||||
containing_repository = Some((
|
||||
@@ -2378,18 +2464,30 @@ impl BackgroundScannerState {
|
||||
|
||||
// Remove any git repositories whose .git entry no longer exists.
|
||||
let snapshot = &mut self.snapshot;
|
||||
let mut repositories = mem::take(&mut snapshot.git_repositories);
|
||||
let mut repository_entries = mem::take(&mut snapshot.repository_entries);
|
||||
repositories.retain(|work_directory_id, _| {
|
||||
snapshot
|
||||
.entry_for_id(*work_directory_id)
|
||||
let mut ids_to_preserve = HashSet::default();
|
||||
for (&work_directory_id, entry) in snapshot.git_repositories.iter() {
|
||||
let exists_in_snapshot = snapshot
|
||||
.entry_for_id(work_directory_id)
|
||||
.map_or(false, |entry| {
|
||||
snapshot.entry_for_path(entry.path.join(*DOT_GIT)).is_some()
|
||||
})
|
||||
});
|
||||
repository_entries.retain(|_, entry| repositories.get(&entry.work_directory.0).is_some());
|
||||
snapshot.git_repositories = repositories;
|
||||
snapshot.repository_entries = repository_entries;
|
||||
});
|
||||
if exists_in_snapshot {
|
||||
ids_to_preserve.insert(work_directory_id);
|
||||
} else {
|
||||
let git_dir_abs_path = snapshot.abs_path().join(&entry.git_dir_path);
|
||||
if snapshot.is_abs_path_excluded(&git_dir_abs_path)
|
||||
&& !matches!(smol::block_on(fs.metadata(&git_dir_abs_path)), Ok(None))
|
||||
{
|
||||
ids_to_preserve.insert(work_directory_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
snapshot
|
||||
.git_repositories
|
||||
.retain(|work_directory_id, _| ids_to_preserve.contains(work_directory_id));
|
||||
snapshot
|
||||
.repository_entries
|
||||
.retain(|_, entry| ids_to_preserve.contains(&entry.work_directory.0));
|
||||
}
|
||||
|
||||
fn build_git_repository(
|
||||
@@ -3094,7 +3192,7 @@ impl BackgroundScanner {
|
||||
let ignore_stack = state
|
||||
.snapshot
|
||||
.ignore_stack_for_abs_path(&root_abs_path, true);
|
||||
if ignore_stack.is_all() {
|
||||
if ignore_stack.is_abs_path_ignored(&root_abs_path, true) {
|
||||
root_entry.is_ignored = true;
|
||||
state.insert_entry(root_entry.clone(), self.fs.as_ref());
|
||||
}
|
||||
@@ -3231,14 +3329,22 @@ impl BackgroundScanner {
|
||||
return false;
|
||||
};
|
||||
|
||||
let parent_dir_is_loaded = relative_path.parent().map_or(true, |parent| {
|
||||
snapshot
|
||||
.entry_for_path(parent)
|
||||
.map_or(false, |entry| entry.kind == EntryKind::Dir)
|
||||
});
|
||||
if !parent_dir_is_loaded {
|
||||
log::debug!("ignoring event {relative_path:?} within unloaded directory");
|
||||
return false;
|
||||
if !is_git_related(&abs_path) {
|
||||
let parent_dir_is_loaded = relative_path.parent().map_or(true, |parent| {
|
||||
snapshot
|
||||
.entry_for_path(parent)
|
||||
.map_or(false, |entry| entry.kind == EntryKind::Dir)
|
||||
});
|
||||
if !parent_dir_is_loaded {
|
||||
log::debug!("ignoring event {relative_path:?} within unloaded directory");
|
||||
return false;
|
||||
}
|
||||
if snapshot.is_abs_path_excluded(abs_path) {
|
||||
log::debug!(
|
||||
"ignoring FS event for path {relative_path:?} within excluded directory"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
relative_paths.push(relative_path);
|
||||
@@ -3401,18 +3507,26 @@ impl BackgroundScanner {
|
||||
}
|
||||
|
||||
async fn scan_dir(&self, job: &ScanJob) -> Result<()> {
|
||||
log::debug!("scan directory {:?}", job.path);
|
||||
|
||||
let mut ignore_stack = job.ignore_stack.clone();
|
||||
let mut new_ignore = None;
|
||||
let (root_abs_path, root_char_bag, next_entry_id) = {
|
||||
let snapshot = &self.state.lock().snapshot;
|
||||
(
|
||||
snapshot.abs_path().clone(),
|
||||
snapshot.root_char_bag,
|
||||
self.next_entry_id.clone(),
|
||||
)
|
||||
};
|
||||
let root_abs_path;
|
||||
let mut ignore_stack;
|
||||
let mut new_ignore;
|
||||
let root_char_bag;
|
||||
let next_entry_id;
|
||||
{
|
||||
let state = self.state.lock();
|
||||
let snapshot = &state.snapshot;
|
||||
root_abs_path = snapshot.abs_path().clone();
|
||||
if snapshot.is_abs_path_excluded(&job.abs_path) {
|
||||
log::error!("skipping excluded directory {:?}", job.path);
|
||||
return Ok(());
|
||||
}
|
||||
log::debug!("scanning directory {:?}", job.path);
|
||||
ignore_stack = job.ignore_stack.clone();
|
||||
new_ignore = None;
|
||||
root_char_bag = snapshot.root_char_bag;
|
||||
next_entry_id = self.next_entry_id.clone();
|
||||
drop(state);
|
||||
}
|
||||
|
||||
let mut dotgit_path = None;
|
||||
let mut root_canonical_path = None;
|
||||
@@ -3427,18 +3541,8 @@ impl BackgroundScanner {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let child_name = child_abs_path.file_name().unwrap();
|
||||
let child_path: Arc<Path> = job.path.join(child_name).into();
|
||||
let child_metadata = match self.fs.metadata(&child_abs_path).await {
|
||||
Ok(Some(metadata)) => metadata,
|
||||
Ok(None) => continue,
|
||||
Err(err) => {
|
||||
log::error!("error processing {:?}: {:?}", child_abs_path, err);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// If we find a .gitignore, add it to the stack of ignores used to determine which paths are ignored
|
||||
if child_name == *GITIGNORE {
|
||||
match build_gitignore(&child_abs_path, self.fs.as_ref()).await {
|
||||
@@ -3482,6 +3586,26 @@ impl BackgroundScanner {
|
||||
dotgit_path = Some(child_path.clone());
|
||||
}
|
||||
|
||||
{
|
||||
let mut state = self.state.lock();
|
||||
if state.snapshot.is_abs_path_excluded(&child_abs_path) {
|
||||
let relative_path = job.path.join(child_name);
|
||||
log::debug!("skipping excluded child entry {relative_path:?}");
|
||||
state.remove_path(&relative_path);
|
||||
continue;
|
||||
}
|
||||
drop(state);
|
||||
}
|
||||
|
||||
let child_metadata = match self.fs.metadata(&child_abs_path).await {
|
||||
Ok(Some(metadata)) => metadata,
|
||||
Ok(None) => continue,
|
||||
Err(err) => {
|
||||
log::error!("error processing {child_abs_path:?}: {err:?}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let mut child_entry = Entry::new(
|
||||
child_path.clone(),
|
||||
&child_metadata,
|
||||
@@ -3662,19 +3786,16 @@ impl BackgroundScanner {
|
||||
self.next_entry_id.as_ref(),
|
||||
state.snapshot.root_char_bag,
|
||||
);
|
||||
fs_entry.is_ignored = ignore_stack.is_all();
|
||||
let is_dir = fs_entry.is_dir();
|
||||
fs_entry.is_ignored = ignore_stack.is_abs_path_ignored(&abs_path, is_dir);
|
||||
fs_entry.is_external = !canonical_path.starts_with(&root_canonical_path);
|
||||
|
||||
if !fs_entry.is_ignored {
|
||||
if !fs_entry.is_dir() {
|
||||
if let Some((work_dir, repo)) =
|
||||
state.snapshot.local_repo_for_path(&path)
|
||||
{
|
||||
if let Ok(repo_path) = path.strip_prefix(work_dir.0) {
|
||||
let repo_path = RepoPath(repo_path.into());
|
||||
let repo = repo.repo_ptr.lock();
|
||||
fs_entry.git_status = repo.status(&repo_path, fs_entry.mtime);
|
||||
}
|
||||
if !is_dir && !fs_entry.is_ignored {
|
||||
if let Some((work_dir, repo)) = state.snapshot.local_repo_for_path(&path) {
|
||||
if let Ok(repo_path) = path.strip_prefix(work_dir.0) {
|
||||
let repo_path = RepoPath(repo_path.into());
|
||||
let repo = repo.repo_ptr.lock();
|
||||
fs_entry.git_status = repo.status(&repo_path, fs_entry.mtime);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3833,8 +3954,7 @@ impl BackgroundScanner {
|
||||
ignore_stack.clone()
|
||||
};
|
||||
|
||||
// Scan any directories that were previously ignored and weren't
|
||||
// previously scanned.
|
||||
// Scan any directories that were previously ignored and weren't previously scanned.
|
||||
if was_ignored && !entry.is_ignored && entry.kind.is_unloaded() {
|
||||
let state = self.state.lock();
|
||||
if state.should_scan_directory(&entry) {
|
||||
@@ -4010,6 +4130,12 @@ impl BackgroundScanner {
|
||||
}
|
||||
}
|
||||
|
||||
fn is_git_related(abs_path: &Path) -> bool {
|
||||
abs_path
|
||||
.components()
|
||||
.any(|c| c.as_os_str() == *DOT_GIT || c.as_os_str() == *GITIGNORE)
|
||||
}
|
||||
|
||||
fn char_bag_for_path(root_char_bag: CharBag, path: &Path) -> CharBag {
|
||||
let mut result = root_char_bag;
|
||||
result.extend(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::{
|
||||
project_settings::ProjectSettings,
|
||||
worktree::{Event, Snapshot, WorktreeModelHandle},
|
||||
Entry, EntryKind, PathChange, Worktree,
|
||||
Entry, EntryKind, PathChange, Project, Worktree,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use client::Client;
|
||||
@@ -12,6 +13,7 @@ use postage::stream::Stream;
|
||||
use pretty_assertions::assert_eq;
|
||||
use rand::prelude::*;
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use std::{
|
||||
env,
|
||||
fmt::Write,
|
||||
@@ -23,6 +25,7 @@ use util::{http::FakeHttpClient, test::temp_tree, ResultExt};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_traversal(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.background());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
@@ -78,6 +81,7 @@ async fn test_traversal(cx: &mut TestAppContext) {
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_descendent_entries(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.background());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
@@ -185,6 +189,7 @@ async fn test_descendent_entries(cx: &mut TestAppContext) {
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_circular_symlinks(executor: Arc<Deterministic>, cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.background());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
@@ -264,6 +269,7 @@ async fn test_circular_symlinks(executor: Arc<Deterministic>, cx: &mut TestAppCo
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_symlinks_pointing_outside(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.background());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
@@ -439,6 +445,7 @@ async fn test_symlinks_pointing_outside(cx: &mut TestAppContext) {
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_open_gitignored_files(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.background());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
@@ -599,6 +606,7 @@ async fn test_open_gitignored_files(cx: &mut TestAppContext) {
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_dirs_no_longer_ignored(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.background());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
@@ -722,6 +730,14 @@ async fn test_dirs_no_longer_ignored(cx: &mut TestAppContext) {
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_rescan_with_gitignore(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
cx.update(|cx| {
|
||||
cx.update_global::<SettingsStore, _, _>(|store, cx| {
|
||||
store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
|
||||
project_settings.file_scan_exclusions = Some(Vec::new());
|
||||
});
|
||||
});
|
||||
});
|
||||
let fs = FakeFs::new(cx.background());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
@@ -827,6 +843,7 @@ async fn test_rescan_with_gitignore(cx: &mut TestAppContext) {
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_write_file(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let dir = temp_tree(json!({
|
||||
".git": {},
|
||||
".gitignore": "ignored-dir\n",
|
||||
@@ -877,8 +894,105 @@ async fn test_write_file(cx: &mut TestAppContext) {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_file_scan_exclusions(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let dir = temp_tree(json!({
|
||||
".gitignore": "**/target\n/node_modules\n",
|
||||
"target": {
|
||||
"index": "blah2"
|
||||
},
|
||||
"node_modules": {
|
||||
".DS_Store": "",
|
||||
"prettier": {
|
||||
"package.json": "{}",
|
||||
},
|
||||
},
|
||||
"src": {
|
||||
".DS_Store": "",
|
||||
"foo": {
|
||||
"foo.rs": "mod another;\n",
|
||||
"another.rs": "// another",
|
||||
},
|
||||
"bar": {
|
||||
"bar.rs": "// bar",
|
||||
},
|
||||
"lib.rs": "mod foo;\nmod bar;\n",
|
||||
},
|
||||
".DS_Store": "",
|
||||
}));
|
||||
cx.update(|cx| {
|
||||
cx.update_global::<SettingsStore, _, _>(|store, cx| {
|
||||
store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
|
||||
project_settings.file_scan_exclusions =
|
||||
Some(vec!["**/foo/**".to_string(), "**/.DS_Store".to_string()]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
let tree = Worktree::local(
|
||||
build_client(cx),
|
||||
dir.path(),
|
||||
true,
|
||||
Arc::new(RealFs),
|
||||
Default::default(),
|
||||
&mut cx.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
tree.flush_fs_events(cx).await;
|
||||
tree.read_with(cx, |tree, _| {
|
||||
check_worktree_entries(
|
||||
tree,
|
||||
&[
|
||||
"src/foo/foo.rs",
|
||||
"src/foo/another.rs",
|
||||
"node_modules/.DS_Store",
|
||||
"src/.DS_Store",
|
||||
".DS_Store",
|
||||
],
|
||||
&["target", "node_modules"],
|
||||
&["src/lib.rs", "src/bar/bar.rs", ".gitignore"],
|
||||
)
|
||||
});
|
||||
|
||||
cx.update(|cx| {
|
||||
cx.update_global::<SettingsStore, _, _>(|store, cx| {
|
||||
store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
|
||||
project_settings.file_scan_exclusions =
|
||||
Some(vec!["**/node_modules/**".to_string()]);
|
||||
});
|
||||
});
|
||||
});
|
||||
tree.flush_fs_events(cx).await;
|
||||
cx.foreground().run_until_parked();
|
||||
tree.read_with(cx, |tree, _| {
|
||||
check_worktree_entries(
|
||||
tree,
|
||||
&[
|
||||
"node_modules/prettier/package.json",
|
||||
"node_modules/.DS_Store",
|
||||
"node_modules",
|
||||
],
|
||||
&["target"],
|
||||
&[
|
||||
".gitignore",
|
||||
"src/lib.rs",
|
||||
"src/bar/bar.rs",
|
||||
"src/foo/foo.rs",
|
||||
"src/foo/another.rs",
|
||||
"src/.DS_Store",
|
||||
".DS_Store",
|
||||
],
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 30)]
|
||||
async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.background());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
@@ -938,6 +1052,7 @@ async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let client_fake = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||
|
||||
let fs_fake = FakeFs::new(cx.background());
|
||||
@@ -1054,6 +1169,7 @@ async fn test_random_worktree_operations_during_initial_scan(
|
||||
cx: &mut TestAppContext,
|
||||
mut rng: StdRng,
|
||||
) {
|
||||
init_test(cx);
|
||||
let operations = env::var("OPERATIONS")
|
||||
.map(|o| o.parse().unwrap())
|
||||
.unwrap_or(5);
|
||||
@@ -1143,6 +1259,7 @@ async fn test_random_worktree_operations_during_initial_scan(
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
async fn test_random_worktree_changes(cx: &mut TestAppContext, mut rng: StdRng) {
|
||||
init_test(cx);
|
||||
let operations = env::var("OPERATIONS")
|
||||
.map(|o| o.parse().unwrap())
|
||||
.unwrap_or(40);
|
||||
@@ -1557,6 +1674,7 @@ fn random_filename(rng: &mut impl Rng) -> String {
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_rename_work_directory(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let root = temp_tree(json!({
|
||||
"projects": {
|
||||
"project1": {
|
||||
@@ -1627,6 +1745,7 @@ async fn test_rename_work_directory(cx: &mut TestAppContext) {
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_git_repository_for_path(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let root = temp_tree(json!({
|
||||
"c.txt": "",
|
||||
"dir1": {
|
||||
@@ -1747,6 +1866,15 @@ async fn test_git_repository_for_path(cx: &mut TestAppContext) {
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_git_status(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
cx.update(|cx| {
|
||||
cx.update_global::<SettingsStore, _, _>(|store, cx| {
|
||||
store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
|
||||
project_settings.file_scan_exclusions =
|
||||
Some(vec!["**/.git".to_string(), "**/.gitignore".to_string()]);
|
||||
});
|
||||
});
|
||||
});
|
||||
const IGNORE_RULE: &'static str = "**/target";
|
||||
|
||||
let root = temp_tree(json!({
|
||||
@@ -1935,6 +2063,7 @@ async fn test_git_status(deterministic: Arc<Deterministic>, cx: &mut TestAppCont
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.background());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
@@ -2139,3 +2268,44 @@ fn git_status(repo: &git2::Repository) -> collections::HashMap<String, git2::Sta
|
||||
.map(|status| (status.path().unwrap().to_string(), status.status()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn check_worktree_entries(
|
||||
tree: &Worktree,
|
||||
expected_excluded_paths: &[&str],
|
||||
expected_ignored_paths: &[&str],
|
||||
expected_tracked_paths: &[&str],
|
||||
) {
|
||||
for path in expected_excluded_paths {
|
||||
let entry = tree.entry_for_path(path);
|
||||
assert!(
|
||||
entry.is_none(),
|
||||
"expected path '{path}' to be excluded, but got entry: {entry:?}",
|
||||
);
|
||||
}
|
||||
for path in expected_ignored_paths {
|
||||
let entry = tree
|
||||
.entry_for_path(path)
|
||||
.unwrap_or_else(|| panic!("Missing entry for expected ignored path '{path}'"));
|
||||
assert!(
|
||||
entry.is_ignored,
|
||||
"expected path '{path}' to be ignored, but got entry: {entry:?}",
|
||||
);
|
||||
}
|
||||
for path in expected_tracked_paths {
|
||||
let entry = tree
|
||||
.entry_for_path(path)
|
||||
.unwrap_or_else(|| panic!("Missing entry for expected tracked path '{path}'"));
|
||||
assert!(
|
||||
!entry.is_ignored,
|
||||
"expected path '{path}' to be tracked, but got entry: {entry:?}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut gpui::TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
cx.set_global(SettingsStore::test(cx));
|
||||
Project::init_settings(cx);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -20,10 +20,6 @@ impl IgnoreStack {
|
||||
Arc::new(Self::All)
|
||||
}
|
||||
|
||||
pub fn is_all(&self) -> bool {
|
||||
matches!(self, IgnoreStack::All)
|
||||
}
|
||||
|
||||
pub fn append(self: Arc<Self>, abs_base_path: Arc<Path>, ignore: Arc<Gitignore>) -> Arc<Self> {
|
||||
match self.as_ref() {
|
||||
IgnoreStack::All => self,
|
||||
|
||||
@@ -5618,7 +5618,16 @@ impl Project {
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let background = cx.background_executor().clone();
|
||||
let path_count: usize = snapshots.iter().map(|s| s.visible_file_count()).sum();
|
||||
let path_count: usize = snapshots
|
||||
.iter()
|
||||
.map(|s| {
|
||||
if query.include_ignored() {
|
||||
s.file_count()
|
||||
} else {
|
||||
s.visible_file_count()
|
||||
}
|
||||
})
|
||||
.sum();
|
||||
if path_count == 0 {
|
||||
let (_, rx) = smol::channel::bounded(1024);
|
||||
return rx;
|
||||
@@ -5631,8 +5640,16 @@ impl Project {
|
||||
.iter()
|
||||
.filter_map(|(_, b)| {
|
||||
let buffer = b.upgrade()?;
|
||||
let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
|
||||
if let Some(path) = snapshot.file().map(|file| file.path()) {
|
||||
let (is_ignored, snapshot) = buffer.update(cx, |buffer, cx| {
|
||||
let is_ignored = buffer
|
||||
.project_path(cx)
|
||||
.and_then(|path| self.entry_for_path(&path, cx))
|
||||
.map_or(false, |entry| entry.is_ignored);
|
||||
(is_ignored, buffer.snapshot())
|
||||
});
|
||||
if is_ignored && !query.include_ignored() {
|
||||
return None;
|
||||
} else if let Some(path) = snapshot.file().map(|file| file.path()) {
|
||||
Some((path.clone(), (buffer, snapshot)))
|
||||
} else {
|
||||
unnamed_files.push(buffer);
|
||||
@@ -5806,7 +5823,12 @@ impl Project {
|
||||
let mut snapshot_start_ix = 0;
|
||||
let mut abs_path = PathBuf::new();
|
||||
for snapshot in snapshots {
|
||||
let snapshot_end_ix = snapshot_start_ix + snapshot.visible_file_count();
|
||||
let snapshot_end_ix = snapshot_start_ix
|
||||
+ if query.include_ignored() {
|
||||
snapshot.file_count()
|
||||
} else {
|
||||
snapshot.visible_file_count()
|
||||
};
|
||||
if worker_end_ix <= snapshot_start_ix {
|
||||
break;
|
||||
} else if worker_start_ix > snapshot_end_ix {
|
||||
@@ -5819,7 +5841,7 @@ impl Project {
|
||||
cmp::min(worker_end_ix, snapshot_end_ix) - snapshot_start_ix;
|
||||
|
||||
for entry in snapshot
|
||||
.files(false, start_in_snapshot)
|
||||
.files(query.include_ignored(), start_in_snapshot)
|
||||
.take(end_in_snapshot - start_in_snapshot)
|
||||
{
|
||||
if matching_paths_tx.is_closed() {
|
||||
|
||||
@@ -11,6 +11,8 @@ pub struct ProjectSettings {
|
||||
pub lsp: HashMap<Arc<str>, LspSettings>,
|
||||
#[serde(default)]
|
||||
pub git: GitSettings,
|
||||
#[serde(default)]
|
||||
pub file_scan_exclusions: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
|
||||
@@ -2633,6 +2633,60 @@ async fn test_file_changes_multiple_times_on_disk(cx: &mut gpui::TestAppContext)
|
||||
.unwrap();
|
||||
worktree.next_event(cx);
|
||||
|
||||
cx.executor().run_until_parked();
|
||||
let on_disk_text = fs.load(Path::new("/dir/file1")).await.unwrap();
|
||||
buffer.read_with(cx, |buffer, _| {
|
||||
assert_eq!(buffer.text(), on_disk_text);
|
||||
assert!(!buffer.is_dirty(), "buffer should not be dirty");
|
||||
assert!(!buffer.has_conflict(), "buffer should not be dirty");
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 30)]
|
||||
async fn test_edit_buffer_while_it_reloads(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor().clone());
|
||||
fs.insert_tree(
|
||||
"/dir",
|
||||
json!({
|
||||
"file1": "the original contents",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
|
||||
let worktree = project.read_with(cx, |project, _| project.worktrees().next().unwrap());
|
||||
let buffer = project
|
||||
.update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Simulate buffer diffs being slow, so that they don't complete before
|
||||
// the next file change occurs.
|
||||
cx.executor().deprioritize(*language::BUFFER_DIFF_TASK);
|
||||
|
||||
// Change the buffer's file on disk, and then wait for the file change
|
||||
// to be detected by the worktree, so that the buffer starts reloading.
|
||||
fs.save(
|
||||
"/dir/file1".as_ref(),
|
||||
&"the first contents".into(),
|
||||
Default::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
worktree.next_event(cx);
|
||||
|
||||
cx.executor()
|
||||
.spawn(cx.executor().simulate_random_delay())
|
||||
.await;
|
||||
|
||||
// Perform a noop edit, causing the buffer's version to increase.
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit([(0..0, " ")], None, cx);
|
||||
buffer.undo(cx);
|
||||
});
|
||||
|
||||
cx.executor().run_until_parked();
|
||||
let on_disk_text = fs.load(Path::new("/dir/file1")).await.unwrap();
|
||||
buffer.read_with(cx, |buffer, _| {
|
||||
@@ -2646,10 +2700,8 @@ async fn test_file_changes_multiple_times_on_disk(cx: &mut gpui::TestAppContext)
|
||||
// If the file change occurred while the buffer was processing the first
|
||||
// change, the buffer will be in a conflicting state.
|
||||
else {
|
||||
assert!(
|
||||
buffer.is_dirty() && buffer.has_conflict(),
|
||||
"buffer should report that it has a conflict. text: {buffer_text:?}, disk text: {on_disk_text:?}"
|
||||
);
|
||||
assert!(buffer.is_dirty(), "buffer should report that it is dirty. text: {buffer_text:?}, disk text: {on_disk_text:?}");
|
||||
assert!(buffer.has_conflict(), "buffer should report that it is dirty. text: {buffer_text:?}, disk text: {on_disk_text:?}");
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -3678,7 +3730,7 @@ async fn test_search(cx: &mut gpui::TestAppContext) {
|
||||
assert_eq!(
|
||||
search(
|
||||
&project,
|
||||
SearchQuery::text("TWO", false, true, Vec::new(), Vec::new()).unwrap(),
|
||||
SearchQuery::text("TWO", false, true, false, Vec::new(), Vec::new()).unwrap(),
|
||||
cx
|
||||
)
|
||||
.await
|
||||
@@ -3703,7 +3755,7 @@ async fn test_search(cx: &mut gpui::TestAppContext) {
|
||||
assert_eq!(
|
||||
search(
|
||||
&project,
|
||||
SearchQuery::text("TWO", false, true, Vec::new(), Vec::new()).unwrap(),
|
||||
SearchQuery::text("TWO", false, true, false, Vec::new(), Vec::new()).unwrap(),
|
||||
cx
|
||||
)
|
||||
.await
|
||||
@@ -3742,6 +3794,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
|
||||
search_query,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
vec![PathMatcher::new("*.odd").unwrap()],
|
||||
Vec::new()
|
||||
)
|
||||
@@ -3761,6 +3814,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
|
||||
search_query,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
vec![PathMatcher::new("*.rs").unwrap()],
|
||||
Vec::new()
|
||||
)
|
||||
@@ -3783,6 +3837,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
|
||||
search_query,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
vec![
|
||||
PathMatcher::new("*.ts").unwrap(),
|
||||
PathMatcher::new("*.odd").unwrap(),
|
||||
@@ -3807,6 +3862,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
|
||||
search_query,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
vec![
|
||||
PathMatcher::new("*.rs").unwrap(),
|
||||
PathMatcher::new("*.ts").unwrap(),
|
||||
@@ -3854,6 +3910,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
|
||||
search_query,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
Vec::new(),
|
||||
vec![PathMatcher::new("*.odd").unwrap()],
|
||||
)
|
||||
@@ -3878,6 +3935,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
|
||||
search_query,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
Vec::new(),
|
||||
vec![PathMatcher::new("*.rs").unwrap()],
|
||||
)
|
||||
@@ -3900,6 +3958,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
|
||||
search_query,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
Vec::new(),
|
||||
vec![
|
||||
PathMatcher::new("*.ts").unwrap(),
|
||||
@@ -3924,6 +3983,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
|
||||
search_query,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
Vec::new(),
|
||||
vec![
|
||||
PathMatcher::new("*.rs").unwrap(),
|
||||
@@ -3965,6 +4025,7 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
|
||||
search_query,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
vec![PathMatcher::new("*.odd").unwrap()],
|
||||
vec![PathMatcher::new("*.odd").unwrap()],
|
||||
)
|
||||
@@ -3984,6 +4045,7 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
|
||||
search_query,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
vec![PathMatcher::new("*.ts").unwrap()],
|
||||
vec![PathMatcher::new("*.ts").unwrap()],
|
||||
).unwrap(),
|
||||
@@ -4002,6 +4064,7 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
|
||||
search_query,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
vec![
|
||||
PathMatcher::new("*.ts").unwrap(),
|
||||
PathMatcher::new("*.odd").unwrap()
|
||||
@@ -4027,6 +4090,7 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
|
||||
search_query,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
vec![
|
||||
PathMatcher::new("*.ts").unwrap(),
|
||||
PathMatcher::new("*.odd").unwrap()
|
||||
@@ -4084,7 +4148,7 @@ async fn search(
|
||||
|
||||
fn init_test(cx: &mut gpui::TestAppContext) {
|
||||
if std::env::var("RUST_LOG").is_ok() {
|
||||
env_logger::init();
|
||||
env_logger::try_init().ok();
|
||||
}
|
||||
|
||||
cx.update(|cx| {
|
||||
|
||||
@@ -39,6 +39,7 @@ pub enum SearchQuery {
|
||||
replacement: Option<String>,
|
||||
whole_word: bool,
|
||||
case_sensitive: bool,
|
||||
include_ignored: bool,
|
||||
inner: SearchInputs,
|
||||
},
|
||||
|
||||
@@ -48,6 +49,7 @@ pub enum SearchQuery {
|
||||
multiline: bool,
|
||||
whole_word: bool,
|
||||
case_sensitive: bool,
|
||||
include_ignored: bool,
|
||||
inner: SearchInputs,
|
||||
},
|
||||
}
|
||||
@@ -57,6 +59,7 @@ impl SearchQuery {
|
||||
query: impl ToString,
|
||||
whole_word: bool,
|
||||
case_sensitive: bool,
|
||||
include_ignored: bool,
|
||||
files_to_include: Vec<PathMatcher>,
|
||||
files_to_exclude: Vec<PathMatcher>,
|
||||
) -> Result<Self> {
|
||||
@@ -74,6 +77,7 @@ impl SearchQuery {
|
||||
replacement: None,
|
||||
whole_word,
|
||||
case_sensitive,
|
||||
include_ignored,
|
||||
inner,
|
||||
})
|
||||
}
|
||||
@@ -82,6 +86,7 @@ impl SearchQuery {
|
||||
query: impl ToString,
|
||||
whole_word: bool,
|
||||
case_sensitive: bool,
|
||||
include_ignored: bool,
|
||||
files_to_include: Vec<PathMatcher>,
|
||||
files_to_exclude: Vec<PathMatcher>,
|
||||
) -> Result<Self> {
|
||||
@@ -111,6 +116,7 @@ impl SearchQuery {
|
||||
multiline,
|
||||
whole_word,
|
||||
case_sensitive,
|
||||
include_ignored,
|
||||
inner,
|
||||
})
|
||||
}
|
||||
@@ -121,6 +127,7 @@ impl SearchQuery {
|
||||
message.query,
|
||||
message.whole_word,
|
||||
message.case_sensitive,
|
||||
message.include_ignored,
|
||||
deserialize_path_matches(&message.files_to_include)?,
|
||||
deserialize_path_matches(&message.files_to_exclude)?,
|
||||
)
|
||||
@@ -129,6 +136,7 @@ impl SearchQuery {
|
||||
message.query,
|
||||
message.whole_word,
|
||||
message.case_sensitive,
|
||||
message.include_ignored,
|
||||
deserialize_path_matches(&message.files_to_include)?,
|
||||
deserialize_path_matches(&message.files_to_exclude)?,
|
||||
)
|
||||
@@ -156,6 +164,7 @@ impl SearchQuery {
|
||||
regex: self.is_regex(),
|
||||
whole_word: self.whole_word(),
|
||||
case_sensitive: self.case_sensitive(),
|
||||
include_ignored: self.include_ignored(),
|
||||
files_to_include: self
|
||||
.files_to_include()
|
||||
.iter()
|
||||
@@ -336,6 +345,17 @@ impl SearchQuery {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn include_ignored(&self) -> bool {
|
||||
match self {
|
||||
Self::Text {
|
||||
include_ignored, ..
|
||||
} => *include_ignored,
|
||||
Self::Regex {
|
||||
include_ignored, ..
|
||||
} => *include_ignored,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_regex(&self) -> bool {
|
||||
matches!(self, Self::Regex { .. })
|
||||
}
|
||||
|
||||
+233
-109
@@ -1,5 +1,6 @@
|
||||
use crate::{
|
||||
copy_recursive, ignore::IgnoreStack, DiagnosticSummary, ProjectEntryId, RemoveOptions,
|
||||
copy_recursive, ignore::IgnoreStack, project_settings::ProjectSettings, DiagnosticSummary,
|
||||
ProjectEntryId, RemoveOptions,
|
||||
};
|
||||
use ::ignore::gitignore::{Gitignore, GitignoreBuilder};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
@@ -25,6 +26,7 @@ use gpui::{
|
||||
AppContext, AsyncAppContext, BackgroundExecutor, Context, EventEmitter, Model, ModelContext,
|
||||
Task,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use language::{
|
||||
proto::{
|
||||
deserialize_fingerprint, deserialize_version, serialize_fingerprint, serialize_line_ending,
|
||||
@@ -39,6 +41,7 @@ use postage::{
|
||||
prelude::{Sink as _, Stream as _},
|
||||
watch,
|
||||
};
|
||||
use settings::{Settings, SettingsStore};
|
||||
use smol::channel::{self, Sender};
|
||||
use std::{
|
||||
any::Any,
|
||||
@@ -58,7 +61,10 @@ use std::{
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet};
|
||||
use util::{paths::HOME, ResultExt};
|
||||
use util::{
|
||||
paths::{PathMatcher, HOME},
|
||||
ResultExt,
|
||||
};
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)]
|
||||
pub struct WorktreeId(usize);
|
||||
@@ -73,7 +79,7 @@ pub struct LocalWorktree {
|
||||
scan_requests_tx: channel::Sender<ScanRequest>,
|
||||
path_prefixes_to_scan_tx: channel::Sender<Arc<Path>>,
|
||||
is_scanning: (watch::Sender<bool>, watch::Receiver<bool>),
|
||||
_background_scanner_task: Task<()>,
|
||||
_background_scanner_tasks: Vec<Task<()>>,
|
||||
share: Option<ShareState>,
|
||||
diagnostics: HashMap<
|
||||
Arc<Path>,
|
||||
@@ -219,6 +225,7 @@ pub struct LocalSnapshot {
|
||||
/// All of the git repositories in the worktree, indexed by the project entry
|
||||
/// id of their parent directory.
|
||||
git_repositories: TreeMap<ProjectEntryId, LocalRepositoryEntry>,
|
||||
file_scan_exclusions: Vec<PathMatcher>,
|
||||
}
|
||||
|
||||
struct BackgroundScannerState {
|
||||
@@ -302,17 +309,56 @@ impl Worktree {
|
||||
.await
|
||||
.context("failed to stat worktree path")?;
|
||||
|
||||
let closure_fs = Arc::clone(&fs);
|
||||
let closure_next_entry_id = Arc::clone(&next_entry_id);
|
||||
let closure_abs_path = abs_path.to_path_buf();
|
||||
cx.build_model(move |cx: &mut ModelContext<Worktree>| {
|
||||
cx.observe_global::<SettingsStore>(move |this, cx| {
|
||||
if let Self::Local(this) = this {
|
||||
let new_file_scan_exclusions =
|
||||
file_scan_exclusions(ProjectSettings::get_global(cx));
|
||||
if new_file_scan_exclusions != this.snapshot.file_scan_exclusions {
|
||||
this.snapshot.file_scan_exclusions = new_file_scan_exclusions;
|
||||
log::info!(
|
||||
"Re-scanning directories, new scan exclude files: {:?}",
|
||||
this.snapshot
|
||||
.file_scan_exclusions
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
|
||||
let (scan_requests_tx, scan_requests_rx) = channel::unbounded();
|
||||
let (path_prefixes_to_scan_tx, path_prefixes_to_scan_rx) =
|
||||
channel::unbounded();
|
||||
this.scan_requests_tx = scan_requests_tx;
|
||||
this.path_prefixes_to_scan_tx = path_prefixes_to_scan_tx;
|
||||
this._background_scanner_tasks = start_background_scan_tasks(
|
||||
&closure_abs_path,
|
||||
this.snapshot(),
|
||||
scan_requests_rx,
|
||||
path_prefixes_to_scan_rx,
|
||||
Arc::clone(&closure_next_entry_id),
|
||||
Arc::clone(&closure_fs),
|
||||
cx,
|
||||
);
|
||||
this.is_scanning = watch::channel_with(true);
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
let root_name = abs_path
|
||||
.file_name()
|
||||
.map_or(String::new(), |f| f.to_string_lossy().to_string());
|
||||
|
||||
let mut snapshot = LocalSnapshot {
|
||||
file_scan_exclusions: file_scan_exclusions(ProjectSettings::get_global(cx)),
|
||||
ignores_by_parent_abs_path: Default::default(),
|
||||
git_repositories: Default::default(),
|
||||
snapshot: Snapshot {
|
||||
id: WorktreeId::from_usize(cx.entity_id().as_u64() as usize),
|
||||
abs_path: abs_path.clone(),
|
||||
abs_path: abs_path.to_path_buf().into(),
|
||||
root_name: root_name.clone(),
|
||||
root_char_bag: root_name.chars().map(|c| c.to_ascii_lowercase()).collect(),
|
||||
entries_by_path: Default::default(),
|
||||
@@ -337,61 +383,22 @@ impl Worktree {
|
||||
|
||||
let (scan_requests_tx, scan_requests_rx) = channel::unbounded();
|
||||
let (path_prefixes_to_scan_tx, path_prefixes_to_scan_rx) = channel::unbounded();
|
||||
let (scan_states_tx, mut scan_states_rx) = mpsc::unbounded();
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
while let Some((state, this)) = scan_states_rx.next().await.zip(this.upgrade()) {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let this = this.as_local_mut().unwrap();
|
||||
match state {
|
||||
ScanState::Started => {
|
||||
*this.is_scanning.0.borrow_mut() = true;
|
||||
}
|
||||
ScanState::Updated {
|
||||
snapshot,
|
||||
changes,
|
||||
barrier,
|
||||
scanning,
|
||||
} => {
|
||||
*this.is_scanning.0.borrow_mut() = scanning;
|
||||
this.set_snapshot(snapshot, changes, cx);
|
||||
drop(barrier);
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
let background_scanner_task = cx.background_executor().spawn({
|
||||
let fs = fs.clone();
|
||||
let snapshot = snapshot.clone();
|
||||
let background = cx.background_executor().clone();
|
||||
async move {
|
||||
let events = fs.watch(&abs_path, Duration::from_millis(100)).await;
|
||||
BackgroundScanner::new(
|
||||
snapshot,
|
||||
next_entry_id,
|
||||
fs,
|
||||
scan_states_tx,
|
||||
background,
|
||||
scan_requests_rx,
|
||||
path_prefixes_to_scan_rx,
|
||||
)
|
||||
.run(events)
|
||||
.await;
|
||||
}
|
||||
});
|
||||
|
||||
let task_snapshot = snapshot.clone();
|
||||
Worktree::Local(LocalWorktree {
|
||||
snapshot,
|
||||
is_scanning: watch::channel_with(true),
|
||||
share: None,
|
||||
scan_requests_tx,
|
||||
path_prefixes_to_scan_tx,
|
||||
_background_scanner_task: background_scanner_task,
|
||||
_background_scanner_tasks: start_background_scan_tasks(
|
||||
&abs_path,
|
||||
task_snapshot,
|
||||
scan_requests_rx,
|
||||
path_prefixes_to_scan_rx,
|
||||
Arc::clone(&next_entry_id),
|
||||
Arc::clone(&fs),
|
||||
cx,
|
||||
),
|
||||
diagnostics: Default::default(),
|
||||
diagnostic_summaries: Default::default(),
|
||||
client,
|
||||
@@ -584,6 +591,77 @@ impl Worktree {
|
||||
}
|
||||
}
|
||||
|
||||
fn start_background_scan_tasks(
|
||||
abs_path: &Path,
|
||||
snapshot: LocalSnapshot,
|
||||
scan_requests_rx: channel::Receiver<ScanRequest>,
|
||||
path_prefixes_to_scan_rx: channel::Receiver<Arc<Path>>,
|
||||
next_entry_id: Arc<AtomicUsize>,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &mut ModelContext<'_, Worktree>,
|
||||
) -> Vec<Task<()>> {
|
||||
let (scan_states_tx, mut scan_states_rx) = mpsc::unbounded();
|
||||
let background_scanner = cx.background_executor().spawn({
|
||||
let abs_path = abs_path.to_path_buf();
|
||||
let background = cx.background_executor().clone();
|
||||
async move {
|
||||
let events = fs.watch(&abs_path, Duration::from_millis(100)).await;
|
||||
BackgroundScanner::new(
|
||||
snapshot,
|
||||
next_entry_id,
|
||||
fs,
|
||||
scan_states_tx,
|
||||
background,
|
||||
scan_requests_rx,
|
||||
path_prefixes_to_scan_rx,
|
||||
)
|
||||
.run(events)
|
||||
.await;
|
||||
}
|
||||
});
|
||||
let scan_state_updater = cx.spawn(|this, mut cx| async move {
|
||||
while let Some((state, this)) = scan_states_rx.next().await.zip(this.upgrade()) {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let this = this.as_local_mut().unwrap();
|
||||
match state {
|
||||
ScanState::Started => {
|
||||
*this.is_scanning.0.borrow_mut() = true;
|
||||
}
|
||||
ScanState::Updated {
|
||||
snapshot,
|
||||
changes,
|
||||
barrier,
|
||||
scanning,
|
||||
} => {
|
||||
*this.is_scanning.0.borrow_mut() = scanning;
|
||||
this.set_snapshot(snapshot, changes, cx);
|
||||
drop(barrier);
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
});
|
||||
vec![background_scanner, scan_state_updater]
|
||||
}
|
||||
|
||||
fn file_scan_exclusions(project_settings: &ProjectSettings) -> Vec<PathMatcher> {
|
||||
project_settings.file_scan_exclusions.as_deref().unwrap_or(&[]).iter()
|
||||
.sorted()
|
||||
.filter_map(|pattern| {
|
||||
PathMatcher::new(pattern)
|
||||
.map(Some)
|
||||
.unwrap_or_else(|e| {
|
||||
log::error!(
|
||||
"Skipping pattern {pattern} in `file_scan_exclusions` project settings due to parsing error: {e:#}"
|
||||
);
|
||||
None
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
impl LocalWorktree {
|
||||
pub fn contains_abs_path(&self, path: &Path) -> bool {
|
||||
path.starts_with(&self.abs_path)
|
||||
@@ -1482,7 +1560,7 @@ impl Snapshot {
|
||||
self.entries_by_id.get(&entry_id, &()).is_some()
|
||||
}
|
||||
|
||||
pub(crate) fn insert_entry(&mut self, entry: proto::Entry) -> Result<Entry> {
|
||||
fn insert_entry(&mut self, entry: proto::Entry) -> Result<Entry> {
|
||||
let entry = Entry::try_from((&self.root_char_bag, entry))?;
|
||||
let old_entry = self.entries_by_id.insert_or_replace(
|
||||
PathEntry {
|
||||
@@ -2143,6 +2221,12 @@ impl LocalSnapshot {
|
||||
paths.sort_by(|a, b| a.0.cmp(b.0));
|
||||
paths
|
||||
}
|
||||
|
||||
fn is_abs_path_excluded(&self, abs_path: &Path) -> bool {
|
||||
self.file_scan_exclusions
|
||||
.iter()
|
||||
.any(|exclude_matcher| exclude_matcher.is_match(abs_path))
|
||||
}
|
||||
}
|
||||
|
||||
impl BackgroundScannerState {
|
||||
@@ -2165,7 +2249,7 @@ impl BackgroundScannerState {
|
||||
let ignore_stack = self.snapshot.ignore_stack_for_abs_path(&abs_path, true);
|
||||
let mut ancestor_inodes = self.snapshot.ancestor_inodes_for_path(&path);
|
||||
let mut containing_repository = None;
|
||||
if !ignore_stack.is_all() {
|
||||
if !ignore_stack.is_abs_path_ignored(&abs_path, true) {
|
||||
if let Some((workdir_path, repo)) = self.snapshot.local_repo_for_path(&path) {
|
||||
if let Ok(repo_path) = path.strip_prefix(&workdir_path.0) {
|
||||
containing_repository = Some((
|
||||
@@ -2376,18 +2460,30 @@ impl BackgroundScannerState {
|
||||
|
||||
// Remove any git repositories whose .git entry no longer exists.
|
||||
let snapshot = &mut self.snapshot;
|
||||
let mut repositories = mem::take(&mut snapshot.git_repositories);
|
||||
let mut repository_entries = mem::take(&mut snapshot.repository_entries);
|
||||
repositories.retain(|work_directory_id, _| {
|
||||
snapshot
|
||||
.entry_for_id(*work_directory_id)
|
||||
let mut ids_to_preserve = HashSet::default();
|
||||
for (&work_directory_id, entry) in snapshot.git_repositories.iter() {
|
||||
let exists_in_snapshot = snapshot
|
||||
.entry_for_id(work_directory_id)
|
||||
.map_or(false, |entry| {
|
||||
snapshot.entry_for_path(entry.path.join(*DOT_GIT)).is_some()
|
||||
})
|
||||
});
|
||||
repository_entries.retain(|_, entry| repositories.get(&entry.work_directory.0).is_some());
|
||||
snapshot.git_repositories = repositories;
|
||||
snapshot.repository_entries = repository_entries;
|
||||
});
|
||||
if exists_in_snapshot {
|
||||
ids_to_preserve.insert(work_directory_id);
|
||||
} else {
|
||||
let git_dir_abs_path = snapshot.abs_path().join(&entry.git_dir_path);
|
||||
if snapshot.is_abs_path_excluded(&git_dir_abs_path)
|
||||
&& !matches!(smol::block_on(fs.metadata(&git_dir_abs_path)), Ok(None))
|
||||
{
|
||||
ids_to_preserve.insert(work_directory_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
snapshot
|
||||
.git_repositories
|
||||
.retain(|work_directory_id, _| ids_to_preserve.contains(work_directory_id));
|
||||
snapshot
|
||||
.repository_entries
|
||||
.retain(|_, entry| ids_to_preserve.contains(&entry.work_directory.0));
|
||||
}
|
||||
|
||||
fn build_git_repository(
|
||||
@@ -3085,7 +3181,7 @@ impl BackgroundScanner {
|
||||
let ignore_stack = state
|
||||
.snapshot
|
||||
.ignore_stack_for_abs_path(&root_abs_path, true);
|
||||
if ignore_stack.is_all() {
|
||||
if ignore_stack.is_abs_path_ignored(&root_abs_path, true) {
|
||||
root_entry.is_ignored = true;
|
||||
state.insert_entry(root_entry.clone(), self.fs.as_ref());
|
||||
}
|
||||
@@ -3222,14 +3318,22 @@ impl BackgroundScanner {
|
||||
return false;
|
||||
};
|
||||
|
||||
let parent_dir_is_loaded = relative_path.parent().map_or(true, |parent| {
|
||||
snapshot
|
||||
.entry_for_path(parent)
|
||||
.map_or(false, |entry| entry.kind == EntryKind::Dir)
|
||||
});
|
||||
if !parent_dir_is_loaded {
|
||||
log::debug!("ignoring event {relative_path:?} within unloaded directory");
|
||||
return false;
|
||||
if !is_git_related(&abs_path) {
|
||||
let parent_dir_is_loaded = relative_path.parent().map_or(true, |parent| {
|
||||
snapshot
|
||||
.entry_for_path(parent)
|
||||
.map_or(false, |entry| entry.kind == EntryKind::Dir)
|
||||
});
|
||||
if !parent_dir_is_loaded {
|
||||
log::debug!("ignoring event {relative_path:?} within unloaded directory");
|
||||
return false;
|
||||
}
|
||||
if snapshot.is_abs_path_excluded(abs_path) {
|
||||
log::debug!(
|
||||
"ignoring FS event for path {relative_path:?} within excluded directory"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
relative_paths.push(relative_path);
|
||||
@@ -3392,18 +3496,26 @@ impl BackgroundScanner {
|
||||
}
|
||||
|
||||
async fn scan_dir(&self, job: &ScanJob) -> Result<()> {
|
||||
log::debug!("scan directory {:?}", job.path);
|
||||
|
||||
let mut ignore_stack = job.ignore_stack.clone();
|
||||
let mut new_ignore = None;
|
||||
let (root_abs_path, root_char_bag, next_entry_id) = {
|
||||
let snapshot = &self.state.lock().snapshot;
|
||||
(
|
||||
snapshot.abs_path().clone(),
|
||||
snapshot.root_char_bag,
|
||||
self.next_entry_id.clone(),
|
||||
)
|
||||
};
|
||||
let root_abs_path;
|
||||
let mut ignore_stack;
|
||||
let mut new_ignore;
|
||||
let root_char_bag;
|
||||
let next_entry_id;
|
||||
{
|
||||
let state = self.state.lock();
|
||||
let snapshot = &state.snapshot;
|
||||
root_abs_path = snapshot.abs_path().clone();
|
||||
if snapshot.is_abs_path_excluded(&job.abs_path) {
|
||||
log::error!("skipping excluded directory {:?}", job.path);
|
||||
return Ok(());
|
||||
}
|
||||
log::debug!("scanning directory {:?}", job.path);
|
||||
ignore_stack = job.ignore_stack.clone();
|
||||
new_ignore = None;
|
||||
root_char_bag = snapshot.root_char_bag;
|
||||
next_entry_id = self.next_entry_id.clone();
|
||||
drop(state);
|
||||
}
|
||||
|
||||
let mut dotgit_path = None;
|
||||
let mut root_canonical_path = None;
|
||||
@@ -3418,18 +3530,8 @@ impl BackgroundScanner {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let child_name = child_abs_path.file_name().unwrap();
|
||||
let child_path: Arc<Path> = job.path.join(child_name).into();
|
||||
let child_metadata = match self.fs.metadata(&child_abs_path).await {
|
||||
Ok(Some(metadata)) => metadata,
|
||||
Ok(None) => continue,
|
||||
Err(err) => {
|
||||
log::error!("error processing {:?}: {:?}", child_abs_path, err);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// If we find a .gitignore, add it to the stack of ignores used to determine which paths are ignored
|
||||
if child_name == *GITIGNORE {
|
||||
match build_gitignore(&child_abs_path, self.fs.as_ref()).await {
|
||||
@@ -3473,6 +3575,26 @@ impl BackgroundScanner {
|
||||
dotgit_path = Some(child_path.clone());
|
||||
}
|
||||
|
||||
{
|
||||
let mut state = self.state.lock();
|
||||
if state.snapshot.is_abs_path_excluded(&child_abs_path) {
|
||||
let relative_path = job.path.join(child_name);
|
||||
log::debug!("skipping excluded child entry {relative_path:?}");
|
||||
state.remove_path(&relative_path);
|
||||
continue;
|
||||
}
|
||||
drop(state);
|
||||
}
|
||||
|
||||
let child_metadata = match self.fs.metadata(&child_abs_path).await {
|
||||
Ok(Some(metadata)) => metadata,
|
||||
Ok(None) => continue,
|
||||
Err(err) => {
|
||||
log::error!("error processing {child_abs_path:?}: {err:?}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let mut child_entry = Entry::new(
|
||||
child_path.clone(),
|
||||
&child_metadata,
|
||||
@@ -3653,19 +3775,16 @@ impl BackgroundScanner {
|
||||
self.next_entry_id.as_ref(),
|
||||
state.snapshot.root_char_bag,
|
||||
);
|
||||
fs_entry.is_ignored = ignore_stack.is_all();
|
||||
let is_dir = fs_entry.is_dir();
|
||||
fs_entry.is_ignored = ignore_stack.is_abs_path_ignored(&abs_path, is_dir);
|
||||
fs_entry.is_external = !canonical_path.starts_with(&root_canonical_path);
|
||||
|
||||
if !fs_entry.is_ignored {
|
||||
if !fs_entry.is_dir() {
|
||||
if let Some((work_dir, repo)) =
|
||||
state.snapshot.local_repo_for_path(&path)
|
||||
{
|
||||
if let Ok(repo_path) = path.strip_prefix(work_dir.0) {
|
||||
let repo_path = RepoPath(repo_path.into());
|
||||
let repo = repo.repo_ptr.lock();
|
||||
fs_entry.git_status = repo.status(&repo_path, fs_entry.mtime);
|
||||
}
|
||||
if !is_dir && !fs_entry.is_ignored {
|
||||
if let Some((work_dir, repo)) = state.snapshot.local_repo_for_path(&path) {
|
||||
if let Ok(repo_path) = path.strip_prefix(work_dir.0) {
|
||||
let repo_path = RepoPath(repo_path.into());
|
||||
let repo = repo.repo_ptr.lock();
|
||||
fs_entry.git_status = repo.status(&repo_path, fs_entry.mtime);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3824,8 +3943,7 @@ impl BackgroundScanner {
|
||||
ignore_stack.clone()
|
||||
};
|
||||
|
||||
// Scan any directories that were previously ignored and weren't
|
||||
// previously scanned.
|
||||
// Scan any directories that were previously ignored and weren't previously scanned.
|
||||
if was_ignored && !entry.is_ignored && entry.kind.is_unloaded() {
|
||||
let state = self.state.lock();
|
||||
if state.should_scan_directory(&entry) {
|
||||
@@ -4001,6 +4119,12 @@ impl BackgroundScanner {
|
||||
}
|
||||
}
|
||||
|
||||
fn is_git_related(abs_path: &Path) -> bool {
|
||||
abs_path
|
||||
.components()
|
||||
.any(|c| c.as_os_str() == *DOT_GIT || c.as_os_str() == *GITIGNORE)
|
||||
}
|
||||
|
||||
fn char_bag_for_path(root_char_bag: CharBag, path: &Path) -> CharBag {
|
||||
let mut result = root_char_bag;
|
||||
result.extend(
|
||||
|
||||
+2310
-2141
File diff suppressed because it is too large
Load Diff
@@ -1732,7 +1732,7 @@ mod tests {
|
||||
use super::*;
|
||||
use gpui::{AnyWindowHandle, TestAppContext, ViewHandle, WindowHandle};
|
||||
use pretty_assertions::assert_eq;
|
||||
use project::FakeFs;
|
||||
use project::{project_settings::ProjectSettings, FakeFs};
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use std::{
|
||||
@@ -1832,6 +1832,123 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_exclusions_in_visible_list(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
cx.update(|cx| {
|
||||
cx.update_global::<SettingsStore, _, _>(|store, cx| {
|
||||
store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
|
||||
project_settings.file_scan_exclusions =
|
||||
Some(vec!["**/.git".to_string(), "**/4/**".to_string()]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
let fs = FakeFs::new(cx.background());
|
||||
fs.insert_tree(
|
||||
"/root1",
|
||||
json!({
|
||||
".dockerignore": "",
|
||||
".git": {
|
||||
"HEAD": "",
|
||||
},
|
||||
"a": {
|
||||
"0": { "q": "", "r": "", "s": "" },
|
||||
"1": { "t": "", "u": "" },
|
||||
"2": { "v": "", "w": "", "x": "", "y": "" },
|
||||
},
|
||||
"b": {
|
||||
"3": { "Q": "" },
|
||||
"4": { "R": "", "S": "", "T": "", "U": "" },
|
||||
},
|
||||
"C": {
|
||||
"5": {},
|
||||
"6": { "V": "", "W": "" },
|
||||
"7": { "X": "" },
|
||||
"8": { "Y": {}, "Z": "" }
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
fs.insert_tree(
|
||||
"/root2",
|
||||
json!({
|
||||
"d": {
|
||||
"4": ""
|
||||
},
|
||||
"e": {}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
|
||||
let workspace = cx
|
||||
.add_window(|cx| Workspace::test_new(project.clone(), cx))
|
||||
.root(cx);
|
||||
let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..50, cx),
|
||||
&[
|
||||
"v root1",
|
||||
" > a",
|
||||
" > b",
|
||||
" > C",
|
||||
" .dockerignore",
|
||||
"v root2",
|
||||
" > d",
|
||||
" > e",
|
||||
]
|
||||
);
|
||||
|
||||
toggle_expand_dir(&panel, "root1/b", cx);
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..50, cx),
|
||||
&[
|
||||
"v root1",
|
||||
" > a",
|
||||
" v b <== selected",
|
||||
" > 3",
|
||||
" > C",
|
||||
" .dockerignore",
|
||||
"v root2",
|
||||
" > d",
|
||||
" > e",
|
||||
]
|
||||
);
|
||||
|
||||
toggle_expand_dir(&panel, "root2/d", cx);
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..50, cx),
|
||||
&[
|
||||
"v root1",
|
||||
" > a",
|
||||
" v b",
|
||||
" > 3",
|
||||
" > C",
|
||||
" .dockerignore",
|
||||
"v root2",
|
||||
" v d <== selected",
|
||||
" > e",
|
||||
]
|
||||
);
|
||||
|
||||
toggle_expand_dir(&panel, "root2/e", cx);
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..50, cx),
|
||||
&[
|
||||
"v root1",
|
||||
" > a",
|
||||
" v b",
|
||||
" > 3",
|
||||
" > C",
|
||||
" .dockerignore",
|
||||
"v root2",
|
||||
" v d",
|
||||
" v e <== selected",
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 30)]
|
||||
async fn test_editing_files(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
@@ -2929,6 +3046,12 @@ mod tests {
|
||||
workspace::init_settings(cx);
|
||||
client::init_settings(cx);
|
||||
Project::init_settings(cx);
|
||||
|
||||
cx.update_global::<SettingsStore, _, _>(|store, cx| {
|
||||
store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
|
||||
project_settings.file_scan_exclusions = Some(Vec::new());
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ path = "src/project_panel.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
context_menu = { path = "../context_menu" }
|
||||
collections = { path = "../collections" }
|
||||
db = { path = "../db2", package = "db2" }
|
||||
editor = { path = "../editor2", package = "editor2" }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
pub mod file_associations;
|
||||
mod project_panel_settings;
|
||||
use settings::Settings;
|
||||
use settings::{Settings, SettingsStore};
|
||||
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use editor::{scroll::autoscroll::Autoscroll, Cancel, Editor};
|
||||
@@ -9,9 +9,9 @@ use file_associations::FileAssociations;
|
||||
use anyhow::{anyhow, Result};
|
||||
use gpui::{
|
||||
actions, div, px, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext,
|
||||
ClipboardItem, Component, Div, EventEmitter, FocusHandle, Focusable, FocusableView,
|
||||
InteractiveComponent, Model, MouseButton, ParentComponent, Pixels, Point, PromptLevel, Render,
|
||||
Stateful, StatefulInteractiveComponent, Styled, Task, UniformListScrollHandle, View,
|
||||
ClipboardItem, Div, EventEmitter, FocusHandle, Focusable, FocusableView, InteractiveElement,
|
||||
Model, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render,
|
||||
RenderOnce, Stateful, StatefulInteractiveElement, Styled, Task, UniformListScrollHandle, View,
|
||||
ViewContext, VisualContext as _, WeakView, WindowContext,
|
||||
};
|
||||
use menu::{Confirm, SelectNext, SelectPrev};
|
||||
@@ -34,7 +34,7 @@ use ui::{h_stack, v_stack, IconElement, Label};
|
||||
use unicase::UniCase;
|
||||
use util::{maybe, ResultExt, TryFutureExt};
|
||||
use workspace::{
|
||||
dock::{DockPosition, PanelEvent},
|
||||
dock::{DockPosition, Panel, PanelEvent},
|
||||
Workspace,
|
||||
};
|
||||
|
||||
@@ -148,7 +148,6 @@ pub enum Event {
|
||||
SplitEntry {
|
||||
entry_id: ProjectEntryId,
|
||||
},
|
||||
DockPositionChanged,
|
||||
Focus,
|
||||
NewSearchInDirectory {
|
||||
dir_entry: Entry,
|
||||
@@ -200,10 +199,11 @@ impl ProjectPanel {
|
||||
let filename_editor = cx.build_view(|cx| Editor::single_line(cx));
|
||||
|
||||
cx.subscribe(&filename_editor, |this, _, event, cx| match event {
|
||||
editor::Event::BufferEdited | editor::Event::SelectionsChanged { .. } => {
|
||||
editor::EditorEvent::BufferEdited
|
||||
| editor::EditorEvent::SelectionsChanged { .. } => {
|
||||
this.autoscroll(cx);
|
||||
}
|
||||
editor::Event::Blurred => {
|
||||
editor::EditorEvent::Blurred => {
|
||||
if this
|
||||
.edit_state
|
||||
.as_ref()
|
||||
@@ -244,16 +244,16 @@ impl ProjectPanel {
|
||||
this.update_visible_entries(None, cx);
|
||||
|
||||
// Update the dock position when the setting changes.
|
||||
// todo!()
|
||||
// let mut old_dock_position = this.position(cx);
|
||||
// cx.observe_global::<SettingsStore, _>(move |this, cx| {
|
||||
// let new_dock_position = this.position(cx);
|
||||
// if new_dock_position != old_dock_position {
|
||||
// old_dock_position = new_dock_position;
|
||||
// cx.emit(Event::DockPositionChanged);
|
||||
// }
|
||||
// })
|
||||
// .detach();
|
||||
let mut old_dock_position = this.position(cx);
|
||||
ProjectPanelSettings::register(cx);
|
||||
cx.observe_global::<SettingsStore>(move |this, cx| {
|
||||
let new_dock_position = this.position(cx);
|
||||
if new_dock_position != old_dock_position {
|
||||
old_dock_position = new_dock_position;
|
||||
cx.emit(PanelEvent::ChangePosition);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
this
|
||||
});
|
||||
@@ -1339,7 +1339,7 @@ impl ProjectPanel {
|
||||
editor: Option<&View<Editor>>,
|
||||
padding: Pixels,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Div<Self> {
|
||||
) -> Div {
|
||||
let show_editor = details.is_editing && !details.is_processing;
|
||||
|
||||
let theme = cx.theme();
|
||||
@@ -1378,7 +1378,7 @@ impl ProjectPanel {
|
||||
details: EntryDetails,
|
||||
// dragged_entry_destination: &mut Option<Arc<Path>>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Stateful<Self, Div<Self>> {
|
||||
) -> Stateful<Div> {
|
||||
let kind = details.kind;
|
||||
let settings = ProjectPanelSettings::get_global(cx);
|
||||
const INDENT_SIZE: Pixels = px(16.0);
|
||||
@@ -1396,7 +1396,7 @@ impl ProjectPanel {
|
||||
this.bg(cx.theme().colors().element_selected)
|
||||
})
|
||||
.hover(|style| style.bg(cx.theme().colors().element_hover))
|
||||
.on_click(move |this, event, cx| {
|
||||
.on_click(cx.listener(move |this, event: &gpui::ClickEvent, cx| {
|
||||
if !show_editor {
|
||||
if kind.is_dir() {
|
||||
this.toggle_expanded(entry_id, cx);
|
||||
@@ -1408,10 +1408,13 @@ impl ProjectPanel {
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.on_mouse_down(MouseButton::Right, move |this, event, cx| {
|
||||
this.deploy_context_menu(event.position, entry_id, cx);
|
||||
})
|
||||
}))
|
||||
.on_mouse_down(
|
||||
MouseButton::Right,
|
||||
cx.listener(move |this, event: &MouseDownEvent, cx| {
|
||||
this.deploy_context_menu(event.position, entry_id, cx);
|
||||
}),
|
||||
)
|
||||
// .on_drop::<ProjectEntryId>(|this, event, cx| {
|
||||
// this.move_entry(
|
||||
// *dragged_entry,
|
||||
@@ -1424,9 +1427,9 @@ impl ProjectPanel {
|
||||
}
|
||||
|
||||
impl Render for ProjectPanel {
|
||||
type Element = Focusable<Self, Stateful<Self, Div<Self>>>;
|
||||
type Element = Focusable<Stateful<Div>>;
|
||||
|
||||
fn render(&mut self, _cx: &mut gpui::ViewContext<Self>) -> Self::Element {
|
||||
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element {
|
||||
let has_worktree = self.visible_entries.len() != 0;
|
||||
|
||||
if has_worktree {
|
||||
@@ -1434,40 +1437,43 @@ impl Render for ProjectPanel {
|
||||
.id("project-panel")
|
||||
.size_full()
|
||||
.key_context("ProjectPanel")
|
||||
.on_action(Self::select_next)
|
||||
.on_action(Self::select_prev)
|
||||
.on_action(Self::expand_selected_entry)
|
||||
.on_action(Self::collapse_selected_entry)
|
||||
.on_action(Self::collapse_all_entries)
|
||||
.on_action(Self::new_file)
|
||||
.on_action(Self::new_directory)
|
||||
.on_action(Self::rename)
|
||||
.on_action(Self::delete)
|
||||
.on_action(Self::confirm)
|
||||
.on_action(Self::open_file)
|
||||
.on_action(Self::cancel)
|
||||
.on_action(Self::cut)
|
||||
.on_action(Self::copy)
|
||||
.on_action(Self::copy_path)
|
||||
.on_action(Self::copy_relative_path)
|
||||
.on_action(Self::paste)
|
||||
.on_action(Self::reveal_in_finder)
|
||||
.on_action(Self::open_in_terminal)
|
||||
.on_action(Self::new_search_in_directory)
|
||||
.on_action(cx.listener(Self::select_next))
|
||||
.on_action(cx.listener(Self::select_prev))
|
||||
.on_action(cx.listener(Self::expand_selected_entry))
|
||||
.on_action(cx.listener(Self::collapse_selected_entry))
|
||||
.on_action(cx.listener(Self::collapse_all_entries))
|
||||
.on_action(cx.listener(Self::new_file))
|
||||
.on_action(cx.listener(Self::new_directory))
|
||||
.on_action(cx.listener(Self::rename))
|
||||
.on_action(cx.listener(Self::delete))
|
||||
.on_action(cx.listener(Self::confirm))
|
||||
.on_action(cx.listener(Self::open_file))
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.on_action(cx.listener(Self::cut))
|
||||
.on_action(cx.listener(Self::copy))
|
||||
.on_action(cx.listener(Self::copy_path))
|
||||
.on_action(cx.listener(Self::copy_relative_path))
|
||||
.on_action(cx.listener(Self::paste))
|
||||
.on_action(cx.listener(Self::reveal_in_finder))
|
||||
.on_action(cx.listener(Self::open_in_terminal))
|
||||
.on_action(cx.listener(Self::new_search_in_directory))
|
||||
.track_focus(&self.focus_handle)
|
||||
.child(
|
||||
uniform_list(
|
||||
cx.view().clone(),
|
||||
"entries",
|
||||
self.visible_entries
|
||||
.iter()
|
||||
.map(|(_, worktree_entries)| worktree_entries.len())
|
||||
.sum(),
|
||||
|this: &mut Self, range, cx| {
|
||||
let mut items = Vec::new();
|
||||
this.for_each_visible_entry(range, cx, |id, details, cx| {
|
||||
items.push(this.render_entry(id, details, cx));
|
||||
});
|
||||
items
|
||||
{
|
||||
|this, range, cx| {
|
||||
let mut items = Vec::new();
|
||||
this.for_each_visible_entry(range, cx, |id, details, cx| {
|
||||
items.push(this.render_entry(id, details, cx));
|
||||
});
|
||||
items
|
||||
}
|
||||
},
|
||||
)
|
||||
.size_full()
|
||||
@@ -1485,7 +1491,7 @@ impl EventEmitter<Event> for ProjectPanel {}
|
||||
|
||||
impl EventEmitter<PanelEvent> for ProjectPanel {}
|
||||
|
||||
impl workspace::dock::Panel for ProjectPanel {
|
||||
impl Panel for ProjectPanel {
|
||||
fn position(&self, cx: &WindowContext) -> DockPosition {
|
||||
match ProjectPanelSettings::get_global(cx).dock {
|
||||
ProjectPanelDockPosition::Left => DockPosition::Left,
|
||||
@@ -1571,7 +1577,7 @@ mod tests {
|
||||
use super::*;
|
||||
use gpui::{TestAppContext, View, VisualTestContext, WindowHandle};
|
||||
use pretty_assertions::assert_eq;
|
||||
use project::FakeFs;
|
||||
use project::{project_settings::ProjectSettings, FakeFs};
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use std::{
|
||||
@@ -1672,6 +1678,124 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_exclusions_in_visible_list(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
cx.update(|cx| {
|
||||
cx.update_global::<SettingsStore, _>(|store, cx| {
|
||||
store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
|
||||
project_settings.file_scan_exclusions =
|
||||
Some(vec!["**/.git".to_string(), "**/4/**".to_string()]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
let fs = FakeFs::new(cx.background_executor.clone());
|
||||
fs.insert_tree(
|
||||
"/root1",
|
||||
json!({
|
||||
".dockerignore": "",
|
||||
".git": {
|
||||
"HEAD": "",
|
||||
},
|
||||
"a": {
|
||||
"0": { "q": "", "r": "", "s": "" },
|
||||
"1": { "t": "", "u": "" },
|
||||
"2": { "v": "", "w": "", "x": "", "y": "" },
|
||||
},
|
||||
"b": {
|
||||
"3": { "Q": "" },
|
||||
"4": { "R": "", "S": "", "T": "", "U": "" },
|
||||
},
|
||||
"C": {
|
||||
"5": {},
|
||||
"6": { "V": "", "W": "" },
|
||||
"7": { "X": "" },
|
||||
"8": { "Y": {}, "Z": "" }
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
fs.insert_tree(
|
||||
"/root2",
|
||||
json!({
|
||||
"d": {
|
||||
"4": ""
|
||||
},
|
||||
"e": {}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
|
||||
let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
let panel = workspace
|
||||
.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..50, cx),
|
||||
&[
|
||||
"v root1",
|
||||
" > a",
|
||||
" > b",
|
||||
" > C",
|
||||
" .dockerignore",
|
||||
"v root2",
|
||||
" > d",
|
||||
" > e",
|
||||
]
|
||||
);
|
||||
|
||||
toggle_expand_dir(&panel, "root1/b", cx);
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..50, cx),
|
||||
&[
|
||||
"v root1",
|
||||
" > a",
|
||||
" v b <== selected",
|
||||
" > 3",
|
||||
" > C",
|
||||
" .dockerignore",
|
||||
"v root2",
|
||||
" > d",
|
||||
" > e",
|
||||
]
|
||||
);
|
||||
|
||||
toggle_expand_dir(&panel, "root2/d", cx);
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..50, cx),
|
||||
&[
|
||||
"v root1",
|
||||
" > a",
|
||||
" v b",
|
||||
" > 3",
|
||||
" > C",
|
||||
" .dockerignore",
|
||||
"v root2",
|
||||
" v d <== selected",
|
||||
" > e",
|
||||
]
|
||||
);
|
||||
|
||||
toggle_expand_dir(&panel, "root2/e", cx);
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..50, cx),
|
||||
&[
|
||||
"v root1",
|
||||
" > a",
|
||||
" v b",
|
||||
" > 3",
|
||||
" > C",
|
||||
" .dockerignore",
|
||||
"v root2",
|
||||
" v d",
|
||||
" v e <== selected",
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 30)]
|
||||
async fn test_editing_files(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
@@ -2792,6 +2916,12 @@ mod tests {
|
||||
workspace::init_settings(cx);
|
||||
client::init_settings(cx);
|
||||
Project::init_settings(cx);
|
||||
|
||||
cx.update_global::<SettingsStore, _>(|store, cx| {
|
||||
store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
|
||||
project_settings.file_scan_exclusions = Some(Vec::new());
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -56,12 +56,12 @@ pub struct Mention {
|
||||
}
|
||||
|
||||
impl RichText {
|
||||
pub fn element<V: 'static>(
|
||||
pub fn element(
|
||||
&self,
|
||||
// syntax: Arc<SyntaxTheme>,
|
||||
// style: RichTextStyle,
|
||||
// cx: &mut ViewContext<V>,
|
||||
) -> AnyElement<V> {
|
||||
) -> AnyElement {
|
||||
todo!();
|
||||
|
||||
// let mut region_id = 0;
|
||||
|
||||
@@ -884,6 +884,7 @@ message SearchProject {
|
||||
bool case_sensitive = 5;
|
||||
string files_to_include = 6;
|
||||
string files_to_exclude = 7;
|
||||
bool include_ignored = 8;
|
||||
}
|
||||
|
||||
message SearchProjectResponse {
|
||||
|
||||
@@ -884,6 +884,7 @@ message SearchProject {
|
||||
bool case_sensitive = 5;
|
||||
string files_to_include = 6;
|
||||
string files_to_exclude = 7;
|
||||
bool include_ignored = 8;
|
||||
}
|
||||
|
||||
message SearchProjectResponse {
|
||||
|
||||
@@ -805,6 +805,7 @@ impl BufferSearchBar {
|
||||
query,
|
||||
self.search_options.contains(SearchOptions::WHOLE_WORD),
|
||||
self.search_options.contains(SearchOptions::CASE_SENSITIVE),
|
||||
false,
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
) {
|
||||
@@ -820,6 +821,7 @@ impl BufferSearchBar {
|
||||
query,
|
||||
self.search_options.contains(SearchOptions::WHOLE_WORD),
|
||||
self.search_options.contains(SearchOptions::CASE_SENSITIVE),
|
||||
false,
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
) {
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::{
|
||||
search_bar::{render_nav_button, render_option_button_icon, render_search_mode_button},
|
||||
ActivateRegexMode, ActivateSemanticMode, ActivateTextMode, CycleMode, NextHistoryQuery,
|
||||
PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions, SelectNextMatch, SelectPrevMatch,
|
||||
ToggleCaseSensitive, ToggleReplace, ToggleWholeWord,
|
||||
ToggleCaseSensitive, ToggleIncludeIgnored, ToggleReplace, ToggleWholeWord,
|
||||
};
|
||||
use anyhow::{Context, Result};
|
||||
use collections::HashMap;
|
||||
@@ -85,6 +85,7 @@ pub fn init(cx: &mut AppContext) {
|
||||
cx.capture_action(ProjectSearchView::replace_next);
|
||||
add_toggle_option_action::<ToggleCaseSensitive>(SearchOptions::CASE_SENSITIVE, cx);
|
||||
add_toggle_option_action::<ToggleWholeWord>(SearchOptions::WHOLE_WORD, cx);
|
||||
add_toggle_option_action::<ToggleIncludeIgnored>(SearchOptions::INCLUDE_IGNORED, cx);
|
||||
add_toggle_filters_action::<ToggleFilters>(cx);
|
||||
}
|
||||
|
||||
@@ -1192,6 +1193,7 @@ impl ProjectSearchView {
|
||||
text,
|
||||
self.search_options.contains(SearchOptions::WHOLE_WORD),
|
||||
self.search_options.contains(SearchOptions::CASE_SENSITIVE),
|
||||
self.search_options.contains(SearchOptions::INCLUDE_IGNORED),
|
||||
included_files,
|
||||
excluded_files,
|
||||
) {
|
||||
@@ -1210,6 +1212,7 @@ impl ProjectSearchView {
|
||||
text,
|
||||
self.search_options.contains(SearchOptions::WHOLE_WORD),
|
||||
self.search_options.contains(SearchOptions::CASE_SENSITIVE),
|
||||
self.search_options.contains(SearchOptions::INCLUDE_IGNORED),
|
||||
included_files,
|
||||
excluded_files,
|
||||
) {
|
||||
@@ -1764,6 +1767,17 @@ impl View for ProjectSearchBar {
|
||||
render_option_button_icon("icons/word_search.svg", SearchOptions::WHOLE_WORD, cx)
|
||||
});
|
||||
|
||||
let mut include_ignored = is_semantic_disabled.then(|| {
|
||||
render_option_button_icon(
|
||||
// TODO proper icon
|
||||
"icons/case_insensitive.svg",
|
||||
SearchOptions::INCLUDE_IGNORED,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
// TODO not implemented yet
|
||||
let _ = include_ignored.take();
|
||||
|
||||
let search_button_for_mode = |mode, side, cx: &mut ViewContext<ProjectSearchBar>| {
|
||||
let is_active = if let Some(search) = self.active_project_search.as_ref() {
|
||||
let search = search.read(cx);
|
||||
@@ -1879,7 +1893,15 @@ impl View for ProjectSearchBar {
|
||||
.with_children(search.filters_enabled.then(|| {
|
||||
Flex::row()
|
||||
.with_child(
|
||||
ChildView::new(&search.included_files_editor, cx)
|
||||
Flex::row()
|
||||
.with_child(
|
||||
ChildView::new(&search.included_files_editor, cx)
|
||||
.contained()
|
||||
.constrained()
|
||||
.with_height(theme.search.search_bar_row_height)
|
||||
.flex(1., true),
|
||||
)
|
||||
.with_children(include_ignored)
|
||||
.contained()
|
||||
.with_style(include_container_style)
|
||||
.constrained()
|
||||
|
||||
@@ -29,6 +29,7 @@ actions!(
|
||||
CycleMode,
|
||||
ToggleWholeWord,
|
||||
ToggleCaseSensitive,
|
||||
ToggleIncludeIgnored,
|
||||
ToggleReplace,
|
||||
SelectNextMatch,
|
||||
SelectPrevMatch,
|
||||
@@ -49,31 +50,35 @@ bitflags! {
|
||||
const NONE = 0b000;
|
||||
const WHOLE_WORD = 0b001;
|
||||
const CASE_SENSITIVE = 0b010;
|
||||
const INCLUDE_IGNORED = 0b100;
|
||||
}
|
||||
}
|
||||
|
||||
impl SearchOptions {
|
||||
pub fn label(&self) -> &'static str {
|
||||
match *self {
|
||||
SearchOptions::WHOLE_WORD => "Match Whole Word",
|
||||
SearchOptions::CASE_SENSITIVE => "Match Case",
|
||||
_ => panic!("{:?} is not a named SearchOption", self),
|
||||
Self::WHOLE_WORD => "Match Whole Word",
|
||||
Self::CASE_SENSITIVE => "Match Case",
|
||||
Self::INCLUDE_IGNORED => "Include Ignored",
|
||||
_ => panic!("{self:?} is not a named SearchOption"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn icon(&self) -> &'static str {
|
||||
match *self {
|
||||
SearchOptions::WHOLE_WORD => "icons/word_search.svg",
|
||||
SearchOptions::CASE_SENSITIVE => "icons/case_insensitive.svg",
|
||||
_ => panic!("{:?} is not a named SearchOption", self),
|
||||
Self::WHOLE_WORD => "icons/word_search.svg",
|
||||
Self::CASE_SENSITIVE => "icons/case_insensitive.svg",
|
||||
Self::INCLUDE_IGNORED => "icons/case_insensitive.svg",
|
||||
_ => panic!("{self:?} is not a named SearchOption"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_toggle_action(&self) -> Box<dyn Action> {
|
||||
match *self {
|
||||
SearchOptions::WHOLE_WORD => Box::new(ToggleWholeWord),
|
||||
SearchOptions::CASE_SENSITIVE => Box::new(ToggleCaseSensitive),
|
||||
_ => panic!("{:?} is not a named SearchOption", self),
|
||||
Self::WHOLE_WORD => Box::new(ToggleWholeWord),
|
||||
Self::CASE_SENSITIVE => Box::new(ToggleCaseSensitive),
|
||||
Self::INCLUDE_IGNORED => Box::new(ToggleIncludeIgnored),
|
||||
_ => panic!("{self:?} is not a named SearchOption"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +90,7 @@ impl SearchOptions {
|
||||
let mut options = SearchOptions::NONE;
|
||||
options.set(SearchOptions::WHOLE_WORD, query.whole_word());
|
||||
options.set(SearchOptions::CASE_SENSITIVE, query.case_sensitive());
|
||||
options.set(SearchOptions::INCLUDE_IGNORED, query.include_ignored());
|
||||
options
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
[package]
|
||||
name = "search2"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/search.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
bitflags = "1"
|
||||
collections = { path = "../collections" }
|
||||
editor = { package = "editor2", path = "../editor2" }
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
language = { package = "language2", path = "../language2" }
|
||||
menu = { package = "menu2", path = "../menu2" }
|
||||
project = { package = "project2", path = "../project2" }
|
||||
settings = { package = "settings2", path = "../settings2" }
|
||||
theme = { package = "theme2", path = "../theme2" }
|
||||
util = { path = "../util" }
|
||||
ui = {package = "ui2", path = "../ui2"}
|
||||
workspace = { package = "workspace2", path = "../workspace2" }
|
||||
#semantic_index = { path = "../semantic_index" }
|
||||
anyhow.workspace = true
|
||||
futures.workspace = true
|
||||
log.workspace = true
|
||||
postage.workspace = true
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
smallvec.workspace = true
|
||||
smol.workspace = true
|
||||
serde_json.workspace = true
|
||||
[dev-dependencies]
|
||||
client = { package = "client2", path = "../client2", features = ["test-support"] }
|
||||
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
|
||||
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
|
||||
|
||||
workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
|
||||
unindent.workspace = true
|
||||
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
Reference in New Issue
Block a user