mirror of
https://github.com/zed-industries/zed.git
synced 2024-11-07 20:39:04 +03:00
Merge branch 'main' into welcome
This commit is contained in:
commit
8faa1f6e58
12
.cargo/ci-config.toml
Normal file
12
.cargo/ci-config.toml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# This config is different from config.toml in this directory, as the latter is recognized by Cargo.
|
||||||
|
# This file is placed in $HOME/.cargo/config.toml on CI runs. Cargo then merges Zeds .cargo/config.toml with $HOME/.cargo/config.toml
|
||||||
|
# with preference for settings from Zeds config.toml.
|
||||||
|
# TL;DR: If a value is set in both ci-config.toml and config.toml, config.toml value takes precedence.
|
||||||
|
# Arrays are merged together though. See: https://doc.rust-lang.org/cargo/reference/config.html#hierarchical-structure
|
||||||
|
# The intent for this file is to configure CI build process with a divergance from Zed developers experience; for example, in this config file
|
||||||
|
# we use `-D warnings` for rustflags (which makes compilation fail in presence of warnings during build process). Placing that in developers `config.toml`
|
||||||
|
# would be incovenient.
|
||||||
|
# We *could* override things like RUSTFLAGS manually by setting them as environment variables, but that is less DRY; worse yet, if you forget to set proper environment variables
|
||||||
|
# in one spot, that's going to trigger a rebuild of all of the artifacts. Using ci-config.toml we can define these overrides for CI in one spot and not worry about it.
|
||||||
|
[build]
|
||||||
|
rustflags = ["-D", "warnings"]
|
6
.github/actions/run_tests/action.yml
vendored
6
.github/actions/run_tests/action.yml
vendored
@ -19,16 +19,12 @@ runs:
|
|||||||
|
|
||||||
- name: Limit target directory size
|
- name: Limit target directory size
|
||||||
shell: bash -euxo pipefail {0}
|
shell: bash -euxo pipefail {0}
|
||||||
run: script/clear-target-dir-if-larger-than 70
|
run: script/clear-target-dir-if-larger-than 100
|
||||||
|
|
||||||
- name: Run check
|
- name: Run check
|
||||||
env:
|
|
||||||
RUSTFLAGS: -D warnings
|
|
||||||
shell: bash -euxo pipefail {0}
|
shell: bash -euxo pipefail {0}
|
||||||
run: cargo check --tests --workspace
|
run: cargo check --tests --workspace
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
env:
|
|
||||||
RUSTFLAGS: -D warnings
|
|
||||||
shell: bash -euxo pipefail {0}
|
shell: bash -euxo pipefail {0}
|
||||||
run: cargo nextest run --workspace --no-fail-fast
|
run: cargo nextest run --workspace --no-fail-fast
|
||||||
|
7
.github/workflows/ci.yml
vendored
7
.github/workflows/ci.yml
vendored
@ -29,6 +29,9 @@ jobs:
|
|||||||
clean: false
|
clean: false
|
||||||
submodules: "recursive"
|
submodules: "recursive"
|
||||||
|
|
||||||
|
- name: Set up default .cargo/config.toml
|
||||||
|
run: cp ./.cargo/ci-config.toml ~/.cargo/config.toml
|
||||||
|
|
||||||
- name: Run rustfmt
|
- name: Run rustfmt
|
||||||
uses: ./.github/actions/check_formatting
|
uses: ./.github/actions/check_formatting
|
||||||
|
|
||||||
@ -87,7 +90,7 @@ jobs:
|
|||||||
submodules: "recursive"
|
submodules: "recursive"
|
||||||
|
|
||||||
- name: Limit target directory size
|
- name: Limit target directory size
|
||||||
run: script/clear-target-dir-if-larger-than 70
|
run: script/clear-target-dir-if-larger-than 100
|
||||||
|
|
||||||
- name: Determine version and release channel
|
- name: Determine version and release channel
|
||||||
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||||
@ -131,8 +134,6 @@ jobs:
|
|||||||
|
|
||||||
- uses: softprops/action-gh-release@v1
|
- uses: softprops/action-gh-release@v1
|
||||||
name: Upload app bundle to release
|
name: Upload app bundle to release
|
||||||
# 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' }}
|
if: ${{ env.RELEASE_CHANNEL == 'preview' || env.RELEASE_CHANNEL == 'stable' }}
|
||||||
with:
|
with:
|
||||||
draft: true
|
draft: true
|
||||||
|
2
.github/workflows/release_nightly.yml
vendored
2
.github/workflows/release_nightly.yml
vendored
@ -79,7 +79,7 @@ jobs:
|
|||||||
submodules: "recursive"
|
submodules: "recursive"
|
||||||
|
|
||||||
- name: Limit target directory size
|
- name: Limit target directory size
|
||||||
run: script/clear-target-dir-if-larger-than 70
|
run: script/clear-target-dir-if-larger-than 100
|
||||||
|
|
||||||
- name: Set release channel to nightly
|
- name: Set release channel to nightly
|
||||||
run: |
|
run: |
|
||||||
|
75
Cargo.lock
generated
75
Cargo.lock
generated
@ -841,6 +841,17 @@ dependencies = [
|
|||||||
"rustc-demangle",
|
"rustc-demangle",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "backtrace-on-stack-overflow"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7fd2d70527f3737a1ad17355e260706c1badebabd1fa06a7a053407380df841b"
|
||||||
|
dependencies = [
|
||||||
|
"backtrace",
|
||||||
|
"libc",
|
||||||
|
"nix 0.23.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64"
|
name = "base64"
|
||||||
version = "0.13.1"
|
version = "0.13.1"
|
||||||
@ -1175,12 +1186,14 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-broadcast",
|
"async-broadcast",
|
||||||
|
"async-trait",
|
||||||
"audio2",
|
"audio2",
|
||||||
"client2",
|
"client2",
|
||||||
"collections",
|
"collections",
|
||||||
"fs2",
|
"fs2",
|
||||||
"futures 0.3.28",
|
"futures 0.3.28",
|
||||||
"gpui2",
|
"gpui2",
|
||||||
|
"image",
|
||||||
"language2",
|
"language2",
|
||||||
"live_kit_client2",
|
"live_kit_client2",
|
||||||
"log",
|
"log",
|
||||||
@ -1192,7 +1205,10 @@ dependencies = [
|
|||||||
"serde_derive",
|
"serde_derive",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"settings2",
|
"settings2",
|
||||||
|
"smallvec",
|
||||||
|
"ui2",
|
||||||
"util",
|
"util",
|
||||||
|
"workspace2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1653,7 +1669,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "collab"
|
name = "collab"
|
||||||
version = "0.28.0"
|
version = "0.29.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@ -5559,6 +5575,19 @@ dependencies = [
|
|||||||
"winapi 0.3.9",
|
"winapi 0.3.9",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nix"
|
||||||
|
version = "0.23.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
"cc",
|
||||||
|
"cfg-if 1.0.0",
|
||||||
|
"libc",
|
||||||
|
"memoffset 0.6.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nix"
|
name = "nix"
|
||||||
version = "0.24.3"
|
version = "0.24.3"
|
||||||
@ -8859,6 +8888,42 @@ version = "1.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "story"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"gpui2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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",
|
||||||
|
"story",
|
||||||
|
"strum",
|
||||||
|
"theme",
|
||||||
|
"theme2",
|
||||||
|
"ui2",
|
||||||
|
"util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "stringprep"
|
name = "stringprep"
|
||||||
version = "0.1.4"
|
version = "0.1.4"
|
||||||
@ -9362,6 +9427,7 @@ dependencies = [
|
|||||||
"serde_derive",
|
"serde_derive",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"settings2",
|
"settings2",
|
||||||
|
"story",
|
||||||
"toml 0.5.11",
|
"toml 0.5.11",
|
||||||
"util",
|
"util",
|
||||||
"uuid 1.4.1",
|
"uuid 1.4.1",
|
||||||
@ -9884,7 +9950,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "tree-sitter"
|
name = "tree-sitter"
|
||||||
version = "0.20.10"
|
version = "0.20.10"
|
||||||
source = "git+https://github.com/tree-sitter/tree-sitter?rev=35a6052fbcafc5e5fc0f9415b8652be7dcaf7222#35a6052fbcafc5e5fc0f9415b8652be7dcaf7222"
|
source = "git+https://github.com/tree-sitter/tree-sitter?rev=3b0159d25559b603af566ade3c83d930bf466db1#3b0159d25559b603af566ade3c83d930bf466db1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"regex",
|
"regex",
|
||||||
@ -10225,6 +10291,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"settings2",
|
"settings2",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
|
"story",
|
||||||
"strum",
|
"strum",
|
||||||
"theme2",
|
"theme2",
|
||||||
]
|
]
|
||||||
@ -11363,6 +11430,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-recursion 1.0.5",
|
"async-recursion 1.0.5",
|
||||||
|
"async-trait",
|
||||||
"bincode",
|
"bincode",
|
||||||
"call2",
|
"call2",
|
||||||
"client2",
|
"client2",
|
||||||
@ -11475,7 +11543,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zed"
|
name = "zed"
|
||||||
version = "0.114.0"
|
version = "0.115.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"activity_indicator",
|
"activity_indicator",
|
||||||
"ai",
|
"ai",
|
||||||
@ -11623,6 +11691,7 @@ dependencies = [
|
|||||||
"async-recursion 0.3.2",
|
"async-recursion 0.3.2",
|
||||||
"async-tar",
|
"async-tar",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
"audio2",
|
||||||
"auto_update2",
|
"auto_update2",
|
||||||
"backtrace",
|
"backtrace",
|
||||||
"call2",
|
"call2",
|
||||||
|
10
Cargo.toml
10
Cargo.toml
@ -97,8 +97,7 @@ members = [
|
|||||||
"crates/sqlez",
|
"crates/sqlez",
|
||||||
"crates/sqlez_macros",
|
"crates/sqlez_macros",
|
||||||
"crates/rich_text",
|
"crates/rich_text",
|
||||||
# "crates/storybook2",
|
"crates/storybook2",
|
||||||
# "crates/storybook3",
|
|
||||||
"crates/sum_tree",
|
"crates/sum_tree",
|
||||||
"crates/terminal",
|
"crates/terminal",
|
||||||
"crates/terminal2",
|
"crates/terminal2",
|
||||||
@ -112,6 +111,7 @@ members = [
|
|||||||
"crates/ui2",
|
"crates/ui2",
|
||||||
"crates/util",
|
"crates/util",
|
||||||
"crates/semantic_index",
|
"crates/semantic_index",
|
||||||
|
"crates/story",
|
||||||
"crates/vim",
|
"crates/vim",
|
||||||
"crates/vcs_menu",
|
"crates/vcs_menu",
|
||||||
"crates/workspace2",
|
"crates/workspace2",
|
||||||
@ -197,8 +197,9 @@ tree-sitter-lua = "0.0.14"
|
|||||||
tree-sitter-nix = { git = "https://github.com/nix-community/tree-sitter-nix", rev = "66e3e9ce9180ae08fc57372061006ef83f0abde7" }
|
tree-sitter-nix = { git = "https://github.com/nix-community/tree-sitter-nix", rev = "66e3e9ce9180ae08fc57372061006ef83f0abde7" }
|
||||||
tree-sitter-nu = { git = "https://github.com/nushell/tree-sitter-nu", rev = "786689b0562b9799ce53e824cb45a1a2a04dc673"}
|
tree-sitter-nu = { git = "https://github.com/nushell/tree-sitter-nu", rev = "786689b0562b9799ce53e824cb45a1a2a04dc673"}
|
||||||
tree-sitter-vue = {git = "https://github.com/zed-industries/tree-sitter-vue", rev = "9b6cb221ccb8d0b956fcb17e9a1efac2feefeb58"}
|
tree-sitter-vue = {git = "https://github.com/zed-industries/tree-sitter-vue", rev = "9b6cb221ccb8d0b956fcb17e9a1efac2feefeb58"}
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "35a6052fbcafc5e5fc0f9415b8652be7dcaf7222" }
|
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "3b0159d25559b603af566ade3c83d930bf466db1" }
|
||||||
async-task = { git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e" }
|
async-task = { git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e" }
|
||||||
|
|
||||||
# TODO - Remove when a version is released with this PR: https://github.com/servo/core-foundation-rs/pull/457
|
# TODO - Remove when a version is released with this PR: https://github.com/servo/core-foundation-rs/pull/457
|
||||||
@ -210,11 +211,12 @@ core-graphics = { git = "https://github.com/servo/core-foundation-rs", rev = "07
|
|||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
split-debuginfo = "unpacked"
|
split-debuginfo = "unpacked"
|
||||||
|
debug = "limited"
|
||||||
|
|
||||||
[profile.dev.package.taffy]
|
[profile.dev.package.taffy]
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
debug = true
|
debug = "limited"
|
||||||
lto = "thin"
|
lto = "thin"
|
||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
|
4
Procfile.zed2
Normal file
4
Procfile.zed2
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
web: cd ../zed.dev && PORT=3000 npm run dev
|
||||||
|
collab: cd crates/collab2 && RUST_LOG=${RUST_LOG:-warn,collab=info} cargo run serve
|
||||||
|
livekit: livekit-server --dev
|
||||||
|
postgrest: postgrest crates/collab2/admin_api.conf
|
@ -43,7 +43,7 @@
|
|||||||
"calt": false
|
"calt": false
|
||||||
},
|
},
|
||||||
// The default font size for text in the UI
|
// The default font size for text in the UI
|
||||||
"ui_font_size": 14,
|
"ui_font_size": 16,
|
||||||
// The factor to grow the active pane by. Defaults to 1.0
|
// The factor to grow the active pane by. Defaults to 1.0
|
||||||
// which gives the same size as all other panes.
|
// which gives the same size as all other panes.
|
||||||
"active_pane_magnification": 1.0,
|
"active_pane_magnification": 1.0,
|
||||||
|
@ -7,5 +7,6 @@
|
|||||||
// custom settings, run the `open default settings` command
|
// custom settings, run the `open default settings` command
|
||||||
// from the command palette or from `Zed` application menu.
|
// from the command palette or from `Zed` application menu.
|
||||||
{
|
{
|
||||||
"buffer_font_size": 15
|
"ui_font_size": 16,
|
||||||
|
"buffer_font_size": 16
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
use gpui::{div, Div, EventEmitter, ParentElement, Render, SemanticVersion, ViewContext};
|
use gpui::{
|
||||||
|
div, DismissEvent, Div, EventEmitter, ParentElement, Render, SemanticVersion, ViewContext,
|
||||||
|
};
|
||||||
use menu::Cancel;
|
use menu::Cancel;
|
||||||
use workspace::notifications::NotificationEvent;
|
|
||||||
|
|
||||||
pub struct UpdateNotification {
|
pub struct UpdateNotification {
|
||||||
_version: SemanticVersion,
|
_version: SemanticVersion,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventEmitter<NotificationEvent> for UpdateNotification {}
|
impl EventEmitter<DismissEvent> for UpdateNotification {}
|
||||||
|
|
||||||
impl Render for UpdateNotification {
|
impl Render for UpdateNotification {
|
||||||
type Element = Div;
|
type Element = Div;
|
||||||
@ -82,6 +83,6 @@ impl UpdateNotification {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn _dismiss(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
|
pub fn _dismiss(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
|
||||||
cx.emit(NotificationEvent::Dismiss);
|
cx.emit(DismissEvent::Dismiss);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,15 +31,19 @@ media = { path = "../media" }
|
|||||||
project = { package = "project2", path = "../project2" }
|
project = { package = "project2", path = "../project2" }
|
||||||
settings = { package = "settings2", path = "../settings2" }
|
settings = { package = "settings2", path = "../settings2" }
|
||||||
util = { path = "../util" }
|
util = { path = "../util" }
|
||||||
|
ui = {package = "ui2", path = "../ui2"}
|
||||||
|
workspace = {package = "workspace2", path = "../workspace2"}
|
||||||
|
async-trait.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
async-broadcast = "0.4"
|
async-broadcast = "0.4"
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
|
image = "0.23"
|
||||||
postage.workspace = true
|
postage.workspace = true
|
||||||
schemars.workspace = true
|
schemars.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
serde_derive.workspace = true
|
serde_derive.workspace = true
|
||||||
|
smallvec.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
client = { package = "client2", path = "../client2", features = ["test-support"] }
|
client = { package = "client2", path = "../client2", features = ["test-support"] }
|
||||||
|
@ -1,25 +1,32 @@
|
|||||||
pub mod call_settings;
|
pub mod call_settings;
|
||||||
pub mod participant;
|
pub mod participant;
|
||||||
pub mod room;
|
pub mod room;
|
||||||
|
mod shared_screen;
|
||||||
|
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
|
use async_trait::async_trait;
|
||||||
use audio::Audio;
|
use audio::Audio;
|
||||||
use call_settings::CallSettings;
|
use call_settings::CallSettings;
|
||||||
use client::{proto, Client, TelemetrySettings, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE};
|
use client::{
|
||||||
|
proto::{self, PeerId},
|
||||||
|
Client, TelemetrySettings, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE,
|
||||||
|
};
|
||||||
use collections::HashSet;
|
use collections::HashSet;
|
||||||
use futures::{channel::oneshot, future::Shared, Future, FutureExt};
|
use futures::{channel::oneshot, future::Shared, Future, FutureExt};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Subscription, Task,
|
AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Subscription, Task,
|
||||||
WeakModel,
|
View, ViewContext, VisualContext, WeakModel, WeakView,
|
||||||
};
|
};
|
||||||
|
pub use participant::ParticipantLocation;
|
||||||
use postage::watch;
|
use postage::watch;
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use room::Event;
|
use room::Event;
|
||||||
use settings::Settings;
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
pub use participant::ParticipantLocation;
|
|
||||||
pub use room::Room;
|
pub use room::Room;
|
||||||
|
use settings::Settings;
|
||||||
|
use shared_screen::SharedScreen;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use util::ResultExt;
|
||||||
|
use workspace::{item::ItemHandle, CallHandler, Pane, Workspace};
|
||||||
|
|
||||||
pub fn init(client: Arc<Client>, user_store: Model<UserStore>, cx: &mut AppContext) {
|
pub fn init(client: Arc<Client>, user_store: Model<UserStore>, cx: &mut AppContext) {
|
||||||
CallSettings::register(cx);
|
CallSettings::register(cx);
|
||||||
@ -464,7 +471,7 @@ impl ActiveCall {
|
|||||||
&self.pending_invites
|
&self.pending_invites
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn report_call_event(&self, operation: &'static str, cx: &AppContext) {
|
pub fn report_call_event(&self, operation: &'static str, cx: &mut AppContext) {
|
||||||
if let Some(room) = self.room() {
|
if let Some(room) = self.room() {
|
||||||
let room = room.read(cx);
|
let room = room.read(cx);
|
||||||
report_call_event_for_room(operation, room.id(), room.channel_id(), &self.client, cx);
|
report_call_event_for_room(operation, room.id(), room.channel_id(), &self.client, cx);
|
||||||
@ -477,7 +484,7 @@ pub fn report_call_event_for_room(
|
|||||||
room_id: u64,
|
room_id: u64,
|
||||||
channel_id: Option<u64>,
|
channel_id: Option<u64>,
|
||||||
client: &Arc<Client>,
|
client: &Arc<Client>,
|
||||||
cx: &AppContext,
|
cx: &mut AppContext,
|
||||||
) {
|
) {
|
||||||
let telemetry = client.telemetry();
|
let telemetry = client.telemetry();
|
||||||
let telemetry_settings = *TelemetrySettings::get_global(cx);
|
let telemetry_settings = *TelemetrySettings::get_global(cx);
|
||||||
@ -505,6 +512,205 @@ pub fn report_call_event_for_channel(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct Call {
|
||||||
|
active_call: Option<(Model<ActiveCall>, Vec<Subscription>)>,
|
||||||
|
parent_workspace: WeakView<Workspace>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Call {
|
||||||
|
pub fn new(
|
||||||
|
parent_workspace: WeakView<Workspace>,
|
||||||
|
cx: &mut ViewContext<'_, Workspace>,
|
||||||
|
) -> Box<dyn CallHandler> {
|
||||||
|
let mut active_call = None;
|
||||||
|
if cx.has_global::<Model<ActiveCall>>() {
|
||||||
|
let call = cx.global::<Model<ActiveCall>>().clone();
|
||||||
|
let subscriptions = vec![cx.subscribe(&call, Self::on_active_call_event)];
|
||||||
|
active_call = Some((call, subscriptions));
|
||||||
|
}
|
||||||
|
Box::new(Self {
|
||||||
|
active_call,
|
||||||
|
parent_workspace,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fn on_active_call_event(
|
||||||
|
workspace: &mut Workspace,
|
||||||
|
_: Model<ActiveCall>,
|
||||||
|
event: &room::Event,
|
||||||
|
cx: &mut ViewContext<Workspace>,
|
||||||
|
) {
|
||||||
|
match event {
|
||||||
|
room::Event::ParticipantLocationChanged { participant_id }
|
||||||
|
| room::Event::RemoteVideoTracksChanged { participant_id } => {
|
||||||
|
workspace.leader_updated(*participant_id, cx);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait(?Send)]
|
||||||
|
impl CallHandler for Call {
|
||||||
|
fn peer_state(
|
||||||
|
&mut self,
|
||||||
|
leader_id: PeerId,
|
||||||
|
cx: &mut ViewContext<Workspace>,
|
||||||
|
) -> Option<(bool, bool)> {
|
||||||
|
let (call, _) = self.active_call.as_ref()?;
|
||||||
|
let room = call.read(cx).room()?.read(cx);
|
||||||
|
let participant = room.remote_participant_for_peer_id(leader_id)?;
|
||||||
|
|
||||||
|
let leader_in_this_app;
|
||||||
|
let leader_in_this_project;
|
||||||
|
match participant.location {
|
||||||
|
ParticipantLocation::SharedProject { project_id } => {
|
||||||
|
leader_in_this_app = true;
|
||||||
|
leader_in_this_project = Some(project_id)
|
||||||
|
== self
|
||||||
|
.parent_workspace
|
||||||
|
.update(cx, |this, cx| this.project().read(cx).remote_id())
|
||||||
|
.log_err()
|
||||||
|
.flatten();
|
||||||
|
}
|
||||||
|
ParticipantLocation::UnsharedProject => {
|
||||||
|
leader_in_this_app = true;
|
||||||
|
leader_in_this_project = false;
|
||||||
|
}
|
||||||
|
ParticipantLocation::External => {
|
||||||
|
leader_in_this_app = false;
|
||||||
|
leader_in_this_project = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Some((leader_in_this_project, leader_in_this_app))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shared_screen_for_peer(
|
||||||
|
&self,
|
||||||
|
peer_id: PeerId,
|
||||||
|
pane: &View<Pane>,
|
||||||
|
cx: &mut ViewContext<Workspace>,
|
||||||
|
) -> Option<Box<dyn ItemHandle>> {
|
||||||
|
let (call, _) = self.active_call.as_ref()?;
|
||||||
|
let room = call.read(cx).room()?.read(cx);
|
||||||
|
let participant = room.remote_participant_for_peer_id(peer_id)?;
|
||||||
|
let track = participant.video_tracks.values().next()?.clone();
|
||||||
|
let user = participant.user.clone();
|
||||||
|
for item in pane.read(cx).items_of_type::<SharedScreen>() {
|
||||||
|
if item.read(cx).peer_id == peer_id {
|
||||||
|
return Some(Box::new(item));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(Box::new(cx.build_view(|cx| {
|
||||||
|
SharedScreen::new(&track, peer_id, user.clone(), cx)
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
fn room_id(&self, cx: &AppContext) -> Option<u64> {
|
||||||
|
Some(self.active_call.as_ref()?.0.read(cx).room()?.read(cx).id())
|
||||||
|
}
|
||||||
|
fn hang_up(&self, cx: &mut AppContext) -> Task<Result<()>> {
|
||||||
|
let Some((call, _)) = self.active_call.as_ref() else {
|
||||||
|
return Task::ready(Err(anyhow!("Cannot exit a call; not in a call")));
|
||||||
|
};
|
||||||
|
|
||||||
|
call.update(cx, |this, cx| this.hang_up(cx))
|
||||||
|
}
|
||||||
|
fn active_project(&self, cx: &AppContext) -> Option<WeakModel<Project>> {
|
||||||
|
ActiveCall::global(cx).read(cx).location().cloned()
|
||||||
|
}
|
||||||
|
fn invite(
|
||||||
|
&mut self,
|
||||||
|
called_user_id: u64,
|
||||||
|
initial_project: Option<Model<Project>>,
|
||||||
|
cx: &mut AppContext,
|
||||||
|
) -> Task<Result<()>> {
|
||||||
|
ActiveCall::global(cx).update(cx, |this, cx| {
|
||||||
|
this.invite(called_user_id, initial_project, cx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fn remote_participants(&self, cx: &AppContext) -> Option<Vec<(Arc<User>, PeerId)>> {
|
||||||
|
self.active_call
|
||||||
|
.as_ref()
|
||||||
|
.map(|call| {
|
||||||
|
call.0.read(cx).room().map(|room| {
|
||||||
|
room.read(cx)
|
||||||
|
.remote_participants()
|
||||||
|
.iter()
|
||||||
|
.map(|participant| {
|
||||||
|
(participant.1.user.clone(), participant.1.peer_id.clone())
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.flatten()
|
||||||
|
}
|
||||||
|
fn is_muted(&self, cx: &AppContext) -> Option<bool> {
|
||||||
|
self.active_call
|
||||||
|
.as_ref()
|
||||||
|
.map(|call| {
|
||||||
|
call.0
|
||||||
|
.read(cx)
|
||||||
|
.room()
|
||||||
|
.map(|room| room.read(cx).is_muted(cx))
|
||||||
|
})
|
||||||
|
.flatten()
|
||||||
|
}
|
||||||
|
fn toggle_mute(&self, cx: &mut AppContext) {
|
||||||
|
self.active_call.as_ref().map(|call| {
|
||||||
|
call.0.update(cx, |this, cx| {
|
||||||
|
this.room().map(|room| {
|
||||||
|
room.update(cx, |this, cx| {
|
||||||
|
this.toggle_mute(cx).log_err();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
fn toggle_screen_share(&self, cx: &mut AppContext) {
|
||||||
|
self.active_call.as_ref().map(|call| {
|
||||||
|
call.0.update(cx, |this, cx| {
|
||||||
|
this.room().map(|room| {
|
||||||
|
room.update(cx, |this, cx| {
|
||||||
|
if this.is_screen_sharing() {
|
||||||
|
this.unshare_screen(cx).log_err();
|
||||||
|
} else {
|
||||||
|
let t = this.share_screen(cx);
|
||||||
|
cx.spawn(move |_, _| async move {
|
||||||
|
t.await.log_err();
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
fn toggle_deafen(&self, cx: &mut AppContext) {
|
||||||
|
self.active_call.as_ref().map(|call| {
|
||||||
|
call.0.update(cx, |this, cx| {
|
||||||
|
this.room().map(|room| {
|
||||||
|
room.update(cx, |this, cx| {
|
||||||
|
this.toggle_deafen(cx).log_err();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
fn is_deafened(&self, cx: &AppContext) -> Option<bool> {
|
||||||
|
self.active_call
|
||||||
|
.as_ref()
|
||||||
|
.map(|call| {
|
||||||
|
call.0
|
||||||
|
.read(cx)
|
||||||
|
.room()
|
||||||
|
.map(|room| room.read(cx).is_deafened())
|
||||||
|
})
|
||||||
|
.flatten()
|
||||||
|
.flatten()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use gpui::TestAppContext;
|
use gpui::TestAppContext;
|
||||||
|
@ -4,7 +4,7 @@ use client::{proto, User};
|
|||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use gpui::WeakModel;
|
use gpui::WeakModel;
|
||||||
pub use live_kit_client::Frame;
|
pub use live_kit_client::Frame;
|
||||||
use live_kit_client::{RemoteAudioTrack, RemoteVideoTrack};
|
pub(crate) use live_kit_client::{RemoteAudioTrack, RemoteVideoTrack};
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
@ -1,7 +1,4 @@
|
|||||||
use crate::{
|
use crate::participant::{LocalParticipant, ParticipantLocation, RemoteParticipant};
|
||||||
call_settings::CallSettings,
|
|
||||||
participant::{LocalParticipant, ParticipantLocation, RemoteParticipant},
|
|
||||||
};
|
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use audio::{Audio, Sound};
|
use audio::{Audio, Sound};
|
||||||
use client::{
|
use client::{
|
||||||
@ -21,7 +18,6 @@ use live_kit_client::{
|
|||||||
};
|
};
|
||||||
use postage::{sink::Sink, stream::Stream, watch};
|
use postage::{sink::Sink, stream::Stream, watch};
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use settings::Settings;
|
|
||||||
use std::{future::Future, mem, sync::Arc, time::Duration};
|
use std::{future::Future, mem, sync::Arc, time::Duration};
|
||||||
use util::{post_inc, ResultExt, TryFutureExt};
|
use util::{post_inc, ResultExt, TryFutureExt};
|
||||||
|
|
||||||
@ -332,8 +328,10 @@ impl Room {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mute_on_join(cx: &AppContext) -> bool {
|
pub fn mute_on_join(_cx: &AppContext) -> bool {
|
||||||
CallSettings::get_global(cx).mute_on_join || client::IMPERSONATE_LOGIN.is_some()
|
// todo!() po: This should be uncommented, though then unmuting does not work
|
||||||
|
false
|
||||||
|
//CallSettings::get_global(cx).mute_on_join || client::IMPERSONATE_LOGIN.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_join_response(
|
fn from_join_response(
|
||||||
|
157
crates/call2/src/shared_screen.rs
Normal file
157
crates/call2/src/shared_screen.rs
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
use crate::participant::{Frame, RemoteVideoTrack};
|
||||||
|
use anyhow::Result;
|
||||||
|
use client::{proto::PeerId, User};
|
||||||
|
use futures::StreamExt;
|
||||||
|
use gpui::{
|
||||||
|
div, AppContext, Div, Element, EventEmitter, FocusHandle, FocusableView, ParentElement, Render,
|
||||||
|
SharedString, Task, View, ViewContext, VisualContext, WindowContext,
|
||||||
|
};
|
||||||
|
use std::sync::{Arc, Weak};
|
||||||
|
use workspace::{item::Item, ItemNavHistory, WorkspaceId};
|
||||||
|
|
||||||
|
pub enum Event {
|
||||||
|
Close,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SharedScreen {
|
||||||
|
track: Weak<RemoteVideoTrack>,
|
||||||
|
frame: Option<Frame>,
|
||||||
|
// temporary addition just to render something interactive.
|
||||||
|
current_frame_id: usize,
|
||||||
|
pub peer_id: PeerId,
|
||||||
|
user: Arc<User>,
|
||||||
|
nav_history: Option<ItemNavHistory>,
|
||||||
|
_maintain_frame: Task<Result<()>>,
|
||||||
|
focus: FocusHandle,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SharedScreen {
|
||||||
|
pub fn new(
|
||||||
|
track: &Arc<RemoteVideoTrack>,
|
||||||
|
peer_id: PeerId,
|
||||||
|
user: Arc<User>,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> Self {
|
||||||
|
cx.focus_handle();
|
||||||
|
let mut frames = track.frames();
|
||||||
|
Self {
|
||||||
|
track: Arc::downgrade(track),
|
||||||
|
frame: None,
|
||||||
|
peer_id,
|
||||||
|
user,
|
||||||
|
nav_history: Default::default(),
|
||||||
|
_maintain_frame: cx.spawn(|this, mut cx| async move {
|
||||||
|
while let Some(frame) = frames.next().await {
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
this.frame = Some(frame);
|
||||||
|
cx.notify();
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
this.update(&mut cx, |_, cx| cx.emit(Event::Close))?;
|
||||||
|
Ok(())
|
||||||
|
}),
|
||||||
|
focus: cx.focus_handle(),
|
||||||
|
current_frame_id: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<Event> for SharedScreen {}
|
||||||
|
impl EventEmitter<workspace::item::ItemEvent> for SharedScreen {}
|
||||||
|
|
||||||
|
impl FocusableView for SharedScreen {
|
||||||
|
fn focus_handle(&self, _: &AppContext) -> FocusHandle {
|
||||||
|
self.focus.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Render for SharedScreen {
|
||||||
|
type Element = Div;
|
||||||
|
fn render(&mut self, _: &mut ViewContext<Self>) -> Self::Element {
|
||||||
|
let frame = self.frame.clone();
|
||||||
|
let frame_id = self.current_frame_id;
|
||||||
|
self.current_frame_id = self.current_frame_id.wrapping_add(1);
|
||||||
|
div().children(frame.map(|_| {
|
||||||
|
ui::Label::new(frame_id.to_string()).color(ui::Color::Error)
|
||||||
|
// img().data(Arc::new(ImageData::new(image::ImageBuffer::new(
|
||||||
|
// frame.width() as u32,
|
||||||
|
// frame.height() as u32,
|
||||||
|
// ))))
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// impl View for SharedScreen {
|
||||||
|
// fn ui_name() -> &'static str {
|
||||||
|
// "SharedScreen"
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||||
|
// enum Focus {}
|
||||||
|
|
||||||
|
// let frame = self.frame.clone();
|
||||||
|
// MouseEventHandler::new::<Focus, _>(0, cx, |_, cx| {
|
||||||
|
// Canvas::new(move |bounds, _, _, cx| {
|
||||||
|
// if let Some(frame) = frame.clone() {
|
||||||
|
// let size = constrain_size_preserving_aspect_ratio(
|
||||||
|
// bounds.size(),
|
||||||
|
// vec2f(frame.width() as f32, frame.height() as f32),
|
||||||
|
// );
|
||||||
|
// let origin = bounds.origin() + (bounds.size() / 2.) - size / 2.;
|
||||||
|
// cx.scene().push_surface(gpui::platform::mac::Surface {
|
||||||
|
// bounds: RectF::new(origin, size),
|
||||||
|
// image_buffer: frame.image(),
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// .contained()
|
||||||
|
// .with_style(theme::current(cx).shared_screen)
|
||||||
|
// })
|
||||||
|
// .on_down(MouseButton::Left, |_, _, cx| cx.focus_parent())
|
||||||
|
// .into_any()
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
impl Item for SharedScreen {
|
||||||
|
fn tab_tooltip_text(&self, _: &AppContext) -> Option<SharedString> {
|
||||||
|
Some(format!("{}'s screen", self.user.github_login).into())
|
||||||
|
}
|
||||||
|
fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
|
if let Some(nav_history) = self.nav_history.as_mut() {
|
||||||
|
nav_history.push::<()>(None, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tab_content(&self, _: Option<usize>, _: &WindowContext<'_>) -> gpui::AnyElement {
|
||||||
|
div().child("Shared screen").into_any()
|
||||||
|
// Flex::row()
|
||||||
|
// .with_child(
|
||||||
|
// Svg::new("icons/desktop.svg")
|
||||||
|
// .with_color(style.label.text.color)
|
||||||
|
// .constrained()
|
||||||
|
// .with_width(style.type_icon_width)
|
||||||
|
// .aligned()
|
||||||
|
// .contained()
|
||||||
|
// .with_margin_right(style.spacing),
|
||||||
|
// )
|
||||||
|
// .with_child(
|
||||||
|
// Label::new(
|
||||||
|
// format!("{}'s screen", self.user.github_login),
|
||||||
|
// style.label.clone(),
|
||||||
|
// )
|
||||||
|
// .aligned(),
|
||||||
|
// )
|
||||||
|
// .into_any()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext<Self>) {
|
||||||
|
self.nav_history = Some(history);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clone_on_split(
|
||||||
|
&self,
|
||||||
|
_workspace_id: WorkspaceId,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> Option<View<Self>> {
|
||||||
|
let track = self.track.upgrade()?;
|
||||||
|
Some(cx.build_view(|cx| Self::new(&track, self.peer_id, self.user.clone(), cx)))
|
||||||
|
}
|
||||||
|
}
|
@ -109,6 +109,10 @@ pub enum ClickhouseEvent {
|
|||||||
virtual_memory_in_bytes: u64,
|
virtual_memory_in_bytes: u64,
|
||||||
milliseconds_since_first_event: i64,
|
milliseconds_since_first_event: i64,
|
||||||
},
|
},
|
||||||
|
App {
|
||||||
|
operation: &'static str,
|
||||||
|
milliseconds_since_first_event: i64,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
@ -168,13 +172,8 @@ impl Telemetry {
|
|||||||
let mut state = self.state.lock();
|
let mut state = self.state.lock();
|
||||||
state.installation_id = installation_id.map(|id| id.into());
|
state.installation_id = installation_id.map(|id| id.into());
|
||||||
state.session_id = Some(session_id.into());
|
state.session_id = Some(session_id.into());
|
||||||
let has_clickhouse_events = !state.clickhouse_events_queue.is_empty();
|
|
||||||
drop(state);
|
drop(state);
|
||||||
|
|
||||||
if has_clickhouse_events {
|
|
||||||
self.flush_clickhouse_events();
|
|
||||||
}
|
|
||||||
|
|
||||||
let this = self.clone();
|
let this = self.clone();
|
||||||
cx.spawn(|mut cx| async move {
|
cx.spawn(|mut cx| async move {
|
||||||
// Avoiding calling `System::new_all()`, as there have been crashes related to it
|
// Avoiding calling `System::new_all()`, as there have been crashes related to it
|
||||||
@ -256,7 +255,7 @@ impl Telemetry {
|
|||||||
milliseconds_since_first_event: self.milliseconds_since_first_event(),
|
milliseconds_since_first_event: self.milliseconds_since_first_event(),
|
||||||
};
|
};
|
||||||
|
|
||||||
self.report_clickhouse_event(event, telemetry_settings)
|
self.report_clickhouse_event(event, telemetry_settings, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn report_copilot_event(
|
pub fn report_copilot_event(
|
||||||
@ -273,7 +272,7 @@ impl Telemetry {
|
|||||||
milliseconds_since_first_event: self.milliseconds_since_first_event(),
|
milliseconds_since_first_event: self.milliseconds_since_first_event(),
|
||||||
};
|
};
|
||||||
|
|
||||||
self.report_clickhouse_event(event, telemetry_settings)
|
self.report_clickhouse_event(event, telemetry_settings, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn report_assistant_event(
|
pub fn report_assistant_event(
|
||||||
@ -290,7 +289,7 @@ impl Telemetry {
|
|||||||
milliseconds_since_first_event: self.milliseconds_since_first_event(),
|
milliseconds_since_first_event: self.milliseconds_since_first_event(),
|
||||||
};
|
};
|
||||||
|
|
||||||
self.report_clickhouse_event(event, telemetry_settings)
|
self.report_clickhouse_event(event, telemetry_settings, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn report_call_event(
|
pub fn report_call_event(
|
||||||
@ -307,7 +306,7 @@ impl Telemetry {
|
|||||||
milliseconds_since_first_event: self.milliseconds_since_first_event(),
|
milliseconds_since_first_event: self.milliseconds_since_first_event(),
|
||||||
};
|
};
|
||||||
|
|
||||||
self.report_clickhouse_event(event, telemetry_settings)
|
self.report_clickhouse_event(event, telemetry_settings, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn report_cpu_event(
|
pub fn report_cpu_event(
|
||||||
@ -322,7 +321,7 @@ impl Telemetry {
|
|||||||
milliseconds_since_first_event: self.milliseconds_since_first_event(),
|
milliseconds_since_first_event: self.milliseconds_since_first_event(),
|
||||||
};
|
};
|
||||||
|
|
||||||
self.report_clickhouse_event(event, telemetry_settings)
|
self.report_clickhouse_event(event, telemetry_settings, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn report_memory_event(
|
pub fn report_memory_event(
|
||||||
@ -337,7 +336,21 @@ impl Telemetry {
|
|||||||
milliseconds_since_first_event: self.milliseconds_since_first_event(),
|
milliseconds_since_first_event: self.milliseconds_since_first_event(),
|
||||||
};
|
};
|
||||||
|
|
||||||
self.report_clickhouse_event(event, telemetry_settings)
|
self.report_clickhouse_event(event, telemetry_settings, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// app_events are called at app open and app close, so flush is set to immediately send
|
||||||
|
pub fn report_app_event(
|
||||||
|
self: &Arc<Self>,
|
||||||
|
telemetry_settings: TelemetrySettings,
|
||||||
|
operation: &'static str,
|
||||||
|
) {
|
||||||
|
let event = ClickhouseEvent::App {
|
||||||
|
operation,
|
||||||
|
milliseconds_since_first_event: self.milliseconds_since_first_event(),
|
||||||
|
};
|
||||||
|
|
||||||
|
self.report_clickhouse_event(event, telemetry_settings, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn milliseconds_since_first_event(&self) -> i64 {
|
fn milliseconds_since_first_event(&self) -> i64 {
|
||||||
@ -358,6 +371,7 @@ impl Telemetry {
|
|||||||
self: &Arc<Self>,
|
self: &Arc<Self>,
|
||||||
event: ClickhouseEvent,
|
event: ClickhouseEvent,
|
||||||
telemetry_settings: TelemetrySettings,
|
telemetry_settings: TelemetrySettings,
|
||||||
|
immediate_flush: bool,
|
||||||
) {
|
) {
|
||||||
if !telemetry_settings.metrics {
|
if !telemetry_settings.metrics {
|
||||||
return;
|
return;
|
||||||
@ -370,7 +384,7 @@ impl Telemetry {
|
|||||||
.push(ClickhouseEventWrapper { signed_in, event });
|
.push(ClickhouseEventWrapper { signed_in, event });
|
||||||
|
|
||||||
if state.installation_id.is_some() {
|
if state.installation_id.is_some() {
|
||||||
if state.clickhouse_events_queue.len() >= MAX_QUEUE_LEN {
|
if immediate_flush || state.clickhouse_events_queue.len() >= MAX_QUEUE_LEN {
|
||||||
drop(state);
|
drop(state);
|
||||||
self.flush_clickhouse_events();
|
self.flush_clickhouse_events();
|
||||||
} else {
|
} else {
|
||||||
|
@ -382,7 +382,7 @@ impl settings::Settings for TelemetrySettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Client {
|
impl Client {
|
||||||
pub fn new(http: Arc<dyn HttpClient>, cx: &AppContext) -> Arc<Self> {
|
pub fn new(http: Arc<dyn HttpClient>, cx: &mut AppContext) -> Arc<Self> {
|
||||||
Arc::new(Self {
|
Arc::new(Self {
|
||||||
id: AtomicU64::new(0),
|
id: AtomicU64::new(0),
|
||||||
peer: Peer::new(0),
|
peer: Peer::new(0),
|
||||||
@ -551,7 +551,6 @@ impl Client {
|
|||||||
F: 'static + Future<Output = Result<()>>,
|
F: 'static + Future<Output = Result<()>>,
|
||||||
{
|
{
|
||||||
let message_type_id = TypeId::of::<M>();
|
let message_type_id = TypeId::of::<M>();
|
||||||
|
|
||||||
let mut state = self.state.write();
|
let mut state = self.state.write();
|
||||||
state
|
state
|
||||||
.models_by_message_type
|
.models_by_message_type
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
use crate::{TelemetrySettings, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
|
use crate::{TelemetrySettings, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
|
use futures::Future;
|
||||||
use gpui::{serde_json, AppContext, AppMetadata, BackgroundExecutor, Task};
|
use gpui::{serde_json, AppContext, AppMetadata, BackgroundExecutor, Task};
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
@ -107,6 +108,10 @@ pub enum ClickhouseEvent {
|
|||||||
virtual_memory_in_bytes: u64,
|
virtual_memory_in_bytes: u64,
|
||||||
milliseconds_since_first_event: i64,
|
milliseconds_since_first_event: i64,
|
||||||
},
|
},
|
||||||
|
App {
|
||||||
|
operation: &'static str,
|
||||||
|
milliseconds_since_first_event: i64,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
@ -122,12 +127,13 @@ const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(1);
|
|||||||
const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(30);
|
const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(30);
|
||||||
|
|
||||||
impl Telemetry {
|
impl Telemetry {
|
||||||
pub fn new(client: Arc<dyn HttpClient>, cx: &AppContext) -> Arc<Self> {
|
pub fn new(client: Arc<dyn HttpClient>, cx: &mut AppContext) -> Arc<Self> {
|
||||||
let release_channel = if cx.has_global::<ReleaseChannel>() {
|
let release_channel = if cx.has_global::<ReleaseChannel>() {
|
||||||
Some(cx.global::<ReleaseChannel>().display_name())
|
Some(cx.global::<ReleaseChannel>().display_name())
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: Replace all hardware stuff with nested SystemSpecs json
|
// TODO: Replace all hardware stuff with nested SystemSpecs json
|
||||||
let this = Arc::new(Self {
|
let this = Arc::new(Self {
|
||||||
http_client: client,
|
http_client: client,
|
||||||
@ -147,9 +153,30 @@ impl Telemetry {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// We should only ever have one instance of Telemetry, leak the subscription to keep it alive
|
||||||
|
// rather than store in TelemetryState, complicating spawn as subscriptions are not Send
|
||||||
|
std::mem::forget(cx.on_app_quit({
|
||||||
|
let this = this.clone();
|
||||||
|
move |cx| this.shutdown_telemetry(cx)
|
||||||
|
}));
|
||||||
|
|
||||||
this
|
this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
fn shutdown_telemetry(self: &Arc<Self>, _: &mut AppContext) -> impl Future<Output = ()> {
|
||||||
|
Task::ready(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip calling this function in tests.
|
||||||
|
// TestAppContext ends up calling this function on shutdown and it panics when trying to find the TelemetrySettings
|
||||||
|
#[cfg(not(any(test, feature = "test-support")))]
|
||||||
|
fn shutdown_telemetry(self: &Arc<Self>, cx: &mut AppContext) -> impl Future<Output = ()> {
|
||||||
|
let telemetry_settings = TelemetrySettings::get_global(cx).clone();
|
||||||
|
self.report_app_event(telemetry_settings, "close");
|
||||||
|
Task::ready(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn log_file_path(&self) -> Option<PathBuf> {
|
pub fn log_file_path(&self) -> Option<PathBuf> {
|
||||||
Some(self.state.lock().log_file.as_ref()?.path().to_path_buf())
|
Some(self.state.lock().log_file.as_ref()?.path().to_path_buf())
|
||||||
}
|
}
|
||||||
@ -163,13 +190,8 @@ impl Telemetry {
|
|||||||
let mut state = self.state.lock();
|
let mut state = self.state.lock();
|
||||||
state.installation_id = installation_id.map(|id| id.into());
|
state.installation_id = installation_id.map(|id| id.into());
|
||||||
state.session_id = Some(session_id.into());
|
state.session_id = Some(session_id.into());
|
||||||
let has_clickhouse_events = !state.clickhouse_events_queue.is_empty();
|
|
||||||
drop(state);
|
drop(state);
|
||||||
|
|
||||||
if has_clickhouse_events {
|
|
||||||
self.flush_clickhouse_events();
|
|
||||||
}
|
|
||||||
|
|
||||||
let this = self.clone();
|
let this = self.clone();
|
||||||
cx.spawn(|cx| async move {
|
cx.spawn(|cx| async move {
|
||||||
// Avoiding calling `System::new_all()`, as there have been crashes related to it
|
// Avoiding calling `System::new_all()`, as there have been crashes related to it
|
||||||
@ -257,7 +279,7 @@ impl Telemetry {
|
|||||||
milliseconds_since_first_event: self.milliseconds_since_first_event(),
|
milliseconds_since_first_event: self.milliseconds_since_first_event(),
|
||||||
};
|
};
|
||||||
|
|
||||||
self.report_clickhouse_event(event, telemetry_settings)
|
self.report_clickhouse_event(event, telemetry_settings, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn report_copilot_event(
|
pub fn report_copilot_event(
|
||||||
@ -274,7 +296,7 @@ impl Telemetry {
|
|||||||
milliseconds_since_first_event: self.milliseconds_since_first_event(),
|
milliseconds_since_first_event: self.milliseconds_since_first_event(),
|
||||||
};
|
};
|
||||||
|
|
||||||
self.report_clickhouse_event(event, telemetry_settings)
|
self.report_clickhouse_event(event, telemetry_settings, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn report_assistant_event(
|
pub fn report_assistant_event(
|
||||||
@ -291,7 +313,7 @@ impl Telemetry {
|
|||||||
milliseconds_since_first_event: self.milliseconds_since_first_event(),
|
milliseconds_since_first_event: self.milliseconds_since_first_event(),
|
||||||
};
|
};
|
||||||
|
|
||||||
self.report_clickhouse_event(event, telemetry_settings)
|
self.report_clickhouse_event(event, telemetry_settings, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn report_call_event(
|
pub fn report_call_event(
|
||||||
@ -308,7 +330,7 @@ impl Telemetry {
|
|||||||
milliseconds_since_first_event: self.milliseconds_since_first_event(),
|
milliseconds_since_first_event: self.milliseconds_since_first_event(),
|
||||||
};
|
};
|
||||||
|
|
||||||
self.report_clickhouse_event(event, telemetry_settings)
|
self.report_clickhouse_event(event, telemetry_settings, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn report_cpu_event(
|
pub fn report_cpu_event(
|
||||||
@ -323,7 +345,7 @@ impl Telemetry {
|
|||||||
milliseconds_since_first_event: self.milliseconds_since_first_event(),
|
milliseconds_since_first_event: self.milliseconds_since_first_event(),
|
||||||
};
|
};
|
||||||
|
|
||||||
self.report_clickhouse_event(event, telemetry_settings)
|
self.report_clickhouse_event(event, telemetry_settings, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn report_memory_event(
|
pub fn report_memory_event(
|
||||||
@ -338,7 +360,21 @@ impl Telemetry {
|
|||||||
milliseconds_since_first_event: self.milliseconds_since_first_event(),
|
milliseconds_since_first_event: self.milliseconds_since_first_event(),
|
||||||
};
|
};
|
||||||
|
|
||||||
self.report_clickhouse_event(event, telemetry_settings)
|
self.report_clickhouse_event(event, telemetry_settings, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// app_events are called at app open and app close, so flush is set to immediately send
|
||||||
|
pub fn report_app_event(
|
||||||
|
self: &Arc<Self>,
|
||||||
|
telemetry_settings: TelemetrySettings,
|
||||||
|
operation: &'static str,
|
||||||
|
) {
|
||||||
|
let event = ClickhouseEvent::App {
|
||||||
|
operation,
|
||||||
|
milliseconds_since_first_event: self.milliseconds_since_first_event(),
|
||||||
|
};
|
||||||
|
|
||||||
|
self.report_clickhouse_event(event, telemetry_settings, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn milliseconds_since_first_event(&self) -> i64 {
|
fn milliseconds_since_first_event(&self) -> i64 {
|
||||||
@ -359,6 +395,7 @@ impl Telemetry {
|
|||||||
self: &Arc<Self>,
|
self: &Arc<Self>,
|
||||||
event: ClickhouseEvent,
|
event: ClickhouseEvent,
|
||||||
telemetry_settings: TelemetrySettings,
|
telemetry_settings: TelemetrySettings,
|
||||||
|
immediate_flush: bool,
|
||||||
) {
|
) {
|
||||||
if !telemetry_settings.metrics {
|
if !telemetry_settings.metrics {
|
||||||
return;
|
return;
|
||||||
@ -371,7 +408,7 @@ impl Telemetry {
|
|||||||
.push(ClickhouseEventWrapper { signed_in, event });
|
.push(ClickhouseEventWrapper { signed_in, event });
|
||||||
|
|
||||||
if state.installation_id.is_some() {
|
if state.installation_id.is_some() {
|
||||||
if state.clickhouse_events_queue.len() >= MAX_QUEUE_LEN {
|
if immediate_flush || state.clickhouse_events_queue.len() >= MAX_QUEUE_LEN {
|
||||||
drop(state);
|
drop(state);
|
||||||
self.flush_clickhouse_events();
|
self.flush_clickhouse_events();
|
||||||
} else {
|
} else {
|
||||||
|
@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
|
|||||||
default-run = "collab"
|
default-run = "collab"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
name = "collab"
|
name = "collab"
|
||||||
version = "0.28.0"
|
version = "0.29.0"
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
|
@ -10,7 +10,7 @@ publish = false
|
|||||||
name = "collab2"
|
name = "collab2"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "seed"
|
name = "seed2"
|
||||||
required-features = ["seed-support"]
|
required-features = ["seed-support"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
@ -149,7 +149,7 @@ impl TestServer {
|
|||||||
.user_id
|
.user_id
|
||||||
};
|
};
|
||||||
let client_name = name.to_string();
|
let client_name = name.to_string();
|
||||||
let mut client = cx.read(|cx| Client::new(http.clone(), cx));
|
let mut client = cx.update(|cx| Client::new(http.clone(), cx));
|
||||||
let server = self.server.clone();
|
let server = self.server.clone();
|
||||||
let db = self.app_state.db.clone();
|
let db = self.app_state.db.clone();
|
||||||
let connection_killers = self.connection_killers.clone();
|
let connection_killers = self.connection_killers.clone();
|
||||||
@ -221,6 +221,7 @@ impl TestServer {
|
|||||||
fs: fs.clone(),
|
fs: fs.clone(),
|
||||||
build_window_options: |_, _, _| Default::default(),
|
build_window_options: |_, _, _| Default::default(),
|
||||||
node_runtime: FakeNodeRuntime::new(),
|
node_runtime: FakeNodeRuntime::new(),
|
||||||
|
call_factory: |_, _| Box::new(workspace::TestCallHandler),
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
|
@ -157,15 +157,17 @@ const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel";
|
|||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use client::{Client, Contact, UserStore};
|
||||||
use db::kvp::KEY_VALUE_STORE;
|
use db::kvp::KEY_VALUE_STORE;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, div, serde_json, AppContext, AsyncWindowContext, Div, EventEmitter, FocusHandle,
|
actions, div, serde_json, AppContext, AsyncWindowContext, Div, EventEmitter, FocusHandle,
|
||||||
Focusable, FocusableView, InteractiveElement, ParentElement, Render, View, ViewContext,
|
Focusable, FocusableView, InteractiveElement, Model, ParentElement, Render, Styled, View,
|
||||||
VisualContext, WeakView,
|
ViewContext, VisualContext, WeakView,
|
||||||
};
|
};
|
||||||
use project::Fs;
|
use project::Fs;
|
||||||
use serde_derive::{Deserialize, Serialize};
|
use serde_derive::{Deserialize, Serialize};
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
|
use ui::{h_stack, Avatar, Label};
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
use workspace::{
|
use workspace::{
|
||||||
dock::{DockPosition, Panel, PanelEvent},
|
dock::{DockPosition, Panel, PanelEvent},
|
||||||
@ -299,8 +301,8 @@ pub struct CollabPanel {
|
|||||||
// channel_editing_state: Option<ChannelEditingState>,
|
// channel_editing_state: Option<ChannelEditingState>,
|
||||||
// entries: Vec<ListEntry>,
|
// entries: Vec<ListEntry>,
|
||||||
// selection: Option<usize>,
|
// selection: Option<usize>,
|
||||||
// user_store: ModelHandle<UserStore>,
|
user_store: Model<UserStore>,
|
||||||
// client: Arc<Client>,
|
_client: Arc<Client>,
|
||||||
// channel_store: ModelHandle<ChannelStore>,
|
// channel_store: ModelHandle<ChannelStore>,
|
||||||
// project: ModelHandle<Project>,
|
// project: ModelHandle<Project>,
|
||||||
// match_candidates: Vec<StringMatchCandidate>,
|
// match_candidates: Vec<StringMatchCandidate>,
|
||||||
@ -595,7 +597,7 @@ impl CollabPanel {
|
|||||||
// entries: Vec::default(),
|
// entries: Vec::default(),
|
||||||
// channel_editing_state: None,
|
// channel_editing_state: None,
|
||||||
// selection: None,
|
// selection: None,
|
||||||
// user_store: workspace.user_store().clone(),
|
user_store: workspace.user_store().clone(),
|
||||||
// channel_store: ChannelStore::global(cx),
|
// channel_store: ChannelStore::global(cx),
|
||||||
// project: workspace.project().clone(),
|
// project: workspace.project().clone(),
|
||||||
// subscriptions: Vec::default(),
|
// subscriptions: Vec::default(),
|
||||||
@ -603,7 +605,7 @@ impl CollabPanel {
|
|||||||
// collapsed_sections: vec![Section::Offline],
|
// collapsed_sections: vec![Section::Offline],
|
||||||
// collapsed_channels: Vec::default(),
|
// collapsed_channels: Vec::default(),
|
||||||
_workspace: workspace.weak_handle(),
|
_workspace: workspace.weak_handle(),
|
||||||
// client: workspace.app_state().client.clone(),
|
_client: workspace.app_state().client.clone(),
|
||||||
// context_menu_on_selected: true,
|
// context_menu_on_selected: true,
|
||||||
// drag_target_channel: ChannelDragTarget::None,
|
// drag_target_channel: ChannelDragTarget::None,
|
||||||
// list_state,
|
// list_state,
|
||||||
@ -663,6 +665,9 @@ impl CollabPanel {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn contacts(&self, cx: &AppContext) -> Option<Vec<Arc<Contact>>> {
|
||||||
|
Some(self.user_store.read(cx).contacts().to_owned())
|
||||||
|
}
|
||||||
pub async fn load(
|
pub async fn load(
|
||||||
workspace: WeakView<Workspace>,
|
workspace: WeakView<Workspace>,
|
||||||
mut cx: AsyncWindowContext,
|
mut cx: AsyncWindowContext,
|
||||||
@ -3297,11 +3302,38 @@ impl CollabPanel {
|
|||||||
impl Render for CollabPanel {
|
impl Render for CollabPanel {
|
||||||
type Element = Focusable<Div>;
|
type Element = Focusable<Div>;
|
||||||
|
|
||||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||||
|
let contacts = self.contacts(cx).unwrap_or_default();
|
||||||
|
let workspace = self._workspace.clone();
|
||||||
div()
|
div()
|
||||||
.key_context("CollabPanel")
|
.key_context("CollabPanel")
|
||||||
.track_focus(&self.focus_handle)
|
.track_focus(&self.focus_handle)
|
||||||
.child("COLLAB PANEL")
|
.children(contacts.into_iter().map(|contact| {
|
||||||
|
let id = contact.user.id;
|
||||||
|
h_stack()
|
||||||
|
.p_2()
|
||||||
|
.gap_2()
|
||||||
|
.children(
|
||||||
|
contact
|
||||||
|
.user
|
||||||
|
.avatar
|
||||||
|
.as_ref()
|
||||||
|
.map(|avatar| Avatar::data(avatar.clone())),
|
||||||
|
)
|
||||||
|
.child(Label::new(contact.user.github_login.clone()))
|
||||||
|
.on_mouse_down(gpui::MouseButton::Left, {
|
||||||
|
let workspace = workspace.clone();
|
||||||
|
move |_, cx| {
|
||||||
|
workspace
|
||||||
|
.update(cx, |this, cx| {
|
||||||
|
this.call_state()
|
||||||
|
.invite(id, None, cx)
|
||||||
|
.detach_and_log_err(cx)
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,15 +31,18 @@ use std::sync::Arc;
|
|||||||
use call::ActiveCall;
|
use call::ActiveCall;
|
||||||
use client::{Client, UserStore};
|
use client::{Client, UserStore};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, px, rems, AppContext, Div, InteractiveElement, Model, ParentElement, Render, RenderOnce,
|
div, px, rems, AppContext, Div, Element, InteractiveElement, IntoElement, Model, MouseButton,
|
||||||
Stateful, StatefulInteractiveElement, Styled, Subscription, ViewContext, VisualContext,
|
ParentElement, Render, RenderOnce, Stateful, StatefulInteractiveElement, Styled, Subscription,
|
||||||
WeakView, WindowBounds,
|
ViewContext, VisualContext, WeakView, WindowBounds,
|
||||||
};
|
};
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::{h_stack, Button, ButtonVariant, KeyBinding, Label, TextColor, Tooltip};
|
use ui::{h_stack, Avatar, Button, ButtonVariant, Color, IconButton, KeyBinding, Tooltip};
|
||||||
|
use util::ResultExt;
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
|
||||||
|
use crate::face_pile::FacePile;
|
||||||
|
|
||||||
// const MAX_PROJECT_NAME_LENGTH: usize = 40;
|
// const MAX_PROJECT_NAME_LENGTH: usize = 40;
|
||||||
// const MAX_BRANCH_NAME_LENGTH: usize = 40;
|
// const MAX_BRANCH_NAME_LENGTH: usize = 40;
|
||||||
|
|
||||||
@ -85,6 +88,41 @@ impl Render for CollabTitlebarItem {
|
|||||||
type Element = Stateful<Div>;
|
type Element = Stateful<Div>;
|
||||||
|
|
||||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||||
|
let is_in_room = self
|
||||||
|
.workspace
|
||||||
|
.update(cx, |this, cx| this.call_state().is_in_room(cx))
|
||||||
|
.unwrap_or_default();
|
||||||
|
let is_shared = is_in_room && self.project.read(cx).is_shared();
|
||||||
|
let current_user = self.user_store.read(cx).current_user();
|
||||||
|
let client = self.client.clone();
|
||||||
|
let users = self
|
||||||
|
.workspace
|
||||||
|
.update(cx, |this, cx| this.call_state().remote_participants(cx))
|
||||||
|
.log_err()
|
||||||
|
.flatten();
|
||||||
|
let mic_icon = if self
|
||||||
|
.workspace
|
||||||
|
.update(cx, |this, cx| this.call_state().is_muted(cx))
|
||||||
|
.log_err()
|
||||||
|
.flatten()
|
||||||
|
.unwrap_or_default()
|
||||||
|
{
|
||||||
|
ui::Icon::MicMute
|
||||||
|
} else {
|
||||||
|
ui::Icon::Mic
|
||||||
|
};
|
||||||
|
let speakers_icon = if self
|
||||||
|
.workspace
|
||||||
|
.update(cx, |this, cx| this.call_state().is_deafened(cx))
|
||||||
|
.log_err()
|
||||||
|
.flatten()
|
||||||
|
.unwrap_or_default()
|
||||||
|
{
|
||||||
|
ui::Icon::AudioOff
|
||||||
|
} else {
|
||||||
|
ui::Icon::AudioOn
|
||||||
|
};
|
||||||
|
let workspace = self.workspace.clone();
|
||||||
h_stack()
|
h_stack()
|
||||||
.id("titlebar")
|
.id("titlebar")
|
||||||
.justify_between()
|
.justify_between()
|
||||||
@ -111,17 +149,21 @@ impl Render for CollabTitlebarItem {
|
|||||||
// TODO - Add player menu
|
// TODO - Add player menu
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
|
.border()
|
||||||
|
.border_color(gpui::red())
|
||||||
.id("project_owner_indicator")
|
.id("project_owner_indicator")
|
||||||
.child(
|
.child(
|
||||||
Button::new("player")
|
Button::new("player")
|
||||||
.variant(ButtonVariant::Ghost)
|
.variant(ButtonVariant::Ghost)
|
||||||
.color(Some(TextColor::Player(0))),
|
.color(Some(Color::Player(0))),
|
||||||
)
|
)
|
||||||
.tooltip(move |cx| Tooltip::text("Toggle following", cx)),
|
.tooltip(move |cx| Tooltip::text("Toggle following", cx)),
|
||||||
)
|
)
|
||||||
// TODO - Add project menu
|
// TODO - Add project menu
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
|
.border()
|
||||||
|
.border_color(gpui::red())
|
||||||
.id("titlebar_project_menu_button")
|
.id("titlebar_project_menu_button")
|
||||||
.child(Button::new("project_name").variant(ButtonVariant::Ghost))
|
.child(Button::new("project_name").variant(ButtonVariant::Ghost))
|
||||||
.tooltip(move |cx| Tooltip::text("Recent Projects", cx)),
|
.tooltip(move |cx| Tooltip::text("Recent Projects", cx)),
|
||||||
@ -129,11 +171,13 @@ impl Render for CollabTitlebarItem {
|
|||||||
// TODO - Add git menu
|
// TODO - Add git menu
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
|
.border()
|
||||||
|
.border_color(gpui::red())
|
||||||
.id("titlebar_git_menu_button")
|
.id("titlebar_git_menu_button")
|
||||||
.child(
|
.child(
|
||||||
Button::new("branch_name")
|
Button::new("branch_name")
|
||||||
.variant(ButtonVariant::Ghost)
|
.variant(ButtonVariant::Ghost)
|
||||||
.color(Some(TextColor::Muted)),
|
.color(Some(Color::Muted)),
|
||||||
)
|
)
|
||||||
.tooltip(move |cx| {
|
.tooltip(move |cx| {
|
||||||
cx.build_view(|_| {
|
cx.build_view(|_| {
|
||||||
@ -149,8 +193,111 @@ impl Render for CollabTitlebarItem {
|
|||||||
.into()
|
.into()
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
) // self.titlebar_item
|
)
|
||||||
.child(h_stack().child(Label::new("Right side titlebar item")))
|
.when_some(
|
||||||
|
users.zip(current_user.clone()),
|
||||||
|
|this, (remote_participants, current_user)| {
|
||||||
|
let mut pile = FacePile::default();
|
||||||
|
pile.extend(
|
||||||
|
current_user
|
||||||
|
.avatar
|
||||||
|
.clone()
|
||||||
|
.map(|avatar| {
|
||||||
|
div().child(Avatar::data(avatar.clone())).into_any_element()
|
||||||
|
})
|
||||||
|
.into_iter()
|
||||||
|
.chain(remote_participants.into_iter().flat_map(|(user, peer_id)| {
|
||||||
|
user.avatar.as_ref().map(|avatar| {
|
||||||
|
div()
|
||||||
|
.child(
|
||||||
|
Avatar::data(avatar.clone()).into_element().into_any(),
|
||||||
|
)
|
||||||
|
.on_mouse_down(MouseButton::Left, {
|
||||||
|
let workspace = workspace.clone();
|
||||||
|
move |_, cx| {
|
||||||
|
workspace
|
||||||
|
.update(cx, |this, cx| {
|
||||||
|
this.open_shared_screen(peer_id, cx);
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.into_any_element()
|
||||||
|
})
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
this.child(pile.render(cx))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.child(div().flex_1())
|
||||||
|
.when(is_in_room, |this| {
|
||||||
|
this.child(
|
||||||
|
h_stack()
|
||||||
|
.child(
|
||||||
|
h_stack()
|
||||||
|
.child(Button::new(if is_shared { "Unshare" } else { "Share" }))
|
||||||
|
.child(IconButton::new("leave-call", ui::Icon::Exit).on_click({
|
||||||
|
let workspace = workspace.clone();
|
||||||
|
move |_, cx| {
|
||||||
|
workspace
|
||||||
|
.update(cx, |this, cx| {
|
||||||
|
this.call_state().hang_up(cx).detach();
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
h_stack()
|
||||||
|
.child(IconButton::new("mute-microphone", mic_icon).on_click({
|
||||||
|
let workspace = workspace.clone();
|
||||||
|
move |_, cx| {
|
||||||
|
workspace
|
||||||
|
.update(cx, |this, cx| {
|
||||||
|
this.call_state().toggle_mute(cx);
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.child(IconButton::new("mute-sound", speakers_icon).on_click({
|
||||||
|
let workspace = workspace.clone();
|
||||||
|
move |_, cx| {
|
||||||
|
workspace
|
||||||
|
.update(cx, |this, cx| {
|
||||||
|
this.call_state().toggle_deafen(cx);
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.child(IconButton::new("screen-share", ui::Icon::Screen).on_click(
|
||||||
|
move |_, cx| {
|
||||||
|
workspace
|
||||||
|
.update(cx, |this, cx| {
|
||||||
|
this.call_state().toggle_screen_share(cx);
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
},
|
||||||
|
))
|
||||||
|
.pl_2(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.map(|this| {
|
||||||
|
if let Some(user) = current_user {
|
||||||
|
this.when_some(user.avatar.clone(), |this, avatar| {
|
||||||
|
this.child(ui::Avatar::data(avatar))
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.child(Button::new("Sign in").on_click(move |_, cx| {
|
||||||
|
let client = client.clone();
|
||||||
|
cx.spawn(move |cx| async move {
|
||||||
|
client.authenticate_and_connect(true, &cx).await?;
|
||||||
|
Ok::<(), anyhow::Error>(())
|
||||||
|
})
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,11 +7,14 @@ pub mod notification_panel;
|
|||||||
pub mod notifications;
|
pub mod notifications;
|
||||||
mod panel_settings;
|
mod panel_settings;
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::{rc::Rc, sync::Arc};
|
||||||
|
|
||||||
pub use collab_panel::CollabPanel;
|
pub use collab_panel::CollabPanel;
|
||||||
pub use collab_titlebar_item::CollabTitlebarItem;
|
pub use collab_titlebar_item::CollabTitlebarItem;
|
||||||
use gpui::AppContext;
|
use gpui::{
|
||||||
|
point, AppContext, GlobalPixels, Pixels, PlatformDisplay, Size, WindowBounds, WindowKind,
|
||||||
|
WindowOptions,
|
||||||
|
};
|
||||||
pub use panel_settings::{
|
pub use panel_settings::{
|
||||||
ChatPanelSettings, CollaborationPanelSettings, NotificationPanelSettings,
|
ChatPanelSettings, CollaborationPanelSettings, NotificationPanelSettings,
|
||||||
};
|
};
|
||||||
@ -23,7 +26,7 @@ use workspace::AppState;
|
|||||||
// [ToggleScreenSharing, ToggleMute, ToggleDeafen, LeaveCall]
|
// [ToggleScreenSharing, ToggleMute, ToggleDeafen, LeaveCall]
|
||||||
// );
|
// );
|
||||||
|
|
||||||
pub fn init(_app_state: &Arc<AppState>, cx: &mut AppContext) {
|
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||||
CollaborationPanelSettings::register(cx);
|
CollaborationPanelSettings::register(cx);
|
||||||
ChatPanelSettings::register(cx);
|
ChatPanelSettings::register(cx);
|
||||||
NotificationPanelSettings::register(cx);
|
NotificationPanelSettings::register(cx);
|
||||||
@ -32,7 +35,7 @@ pub fn init(_app_state: &Arc<AppState>, cx: &mut AppContext) {
|
|||||||
collab_titlebar_item::init(cx);
|
collab_titlebar_item::init(cx);
|
||||||
collab_panel::init(cx);
|
collab_panel::init(cx);
|
||||||
// chat_panel::init(cx);
|
// chat_panel::init(cx);
|
||||||
// notifications::init(&app_state, cx);
|
notifications::init(&app_state, cx);
|
||||||
|
|
||||||
// cx.add_global_action(toggle_screen_sharing);
|
// cx.add_global_action(toggle_screen_sharing);
|
||||||
// cx.add_global_action(toggle_mute);
|
// cx.add_global_action(toggle_mute);
|
||||||
@ -95,31 +98,36 @@ pub fn init(_app_state: &Arc<AppState>, cx: &mut AppContext) {
|
|||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// fn notification_window_options(
|
fn notification_window_options(
|
||||||
// screen: Rc<dyn Screen>,
|
screen: Rc<dyn PlatformDisplay>,
|
||||||
// window_size: Vector2F,
|
window_size: Size<Pixels>,
|
||||||
// ) -> WindowOptions<'static> {
|
) -> WindowOptions {
|
||||||
// const NOTIFICATION_PADDING: f32 = 16.;
|
let notification_margin_width = GlobalPixels::from(16.);
|
||||||
|
let notification_margin_height = GlobalPixels::from(-0.) - GlobalPixels::from(48.);
|
||||||
|
|
||||||
// let screen_bounds = screen.content_bounds();
|
let screen_bounds = screen.bounds();
|
||||||
// WindowOptions {
|
let size: Size<GlobalPixels> = window_size.into();
|
||||||
// bounds: WindowBounds::Fixed(RectF::new(
|
|
||||||
// screen_bounds.upper_right()
|
// todo!() use content bounds instead of screen.bounds and get rid of magics in point's 2nd argument.
|
||||||
// + vec2f(
|
let bounds = gpui::Bounds::<GlobalPixels> {
|
||||||
// -NOTIFICATION_PADDING - window_size.x(),
|
origin: screen_bounds.upper_right()
|
||||||
// NOTIFICATION_PADDING,
|
- point(
|
||||||
// ),
|
size.width + notification_margin_width,
|
||||||
// window_size,
|
notification_margin_height,
|
||||||
// )),
|
),
|
||||||
// titlebar: None,
|
size: window_size.into(),
|
||||||
// center: false,
|
};
|
||||||
// focus: false,
|
WindowOptions {
|
||||||
// show: true,
|
bounds: WindowBounds::Fixed(bounds),
|
||||||
// kind: WindowKind::PopUp,
|
titlebar: None,
|
||||||
// is_movable: false,
|
center: false,
|
||||||
// screen: Some(screen),
|
focus: false,
|
||||||
// }
|
show: true,
|
||||||
// }
|
kind: WindowKind::PopUp,
|
||||||
|
is_movable: false,
|
||||||
|
display_id: Some(screen.id()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// fn render_avatar<T: 'static>(
|
// fn render_avatar<T: 'static>(
|
||||||
// avatar: Option<Arc<ImageData>>,
|
// avatar: Option<Arc<ImageData>>,
|
||||||
|
@ -1,54 +1,48 @@
|
|||||||
// use std::ops::Range;
|
use gpui::{
|
||||||
|
div, AnyElement, Div, IntoElement as _, ParentElement as _, RenderOnce, Styled, WindowContext,
|
||||||
|
};
|
||||||
|
|
||||||
// use gpui::{
|
#[derive(Default)]
|
||||||
// geometry::{
|
pub(crate) struct FacePile {
|
||||||
// rect::RectF,
|
faces: Vec<AnyElement>,
|
||||||
// vector::{vec2f, Vector2F},
|
}
|
||||||
// },
|
|
||||||
// json::ToJson,
|
|
||||||
// serde_json::{self, json},
|
|
||||||
// AnyElement, Axis, Element, View, ViewContext,
|
|
||||||
// };
|
|
||||||
|
|
||||||
// pub(crate) struct FacePile<V: View> {
|
impl RenderOnce for FacePile {
|
||||||
// overlap: f32,
|
type Rendered = Div;
|
||||||
// faces: Vec<AnyElement<V>>,
|
|
||||||
// }
|
|
||||||
|
|
||||||
// impl<V: View> FacePile<V> {
|
fn render(self, _: &mut WindowContext) -> Self::Rendered {
|
||||||
// pub fn new(overlap: f32) -> Self {
|
let player_count = self.faces.len();
|
||||||
// Self {
|
let player_list = self.faces.into_iter().enumerate().map(|(ix, player)| {
|
||||||
// overlap,
|
let isnt_last = ix < player_count - 1;
|
||||||
// faces: Vec::new(),
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// impl<V: View> Element<V> for FacePile<V> {
|
div().when(isnt_last, |div| div.neg_mr_1()).child(player)
|
||||||
// type LayoutState = ();
|
});
|
||||||
// type PaintState = ();
|
div().p_1().flex().items_center().children(player_list)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// impl Element for FacePile {
|
||||||
|
// type State = ();
|
||||||
// fn layout(
|
// fn layout(
|
||||||
// &mut self,
|
// &mut self,
|
||||||
// constraint: gpui::SizeConstraint,
|
// state: Option<Self::State>,
|
||||||
// view: &mut V,
|
// cx: &mut WindowContext,
|
||||||
// cx: &mut ViewContext<V>,
|
// ) -> (LayoutId, Self::State) {
|
||||||
// ) -> (Vector2F, Self::LayoutState) {
|
|
||||||
// debug_assert!(constraint.max_along(Axis::Horizontal) == f32::INFINITY);
|
|
||||||
|
|
||||||
// let mut width = 0.;
|
// let mut width = 0.;
|
||||||
// let mut max_height = 0.;
|
// let mut max_height = 0.;
|
||||||
|
// let mut faces = Vec::with_capacity(self.faces.len());
|
||||||
// for face in &mut self.faces {
|
// for face in &mut self.faces {
|
||||||
// let layout = face.layout(constraint, view, cx);
|
// let layout = face.layout(cx);
|
||||||
// width += layout.x();
|
// width += layout.x();
|
||||||
// max_height = f32::max(max_height, layout.y());
|
// max_height = f32::max(max_height, layout.y());
|
||||||
|
// faces.push(layout);
|
||||||
// }
|
// }
|
||||||
// width -= self.overlap * self.faces.len().saturating_sub(1) as f32;
|
// width -= self.overlap * self.faces.len().saturating_sub(1) as f32;
|
||||||
|
// (cx.request_layout(&Style::default(), faces), ())
|
||||||
// (
|
// // (
|
||||||
// Vector2F::new(width, max_height.clamp(1., constraint.max.y())),
|
// // Vector2F::new(width, max_height.clamp(1., constraint.max.y())),
|
||||||
// (),
|
// // (),
|
||||||
// )
|
// // ))
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// fn paint(
|
// fn paint(
|
||||||
@ -77,37 +71,10 @@
|
|||||||
|
|
||||||
// ()
|
// ()
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// fn rect_for_text_range(
|
|
||||||
// &self,
|
|
||||||
// _: Range<usize>,
|
|
||||||
// _: RectF,
|
|
||||||
// _: RectF,
|
|
||||||
// _: &Self::LayoutState,
|
|
||||||
// _: &Self::PaintState,
|
|
||||||
// _: &V,
|
|
||||||
// _: &ViewContext<V>,
|
|
||||||
// ) -> Option<RectF> {
|
|
||||||
// None
|
|
||||||
// }
|
|
||||||
|
|
||||||
// fn debug(
|
|
||||||
// &self,
|
|
||||||
// bounds: RectF,
|
|
||||||
// _: &Self::LayoutState,
|
|
||||||
// _: &Self::PaintState,
|
|
||||||
// _: &V,
|
|
||||||
// _: &ViewContext<V>,
|
|
||||||
// ) -> serde_json::Value {
|
|
||||||
// json!({
|
|
||||||
// "type": "FacePile",
|
|
||||||
// "bounds": bounds.to_json()
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// impl<V: View> Extend<AnyElement<V>> for FacePile<V> {
|
impl Extend<AnyElement> for FacePile {
|
||||||
// fn extend<T: IntoIterator<Item = AnyElement<V>>>(&mut self, children: T) {
|
fn extend<T: IntoIterator<Item = AnyElement>>(&mut self, children: T) {
|
||||||
// self.faces.extend(children);
|
self.faces.extend(children);
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
// use gpui::AppContext;
|
use gpui::AppContext;
|
||||||
// use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
// use workspace::AppState;
|
use workspace::AppState;
|
||||||
|
|
||||||
// pub mod incoming_call_notification;
|
pub mod incoming_call_notification;
|
||||||
// pub mod project_shared_notification;
|
// pub mod project_shared_notification;
|
||||||
|
|
||||||
// pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||||
// incoming_call_notification::init(app_state, cx);
|
incoming_call_notification::init(app_state, cx);
|
||||||
// project_shared_notification::init(app_state, cx);
|
//project_shared_notification::init(app_state, cx);
|
||||||
// }
|
}
|
||||||
|
@ -1,14 +1,12 @@
|
|||||||
use crate::notification_window_options;
|
use crate::notification_window_options;
|
||||||
use call::{ActiveCall, IncomingCall};
|
use call::{ActiveCall, IncomingCall};
|
||||||
use client::proto;
|
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
elements::*,
|
div, green, px, red, AppContext, Div, Element, ParentElement, Render, RenderOnce,
|
||||||
geometry::vector::vec2f,
|
StatefulInteractiveElement, Styled, ViewContext, VisualContext as _, WindowHandle,
|
||||||
platform::{CursorStyle, MouseButton},
|
|
||||||
AnyElement, AppContext, Entity, View, ViewContext, WindowHandle,
|
|
||||||
};
|
};
|
||||||
use std::sync::{Arc, Weak};
|
use std::sync::{Arc, Weak};
|
||||||
|
use ui::{h_stack, v_stack, Avatar, Button, Label};
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
use workspace::AppState;
|
use workspace::AppState;
|
||||||
|
|
||||||
@ -19,23 +17,44 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
|||||||
let mut notification_windows: Vec<WindowHandle<IncomingCallNotification>> = Vec::new();
|
let mut notification_windows: Vec<WindowHandle<IncomingCallNotification>> = Vec::new();
|
||||||
while let Some(incoming_call) = incoming_call.next().await {
|
while let Some(incoming_call) = incoming_call.next().await {
|
||||||
for window in notification_windows.drain(..) {
|
for window in notification_windows.drain(..) {
|
||||||
window.remove(&mut cx);
|
window
|
||||||
|
.update(&mut cx, |_, cx| {
|
||||||
|
// todo!()
|
||||||
|
cx.remove_window();
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(incoming_call) = incoming_call {
|
if let Some(incoming_call) = incoming_call {
|
||||||
let window_size = cx.read(|cx| {
|
let unique_screens = cx.update(|cx| cx.displays()).unwrap();
|
||||||
let theme = &theme::current(cx).incoming_call_notification;
|
let window_size = gpui::Size {
|
||||||
vec2f(theme.window_width, theme.window_height)
|
width: px(380.),
|
||||||
});
|
height: px(64.),
|
||||||
|
};
|
||||||
|
|
||||||
for screen in cx.platform().screens() {
|
for window in unique_screens {
|
||||||
|
let options = notification_window_options(window, window_size);
|
||||||
let window = cx
|
let window = cx
|
||||||
.add_window(notification_window_options(screen, window_size), |_| {
|
.open_window(options, |cx| {
|
||||||
IncomingCallNotification::new(incoming_call.clone(), app_state.clone())
|
cx.build_view(|_| {
|
||||||
});
|
IncomingCallNotification::new(
|
||||||
|
incoming_call.clone(),
|
||||||
|
app_state.clone(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
notification_windows.push(window);
|
notification_windows.push(window);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// for screen in cx.platform().screens() {
|
||||||
|
// let window = cx
|
||||||
|
// .add_window(notification_window_options(screen, window_size), |_| {
|
||||||
|
// IncomingCallNotification::new(incoming_call.clone(), app_state.clone())
|
||||||
|
// });
|
||||||
|
|
||||||
|
// notification_windows.push(window);
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -47,167 +66,196 @@ struct RespondToCall {
|
|||||||
accept: bool,
|
accept: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct IncomingCallNotification {
|
struct IncomingCallNotificationState {
|
||||||
call: IncomingCall,
|
call: IncomingCall,
|
||||||
app_state: Weak<AppState>,
|
app_state: Weak<AppState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IncomingCallNotification {
|
pub struct IncomingCallNotification {
|
||||||
|
state: Arc<IncomingCallNotificationState>,
|
||||||
|
}
|
||||||
|
impl IncomingCallNotificationState {
|
||||||
pub fn new(call: IncomingCall, app_state: Weak<AppState>) -> Self {
|
pub fn new(call: IncomingCall, app_state: Weak<AppState>) -> Self {
|
||||||
Self { call, app_state }
|
Self { call, app_state }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn respond(&mut self, accept: bool, cx: &mut ViewContext<Self>) {
|
fn respond(&self, accept: bool, cx: &mut AppContext) {
|
||||||
let active_call = ActiveCall::global(cx);
|
let active_call = ActiveCall::global(cx);
|
||||||
if accept {
|
if accept {
|
||||||
let join = active_call.update(cx, |active_call, cx| active_call.accept_incoming(cx));
|
let join = active_call.update(cx, |active_call, cx| active_call.accept_incoming(cx));
|
||||||
let caller_user_id = self.call.calling_user.id;
|
|
||||||
let initial_project_id = self.call.initial_project.as_ref().map(|project| project.id);
|
let initial_project_id = self.call.initial_project.as_ref().map(|project| project.id);
|
||||||
let app_state = self.app_state.clone();
|
let app_state = self.app_state.clone();
|
||||||
cx.app_context()
|
let cx: &mut AppContext = cx;
|
||||||
.spawn(|mut cx| async move {
|
cx.spawn(|cx| async move {
|
||||||
join.await?;
|
join.await?;
|
||||||
if let Some(project_id) = initial_project_id {
|
if let Some(_project_id) = initial_project_id {
|
||||||
cx.update(|cx| {
|
cx.update(|_cx| {
|
||||||
if let Some(app_state) = app_state.upgrade() {
|
if let Some(_app_state) = app_state.upgrade() {
|
||||||
workspace::join_remote_project(
|
// workspace::join_remote_project(
|
||||||
project_id,
|
// project_id,
|
||||||
caller_user_id,
|
// caller_user_id,
|
||||||
app_state,
|
// app_state,
|
||||||
cx,
|
// cx,
|
||||||
)
|
// )
|
||||||
.detach_and_log_err(cx);
|
// .detach_and_log_err(cx);
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
}
|
.log_err();
|
||||||
anyhow::Ok(())
|
}
|
||||||
})
|
anyhow::Ok(())
|
||||||
.detach_and_log_err(cx);
|
})
|
||||||
|
.detach_and_log_err(cx);
|
||||||
} else {
|
} else {
|
||||||
active_call.update(cx, |active_call, cx| {
|
active_call.update(cx, |active_call, cx| {
|
||||||
active_call.decline_incoming(cx).log_err();
|
active_call.decline_incoming(cx).log_err();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn render_caller(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
impl IncomingCallNotification {
|
||||||
let theme = &theme::current(cx).incoming_call_notification;
|
pub fn new(call: IncomingCall, app_state: Weak<AppState>) -> Self {
|
||||||
let default_project = proto::ParticipantProject::default();
|
Self {
|
||||||
let initial_project = self
|
state: Arc::new(IncomingCallNotificationState::new(call, app_state)),
|
||||||
.call
|
}
|
||||||
.initial_project
|
}
|
||||||
.as_ref()
|
fn render_caller(&self, cx: &mut ViewContext<Self>) -> impl Element {
|
||||||
.unwrap_or(&default_project);
|
h_stack()
|
||||||
Flex::row()
|
.children(
|
||||||
.with_children(self.call.calling_user.avatar.clone().map(|avatar| {
|
self.state
|
||||||
Image::from_data(avatar)
|
.call
|
||||||
.with_style(theme.caller_avatar)
|
.calling_user
|
||||||
.aligned()
|
.avatar
|
||||||
|
.as_ref()
|
||||||
|
.map(|avatar| Avatar::data(avatar.clone())),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
v_stack()
|
||||||
|
.child(Label::new(format!(
|
||||||
|
"{} is sharing a project in Zed",
|
||||||
|
self.state.call.calling_user.github_login
|
||||||
|
)))
|
||||||
|
.child(self.render_buttons(cx)),
|
||||||
|
)
|
||||||
|
// let theme = &theme::current(cx).incoming_call_notification;
|
||||||
|
// let default_project = proto::ParticipantProject::default();
|
||||||
|
// let initial_project = self
|
||||||
|
// .call
|
||||||
|
// .initial_project
|
||||||
|
// .as_ref()
|
||||||
|
// .unwrap_or(&default_project);
|
||||||
|
// Flex::row()
|
||||||
|
// .with_children(self.call.calling_user.avatar.clone().map(|avatar| {
|
||||||
|
// Image::from_data(avatar)
|
||||||
|
// .with_style(theme.caller_avatar)
|
||||||
|
// .aligned()
|
||||||
|
// }))
|
||||||
|
// .with_child(
|
||||||
|
// Flex::column()
|
||||||
|
// .with_child(
|
||||||
|
// Label::new(
|
||||||
|
// self.call.calling_user.github_login.clone(),
|
||||||
|
// theme.caller_username.text.clone(),
|
||||||
|
// )
|
||||||
|
// .contained()
|
||||||
|
// .with_style(theme.caller_username.container),
|
||||||
|
// )
|
||||||
|
// .with_child(
|
||||||
|
// Label::new(
|
||||||
|
// format!(
|
||||||
|
// "is sharing a project in Zed{}",
|
||||||
|
// if initial_project.worktree_root_names.is_empty() {
|
||||||
|
// ""
|
||||||
|
// } else {
|
||||||
|
// ":"
|
||||||
|
// }
|
||||||
|
// ),
|
||||||
|
// theme.caller_message.text.clone(),
|
||||||
|
// )
|
||||||
|
// .contained()
|
||||||
|
// .with_style(theme.caller_message.container),
|
||||||
|
// )
|
||||||
|
// .with_children(if initial_project.worktree_root_names.is_empty() {
|
||||||
|
// None
|
||||||
|
// } else {
|
||||||
|
// Some(
|
||||||
|
// Label::new(
|
||||||
|
// initial_project.worktree_root_names.join(", "),
|
||||||
|
// theme.worktree_roots.text.clone(),
|
||||||
|
// )
|
||||||
|
// .contained()
|
||||||
|
// .with_style(theme.worktree_roots.container),
|
||||||
|
// )
|
||||||
|
// })
|
||||||
|
// .contained()
|
||||||
|
// .with_style(theme.caller_metadata)
|
||||||
|
// .aligned(),
|
||||||
|
// )
|
||||||
|
// .contained()
|
||||||
|
// .with_style(theme.caller_container)
|
||||||
|
// .flex(1., true)
|
||||||
|
// .into_any()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_buttons(&self, cx: &mut ViewContext<Self>) -> impl Element {
|
||||||
|
h_stack()
|
||||||
|
.child(Button::new("Accept").render(cx).bg(green()).on_click({
|
||||||
|
let state = self.state.clone();
|
||||||
|
move |_, cx| state.respond(true, cx)
|
||||||
|
}))
|
||||||
|
.child(Button::new("Decline").render(cx).bg(red()).on_click({
|
||||||
|
let state = self.state.clone();
|
||||||
|
move |_, cx| state.respond(false, cx)
|
||||||
}))
|
}))
|
||||||
.with_child(
|
|
||||||
Flex::column()
|
|
||||||
.with_child(
|
|
||||||
Label::new(
|
|
||||||
self.call.calling_user.github_login.clone(),
|
|
||||||
theme.caller_username.text.clone(),
|
|
||||||
)
|
|
||||||
.contained()
|
|
||||||
.with_style(theme.caller_username.container),
|
|
||||||
)
|
|
||||||
.with_child(
|
|
||||||
Label::new(
|
|
||||||
format!(
|
|
||||||
"is sharing a project in Zed{}",
|
|
||||||
if initial_project.worktree_root_names.is_empty() {
|
|
||||||
""
|
|
||||||
} else {
|
|
||||||
":"
|
|
||||||
}
|
|
||||||
),
|
|
||||||
theme.caller_message.text.clone(),
|
|
||||||
)
|
|
||||||
.contained()
|
|
||||||
.with_style(theme.caller_message.container),
|
|
||||||
)
|
|
||||||
.with_children(if initial_project.worktree_root_names.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(
|
|
||||||
Label::new(
|
|
||||||
initial_project.worktree_root_names.join(", "),
|
|
||||||
theme.worktree_roots.text.clone(),
|
|
||||||
)
|
|
||||||
.contained()
|
|
||||||
.with_style(theme.worktree_roots.container),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.contained()
|
|
||||||
.with_style(theme.caller_metadata)
|
|
||||||
.aligned(),
|
|
||||||
)
|
|
||||||
.contained()
|
|
||||||
.with_style(theme.caller_container)
|
|
||||||
.flex(1., true)
|
|
||||||
.into_any()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_buttons(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
// enum Accept {}
|
||||||
enum Accept {}
|
// enum Decline {}
|
||||||
enum Decline {}
|
|
||||||
|
|
||||||
let theme = theme::current(cx);
|
// let theme = theme::current(cx);
|
||||||
Flex::column()
|
// Flex::column()
|
||||||
.with_child(
|
// .with_child(
|
||||||
MouseEventHandler::new::<Accept, _>(0, cx, |_, _| {
|
// MouseEventHandler::new::<Accept, _>(0, cx, |_, _| {
|
||||||
let theme = &theme.incoming_call_notification;
|
// let theme = &theme.incoming_call_notification;
|
||||||
Label::new("Accept", theme.accept_button.text.clone())
|
// Label::new("Accept", theme.accept_button.text.clone())
|
||||||
.aligned()
|
// .aligned()
|
||||||
.contained()
|
// .contained()
|
||||||
.with_style(theme.accept_button.container)
|
// .with_style(theme.accept_button.container)
|
||||||
})
|
// })
|
||||||
.with_cursor_style(CursorStyle::PointingHand)
|
// .with_cursor_style(CursorStyle::PointingHand)
|
||||||
.on_click(MouseButton::Left, |_, this, cx| {
|
// .on_click(MouseButton::Left, |_, this, cx| {
|
||||||
this.respond(true, cx);
|
// this.respond(true, cx);
|
||||||
})
|
// })
|
||||||
.flex(1., true),
|
// .flex(1., true),
|
||||||
)
|
// )
|
||||||
.with_child(
|
// .with_child(
|
||||||
MouseEventHandler::new::<Decline, _>(0, cx, |_, _| {
|
// MouseEventHandler::new::<Decline, _>(0, cx, |_, _| {
|
||||||
let theme = &theme.incoming_call_notification;
|
// let theme = &theme.incoming_call_notification;
|
||||||
Label::new("Decline", theme.decline_button.text.clone())
|
// Label::new("Decline", theme.decline_button.text.clone())
|
||||||
.aligned()
|
// .aligned()
|
||||||
.contained()
|
// .contained()
|
||||||
.with_style(theme.decline_button.container)
|
// .with_style(theme.decline_button.container)
|
||||||
})
|
// })
|
||||||
.with_cursor_style(CursorStyle::PointingHand)
|
// .with_cursor_style(CursorStyle::PointingHand)
|
||||||
.on_click(MouseButton::Left, |_, this, cx| {
|
// .on_click(MouseButton::Left, |_, this, cx| {
|
||||||
this.respond(false, cx);
|
// this.respond(false, cx);
|
||||||
})
|
// })
|
||||||
.flex(1., true),
|
// .flex(1., true),
|
||||||
)
|
// )
|
||||||
.constrained()
|
// .constrained()
|
||||||
.with_width(theme.incoming_call_notification.button_width)
|
// .with_width(theme.incoming_call_notification.button_width)
|
||||||
.into_any()
|
// .into_any()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
impl Render for IncomingCallNotification {
|
||||||
impl Entity for IncomingCallNotification {
|
type Element = Div;
|
||||||
type Event = ();
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||||
}
|
div().bg(red()).flex_none().child(self.render_caller(cx))
|
||||||
|
// Flex::row()
|
||||||
impl View for IncomingCallNotification {
|
// .with_child()
|
||||||
fn ui_name() -> &'static str {
|
// .with_child(self.render_buttons(cx))
|
||||||
"IncomingCallNotification"
|
// .contained()
|
||||||
}
|
// .with_background_color(background)
|
||||||
|
// .expanded()
|
||||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
// .into_any()
|
||||||
let background = theme::current(cx).incoming_call_notification.background;
|
|
||||||
Flex::row()
|
|
||||||
.with_child(self.render_caller(cx))
|
|
||||||
.with_child(self.render_buttons(cx))
|
|
||||||
.contained()
|
|
||||||
.with_background_color(background)
|
|
||||||
.expanded()
|
|
||||||
.into_any()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
use collections::{CommandPaletteFilter, HashMap};
|
use collections::{CommandPaletteFilter, HashMap};
|
||||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, div, prelude::*, Action, AppContext, Div, EventEmitter, FocusHandle, FocusableView,
|
actions, div, prelude::*, Action, AppContext, DismissEvent, Div, EventEmitter, FocusHandle,
|
||||||
Keystroke, Manager, ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView,
|
FocusableView, Keystroke, ParentElement, Render, Styled, View, ViewContext, VisualContext,
|
||||||
|
WeakView,
|
||||||
};
|
};
|
||||||
use picker::{Picker, PickerDelegate};
|
use picker::{Picker, PickerDelegate};
|
||||||
use std::{
|
use std::{
|
||||||
@ -68,7 +69,7 @@ impl CommandPalette {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventEmitter<Manager> for CommandPalette {}
|
impl EventEmitter<DismissEvent> for CommandPalette {}
|
||||||
|
|
||||||
impl FocusableView for CommandPalette {
|
impl FocusableView for CommandPalette {
|
||||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||||
@ -268,7 +269,7 @@ impl PickerDelegate for CommandPaletteDelegate {
|
|||||||
|
|
||||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||||
self.command_palette
|
self.command_palette
|
||||||
.update(cx, |_, cx| cx.emit(Manager::Dismiss))
|
.update(cx, |_, cx| cx.emit(DismissEvent::Dismiss))
|
||||||
.log_err();
|
.log_err();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,8 +14,8 @@ use editor::{
|
|||||||
use futures::future::try_join_all;
|
use futures::future::try_join_all;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, div, AnyElement, AnyView, AppContext, Context, Div, EventEmitter, FocusEvent,
|
actions, div, AnyElement, AnyView, AppContext, Context, Div, EventEmitter, FocusEvent,
|
||||||
FocusHandle, Focusable, FocusableElement, FocusableView, InteractiveElement, Model,
|
FocusHandle, Focusable, FocusableElement, FocusableView, InteractiveElement, IntoElement,
|
||||||
ParentElement, Render, RenderOnce, SharedString, Styled, Subscription, Task, View, ViewContext,
|
Model, ParentElement, Render, SharedString, Styled, Subscription, Task, View, ViewContext,
|
||||||
VisualContext, WeakView, WindowContext,
|
VisualContext, WeakView, WindowContext,
|
||||||
};
|
};
|
||||||
use language::{
|
use language::{
|
||||||
@ -36,7 +36,7 @@ use std::{
|
|||||||
};
|
};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
pub use toolbar_controls::ToolbarControls;
|
pub use toolbar_controls::ToolbarControls;
|
||||||
use ui::{h_stack, HighlightedLabel, Icon, IconElement, Label, TextColor};
|
use ui::{h_stack, Color, HighlightedLabel, Icon, IconElement, Label};
|
||||||
use util::TryFutureExt;
|
use util::TryFutureExt;
|
||||||
use workspace::{
|
use workspace::{
|
||||||
item::{BreadcrumbText, Item, ItemEvent, ItemHandle},
|
item::{BreadcrumbText, Item, ItemEvent, ItemHandle},
|
||||||
@ -778,28 +778,28 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
|
|||||||
.bg(gpui::red())
|
.bg(gpui::red())
|
||||||
.map(|stack| {
|
.map(|stack| {
|
||||||
let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
|
let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
|
||||||
IconElement::new(Icon::XCircle).color(TextColor::Error)
|
IconElement::new(Icon::XCircle).color(Color::Error)
|
||||||
} else {
|
} else {
|
||||||
IconElement::new(Icon::ExclamationTriangle).color(TextColor::Warning)
|
IconElement::new(Icon::ExclamationTriangle).color(Color::Warning)
|
||||||
};
|
};
|
||||||
|
|
||||||
stack.child(div().pl_8().child(icon))
|
stack.child(div().pl_8().child(icon))
|
||||||
})
|
})
|
||||||
.when_some(diagnostic.source.as_ref(), |stack, source| {
|
.when_some(diagnostic.source.as_ref(), |stack, source| {
|
||||||
stack.child(Label::new(format!("{source}:")).color(TextColor::Accent))
|
stack.child(Label::new(format!("{source}:")).color(Color::Accent))
|
||||||
})
|
})
|
||||||
.child(HighlightedLabel::new(message.clone(), highlights.clone()))
|
.child(HighlightedLabel::new(message.clone(), highlights.clone()))
|
||||||
.when_some(diagnostic.code.as_ref(), |stack, code| {
|
.when_some(diagnostic.code.as_ref(), |stack, code| {
|
||||||
stack.child(Label::new(code.clone()))
|
stack.child(Label::new(code.clone()))
|
||||||
})
|
})
|
||||||
.render_into_any()
|
.into_any_element()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn render_summary(summary: &DiagnosticSummary) -> AnyElement {
|
pub(crate) fn render_summary(summary: &DiagnosticSummary) -> AnyElement {
|
||||||
if summary.error_count == 0 && summary.warning_count == 0 {
|
if summary.error_count == 0 && summary.warning_count == 0 {
|
||||||
let label = Label::new("No problems");
|
let label = Label::new("No problems");
|
||||||
label.render_into_any()
|
label.into_any_element()
|
||||||
} else {
|
} else {
|
||||||
h_stack()
|
h_stack()
|
||||||
.bg(gpui::red())
|
.bg(gpui::red())
|
||||||
@ -807,7 +807,7 @@ pub(crate) fn render_summary(summary: &DiagnosticSummary) -> AnyElement {
|
|||||||
.child(Label::new(summary.error_count.to_string()))
|
.child(Label::new(summary.error_count.to_string()))
|
||||||
.child(IconElement::new(Icon::ExclamationTriangle))
|
.child(IconElement::new(Icon::ExclamationTriangle))
|
||||||
.child(Label::new(summary.warning_count.to_string()))
|
.child(Label::new(summary.warning_count.to_string()))
|
||||||
.render_into_any()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1550,7 +1550,7 @@ mod tests {
|
|||||||
block_id: ix,
|
block_id: ix,
|
||||||
editor_style: &editor::EditorStyle::default(),
|
editor_style: &editor::EditorStyle::default(),
|
||||||
})
|
})
|
||||||
.element_id()?
|
.inner_id()?
|
||||||
.try_into()
|
.try_into()
|
||||||
.ok()?,
|
.ok()?,
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ use gpui::{
|
|||||||
use language::Diagnostic;
|
use language::Diagnostic;
|
||||||
use lsp::LanguageServerId;
|
use lsp::LanguageServerId;
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::{h_stack, Icon, IconElement, Label, TextColor, Tooltip};
|
use ui::{h_stack, Color, Icon, IconElement, Label, Tooltip};
|
||||||
use workspace::{item::ItemHandle, StatusItemView, ToolbarItemEvent, Workspace};
|
use workspace::{item::ItemHandle, StatusItemView, ToolbarItemEvent, Workspace};
|
||||||
|
|
||||||
use crate::ProjectDiagnosticsEditor;
|
use crate::ProjectDiagnosticsEditor;
|
||||||
@ -26,25 +26,25 @@ impl Render for DiagnosticIndicator {
|
|||||||
|
|
||||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||||
let diagnostic_indicator = match (self.summary.error_count, self.summary.warning_count) {
|
let diagnostic_indicator = match (self.summary.error_count, self.summary.warning_count) {
|
||||||
(0, 0) => h_stack().child(IconElement::new(Icon::Check).color(TextColor::Success)),
|
(0, 0) => h_stack().child(IconElement::new(Icon::Check).color(Color::Success)),
|
||||||
(0, warning_count) => h_stack()
|
(0, warning_count) => h_stack()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.child(IconElement::new(Icon::ExclamationTriangle).color(TextColor::Warning))
|
.child(IconElement::new(Icon::ExclamationTriangle).color(Color::Warning))
|
||||||
.child(Label::new(warning_count.to_string())),
|
.child(Label::new(warning_count.to_string())),
|
||||||
(error_count, 0) => h_stack()
|
(error_count, 0) => h_stack()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.child(IconElement::new(Icon::XCircle).color(TextColor::Error))
|
.child(IconElement::new(Icon::XCircle).color(Color::Error))
|
||||||
.child(Label::new(error_count.to_string())),
|
.child(Label::new(error_count.to_string())),
|
||||||
(error_count, warning_count) => h_stack()
|
(error_count, warning_count) => h_stack()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.child(IconElement::new(Icon::XCircle).color(TextColor::Error))
|
.child(IconElement::new(Icon::XCircle).color(Color::Error))
|
||||||
.child(Label::new(error_count.to_string()))
|
.child(Label::new(error_count.to_string()))
|
||||||
.child(IconElement::new(Icon::ExclamationTriangle).color(TextColor::Warning))
|
.child(IconElement::new(Icon::ExclamationTriangle).color(Color::Warning))
|
||||||
.child(Label::new(warning_count.to_string())),
|
.child(Label::new(warning_count.to_string())),
|
||||||
};
|
};
|
||||||
|
|
||||||
h_stack()
|
h_stack()
|
||||||
.id(cx.entity_id())
|
.id("diagnostic-indicator")
|
||||||
.on_action(cx.listener(Self::go_to_next_diagnostic))
|
.on_action(cx.listener(Self::go_to_next_diagnostic))
|
||||||
.rounded_md()
|
.rounded_md()
|
||||||
.flex_none()
|
.flex_none()
|
||||||
|
@ -1001,17 +1001,18 @@ impl CompletionsMenu {
|
|||||||
|
|
||||||
fn pre_resolve_completion_documentation(
|
fn pre_resolve_completion_documentation(
|
||||||
&self,
|
&self,
|
||||||
project: Option<ModelHandle<Project>>,
|
editor: &Editor,
|
||||||
cx: &mut ViewContext<Editor>,
|
cx: &mut ViewContext<Editor>,
|
||||||
) {
|
) -> Option<Task<()>> {
|
||||||
let settings = settings::get::<EditorSettings>(cx);
|
let settings = settings::get::<EditorSettings>(cx);
|
||||||
if !settings.show_completion_documentation {
|
if !settings.show_completion_documentation {
|
||||||
return;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(project) = project else {
|
let Some(project) = editor.project.clone() else {
|
||||||
return;
|
return None;
|
||||||
};
|
};
|
||||||
|
|
||||||
let client = project.read(cx).client();
|
let client = project.read(cx).client();
|
||||||
let language_registry = project.read(cx).languages().clone();
|
let language_registry = project.read(cx).languages().clone();
|
||||||
|
|
||||||
@ -1021,7 +1022,7 @@ impl CompletionsMenu {
|
|||||||
let completions = self.completions.clone();
|
let completions = self.completions.clone();
|
||||||
let completion_indices: Vec<_> = self.matches.iter().map(|m| m.candidate_id).collect();
|
let completion_indices: Vec<_> = self.matches.iter().map(|m| m.candidate_id).collect();
|
||||||
|
|
||||||
cx.spawn(move |this, mut cx| async move {
|
Some(cx.spawn(move |this, mut cx| async move {
|
||||||
if is_remote {
|
if is_remote {
|
||||||
let Some(project_id) = project_id else {
|
let Some(project_id) = project_id else {
|
||||||
log::error!("Remote project without remote_id");
|
log::error!("Remote project without remote_id");
|
||||||
@ -1083,8 +1084,7 @@ impl CompletionsMenu {
|
|||||||
_ = this.update(&mut cx, |_, cx| cx.notify());
|
_ = this.update(&mut cx, |_, cx| cx.notify());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
}))
|
||||||
.detach();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn attempt_resolve_selected_completion_documentation(
|
fn attempt_resolve_selected_completion_documentation(
|
||||||
@ -3423,7 +3423,7 @@ impl Editor {
|
|||||||
to_insert,
|
to_insert,
|
||||||
}) = self.inlay_hint_cache.spawn_hint_refresh(
|
}) = self.inlay_hint_cache.spawn_hint_refresh(
|
||||||
reason_description,
|
reason_description,
|
||||||
self.excerpt_visible_offsets(required_languages.as_ref(), cx),
|
self.excerpts_for_inlay_hints_query(required_languages.as_ref(), cx),
|
||||||
invalidate_cache,
|
invalidate_cache,
|
||||||
cx,
|
cx,
|
||||||
) {
|
) {
|
||||||
@ -3442,11 +3442,15 @@ impl Editor {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn excerpt_visible_offsets(
|
pub fn excerpts_for_inlay_hints_query(
|
||||||
&self,
|
&self,
|
||||||
restrict_to_languages: Option<&HashSet<Arc<Language>>>,
|
restrict_to_languages: Option<&HashSet<Arc<Language>>>,
|
||||||
cx: &mut ViewContext<'_, '_, Editor>,
|
cx: &mut ViewContext<'_, '_, Editor>,
|
||||||
) -> HashMap<ExcerptId, (ModelHandle<Buffer>, Global, Range<usize>)> {
|
) -> HashMap<ExcerptId, (ModelHandle<Buffer>, Global, Range<usize>)> {
|
||||||
|
let Some(project) = self.project.as_ref() else {
|
||||||
|
return HashMap::default();
|
||||||
|
};
|
||||||
|
let project = project.read(cx);
|
||||||
let multi_buffer = self.buffer().read(cx);
|
let multi_buffer = self.buffer().read(cx);
|
||||||
let multi_buffer_snapshot = multi_buffer.snapshot(cx);
|
let multi_buffer_snapshot = multi_buffer.snapshot(cx);
|
||||||
let multi_buffer_visible_start = self
|
let multi_buffer_visible_start = self
|
||||||
@ -3466,6 +3470,14 @@ impl Editor {
|
|||||||
.filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty())
|
.filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty())
|
||||||
.filter_map(|(buffer_handle, excerpt_visible_range, excerpt_id)| {
|
.filter_map(|(buffer_handle, excerpt_visible_range, excerpt_id)| {
|
||||||
let buffer = buffer_handle.read(cx);
|
let buffer = buffer_handle.read(cx);
|
||||||
|
let buffer_file = project::worktree::File::from_dyn(buffer.file())?;
|
||||||
|
let buffer_worktree = project.worktree_for_id(buffer_file.worktree_id(cx), cx)?;
|
||||||
|
let worktree_entry = buffer_worktree
|
||||||
|
.read(cx)
|
||||||
|
.entry_for_id(buffer_file.project_entry_id(cx)?)?;
|
||||||
|
if worktree_entry.is_ignored {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
let language = buffer.language()?;
|
let language = buffer.language()?;
|
||||||
if let Some(restrict_to_languages) = restrict_to_languages {
|
if let Some(restrict_to_languages) = restrict_to_languages {
|
||||||
if !restrict_to_languages.contains(language) {
|
if !restrict_to_languages.contains(language) {
|
||||||
@ -3580,7 +3592,8 @@ impl Editor {
|
|||||||
let id = post_inc(&mut self.next_completion_id);
|
let id = post_inc(&mut self.next_completion_id);
|
||||||
let task = cx.spawn(|this, mut cx| {
|
let task = cx.spawn(|this, mut cx| {
|
||||||
async move {
|
async move {
|
||||||
let menu = if let Some(completions) = completions.await.log_err() {
|
let completions = completions.await.log_err();
|
||||||
|
let (menu, pre_resolve_task) = if let Some(completions) = completions {
|
||||||
let mut menu = CompletionsMenu {
|
let mut menu = CompletionsMenu {
|
||||||
id,
|
id,
|
||||||
initial_position: position,
|
initial_position: position,
|
||||||
@ -3601,21 +3614,26 @@ impl Editor {
|
|||||||
selected_item: 0,
|
selected_item: 0,
|
||||||
list: Default::default(),
|
list: Default::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
menu.filter(query.as_deref(), cx.background()).await;
|
menu.filter(query.as_deref(), cx.background()).await;
|
||||||
|
|
||||||
if menu.matches.is_empty() {
|
if menu.matches.is_empty() {
|
||||||
None
|
(None, None)
|
||||||
} else {
|
} else {
|
||||||
_ = this.update(&mut cx, |editor, cx| {
|
let pre_resolve_task = this
|
||||||
menu.pre_resolve_completion_documentation(editor.project.clone(), cx);
|
.update(&mut cx, |editor, cx| {
|
||||||
});
|
menu.pre_resolve_completion_documentation(editor, cx)
|
||||||
Some(menu)
|
})
|
||||||
|
.ok()
|
||||||
|
.flatten();
|
||||||
|
(Some(menu), pre_resolve_task)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
None
|
(None, None)
|
||||||
};
|
};
|
||||||
|
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
this.completion_tasks.retain(|(task_id, _)| *task_id > id);
|
this.completion_tasks.retain(|(task_id, _)| *task_id >= id);
|
||||||
|
|
||||||
let mut context_menu = this.context_menu.write();
|
let mut context_menu = this.context_menu.write();
|
||||||
match context_menu.as_ref() {
|
match context_menu.as_ref() {
|
||||||
@ -3636,10 +3654,10 @@ impl Editor {
|
|||||||
drop(context_menu);
|
drop(context_menu);
|
||||||
this.discard_copilot_suggestion(cx);
|
this.discard_copilot_suggestion(cx);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
} else if this.completion_tasks.is_empty() {
|
} else if this.completion_tasks.len() <= 1 {
|
||||||
// If there are no more completion tasks and the last menu was
|
// If there are no more completion tasks (omitting ourself) and
|
||||||
// empty, we should hide it. If it was already hidden, we should
|
// the last menu was empty, we should hide it. If it was already
|
||||||
// also show the copilot suggestion when available.
|
// hidden, we should also show the copilot suggestion when available.
|
||||||
drop(context_menu);
|
drop(context_menu);
|
||||||
if this.hide_context_menu(cx).is_none() {
|
if this.hide_context_menu(cx).is_none() {
|
||||||
this.update_visible_copilot_suggestion(cx);
|
this.update_visible_copilot_suggestion(cx);
|
||||||
@ -3647,10 +3665,15 @@ impl Editor {
|
|||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
if let Some(pre_resolve_task) = pre_resolve_task {
|
||||||
|
pre_resolve_task.await;
|
||||||
|
}
|
||||||
|
|
||||||
Ok::<_, anyhow::Error>(())
|
Ok::<_, anyhow::Error>(())
|
||||||
}
|
}
|
||||||
.log_err()
|
.log_err()
|
||||||
});
|
});
|
||||||
|
|
||||||
self.completion_tasks.push((id, task));
|
self.completion_tasks.push((id, task));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -861,7 +861,7 @@ async fn fetch_and_update_hints(
|
|||||||
let inlay_hints_fetch_task = editor
|
let inlay_hints_fetch_task = editor
|
||||||
.update(&mut cx, |editor, cx| {
|
.update(&mut cx, |editor, cx| {
|
||||||
if got_throttled {
|
if got_throttled {
|
||||||
let query_not_around_visible_range = match editor.excerpt_visible_offsets(None, cx).remove(&query.excerpt_id) {
|
let query_not_around_visible_range = match editor.excerpts_for_inlay_hints_query(None, cx).remove(&query.excerpt_id) {
|
||||||
Some((_, _, current_visible_range)) => {
|
Some((_, _, current_visible_range)) => {
|
||||||
let visible_offset_length = current_visible_range.len();
|
let visible_offset_length = current_visible_range.len();
|
||||||
let double_visible_range = current_visible_range
|
let double_visible_range = current_visible_range
|
||||||
@ -2237,7 +2237,9 @@ pub mod tests {
|
|||||||
editor: &ViewHandle<Editor>,
|
editor: &ViewHandle<Editor>,
|
||||||
cx: &mut gpui::TestAppContext,
|
cx: &mut gpui::TestAppContext,
|
||||||
) -> Range<Point> {
|
) -> Range<Point> {
|
||||||
let ranges = editor.update(cx, |editor, cx| editor.excerpt_visible_offsets(None, cx));
|
let ranges = editor.update(cx, |editor, cx| {
|
||||||
|
editor.excerpts_for_inlay_hints_query(None, cx)
|
||||||
|
});
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ranges.len(),
|
ranges.len(),
|
||||||
1,
|
1,
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -6740,75 +6740,6 @@ async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) {
|
|||||||
// );
|
// );
|
||||||
// }
|
// }
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_combine_syntax_and_fuzzy_match_highlights() {
|
|
||||||
let string = "abcdefghijklmnop";
|
|
||||||
let syntax_ranges = [
|
|
||||||
(
|
|
||||||
0..3,
|
|
||||||
HighlightStyle {
|
|
||||||
color: Some(Hsla::red()),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
),
|
|
||||||
(
|
|
||||||
4..8,
|
|
||||||
HighlightStyle {
|
|
||||||
color: Some(Hsla::green()),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
),
|
|
||||||
];
|
|
||||||
let match_indices = [4, 6, 7, 8];
|
|
||||||
assert_eq!(
|
|
||||||
combine_syntax_and_fuzzy_match_highlights(
|
|
||||||
string,
|
|
||||||
Default::default(),
|
|
||||||
syntax_ranges.into_iter(),
|
|
||||||
&match_indices,
|
|
||||||
),
|
|
||||||
&[
|
|
||||||
(
|
|
||||||
0..3,
|
|
||||||
HighlightStyle {
|
|
||||||
color: Some(Hsla::red()),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
),
|
|
||||||
(
|
|
||||||
4..5,
|
|
||||||
HighlightStyle {
|
|
||||||
color: Some(Hsla::green()),
|
|
||||||
font_weight: Some(gpui::FontWeight::BOLD),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
),
|
|
||||||
(
|
|
||||||
5..6,
|
|
||||||
HighlightStyle {
|
|
||||||
color: Some(Hsla::green()),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
),
|
|
||||||
(
|
|
||||||
6..8,
|
|
||||||
HighlightStyle {
|
|
||||||
color: Some(Hsla::green()),
|
|
||||||
font_weight: Some(gpui::FontWeight::BOLD),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
),
|
|
||||||
(
|
|
||||||
8..9,
|
|
||||||
HighlightStyle {
|
|
||||||
font_weight: Some(gpui::FontWeight::BOLD),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn go_to_prev_overlapping_diagnostic(
|
async fn go_to_prev_overlapping_diagnostic(
|
||||||
executor: BackgroundExecutor,
|
executor: BackgroundExecutor,
|
||||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -861,7 +861,7 @@ async fn fetch_and_update_hints(
|
|||||||
let inlay_hints_fetch_task = editor
|
let inlay_hints_fetch_task = editor
|
||||||
.update(&mut cx, |editor, cx| {
|
.update(&mut cx, |editor, cx| {
|
||||||
if got_throttled {
|
if got_throttled {
|
||||||
let query_not_around_visible_range = match editor.excerpt_visible_offsets(None, cx).remove(&query.excerpt_id) {
|
let query_not_around_visible_range = match editor.excerpts_for_inlay_hints_query(None, cx).remove(&query.excerpt_id) {
|
||||||
Some((_, _, current_visible_range)) => {
|
Some((_, _, current_visible_range)) => {
|
||||||
let visible_offset_length = current_visible_range.len();
|
let visible_offset_length = current_visible_range.len();
|
||||||
let double_visible_range = current_visible_range
|
let double_visible_range = current_visible_range
|
||||||
@ -2201,7 +2201,9 @@ pub mod tests {
|
|||||||
cx: &mut gpui::TestAppContext,
|
cx: &mut gpui::TestAppContext,
|
||||||
) -> Range<Point> {
|
) -> Range<Point> {
|
||||||
let ranges = editor
|
let ranges = editor
|
||||||
.update(cx, |editor, cx| editor.excerpt_visible_offsets(None, cx))
|
.update(cx, |editor, cx| {
|
||||||
|
editor.excerpts_for_inlay_hints_query(None, cx)
|
||||||
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ranges.len(),
|
ranges.len(),
|
||||||
|
@ -30,7 +30,7 @@ use std::{
|
|||||||
};
|
};
|
||||||
use text::Selection;
|
use text::Selection;
|
||||||
use theme::{ActiveTheme, Theme};
|
use theme::{ActiveTheme, Theme};
|
||||||
use ui::{Label, TextColor};
|
use ui::{Color, Label};
|
||||||
use util::{paths::PathExt, ResultExt, TryFutureExt};
|
use util::{paths::PathExt, ResultExt, TryFutureExt};
|
||||||
use workspace::item::{BreadcrumbText, FollowEvent, FollowableEvents, FollowableItemHandle};
|
use workspace::item::{BreadcrumbText, FollowEvent, FollowableEvents, FollowableItemHandle};
|
||||||
use workspace::{
|
use workspace::{
|
||||||
@ -604,7 +604,7 @@ impl Item for Editor {
|
|||||||
&description,
|
&description,
|
||||||
MAX_TAB_TITLE_LEN,
|
MAX_TAB_TITLE_LEN,
|
||||||
))
|
))
|
||||||
.color(TextColor::Muted),
|
.color(Color::Muted),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
})),
|
})),
|
||||||
|
@ -2,8 +2,8 @@ use collections::HashMap;
|
|||||||
use editor::{scroll::autoscroll::Autoscroll, Bias, Editor};
|
use editor::{scroll::autoscroll::Autoscroll, Bias, Editor};
|
||||||
use fuzzy::{CharBag, PathMatch, PathMatchCandidate};
|
use fuzzy::{CharBag, PathMatch, PathMatchCandidate};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, div, AppContext, Div, EventEmitter, FocusHandle, FocusableView, InteractiveElement,
|
actions, div, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView,
|
||||||
Manager, Model, ParentElement, Render, RenderOnce, Styled, Task, View, ViewContext,
|
InteractiveElement, IntoElement, Model, ParentElement, Render, Styled, Task, View, ViewContext,
|
||||||
VisualContext, WeakView,
|
VisualContext, WeakView,
|
||||||
};
|
};
|
||||||
use picker::{Picker, PickerDelegate};
|
use picker::{Picker, PickerDelegate};
|
||||||
@ -111,7 +111,7 @@ impl FileFinder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventEmitter<Manager> for FileFinder {}
|
impl EventEmitter<DismissEvent> for FileFinder {}
|
||||||
impl FocusableView for FileFinder {
|
impl FocusableView for FileFinder {
|
||||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||||
self.picker.focus_handle(cx)
|
self.picker.focus_handle(cx)
|
||||||
@ -690,7 +690,7 @@ impl PickerDelegate for FileFinderDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
finder
|
finder
|
||||||
.update(&mut cx, |_, cx| cx.emit(Manager::Dismiss))
|
.update(&mut cx, |_, cx| cx.emit(DismissEvent::Dismiss))
|
||||||
.ok()?;
|
.ok()?;
|
||||||
|
|
||||||
Some(())
|
Some(())
|
||||||
@ -702,7 +702,7 @@ impl PickerDelegate for FileFinderDelegate {
|
|||||||
|
|
||||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<FileFinderDelegate>>) {
|
fn dismissed(&mut self, cx: &mut ViewContext<Picker<FileFinderDelegate>>) {
|
||||||
self.file_finder
|
self.file_finder
|
||||||
.update(cx, |_, cx| cx.emit(Manager::Dismiss))
|
.update(cx, |_, cx| cx.emit(DismissEvent::Dismiss))
|
||||||
.log_err();
|
.log_err();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,6 +6,8 @@ use gpui::BackgroundExecutor;
|
|||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
cmp::{self, Ordering},
|
cmp::{self, Ordering},
|
||||||
|
iter,
|
||||||
|
ops::Range,
|
||||||
sync::atomic::AtomicBool,
|
sync::atomic::AtomicBool,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -54,6 +56,32 @@ pub struct StringMatch {
|
|||||||
pub string: String,
|
pub string: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl StringMatch {
|
||||||
|
pub fn ranges<'a>(&'a self) -> impl 'a + Iterator<Item = Range<usize>> {
|
||||||
|
let mut positions = self.positions.iter().peekable();
|
||||||
|
iter::from_fn(move || {
|
||||||
|
while let Some(start) = positions.next().copied() {
|
||||||
|
let mut end = start + self.char_len_at_index(start);
|
||||||
|
while let Some(next_start) = positions.peek() {
|
||||||
|
if end == **next_start {
|
||||||
|
end += self.char_len_at_index(end);
|
||||||
|
positions.next();
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Some(start..end);
|
||||||
|
}
|
||||||
|
None
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn char_len_at_index(&self, ix: usize) -> usize {
|
||||||
|
self.string[ix..].chars().next().unwrap().len_utf8()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl PartialEq for StringMatch {
|
impl PartialEq for StringMatch {
|
||||||
fn eq(&self, other: &Self) -> bool {
|
fn eq(&self, other: &Self) -> bool {
|
||||||
self.cmp(other).is_eq()
|
self.cmp(other).is_eq()
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Editor};
|
use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Editor};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, div, prelude::*, AppContext, Div, EventEmitter, FocusHandle, FocusableView, Manager,
|
actions, div, prelude::*, AppContext, DismissEvent, Div, EventEmitter, FocusHandle,
|
||||||
Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext, WindowContext,
|
FocusableView, Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext,
|
||||||
|
WindowContext,
|
||||||
};
|
};
|
||||||
use text::{Bias, Point};
|
use text::{Bias, Point};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::{h_stack, v_stack, Label, StyledExt, TextColor};
|
use ui::{h_stack, v_stack, Color, Label, StyledExt};
|
||||||
use util::paths::FILE_ROW_COLUMN_DELIMITER;
|
use util::paths::FILE_ROW_COLUMN_DELIMITER;
|
||||||
use workspace::Workspace;
|
|
||||||
|
|
||||||
actions!(Toggle);
|
actions!(Toggle);
|
||||||
|
|
||||||
@ -25,22 +25,24 @@ pub struct GoToLine {
|
|||||||
|
|
||||||
impl FocusableView for GoToLine {
|
impl FocusableView for GoToLine {
|
||||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||||
self.active_editor.focus_handle(cx)
|
self.line_editor.focus_handle(cx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl EventEmitter<Manager> for GoToLine {}
|
impl EventEmitter<DismissEvent> for GoToLine {}
|
||||||
|
|
||||||
impl GoToLine {
|
impl GoToLine {
|
||||||
fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
|
fn register(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
|
||||||
workspace.register_action(|workspace, _: &Toggle, cx| {
|
let handle = cx.view().downgrade();
|
||||||
let Some(editor) = workspace
|
editor.register_action(move |_: &Toggle, cx| {
|
||||||
.active_item(cx)
|
let Some(editor) = handle.upgrade() else {
|
||||||
.and_then(|active_item| active_item.downcast::<Editor>())
|
|
||||||
else {
|
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
let Some(workspace) = editor.read(cx).workspace() else {
|
||||||
workspace.toggle_modal(cx, move |cx| GoToLine::new(editor, cx));
|
return;
|
||||||
|
};
|
||||||
|
workspace.update(cx, |workspace, cx| {
|
||||||
|
workspace.toggle_modal(cx, move |cx| GoToLine::new(editor, cx));
|
||||||
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,7 +90,7 @@ impl GoToLine {
|
|||||||
) {
|
) {
|
||||||
match event {
|
match event {
|
||||||
// todo!() this isn't working...
|
// todo!() this isn't working...
|
||||||
editor::EditorEvent::Blurred => cx.emit(Manager::Dismiss),
|
editor::EditorEvent::Blurred => cx.emit(DismissEvent::Dismiss),
|
||||||
editor::EditorEvent::BufferEdited { .. } => self.highlight_current_line(cx),
|
editor::EditorEvent::BufferEdited { .. } => self.highlight_current_line(cx),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@ -123,7 +125,7 @@ impl GoToLine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||||
cx.emit(Manager::Dismiss);
|
cx.emit(DismissEvent::Dismiss);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
|
fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
|
||||||
@ -140,7 +142,7 @@ impl GoToLine {
|
|||||||
self.prev_scroll_position.take();
|
self.prev_scroll_position.take();
|
||||||
}
|
}
|
||||||
|
|
||||||
cx.emit(Manager::Dismiss);
|
cx.emit(DismissEvent::Dismiss);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -176,7 +178,7 @@ impl Render for GoToLine {
|
|||||||
.justify_between()
|
.justify_between()
|
||||||
.px_2()
|
.px_2()
|
||||||
.py_1()
|
.py_1()
|
||||||
.child(Label::new(self.current_text.clone()).color(TextColor::Muted)),
|
.child(Label::new(self.current_text.clone()).color(Color::Muted)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ pub use entity_map::*;
|
|||||||
pub use model_context::*;
|
pub use model_context::*;
|
||||||
use refineable::Refineable;
|
use refineable::Refineable;
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
|
use smol::future::FutureExt;
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
pub use test_context::*;
|
pub use test_context::*;
|
||||||
|
|
||||||
@ -579,7 +580,7 @@ impl AppContext {
|
|||||||
.windows
|
.windows
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|(_, window)| {
|
.filter_map(|(_, window)| {
|
||||||
let window = window.as_ref().unwrap();
|
let window = window.as_ref()?;
|
||||||
if window.dirty {
|
if window.dirty {
|
||||||
Some(window.handle.clone())
|
Some(window.handle.clone())
|
||||||
} else {
|
} else {
|
||||||
@ -983,6 +984,22 @@ impl AppContext {
|
|||||||
pub fn all_action_names(&self) -> &[SharedString] {
|
pub fn all_action_names(&self) -> &[SharedString] {
|
||||||
self.actions.all_action_names()
|
self.actions.all_action_names()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn on_app_quit<Fut>(
|
||||||
|
&mut self,
|
||||||
|
mut on_quit: impl FnMut(&mut AppContext) -> Fut + 'static,
|
||||||
|
) -> Subscription
|
||||||
|
where
|
||||||
|
Fut: 'static + Future<Output = ()>,
|
||||||
|
{
|
||||||
|
self.quit_observers.insert(
|
||||||
|
(),
|
||||||
|
Box::new(move |cx| {
|
||||||
|
let future = on_quit(cx);
|
||||||
|
async move { future.await }.boxed_local()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Context for AppContext {
|
impl Context for AppContext {
|
||||||
@ -1032,7 +1049,9 @@ impl Context for AppContext {
|
|||||||
let root_view = window.root_view.clone().unwrap();
|
let root_view = window.root_view.clone().unwrap();
|
||||||
let result = update(root_view, &mut WindowContext::new(cx, &mut window));
|
let result = update(root_view, &mut WindowContext::new(cx, &mut window));
|
||||||
|
|
||||||
if !window.removed {
|
if window.removed {
|
||||||
|
cx.windows.remove(handle.id);
|
||||||
|
} else {
|
||||||
cx.windows
|
cx.windows
|
||||||
.get_mut(handle.id)
|
.get_mut(handle.id)
|
||||||
.ok_or_else(|| anyhow!("window not found"))?
|
.ok_or_else(|| anyhow!("window not found"))?
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
AnyView, AnyWindowHandle, AppCell, AppContext, BackgroundExecutor, Context, FocusableView,
|
AnyView, AnyWindowHandle, AppCell, AppContext, BackgroundExecutor, Context, DismissEvent,
|
||||||
ForegroundExecutor, Manager, Model, ModelContext, Render, Result, Task, View, ViewContext,
|
FocusableView, ForegroundExecutor, Model, ModelContext, Render, Result, Task, View,
|
||||||
VisualContext, WindowContext, WindowHandle,
|
ViewContext, VisualContext, WindowContext, WindowHandle,
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, Context as _};
|
use anyhow::{anyhow, Context as _};
|
||||||
use derive_more::{Deref, DerefMut};
|
use derive_more::{Deref, DerefMut};
|
||||||
@ -326,7 +326,7 @@ impl VisualContext for AsyncWindowContext {
|
|||||||
V: crate::ManagedView,
|
V: crate::ManagedView,
|
||||||
{
|
{
|
||||||
self.window.update(self, |_, cx| {
|
self.window.update(self, |_, cx| {
|
||||||
view.update(cx, |_, cx| cx.emit(Manager::Dismiss))
|
view.update(cx, |_, cx| cx.emit(DismissEvent::Dismiss))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -611,7 +611,7 @@ impl<'a> VisualContext for VisualTestContext<'a> {
|
|||||||
{
|
{
|
||||||
self.window
|
self.window
|
||||||
.update(self.cx, |_, cx| {
|
.update(self.cx, |_, cx| {
|
||||||
view.update(cx, |_, cx| cx.emit(crate::Manager::Dismiss))
|
view.update(cx, |_, cx| cx.emit(crate::DismissEvent::Dismiss))
|
||||||
})
|
})
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
@ -12,15 +12,15 @@ pub trait Render: 'static + Sized {
|
|||||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element;
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait RenderOnce: Sized {
|
pub trait IntoElement: Sized {
|
||||||
type Element: Element + 'static;
|
type Element: Element + 'static;
|
||||||
|
|
||||||
fn element_id(&self) -> Option<ElementId>;
|
fn element_id(&self) -> Option<ElementId>;
|
||||||
|
|
||||||
fn render_once(self) -> Self::Element;
|
fn into_element(self) -> Self::Element;
|
||||||
|
|
||||||
fn render_into_any(self) -> AnyElement {
|
fn into_any_element(self) -> AnyElement {
|
||||||
self.render_once().into_any()
|
self.into_element().into_any()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw<T, R>(
|
fn draw<T, R>(
|
||||||
@ -33,7 +33,7 @@ pub trait RenderOnce: Sized {
|
|||||||
where
|
where
|
||||||
T: Clone + Default + Debug + Into<AvailableSpace>,
|
T: Clone + Default + Debug + Into<AvailableSpace>,
|
||||||
{
|
{
|
||||||
let element = self.render_once();
|
let element = self.into_element();
|
||||||
let element_id = element.element_id();
|
let element_id = element.element_id();
|
||||||
let element = DrawableElement {
|
let element = DrawableElement {
|
||||||
element: Some(element),
|
element: Some(element),
|
||||||
@ -57,7 +57,7 @@ pub trait RenderOnce: Sized {
|
|||||||
fn map<U>(self, f: impl FnOnce(Self) -> U) -> U
|
fn map<U>(self, f: impl FnOnce(Self) -> U) -> U
|
||||||
where
|
where
|
||||||
Self: Sized,
|
Self: Sized,
|
||||||
U: RenderOnce,
|
U: IntoElement,
|
||||||
{
|
{
|
||||||
f(self)
|
f(self)
|
||||||
}
|
}
|
||||||
@ -83,7 +83,7 @@ pub trait RenderOnce: Sized {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Element: 'static + RenderOnce {
|
pub trait Element: 'static + IntoElement {
|
||||||
type State: 'static;
|
type State: 'static;
|
||||||
|
|
||||||
fn layout(
|
fn layout(
|
||||||
@ -99,30 +99,30 @@ pub trait Element: 'static + RenderOnce {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Component: 'static {
|
pub trait RenderOnce: 'static {
|
||||||
type Rendered: RenderOnce;
|
type Rendered: IntoElement;
|
||||||
|
|
||||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered;
|
fn render(self, cx: &mut WindowContext) -> Self::Rendered;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct CompositeElement<C> {
|
pub struct Component<C> {
|
||||||
component: Option<C>,
|
component: Option<C>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct CompositeElementState<C: Component> {
|
pub struct CompositeElementState<C: RenderOnce> {
|
||||||
rendered_element: Option<<C::Rendered as RenderOnce>::Element>,
|
rendered_element: Option<<C::Rendered as IntoElement>::Element>,
|
||||||
rendered_element_state: <<C::Rendered as RenderOnce>::Element as Element>::State,
|
rendered_element_state: <<C::Rendered as IntoElement>::Element as Element>::State,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<C> CompositeElement<C> {
|
impl<C> Component<C> {
|
||||||
pub fn new(component: C) -> Self {
|
pub fn new(component: C) -> Self {
|
||||||
CompositeElement {
|
Component {
|
||||||
component: Some(component),
|
component: Some(component),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<C: Component> Element for CompositeElement<C> {
|
impl<C: RenderOnce> Element for Component<C> {
|
||||||
type State = CompositeElementState<C>;
|
type State = CompositeElementState<C>;
|
||||||
|
|
||||||
fn layout(
|
fn layout(
|
||||||
@ -130,7 +130,7 @@ impl<C: Component> Element for CompositeElement<C> {
|
|||||||
state: Option<Self::State>,
|
state: Option<Self::State>,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
) -> (LayoutId, Self::State) {
|
) -> (LayoutId, Self::State) {
|
||||||
let mut element = self.component.take().unwrap().render(cx).render_once();
|
let mut element = self.component.take().unwrap().render(cx).into_element();
|
||||||
let (layout_id, state) = element.layout(state.map(|s| s.rendered_element_state), cx);
|
let (layout_id, state) = element.layout(state.map(|s| s.rendered_element_state), cx);
|
||||||
let state = CompositeElementState {
|
let state = CompositeElementState {
|
||||||
rendered_element: Some(element),
|
rendered_element: Some(element),
|
||||||
@ -148,14 +148,14 @@ impl<C: Component> Element for CompositeElement<C> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<C: Component> RenderOnce for CompositeElement<C> {
|
impl<C: RenderOnce> IntoElement for Component<C> {
|
||||||
type Element = Self;
|
type Element = Self;
|
||||||
|
|
||||||
fn element_id(&self) -> Option<ElementId> {
|
fn element_id(&self) -> Option<ElementId> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_once(self) -> Self::Element {
|
fn into_element(self) -> Self::Element {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -166,23 +166,20 @@ pub struct GlobalElementId(SmallVec<[ElementId; 32]>);
|
|||||||
pub trait ParentElement {
|
pub trait ParentElement {
|
||||||
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]>;
|
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]>;
|
||||||
|
|
||||||
fn child(mut self, child: impl RenderOnce) -> Self
|
fn child(mut self, child: impl IntoElement) -> Self
|
||||||
where
|
where
|
||||||
Self: Sized,
|
Self: Sized,
|
||||||
{
|
{
|
||||||
self.children_mut().push(child.render_once().into_any());
|
self.children_mut().push(child.into_element().into_any());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
fn children(mut self, children: impl IntoIterator<Item = impl RenderOnce>) -> Self
|
fn children(mut self, children: impl IntoIterator<Item = impl IntoElement>) -> Self
|
||||||
where
|
where
|
||||||
Self: Sized,
|
Self: Sized,
|
||||||
{
|
{
|
||||||
self.children_mut().extend(
|
self.children_mut()
|
||||||
children
|
.extend(children.into_iter().map(|child| child.into_any_element()));
|
||||||
.into_iter()
|
|
||||||
.map(|child| child.render_once().into_any()),
|
|
||||||
);
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -432,10 +429,6 @@ impl AnyElement {
|
|||||||
AnyElement(Box::new(Some(DrawableElement::new(element))) as Box<dyn ElementObject>)
|
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 {
|
pub fn layout(&mut self, cx: &mut WindowContext) -> LayoutId {
|
||||||
self.0.layout(cx)
|
self.0.layout(cx)
|
||||||
}
|
}
|
||||||
@ -467,6 +460,10 @@ impl AnyElement {
|
|||||||
pub fn into_any(self) -> AnyElement {
|
pub fn into_any(self) -> AnyElement {
|
||||||
AnyElement::new(self)
|
AnyElement::new(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn inner_id(&self) -> Option<ElementId> {
|
||||||
|
self.0.element_id()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Element for AnyElement {
|
impl Element for AnyElement {
|
||||||
@ -486,14 +483,14 @@ impl Element for AnyElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderOnce for AnyElement {
|
impl IntoElement for AnyElement {
|
||||||
type Element = Self;
|
type Element = Self;
|
||||||
|
|
||||||
fn element_id(&self) -> Option<ElementId> {
|
fn element_id(&self) -> Option<ElementId> {
|
||||||
AnyElement::element_id(self)
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_once(self) -> Self::Element {
|
fn into_element(self) -> Self::Element {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
point, px, Action, AnyDrag, AnyElement, AnyTooltip, AnyView, AppContext, BorrowAppContext,
|
point, px, Action, AnyDrag, AnyElement, AnyTooltip, AnyView, AppContext, BorrowAppContext,
|
||||||
BorrowWindow, Bounds, ClickEvent, DispatchPhase, Element, ElementId, FocusEvent, FocusHandle,
|
BorrowWindow, Bounds, ClickEvent, DispatchPhase, Element, ElementId, FocusEvent, FocusHandle,
|
||||||
KeyContext, KeyDownEvent, KeyUpEvent, LayoutId, MouseButton, MouseDownEvent, MouseMoveEvent,
|
IntoElement, KeyContext, KeyDownEvent, KeyUpEvent, LayoutId, MouseButton, MouseDownEvent,
|
||||||
MouseUpEvent, ParentElement, Pixels, Point, Render, RenderOnce, ScrollWheelEvent, SharedString,
|
MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Point, Render, ScrollWheelEvent,
|
||||||
Size, Style, StyleRefinement, Styled, Task, View, Visibility, WindowContext,
|
SharedString, Size, Style, StyleRefinement, Styled, Task, View, Visibility, WindowContext,
|
||||||
};
|
};
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use refineable::Refineable;
|
use refineable::Refineable;
|
||||||
@ -666,14 +666,14 @@ impl Element for Div {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderOnce for Div {
|
impl IntoElement for Div {
|
||||||
type Element = Self;
|
type Element = Self;
|
||||||
|
|
||||||
fn element_id(&self) -> Option<ElementId> {
|
fn element_id(&self) -> Option<ElementId> {
|
||||||
self.interactivity.element_id.clone()
|
self.interactivity.element_id.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_once(self) -> Self::Element {
|
fn into_element(self) -> Self::Element {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1278,7 +1278,7 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<E> RenderOnce for Focusable<E>
|
impl<E> IntoElement for Focusable<E>
|
||||||
where
|
where
|
||||||
E: Element,
|
E: Element,
|
||||||
{
|
{
|
||||||
@ -1288,7 +1288,7 @@ where
|
|||||||
self.element.element_id()
|
self.element.element_id()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_once(self) -> Self::Element {
|
fn into_element(self) -> Self::Element {
|
||||||
self.element
|
self.element
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1352,7 +1352,7 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<E> RenderOnce for Stateful<E>
|
impl<E> IntoElement for Stateful<E>
|
||||||
where
|
where
|
||||||
E: Element,
|
E: Element,
|
||||||
{
|
{
|
||||||
@ -1362,7 +1362,7 @@ where
|
|||||||
self.element.element_id()
|
self.element.element_id()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_once(self) -> Self::Element {
|
fn into_element(self) -> Self::Element {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,30 +1,59 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Bounds, Element, InteractiveElement, InteractiveElementState, Interactivity, LayoutId, Pixels,
|
Bounds, Element, ImageData, InteractiveElement, InteractiveElementState, Interactivity,
|
||||||
RenderOnce, SharedString, StyleRefinement, Styled, WindowContext,
|
IntoElement, LayoutId, Pixels, SharedString, StyleRefinement, Styled, WindowContext,
|
||||||
};
|
};
|
||||||
use futures::FutureExt;
|
use futures::FutureExt;
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum ImageSource {
|
||||||
|
/// Image content will be loaded from provided URI at render time.
|
||||||
|
Uri(SharedString),
|
||||||
|
Data(Arc<ImageData>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SharedString> for ImageSource {
|
||||||
|
fn from(value: SharedString) -> Self {
|
||||||
|
Self::Uri(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Arc<ImageData>> for ImageSource {
|
||||||
|
fn from(value: Arc<ImageData>) -> Self {
|
||||||
|
Self::Data(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Img {
|
pub struct Img {
|
||||||
interactivity: Interactivity,
|
interactivity: Interactivity,
|
||||||
uri: Option<SharedString>,
|
source: Option<ImageSource>,
|
||||||
grayscale: bool,
|
grayscale: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn img() -> Img {
|
pub fn img() -> Img {
|
||||||
Img {
|
Img {
|
||||||
interactivity: Interactivity::default(),
|
interactivity: Interactivity::default(),
|
||||||
uri: None,
|
source: None,
|
||||||
grayscale: false,
|
grayscale: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Img {
|
impl Img {
|
||||||
pub fn uri(mut self, uri: impl Into<SharedString>) -> Self {
|
pub fn uri(mut self, uri: impl Into<SharedString>) -> Self {
|
||||||
self.uri = Some(uri.into());
|
self.source = Some(ImageSource::from(uri.into()));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
pub fn data(mut self, data: Arc<ImageData>) -> Self {
|
||||||
|
self.source = Some(ImageSource::from(data));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn source(mut self, source: impl Into<ImageSource>) -> Self {
|
||||||
|
self.source = Some(source.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
pub fn grayscale(mut self, grayscale: bool) -> Self {
|
pub fn grayscale(mut self, grayscale: bool) -> Self {
|
||||||
self.grayscale = grayscale;
|
self.grayscale = grayscale;
|
||||||
self
|
self
|
||||||
@ -58,42 +87,47 @@ impl Element for Img {
|
|||||||
|style, _scroll_offset, cx| {
|
|style, _scroll_offset, cx| {
|
||||||
let corner_radii = style.corner_radii;
|
let corner_radii = style.corner_radii;
|
||||||
|
|
||||||
if let Some(uri) = self.uri.clone() {
|
if let Some(source) = self.source {
|
||||||
// eprintln!(">>> image_cache.get({uri}");
|
let image = match source {
|
||||||
let image_future = cx.image_cache.get(uri.clone());
|
ImageSource::Uri(uri) => {
|
||||||
// eprintln!("<<< image_cache.get({uri}");
|
let image_future = cx.image_cache.get(uri.clone());
|
||||||
if let Some(data) = image_future
|
if let Some(data) = image_future
|
||||||
.clone()
|
.clone()
|
||||||
.now_or_never()
|
.now_or_never()
|
||||||
.and_then(|result| result.ok())
|
.and_then(|result| result.ok())
|
||||||
{
|
{
|
||||||
let corner_radii = corner_radii.to_pixels(bounds.size, cx.rem_size());
|
data
|
||||||
cx.with_z_index(1, |cx| {
|
} else {
|
||||||
cx.paint_image(bounds, corner_radii, data, self.grayscale)
|
cx.spawn(|mut cx| async move {
|
||||||
.log_err()
|
if image_future.await.ok().is_some() {
|
||||||
});
|
cx.on_next_frame(|cx| cx.notify());
|
||||||
} else {
|
}
|
||||||
cx.spawn(|mut cx| async move {
|
})
|
||||||
if image_future.await.ok().is_some() {
|
.detach();
|
||||||
cx.on_next_frame(|cx| cx.notify());
|
return;
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
.detach()
|
ImageSource::Data(image) => image,
|
||||||
}
|
};
|
||||||
|
let corner_radii = corner_radii.to_pixels(bounds.size, cx.rem_size());
|
||||||
|
cx.with_z_index(1, |cx| {
|
||||||
|
cx.paint_image(bounds, corner_radii, image, self.grayscale)
|
||||||
|
.log_err()
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderOnce for Img {
|
impl IntoElement for Img {
|
||||||
type Element = Self;
|
type Element = Self;
|
||||||
|
|
||||||
fn element_id(&self) -> Option<crate::ElementId> {
|
fn element_id(&self) -> Option<crate::ElementId> {
|
||||||
self.interactivity.element_id.clone()
|
self.interactivity.element_id.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_once(self) -> Self::Element {
|
fn into_element(self) -> Self::Element {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,8 +2,8 @@ use smallvec::SmallVec;
|
|||||||
use taffy::style::{Display, Position};
|
use taffy::style::{Display, Position};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
point, AnyElement, BorrowWindow, Bounds, Element, LayoutId, ParentElement, Pixels, Point,
|
point, AnyElement, BorrowWindow, Bounds, Element, IntoElement, LayoutId, ParentElement, Pixels,
|
||||||
RenderOnce, Size, Style, WindowContext,
|
Point, Size, Style, WindowContext,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct OverlayState {
|
pub struct OverlayState {
|
||||||
@ -144,21 +144,23 @@ impl Element for Overlay {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cx.with_element_offset(desired.origin - bounds.origin, |cx| {
|
cx.with_element_offset(desired.origin - bounds.origin, |cx| {
|
||||||
for child in self.children {
|
cx.break_content_mask(|cx| {
|
||||||
child.paint(cx);
|
for child in self.children {
|
||||||
}
|
child.paint(cx);
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderOnce for Overlay {
|
impl IntoElement for Overlay {
|
||||||
type Element = Self;
|
type Element = Self;
|
||||||
|
|
||||||
fn element_id(&self) -> Option<crate::ElementId> {
|
fn element_id(&self) -> Option<crate::ElementId> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_once(self) -> Self::Element {
|
fn into_element(self) -> Self::Element {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
Bounds, Element, ElementId, InteractiveElement, InteractiveElementState, Interactivity,
|
Bounds, Element, ElementId, InteractiveElement, InteractiveElementState, Interactivity,
|
||||||
LayoutId, Pixels, RenderOnce, SharedString, StyleRefinement, Styled, WindowContext,
|
IntoElement, LayoutId, Pixels, SharedString, StyleRefinement, Styled, WindowContext,
|
||||||
};
|
};
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
|
|
||||||
@ -49,14 +49,14 @@ impl Element for Svg {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderOnce for Svg {
|
impl IntoElement for Svg {
|
||||||
type Element = Self;
|
type Element = Self;
|
||||||
|
|
||||||
fn element_id(&self) -> Option<ElementId> {
|
fn element_id(&self) -> Option<ElementId> {
|
||||||
self.interactivity.element_id.clone()
|
self.interactivity.element_id.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_once(self) -> Self::Element {
|
fn into_element(self) -> Self::Element {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
Bounds, Element, ElementId, LayoutId, Pixels, RenderOnce, SharedString, Size, TextRun,
|
Bounds, DispatchPhase, Element, ElementId, IntoElement, LayoutId, MouseDownEvent, MouseUpEvent,
|
||||||
WindowContext, WrappedLine,
|
Pixels, Point, SharedString, Size, TextRun, WhiteSpace, WindowContext, WrappedLine,
|
||||||
};
|
};
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use parking_lot::{Mutex, MutexGuard};
|
use parking_lot::{Mutex, MutexGuard};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use std::{cell::Cell, rc::Rc, sync::Arc};
|
use std::{cell::Cell, ops::Range, rc::Rc, sync::Arc};
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
|
|
||||||
impl Element for &'static str {
|
impl Element for &'static str {
|
||||||
@ -26,14 +26,14 @@ impl Element for &'static str {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderOnce for &'static str {
|
impl IntoElement for &'static str {
|
||||||
type Element = Self;
|
type Element = Self;
|
||||||
|
|
||||||
fn element_id(&self) -> Option<ElementId> {
|
fn element_id(&self) -> Option<ElementId> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_once(self) -> Self::Element {
|
fn into_element(self) -> Self::Element {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -57,35 +57,40 @@ impl Element for SharedString {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderOnce for SharedString {
|
impl IntoElement for SharedString {
|
||||||
type Element = Self;
|
type Element = Self;
|
||||||
|
|
||||||
fn element_id(&self) -> Option<ElementId> {
|
fn element_id(&self) -> Option<ElementId> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_once(self) -> Self::Element {
|
fn into_element(self) -> Self::Element {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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 struct StyledText {
|
pub struct StyledText {
|
||||||
text: SharedString,
|
text: SharedString,
|
||||||
runs: Option<Vec<TextRun>>,
|
runs: Option<Vec<TextRun>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StyledText {
|
impl StyledText {
|
||||||
/// Renders text with runs of different styles.
|
pub fn new(text: impl Into<SharedString>) -> Self {
|
||||||
///
|
|
||||||
/// 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 new(text: SharedString, runs: Vec<TextRun>) -> Self {
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text,
|
text: text.into(),
|
||||||
runs: Some(runs),
|
runs: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_runs(mut self, runs: Vec<TextRun>) -> Self {
|
||||||
|
self.runs = Some(runs);
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Element for StyledText {
|
impl Element for StyledText {
|
||||||
@ -106,14 +111,14 @@ impl Element for StyledText {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderOnce for StyledText {
|
impl IntoElement for StyledText {
|
||||||
type Element = Self;
|
type Element = Self;
|
||||||
|
|
||||||
fn element_id(&self) -> Option<crate::ElementId> {
|
fn element_id(&self) -> Option<crate::ElementId> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_once(self) -> Self::Element {
|
fn into_element(self) -> Self::Element {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -159,10 +164,14 @@ impl TextState {
|
|||||||
let element_state = self.clone();
|
let element_state = self.clone();
|
||||||
|
|
||||||
move |known_dimensions, available_space| {
|
move |known_dimensions, available_space| {
|
||||||
let wrap_width = known_dimensions.width.or(match available_space.width {
|
let wrap_width = if text_style.white_space == WhiteSpace::Normal {
|
||||||
crate::AvailableSpace::Definite(x) => Some(x),
|
known_dimensions.width.or(match available_space.width {
|
||||||
_ => None,
|
crate::AvailableSpace::Definite(x) => Some(x),
|
||||||
});
|
_ => None,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
if let Some(text_state) = element_state.0.lock().as_ref() {
|
if let Some(text_state) = element_state.0.lock().as_ref() {
|
||||||
if text_state.size.is_some()
|
if text_state.size.is_some()
|
||||||
@ -174,10 +183,7 @@ impl TextState {
|
|||||||
|
|
||||||
let Some(lines) = text_system
|
let Some(lines) = text_system
|
||||||
.shape_text(
|
.shape_text(
|
||||||
&text,
|
&text, font_size, &runs, wrap_width, // Wrap if we know the width.
|
||||||
font_size,
|
|
||||||
&runs[..],
|
|
||||||
wrap_width, // Wrap if we know the width.
|
|
||||||
)
|
)
|
||||||
.log_err()
|
.log_err()
|
||||||
else {
|
else {
|
||||||
@ -194,7 +200,7 @@ impl TextState {
|
|||||||
for line in &lines {
|
for line in &lines {
|
||||||
let line_size = line.size(line_height);
|
let line_size = line.size(line_height);
|
||||||
size.height += line_size.height;
|
size.height += line_size.height;
|
||||||
size.width = size.width.max(line_size.width);
|
size.width = size.width.max(line_size.width).ceil();
|
||||||
}
|
}
|
||||||
|
|
||||||
element_state.lock().replace(TextStateInner {
|
element_state.lock().replace(TextStateInner {
|
||||||
@ -225,16 +231,77 @@ impl TextState {
|
|||||||
line_origin.y += line.size(line_height).height;
|
line_origin.y += line.size(line_height).height;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn index_for_position(&self, bounds: Bounds<Pixels>, position: Point<Pixels>) -> Option<usize> {
|
||||||
|
if !bounds.contains_point(&position) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let element_state = self.lock();
|
||||||
|
let element_state = element_state
|
||||||
|
.as_ref()
|
||||||
|
.expect("measurement has not been performed");
|
||||||
|
|
||||||
|
let line_height = element_state.line_height;
|
||||||
|
let mut line_origin = bounds.origin;
|
||||||
|
let mut line_start_ix = 0;
|
||||||
|
for line in &element_state.lines {
|
||||||
|
let line_bottom = line_origin.y + line.size(line_height).height;
|
||||||
|
if position.y > line_bottom {
|
||||||
|
line_origin.y = line_bottom;
|
||||||
|
line_start_ix += line.len() + 1;
|
||||||
|
} else {
|
||||||
|
let position_within_line = position - line_origin;
|
||||||
|
let index_within_line =
|
||||||
|
line.index_for_position(position_within_line, line_height)?;
|
||||||
|
return Some(line_start_ix + index_within_line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct InteractiveText {
|
pub struct InteractiveText {
|
||||||
element_id: ElementId,
|
element_id: ElementId,
|
||||||
text: StyledText,
|
text: StyledText,
|
||||||
|
click_listener: Option<Box<dyn Fn(InteractiveTextClickEvent, &mut WindowContext<'_>)>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct InteractiveTextState {
|
struct InteractiveTextClickEvent {
|
||||||
|
mouse_down_index: usize,
|
||||||
|
mouse_up_index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct InteractiveTextState {
|
||||||
text_state: TextState,
|
text_state: TextState,
|
||||||
clicked_range_ixs: Rc<Cell<SmallVec<[usize; 1]>>>,
|
mouse_down_index: Rc<Cell<Option<usize>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InteractiveText {
|
||||||
|
pub fn new(id: impl Into<ElementId>, text: StyledText) -> Self {
|
||||||
|
Self {
|
||||||
|
element_id: id.into(),
|
||||||
|
text,
|
||||||
|
click_listener: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_click(
|
||||||
|
mut self,
|
||||||
|
ranges: Vec<Range<usize>>,
|
||||||
|
listener: impl Fn(usize, &mut WindowContext<'_>) + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.click_listener = Some(Box::new(move |event, cx| {
|
||||||
|
for (range_ix, range) in ranges.iter().enumerate() {
|
||||||
|
if range.contains(&event.mouse_down_index) && range.contains(&event.mouse_up_index)
|
||||||
|
{
|
||||||
|
listener(range_ix, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Element for InteractiveText {
|
impl Element for InteractiveText {
|
||||||
@ -246,39 +313,74 @@ impl Element for InteractiveText {
|
|||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
) -> (LayoutId, Self::State) {
|
) -> (LayoutId, Self::State) {
|
||||||
if let Some(InteractiveTextState {
|
if let Some(InteractiveTextState {
|
||||||
text_state,
|
mouse_down_index, ..
|
||||||
clicked_range_ixs,
|
|
||||||
}) = state
|
}) = state
|
||||||
{
|
{
|
||||||
let (layout_id, text_state) = self.text.layout(Some(text_state), cx);
|
let (layout_id, text_state) = self.text.layout(None, cx);
|
||||||
let element_state = InteractiveTextState {
|
let element_state = InteractiveTextState {
|
||||||
text_state,
|
text_state,
|
||||||
clicked_range_ixs,
|
mouse_down_index,
|
||||||
};
|
};
|
||||||
(layout_id, element_state)
|
(layout_id, element_state)
|
||||||
} else {
|
} else {
|
||||||
let (layout_id, text_state) = self.text.layout(None, cx);
|
let (layout_id, text_state) = self.text.layout(None, cx);
|
||||||
let element_state = InteractiveTextState {
|
let element_state = InteractiveTextState {
|
||||||
text_state,
|
text_state,
|
||||||
clicked_range_ixs: Rc::default(),
|
mouse_down_index: Rc::default(),
|
||||||
};
|
};
|
||||||
(layout_id, element_state)
|
(layout_id, element_state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn paint(self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
|
fn paint(self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
|
||||||
|
if let Some(click_listener) = self.click_listener {
|
||||||
|
let text_state = state.text_state.clone();
|
||||||
|
let mouse_down = state.mouse_down_index.clone();
|
||||||
|
if let Some(mouse_down_index) = mouse_down.get() {
|
||||||
|
cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| {
|
||||||
|
if phase == DispatchPhase::Bubble {
|
||||||
|
if let Some(mouse_up_index) =
|
||||||
|
text_state.index_for_position(bounds, event.position)
|
||||||
|
{
|
||||||
|
click_listener(
|
||||||
|
InteractiveTextClickEvent {
|
||||||
|
mouse_down_index,
|
||||||
|
mouse_up_index,
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
mouse_down.take();
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| {
|
||||||
|
if phase == DispatchPhase::Bubble {
|
||||||
|
if let Some(mouse_down_index) =
|
||||||
|
text_state.index_for_position(bounds, event.position)
|
||||||
|
{
|
||||||
|
mouse_down.set(Some(mouse_down_index));
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.text.paint(bounds, &mut state.text_state, cx)
|
self.text.paint(bounds, &mut state.text_state, cx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderOnce for InteractiveText {
|
impl IntoElement for InteractiveText {
|
||||||
type Element = Self;
|
type Element = Self;
|
||||||
|
|
||||||
fn element_id(&self) -> Option<ElementId> {
|
fn element_id(&self) -> Option<ElementId> {
|
||||||
Some(self.element_id.clone())
|
Some(self.element_id.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_once(self) -> Self::Element {
|
fn into_element(self) -> Self::Element {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
point, px, size, AnyElement, AvailableSpace, Bounds, Element, ElementId, InteractiveElement,
|
point, px, size, AnyElement, AvailableSpace, BorrowWindow, Bounds, ContentMask, Element,
|
||||||
InteractiveElementState, Interactivity, LayoutId, Pixels, Point, Render, RenderOnce, Size,
|
ElementId, InteractiveElement, InteractiveElementState, Interactivity, IntoElement, LayoutId,
|
||||||
StyleRefinement, Styled, View, ViewContext, WindowContext,
|
Pixels, Point, Render, Size, StyleRefinement, Styled, View, ViewContext, WindowContext,
|
||||||
};
|
};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
|
use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
|
||||||
@ -9,7 +9,7 @@ use taffy::style::Overflow;
|
|||||||
|
|
||||||
/// uniform_list provides lazy rendering for a set of items that are of uniform height.
|
/// uniform_list provides lazy rendering for a set of items that are of uniform height.
|
||||||
/// When rendered into a container with overflow-y: hidden and a fixed (or max) height,
|
/// When rendered into a container with overflow-y: hidden and a fixed (or max) height,
|
||||||
/// uniform_list will only render the visibile subset of items.
|
/// uniform_list will only render the visible subset of items.
|
||||||
pub fn uniform_list<I, R, V>(
|
pub fn uniform_list<I, R, V>(
|
||||||
view: View<V>,
|
view: View<V>,
|
||||||
id: I,
|
id: I,
|
||||||
@ -18,30 +18,30 @@ pub fn uniform_list<I, R, V>(
|
|||||||
) -> UniformList
|
) -> UniformList
|
||||||
where
|
where
|
||||||
I: Into<ElementId>,
|
I: Into<ElementId>,
|
||||||
R: RenderOnce,
|
R: IntoElement,
|
||||||
V: Render,
|
V: Render,
|
||||||
{
|
{
|
||||||
let id = id.into();
|
let id = id.into();
|
||||||
let mut style = StyleRefinement::default();
|
let mut base_style = StyleRefinement::default();
|
||||||
style.overflow.y = Some(Overflow::Hidden);
|
base_style.overflow.y = Some(Overflow::Scroll);
|
||||||
|
|
||||||
let render_range = move |range, cx: &mut WindowContext| {
|
let render_range = move |range, cx: &mut WindowContext| {
|
||||||
view.update(cx, |this, cx| {
|
view.update(cx, |this, cx| {
|
||||||
f(this, range, cx)
|
f(this, range, cx)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|component| component.render_into_any())
|
.map(|component| component.into_any_element())
|
||||||
.collect()
|
.collect()
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
UniformList {
|
UniformList {
|
||||||
id: id.clone(),
|
id: id.clone(),
|
||||||
style,
|
|
||||||
item_count,
|
item_count,
|
||||||
item_to_measure_index: 0,
|
item_to_measure_index: 0,
|
||||||
render_items: Box::new(render_range),
|
render_items: Box::new(render_range),
|
||||||
interactivity: Interactivity {
|
interactivity: Interactivity {
|
||||||
element_id: Some(id.into()),
|
element_id: Some(id.into()),
|
||||||
|
base_style,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
scroll_handle: None,
|
scroll_handle: None,
|
||||||
@ -50,7 +50,6 @@ where
|
|||||||
|
|
||||||
pub struct UniformList {
|
pub struct UniformList {
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
style: StyleRefinement,
|
|
||||||
item_count: usize,
|
item_count: usize,
|
||||||
item_to_measure_index: usize,
|
item_to_measure_index: usize,
|
||||||
render_items:
|
render_items:
|
||||||
@ -91,7 +90,7 @@ impl UniformListScrollHandle {
|
|||||||
|
|
||||||
impl Styled for UniformList {
|
impl Styled for UniformList {
|
||||||
fn style(&mut self) -> &mut StyleRefinement {
|
fn style(&mut self) -> &mut StyleRefinement {
|
||||||
&mut self.style
|
&mut self.interactivity.base_style
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -211,31 +210,31 @@ impl Element for UniformList {
|
|||||||
scroll_offset: shared_scroll_offset,
|
scroll_offset: shared_scroll_offset,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let visible_item_count = if item_height > px(0.) {
|
|
||||||
(padded_bounds.size.height / item_height).ceil() as usize + 1
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
};
|
|
||||||
|
|
||||||
let first_visible_element_ix =
|
let first_visible_element_ix =
|
||||||
(-scroll_offset.y / item_height).floor() as usize;
|
(-scroll_offset.y / item_height).floor() as usize;
|
||||||
|
let last_visible_element_ix =
|
||||||
|
((-scroll_offset.y + padded_bounds.size.height) / item_height).ceil()
|
||||||
|
as usize;
|
||||||
let visible_range = first_visible_element_ix
|
let visible_range = first_visible_element_ix
|
||||||
..cmp::min(
|
..cmp::min(last_visible_element_ix, self.item_count);
|
||||||
first_visible_element_ix + visible_item_count,
|
|
||||||
self.item_count,
|
|
||||||
);
|
|
||||||
|
|
||||||
let items = (self.render_items)(visible_range.clone(), cx);
|
let items = (self.render_items)(visible_range.clone(), cx);
|
||||||
cx.with_z_index(1, |cx| {
|
cx.with_z_index(1, |cx| {
|
||||||
for (item, ix) in items.into_iter().zip(visible_range) {
|
let content_mask = ContentMask {
|
||||||
let item_origin = padded_bounds.origin
|
bounds: padded_bounds,
|
||||||
+ point(px(0.), item_height * ix + scroll_offset.y);
|
};
|
||||||
let available_space = size(
|
cx.with_content_mask(Some(content_mask), |cx| {
|
||||||
AvailableSpace::Definite(padded_bounds.size.width),
|
for (item, ix) in items.into_iter().zip(visible_range) {
|
||||||
AvailableSpace::Definite(item_height),
|
let item_origin = padded_bounds.origin
|
||||||
);
|
+ point(px(0.), item_height * ix + scroll_offset.y);
|
||||||
item.draw(item_origin, available_space, cx);
|
let available_space = size(
|
||||||
}
|
AvailableSpace::Definite(padded_bounds.size.width),
|
||||||
|
AvailableSpace::Definite(item_height),
|
||||||
|
);
|
||||||
|
item.draw(item_origin, available_space, cx);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -244,14 +243,14 @@ impl Element for UniformList {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderOnce for UniformList {
|
impl IntoElement for UniformList {
|
||||||
type Element = Self;
|
type Element = Self;
|
||||||
|
|
||||||
fn element_id(&self) -> Option<crate::ElementId> {
|
fn element_id(&self) -> Option<crate::ElementId> {
|
||||||
Some(self.id.clone())
|
Some(self.id.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_once(self) -> Self::Element {
|
fn into_element(self) -> Self::Element {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
div, point, Div, Element, FocusHandle, Keystroke, Modifiers, Pixels, Point, Render, RenderOnce,
|
div, point, Div, Element, FocusHandle, IntoElement, Keystroke, Modifiers, Pixels, Point,
|
||||||
ViewContext,
|
Render, ViewContext,
|
||||||
};
|
};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use std::{any::Any, fmt::Debug, marker::PhantomData, ops::Deref, path::PathBuf};
|
use std::{any::Any, fmt::Debug, marker::PhantomData, ops::Deref, path::PathBuf};
|
||||||
@ -64,7 +64,7 @@ pub struct Drag<S, R, V, E>
|
|||||||
where
|
where
|
||||||
R: Fn(&mut V, &mut ViewContext<V>) -> E,
|
R: Fn(&mut V, &mut ViewContext<V>) -> E,
|
||||||
V: 'static,
|
V: 'static,
|
||||||
E: RenderOnce,
|
E: IntoElement,
|
||||||
{
|
{
|
||||||
pub state: S,
|
pub state: S,
|
||||||
pub render_drag_handle: R,
|
pub render_drag_handle: R,
|
||||||
@ -286,8 +286,8 @@ pub struct FocusEvent {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use crate::{
|
use crate::{
|
||||||
self as gpui, div, Div, FocusHandle, InteractiveElement, KeyBinding, Keystroke,
|
self as gpui, div, Div, FocusHandle, InteractiveElement, IntoElement, KeyBinding,
|
||||||
ParentElement, Render, RenderOnce, Stateful, TestAppContext, VisualContext,
|
Keystroke, ParentElement, Render, Stateful, TestAppContext, VisualContext,
|
||||||
};
|
};
|
||||||
|
|
||||||
struct TestView {
|
struct TestView {
|
||||||
@ -315,7 +315,7 @@ mod test {
|
|||||||
div()
|
div()
|
||||||
.key_context("nested")
|
.key_context("nested")
|
||||||
.track_focus(&self.focus_handle)
|
.track_focus(&self.focus_handle)
|
||||||
.render_once(),
|
.into_element(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -683,6 +683,9 @@ impl Drop for MacWindow {
|
|||||||
this.executor
|
this.executor
|
||||||
.spawn(async move {
|
.spawn(async move {
|
||||||
unsafe {
|
unsafe {
|
||||||
|
// todo!() this panic()s when you click the red close button
|
||||||
|
// unless should_close returns false.
|
||||||
|
// (luckliy in zed it always returns false)
|
||||||
window.close();
|
window.close();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
pub use crate::{
|
pub use crate::{
|
||||||
BorrowAppContext, BorrowWindow, Component, Context, Element, FocusableElement,
|
BorrowAppContext, BorrowWindow, Context, Element, FocusableElement, InteractiveElement,
|
||||||
InteractiveElement, ParentElement, Refineable, Render, RenderOnce, StatefulInteractiveElement,
|
IntoElement, ParentElement, Refineable, Render, RenderOnce, StatefulInteractiveElement, Styled,
|
||||||
Styled, VisualContext,
|
VisualContext,
|
||||||
};
|
};
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
|
use std::{iter, mem, ops::Range};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
black, phi, point, rems, AbsoluteLength, BorrowAppContext, BorrowWindow, Bounds, ContentMask,
|
black, phi, point, rems, AbsoluteLength, BorrowAppContext, BorrowWindow, Bounds, ContentMask,
|
||||||
Corners, CornersRefinement, CursorStyle, DefiniteLength, Edges, EdgesRefinement, Font,
|
Corners, CornersRefinement, CursorStyle, DefiniteLength, Edges, EdgesRefinement, Font,
|
||||||
FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Rgba,
|
FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Rgba,
|
||||||
SharedString, Size, SizeRefinement, Styled, TextRun, WindowContext,
|
SharedString, Size, SizeRefinement, Styled, TextRun, WindowContext,
|
||||||
};
|
};
|
||||||
|
use collections::HashSet;
|
||||||
use refineable::{Cascade, Refineable};
|
use refineable::{Cascade, Refineable};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
pub use taffy::style::{
|
pub use taffy::style::{
|
||||||
@ -128,6 +131,13 @@ pub struct BoxShadow {
|
|||||||
pub spread_radius: Pixels,
|
pub spread_radius: Pixels,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
|
||||||
|
pub enum WhiteSpace {
|
||||||
|
#[default]
|
||||||
|
Normal,
|
||||||
|
Nowrap,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Refineable, Clone, Debug)]
|
#[derive(Refineable, Clone, Debug)]
|
||||||
#[refineable(Debug)]
|
#[refineable(Debug)]
|
||||||
pub struct TextStyle {
|
pub struct TextStyle {
|
||||||
@ -138,7 +148,9 @@ pub struct TextStyle {
|
|||||||
pub line_height: DefiniteLength,
|
pub line_height: DefiniteLength,
|
||||||
pub font_weight: FontWeight,
|
pub font_weight: FontWeight,
|
||||||
pub font_style: FontStyle,
|
pub font_style: FontStyle,
|
||||||
|
pub background_color: Option<Hsla>,
|
||||||
pub underline: Option<UnderlineStyle>,
|
pub underline: Option<UnderlineStyle>,
|
||||||
|
pub white_space: WhiteSpace,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for TextStyle {
|
impl Default for TextStyle {
|
||||||
@ -151,13 +163,16 @@ impl Default for TextStyle {
|
|||||||
line_height: phi(),
|
line_height: phi(),
|
||||||
font_weight: FontWeight::default(),
|
font_weight: FontWeight::default(),
|
||||||
font_style: FontStyle::default(),
|
font_style: FontStyle::default(),
|
||||||
|
background_color: None,
|
||||||
underline: None,
|
underline: None,
|
||||||
|
white_space: WhiteSpace::Normal,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TextStyle {
|
impl TextStyle {
|
||||||
pub fn highlight(mut self, style: HighlightStyle) -> Self {
|
pub fn highlight(mut self, style: impl Into<HighlightStyle>) -> Self {
|
||||||
|
let style = style.into();
|
||||||
if let Some(weight) = style.font_weight {
|
if let Some(weight) = style.font_weight {
|
||||||
self.font_weight = weight;
|
self.font_weight = weight;
|
||||||
}
|
}
|
||||||
@ -173,6 +188,10 @@ impl TextStyle {
|
|||||||
self.color.fade_out(factor);
|
self.color.fade_out(factor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(background_color) = style.background_color {
|
||||||
|
self.background_color = Some(background_color);
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(underline) = style.underline {
|
if let Some(underline) = style.underline {
|
||||||
self.underline = Some(underline);
|
self.underline = Some(underline);
|
||||||
}
|
}
|
||||||
@ -203,7 +222,7 @@ impl TextStyle {
|
|||||||
style: self.font_style,
|
style: self.font_style,
|
||||||
},
|
},
|
||||||
color: self.color,
|
color: self.color,
|
||||||
background_color: None,
|
background_color: self.background_color,
|
||||||
underline: self.underline.clone(),
|
underline: self.underline.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -214,6 +233,7 @@ pub struct HighlightStyle {
|
|||||||
pub color: Option<Hsla>,
|
pub color: Option<Hsla>,
|
||||||
pub font_weight: Option<FontWeight>,
|
pub font_weight: Option<FontWeight>,
|
||||||
pub font_style: Option<FontStyle>,
|
pub font_style: Option<FontStyle>,
|
||||||
|
pub background_color: Option<Hsla>,
|
||||||
pub underline: Option<UnderlineStyle>,
|
pub underline: Option<UnderlineStyle>,
|
||||||
pub fade_out: Option<f32>,
|
pub fade_out: Option<f32>,
|
||||||
}
|
}
|
||||||
@ -432,6 +452,7 @@ impl From<&TextStyle> for HighlightStyle {
|
|||||||
color: Some(other.color),
|
color: Some(other.color),
|
||||||
font_weight: Some(other.font_weight),
|
font_weight: Some(other.font_weight),
|
||||||
font_style: Some(other.font_style),
|
font_style: Some(other.font_style),
|
||||||
|
background_color: other.background_color,
|
||||||
underline: other.underline.clone(),
|
underline: other.underline.clone(),
|
||||||
fade_out: None,
|
fade_out: None,
|
||||||
}
|
}
|
||||||
@ -458,6 +479,10 @@ impl HighlightStyle {
|
|||||||
self.font_style = other.font_style;
|
self.font_style = other.font_style;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if other.background_color.is_some() {
|
||||||
|
self.background_color = other.background_color;
|
||||||
|
}
|
||||||
|
|
||||||
if other.underline.is_some() {
|
if other.underline.is_some() {
|
||||||
self.underline = other.underline;
|
self.underline = other.underline;
|
||||||
}
|
}
|
||||||
@ -481,6 +506,24 @@ impl From<Hsla> for HighlightStyle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<FontWeight> for HighlightStyle {
|
||||||
|
fn from(font_weight: FontWeight) -> Self {
|
||||||
|
Self {
|
||||||
|
font_weight: Some(font_weight),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<FontStyle> for HighlightStyle {
|
||||||
|
fn from(font_style: FontStyle) -> Self {
|
||||||
|
Self {
|
||||||
|
font_style: Some(font_style),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<Rgba> for HighlightStyle {
|
impl From<Rgba> for HighlightStyle {
|
||||||
fn from(color: Rgba) -> Self {
|
fn from(color: Rgba) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@ -489,3 +532,140 @@ impl From<Rgba> for HighlightStyle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn combine_highlights(
|
||||||
|
a: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
|
||||||
|
b: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
|
||||||
|
) -> impl Iterator<Item = (Range<usize>, HighlightStyle)> {
|
||||||
|
let mut endpoints = Vec::new();
|
||||||
|
let mut highlights = Vec::new();
|
||||||
|
for (range, highlight) in a.into_iter().chain(b) {
|
||||||
|
if !range.is_empty() {
|
||||||
|
let highlight_id = highlights.len();
|
||||||
|
endpoints.push((range.start, highlight_id, true));
|
||||||
|
endpoints.push((range.end, highlight_id, false));
|
||||||
|
highlights.push(highlight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
endpoints.sort_unstable_by_key(|(position, _, _)| *position);
|
||||||
|
let mut endpoints = endpoints.into_iter().peekable();
|
||||||
|
|
||||||
|
let mut active_styles = HashSet::default();
|
||||||
|
let mut ix = 0;
|
||||||
|
iter::from_fn(move || {
|
||||||
|
while let Some((endpoint_ix, highlight_id, is_start)) = endpoints.peek() {
|
||||||
|
let prev_index = mem::replace(&mut ix, *endpoint_ix);
|
||||||
|
if ix > prev_index && !active_styles.is_empty() {
|
||||||
|
let mut current_style = HighlightStyle::default();
|
||||||
|
for highlight_id in &active_styles {
|
||||||
|
current_style.highlight(highlights[*highlight_id]);
|
||||||
|
}
|
||||||
|
return Some((prev_index..ix, current_style));
|
||||||
|
}
|
||||||
|
|
||||||
|
if *is_start {
|
||||||
|
active_styles.insert(*highlight_id);
|
||||||
|
} else {
|
||||||
|
active_styles.remove(highlight_id);
|
||||||
|
}
|
||||||
|
endpoints.next();
|
||||||
|
}
|
||||||
|
None
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::{blue, green, red, yellow};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_combine_highlights() {
|
||||||
|
assert_eq!(
|
||||||
|
combine_highlights(
|
||||||
|
[
|
||||||
|
(0..5, green().into()),
|
||||||
|
(4..10, FontWeight::BOLD.into()),
|
||||||
|
(15..20, yellow().into()),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
(2..6, FontStyle::Italic.into()),
|
||||||
|
(1..3, blue().into()),
|
||||||
|
(21..23, red().into()),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
[
|
||||||
|
(
|
||||||
|
0..1,
|
||||||
|
HighlightStyle {
|
||||||
|
color: Some(green()),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
),
|
||||||
|
(
|
||||||
|
1..2,
|
||||||
|
HighlightStyle {
|
||||||
|
color: Some(blue()),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
),
|
||||||
|
(
|
||||||
|
2..3,
|
||||||
|
HighlightStyle {
|
||||||
|
color: Some(blue()),
|
||||||
|
font_style: Some(FontStyle::Italic),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
),
|
||||||
|
(
|
||||||
|
3..4,
|
||||||
|
HighlightStyle {
|
||||||
|
color: Some(green()),
|
||||||
|
font_style: Some(FontStyle::Italic),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
),
|
||||||
|
(
|
||||||
|
4..5,
|
||||||
|
HighlightStyle {
|
||||||
|
color: Some(green()),
|
||||||
|
font_weight: Some(FontWeight::BOLD),
|
||||||
|
font_style: Some(FontStyle::Italic),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
),
|
||||||
|
(
|
||||||
|
5..6,
|
||||||
|
HighlightStyle {
|
||||||
|
font_weight: Some(FontWeight::BOLD),
|
||||||
|
font_style: Some(FontStyle::Italic),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
),
|
||||||
|
(
|
||||||
|
6..10,
|
||||||
|
HighlightStyle {
|
||||||
|
font_weight: Some(FontWeight::BOLD),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
),
|
||||||
|
(
|
||||||
|
15..20,
|
||||||
|
HighlightStyle {
|
||||||
|
color: Some(yellow()),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
),
|
||||||
|
(
|
||||||
|
21..23,
|
||||||
|
HighlightStyle {
|
||||||
|
color: Some(red()),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
self as gpui, hsla, point, px, relative, rems, AbsoluteLength, AlignItems, CursorStyle,
|
self as gpui, hsla, point, px, relative, rems, AbsoluteLength, AlignItems, CursorStyle,
|
||||||
DefiniteLength, Display, Fill, FlexDirection, Hsla, JustifyContent, Length, Position,
|
DefiniteLength, Display, Fill, FlexDirection, Hsla, JustifyContent, Length, Position,
|
||||||
SharedString, StyleRefinement, Visibility,
|
SharedString, StyleRefinement, Visibility, WhiteSpace,
|
||||||
};
|
};
|
||||||
use crate::{BoxShadow, TextStyleRefinement};
|
use crate::{BoxShadow, TextStyleRefinement};
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
@ -101,6 +101,24 @@ pub trait Styled: Sized {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets the whitespace of the element to `normal`.
|
||||||
|
/// [Docs](https://tailwindcss.com/docs/whitespace#normal)
|
||||||
|
fn whitespace_normal(mut self) -> Self {
|
||||||
|
self.text_style()
|
||||||
|
.get_or_insert_with(Default::default)
|
||||||
|
.white_space = Some(WhiteSpace::Normal);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the whitespace of the element to `nowrap`.
|
||||||
|
/// [Docs](https://tailwindcss.com/docs/whitespace#nowrap)
|
||||||
|
fn whitespace_nowrap(mut self) -> Self {
|
||||||
|
self.text_style()
|
||||||
|
.get_or_insert_with(Default::default)
|
||||||
|
.white_space = Some(WhiteSpace::Nowrap);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Sets the flex direction of the element to `column`.
|
/// Sets the flex direction of the element to `column`.
|
||||||
/// [Docs](https://tailwindcss.com/docs/flex-direction#column)
|
/// [Docs](https://tailwindcss.com/docs/flex-direction#column)
|
||||||
fn flex_col(mut self) -> Self {
|
fn flex_col(mut self) -> Self {
|
||||||
@ -343,6 +361,13 @@ pub trait Styled: Sized {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn text_bg(mut self, bg: impl Into<Hsla>) -> Self {
|
||||||
|
self.text_style()
|
||||||
|
.get_or_insert_with(Default::default)
|
||||||
|
.background_color = Some(bg.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
fn text_size(mut self, size: impl Into<AbsoluteLength>) -> Self {
|
fn text_size(mut self, size: impl Into<AbsoluteLength>) -> Self {
|
||||||
self.text_style()
|
self.text_style()
|
||||||
.get_or_insert_with(Default::default)
|
.get_or_insert_with(Default::default)
|
||||||
|
@ -196,7 +196,10 @@ impl TextSystem {
|
|||||||
let mut decoration_runs = SmallVec::<[DecorationRun; 32]>::new();
|
let mut decoration_runs = SmallVec::<[DecorationRun; 32]>::new();
|
||||||
for run in runs {
|
for run in runs {
|
||||||
if let Some(last_run) = decoration_runs.last_mut() {
|
if let Some(last_run) = decoration_runs.last_mut() {
|
||||||
if last_run.color == run.color && last_run.underline == run.underline {
|
if last_run.color == run.color
|
||||||
|
&& last_run.underline == run.underline
|
||||||
|
&& last_run.background_color == run.background_color
|
||||||
|
{
|
||||||
last_run.len += run.len as u32;
|
last_run.len += run.len as u32;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -204,6 +207,7 @@ impl TextSystem {
|
|||||||
decoration_runs.push(DecorationRun {
|
decoration_runs.push(DecorationRun {
|
||||||
len: run.len as u32,
|
len: run.len as u32,
|
||||||
color: run.color,
|
color: run.color,
|
||||||
|
background_color: run.background_color,
|
||||||
underline: run.underline.clone(),
|
underline: run.underline.clone(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -254,13 +258,16 @@ impl TextSystem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if decoration_runs.last().map_or(false, |last_run| {
|
if decoration_runs.last().map_or(false, |last_run| {
|
||||||
last_run.color == run.color && last_run.underline == run.underline
|
last_run.color == run.color
|
||||||
|
&& last_run.underline == run.underline
|
||||||
|
&& last_run.background_color == run.background_color
|
||||||
}) {
|
}) {
|
||||||
decoration_runs.last_mut().unwrap().len += run_len_within_line as u32;
|
decoration_runs.last_mut().unwrap().len += run_len_within_line as u32;
|
||||||
} else {
|
} else {
|
||||||
decoration_runs.push(DecorationRun {
|
decoration_runs.push(DecorationRun {
|
||||||
len: run_len_within_line as u32,
|
len: run_len_within_line as u32,
|
||||||
color: run.color,
|
color: run.color,
|
||||||
|
background_color: run.background_color,
|
||||||
underline: run.underline.clone(),
|
underline: run.underline.clone(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -283,7 +290,15 @@ impl TextSystem {
|
|||||||
text: SharedString::from(line_text),
|
text: SharedString::from(line_text),
|
||||||
});
|
});
|
||||||
|
|
||||||
line_start = line_end + 1; // Skip `\n` character.
|
// Skip `\n` character.
|
||||||
|
line_start = line_end + 1;
|
||||||
|
if let Some(run) = runs.peek_mut() {
|
||||||
|
run.len = run.len.saturating_sub(1);
|
||||||
|
if run.len == 0 {
|
||||||
|
runs.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
font_runs.clear();
|
font_runs.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
black, point, px, BorrowWindow, Bounds, Hsla, LineLayout, Pixels, Point, Result, SharedString,
|
black, point, px, size, transparent_black, BorrowWindow, Bounds, Corners, Edges, Hsla,
|
||||||
UnderlineStyle, WindowContext, WrapBoundary, WrappedLineLayout,
|
LineLayout, Pixels, Point, Result, SharedString, UnderlineStyle, WindowContext, WrapBoundary,
|
||||||
|
WrappedLineLayout,
|
||||||
};
|
};
|
||||||
use derive_more::{Deref, DerefMut};
|
use derive_more::{Deref, DerefMut};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
@ -10,6 +11,7 @@ use std::sync::Arc;
|
|||||||
pub struct DecorationRun {
|
pub struct DecorationRun {
|
||||||
pub len: u32,
|
pub len: u32,
|
||||||
pub color: Hsla,
|
pub color: Hsla,
|
||||||
|
pub background_color: Option<Hsla>,
|
||||||
pub underline: Option<UnderlineStyle>,
|
pub underline: Option<UnderlineStyle>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,7 +40,6 @@ impl ShapedLine {
|
|||||||
&self.layout,
|
&self.layout,
|
||||||
line_height,
|
line_height,
|
||||||
&self.decoration_runs,
|
&self.decoration_runs,
|
||||||
None,
|
|
||||||
&[],
|
&[],
|
||||||
cx,
|
cx,
|
||||||
)?;
|
)?;
|
||||||
@ -72,7 +73,6 @@ impl WrappedLine {
|
|||||||
&self.layout.unwrapped_layout,
|
&self.layout.unwrapped_layout,
|
||||||
line_height,
|
line_height,
|
||||||
&self.decoration_runs,
|
&self.decoration_runs,
|
||||||
self.wrap_width,
|
|
||||||
&self.wrap_boundaries,
|
&self.wrap_boundaries,
|
||||||
cx,
|
cx,
|
||||||
)?;
|
)?;
|
||||||
@ -86,7 +86,6 @@ fn paint_line(
|
|||||||
layout: &LineLayout,
|
layout: &LineLayout,
|
||||||
line_height: Pixels,
|
line_height: Pixels,
|
||||||
decoration_runs: &[DecorationRun],
|
decoration_runs: &[DecorationRun],
|
||||||
wrap_width: Option<Pixels>,
|
|
||||||
wrap_boundaries: &[WrapBoundary],
|
wrap_boundaries: &[WrapBoundary],
|
||||||
cx: &mut WindowContext<'_>,
|
cx: &mut WindowContext<'_>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
@ -97,6 +96,7 @@ fn paint_line(
|
|||||||
let mut run_end = 0;
|
let mut run_end = 0;
|
||||||
let mut color = black();
|
let mut color = black();
|
||||||
let mut current_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
|
let mut current_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
|
||||||
|
let mut current_background: Option<(Point<Pixels>, Hsla)> = None;
|
||||||
let text_system = cx.text_system().clone();
|
let text_system = cx.text_system().clone();
|
||||||
let mut glyph_origin = origin;
|
let mut glyph_origin = origin;
|
||||||
let mut prev_glyph_position = Point::default();
|
let mut prev_glyph_position = Point::default();
|
||||||
@ -110,12 +110,28 @@ fn paint_line(
|
|||||||
|
|
||||||
if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) {
|
if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) {
|
||||||
wraps.next();
|
wraps.next();
|
||||||
if let Some((underline_origin, underline_style)) = current_underline.take() {
|
if let Some((background_origin, background_color)) = current_background.as_mut() {
|
||||||
|
cx.paint_quad(
|
||||||
|
Bounds {
|
||||||
|
origin: *background_origin,
|
||||||
|
size: size(glyph_origin.x - background_origin.x, line_height),
|
||||||
|
},
|
||||||
|
Corners::default(),
|
||||||
|
*background_color,
|
||||||
|
Edges::default(),
|
||||||
|
transparent_black(),
|
||||||
|
);
|
||||||
|
background_origin.x = origin.x;
|
||||||
|
background_origin.y += line_height;
|
||||||
|
}
|
||||||
|
if let Some((underline_origin, underline_style)) = current_underline.as_mut() {
|
||||||
cx.paint_underline(
|
cx.paint_underline(
|
||||||
underline_origin,
|
*underline_origin,
|
||||||
glyph_origin.x - underline_origin.x,
|
glyph_origin.x - underline_origin.x,
|
||||||
&underline_style,
|
underline_style,
|
||||||
)?;
|
);
|
||||||
|
underline_origin.x = origin.x;
|
||||||
|
underline_origin.y += line_height;
|
||||||
}
|
}
|
||||||
|
|
||||||
glyph_origin.x = origin.x;
|
glyph_origin.x = origin.x;
|
||||||
@ -123,9 +139,20 @@ fn paint_line(
|
|||||||
}
|
}
|
||||||
prev_glyph_position = glyph.position;
|
prev_glyph_position = glyph.position;
|
||||||
|
|
||||||
|
let mut finished_background: Option<(Point<Pixels>, Hsla)> = None;
|
||||||
let mut finished_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
|
let mut finished_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
|
||||||
if glyph.index >= run_end {
|
if glyph.index >= run_end {
|
||||||
if let Some(style_run) = decoration_runs.next() {
|
if let Some(style_run) = decoration_runs.next() {
|
||||||
|
if let Some((_, background_color)) = &mut current_background {
|
||||||
|
if style_run.background_color.as_ref() != Some(background_color) {
|
||||||
|
finished_background = current_background.take();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(run_background) = style_run.background_color {
|
||||||
|
current_background
|
||||||
|
.get_or_insert((point(glyph_origin.x, glyph_origin.y), run_background));
|
||||||
|
}
|
||||||
|
|
||||||
if let Some((_, underline_style)) = &mut current_underline {
|
if let Some((_, underline_style)) = &mut current_underline {
|
||||||
if style_run.underline.as_ref() != Some(underline_style) {
|
if style_run.underline.as_ref() != Some(underline_style) {
|
||||||
finished_underline = current_underline.take();
|
finished_underline = current_underline.take();
|
||||||
@ -135,7 +162,7 @@ fn paint_line(
|
|||||||
current_underline.get_or_insert((
|
current_underline.get_or_insert((
|
||||||
point(
|
point(
|
||||||
glyph_origin.x,
|
glyph_origin.x,
|
||||||
origin.y + baseline_offset.y + (layout.descent * 0.618),
|
glyph_origin.y + baseline_offset.y + (layout.descent * 0.618),
|
||||||
),
|
),
|
||||||
UnderlineStyle {
|
UnderlineStyle {
|
||||||
color: Some(run_underline.color.unwrap_or(style_run.color)),
|
color: Some(run_underline.color.unwrap_or(style_run.color)),
|
||||||
@ -149,16 +176,30 @@ fn paint_line(
|
|||||||
color = style_run.color;
|
color = style_run.color;
|
||||||
} else {
|
} else {
|
||||||
run_end = layout.len;
|
run_end = layout.len;
|
||||||
|
finished_background = current_background.take();
|
||||||
finished_underline = current_underline.take();
|
finished_underline = current_underline.take();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some((background_origin, background_color)) = finished_background {
|
||||||
|
cx.paint_quad(
|
||||||
|
Bounds {
|
||||||
|
origin: background_origin,
|
||||||
|
size: size(glyph_origin.x - background_origin.x, line_height),
|
||||||
|
},
|
||||||
|
Corners::default(),
|
||||||
|
background_color,
|
||||||
|
Edges::default(),
|
||||||
|
transparent_black(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if let Some((underline_origin, underline_style)) = finished_underline {
|
if let Some((underline_origin, underline_style)) = finished_underline {
|
||||||
cx.paint_underline(
|
cx.paint_underline(
|
||||||
underline_origin,
|
underline_origin,
|
||||||
glyph_origin.x - underline_origin.x,
|
glyph_origin.x - underline_origin.x,
|
||||||
&underline_style,
|
&underline_style,
|
||||||
)?;
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let max_glyph_bounds = Bounds {
|
let max_glyph_bounds = Bounds {
|
||||||
@ -188,13 +229,32 @@ fn paint_line(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut last_line_end_x = origin.x + layout.width;
|
||||||
|
if let Some(boundary) = wrap_boundaries.last() {
|
||||||
|
let run = &layout.runs[boundary.run_ix];
|
||||||
|
let glyph = &run.glyphs[boundary.glyph_ix];
|
||||||
|
last_line_end_x -= glyph.position.x;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some((background_origin, background_color)) = current_background.take() {
|
||||||
|
cx.paint_quad(
|
||||||
|
Bounds {
|
||||||
|
origin: background_origin,
|
||||||
|
size: size(last_line_end_x - background_origin.x, line_height),
|
||||||
|
},
|
||||||
|
Corners::default(),
|
||||||
|
background_color,
|
||||||
|
Edges::default(),
|
||||||
|
transparent_black(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if let Some((underline_start, underline_style)) = current_underline.take() {
|
if let Some((underline_start, underline_style)) = current_underline.take() {
|
||||||
let line_end_x = origin.x + wrap_width.unwrap_or(Pixels::MAX).min(layout.width);
|
|
||||||
cx.paint_underline(
|
cx.paint_underline(
|
||||||
underline_start,
|
underline_start,
|
||||||
line_end_x - underline_start.x,
|
last_line_end_x - underline_start.x,
|
||||||
&underline_style,
|
&underline_style,
|
||||||
)?;
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -198,6 +198,41 @@ impl WrappedLineLayout {
|
|||||||
pub fn runs(&self) -> &[ShapedRun] {
|
pub fn runs(&self) -> &[ShapedRun] {
|
||||||
&self.unwrapped_layout.runs
|
&self.unwrapped_layout.runs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn index_for_position(
|
||||||
|
&self,
|
||||||
|
position: Point<Pixels>,
|
||||||
|
line_height: Pixels,
|
||||||
|
) -> Option<usize> {
|
||||||
|
let wrapped_line_ix = (position.y / line_height) as usize;
|
||||||
|
|
||||||
|
let wrapped_line_start_x = if wrapped_line_ix > 0 {
|
||||||
|
let wrap_boundary_ix = wrapped_line_ix - 1;
|
||||||
|
let wrap_boundary = self.wrap_boundaries[wrap_boundary_ix];
|
||||||
|
let run = &self.unwrapped_layout.runs[wrap_boundary.run_ix];
|
||||||
|
run.glyphs[wrap_boundary.glyph_ix].position.x
|
||||||
|
} else {
|
||||||
|
Pixels::ZERO
|
||||||
|
};
|
||||||
|
|
||||||
|
let wrapped_line_end_x = if wrapped_line_ix < self.wrap_boundaries.len() {
|
||||||
|
let next_wrap_boundary_ix = wrapped_line_ix;
|
||||||
|
let next_wrap_boundary = self.wrap_boundaries[next_wrap_boundary_ix];
|
||||||
|
let run = &self.unwrapped_layout.runs[next_wrap_boundary.run_ix];
|
||||||
|
run.glyphs[next_wrap_boundary.glyph_ix].position.x
|
||||||
|
} else {
|
||||||
|
self.unwrapped_layout.width
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut position_in_unwrapped_line = position;
|
||||||
|
position_in_unwrapped_line.x += wrapped_line_start_x;
|
||||||
|
if position_in_unwrapped_line.x > wrapped_line_end_x {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
self.unwrapped_layout
|
||||||
|
.index_for_x(position_in_unwrapped_line.x)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct LineLayoutCache {
|
pub(crate) struct LineLayoutCache {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
private::Sealed, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace, BorrowWindow,
|
private::Sealed, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace, BorrowWindow,
|
||||||
Bounds, Element, ElementId, Entity, EntityId, Flatten, FocusHandle, FocusableView, LayoutId,
|
Bounds, Element, ElementId, Entity, EntityId, Flatten, FocusHandle, FocusableView, IntoElement,
|
||||||
Model, Pixels, Point, Render, RenderOnce, Size, ViewContext, VisualContext, WeakModel,
|
LayoutId, Model, Pixels, Point, Render, Size, ViewContext, VisualContext, WeakModel,
|
||||||
WindowContext,
|
WindowContext,
|
||||||
};
|
};
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
@ -244,26 +244,26 @@ impl Element for AnyView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: 'static + Render> RenderOnce for View<V> {
|
impl<V: 'static + Render> IntoElement for View<V> {
|
||||||
type Element = View<V>;
|
type Element = View<V>;
|
||||||
|
|
||||||
fn element_id(&self) -> Option<ElementId> {
|
fn element_id(&self) -> Option<ElementId> {
|
||||||
Some(self.model.entity_id.into())
|
Some(ElementId::from_entity_id(self.model.entity_id))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_once(self) -> Self::Element {
|
fn into_element(self) -> Self::Element {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderOnce for AnyView {
|
impl IntoElement for AnyView {
|
||||||
type Element = Self;
|
type Element = Self;
|
||||||
|
|
||||||
fn element_id(&self) -> Option<ElementId> {
|
fn element_id(&self) -> Option<ElementId> {
|
||||||
Some(self.model.entity_id.into())
|
Some(ElementId::from_entity_id(self.model.entity_id))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_once(self) -> Self::Element {
|
fn into_element(self) -> Self::Element {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -308,27 +308,23 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
mod any_view {
|
mod any_view {
|
||||||
use crate::{AnyElement, AnyView, BorrowWindow, Element, LayoutId, Render, WindowContext};
|
use crate::{AnyElement, AnyView, Element, LayoutId, Render, WindowContext};
|
||||||
|
|
||||||
pub(crate) fn layout<V: 'static + Render>(
|
pub(crate) fn layout<V: 'static + Render>(
|
||||||
view: &AnyView,
|
view: &AnyView,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
) -> (LayoutId, AnyElement) {
|
) -> (LayoutId, AnyElement) {
|
||||||
cx.with_element_id(Some(view.model.entity_id), |cx| {
|
let view = view.clone().downcast::<V>().unwrap();
|
||||||
let view = view.clone().downcast::<V>().unwrap();
|
let mut element = view.update(cx, |view, cx| view.render(cx).into_any());
|
||||||
let mut element = view.update(cx, |view, cx| view.render(cx).into_any());
|
let layout_id = element.layout(cx);
|
||||||
let layout_id = element.layout(cx);
|
(layout_id, element)
|
||||||
(layout_id, element)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn paint<V: 'static + Render>(
|
pub(crate) fn paint<V: 'static + Render>(
|
||||||
view: &AnyView,
|
_view: &AnyView,
|
||||||
element: AnyElement,
|
element: AnyElement,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
) {
|
) {
|
||||||
cx.with_element_id(Some(view.model.entity_id), |cx| {
|
element.paint(cx);
|
||||||
element.paint(cx);
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -193,11 +193,11 @@ pub trait FocusableView: 'static + Render {
|
|||||||
|
|
||||||
/// ManagedView is a view (like a Modal, Popover, Menu, etc.)
|
/// ManagedView is a view (like a Modal, Popover, Menu, etc.)
|
||||||
/// where the lifecycle of the view is handled by another view.
|
/// where the lifecycle of the view is handled by another view.
|
||||||
pub trait ManagedView: FocusableView + EventEmitter<Manager> {}
|
pub trait ManagedView: FocusableView + EventEmitter<DismissEvent> {}
|
||||||
|
|
||||||
impl<M: FocusableView + EventEmitter<Manager>> ManagedView for M {}
|
impl<M: FocusableView + EventEmitter<DismissEvent>> ManagedView for M {}
|
||||||
|
|
||||||
pub enum Manager {
|
pub enum DismissEvent {
|
||||||
Dismiss,
|
Dismiss,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -230,9 +230,15 @@ pub struct Window {
|
|||||||
pub(crate) focus: Option<FocusId>,
|
pub(crate) focus: Option<FocusId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) struct ElementStateBox {
|
||||||
|
inner: Box<dyn Any>,
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
type_name: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
// #[derive(Default)]
|
// #[derive(Default)]
|
||||||
pub(crate) struct Frame {
|
pub(crate) struct Frame {
|
||||||
pub(crate) element_states: HashMap<GlobalElementId, Box<dyn Any>>,
|
pub(crate) element_states: HashMap<GlobalElementId, ElementStateBox>,
|
||||||
mouse_listeners: HashMap<TypeId, Vec<(StackingOrder, AnyMouseListener)>>,
|
mouse_listeners: HashMap<TypeId, Vec<(StackingOrder, AnyMouseListener)>>,
|
||||||
pub(crate) dispatch_tree: DispatchTree,
|
pub(crate) dispatch_tree: DispatchTree,
|
||||||
pub(crate) focus_listeners: Vec<AnyFocusListener>,
|
pub(crate) focus_listeners: Vec<AnyFocusListener>,
|
||||||
@ -875,7 +881,7 @@ impl<'a> WindowContext<'a> {
|
|||||||
origin: Point<Pixels>,
|
origin: Point<Pixels>,
|
||||||
width: Pixels,
|
width: Pixels,
|
||||||
style: &UnderlineStyle,
|
style: &UnderlineStyle,
|
||||||
) -> Result<()> {
|
) {
|
||||||
let scale_factor = self.scale_factor();
|
let scale_factor = self.scale_factor();
|
||||||
let height = if style.wavy {
|
let height = if style.wavy {
|
||||||
style.thickness * 3.
|
style.thickness * 3.
|
||||||
@ -899,7 +905,6 @@ impl<'a> WindowContext<'a> {
|
|||||||
wavy: style.wavy,
|
wavy: style.wavy,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Paint a monochrome (non-emoji) glyph into the scene for the current frame at the current z-index.
|
/// Paint a monochrome (non-emoji) glyph into the scene for the current frame at the current z-index.
|
||||||
@ -1512,6 +1517,13 @@ impl<'a> WindowContext<'a> {
|
|||||||
.set_input_handler(Box::new(input_handler));
|
.set_input_handler(Box::new(input_handler));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn on_window_should_close(&mut self, f: impl Fn(&mut WindowContext) -> bool + 'static) {
|
||||||
|
let mut this = self.to_async();
|
||||||
|
self.window
|
||||||
|
.platform_window
|
||||||
|
.on_should_close(Box::new(move || this.update(|_, cx| f(cx)).unwrap_or(true)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Context for WindowContext<'_> {
|
impl Context for WindowContext<'_> {
|
||||||
@ -1658,7 +1670,7 @@ impl VisualContext for WindowContext<'_> {
|
|||||||
where
|
where
|
||||||
V: ManagedView,
|
V: ManagedView,
|
||||||
{
|
{
|
||||||
self.update_view(view, |_, cx| cx.emit(Manager::Dismiss))
|
self.update_view(view, |_, cx| cx.emit(DismissEvent::Dismiss))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1747,6 +1759,24 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Invoke the given function with the content mask reset to that
|
||||||
|
/// of the window.
|
||||||
|
fn break_content_mask<R>(&mut self, f: impl FnOnce(&mut Self) -> R) -> R {
|
||||||
|
let mask = ContentMask {
|
||||||
|
bounds: Bounds {
|
||||||
|
origin: Point::default(),
|
||||||
|
size: self.window().viewport_size,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
self.window_mut()
|
||||||
|
.current_frame
|
||||||
|
.content_mask_stack
|
||||||
|
.push(mask);
|
||||||
|
let result = f(self);
|
||||||
|
self.window_mut().current_frame.content_mask_stack.pop();
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
/// Update the global element offset relative to the current offset. This is used to implement
|
/// Update the global element offset relative to the current offset. This is used to implement
|
||||||
/// scrolling.
|
/// scrolling.
|
||||||
fn with_element_offset<R>(
|
fn with_element_offset<R>(
|
||||||
@ -1815,10 +1845,37 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
|
|||||||
.remove(&global_id)
|
.remove(&global_id)
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
|
let ElementStateBox {
|
||||||
|
inner,
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
type_name
|
||||||
|
} = any;
|
||||||
// Using the extra inner option to avoid needing to reallocate a new box.
|
// Using the extra inner option to avoid needing to reallocate a new box.
|
||||||
let mut state_box = any
|
let mut state_box = inner
|
||||||
.downcast::<Option<S>>()
|
.downcast::<Option<S>>()
|
||||||
.expect("invalid element state type for id");
|
.map_err(|_| {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
{
|
||||||
|
anyhow!(
|
||||||
|
"invalid element state type for id, requested_type {:?}, actual type: {:?}",
|
||||||
|
std::any::type_name::<S>(),
|
||||||
|
type_name
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
{
|
||||||
|
anyhow!(
|
||||||
|
"invalid element state type for id, requested_type {:?}",
|
||||||
|
std::any::type_name::<S>(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Actual: Option<AnyElement> <- View
|
||||||
|
// Requested: () <- AnyElemet
|
||||||
let state = state_box
|
let state = state_box
|
||||||
.take()
|
.take()
|
||||||
.expect("element state is already on the stack");
|
.expect("element state is already on the stack");
|
||||||
@ -1827,14 +1884,27 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
|
|||||||
cx.window_mut()
|
cx.window_mut()
|
||||||
.current_frame
|
.current_frame
|
||||||
.element_states
|
.element_states
|
||||||
.insert(global_id, state_box);
|
.insert(global_id, ElementStateBox {
|
||||||
|
inner: state_box,
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
type_name
|
||||||
|
});
|
||||||
result
|
result
|
||||||
} else {
|
} else {
|
||||||
let (result, state) = f(None, cx);
|
let (result, state) = f(None, cx);
|
||||||
cx.window_mut()
|
cx.window_mut()
|
||||||
.current_frame
|
.current_frame
|
||||||
.element_states
|
.element_states
|
||||||
.insert(global_id, Box::new(Some(state)));
|
.insert(global_id,
|
||||||
|
ElementStateBox {
|
||||||
|
inner: Box::new(Some(state)),
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
type_name: std::any::type_name::<S>()
|
||||||
|
}
|
||||||
|
|
||||||
|
);
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -2304,7 +2374,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
|||||||
where
|
where
|
||||||
V: ManagedView,
|
V: ManagedView,
|
||||||
{
|
{
|
||||||
self.defer(|_, cx| cx.emit(Manager::Dismiss))
|
self.defer(|_, cx| cx.emit(DismissEvent::Dismiss))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn listener<E>(
|
pub fn listener<E>(
|
||||||
@ -2599,6 +2669,12 @@ pub enum ElementId {
|
|||||||
FocusHandle(FocusId),
|
FocusHandle(FocusId),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ElementId {
|
||||||
|
pub(crate) fn from_entity_id(entity_id: EntityId) -> Self {
|
||||||
|
ElementId::View(entity_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl TryInto<SharedString> for ElementId {
|
impl TryInto<SharedString> for ElementId {
|
||||||
type Error = anyhow::Error;
|
type Error = anyhow::Error;
|
||||||
|
|
||||||
@ -2611,12 +2687,6 @@ impl TryInto<SharedString> for ElementId {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<EntityId> for ElementId {
|
|
||||||
fn from(id: EntityId) -> Self {
|
|
||||||
ElementId::View(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<usize> for ElementId {
|
impl From<usize> for ElementId {
|
||||||
fn from(id: usize) -> Self {
|
fn from(id: usize) -> Self {
|
||||||
ElementId::Integer(id)
|
ElementId::Integer(id)
|
||||||
|
@ -1,66 +0,0 @@
|
|||||||
use proc_macro::TokenStream;
|
|
||||||
use quote::quote;
|
|
||||||
use syn::{parse_macro_input, parse_quote, DeriveInput};
|
|
||||||
|
|
||||||
pub fn derive_component(input: TokenStream) -> TokenStream {
|
|
||||||
let ast = parse_macro_input!(input as DeriveInput);
|
|
||||||
let name = &ast.ident;
|
|
||||||
|
|
||||||
let mut trait_generics = ast.generics.clone();
|
|
||||||
let view_type = if let Some(view_type) = specified_view_type(&ast) {
|
|
||||||
quote! { #view_type }
|
|
||||||
} else {
|
|
||||||
if let Some(first_type_param) = ast.generics.params.iter().find_map(|param| {
|
|
||||||
if let syn::GenericParam::Type(type_param) = param {
|
|
||||||
Some(type_param.ident.clone())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
quote! { #first_type_param }
|
|
||||||
} else {
|
|
||||||
trait_generics.params.push(parse_quote! { V: 'static });
|
|
||||||
quote! { V }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let (impl_generics, _, where_clause) = trait_generics.split_for_impl();
|
|
||||||
let (_, ty_generics, _) = ast.generics.split_for_impl();
|
|
||||||
|
|
||||||
let expanded = quote! {
|
|
||||||
impl #impl_generics gpui::Component<#view_type> for #name #ty_generics #where_clause {
|
|
||||||
fn render(self) -> gpui::AnyElement<#view_type> {
|
|
||||||
(move |view_state: &mut #view_type, cx: &mut gpui::ViewContext<'_, #view_type>| self.render(view_state, cx))
|
|
||||||
.render()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
TokenStream::from(expanded)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn specified_view_type(ast: &DeriveInput) -> Option<proc_macro2::Ident> {
|
|
||||||
let component_attr = ast
|
|
||||||
.attrs
|
|
||||||
.iter()
|
|
||||||
.find(|attr| attr.path.is_ident("component"))?;
|
|
||||||
|
|
||||||
if let Ok(syn::Meta::List(meta_list)) = component_attr.parse_meta() {
|
|
||||||
meta_list.nested.iter().find_map(|nested| {
|
|
||||||
if let syn::NestedMeta::Meta(syn::Meta::NameValue(nv)) = nested {
|
|
||||||
if nv.path.is_ident("view_type") {
|
|
||||||
if let syn::Lit::Str(lit_str) = &nv.lit {
|
|
||||||
return Some(
|
|
||||||
lit_str
|
|
||||||
.parse::<syn::Ident>()
|
|
||||||
.expect("Failed to parse view_type"),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,23 +2,23 @@ use proc_macro::TokenStream;
|
|||||||
use quote::quote;
|
use quote::quote;
|
||||||
use syn::{parse_macro_input, DeriveInput};
|
use syn::{parse_macro_input, DeriveInput};
|
||||||
|
|
||||||
pub fn derive_render_once(input: TokenStream) -> TokenStream {
|
pub fn derive_into_element(input: TokenStream) -> TokenStream {
|
||||||
let ast = parse_macro_input!(input as DeriveInput);
|
let ast = parse_macro_input!(input as DeriveInput);
|
||||||
let type_name = &ast.ident;
|
let type_name = &ast.ident;
|
||||||
let (impl_generics, type_generics, where_clause) = ast.generics.split_for_impl();
|
let (impl_generics, type_generics, where_clause) = ast.generics.split_for_impl();
|
||||||
|
|
||||||
let gen = quote! {
|
let gen = quote! {
|
||||||
impl #impl_generics gpui::RenderOnce for #type_name #type_generics
|
impl #impl_generics gpui::IntoElement for #type_name #type_generics
|
||||||
#where_clause
|
#where_clause
|
||||||
{
|
{
|
||||||
type Element = gpui::CompositeElement<Self>;
|
type Element = gpui::Component<Self>;
|
||||||
|
|
||||||
fn element_id(&self) -> Option<ElementId> {
|
fn element_id(&self) -> Option<ElementId> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_once(self) -> Self::Element {
|
fn into_element(self) -> Self::Element {
|
||||||
gpui::CompositeElement::new(self)
|
gpui::Component::new(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
@ -1,6 +1,5 @@
|
|||||||
mod action;
|
mod action;
|
||||||
mod derive_component;
|
mod derive_into_element;
|
||||||
mod derive_render_once;
|
|
||||||
mod register_action;
|
mod register_action;
|
||||||
mod style_helpers;
|
mod style_helpers;
|
||||||
mod test;
|
mod test;
|
||||||
@ -17,14 +16,9 @@ pub fn register_action(attr: TokenStream, item: TokenStream) -> TokenStream {
|
|||||||
register_action::register_action_macro(attr, item)
|
register_action::register_action_macro(attr, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[proc_macro_derive(Component, attributes(component))]
|
#[proc_macro_derive(IntoElement)]
|
||||||
pub fn derive_component(input: TokenStream) -> TokenStream {
|
pub fn derive_into_element(input: TokenStream) -> TokenStream {
|
||||||
derive_component::derive_component(input)
|
derive_into_element::derive_into_element(input)
|
||||||
}
|
|
||||||
|
|
||||||
#[proc_macro_derive(RenderOnce, attributes(view))]
|
|
||||||
pub fn derive_render_once(input: TokenStream) -> TokenStream {
|
|
||||||
derive_render_once::derive_render_once(input)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[proc_macro]
|
#[proc_macro]
|
||||||
|
@ -11,7 +11,7 @@ pub struct HighlightId(pub u32);
|
|||||||
const DEFAULT_SYNTAX_HIGHLIGHT_ID: HighlightId = HighlightId(u32::MAX);
|
const DEFAULT_SYNTAX_HIGHLIGHT_ID: HighlightId = HighlightId(u32::MAX);
|
||||||
|
|
||||||
impl HighlightMap {
|
impl HighlightMap {
|
||||||
pub fn new(capture_names: &[String], theme: &SyntaxTheme) -> Self {
|
pub fn new(capture_names: &[&str], theme: &SyntaxTheme) -> Self {
|
||||||
// For each capture name in the highlight query, find the longest
|
// For each capture name in the highlight query, find the longest
|
||||||
// key in the theme's syntax styles that matches all of the
|
// key in the theme's syntax styles that matches all of the
|
||||||
// dot-separated components of the capture name.
|
// dot-separated components of the capture name.
|
||||||
@ -98,9 +98,9 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let capture_names = &[
|
let capture_names = &[
|
||||||
"function.special".to_string(),
|
"function.special",
|
||||||
"function.async.rust".to_string(),
|
"function.async.rust",
|
||||||
"variable.builtin.self".to_string(),
|
"variable.builtin.self",
|
||||||
];
|
];
|
||||||
|
|
||||||
let map = HighlightMap::new(capture_names, &theme);
|
let map = HighlightMap::new(capture_names, &theme);
|
||||||
|
@ -1383,7 +1383,7 @@ impl Language {
|
|||||||
let query = Query::new(self.grammar_mut().ts_language, source)?;
|
let query = Query::new(self.grammar_mut().ts_language, source)?;
|
||||||
|
|
||||||
let mut override_configs_by_id = HashMap::default();
|
let mut override_configs_by_id = HashMap::default();
|
||||||
for (ix, name) in query.capture_names().iter().enumerate() {
|
for (ix, name) in query.capture_names().iter().copied().enumerate() {
|
||||||
if !name.starts_with('_') {
|
if !name.starts_with('_') {
|
||||||
let value = self.config.overrides.remove(name).unwrap_or_default();
|
let value = self.config.overrides.remove(name).unwrap_or_default();
|
||||||
for server_name in &value.opt_into_language_servers {
|
for server_name in &value.opt_into_language_servers {
|
||||||
@ -1396,7 +1396,7 @@ impl Language {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override_configs_by_id.insert(ix as u32, (name.clone(), value));
|
override_configs_by_id.insert(ix as u32, (name.into(), value));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1300,7 +1300,7 @@ fn assert_capture_ranges(
|
|||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
for capture in captures {
|
for capture in captures {
|
||||||
let name = &queries[capture.grammar_index].capture_names()[capture.index as usize];
|
let name = &queries[capture.grammar_index].capture_names()[capture.index as usize];
|
||||||
if highlight_query_capture_names.contains(&name.as_str()) {
|
if highlight_query_capture_names.contains(&name) {
|
||||||
actual_ranges.push(capture.node.byte_range());
|
actual_ranges.push(capture.node.byte_range());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ pub use crate::{
|
|||||||
use crate::{
|
use crate::{
|
||||||
diagnostic_set::{DiagnosticEntry, DiagnosticGroup},
|
diagnostic_set::{DiagnosticEntry, DiagnosticGroup},
|
||||||
language_settings::{language_settings, LanguageSettings},
|
language_settings::{language_settings, LanguageSettings},
|
||||||
|
markdown::parse_markdown,
|
||||||
outline::OutlineItem,
|
outline::OutlineItem,
|
||||||
syntax_map::{
|
syntax_map::{
|
||||||
SyntaxLayerInfo, SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures, SyntaxMapMatches,
|
SyntaxLayerInfo, SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures, SyntaxMapMatches,
|
||||||
@ -155,12 +156,52 @@ pub struct Diagnostic {
|
|||||||
pub is_unnecessary: bool,
|
pub is_unnecessary: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn prepare_completion_documentation(
|
||||||
|
documentation: &lsp::Documentation,
|
||||||
|
language_registry: &Arc<LanguageRegistry>,
|
||||||
|
language: Option<Arc<Language>>,
|
||||||
|
) -> Documentation {
|
||||||
|
match documentation {
|
||||||
|
lsp::Documentation::String(text) => {
|
||||||
|
if text.lines().count() <= 1 {
|
||||||
|
Documentation::SingleLine(text.clone())
|
||||||
|
} else {
|
||||||
|
Documentation::MultiLinePlainText(text.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lsp::Documentation::MarkupContent(lsp::MarkupContent { kind, value }) => match kind {
|
||||||
|
lsp::MarkupKind::PlainText => {
|
||||||
|
if value.lines().count() <= 1 {
|
||||||
|
Documentation::SingleLine(value.clone())
|
||||||
|
} else {
|
||||||
|
Documentation::MultiLinePlainText(value.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lsp::MarkupKind::Markdown => {
|
||||||
|
let parsed = parse_markdown(value, language_registry, language).await;
|
||||||
|
Documentation::MultiLineMarkdown(parsed)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum Documentation {
|
||||||
|
Undocumented,
|
||||||
|
SingleLine(String),
|
||||||
|
MultiLinePlainText(String),
|
||||||
|
MultiLineMarkdown(ParsedMarkdown),
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Completion {
|
pub struct Completion {
|
||||||
pub old_range: Range<Anchor>,
|
pub old_range: Range<Anchor>,
|
||||||
pub new_text: String,
|
pub new_text: String,
|
||||||
pub label: CodeLabel,
|
pub label: CodeLabel,
|
||||||
pub server_id: LanguageServerId,
|
pub server_id: LanguageServerId,
|
||||||
|
pub documentation: Option<Documentation>,
|
||||||
pub lsp_completion: lsp::CompletionItem,
|
pub lsp_completion: lsp::CompletionItem,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ pub struct HighlightId(pub u32);
|
|||||||
const DEFAULT_SYNTAX_HIGHLIGHT_ID: HighlightId = HighlightId(u32::MAX);
|
const DEFAULT_SYNTAX_HIGHLIGHT_ID: HighlightId = HighlightId(u32::MAX);
|
||||||
|
|
||||||
impl HighlightMap {
|
impl HighlightMap {
|
||||||
pub fn new(capture_names: &[String], theme: &SyntaxTheme) -> Self {
|
pub fn new(capture_names: &[&str], theme: &SyntaxTheme) -> Self {
|
||||||
// For each capture name in the highlight query, find the longest
|
// For each capture name in the highlight query, find the longest
|
||||||
// key in the theme's syntax styles that matches all of the
|
// key in the theme's syntax styles that matches all of the
|
||||||
// dot-separated components of the capture name.
|
// dot-separated components of the capture name.
|
||||||
@ -100,9 +100,9 @@ mod tests {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let capture_names = &[
|
let capture_names = &[
|
||||||
"function.special".to_string(),
|
"function.special",
|
||||||
"function.async.rust".to_string(),
|
"function.async.rust",
|
||||||
"variable.builtin.self".to_string(),
|
"variable.builtin.self",
|
||||||
];
|
];
|
||||||
|
|
||||||
let map = HighlightMap::new(capture_names, &theme);
|
let map = HighlightMap::new(capture_names, &theme);
|
||||||
|
@ -1391,7 +1391,7 @@ impl Language {
|
|||||||
let mut override_configs_by_id = HashMap::default();
|
let mut override_configs_by_id = HashMap::default();
|
||||||
for (ix, name) in query.capture_names().iter().enumerate() {
|
for (ix, name) in query.capture_names().iter().enumerate() {
|
||||||
if !name.starts_with('_') {
|
if !name.starts_with('_') {
|
||||||
let value = self.config.overrides.remove(name).unwrap_or_default();
|
let value = self.config.overrides.remove(*name).unwrap_or_default();
|
||||||
for server_name in &value.opt_into_language_servers {
|
for server_name in &value.opt_into_language_servers {
|
||||||
if !self
|
if !self
|
||||||
.config
|
.config
|
||||||
@ -1402,7 +1402,7 @@ impl Language {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override_configs_by_id.insert(ix as u32, (name.clone(), value));
|
override_configs_by_id.insert(ix as u32, (name.to_string(), value));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -482,6 +482,7 @@ pub async fn deserialize_completion(
|
|||||||
lsp_completion.filter_text.as_deref(),
|
lsp_completion.filter_text.as_deref(),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
|
documentation: None,
|
||||||
server_id: LanguageServerId(completion.server_id as usize),
|
server_id: LanguageServerId(completion.server_id as usize),
|
||||||
lsp_completion,
|
lsp_completion,
|
||||||
})
|
})
|
||||||
|
@ -1300,7 +1300,7 @@ fn assert_capture_ranges(
|
|||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
for capture in captures {
|
for capture in captures {
|
||||||
let name = &queries[capture.grammar_index].capture_names()[capture.index as usize];
|
let name = &queries[capture.grammar_index].capture_names()[capture.index as usize];
|
||||||
if highlight_query_capture_names.contains(&name.as_str()) {
|
if highlight_query_capture_names.contains(&name) {
|
||||||
actual_ranges.push(capture.node.byte_range());
|
actual_ranges.push(capture.node.byte_range());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ use gpui::{
|
|||||||
MouseDownEvent, Render, Task, UniformListScrollHandle, View, ViewContext, WindowContext,
|
MouseDownEvent, Render, Task, UniformListScrollHandle, View, ViewContext, WindowContext,
|
||||||
};
|
};
|
||||||
use std::{cmp, sync::Arc};
|
use std::{cmp, sync::Arc};
|
||||||
use ui::{prelude::*, v_stack, Divider, Label, TextColor};
|
use ui::{prelude::*, v_stack, Color, Divider, Label};
|
||||||
|
|
||||||
pub struct Picker<D: PickerDelegate> {
|
pub struct Picker<D: PickerDelegate> {
|
||||||
pub delegate: D,
|
pub delegate: D,
|
||||||
@ -15,7 +15,7 @@ pub struct Picker<D: PickerDelegate> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub trait PickerDelegate: Sized + 'static {
|
pub trait PickerDelegate: Sized + 'static {
|
||||||
type ListItem: RenderOnce;
|
type ListItem: IntoElement;
|
||||||
|
|
||||||
fn match_count(&self) -> usize;
|
fn match_count(&self) -> usize;
|
||||||
fn selected_index(&self) -> usize;
|
fn selected_index(&self) -> usize;
|
||||||
@ -114,6 +114,7 @@ impl<D: PickerDelegate> Picker<D> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||||
|
dbg!("canceling!");
|
||||||
self.delegate.dismissed(cx);
|
self.delegate.dismissed(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -250,7 +251,7 @@ impl<D: PickerDelegate> Render for Picker<D> {
|
|||||||
v_stack().p_1().grow().child(
|
v_stack().p_1().grow().child(
|
||||||
div()
|
div()
|
||||||
.px_1()
|
.px_1()
|
||||||
.child(Label::new("No matches").color(TextColor::Muted)),
|
.child(Label::new("No matches").color(Color::Muted)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -13,7 +13,7 @@ mod worktree_tests;
|
|||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use client::{proto, Client, Collaborator, TypedEnvelope, UserStore};
|
use client::{proto, Client, Collaborator, TypedEnvelope, UserStore};
|
||||||
use clock::ReplicaId;
|
use clock::ReplicaId;
|
||||||
use collections::{hash_map, BTreeMap, HashMap, HashSet};
|
use collections::{hash_map, BTreeMap, HashMap, HashSet, VecDeque};
|
||||||
use copilot::Copilot;
|
use copilot::Copilot;
|
||||||
use futures::{
|
use futures::{
|
||||||
channel::{
|
channel::{
|
||||||
@ -62,7 +62,10 @@ use serde::Serialize;
|
|||||||
use settings::SettingsStore;
|
use settings::SettingsStore;
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use similar::{ChangeTag, TextDiff};
|
use similar::{ChangeTag, TextDiff};
|
||||||
use smol::channel::{Receiver, Sender};
|
use smol::{
|
||||||
|
channel::{Receiver, Sender},
|
||||||
|
lock::Semaphore,
|
||||||
|
};
|
||||||
use std::{
|
use std::{
|
||||||
cmp::{self, Ordering},
|
cmp::{self, Ordering},
|
||||||
convert::TryInto,
|
convert::TryInto,
|
||||||
@ -557,6 +560,7 @@ enum SearchMatchCandidate {
|
|||||||
},
|
},
|
||||||
Path {
|
Path {
|
||||||
worktree_id: WorktreeId,
|
worktree_id: WorktreeId,
|
||||||
|
is_ignored: bool,
|
||||||
path: Arc<Path>,
|
path: Arc<Path>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -5742,13 +5746,18 @@ impl Project {
|
|||||||
.await
|
.await
|
||||||
.log_err();
|
.log_err();
|
||||||
}
|
}
|
||||||
|
|
||||||
background
|
background
|
||||||
.scoped(|scope| {
|
.scoped(|scope| {
|
||||||
|
let max_concurrent_workers = Arc::new(Semaphore::new(workers));
|
||||||
|
|
||||||
for worker_ix in 0..workers {
|
for worker_ix in 0..workers {
|
||||||
let worker_start_ix = worker_ix * paths_per_worker;
|
let worker_start_ix = worker_ix * paths_per_worker;
|
||||||
let worker_end_ix = worker_start_ix + paths_per_worker;
|
let worker_end_ix = worker_start_ix + paths_per_worker;
|
||||||
let unnamed_buffers = opened_buffers.clone();
|
let unnamed_buffers = opened_buffers.clone();
|
||||||
|
let limiter = Arc::clone(&max_concurrent_workers);
|
||||||
scope.spawn(async move {
|
scope.spawn(async move {
|
||||||
|
let _guard = limiter.acquire().await;
|
||||||
let mut snapshot_start_ix = 0;
|
let mut snapshot_start_ix = 0;
|
||||||
let mut abs_path = PathBuf::new();
|
let mut abs_path = PathBuf::new();
|
||||||
for snapshot in snapshots {
|
for snapshot in snapshots {
|
||||||
@ -5797,6 +5806,7 @@ impl Project {
|
|||||||
let project_path = SearchMatchCandidate::Path {
|
let project_path = SearchMatchCandidate::Path {
|
||||||
worktree_id: snapshot.id(),
|
worktree_id: snapshot.id(),
|
||||||
path: entry.path.clone(),
|
path: entry.path.clone(),
|
||||||
|
is_ignored: entry.is_ignored,
|
||||||
};
|
};
|
||||||
if matching_paths_tx.send(project_path).await.is_err() {
|
if matching_paths_tx.send(project_path).await.is_err() {
|
||||||
break;
|
break;
|
||||||
@ -5809,6 +5819,94 @@ impl Project {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if query.include_ignored() {
|
||||||
|
for snapshot in snapshots {
|
||||||
|
for ignored_entry in snapshot
|
||||||
|
.entries(query.include_ignored())
|
||||||
|
.filter(|e| e.is_ignored)
|
||||||
|
{
|
||||||
|
let limiter = Arc::clone(&max_concurrent_workers);
|
||||||
|
scope.spawn(async move {
|
||||||
|
let _guard = limiter.acquire().await;
|
||||||
|
let mut ignored_paths_to_process =
|
||||||
|
VecDeque::from([snapshot.abs_path().join(&ignored_entry.path)]);
|
||||||
|
while let Some(ignored_abs_path) =
|
||||||
|
ignored_paths_to_process.pop_front()
|
||||||
|
{
|
||||||
|
if !query.file_matches(Some(&ignored_abs_path))
|
||||||
|
|| snapshot.is_path_excluded(&ignored_abs_path)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(fs_metadata) = fs
|
||||||
|
.metadata(&ignored_abs_path)
|
||||||
|
.await
|
||||||
|
.with_context(|| {
|
||||||
|
format!("fetching fs metadata for {ignored_abs_path:?}")
|
||||||
|
})
|
||||||
|
.log_err()
|
||||||
|
.flatten()
|
||||||
|
{
|
||||||
|
if fs_metadata.is_dir {
|
||||||
|
if let Some(mut subfiles) = fs
|
||||||
|
.read_dir(&ignored_abs_path)
|
||||||
|
.await
|
||||||
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"listing ignored path {ignored_abs_path:?}"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.log_err()
|
||||||
|
{
|
||||||
|
while let Some(subfile) = subfiles.next().await {
|
||||||
|
if let Some(subfile) = subfile.log_err() {
|
||||||
|
ignored_paths_to_process.push_back(subfile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if !fs_metadata.is_symlink {
|
||||||
|
let matches = if let Some(file) = fs
|
||||||
|
.open_sync(&ignored_abs_path)
|
||||||
|
.await
|
||||||
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Opening ignored path {ignored_abs_path:?}"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.log_err()
|
||||||
|
{
|
||||||
|
query.detect(file).unwrap_or(false)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
if matches {
|
||||||
|
let project_path = SearchMatchCandidate::Path {
|
||||||
|
worktree_id: snapshot.id(),
|
||||||
|
path: Arc::from(
|
||||||
|
ignored_abs_path
|
||||||
|
.strip_prefix(snapshot.abs_path())
|
||||||
|
.expect(
|
||||||
|
"scanning worktree-related files",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
is_ignored: true,
|
||||||
|
};
|
||||||
|
if matching_paths_tx
|
||||||
|
.send(project_path)
|
||||||
|
.await
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
@ -5917,11 +6015,24 @@ impl Project {
|
|||||||
let (buffers_tx, buffers_rx) = smol::channel::bounded(1024);
|
let (buffers_tx, buffers_rx) = smol::channel::bounded(1024);
|
||||||
let (sorted_buffers_tx, sorted_buffers_rx) = futures::channel::oneshot::channel();
|
let (sorted_buffers_tx, sorted_buffers_rx) = futures::channel::oneshot::channel();
|
||||||
cx.spawn(|this, cx| async move {
|
cx.spawn(|this, cx| async move {
|
||||||
let mut buffers = vec![];
|
let mut buffers = Vec::new();
|
||||||
|
let mut ignored_buffers = Vec::new();
|
||||||
while let Some(entry) = matching_paths_rx.next().await {
|
while let Some(entry) = matching_paths_rx.next().await {
|
||||||
buffers.push(entry);
|
if matches!(
|
||||||
|
entry,
|
||||||
|
SearchMatchCandidate::Path {
|
||||||
|
is_ignored: true,
|
||||||
|
..
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
ignored_buffers.push(entry);
|
||||||
|
} else {
|
||||||
|
buffers.push(entry);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
buffers.sort_by_key(|candidate| candidate.path());
|
buffers.sort_by_key(|candidate| candidate.path());
|
||||||
|
ignored_buffers.sort_by_key(|candidate| candidate.path());
|
||||||
|
buffers.extend(ignored_buffers);
|
||||||
let matching_paths = buffers.clone();
|
let matching_paths = buffers.clone();
|
||||||
let _ = sorted_buffers_tx.send(buffers);
|
let _ = sorted_buffers_tx.send(buffers);
|
||||||
for (index, candidate) in matching_paths.into_iter().enumerate() {
|
for (index, candidate) in matching_paths.into_iter().enumerate() {
|
||||||
@ -5933,7 +6044,9 @@ impl Project {
|
|||||||
cx.spawn(|mut cx| async move {
|
cx.spawn(|mut cx| async move {
|
||||||
let buffer = match candidate {
|
let buffer = match candidate {
|
||||||
SearchMatchCandidate::OpenBuffer { buffer, .. } => Some(buffer),
|
SearchMatchCandidate::OpenBuffer { buffer, .. } => Some(buffer),
|
||||||
SearchMatchCandidate::Path { worktree_id, path } => this
|
SearchMatchCandidate::Path {
|
||||||
|
worktree_id, path, ..
|
||||||
|
} => this
|
||||||
.update(&mut cx, |this, cx| {
|
.update(&mut cx, |this, cx| {
|
||||||
this.open_buffer((worktree_id, path), cx)
|
this.open_buffer((worktree_id, path), cx)
|
||||||
})
|
})
|
||||||
|
@ -2226,7 +2226,7 @@ impl LocalSnapshot {
|
|||||||
paths
|
paths
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_abs_path_excluded(&self, abs_path: &Path) -> bool {
|
pub fn is_path_excluded(&self, abs_path: &Path) -> bool {
|
||||||
self.file_scan_exclusions
|
self.file_scan_exclusions
|
||||||
.iter()
|
.iter()
|
||||||
.any(|exclude_matcher| exclude_matcher.is_match(abs_path))
|
.any(|exclude_matcher| exclude_matcher.is_match(abs_path))
|
||||||
@ -2399,26 +2399,9 @@ impl BackgroundScannerState {
|
|||||||
self.snapshot.check_invariants(false);
|
self.snapshot.check_invariants(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reload_repositories(&mut self, changed_paths: &[Arc<Path>], fs: &dyn Fs) {
|
fn reload_repositories(&mut self, dot_git_dirs_to_reload: &HashSet<PathBuf>, fs: &dyn Fs) {
|
||||||
let scan_id = self.snapshot.scan_id;
|
let scan_id = self.snapshot.scan_id;
|
||||||
|
for dot_git_dir in dot_git_dirs_to_reload {
|
||||||
// Find each of the .git directories that contain any of the given paths.
|
|
||||||
let mut prev_dot_git_dir = None;
|
|
||||||
for changed_path in changed_paths {
|
|
||||||
let Some(dot_git_dir) = changed_path
|
|
||||||
.ancestors()
|
|
||||||
.find(|ancestor| ancestor.file_name() == Some(&*DOT_GIT))
|
|
||||||
else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Avoid processing the same repository multiple times, if multiple paths
|
|
||||||
// within it have changed.
|
|
||||||
if prev_dot_git_dir == Some(dot_git_dir) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
prev_dot_git_dir = Some(dot_git_dir);
|
|
||||||
|
|
||||||
// If there is already a repository for this .git directory, reload
|
// If there is already a repository for this .git directory, reload
|
||||||
// the status for all of its files.
|
// the status for all of its files.
|
||||||
let repository = self
|
let repository = self
|
||||||
@ -2430,7 +2413,7 @@ impl BackgroundScannerState {
|
|||||||
});
|
});
|
||||||
match repository {
|
match repository {
|
||||||
None => {
|
None => {
|
||||||
self.build_git_repository(dot_git_dir.into(), fs);
|
self.build_git_repository(Arc::from(dot_git_dir.as_path()), fs);
|
||||||
}
|
}
|
||||||
Some((entry_id, repository)) => {
|
Some((entry_id, repository)) => {
|
||||||
if repository.git_dir_scan_id == scan_id {
|
if repository.git_dir_scan_id == scan_id {
|
||||||
@ -2444,7 +2427,7 @@ impl BackgroundScannerState {
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
log::info!("reload git repository {:?}", dot_git_dir);
|
log::info!("reload git repository {dot_git_dir:?}");
|
||||||
let repository = repository.repo_ptr.lock();
|
let repository = repository.repo_ptr.lock();
|
||||||
let branch = repository.branch_name();
|
let branch = repository.branch_name();
|
||||||
repository.reload_index();
|
repository.reload_index();
|
||||||
@ -2475,7 +2458,9 @@ impl BackgroundScannerState {
|
|||||||
ids_to_preserve.insert(work_directory_id);
|
ids_to_preserve.insert(work_directory_id);
|
||||||
} else {
|
} else {
|
||||||
let git_dir_abs_path = snapshot.abs_path().join(&entry.git_dir_path);
|
let git_dir_abs_path = snapshot.abs_path().join(&entry.git_dir_path);
|
||||||
if snapshot.is_abs_path_excluded(&git_dir_abs_path)
|
let git_dir_excluded = snapshot.is_path_excluded(&entry.git_dir_path)
|
||||||
|
|| snapshot.is_path_excluded(&git_dir_abs_path);
|
||||||
|
if git_dir_excluded
|
||||||
&& !matches!(smol::block_on(fs.metadata(&git_dir_abs_path)), Ok(None))
|
&& !matches!(smol::block_on(fs.metadata(&git_dir_abs_path)), Ok(None))
|
||||||
{
|
{
|
||||||
ids_to_preserve.insert(work_directory_id);
|
ids_to_preserve.insert(work_directory_id);
|
||||||
@ -3314,11 +3299,26 @@ impl BackgroundScanner {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let mut relative_paths = Vec::with_capacity(abs_paths.len());
|
let mut relative_paths = Vec::with_capacity(abs_paths.len());
|
||||||
|
let mut dot_git_paths_to_reload = HashSet::default();
|
||||||
abs_paths.sort_unstable();
|
abs_paths.sort_unstable();
|
||||||
abs_paths.dedup_by(|a, b| a.starts_with(&b));
|
abs_paths.dedup_by(|a, b| a.starts_with(&b));
|
||||||
abs_paths.retain(|abs_path| {
|
abs_paths.retain(|abs_path| {
|
||||||
let snapshot = &self.state.lock().snapshot;
|
let snapshot = &self.state.lock().snapshot;
|
||||||
{
|
{
|
||||||
|
let mut is_git_related = false;
|
||||||
|
if let Some(dot_git_dir) = abs_path
|
||||||
|
.ancestors()
|
||||||
|
.find(|ancestor| ancestor.file_name() == Some(&*DOT_GIT))
|
||||||
|
{
|
||||||
|
let dot_git_path = dot_git_dir
|
||||||
|
.strip_prefix(&root_canonical_path)
|
||||||
|
.ok()
|
||||||
|
.map(|path| path.to_path_buf())
|
||||||
|
.unwrap_or_else(|| dot_git_dir.to_path_buf());
|
||||||
|
dot_git_paths_to_reload.insert(dot_git_path.to_path_buf());
|
||||||
|
is_git_related = true;
|
||||||
|
}
|
||||||
|
|
||||||
let relative_path: Arc<Path> =
|
let relative_path: Arc<Path> =
|
||||||
if let Ok(path) = abs_path.strip_prefix(&root_canonical_path) {
|
if let Ok(path) = abs_path.strip_prefix(&root_canonical_path) {
|
||||||
path.into()
|
path.into()
|
||||||
@ -3328,23 +3328,30 @@ impl BackgroundScanner {
|
|||||||
);
|
);
|
||||||
return false;
|
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) {
|
// FS events may come for files which parent directory is excluded, need to check ignore those.
|
||||||
let parent_dir_is_loaded = relative_path.parent().map_or(true, |parent| {
|
let mut path_to_test = abs_path.clone();
|
||||||
snapshot
|
let mut excluded_file_event = snapshot.is_path_excluded(abs_path)
|
||||||
.entry_for_path(parent)
|
|| snapshot.is_path_excluded(&relative_path);
|
||||||
.map_or(false, |entry| entry.kind == EntryKind::Dir)
|
while !excluded_file_event && path_to_test.pop() {
|
||||||
});
|
if snapshot.is_path_excluded(&path_to_test) {
|
||||||
if !parent_dir_is_loaded {
|
excluded_file_event = true;
|
||||||
log::debug!("ignoring event {relative_path:?} within unloaded directory");
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
if snapshot.is_abs_path_excluded(abs_path) {
|
}
|
||||||
log::debug!(
|
if excluded_file_event {
|
||||||
"ignoring FS event for path {relative_path:?} within excluded directory"
|
if !is_git_related {
|
||||||
);
|
log::debug!("ignoring FS event for excluded path {relative_path:?}");
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
relative_paths.push(relative_path);
|
relative_paths.push(relative_path);
|
||||||
@ -3352,31 +3359,39 @@ impl BackgroundScanner {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if relative_paths.is_empty() {
|
if dot_git_paths_to_reload.is_empty() && relative_paths.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
log::debug!("received fs events {:?}", relative_paths);
|
if !relative_paths.is_empty() {
|
||||||
|
log::debug!("received fs events {:?}", relative_paths);
|
||||||
|
|
||||||
let (scan_job_tx, scan_job_rx) = channel::unbounded();
|
let (scan_job_tx, scan_job_rx) = channel::unbounded();
|
||||||
self.reload_entries_for_paths(
|
self.reload_entries_for_paths(
|
||||||
root_path,
|
root_path,
|
||||||
root_canonical_path,
|
root_canonical_path,
|
||||||
&relative_paths,
|
&relative_paths,
|
||||||
abs_paths,
|
abs_paths,
|
||||||
Some(scan_job_tx.clone()),
|
Some(scan_job_tx.clone()),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
drop(scan_job_tx);
|
drop(scan_job_tx);
|
||||||
self.scan_dirs(false, scan_job_rx).await;
|
self.scan_dirs(false, scan_job_rx).await;
|
||||||
|
|
||||||
let (scan_job_tx, scan_job_rx) = channel::unbounded();
|
let (scan_job_tx, scan_job_rx) = channel::unbounded();
|
||||||
self.update_ignore_statuses(scan_job_tx).await;
|
self.update_ignore_statuses(scan_job_tx).await;
|
||||||
self.scan_dirs(false, scan_job_rx).await;
|
self.scan_dirs(false, scan_job_rx).await;
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut state = self.state.lock();
|
let mut state = self.state.lock();
|
||||||
state.reload_repositories(&relative_paths, self.fs.as_ref());
|
if !dot_git_paths_to_reload.is_empty() {
|
||||||
|
if relative_paths.is_empty() {
|
||||||
|
state.snapshot.scan_id += 1;
|
||||||
|
}
|
||||||
|
log::debug!("reloading repositories: {dot_git_paths_to_reload:?}");
|
||||||
|
state.reload_repositories(&dot_git_paths_to_reload, self.fs.as_ref());
|
||||||
|
}
|
||||||
state.snapshot.completed_scan_id = state.snapshot.scan_id;
|
state.snapshot.completed_scan_id = state.snapshot.scan_id;
|
||||||
for (_, entry_id) in mem::take(&mut state.removed_entry_ids) {
|
for (_, entry_id) in mem::take(&mut state.removed_entry_ids) {
|
||||||
state.scanned_dirs.remove(&entry_id);
|
state.scanned_dirs.remove(&entry_id);
|
||||||
@ -3516,7 +3531,7 @@ impl BackgroundScanner {
|
|||||||
let state = self.state.lock();
|
let state = self.state.lock();
|
||||||
let snapshot = &state.snapshot;
|
let snapshot = &state.snapshot;
|
||||||
root_abs_path = snapshot.abs_path().clone();
|
root_abs_path = snapshot.abs_path().clone();
|
||||||
if snapshot.is_abs_path_excluded(&job.abs_path) {
|
if snapshot.is_path_excluded(&job.abs_path) {
|
||||||
log::error!("skipping excluded directory {:?}", job.path);
|
log::error!("skipping excluded directory {:?}", job.path);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@ -3588,7 +3603,7 @@ impl BackgroundScanner {
|
|||||||
|
|
||||||
{
|
{
|
||||||
let mut state = self.state.lock();
|
let mut state = self.state.lock();
|
||||||
if state.snapshot.is_abs_path_excluded(&child_abs_path) {
|
if state.snapshot.is_path_excluded(&child_abs_path) {
|
||||||
let relative_path = job.path.join(child_name);
|
let relative_path = job.path.join(child_name);
|
||||||
log::debug!("skipping excluded child entry {relative_path:?}");
|
log::debug!("skipping excluded child entry {relative_path:?}");
|
||||||
state.remove_path(&relative_path);
|
state.remove_path(&relative_path);
|
||||||
@ -4130,12 +4145,6 @@ impl BackgroundScanner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_git_related(abs_path: &Path) -> bool {
|
|
||||||
abs_path
|
|
||||||
.components()
|
|
||||||
.any(|c| c.as_os_str() == *DOT_GIT || c.as_os_str() == *GITIGNORE)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn char_bag_for_path(root_char_bag: CharBag, path: &Path) -> CharBag {
|
fn char_bag_for_path(root_char_bag: CharBag, path: &Path) -> CharBag {
|
||||||
let mut result = root_char_bag;
|
let mut result = root_char_bag;
|
||||||
result.extend(
|
result.extend(
|
||||||
|
@ -990,6 +990,145 @@ async fn test_file_scan_exclusions(cx: &mut TestAppContext) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
let dir = temp_tree(json!({
|
||||||
|
".git": {
|
||||||
|
"HEAD": "ref: refs/heads/main\n",
|
||||||
|
"foo": "bar",
|
||||||
|
},
|
||||||
|
".gitignore": "**/target\n/node_modules\ntest_output\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![
|
||||||
|
"**/.git".to_string(),
|
||||||
|
"node_modules/".to_string(),
|
||||||
|
"build_output".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,
|
||||||
|
&[
|
||||||
|
".git/HEAD",
|
||||||
|
".git/foo",
|
||||||
|
"node_modules/.DS_Store",
|
||||||
|
"node_modules/prettier",
|
||||||
|
"node_modules/prettier/package.json",
|
||||||
|
],
|
||||||
|
&["target", "node_modules"],
|
||||||
|
&[
|
||||||
|
".DS_Store",
|
||||||
|
"src/.DS_Store",
|
||||||
|
"src/lib.rs",
|
||||||
|
"src/foo/foo.rs",
|
||||||
|
"src/foo/another.rs",
|
||||||
|
"src/bar/bar.rs",
|
||||||
|
".gitignore",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
let new_excluded_dir = dir.path().join("build_output");
|
||||||
|
let new_ignored_dir = dir.path().join("test_output");
|
||||||
|
std::fs::create_dir_all(&new_excluded_dir)
|
||||||
|
.unwrap_or_else(|e| panic!("Failed to create a {new_excluded_dir:?} directory: {e}"));
|
||||||
|
std::fs::create_dir_all(&new_ignored_dir)
|
||||||
|
.unwrap_or_else(|e| panic!("Failed to create a {new_ignored_dir:?} directory: {e}"));
|
||||||
|
let node_modules_dir = dir.path().join("node_modules");
|
||||||
|
let dot_git_dir = dir.path().join(".git");
|
||||||
|
let src_dir = dir.path().join("src");
|
||||||
|
for existing_dir in [&node_modules_dir, &dot_git_dir, &src_dir] {
|
||||||
|
assert!(
|
||||||
|
existing_dir.is_dir(),
|
||||||
|
"Expect {existing_dir:?} to be present in the FS already"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for directory_for_new_file in [
|
||||||
|
new_excluded_dir,
|
||||||
|
new_ignored_dir,
|
||||||
|
node_modules_dir,
|
||||||
|
dot_git_dir,
|
||||||
|
src_dir,
|
||||||
|
] {
|
||||||
|
std::fs::write(directory_for_new_file.join("new_file"), "new file contents")
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
|
panic!("Failed to create in {directory_for_new_file:?} a new file: {e}")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
tree.flush_fs_events(cx).await;
|
||||||
|
|
||||||
|
tree.read_with(cx, |tree, _| {
|
||||||
|
check_worktree_entries(
|
||||||
|
tree,
|
||||||
|
&[
|
||||||
|
".git/HEAD",
|
||||||
|
".git/foo",
|
||||||
|
".git/new_file",
|
||||||
|
"node_modules/.DS_Store",
|
||||||
|
"node_modules/prettier",
|
||||||
|
"node_modules/prettier/package.json",
|
||||||
|
"node_modules/new_file",
|
||||||
|
"build_output",
|
||||||
|
"build_output/new_file",
|
||||||
|
"test_output/new_file",
|
||||||
|
],
|
||||||
|
&["target", "node_modules", "test_output"],
|
||||||
|
&[
|
||||||
|
".DS_Store",
|
||||||
|
"src/.DS_Store",
|
||||||
|
"src/lib.rs",
|
||||||
|
"src/foo/foo.rs",
|
||||||
|
"src/foo/another.rs",
|
||||||
|
"src/bar/bar.rs",
|
||||||
|
"src/new_file",
|
||||||
|
".gitignore",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test(iterations = 30)]
|
#[gpui::test(iterations = 30)]
|
||||||
async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
|
async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
|
||||||
init_test(cx);
|
init_test(cx);
|
||||||
|
@ -10,7 +10,7 @@ use futures::future;
|
|||||||
use gpui::{AppContext, AsyncAppContext, Model};
|
use gpui::{AppContext, AsyncAppContext, Model};
|
||||||
use language::{
|
use language::{
|
||||||
language_settings::{language_settings, InlayHintKind},
|
language_settings::{language_settings, InlayHintKind},
|
||||||
point_from_lsp, point_to_lsp,
|
point_from_lsp, point_to_lsp, prepare_completion_documentation,
|
||||||
proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version},
|
proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version},
|
||||||
range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CharKind,
|
range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CharKind,
|
||||||
CodeAction, Completion, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction,
|
CodeAction, Completion, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction,
|
||||||
@ -1339,7 +1339,7 @@ impl LspCommand for GetCompletions {
|
|||||||
async fn response_from_lsp(
|
async fn response_from_lsp(
|
||||||
self,
|
self,
|
||||||
completions: Option<lsp::CompletionResponse>,
|
completions: Option<lsp::CompletionResponse>,
|
||||||
_: Model<Project>,
|
project: Model<Project>,
|
||||||
buffer: Model<Buffer>,
|
buffer: Model<Buffer>,
|
||||||
server_id: LanguageServerId,
|
server_id: LanguageServerId,
|
||||||
mut cx: AsyncAppContext,
|
mut cx: AsyncAppContext,
|
||||||
@ -1359,7 +1359,8 @@ impl LspCommand for GetCompletions {
|
|||||||
Default::default()
|
Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
let completions = buffer.update(&mut cx, |buffer, _| {
|
let completions = buffer.update(&mut cx, |buffer, cx| {
|
||||||
|
let language_registry = project.read(cx).languages().clone();
|
||||||
let language = buffer.language().cloned();
|
let language = buffer.language().cloned();
|
||||||
let snapshot = buffer.snapshot();
|
let snapshot = buffer.snapshot();
|
||||||
let clipped_position = buffer.clip_point_utf16(Unclipped(self.position), Bias::Left);
|
let clipped_position = buffer.clip_point_utf16(Unclipped(self.position), Bias::Left);
|
||||||
@ -1443,14 +1444,29 @@ impl LspCommand for GetCompletions {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let language_registry = language_registry.clone();
|
||||||
let language = language.clone();
|
let language = language.clone();
|
||||||
LineEnding::normalize(&mut new_text);
|
LineEnding::normalize(&mut new_text);
|
||||||
Some(async move {
|
Some(async move {
|
||||||
let mut label = None;
|
let mut label = None;
|
||||||
if let Some(language) = language {
|
if let Some(language) = language.as_ref() {
|
||||||
language.process_completion(&mut lsp_completion).await;
|
language.process_completion(&mut lsp_completion).await;
|
||||||
label = language.label_for_completion(&lsp_completion).await;
|
label = language.label_for_completion(&lsp_completion).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let documentation = if let Some(lsp_docs) = &lsp_completion.documentation {
|
||||||
|
Some(
|
||||||
|
prepare_completion_documentation(
|
||||||
|
lsp_docs,
|
||||||
|
&language_registry,
|
||||||
|
language.clone(),
|
||||||
|
)
|
||||||
|
.await,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
Completion {
|
Completion {
|
||||||
old_range,
|
old_range,
|
||||||
new_text,
|
new_text,
|
||||||
@ -1460,6 +1476,7 @@ impl LspCommand for GetCompletions {
|
|||||||
lsp_completion.filter_text.as_deref(),
|
lsp_completion.filter_text.as_deref(),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
|
documentation,
|
||||||
server_id,
|
server_id,
|
||||||
lsp_completion,
|
lsp_completion,
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ mod worktree_tests;
|
|||||||
use anyhow::{anyhow, Context as _, Result};
|
use anyhow::{anyhow, Context as _, Result};
|
||||||
use client::{proto, Client, Collaborator, TypedEnvelope, UserStore};
|
use client::{proto, Client, Collaborator, TypedEnvelope, UserStore};
|
||||||
use clock::ReplicaId;
|
use clock::ReplicaId;
|
||||||
use collections::{hash_map, BTreeMap, HashMap, HashSet};
|
use collections::{hash_map, BTreeMap, HashMap, HashSet, VecDeque};
|
||||||
use copilot::Copilot;
|
use copilot::Copilot;
|
||||||
use futures::{
|
use futures::{
|
||||||
channel::{
|
channel::{
|
||||||
@ -63,6 +63,7 @@ use settings::{Settings, SettingsStore};
|
|||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use similar::{ChangeTag, TextDiff};
|
use similar::{ChangeTag, TextDiff};
|
||||||
use smol::channel::{Receiver, Sender};
|
use smol::channel::{Receiver, Sender};
|
||||||
|
use smol::lock::Semaphore;
|
||||||
use std::{
|
use std::{
|
||||||
cmp::{self, Ordering},
|
cmp::{self, Ordering},
|
||||||
convert::TryInto,
|
convert::TryInto,
|
||||||
@ -557,6 +558,7 @@ enum SearchMatchCandidate {
|
|||||||
},
|
},
|
||||||
Path {
|
Path {
|
||||||
worktree_id: WorktreeId,
|
worktree_id: WorktreeId,
|
||||||
|
is_ignored: bool,
|
||||||
path: Arc<Path>,
|
path: Arc<Path>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -5815,11 +5817,15 @@ impl Project {
|
|||||||
}
|
}
|
||||||
executor
|
executor
|
||||||
.scoped(|scope| {
|
.scoped(|scope| {
|
||||||
|
let max_concurrent_workers = Arc::new(Semaphore::new(workers));
|
||||||
|
|
||||||
for worker_ix in 0..workers {
|
for worker_ix in 0..workers {
|
||||||
let worker_start_ix = worker_ix * paths_per_worker;
|
let worker_start_ix = worker_ix * paths_per_worker;
|
||||||
let worker_end_ix = worker_start_ix + paths_per_worker;
|
let worker_end_ix = worker_start_ix + paths_per_worker;
|
||||||
let unnamed_buffers = opened_buffers.clone();
|
let unnamed_buffers = opened_buffers.clone();
|
||||||
|
let limiter = Arc::clone(&max_concurrent_workers);
|
||||||
scope.spawn(async move {
|
scope.spawn(async move {
|
||||||
|
let _guard = limiter.acquire().await;
|
||||||
let mut snapshot_start_ix = 0;
|
let mut snapshot_start_ix = 0;
|
||||||
let mut abs_path = PathBuf::new();
|
let mut abs_path = PathBuf::new();
|
||||||
for snapshot in snapshots {
|
for snapshot in snapshots {
|
||||||
@ -5868,6 +5874,7 @@ impl Project {
|
|||||||
let project_path = SearchMatchCandidate::Path {
|
let project_path = SearchMatchCandidate::Path {
|
||||||
worktree_id: snapshot.id(),
|
worktree_id: snapshot.id(),
|
||||||
path: entry.path.clone(),
|
path: entry.path.clone(),
|
||||||
|
is_ignored: entry.is_ignored,
|
||||||
};
|
};
|
||||||
if matching_paths_tx.send(project_path).await.is_err() {
|
if matching_paths_tx.send(project_path).await.is_err() {
|
||||||
break;
|
break;
|
||||||
@ -5880,6 +5887,94 @@ impl Project {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if query.include_ignored() {
|
||||||
|
for snapshot in snapshots {
|
||||||
|
for ignored_entry in snapshot
|
||||||
|
.entries(query.include_ignored())
|
||||||
|
.filter(|e| e.is_ignored)
|
||||||
|
{
|
||||||
|
let limiter = Arc::clone(&max_concurrent_workers);
|
||||||
|
scope.spawn(async move {
|
||||||
|
let _guard = limiter.acquire().await;
|
||||||
|
let mut ignored_paths_to_process =
|
||||||
|
VecDeque::from([snapshot.abs_path().join(&ignored_entry.path)]);
|
||||||
|
while let Some(ignored_abs_path) =
|
||||||
|
ignored_paths_to_process.pop_front()
|
||||||
|
{
|
||||||
|
if !query.file_matches(Some(&ignored_abs_path))
|
||||||
|
|| snapshot.is_path_excluded(&ignored_abs_path)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(fs_metadata) = fs
|
||||||
|
.metadata(&ignored_abs_path)
|
||||||
|
.await
|
||||||
|
.with_context(|| {
|
||||||
|
format!("fetching fs metadata for {ignored_abs_path:?}")
|
||||||
|
})
|
||||||
|
.log_err()
|
||||||
|
.flatten()
|
||||||
|
{
|
||||||
|
if fs_metadata.is_dir {
|
||||||
|
if let Some(mut subfiles) = fs
|
||||||
|
.read_dir(&ignored_abs_path)
|
||||||
|
.await
|
||||||
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"listing ignored path {ignored_abs_path:?}"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.log_err()
|
||||||
|
{
|
||||||
|
while let Some(subfile) = subfiles.next().await {
|
||||||
|
if let Some(subfile) = subfile.log_err() {
|
||||||
|
ignored_paths_to_process.push_back(subfile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if !fs_metadata.is_symlink {
|
||||||
|
let matches = if let Some(file) = fs
|
||||||
|
.open_sync(&ignored_abs_path)
|
||||||
|
.await
|
||||||
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Opening ignored path {ignored_abs_path:?}"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.log_err()
|
||||||
|
{
|
||||||
|
query.detect(file).unwrap_or(false)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
if matches {
|
||||||
|
let project_path = SearchMatchCandidate::Path {
|
||||||
|
worktree_id: snapshot.id(),
|
||||||
|
path: Arc::from(
|
||||||
|
ignored_abs_path
|
||||||
|
.strip_prefix(snapshot.abs_path())
|
||||||
|
.expect(
|
||||||
|
"scanning worktree-related files",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
is_ignored: true,
|
||||||
|
};
|
||||||
|
if matching_paths_tx
|
||||||
|
.send(project_path)
|
||||||
|
.await
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
@ -5986,11 +6081,24 @@ impl Project {
|
|||||||
let (buffers_tx, buffers_rx) = smol::channel::bounded(1024);
|
let (buffers_tx, buffers_rx) = smol::channel::bounded(1024);
|
||||||
let (sorted_buffers_tx, sorted_buffers_rx) = futures::channel::oneshot::channel();
|
let (sorted_buffers_tx, sorted_buffers_rx) = futures::channel::oneshot::channel();
|
||||||
cx.spawn(move |this, cx| async move {
|
cx.spawn(move |this, cx| async move {
|
||||||
let mut buffers = vec![];
|
let mut buffers = Vec::new();
|
||||||
|
let mut ignored_buffers = Vec::new();
|
||||||
while let Some(entry) = matching_paths_rx.next().await {
|
while let Some(entry) = matching_paths_rx.next().await {
|
||||||
buffers.push(entry);
|
if matches!(
|
||||||
|
entry,
|
||||||
|
SearchMatchCandidate::Path {
|
||||||
|
is_ignored: true,
|
||||||
|
..
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
ignored_buffers.push(entry);
|
||||||
|
} else {
|
||||||
|
buffers.push(entry);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
buffers.sort_by_key(|candidate| candidate.path());
|
buffers.sort_by_key(|candidate| candidate.path());
|
||||||
|
ignored_buffers.sort_by_key(|candidate| candidate.path());
|
||||||
|
buffers.extend(ignored_buffers);
|
||||||
let matching_paths = buffers.clone();
|
let matching_paths = buffers.clone();
|
||||||
let _ = sorted_buffers_tx.send(buffers);
|
let _ = sorted_buffers_tx.send(buffers);
|
||||||
for (index, candidate) in matching_paths.into_iter().enumerate() {
|
for (index, candidate) in matching_paths.into_iter().enumerate() {
|
||||||
@ -6002,7 +6110,9 @@ impl Project {
|
|||||||
cx.spawn(move |mut cx| async move {
|
cx.spawn(move |mut cx| async move {
|
||||||
let buffer = match candidate {
|
let buffer = match candidate {
|
||||||
SearchMatchCandidate::OpenBuffer { buffer, .. } => Some(buffer),
|
SearchMatchCandidate::OpenBuffer { buffer, .. } => Some(buffer),
|
||||||
SearchMatchCandidate::Path { worktree_id, path } => this
|
SearchMatchCandidate::Path {
|
||||||
|
worktree_id, path, ..
|
||||||
|
} => this
|
||||||
.update(&mut cx, |this, cx| {
|
.update(&mut cx, |this, cx| {
|
||||||
this.open_buffer((worktree_id, path), cx)
|
this.open_buffer((worktree_id, path), cx)
|
||||||
})?
|
})?
|
||||||
|
@ -2222,7 +2222,7 @@ impl LocalSnapshot {
|
|||||||
paths
|
paths
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_abs_path_excluded(&self, abs_path: &Path) -> bool {
|
pub fn is_path_excluded(&self, abs_path: &Path) -> bool {
|
||||||
self.file_scan_exclusions
|
self.file_scan_exclusions
|
||||||
.iter()
|
.iter()
|
||||||
.any(|exclude_matcher| exclude_matcher.is_match(abs_path))
|
.any(|exclude_matcher| exclude_matcher.is_match(abs_path))
|
||||||
@ -2395,26 +2395,10 @@ impl BackgroundScannerState {
|
|||||||
self.snapshot.check_invariants(false);
|
self.snapshot.check_invariants(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reload_repositories(&mut self, changed_paths: &[Arc<Path>], fs: &dyn Fs) {
|
fn reload_repositories(&mut self, dot_git_dirs_to_reload: &HashSet<PathBuf>, fs: &dyn Fs) {
|
||||||
let scan_id = self.snapshot.scan_id;
|
let scan_id = self.snapshot.scan_id;
|
||||||
|
|
||||||
// Find each of the .git directories that contain any of the given paths.
|
for dot_git_dir in dot_git_dirs_to_reload {
|
||||||
let mut prev_dot_git_dir = None;
|
|
||||||
for changed_path in changed_paths {
|
|
||||||
let Some(dot_git_dir) = changed_path
|
|
||||||
.ancestors()
|
|
||||||
.find(|ancestor| ancestor.file_name() == Some(&*DOT_GIT))
|
|
||||||
else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Avoid processing the same repository multiple times, if multiple paths
|
|
||||||
// within it have changed.
|
|
||||||
if prev_dot_git_dir == Some(dot_git_dir) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
prev_dot_git_dir = Some(dot_git_dir);
|
|
||||||
|
|
||||||
// If there is already a repository for this .git directory, reload
|
// If there is already a repository for this .git directory, reload
|
||||||
// the status for all of its files.
|
// the status for all of its files.
|
||||||
let repository = self
|
let repository = self
|
||||||
@ -2426,7 +2410,7 @@ impl BackgroundScannerState {
|
|||||||
});
|
});
|
||||||
match repository {
|
match repository {
|
||||||
None => {
|
None => {
|
||||||
self.build_git_repository(dot_git_dir.into(), fs);
|
self.build_git_repository(Arc::from(dot_git_dir.as_path()), fs);
|
||||||
}
|
}
|
||||||
Some((entry_id, repository)) => {
|
Some((entry_id, repository)) => {
|
||||||
if repository.git_dir_scan_id == scan_id {
|
if repository.git_dir_scan_id == scan_id {
|
||||||
@ -2440,7 +2424,7 @@ impl BackgroundScannerState {
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
log::info!("reload git repository {:?}", dot_git_dir);
|
log::info!("reload git repository {dot_git_dir:?}");
|
||||||
let repository = repository.repo_ptr.lock();
|
let repository = repository.repo_ptr.lock();
|
||||||
let branch = repository.branch_name();
|
let branch = repository.branch_name();
|
||||||
repository.reload_index();
|
repository.reload_index();
|
||||||
@ -2471,7 +2455,9 @@ impl BackgroundScannerState {
|
|||||||
ids_to_preserve.insert(work_directory_id);
|
ids_to_preserve.insert(work_directory_id);
|
||||||
} else {
|
} else {
|
||||||
let git_dir_abs_path = snapshot.abs_path().join(&entry.git_dir_path);
|
let git_dir_abs_path = snapshot.abs_path().join(&entry.git_dir_path);
|
||||||
if snapshot.is_abs_path_excluded(&git_dir_abs_path)
|
let git_dir_excluded = snapshot.is_path_excluded(&entry.git_dir_path)
|
||||||
|
|| snapshot.is_path_excluded(&git_dir_abs_path);
|
||||||
|
if git_dir_excluded
|
||||||
&& !matches!(smol::block_on(fs.metadata(&git_dir_abs_path)), Ok(None))
|
&& !matches!(smol::block_on(fs.metadata(&git_dir_abs_path)), Ok(None))
|
||||||
{
|
{
|
||||||
ids_to_preserve.insert(work_directory_id);
|
ids_to_preserve.insert(work_directory_id);
|
||||||
@ -3303,11 +3289,26 @@ impl BackgroundScanner {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let mut relative_paths = Vec::with_capacity(abs_paths.len());
|
let mut relative_paths = Vec::with_capacity(abs_paths.len());
|
||||||
|
let mut dot_git_paths_to_reload = HashSet::default();
|
||||||
abs_paths.sort_unstable();
|
abs_paths.sort_unstable();
|
||||||
abs_paths.dedup_by(|a, b| a.starts_with(&b));
|
abs_paths.dedup_by(|a, b| a.starts_with(&b));
|
||||||
abs_paths.retain(|abs_path| {
|
abs_paths.retain(|abs_path| {
|
||||||
let snapshot = &self.state.lock().snapshot;
|
let snapshot = &self.state.lock().snapshot;
|
||||||
{
|
{
|
||||||
|
let mut is_git_related = false;
|
||||||
|
if let Some(dot_git_dir) = abs_path
|
||||||
|
.ancestors()
|
||||||
|
.find(|ancestor| ancestor.file_name() == Some(&*DOT_GIT))
|
||||||
|
{
|
||||||
|
let dot_git_path = dot_git_dir
|
||||||
|
.strip_prefix(&root_canonical_path)
|
||||||
|
.ok()
|
||||||
|
.map(|path| path.to_path_buf())
|
||||||
|
.unwrap_or_else(|| dot_git_dir.to_path_buf());
|
||||||
|
dot_git_paths_to_reload.insert(dot_git_path.to_path_buf());
|
||||||
|
is_git_related = true;
|
||||||
|
}
|
||||||
|
|
||||||
let relative_path: Arc<Path> =
|
let relative_path: Arc<Path> =
|
||||||
if let Ok(path) = abs_path.strip_prefix(&root_canonical_path) {
|
if let Ok(path) = abs_path.strip_prefix(&root_canonical_path) {
|
||||||
path.into()
|
path.into()
|
||||||
@ -3318,22 +3319,30 @@ impl BackgroundScanner {
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
if !is_git_related(&abs_path) {
|
let parent_dir_is_loaded = relative_path.parent().map_or(true, |parent| {
|
||||||
let parent_dir_is_loaded = relative_path.parent().map_or(true, |parent| {
|
snapshot
|
||||||
snapshot
|
.entry_for_path(parent)
|
||||||
.entry_for_path(parent)
|
.map_or(false, |entry| entry.kind == EntryKind::Dir)
|
||||||
.map_or(false, |entry| entry.kind == EntryKind::Dir)
|
});
|
||||||
});
|
if !parent_dir_is_loaded {
|
||||||
if !parent_dir_is_loaded {
|
log::debug!("ignoring event {relative_path:?} within unloaded directory");
|
||||||
log::debug!("ignoring event {relative_path:?} within unloaded directory");
|
return false;
|
||||||
return false;
|
}
|
||||||
|
|
||||||
|
// FS events may come for files which parent directory is excluded, need to check ignore those.
|
||||||
|
let mut path_to_test = abs_path.clone();
|
||||||
|
let mut excluded_file_event = snapshot.is_path_excluded(abs_path)
|
||||||
|
|| snapshot.is_path_excluded(&relative_path);
|
||||||
|
while !excluded_file_event && path_to_test.pop() {
|
||||||
|
if snapshot.is_path_excluded(&path_to_test) {
|
||||||
|
excluded_file_event = true;
|
||||||
}
|
}
|
||||||
if snapshot.is_abs_path_excluded(abs_path) {
|
}
|
||||||
log::debug!(
|
if excluded_file_event {
|
||||||
"ignoring FS event for path {relative_path:?} within excluded directory"
|
if !is_git_related {
|
||||||
);
|
log::debug!("ignoring FS event for excluded path {relative_path:?}");
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
relative_paths.push(relative_path);
|
relative_paths.push(relative_path);
|
||||||
@ -3341,31 +3350,39 @@ impl BackgroundScanner {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if relative_paths.is_empty() {
|
if dot_git_paths_to_reload.is_empty() && relative_paths.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
log::debug!("received fs events {:?}", relative_paths);
|
if !relative_paths.is_empty() {
|
||||||
|
log::debug!("received fs events {:?}", relative_paths);
|
||||||
|
|
||||||
let (scan_job_tx, scan_job_rx) = channel::unbounded();
|
let (scan_job_tx, scan_job_rx) = channel::unbounded();
|
||||||
self.reload_entries_for_paths(
|
self.reload_entries_for_paths(
|
||||||
root_path,
|
root_path,
|
||||||
root_canonical_path,
|
root_canonical_path,
|
||||||
&relative_paths,
|
&relative_paths,
|
||||||
abs_paths,
|
abs_paths,
|
||||||
Some(scan_job_tx.clone()),
|
Some(scan_job_tx.clone()),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
drop(scan_job_tx);
|
drop(scan_job_tx);
|
||||||
self.scan_dirs(false, scan_job_rx).await;
|
self.scan_dirs(false, scan_job_rx).await;
|
||||||
|
|
||||||
let (scan_job_tx, scan_job_rx) = channel::unbounded();
|
let (scan_job_tx, scan_job_rx) = channel::unbounded();
|
||||||
self.update_ignore_statuses(scan_job_tx).await;
|
self.update_ignore_statuses(scan_job_tx).await;
|
||||||
self.scan_dirs(false, scan_job_rx).await;
|
self.scan_dirs(false, scan_job_rx).await;
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut state = self.state.lock();
|
let mut state = self.state.lock();
|
||||||
state.reload_repositories(&relative_paths, self.fs.as_ref());
|
if !dot_git_paths_to_reload.is_empty() {
|
||||||
|
if relative_paths.is_empty() {
|
||||||
|
state.snapshot.scan_id += 1;
|
||||||
|
}
|
||||||
|
log::debug!("reloading repositories: {dot_git_paths_to_reload:?}");
|
||||||
|
state.reload_repositories(&dot_git_paths_to_reload, self.fs.as_ref());
|
||||||
|
}
|
||||||
state.snapshot.completed_scan_id = state.snapshot.scan_id;
|
state.snapshot.completed_scan_id = state.snapshot.scan_id;
|
||||||
for (_, entry_id) in mem::take(&mut state.removed_entry_ids) {
|
for (_, entry_id) in mem::take(&mut state.removed_entry_ids) {
|
||||||
state.scanned_dirs.remove(&entry_id);
|
state.scanned_dirs.remove(&entry_id);
|
||||||
@ -3505,7 +3522,7 @@ impl BackgroundScanner {
|
|||||||
let state = self.state.lock();
|
let state = self.state.lock();
|
||||||
let snapshot = &state.snapshot;
|
let snapshot = &state.snapshot;
|
||||||
root_abs_path = snapshot.abs_path().clone();
|
root_abs_path = snapshot.abs_path().clone();
|
||||||
if snapshot.is_abs_path_excluded(&job.abs_path) {
|
if snapshot.is_path_excluded(&job.abs_path) {
|
||||||
log::error!("skipping excluded directory {:?}", job.path);
|
log::error!("skipping excluded directory {:?}", job.path);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@ -3577,7 +3594,7 @@ impl BackgroundScanner {
|
|||||||
|
|
||||||
{
|
{
|
||||||
let mut state = self.state.lock();
|
let mut state = self.state.lock();
|
||||||
if state.snapshot.is_abs_path_excluded(&child_abs_path) {
|
if state.snapshot.is_path_excluded(&child_abs_path) {
|
||||||
let relative_path = job.path.join(child_name);
|
let relative_path = job.path.join(child_name);
|
||||||
log::debug!("skipping excluded child entry {relative_path:?}");
|
log::debug!("skipping excluded child entry {relative_path:?}");
|
||||||
state.remove_path(&relative_path);
|
state.remove_path(&relative_path);
|
||||||
@ -4119,12 +4136,6 @@ impl BackgroundScanner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_git_related(abs_path: &Path) -> bool {
|
|
||||||
abs_path
|
|
||||||
.components()
|
|
||||||
.any(|c| c.as_os_str() == *DOT_GIT || c.as_os_str() == *GITIGNORE)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn char_bag_for_path(root_char_bag: CharBag, path: &Path) -> CharBag {
|
fn char_bag_for_path(root_char_bag: CharBag, path: &Path) -> CharBag {
|
||||||
let mut result = root_char_bag;
|
let mut result = root_char_bag;
|
||||||
result.extend(
|
result.extend(
|
||||||
|
@ -992,6 +992,146 @@ async fn test_file_scan_exclusions(cx: &mut TestAppContext) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
cx.executor().allow_parking();
|
||||||
|
let dir = temp_tree(json!({
|
||||||
|
".git": {
|
||||||
|
"HEAD": "ref: refs/heads/main\n",
|
||||||
|
"foo": "bar",
|
||||||
|
},
|
||||||
|
".gitignore": "**/target\n/node_modules\ntest_output\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![
|
||||||
|
"**/.git".to_string(),
|
||||||
|
"node_modules/".to_string(),
|
||||||
|
"build_output".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,
|
||||||
|
&[
|
||||||
|
".git/HEAD",
|
||||||
|
".git/foo",
|
||||||
|
"node_modules/.DS_Store",
|
||||||
|
"node_modules/prettier",
|
||||||
|
"node_modules/prettier/package.json",
|
||||||
|
],
|
||||||
|
&["target", "node_modules"],
|
||||||
|
&[
|
||||||
|
".DS_Store",
|
||||||
|
"src/.DS_Store",
|
||||||
|
"src/lib.rs",
|
||||||
|
"src/foo/foo.rs",
|
||||||
|
"src/foo/another.rs",
|
||||||
|
"src/bar/bar.rs",
|
||||||
|
".gitignore",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
let new_excluded_dir = dir.path().join("build_output");
|
||||||
|
let new_ignored_dir = dir.path().join("test_output");
|
||||||
|
std::fs::create_dir_all(&new_excluded_dir)
|
||||||
|
.unwrap_or_else(|e| panic!("Failed to create a {new_excluded_dir:?} directory: {e}"));
|
||||||
|
std::fs::create_dir_all(&new_ignored_dir)
|
||||||
|
.unwrap_or_else(|e| panic!("Failed to create a {new_ignored_dir:?} directory: {e}"));
|
||||||
|
let node_modules_dir = dir.path().join("node_modules");
|
||||||
|
let dot_git_dir = dir.path().join(".git");
|
||||||
|
let src_dir = dir.path().join("src");
|
||||||
|
for existing_dir in [&node_modules_dir, &dot_git_dir, &src_dir] {
|
||||||
|
assert!(
|
||||||
|
existing_dir.is_dir(),
|
||||||
|
"Expect {existing_dir:?} to be present in the FS already"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for directory_for_new_file in [
|
||||||
|
new_excluded_dir,
|
||||||
|
new_ignored_dir,
|
||||||
|
node_modules_dir,
|
||||||
|
dot_git_dir,
|
||||||
|
src_dir,
|
||||||
|
] {
|
||||||
|
std::fs::write(directory_for_new_file.join("new_file"), "new file contents")
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
|
panic!("Failed to create in {directory_for_new_file:?} a new file: {e}")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
tree.flush_fs_events(cx).await;
|
||||||
|
|
||||||
|
tree.read_with(cx, |tree, _| {
|
||||||
|
check_worktree_entries(
|
||||||
|
tree,
|
||||||
|
&[
|
||||||
|
".git/HEAD",
|
||||||
|
".git/foo",
|
||||||
|
".git/new_file",
|
||||||
|
"node_modules/.DS_Store",
|
||||||
|
"node_modules/prettier",
|
||||||
|
"node_modules/prettier/package.json",
|
||||||
|
"node_modules/new_file",
|
||||||
|
"build_output",
|
||||||
|
"build_output/new_file",
|
||||||
|
"test_output/new_file",
|
||||||
|
],
|
||||||
|
&["target", "node_modules", "test_output"],
|
||||||
|
&[
|
||||||
|
".DS_Store",
|
||||||
|
"src/.DS_Store",
|
||||||
|
"src/lib.rs",
|
||||||
|
"src/foo/foo.rs",
|
||||||
|
"src/foo/another.rs",
|
||||||
|
"src/bar/bar.rs",
|
||||||
|
"src/new_file",
|
||||||
|
".gitignore",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test(iterations = 30)]
|
#[gpui::test(iterations = 30)]
|
||||||
async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
|
async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
|
||||||
init_test(cx);
|
init_test(cx);
|
||||||
@ -1056,7 +1196,7 @@ async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
|
|||||||
async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
|
async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
|
||||||
init_test(cx);
|
init_test(cx);
|
||||||
cx.executor().allow_parking();
|
cx.executor().allow_parking();
|
||||||
let client_fake = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
let client_fake = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||||
|
|
||||||
let fs_fake = FakeFs::new(cx.background_executor.clone());
|
let fs_fake = FakeFs::new(cx.background_executor.clone());
|
||||||
fs_fake
|
fs_fake
|
||||||
@ -1096,7 +1236,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
|
|||||||
assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
|
assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
|
||||||
});
|
});
|
||||||
|
|
||||||
let client_real = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
let client_real = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||||
|
|
||||||
let fs_real = Arc::new(RealFs);
|
let fs_real = Arc::new(RealFs);
|
||||||
let temp_root = temp_tree(json!({
|
let temp_root = temp_tree(json!({
|
||||||
@ -2181,7 +2321,7 @@ async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
|
|||||||
|
|
||||||
fn build_client(cx: &mut TestAppContext) -> Arc<Client> {
|
fn build_client(cx: &mut TestAppContext) -> Arc<Client> {
|
||||||
let http_client = FakeHttpClient::with_404_response();
|
let http_client = FakeHttpClient::with_404_response();
|
||||||
cx.read(|cx| Client::new(http_client, cx))
|
cx.update(|cx| Client::new(http_client, cx))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
|
@ -10,8 +10,8 @@ use anyhow::{anyhow, Result};
|
|||||||
use gpui::{
|
use gpui::{
|
||||||
actions, div, px, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext,
|
actions, div, px, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext,
|
||||||
ClipboardItem, Div, EventEmitter, FocusHandle, Focusable, FocusableView, InteractiveElement,
|
ClipboardItem, Div, EventEmitter, FocusHandle, Focusable, FocusableView, InteractiveElement,
|
||||||
Model, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render,
|
IntoElement, Model, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel,
|
||||||
RenderOnce, Stateful, StatefulInteractiveElement, Styled, Task, UniformListScrollHandle, View,
|
Render, Stateful, StatefulInteractiveElement, Styled, Task, UniformListScrollHandle, View,
|
||||||
ViewContext, VisualContext as _, WeakView, WindowContext,
|
ViewContext, VisualContext as _, WeakView, WindowContext,
|
||||||
};
|
};
|
||||||
use menu::{Confirm, SelectNext, SelectPrev};
|
use menu::{Confirm, SelectNext, SelectPrev};
|
||||||
@ -371,7 +371,7 @@ impl ProjectPanel {
|
|||||||
_entry_id: ProjectEntryId,
|
_entry_id: ProjectEntryId,
|
||||||
_cx: &mut ViewContext<Self>,
|
_cx: &mut ViewContext<Self>,
|
||||||
) {
|
) {
|
||||||
todo!()
|
// todo!()
|
||||||
// let project = self.project.read(cx);
|
// let project = self.project.read(cx);
|
||||||
|
|
||||||
// let worktree_id = if let Some(id) = project.worktree_id_for_entry(entry_id, cx) {
|
// let worktree_id = if let Some(id) = project.worktree_id_for_entry(entry_id, cx) {
|
||||||
@ -644,6 +644,7 @@ impl ProjectPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
|
fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
|
||||||
|
dbg!("odd");
|
||||||
self.edit_state = None;
|
self.edit_state = None;
|
||||||
self.update_visible_entries(None, cx);
|
self.update_visible_entries(None, cx);
|
||||||
cx.focus(&self.focus_handle);
|
cx.focus(&self.focus_handle);
|
||||||
|
@ -1767,16 +1767,13 @@ impl View for ProjectSearchBar {
|
|||||||
render_option_button_icon("icons/word_search.svg", SearchOptions::WHOLE_WORD, cx)
|
render_option_button_icon("icons/word_search.svg", SearchOptions::WHOLE_WORD, cx)
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut include_ignored = is_semantic_disabled.then(|| {
|
let include_ignored = is_semantic_disabled.then(|| {
|
||||||
render_option_button_icon(
|
render_option_button_icon(
|
||||||
// TODO proper icon
|
"icons/file_icons/git.svg",
|
||||||
"icons/case_insensitive.svg",
|
|
||||||
SearchOptions::INCLUDE_IGNORED,
|
SearchOptions::INCLUDE_IGNORED,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
// TODO not implemented yet
|
|
||||||
let _ = include_ignored.take();
|
|
||||||
|
|
||||||
let search_button_for_mode = |mode, side, cx: &mut ViewContext<ProjectSearchBar>| {
|
let search_button_for_mode = |mode, side, cx: &mut ViewContext<ProjectSearchBar>| {
|
||||||
let is_active = if let Some(search) = self.active_project_search.as_ref() {
|
let is_active = if let Some(search) = self.active_project_search.as_ref() {
|
||||||
|
@ -7,12 +7,12 @@ use crate::{
|
|||||||
ToggleCaseSensitive, ToggleReplace, ToggleWholeWord,
|
ToggleCaseSensitive, ToggleReplace, ToggleWholeWord,
|
||||||
};
|
};
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use editor::Editor;
|
use editor::{Editor, EditorMode};
|
||||||
use futures::channel::oneshot;
|
use futures::channel::oneshot;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, div, red, Action, AppContext, Div, EventEmitter, InteractiveElement as _,
|
actions, div, red, Action, AppContext, Div, EventEmitter, InteractiveElement as _, IntoElement,
|
||||||
ParentElement as _, Render, RenderOnce, Styled, Subscription, Task, View, ViewContext,
|
ParentElement as _, Render, Styled, Subscription, Task, View, ViewContext, VisualContext as _,
|
||||||
VisualContext as _, WindowContext,
|
WeakView, WindowContext,
|
||||||
};
|
};
|
||||||
use project::search::SearchQuery;
|
use project::search::SearchQuery;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
@ -23,7 +23,7 @@ use util::ResultExt;
|
|||||||
use workspace::{
|
use workspace::{
|
||||||
item::ItemHandle,
|
item::ItemHandle,
|
||||||
searchable::{Direction, SearchEvent, SearchableItemHandle, WeakSearchableItemHandle},
|
searchable::{Direction, SearchEvent, SearchableItemHandle, WeakSearchableItemHandle},
|
||||||
ToolbarItemLocation, ToolbarItemView, Workspace,
|
ToolbarItemLocation, ToolbarItemView,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(PartialEq, Clone, Deserialize, Default, Action)]
|
#[derive(PartialEq, Clone, Deserialize, Default, Action)]
|
||||||
@ -38,7 +38,7 @@ pub enum Event {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn init(cx: &mut AppContext) {
|
pub fn init(cx: &mut AppContext) {
|
||||||
cx.observe_new_views(|workspace: &mut Workspace, _| BufferSearchBar::register(workspace))
|
cx.observe_new_views(|editor: &mut Editor, cx| BufferSearchBar::register(editor, cx))
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -187,6 +187,7 @@ impl Render for BufferSearchBar {
|
|||||||
})
|
})
|
||||||
.on_action(cx.listener(Self::previous_history_query))
|
.on_action(cx.listener(Self::previous_history_query))
|
||||||
.on_action(cx.listener(Self::next_history_query))
|
.on_action(cx.listener(Self::next_history_query))
|
||||||
|
.on_action(cx.listener(Self::dismiss))
|
||||||
.w_full()
|
.w_full()
|
||||||
.p_1()
|
.p_1()
|
||||||
.child(
|
.child(
|
||||||
@ -294,9 +295,19 @@ impl ToolbarItemView for BufferSearchBar {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl BufferSearchBar {
|
impl BufferSearchBar {
|
||||||
pub fn register(workspace: &mut Workspace) {
|
pub fn register(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
|
||||||
workspace.register_action(|workspace, a: &Deploy, cx| {
|
if editor.mode() != EditorMode::Full {
|
||||||
workspace.active_pane().update(cx, |this, cx| {
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let handle = cx.view().downgrade();
|
||||||
|
|
||||||
|
editor.register_action(move |a: &Deploy, cx| {
|
||||||
|
let Some(pane) = handle.upgrade().and_then(|editor| editor.read(cx).pane(cx)) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
pane.update(cx, |this, cx| {
|
||||||
this.toolbar().update(cx, |this, cx| {
|
this.toolbar().update(cx, |this, cx| {
|
||||||
if let Some(search_bar) = this.item_of_type::<BufferSearchBar>() {
|
if let Some(search_bar) = this.item_of_type::<BufferSearchBar>() {
|
||||||
search_bar.update(cx, |this, cx| {
|
search_bar.update(cx, |this, cx| {
|
||||||
@ -316,11 +327,16 @@ impl BufferSearchBar {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
fn register_action<A: Action>(
|
fn register_action<A: Action>(
|
||||||
workspace: &mut Workspace,
|
editor: &mut Editor,
|
||||||
|
handle: WeakView<Editor>,
|
||||||
update: fn(&mut BufferSearchBar, &A, &mut ViewContext<BufferSearchBar>),
|
update: fn(&mut BufferSearchBar, &A, &mut ViewContext<BufferSearchBar>),
|
||||||
) {
|
) {
|
||||||
workspace.register_action(move |workspace, action: &A, cx| {
|
editor.register_action(move |action: &A, cx| {
|
||||||
workspace.active_pane().update(cx, move |this, cx| {
|
let Some(pane) = handle.upgrade().and_then(|editor| editor.read(cx).pane(cx))
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
pane.update(cx, move |this, cx| {
|
||||||
this.toolbar().update(cx, move |this, cx| {
|
this.toolbar().update(cx, move |this, cx| {
|
||||||
if let Some(search_bar) = this.item_of_type::<BufferSearchBar>() {
|
if let Some(search_bar) = this.item_of_type::<BufferSearchBar>() {
|
||||||
search_bar.update(cx, move |this, cx| update(this, action, cx));
|
search_bar.update(cx, move |this, cx| update(this, action, cx));
|
||||||
@ -331,49 +347,76 @@ impl BufferSearchBar {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
register_action(workspace, |this, action: &ToggleCaseSensitive, cx| {
|
let handle = cx.view().downgrade();
|
||||||
if this.supported_options().case {
|
register_action(
|
||||||
this.toggle_case_sensitive(action, cx);
|
editor,
|
||||||
}
|
handle.clone(),
|
||||||
});
|
|this, action: &ToggleCaseSensitive, cx| {
|
||||||
register_action(workspace, |this, action: &ToggleWholeWord, cx| {
|
if this.supported_options().case {
|
||||||
if this.supported_options().word {
|
this.toggle_case_sensitive(action, cx);
|
||||||
this.toggle_whole_word(action, cx);
|
}
|
||||||
}
|
},
|
||||||
});
|
);
|
||||||
register_action(workspace, |this, action: &ToggleReplace, cx| {
|
register_action(
|
||||||
if this.supported_options().replacement {
|
editor,
|
||||||
this.toggle_replace(action, cx);
|
handle.clone(),
|
||||||
}
|
|this, action: &ToggleWholeWord, cx| {
|
||||||
});
|
if this.supported_options().word {
|
||||||
register_action(workspace, |this, _: &ActivateRegexMode, cx| {
|
this.toggle_whole_word(action, cx);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
register_action(
|
||||||
|
editor,
|
||||||
|
handle.clone(),
|
||||||
|
|this, action: &ToggleReplace, cx| {
|
||||||
|
if this.supported_options().replacement {
|
||||||
|
this.toggle_replace(action, cx);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
register_action(editor, handle.clone(), |this, _: &ActivateRegexMode, cx| {
|
||||||
if this.supported_options().regex {
|
if this.supported_options().regex {
|
||||||
this.activate_search_mode(SearchMode::Regex, cx);
|
this.activate_search_mode(SearchMode::Regex, cx);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
register_action(workspace, |this, _: &ActivateTextMode, cx| {
|
register_action(editor, handle.clone(), |this, _: &ActivateTextMode, cx| {
|
||||||
this.activate_search_mode(SearchMode::Text, cx);
|
this.activate_search_mode(SearchMode::Text, cx);
|
||||||
});
|
});
|
||||||
register_action(workspace, |this, action: &CycleMode, cx| {
|
register_action(editor, handle.clone(), |this, action: &CycleMode, cx| {
|
||||||
if this.supported_options().regex {
|
if this.supported_options().regex {
|
||||||
// If regex is not supported then search has just one mode (text) - in that case there's no point in supporting
|
// If regex is not supported then search has just one mode (text) - in that case there's no point in supporting
|
||||||
// cycling.
|
// cycling.
|
||||||
this.cycle_mode(action, cx)
|
this.cycle_mode(action, cx)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
register_action(workspace, |this, action: &SelectNextMatch, cx| {
|
register_action(
|
||||||
this.select_next_match(action, cx);
|
editor,
|
||||||
});
|
handle.clone(),
|
||||||
register_action(workspace, |this, action: &SelectPrevMatch, cx| {
|
|this, action: &SelectNextMatch, cx| {
|
||||||
this.select_prev_match(action, cx);
|
this.select_next_match(action, cx);
|
||||||
});
|
},
|
||||||
register_action(workspace, |this, action: &SelectAllMatches, cx| {
|
);
|
||||||
this.select_all_matches(action, cx);
|
register_action(
|
||||||
});
|
editor,
|
||||||
register_action(workspace, |this, _: &editor::Cancel, cx| {
|
handle.clone(),
|
||||||
|
|this, action: &SelectPrevMatch, cx| {
|
||||||
|
this.select_prev_match(action, cx);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
register_action(
|
||||||
|
editor,
|
||||||
|
handle.clone(),
|
||||||
|
|this, action: &SelectAllMatches, cx| {
|
||||||
|
this.select_all_matches(action, cx);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
register_action(editor, handle.clone(), |this, _: &editor::Cancel, cx| {
|
||||||
if !this.dismissed {
|
if !this.dismissed {
|
||||||
this.dismiss(&Dismiss, cx);
|
this.dismiss(&Dismiss, cx);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
cx.propagate();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
pub fn new(cx: &mut ViewContext<Self>) -> Self {
|
pub fn new(cx: &mut ViewContext<Self>) -> Self {
|
||||||
@ -538,7 +581,7 @@ impl BufferSearchBar {
|
|||||||
self.update_matches(cx)
|
self.update_matches(cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_action_button(&self) -> impl RenderOnce {
|
fn render_action_button(&self) -> impl IntoElement {
|
||||||
// let tooltip_style = theme.tooltip.clone();
|
// let tooltip_style = theme.tooltip.clone();
|
||||||
|
|
||||||
// let style = theme.search.action_button.clone();
|
// let style = theme.search.action_button.clone();
|
||||||
|
@ -85,6 +85,7 @@ pub fn init(cx: &mut AppContext) {
|
|||||||
cx.capture_action(ProjectSearchView::replace_next);
|
cx.capture_action(ProjectSearchView::replace_next);
|
||||||
add_toggle_option_action::<ToggleCaseSensitive>(SearchOptions::CASE_SENSITIVE, cx);
|
add_toggle_option_action::<ToggleCaseSensitive>(SearchOptions::CASE_SENSITIVE, cx);
|
||||||
add_toggle_option_action::<ToggleWholeWord>(SearchOptions::WHOLE_WORD, cx);
|
add_toggle_option_action::<ToggleWholeWord>(SearchOptions::WHOLE_WORD, cx);
|
||||||
|
add_toggle_option_action::<ToggleIncludeIgnored>(SearchOptions::INCLUDE_IGNORED, cx);
|
||||||
add_toggle_filters_action::<ToggleFilters>(cx);
|
add_toggle_filters_action::<ToggleFilters>(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1192,6 +1193,7 @@ impl ProjectSearchView {
|
|||||||
text,
|
text,
|
||||||
self.search_options.contains(SearchOptions::WHOLE_WORD),
|
self.search_options.contains(SearchOptions::WHOLE_WORD),
|
||||||
self.search_options.contains(SearchOptions::CASE_SENSITIVE),
|
self.search_options.contains(SearchOptions::CASE_SENSITIVE),
|
||||||
|
self.search_options.contains(SearchOptions::INCLUDE_IGNORED),
|
||||||
included_files,
|
included_files,
|
||||||
excluded_files,
|
excluded_files,
|
||||||
) {
|
) {
|
||||||
@ -1210,6 +1212,7 @@ impl ProjectSearchView {
|
|||||||
text,
|
text,
|
||||||
self.search_options.contains(SearchOptions::WHOLE_WORD),
|
self.search_options.contains(SearchOptions::WHOLE_WORD),
|
||||||
self.search_options.contains(SearchOptions::CASE_SENSITIVE),
|
self.search_options.contains(SearchOptions::CASE_SENSITIVE),
|
||||||
|
self.search_options.contains(SearchOptions::INCLUDE_IGNORED),
|
||||||
included_files,
|
included_files,
|
||||||
excluded_files,
|
excluded_files,
|
||||||
) {
|
) {
|
||||||
@ -1764,6 +1767,14 @@ impl View for ProjectSearchBar {
|
|||||||
render_option_button_icon("icons/word_search.svg", SearchOptions::WHOLE_WORD, cx)
|
render_option_button_icon("icons/word_search.svg", SearchOptions::WHOLE_WORD, cx)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let include_ignored = is_semantic_disabled.then(|| {
|
||||||
|
render_option_button_icon(
|
||||||
|
"icons/file_icons/git.svg",
|
||||||
|
SearchOptions::INCLUDE_IGNORED,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
let search_button_for_mode = |mode, side, cx: &mut ViewContext<ProjectSearchBar>| {
|
let search_button_for_mode = |mode, side, cx: &mut ViewContext<ProjectSearchBar>| {
|
||||||
let is_active = if let Some(search) = self.active_project_search.as_ref() {
|
let is_active = if let Some(search) = self.active_project_search.as_ref() {
|
||||||
let search = search.read(cx);
|
let search = search.read(cx);
|
||||||
@ -1879,7 +1890,15 @@ impl View for ProjectSearchBar {
|
|||||||
.with_children(search.filters_enabled.then(|| {
|
.with_children(search.filters_enabled.then(|| {
|
||||||
Flex::row()
|
Flex::row()
|
||||||
.with_child(
|
.with_child(
|
||||||
ChildView::new(&search.included_files_editor, cx)
|
Flex::row()
|
||||||
|
.with_child(
|
||||||
|
ChildView::new(&search.included_files_editor, cx)
|
||||||
|
.contained()
|
||||||
|
.constrained()
|
||||||
|
.with_height(theme.search.search_bar_row_height)
|
||||||
|
.flex(1., true),
|
||||||
|
)
|
||||||
|
.with_children(include_ignored)
|
||||||
.contained()
|
.contained()
|
||||||
.with_style(include_container_style)
|
.with_style(include_container_style)
|
||||||
.constrained()
|
.constrained()
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use bitflags::bitflags;
|
use bitflags::bitflags;
|
||||||
pub use buffer_search::BufferSearchBar;
|
pub use buffer_search::BufferSearchBar;
|
||||||
use gpui::{actions, Action, AppContext, RenderOnce};
|
use gpui::{actions, Action, AppContext, IntoElement};
|
||||||
pub use mode::SearchMode;
|
pub use mode::SearchMode;
|
||||||
use project::search::SearchQuery;
|
use project::search::SearchQuery;
|
||||||
use ui::ButtonVariant;
|
use ui::ButtonVariant;
|
||||||
@ -82,7 +82,7 @@ impl SearchOptions {
|
|||||||
options
|
options
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn as_button(&self, active: bool) -> impl RenderOnce {
|
pub fn as_button(&self, active: bool) -> impl IntoElement {
|
||||||
ui::IconButton::new(0, self.icon())
|
ui::IconButton::new(0, self.icon())
|
||||||
.on_click({
|
.on_click({
|
||||||
let action = self.to_toggle_action();
|
let action = self.to_toggle_action();
|
||||||
@ -95,7 +95,7 @@ impl SearchOptions {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn toggle_replace_button(active: bool) -> impl RenderOnce {
|
fn toggle_replace_button(active: bool) -> impl IntoElement {
|
||||||
// todo: add toggle_replace button
|
// todo: add toggle_replace button
|
||||||
ui::IconButton::new(0, ui::Icon::Replace)
|
ui::IconButton::new(0, ui::Icon::Replace)
|
||||||
.on_click(|_, cx| {
|
.on_click(|_, cx| {
|
||||||
@ -109,7 +109,7 @@ fn toggle_replace_button(active: bool) -> impl RenderOnce {
|
|||||||
fn render_replace_button(
|
fn render_replace_button(
|
||||||
action: impl Action + 'static + Send + Sync,
|
action: impl Action + 'static + Send + Sync,
|
||||||
icon: ui::Icon,
|
icon: ui::Icon,
|
||||||
) -> impl RenderOnce {
|
) -> impl IntoElement {
|
||||||
// todo: add tooltip
|
// todo: add tooltip
|
||||||
ui::IconButton::new(0, icon).on_click(move |_, cx| {
|
ui::IconButton::new(0, icon).on_click(move |_, cx| {
|
||||||
cx.dispatch_action(action.boxed_clone());
|
cx.dispatch_action(action.boxed_clone());
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use gpui::{MouseDownEvent, RenderOnce, WindowContext};
|
use gpui::{IntoElement, MouseDownEvent, WindowContext};
|
||||||
use ui::{Button, ButtonVariant, IconButton};
|
use ui::{Button, ButtonVariant, IconButton};
|
||||||
|
|
||||||
use crate::mode::SearchMode;
|
use crate::mode::SearchMode;
|
||||||
@ -7,7 +7,7 @@ pub(super) fn render_nav_button(
|
|||||||
icon: ui::Icon,
|
icon: ui::Icon,
|
||||||
_active: bool,
|
_active: bool,
|
||||||
on_click: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
|
on_click: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
|
||||||
) -> impl RenderOnce {
|
) -> impl IntoElement {
|
||||||
// let tooltip_style = cx.theme().tooltip.clone();
|
// let tooltip_style = cx.theme().tooltip.clone();
|
||||||
// let cursor_style = if active {
|
// let cursor_style = if active {
|
||||||
// CursorStyle::PointingHand
|
// CursorStyle::PointingHand
|
||||||
|
@ -1659,13 +1659,13 @@ fn elixir_lang() -> Arc<Language> {
|
|||||||
target: (identifier) @name)
|
target: (identifier) @name)
|
||||||
operator: "when")
|
operator: "when")
|
||||||
])
|
])
|
||||||
(#match? @name "^(def|defp|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp)$")) @item
|
(#any-match? @name "^(def|defp|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp)$")) @item
|
||||||
)
|
)
|
||||||
|
|
||||||
(call
|
(call
|
||||||
target: (identifier) @name
|
target: (identifier) @name
|
||||||
(arguments (alias) @name)
|
(arguments (alias) @name)
|
||||||
(#match? @name "^(defmodule|defprotocol)$")) @item
|
(#any-match? @name "^(defmodule|defprotocol)$")) @item
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
|
10
crates/story/Cargo.toml
Normal file
10
crates/story/Cargo.toml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
[package]
|
||||||
|
name = "story"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
gpui = { package = "gpui2", path = "../gpui2" }
|
3
crates/story/src/lib.rs
Normal file
3
crates/story/src/lib.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
mod story;
|
||||||
|
|
||||||
|
pub use story::*;
|
35
crates/story/src/story.rs
Normal file
35
crates/story/src/story.rs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
use gpui::prelude::*;
|
||||||
|
use gpui::{div, hsla, Div, SharedString};
|
||||||
|
|
||||||
|
pub struct Story {}
|
||||||
|
|
||||||
|
impl Story {
|
||||||
|
pub fn container() -> Div {
|
||||||
|
div().size_full().flex().flex_col().pt_2().px_4().bg(hsla(
|
||||||
|
0. / 360.,
|
||||||
|
0. / 100.,
|
||||||
|
100. / 100.,
|
||||||
|
1.,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn title(title: impl Into<SharedString>) -> impl Element {
|
||||||
|
div()
|
||||||
|
.text_xl()
|
||||||
|
.text_color(hsla(0. / 360., 0. / 100., 0. / 100., 1.))
|
||||||
|
.child(title.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn title_for<T>() -> impl Element {
|
||||||
|
Self::title(std::any::type_name::<T>())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn label(label: impl Into<SharedString>) -> impl Element {
|
||||||
|
div()
|
||||||
|
.mt_4()
|
||||||
|
.mb_2()
|
||||||
|
.text_xs()
|
||||||
|
.text_color(hsla(0. / 360., 0. / 100., 0. / 100., 1.))
|
||||||
|
.child(label.into())
|
||||||
|
}
|
||||||
|
}
|
@ -25,6 +25,7 @@ serde.workspace = true
|
|||||||
settings2 = { path = "../settings2" }
|
settings2 = { path = "../settings2" }
|
||||||
simplelog = "0.9"
|
simplelog = "0.9"
|
||||||
smallvec.workspace = true
|
smallvec.workspace = true
|
||||||
|
story = { path = "../story" }
|
||||||
strum = { version = "0.25.0", features = ["derive"] }
|
strum = { version = "0.25.0", features = ["derive"] }
|
||||||
theme = { path = "../theme" }
|
theme = { path = "../theme" }
|
||||||
theme2 = { path = "../theme2" }
|
theme2 = { path = "../theme2" }
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
mod colors;
|
|
||||||
mod focus;
|
mod focus;
|
||||||
mod kitchen_sink;
|
mod kitchen_sink;
|
||||||
mod picker;
|
mod picker;
|
||||||
@ -6,7 +5,6 @@ mod scroll;
|
|||||||
mod text;
|
mod text;
|
||||||
mod z_index;
|
mod z_index;
|
||||||
|
|
||||||
pub use colors::*;
|
|
||||||
pub use focus::*;
|
pub use focus::*;
|
||||||
pub use kitchen_sink::*;
|
pub use kitchen_sink::*;
|
||||||
pub use picker::*;
|
pub use picker::*;
|
||||||
|
@ -1,44 +0,0 @@
|
|||||||
use crate::story::Story;
|
|
||||||
use gpui::{prelude::*, px, Div, Render};
|
|
||||||
use theme2::{default_color_scales, ColorScaleStep};
|
|
||||||
use ui::prelude::*;
|
|
||||||
|
|
||||||
pub struct ColorsStory;
|
|
||||||
|
|
||||||
impl Render for ColorsStory {
|
|
||||||
type Element = Div;
|
|
||||||
|
|
||||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
|
||||||
let color_scales = default_color_scales();
|
|
||||||
|
|
||||||
Story::container(cx)
|
|
||||||
.child(Story::title(cx, "Colors"))
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.id("colors")
|
|
||||||
.flex()
|
|
||||||
.flex_col()
|
|
||||||
.gap_1()
|
|
||||||
.overflow_y_scroll()
|
|
||||||
.text_color(gpui::white())
|
|
||||||
.children(color_scales.into_iter().map(|scale| {
|
|
||||||
div()
|
|
||||||
.flex()
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.w(px(75.))
|
|
||||||
.line_height(px(24.))
|
|
||||||
.child(scale.name().clone()),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.flex()
|
|
||||||
.gap_1()
|
|
||||||
.children(ColorScaleStep::ALL.map(|step| {
|
|
||||||
div().flex().size_6().bg(scale.step(cx, step))
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -26,7 +26,7 @@ impl FocusStory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render<Self> for FocusStory {
|
impl Render for FocusStory {
|
||||||
type Element = Focusable<Stateful<Div>>;
|
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 {
|
||||||
@ -52,10 +52,8 @@ impl Render<Self> for FocusStory {
|
|||||||
.on_blur(cx.listener(|_, _, _| println!("Parent blurred")))
|
.on_blur(cx.listener(|_, _, _| println!("Parent blurred")))
|
||||||
.on_focus_in(cx.listener(|_, _, _| println!("Parent focus_in")))
|
.on_focus_in(cx.listener(|_, _, _| println!("Parent focus_in")))
|
||||||
.on_focus_out(cx.listener(|_, _, _| println!("Parent focus_out")))
|
.on_focus_out(cx.listener(|_, _, _| println!("Parent focus_out")))
|
||||||
.on_key_down(
|
.on_key_down(cx.listener(|_, event, _| println!("Key down on parent {:?}", event)))
|
||||||
cx.listener(|_, event, phase, _| println!("Key down on parent {:?}", event)),
|
.on_key_up(cx.listener(|_, event, _| println!("Key up on parent {:?}", event)))
|
||||||
)
|
|
||||||
.on_key_up(cx.listener(|_, event, phase, _| println!("Key up on parent {:?}", event)))
|
|
||||||
.size_full()
|
.size_full()
|
||||||
.bg(color_1)
|
.bg(color_1)
|
||||||
.focus(|style| style.bg(color_2))
|
.focus(|style| style.bg(color_2))
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
use crate::{story::Story, story_selector::ComponentStory};
|
|
||||||
use gpui::{prelude::*, Div, Render, Stateful, View};
|
use gpui::{prelude::*, Div, Render, Stateful, View};
|
||||||
|
use story::Story;
|
||||||
use strum::IntoEnumIterator;
|
use strum::IntoEnumIterator;
|
||||||
use ui::prelude::*;
|
use ui::prelude::*;
|
||||||
|
|
||||||
|
use crate::story_selector::ComponentStory;
|
||||||
|
|
||||||
pub struct KitchenSinkStory;
|
pub struct KitchenSinkStory;
|
||||||
|
|
||||||
impl KitchenSinkStory {
|
impl KitchenSinkStory {
|
||||||
@ -19,11 +21,11 @@ impl Render for KitchenSinkStory {
|
|||||||
.map(|selector| selector.story(cx))
|
.map(|selector| selector.story(cx))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
Story::container(cx)
|
Story::container()
|
||||||
.id("kitchen-sink")
|
.id("kitchen-sink")
|
||||||
.overflow_y_scroll()
|
.overflow_y_scroll()
|
||||||
.child(Story::title(cx, "Kitchen Sink"))
|
.child(Story::title("Kitchen Sink"))
|
||||||
.child(Story::label(cx, "Components"))
|
.child(Story::label("Components"))
|
||||||
.child(div().flex().flex_col().children(component_stories))
|
.child(div().flex().flex_col().children(component_stories))
|
||||||
// Add a bit of space at the bottom of the kitchen sink so elements
|
// Add a bit of space at the bottom of the kitchen sink so elements
|
||||||
// don't end up squished right up against the bottom of the screen.
|
// don't end up squished right up against the bottom of the screen.
|
||||||
|
@ -36,7 +36,7 @@ impl Delegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl PickerDelegate for Delegate {
|
impl PickerDelegate for Delegate {
|
||||||
type ListItem = Div<Picker<Self>>;
|
type ListItem = Div;
|
||||||
|
|
||||||
fn match_count(&self) -> usize {
|
fn match_count(&self) -> usize {
|
||||||
self.candidates.len()
|
self.candidates.len()
|
||||||
@ -205,8 +205,8 @@ impl PickerStory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render<Self> for PickerStory {
|
impl Render for PickerStory {
|
||||||
type Element = Div<Self>;
|
type Element = Div;
|
||||||
|
|
||||||
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element {
|
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element {
|
||||||
div()
|
div()
|
||||||
|
@ -10,8 +10,8 @@ impl ScrollStory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render<Self> for ScrollStory {
|
impl Render for ScrollStory {
|
||||||
type Element = Stateful<Self, Div<Self>>;
|
type Element = 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 theme = cx.theme();
|
let theme = cx.theme();
|
||||||
@ -38,7 +38,7 @@ impl Render<Self> for ScrollStory {
|
|||||||
};
|
};
|
||||||
div()
|
div()
|
||||||
.id(id)
|
.id(id)
|
||||||
.tooltip(move |_, cx| Tooltip::text(format!("{}, {}", row, column), cx))
|
.tooltip(move |cx| Tooltip::text(format!("{}, {}", row, column), cx))
|
||||||
.bg(bg)
|
.bg(bg)
|
||||||
.size(px(100. as f32))
|
.size(px(100. as f32))
|
||||||
.when(row >= 5 && column >= 5, |d| {
|
.when(row >= 5 && column >= 5, |d| {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
use gpui::{
|
use gpui::{
|
||||||
blue, div, red, white, Div, ParentElement, Render, Styled, View, VisualContext, WindowContext,
|
blue, div, green, red, white, Div, InteractiveText, ParentElement, Render, Styled, StyledText,
|
||||||
|
TextRun, View, VisualContext, WindowContext,
|
||||||
};
|
};
|
||||||
use ui::v_stack;
|
use ui::v_stack;
|
||||||
|
|
||||||
@ -55,6 +56,21 @@ impl Render for TextStory {
|
|||||||
"flex-row. width 96. The quick brown fox jumps over the lazy dog. ",
|
"flex-row. width 96. The quick brown fox jumps over the lazy dog. ",
|
||||||
"Meanwhile, the lazy dog decided it was time for a change. ",
|
"Meanwhile, the lazy dog decided it was time for a change. ",
|
||||||
"He started daily workout routines, ate healthier and became the fastest dog in town.",
|
"He started daily workout routines, ate healthier and became the fastest dog in town.",
|
||||||
)))
|
))).child(
|
||||||
|
InteractiveText::new(
|
||||||
|
"interactive",
|
||||||
|
StyledText::new("Hello world, how is it going?").with_runs(vec![
|
||||||
|
cx.text_style().to_run(6),
|
||||||
|
TextRun {
|
||||||
|
background_color: Some(green()),
|
||||||
|
..cx.text_style().to_run(5)
|
||||||
|
},
|
||||||
|
cx.text_style().to_run(18),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
.on_click(vec![2..4, 1..3, 7..9], |range_ix, cx| {
|
||||||
|
println!("Clicked range {range_ix}");
|
||||||
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user