mirror of
https://github.com/zed-industries/zed.git
synced 2024-12-26 06:22:42 +03:00
Merge branch 'main' into refine-keybindings
This commit is contained in:
commit
f33cd3d463
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
|
||||
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
|
||||
env:
|
||||
RUSTFLAGS: -D warnings
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: cargo check --tests --workspace
|
||||
|
||||
- name: Run tests
|
||||
env:
|
||||
RUSTFLAGS: -D warnings
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: cargo nextest run --workspace --no-fail-fast
|
||||
|
7
.github/workflows/ci.yml
vendored
7
.github/workflows/ci.yml
vendored
@ -29,6 +29,9 @@ jobs:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Set up default .cargo/config.toml
|
||||
run: cp ./.cargo/ci-config.toml ~/.cargo/config.toml
|
||||
|
||||
- name: Run rustfmt
|
||||
uses: ./.github/actions/check_formatting
|
||||
|
||||
@ -87,7 +90,7 @@ jobs:
|
||||
submodules: "recursive"
|
||||
|
||||
- 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
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
@ -131,8 +134,6 @@ jobs:
|
||||
|
||||
- uses: softprops/action-gh-release@v1
|
||||
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' }}
|
||||
with:
|
||||
draft: true
|
||||
|
2
.github/workflows/release_nightly.yml
vendored
2
.github/workflows/release_nightly.yml
vendored
@ -79,7 +79,7 @@ jobs:
|
||||
submodules: "recursive"
|
||||
|
||||
- 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
|
||||
run: |
|
||||
|
82
Cargo.lock
generated
82
Cargo.lock
generated
@ -1186,12 +1186,14 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-broadcast",
|
||||
"async-trait",
|
||||
"audio2",
|
||||
"client2",
|
||||
"collections",
|
||||
"fs2",
|
||||
"futures 0.3.28",
|
||||
"gpui2",
|
||||
"image",
|
||||
"language2",
|
||||
"live_kit_client2",
|
||||
"log",
|
||||
@ -1203,7 +1205,10 @@ dependencies = [
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"settings2",
|
||||
"smallvec",
|
||||
"ui2",
|
||||
"util",
|
||||
"workspace2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1398,7 +1403,7 @@ dependencies = [
|
||||
"smol",
|
||||
"sum_tree",
|
||||
"tempfile",
|
||||
"text",
|
||||
"text2",
|
||||
"thiserror",
|
||||
"time",
|
||||
"tiny_http",
|
||||
@ -1664,7 +1669,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "collab"
|
||||
version = "0.28.0"
|
||||
version = "0.29.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@ -1869,7 +1874,7 @@ dependencies = [
|
||||
"editor2",
|
||||
"feature_flags2",
|
||||
"futures 0.3.28",
|
||||
"fuzzy",
|
||||
"fuzzy2",
|
||||
"gpui2",
|
||||
"language2",
|
||||
"lazy_static",
|
||||
@ -6763,7 +6768,6 @@ dependencies = [
|
||||
"anyhow",
|
||||
"client2",
|
||||
"collections",
|
||||
"context_menu",
|
||||
"db2",
|
||||
"editor2",
|
||||
"futures 0.3.28",
|
||||
@ -8032,6 +8036,35 @@ dependencies = [
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "search2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 1.3.2",
|
||||
"client2",
|
||||
"collections",
|
||||
"editor2",
|
||||
"futures 0.3.28",
|
||||
"gpui2",
|
||||
"language2",
|
||||
"log",
|
||||
"menu2",
|
||||
"postage",
|
||||
"project2",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"settings2",
|
||||
"smallvec",
|
||||
"smol",
|
||||
"theme2",
|
||||
"ui2",
|
||||
"unindent",
|
||||
"util",
|
||||
"workspace2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "2.9.2"
|
||||
@ -8855,6 +8888,13 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "story"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"gpui2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "storybook2"
|
||||
version = "0.1.0"
|
||||
@ -8876,6 +8916,7 @@ dependencies = [
|
||||
"settings2",
|
||||
"simplelog",
|
||||
"smallvec",
|
||||
"story",
|
||||
"strum",
|
||||
"theme",
|
||||
"theme2",
|
||||
@ -8883,17 +8924,6 @@ dependencies = [
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "storybook3"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"gpui2",
|
||||
"settings2",
|
||||
"theme2",
|
||||
"ui2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stringprep"
|
||||
version = "0.1.4"
|
||||
@ -9397,6 +9427,7 @@ dependencies = [
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"settings2",
|
||||
"story",
|
||||
"toml 0.5.11",
|
||||
"util",
|
||||
"uuid 1.4.1",
|
||||
@ -9899,7 +9930,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "tree-sitter"
|
||||
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 = [
|
||||
"cc",
|
||||
"regex",
|
||||
@ -10147,6 +10178,15 @@ dependencies = [
|
||||
"tree-sitter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-uiua"
|
||||
version = "0.3.3"
|
||||
source = "git+https://github.com/shnarazk/tree-sitter-uiua?rev=9260f11be5900beda4ee6d1a24ab8ddfaf5a19b2#9260f11be5900beda4ee6d1a24ab8ddfaf5a19b2"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-vue"
|
||||
version = "0.0.1"
|
||||
@ -10240,6 +10280,7 @@ dependencies = [
|
||||
"serde",
|
||||
"settings2",
|
||||
"smallvec",
|
||||
"story",
|
||||
"strum",
|
||||
"theme2",
|
||||
]
|
||||
@ -11354,6 +11395,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion 1.0.5",
|
||||
"async-trait",
|
||||
"bincode",
|
||||
"call2",
|
||||
"client2",
|
||||
@ -11466,7 +11508,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.114.0"
|
||||
version = "0.115.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"ai",
|
||||
@ -11583,6 +11625,7 @@ dependencies = [
|
||||
"tree-sitter-svelte",
|
||||
"tree-sitter-toml",
|
||||
"tree-sitter-typescript",
|
||||
"tree-sitter-uiua",
|
||||
"tree-sitter-vue",
|
||||
"tree-sitter-yaml",
|
||||
"unindent",
|
||||
@ -11614,9 +11657,11 @@ dependencies = [
|
||||
"async-recursion 0.3.2",
|
||||
"async-tar",
|
||||
"async-trait",
|
||||
"audio2",
|
||||
"auto_update2",
|
||||
"backtrace",
|
||||
"call2",
|
||||
"channel2",
|
||||
"chrono",
|
||||
"cli",
|
||||
"client2",
|
||||
@ -11634,7 +11679,6 @@ dependencies = [
|
||||
"fs2",
|
||||
"fsevent",
|
||||
"futures 0.3.28",
|
||||
"fuzzy",
|
||||
"go_to_line2",
|
||||
"gpui2",
|
||||
"ignore",
|
||||
@ -11662,6 +11706,7 @@ dependencies = [
|
||||
"rsa 0.4.0",
|
||||
"rust-embed",
|
||||
"schemars",
|
||||
"search2",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
@ -11704,6 +11749,7 @@ dependencies = [
|
||||
"tree-sitter-svelte",
|
||||
"tree-sitter-toml",
|
||||
"tree-sitter-typescript",
|
||||
"tree-sitter-uiua",
|
||||
"tree-sitter-vue",
|
||||
"tree-sitter-yaml",
|
||||
"unindent",
|
||||
|
10
Cargo.toml
10
Cargo.toml
@ -90,6 +90,7 @@ members = [
|
||||
"crates/rpc",
|
||||
"crates/rpc2",
|
||||
"crates/search",
|
||||
"crates/search2",
|
||||
"crates/settings",
|
||||
"crates/settings2",
|
||||
"crates/snippet",
|
||||
@ -97,7 +98,6 @@ members = [
|
||||
"crates/sqlez_macros",
|
||||
"crates/rich_text",
|
||||
"crates/storybook2",
|
||||
"crates/storybook3",
|
||||
"crates/sum_tree",
|
||||
"crates/terminal",
|
||||
"crates/terminal2",
|
||||
@ -110,6 +110,7 @@ members = [
|
||||
"crates/ui2",
|
||||
"crates/util",
|
||||
"crates/semantic_index",
|
||||
"crates/story",
|
||||
"crates/vim",
|
||||
"crates/vcs_menu",
|
||||
"crates/workspace2",
|
||||
@ -194,8 +195,10 @@ tree-sitter-lua = "0.0.14"
|
||||
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-vue = {git = "https://github.com/zed-industries/tree-sitter-vue", rev = "9b6cb221ccb8d0b956fcb17e9a1efac2feefeb58"}
|
||||
tree-sitter-uiua = {git = "https://github.com/shnarazk/tree-sitter-uiua", rev = "9260f11be5900beda4ee6d1a24ab8ddfaf5a19b2"}
|
||||
|
||||
[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" }
|
||||
|
||||
# TODO - Remove when a version is released with this PR: https://github.com/servo/core-foundation-rs/pull/457
|
||||
@ -207,11 +210,12 @@ core-graphics = { git = "https://github.com/servo/core-foundation-rs", rev = "07
|
||||
|
||||
[profile.dev]
|
||||
split-debuginfo = "unpacked"
|
||||
debug = "limited"
|
||||
|
||||
[profile.dev.package.taffy]
|
||||
opt-level = 3
|
||||
|
||||
[profile.release]
|
||||
debug = true
|
||||
debug = "limited"
|
||||
lto = "thin"
|
||||
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
|
||||
},
|
||||
// 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
|
||||
// which gives the same size as all other panes.
|
||||
"active_pane_magnification": 1.0,
|
||||
|
@ -7,5 +7,6 @@
|
||||
// custom settings, run the `open default settings` command
|
||||
// from the command palette or from `Zed` application menu.
|
||||
{
|
||||
"buffer_font_size": 15
|
||||
"ui_font_size": 16,
|
||||
"buffer_font_size": 16
|
||||
}
|
||||
|
@ -84,8 +84,8 @@ impl Settings for AutoUpdateSetting {
|
||||
pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut AppContext) {
|
||||
AutoUpdateSetting::register(cx);
|
||||
|
||||
cx.observe_new_views(|wokrspace: &mut Workspace, _cx| {
|
||||
wokrspace
|
||||
cx.observe_new_views(|workspace: &mut Workspace, _cx| {
|
||||
workspace
|
||||
.register_action(|_, action: &Check, cx| check(action, cx))
|
||||
.register_action(|_, _action: &CheckThatAutoUpdaterWorks, cx| {
|
||||
let prompt = cx.prompt(gpui::PromptLevel::Info, "It does!", &["Ok"]);
|
||||
@ -94,6 +94,11 @@ pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut AppCo
|
||||
})
|
||||
.detach();
|
||||
});
|
||||
|
||||
// @nate - code to trigger update notification on launch
|
||||
// workspace.show_notification(0, _cx, |cx| {
|
||||
// cx.build_view(|_| UpdateNotification::new(SemanticVersion::from_str("1.1.1").unwrap()))
|
||||
// });
|
||||
})
|
||||
.detach();
|
||||
|
||||
@ -131,7 +136,7 @@ pub fn check(_: &Check, cx: &mut AppContext) {
|
||||
}
|
||||
}
|
||||
|
||||
fn _view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) {
|
||||
pub fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) {
|
||||
if let Some(auto_updater) = AutoUpdater::get(cx) {
|
||||
let auto_updater = auto_updater.read(cx);
|
||||
let server_url = &auto_updater.server_url;
|
||||
|
@ -1,87 +1,56 @@
|
||||
use gpui::{div, Div, EventEmitter, ParentComponent, Render, SemanticVersion, ViewContext};
|
||||
use menu::Cancel;
|
||||
use workspace::notifications::NotificationEvent;
|
||||
use gpui::{
|
||||
div, DismissEvent, Div, EventEmitter, InteractiveElement, ParentElement, Render,
|
||||
SemanticVersion, StatefulInteractiveElement, Styled, ViewContext,
|
||||
};
|
||||
use util::channel::ReleaseChannel;
|
||||
use workspace::ui::{h_stack, v_stack, Icon, IconElement, Label, StyledExt};
|
||||
|
||||
pub struct UpdateNotification {
|
||||
_version: SemanticVersion,
|
||||
version: SemanticVersion,
|
||||
}
|
||||
|
||||
impl EventEmitter<NotificationEvent> for UpdateNotification {}
|
||||
impl EventEmitter<DismissEvent> for UpdateNotification {}
|
||||
|
||||
impl Render for UpdateNotification {
|
||||
type Element = Div<Self>;
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, _cx: &mut gpui::ViewContext<Self>) -> Self::Element {
|
||||
div().child("Updated zed!")
|
||||
// let theme = theme::current(cx).clone();
|
||||
// let theme = &theme.update_notification;
|
||||
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element {
|
||||
let app_name = cx.global::<ReleaseChannel>().display_name();
|
||||
|
||||
// let app_name = cx.global::<ReleaseChannel>().display_name();
|
||||
|
||||
// MouseEventHandler::new::<ViewReleaseNotes, _>(0, cx, |state, cx| {
|
||||
// Flex::column()
|
||||
// .with_child(
|
||||
// Flex::row()
|
||||
// .with_child(
|
||||
// Text::new(
|
||||
// format!("Updated to {app_name} {}", self.version),
|
||||
// theme.message.text.clone(),
|
||||
// )
|
||||
// .contained()
|
||||
// .with_style(theme.message.container)
|
||||
// .aligned()
|
||||
// .top()
|
||||
// .left()
|
||||
// .flex(1., true),
|
||||
// )
|
||||
// .with_child(
|
||||
// MouseEventHandler::new::<Cancel, _>(0, cx, |state, _| {
|
||||
// let style = theme.dismiss_button.style_for(state);
|
||||
// Svg::new("icons/x.svg")
|
||||
// .with_color(style.color)
|
||||
// .constrained()
|
||||
// .with_width(style.icon_width)
|
||||
// .aligned()
|
||||
// .contained()
|
||||
// .with_style(style.container)
|
||||
// .constrained()
|
||||
// .with_width(style.button_width)
|
||||
// .with_height(style.button_width)
|
||||
// })
|
||||
// .with_padding(Padding::uniform(5.))
|
||||
// .on_click(MouseButton::Left, move |_, this, cx| {
|
||||
// this.dismiss(&Default::default(), cx)
|
||||
// })
|
||||
// .aligned()
|
||||
// .constrained()
|
||||
// .with_height(cx.font_cache().line_height(theme.message.text.font_size))
|
||||
// .aligned()
|
||||
// .top()
|
||||
// .flex_float(),
|
||||
// ),
|
||||
// )
|
||||
// .with_child({
|
||||
// let style = theme.action_message.style_for(state);
|
||||
// Text::new("View the release notes", style.text.clone())
|
||||
// .contained()
|
||||
// .with_style(style.container)
|
||||
// })
|
||||
// .contained()
|
||||
// })
|
||||
// .with_cursor_style(CursorStyle::PointingHand)
|
||||
// .on_click(MouseButton::Left, |_, _, cx| {
|
||||
// crate::view_release_notes(&Default::default(), cx)
|
||||
// })
|
||||
// .into_any_named("update notification")
|
||||
v_stack()
|
||||
.elevation_3(cx)
|
||||
.p_4()
|
||||
.child(
|
||||
h_stack()
|
||||
.justify_between()
|
||||
.child(Label::new(format!(
|
||||
"Updated to {app_name} {}",
|
||||
self.version
|
||||
)))
|
||||
.child(
|
||||
div()
|
||||
.id("cancel")
|
||||
.child(IconElement::new(Icon::Close))
|
||||
.cursor_pointer()
|
||||
.on_click(cx.listener(|this, _, cx| this.dismiss(cx))),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("notes")
|
||||
.child(Label::new("View the release notes"))
|
||||
.cursor_pointer()
|
||||
.on_click(|_, cx| crate::view_release_notes(&Default::default(), cx)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl UpdateNotification {
|
||||
pub fn new(version: SemanticVersion) -> Self {
|
||||
Self { _version: version }
|
||||
Self { version }
|
||||
}
|
||||
|
||||
pub fn _dismiss(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(NotificationEvent::Dismiss);
|
||||
pub fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(DismissEvent::Dismiss);
|
||||
}
|
||||
}
|
||||
|
@ -31,15 +31,19 @@ media = { path = "../media" }
|
||||
project = { package = "project2", path = "../project2" }
|
||||
settings = { package = "settings2", path = "../settings2" }
|
||||
util = { path = "../util" }
|
||||
|
||||
ui = {package = "ui2", path = "../ui2"}
|
||||
workspace = {package = "workspace2", path = "../workspace2"}
|
||||
async-trait.workspace = true
|
||||
anyhow.workspace = true
|
||||
async-broadcast = "0.4"
|
||||
futures.workspace = true
|
||||
image = "0.23"
|
||||
postage.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde_derive.workspace = true
|
||||
smallvec.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
client = { package = "client2", path = "../client2", features = ["test-support"] }
|
||||
|
@ -1,25 +1,32 @@
|
||||
pub mod call_settings;
|
||||
pub mod participant;
|
||||
pub mod room;
|
||||
mod shared_screen;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_trait::async_trait;
|
||||
use audio::Audio;
|
||||
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 futures::{channel::oneshot, future::Shared, Future, FutureExt};
|
||||
use gpui::{
|
||||
AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Subscription, Task,
|
||||
WeakModel,
|
||||
View, ViewContext, VisualContext, WeakModel, WeakView,
|
||||
};
|
||||
pub use participant::ParticipantLocation;
|
||||
use postage::watch;
|
||||
use project::Project;
|
||||
use room::Event;
|
||||
use settings::Settings;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub use participant::ParticipantLocation;
|
||||
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) {
|
||||
CallSettings::register(cx);
|
||||
@ -464,7 +471,7 @@ impl ActiveCall {
|
||||
&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() {
|
||||
let room = room.read(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,
|
||||
channel_id: Option<u64>,
|
||||
client: &Arc<Client>,
|
||||
cx: &AppContext,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let telemetry = client.telemetry();
|
||||
let telemetry_settings = *TelemetrySettings::get_global(cx);
|
||||
@ -505,6 +512,208 @@ 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| {
|
||||
let room = room.clone();
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
room.update(&mut cx, |this, cx| this.toggle_mute(cx))??
|
||||
.await
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
})
|
||||
});
|
||||
}
|
||||
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)]
|
||||
mod test {
|
||||
use gpui::TestAppContext;
|
||||
|
@ -4,7 +4,7 @@ use client::{proto, User};
|
||||
use collections::HashMap;
|
||||
use gpui::WeakModel;
|
||||
pub use live_kit_client::Frame;
|
||||
use live_kit_client::{RemoteAudioTrack, RemoteVideoTrack};
|
||||
pub(crate) use live_kit_client::{RemoteAudioTrack, RemoteVideoTrack};
|
||||
use project::Project;
|
||||
use std::sync::Arc;
|
||||
|
||||
|
@ -21,7 +21,7 @@ use live_kit_client::{
|
||||
};
|
||||
use postage::{sink::Sink, stream::Stream, watch};
|
||||
use project::Project;
|
||||
use settings::Settings;
|
||||
use settings::Settings as _;
|
||||
use std::{future::Future, mem, sync::Arc, time::Duration};
|
||||
use util::{post_inc, ResultExt, TryFutureExt};
|
||||
|
||||
@ -1267,7 +1267,6 @@ impl Room {
|
||||
.ok_or_else(|| anyhow!("live-kit was not initialized"))?
|
||||
.await
|
||||
};
|
||||
|
||||
let publication = publish_track.await;
|
||||
this.upgrade()
|
||||
.ok_or_else(|| anyhow!("room was dropped"))?
|
||||
|
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)))
|
||||
}
|
||||
}
|
@ -18,7 +18,7 @@ db = { package = "db2", path = "../db2" }
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
util = { path = "../util" }
|
||||
rpc = { package = "rpc2", path = "../rpc2" }
|
||||
text = { path = "../text" }
|
||||
text = { package = "text2", path = "../text2" }
|
||||
language = { package = "language2", path = "../language2" }
|
||||
settings = { package = "settings2", path = "../settings2" }
|
||||
feature_flags = { package = "feature_flags2", path = "../feature_flags2" }
|
||||
|
@ -109,6 +109,10 @@ pub enum ClickhouseEvent {
|
||||
virtual_memory_in_bytes: u64,
|
||||
milliseconds_since_first_event: i64,
|
||||
},
|
||||
App {
|
||||
operation: &'static str,
|
||||
milliseconds_since_first_event: i64,
|
||||
},
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
@ -168,13 +172,8 @@ impl Telemetry {
|
||||
let mut state = self.state.lock();
|
||||
state.installation_id = installation_id.map(|id| id.into());
|
||||
state.session_id = Some(session_id.into());
|
||||
let has_clickhouse_events = !state.clickhouse_events_queue.is_empty();
|
||||
drop(state);
|
||||
|
||||
if has_clickhouse_events {
|
||||
self.flush_clickhouse_events();
|
||||
}
|
||||
|
||||
let this = self.clone();
|
||||
cx.spawn(|mut cx| async move {
|
||||
// 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(),
|
||||
};
|
||||
|
||||
self.report_clickhouse_event(event, telemetry_settings)
|
||||
self.report_clickhouse_event(event, telemetry_settings, false)
|
||||
}
|
||||
|
||||
pub fn report_copilot_event(
|
||||
@ -273,7 +272,7 @@ impl Telemetry {
|
||||
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(
|
||||
@ -290,7 +289,7 @@ impl Telemetry {
|
||||
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(
|
||||
@ -307,7 +306,7 @@ impl Telemetry {
|
||||
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(
|
||||
@ -322,7 +321,7 @@ impl Telemetry {
|
||||
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(
|
||||
@ -337,7 +336,21 @@ impl Telemetry {
|
||||
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 {
|
||||
@ -358,6 +371,7 @@ impl Telemetry {
|
||||
self: &Arc<Self>,
|
||||
event: ClickhouseEvent,
|
||||
telemetry_settings: TelemetrySettings,
|
||||
immediate_flush: bool,
|
||||
) {
|
||||
if !telemetry_settings.metrics {
|
||||
return;
|
||||
@ -370,7 +384,7 @@ impl Telemetry {
|
||||
.push(ClickhouseEventWrapper { signed_in, event });
|
||||
|
||||
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);
|
||||
self.flush_clickhouse_events();
|
||||
} else {
|
||||
|
@ -382,7 +382,7 @@ impl settings::Settings for TelemetrySettings {
|
||||
}
|
||||
|
||||
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 {
|
||||
id: AtomicU64::new(0),
|
||||
peer: Peer::new(0),
|
||||
@ -551,7 +551,6 @@ impl Client {
|
||||
F: 'static + Future<Output = Result<()>>,
|
||||
{
|
||||
let message_type_id = TypeId::of::<M>();
|
||||
|
||||
let mut state = self.state.write();
|
||||
state
|
||||
.models_by_message_type
|
||||
|
@ -1,5 +1,6 @@
|
||||
use crate::{TelemetrySettings, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
|
||||
use chrono::{DateTime, Utc};
|
||||
use futures::Future;
|
||||
use gpui::{serde_json, AppContext, AppMetadata, BackgroundExecutor, Task};
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::Mutex;
|
||||
@ -107,6 +108,10 @@ pub enum ClickhouseEvent {
|
||||
virtual_memory_in_bytes: u64,
|
||||
milliseconds_since_first_event: i64,
|
||||
},
|
||||
App {
|
||||
operation: &'static str,
|
||||
milliseconds_since_first_event: i64,
|
||||
},
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
@ -122,12 +127,13 @@ const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(1);
|
||||
const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(30);
|
||||
|
||||
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>() {
|
||||
Some(cx.global::<ReleaseChannel>().display_name())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// TODO: Replace all hardware stuff with nested SystemSpecs json
|
||||
let this = Arc::new(Self {
|
||||
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
|
||||
}
|
||||
|
||||
#[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> {
|
||||
Some(self.state.lock().log_file.as_ref()?.path().to_path_buf())
|
||||
}
|
||||
@ -163,13 +190,8 @@ impl Telemetry {
|
||||
let mut state = self.state.lock();
|
||||
state.installation_id = installation_id.map(|id| id.into());
|
||||
state.session_id = Some(session_id.into());
|
||||
let has_clickhouse_events = !state.clickhouse_events_queue.is_empty();
|
||||
drop(state);
|
||||
|
||||
if has_clickhouse_events {
|
||||
self.flush_clickhouse_events();
|
||||
}
|
||||
|
||||
let this = self.clone();
|
||||
cx.spawn(|cx| async move {
|
||||
// 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(),
|
||||
};
|
||||
|
||||
self.report_clickhouse_event(event, telemetry_settings)
|
||||
self.report_clickhouse_event(event, telemetry_settings, false)
|
||||
}
|
||||
|
||||
pub fn report_copilot_event(
|
||||
@ -274,7 +296,7 @@ impl Telemetry {
|
||||
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(
|
||||
@ -291,7 +313,7 @@ impl Telemetry {
|
||||
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(
|
||||
@ -308,7 +330,7 @@ impl Telemetry {
|
||||
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(
|
||||
@ -323,7 +345,7 @@ impl Telemetry {
|
||||
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(
|
||||
@ -338,7 +360,21 @@ impl Telemetry {
|
||||
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 {
|
||||
@ -359,6 +395,7 @@ impl Telemetry {
|
||||
self: &Arc<Self>,
|
||||
event: ClickhouseEvent,
|
||||
telemetry_settings: TelemetrySettings,
|
||||
immediate_flush: bool,
|
||||
) {
|
||||
if !telemetry_settings.metrics {
|
||||
return;
|
||||
@ -371,7 +408,7 @@ impl Telemetry {
|
||||
.push(ClickhouseEventWrapper { signed_in, event });
|
||||
|
||||
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);
|
||||
self.flush_clickhouse_events();
|
||||
} else {
|
||||
|
@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
|
||||
default-run = "collab"
|
||||
edition = "2021"
|
||||
name = "collab"
|
||||
version = "0.28.0"
|
||||
version = "0.29.0"
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
|
@ -10,7 +10,7 @@ publish = false
|
||||
name = "collab2"
|
||||
|
||||
[[bin]]
|
||||
name = "seed"
|
||||
name = "seed2"
|
||||
required-features = ["seed-support"]
|
||||
|
||||
[dependencies]
|
||||
|
@ -149,7 +149,7 @@ impl TestServer {
|
||||
.user_id
|
||||
};
|
||||
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 db = self.app_state.db.clone();
|
||||
let connection_killers = self.connection_killers.clone();
|
||||
@ -221,6 +221,7 @@ impl TestServer {
|
||||
fs: fs.clone(),
|
||||
build_window_options: |_, _, _| Default::default(),
|
||||
node_runtime: FakeNodeRuntime::new(),
|
||||
call_factory: |_, _| Box::new(workspace::TestCallHandler),
|
||||
});
|
||||
|
||||
cx.update(|cx| {
|
||||
|
@ -33,7 +33,7 @@ collections = { path = "../collections" }
|
||||
# drag_and_drop = { path = "../drag_and_drop" }
|
||||
editor = { package="editor2", path = "../editor2" }
|
||||
#feedback = { path = "../feedback" }
|
||||
fuzzy = { path = "../fuzzy" }
|
||||
fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
language = { package = "language2", path = "../language2" }
|
||||
menu = { package = "menu2", path = "../menu2" }
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,37 +1,34 @@
|
||||
use client::{ContactRequestStatus, User, UserStore};
|
||||
use gpui::{
|
||||
elements::*, AppContext, Entity, ModelHandle, MouseState, Task, View, ViewContext, ViewHandle,
|
||||
div, img, svg, AnyElement, AppContext, DismissEvent, Div, Entity, EventEmitter, FocusHandle,
|
||||
FocusableView, Img, IntoElement, Model, ParentElement as _, Render, Styled, Task, View,
|
||||
ViewContext, VisualContext, WeakView,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate, PickerEvent};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use std::sync::Arc;
|
||||
use util::TryFutureExt;
|
||||
use workspace::Modal;
|
||||
use theme::ActiveTheme as _;
|
||||
use ui::{h_stack, v_stack, Label};
|
||||
use util::{ResultExt as _, TryFutureExt};
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
Picker::<ContactFinderDelegate>::init(cx);
|
||||
cx.add_action(ContactFinder::dismiss)
|
||||
//Picker::<ContactFinderDelegate>::init(cx);
|
||||
//cx.add_action(ContactFinder::dismiss)
|
||||
}
|
||||
|
||||
pub struct ContactFinder {
|
||||
picker: ViewHandle<Picker<ContactFinderDelegate>>,
|
||||
picker: View<Picker<ContactFinderDelegate>>,
|
||||
has_focus: bool,
|
||||
}
|
||||
|
||||
impl ContactFinder {
|
||||
pub fn new(user_store: ModelHandle<UserStore>, cx: &mut ViewContext<Self>) -> Self {
|
||||
let picker = cx.add_view(|cx| {
|
||||
Picker::new(
|
||||
ContactFinderDelegate {
|
||||
user_store,
|
||||
potential_contacts: Arc::from([]),
|
||||
selected_index: 0,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.with_theme(|theme| theme.collab_panel.tabbed_modal.picker.clone())
|
||||
});
|
||||
|
||||
cx.subscribe(&picker, |_, _, e, cx| cx.emit(*e)).detach();
|
||||
pub fn new(user_store: Model<UserStore>, cx: &mut ViewContext<Self>) -> Self {
|
||||
let delegate = ContactFinderDelegate {
|
||||
parent: cx.view().downgrade(),
|
||||
user_store,
|
||||
potential_contacts: Arc::from([]),
|
||||
selected_index: 0,
|
||||
};
|
||||
let picker = cx.build_view(|cx| Picker::new(delegate, cx));
|
||||
|
||||
Self {
|
||||
picker,
|
||||
@ -41,105 +38,72 @@ impl ContactFinder {
|
||||
|
||||
pub fn set_query(&mut self, query: String, cx: &mut ViewContext<Self>) {
|
||||
self.picker.update(cx, |picker, cx| {
|
||||
picker.set_query(query, cx);
|
||||
// todo!()
|
||||
// picker.set_query(query, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(PickerEvent::Dismiss);
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for ContactFinder {
|
||||
type Event = PickerEvent;
|
||||
}
|
||||
|
||||
impl View for ContactFinder {
|
||||
fn ui_name() -> &'static str {
|
||||
"ContactFinder"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
let full_theme = &theme::current(cx);
|
||||
let theme = &full_theme.collab_panel.tabbed_modal;
|
||||
|
||||
fn render_mode_button(
|
||||
text: &'static str,
|
||||
theme: &theme::TabbedModal,
|
||||
_cx: &mut ViewContext<ContactFinder>,
|
||||
) -> AnyElement<ContactFinder> {
|
||||
let contained_text = &theme.tab_button.active_state().default;
|
||||
Label::new(text, contained_text.text.clone())
|
||||
.contained()
|
||||
.with_style(contained_text.container.clone())
|
||||
.into_any()
|
||||
impl Render for ContactFinder {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
fn render_mode_button(text: &'static str) -> AnyElement {
|
||||
Label::new(text).into_any_element()
|
||||
}
|
||||
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Label::new("Contacts", theme.title.text.clone())
|
||||
.contained()
|
||||
.with_style(theme.title.container.clone()),
|
||||
)
|
||||
.with_child(Flex::row().with_children([render_mode_button(
|
||||
"Invite new contacts",
|
||||
&theme,
|
||||
cx,
|
||||
)]))
|
||||
.expanded()
|
||||
.contained()
|
||||
.with_style(theme.header),
|
||||
v_stack()
|
||||
.child(
|
||||
v_stack()
|
||||
.child(Label::new("Contacts"))
|
||||
.child(h_stack().children([render_mode_button("Invite new contacts")]))
|
||||
.bg(cx.theme().colors().element_background),
|
||||
)
|
||||
.with_child(
|
||||
ChildView::new(&self.picker, cx)
|
||||
.contained()
|
||||
.with_style(theme.body),
|
||||
)
|
||||
.constrained()
|
||||
.with_max_height(theme.max_height)
|
||||
.with_max_width(theme.max_width)
|
||||
.contained()
|
||||
.with_style(theme.modal)
|
||||
.into_any()
|
||||
.child(self.picker.clone())
|
||||
.w_96()
|
||||
}
|
||||
|
||||
fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
self.has_focus = true;
|
||||
if cx.is_self_focused() {
|
||||
cx.focus(&self.picker)
|
||||
}
|
||||
}
|
||||
// fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
// self.has_focus = true;
|
||||
// if cx.is_self_focused() {
|
||||
// cx.focus(&self.picker)
|
||||
// }
|
||||
// }
|
||||
|
||||
fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
|
||||
self.has_focus = false;
|
||||
}
|
||||
// fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
|
||||
// self.has_focus = false;
|
||||
// }
|
||||
|
||||
type Element = Div;
|
||||
}
|
||||
|
||||
impl Modal for ContactFinder {
|
||||
fn has_focus(&self) -> bool {
|
||||
self.has_focus
|
||||
}
|
||||
// impl Modal for ContactFinder {
|
||||
// fn has_focus(&self) -> bool {
|
||||
// self.has_focus
|
||||
// }
|
||||
|
||||
fn dismiss_on_event(event: &Self::Event) -> bool {
|
||||
match event {
|
||||
PickerEvent::Dismiss => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
// fn dismiss_on_event(event: &Self::Event) -> bool {
|
||||
// match event {
|
||||
// PickerEvent::Dismiss => true,
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
pub struct ContactFinderDelegate {
|
||||
parent: WeakView<ContactFinder>,
|
||||
potential_contacts: Arc<[Arc<User>]>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
user_store: Model<UserStore>,
|
||||
selected_index: usize,
|
||||
}
|
||||
|
||||
impl PickerDelegate for ContactFinderDelegate {
|
||||
fn placeholder_text(&self) -> Arc<str> {
|
||||
"Search collaborator by username...".into()
|
||||
}
|
||||
impl EventEmitter<DismissEvent> for ContactFinder {}
|
||||
|
||||
impl FocusableView for ContactFinder {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
self.picker.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for ContactFinderDelegate {
|
||||
type ListItem = Div;
|
||||
fn match_count(&self) -> usize {
|
||||
self.potential_contacts.len()
|
||||
}
|
||||
@ -152,6 +116,10 @@ impl PickerDelegate for ContactFinderDelegate {
|
||||
self.selected_index = ix;
|
||||
}
|
||||
|
||||
fn placeholder_text(&self) -> Arc<str> {
|
||||
"Search collaborator by username...".into()
|
||||
}
|
||||
|
||||
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
|
||||
let search_users = self
|
||||
.user_store
|
||||
@ -161,7 +129,7 @@ impl PickerDelegate for ContactFinderDelegate {
|
||||
async {
|
||||
let potential_contacts = search_users.await?;
|
||||
picker.update(&mut cx, |picker, cx| {
|
||||
picker.delegate_mut().potential_contacts = potential_contacts.into();
|
||||
picker.delegate.potential_contacts = potential_contacts.into();
|
||||
cx.notify();
|
||||
})?;
|
||||
anyhow::Ok(())
|
||||
@ -191,19 +159,18 @@ impl PickerDelegate for ContactFinderDelegate {
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||
cx.emit(PickerEvent::Dismiss);
|
||||
//cx.emit(PickerEvent::Dismiss);
|
||||
self.parent
|
||||
.update(cx, |_, cx| cx.emit(DismissEvent::Dismiss))
|
||||
.log_err();
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
mouse_state: &mut MouseState,
|
||||
selected: bool,
|
||||
cx: &gpui::AppContext,
|
||||
) -> AnyElement<Picker<Self>> {
|
||||
let full_theme = &theme::current(cx);
|
||||
let theme = &full_theme.collab_panel.contact_finder;
|
||||
let tabbed_modal = &full_theme.collab_panel.tabbed_modal;
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let user = &self.potential_contacts[ix];
|
||||
let request_status = self.user_store.read(cx).contact_request_status(user);
|
||||
|
||||
@ -214,48 +181,47 @@ impl PickerDelegate for ContactFinderDelegate {
|
||||
ContactRequestStatus::RequestSent => Some("icons/x.svg"),
|
||||
ContactRequestStatus::RequestAccepted => None,
|
||||
};
|
||||
let button_style = if self.user_store.read(cx).is_contact_request_pending(user) {
|
||||
&theme.disabled_contact_button
|
||||
} else {
|
||||
&theme.contact_button
|
||||
};
|
||||
let style = tabbed_modal
|
||||
.picker
|
||||
.item
|
||||
.in_state(selected)
|
||||
.style_for(mouse_state);
|
||||
Flex::row()
|
||||
.with_children(user.avatar.clone().map(|avatar| {
|
||||
Image::from_data(avatar)
|
||||
.with_style(theme.contact_avatar)
|
||||
.aligned()
|
||||
.left()
|
||||
}))
|
||||
.with_child(
|
||||
Label::new(user.github_login.clone(), style.label.clone())
|
||||
.contained()
|
||||
.with_style(theme.contact_username)
|
||||
.aligned()
|
||||
.left(),
|
||||
)
|
||||
.with_children(icon_path.map(|icon_path| {
|
||||
Svg::new(icon_path)
|
||||
.with_color(button_style.color)
|
||||
.constrained()
|
||||
.with_width(button_style.icon_width)
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(button_style.container)
|
||||
.constrained()
|
||||
.with_width(button_style.button_width)
|
||||
.with_height(button_style.button_width)
|
||||
.aligned()
|
||||
.flex_float()
|
||||
}))
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.constrained()
|
||||
.with_height(tabbed_modal.row_height)
|
||||
.into_any()
|
||||
dbg!(icon_path);
|
||||
Some(
|
||||
div()
|
||||
.flex_1()
|
||||
.justify_between()
|
||||
.children(user.avatar.clone().map(|avatar| img().data(avatar)))
|
||||
.child(Label::new(user.github_login.clone()))
|
||||
.children(icon_path.map(|icon_path| svg().path(icon_path))),
|
||||
)
|
||||
// Flex::row()
|
||||
// .with_children(user.avatar.clone().map(|avatar| {
|
||||
// Image::from_data(avatar)
|
||||
// .with_style(theme.contact_avatar)
|
||||
// .aligned()
|
||||
// .left()
|
||||
// }))
|
||||
// .with_child(
|
||||
// Label::new(user.github_login.clone(), style.label.clone())
|
||||
// .contained()
|
||||
// .with_style(theme.contact_username)
|
||||
// .aligned()
|
||||
// .left(),
|
||||
// )
|
||||
// .with_children(icon_path.map(|icon_path| {
|
||||
// Svg::new(icon_path)
|
||||
// .with_color(button_style.color)
|
||||
// .constrained()
|
||||
// .with_width(button_style.icon_width)
|
||||
// .aligned()
|
||||
// .contained()
|
||||
// .with_style(button_style.container)
|
||||
// .constrained()
|
||||
// .with_width(button_style.button_width)
|
||||
// .with_height(button_style.button_width)
|
||||
// .aligned()
|
||||
// .flex_float()
|
||||
// }))
|
||||
// .contained()
|
||||
// .with_style(style.container)
|
||||
// .constrained()
|
||||
// .with_height(tabbed_modal.row_height)
|
||||
// .into_any()
|
||||
}
|
||||
}
|
||||
|
@ -31,14 +31,17 @@ use std::sync::Arc;
|
||||
use call::ActiveCall;
|
||||
use client::{Client, UserStore};
|
||||
use gpui::{
|
||||
div, px, rems, AppContext, Component, Div, InteractiveComponent, Model, ParentComponent,
|
||||
Render, Stateful, StatefulInteractiveComponent, Styled, Subscription, ViewContext,
|
||||
VisualContext, WeakView, WindowBounds,
|
||||
div, px, rems, AppContext, Div, Element, InteractiveElement, IntoElement, Model, MouseButton,
|
||||
ParentElement, Render, RenderOnce, Stateful, StatefulInteractiveElement, Styled, Subscription,
|
||||
ViewContext, VisualContext, WeakView, WindowBounds,
|
||||
};
|
||||
use project::Project;
|
||||
use theme::ActiveTheme;
|
||||
use ui::{h_stack, Button, ButtonVariant, KeyBinding, Label, TextColor, Tooltip};
|
||||
use workspace::Workspace;
|
||||
use ui::{h_stack, Avatar, Button, ButtonVariant, Color, IconButton, KeyBinding, Tooltip};
|
||||
use util::ResultExt;
|
||||
use workspace::{notifications::NotifyResultExt, Workspace};
|
||||
|
||||
use crate::face_pile::FacePile;
|
||||
|
||||
// const MAX_PROJECT_NAME_LENGTH: usize = 40;
|
||||
// const MAX_BRANCH_NAME_LENGTH: usize = 40;
|
||||
@ -82,9 +85,44 @@ pub struct CollabTitlebarItem {
|
||||
}
|
||||
|
||||
impl Render for CollabTitlebarItem {
|
||||
type Element = Stateful<Self, Div<Self>>;
|
||||
type Element = Stateful<Div>;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
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()
|
||||
.id("titlebar")
|
||||
.justify_between()
|
||||
@ -100,7 +138,7 @@ impl Render for CollabTitlebarItem {
|
||||
|s| s.pl(px(68.)),
|
||||
)
|
||||
.bg(cx.theme().colors().title_bar_background)
|
||||
.on_click(|_, event, cx| {
|
||||
.on_click(|event, cx| {
|
||||
if event.up.click_count == 2 {
|
||||
cx.zoom_window();
|
||||
}
|
||||
@ -111,31 +149,37 @@ impl Render for CollabTitlebarItem {
|
||||
// TODO - Add player menu
|
||||
.child(
|
||||
div()
|
||||
.border()
|
||||
.border_color(gpui::red())
|
||||
.id("project_owner_indicator")
|
||||
.child(
|
||||
Button::new("player")
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.color(Some(TextColor::Player(0))),
|
||||
.color(Some(Color::Player(0))),
|
||||
)
|
||||
.tooltip(move |_, cx| Tooltip::text("Toggle following", cx)),
|
||||
.tooltip(move |cx| Tooltip::text("Toggle following", cx)),
|
||||
)
|
||||
// TODO - Add project menu
|
||||
.child(
|
||||
div()
|
||||
.border()
|
||||
.border_color(gpui::red())
|
||||
.id("titlebar_project_menu_button")
|
||||
.child(Button::new("project_name").variant(ButtonVariant::Ghost))
|
||||
.tooltip(move |_, cx| Tooltip::text("Recent Projects", cx)),
|
||||
.tooltip(move |cx| Tooltip::text("Recent Projects", cx)),
|
||||
)
|
||||
// TODO - Add git menu
|
||||
.child(
|
||||
div()
|
||||
.border()
|
||||
.border_color(gpui::red())
|
||||
.id("titlebar_git_menu_button")
|
||||
.child(
|
||||
Button::new("branch_name")
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.color(Some(TextColor::Muted)),
|
||||
.color(Some(Color::Muted)),
|
||||
)
|
||||
.tooltip(move |_, cx| {
|
||||
.tooltip(move |cx| {
|
||||
cx.build_view(|_| {
|
||||
Tooltip::new("Recent Branches")
|
||||
.key_binding(KeyBinding::new(gpui::KeyBinding::new(
|
||||
@ -149,8 +193,113 @@ impl Render for CollabTitlebarItem {
|
||||
.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 |mut cx| async move {
|
||||
client
|
||||
.authenticate_and_connect(true, &cx)
|
||||
.await
|
||||
.notify_async_err(&mut cx);
|
||||
})
|
||||
.detach();
|
||||
}))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,11 +7,14 @@ pub mod notification_panel;
|
||||
pub mod notifications;
|
||||
mod panel_settings;
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::{rc::Rc, sync::Arc};
|
||||
|
||||
pub use collab_panel::CollabPanel;
|
||||
pub use collab_titlebar_item::CollabTitlebarItem;
|
||||
use gpui::AppContext;
|
||||
use gpui::{
|
||||
point, AppContext, GlobalPixels, Pixels, PlatformDisplay, Size, WindowBounds, WindowKind,
|
||||
WindowOptions,
|
||||
};
|
||||
pub use panel_settings::{
|
||||
ChatPanelSettings, CollaborationPanelSettings, NotificationPanelSettings,
|
||||
};
|
||||
@ -23,7 +26,7 @@ use workspace::AppState;
|
||||
// [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);
|
||||
ChatPanelSettings::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_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_mute);
|
||||
@ -95,31 +98,36 @@ pub fn init(_app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
// }
|
||||
// }
|
||||
|
||||
// fn notification_window_options(
|
||||
// screen: Rc<dyn Screen>,
|
||||
// window_size: Vector2F,
|
||||
// ) -> WindowOptions<'static> {
|
||||
// const NOTIFICATION_PADDING: f32 = 16.;
|
||||
fn notification_window_options(
|
||||
screen: Rc<dyn PlatformDisplay>,
|
||||
window_size: Size<Pixels>,
|
||||
) -> WindowOptions {
|
||||
let notification_margin_width = GlobalPixels::from(16.);
|
||||
let notification_margin_height = GlobalPixels::from(-0.) - GlobalPixels::from(48.);
|
||||
|
||||
// let screen_bounds = screen.content_bounds();
|
||||
// WindowOptions {
|
||||
// bounds: WindowBounds::Fixed(RectF::new(
|
||||
// screen_bounds.upper_right()
|
||||
// + vec2f(
|
||||
// -NOTIFICATION_PADDING - window_size.x(),
|
||||
// NOTIFICATION_PADDING,
|
||||
// ),
|
||||
// window_size,
|
||||
// )),
|
||||
// titlebar: None,
|
||||
// center: false,
|
||||
// focus: false,
|
||||
// show: true,
|
||||
// kind: WindowKind::PopUp,
|
||||
// is_movable: false,
|
||||
// screen: Some(screen),
|
||||
// }
|
||||
// }
|
||||
let screen_bounds = screen.bounds();
|
||||
let size: Size<GlobalPixels> = window_size.into();
|
||||
|
||||
// todo!() use content bounds instead of screen.bounds and get rid of magics in point's 2nd argument.
|
||||
let bounds = gpui::Bounds::<GlobalPixels> {
|
||||
origin: screen_bounds.upper_right()
|
||||
- point(
|
||||
size.width + notification_margin_width,
|
||||
notification_margin_height,
|
||||
),
|
||||
size: window_size.into(),
|
||||
};
|
||||
WindowOptions {
|
||||
bounds: WindowBounds::Fixed(bounds),
|
||||
titlebar: None,
|
||||
center: false,
|
||||
focus: false,
|
||||
show: true,
|
||||
kind: WindowKind::PopUp,
|
||||
is_movable: false,
|
||||
display_id: Some(screen.id()),
|
||||
}
|
||||
}
|
||||
|
||||
// fn render_avatar<T: 'static>(
|
||||
// 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::{
|
||||
// geometry::{
|
||||
// rect::RectF,
|
||||
// vector::{vec2f, Vector2F},
|
||||
// },
|
||||
// json::ToJson,
|
||||
// serde_json::{self, json},
|
||||
// AnyElement, Axis, Element, View, ViewContext,
|
||||
// };
|
||||
#[derive(Default)]
|
||||
pub(crate) struct FacePile {
|
||||
faces: Vec<AnyElement>,
|
||||
}
|
||||
|
||||
// pub(crate) struct FacePile<V: View> {
|
||||
// overlap: f32,
|
||||
// faces: Vec<AnyElement<V>>,
|
||||
// }
|
||||
impl RenderOnce for FacePile {
|
||||
type Rendered = Div;
|
||||
|
||||
// impl<V: View> FacePile<V> {
|
||||
// pub fn new(overlap: f32) -> Self {
|
||||
// Self {
|
||||
// overlap,
|
||||
// faces: Vec::new(),
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
fn render(self, _: &mut WindowContext) -> Self::Rendered {
|
||||
let player_count = self.faces.len();
|
||||
let player_list = self.faces.into_iter().enumerate().map(|(ix, player)| {
|
||||
let isnt_last = ix < player_count - 1;
|
||||
|
||||
// impl<V: View> Element<V> for FacePile<V> {
|
||||
// type LayoutState = ();
|
||||
// type PaintState = ();
|
||||
div().when(isnt_last, |div| div.neg_mr_1()).child(player)
|
||||
});
|
||||
div().p_1().flex().items_center().children(player_list)
|
||||
}
|
||||
}
|
||||
|
||||
// impl Element for FacePile {
|
||||
// type State = ();
|
||||
// fn layout(
|
||||
// &mut self,
|
||||
// constraint: gpui::SizeConstraint,
|
||||
// view: &mut V,
|
||||
// cx: &mut ViewContext<V>,
|
||||
// ) -> (Vector2F, Self::LayoutState) {
|
||||
// debug_assert!(constraint.max_along(Axis::Horizontal) == f32::INFINITY);
|
||||
|
||||
// state: Option<Self::State>,
|
||||
// cx: &mut WindowContext,
|
||||
// ) -> (LayoutId, Self::State) {
|
||||
// let mut width = 0.;
|
||||
// let mut max_height = 0.;
|
||||
// let mut faces = Vec::with_capacity(self.faces.len());
|
||||
// for face in &mut self.faces {
|
||||
// let layout = face.layout(constraint, view, cx);
|
||||
// let layout = face.layout(cx);
|
||||
// width += layout.x();
|
||||
// max_height = f32::max(max_height, layout.y());
|
||||
// faces.push(layout);
|
||||
// }
|
||||
// width -= self.overlap * self.faces.len().saturating_sub(1) as f32;
|
||||
|
||||
// (
|
||||
// Vector2F::new(width, max_height.clamp(1., constraint.max.y())),
|
||||
// (),
|
||||
// )
|
||||
// (cx.request_layout(&Style::default(), faces), ())
|
||||
// // (
|
||||
// // Vector2F::new(width, max_height.clamp(1., constraint.max.y())),
|
||||
// // (),
|
||||
// // ))
|
||||
// }
|
||||
|
||||
// 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> {
|
||||
// fn extend<T: IntoIterator<Item = AnyElement<V>>>(&mut self, children: T) {
|
||||
// self.faces.extend(children);
|
||||
// }
|
||||
// }
|
||||
impl Extend<AnyElement> for FacePile {
|
||||
fn extend<T: IntoIterator<Item = AnyElement>>(&mut self, children: T) {
|
||||
self.faces.extend(children);
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
// use gpui::AppContext;
|
||||
// use std::sync::Arc;
|
||||
// use workspace::AppState;
|
||||
use gpui::AppContext;
|
||||
use std::sync::Arc;
|
||||
use workspace::AppState;
|
||||
|
||||
// pub mod incoming_call_notification;
|
||||
pub mod incoming_call_notification;
|
||||
// pub mod project_shared_notification;
|
||||
|
||||
// pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
// incoming_call_notification::init(app_state, cx);
|
||||
// project_shared_notification::init(app_state, cx);
|
||||
// }
|
||||
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
incoming_call_notification::init(app_state, cx);
|
||||
//project_shared_notification::init(app_state, cx);
|
||||
}
|
||||
|
@ -1,14 +1,12 @@
|
||||
use crate::notification_window_options;
|
||||
use call::{ActiveCall, IncomingCall};
|
||||
use client::proto;
|
||||
use futures::StreamExt;
|
||||
use gpui::{
|
||||
elements::*,
|
||||
geometry::vector::vec2f,
|
||||
platform::{CursorStyle, MouseButton},
|
||||
AnyElement, AppContext, Entity, View, ViewContext, WindowHandle,
|
||||
div, green, px, red, AppContext, Div, Element, ParentElement, Render, RenderOnce,
|
||||
StatefulInteractiveElement, Styled, ViewContext, VisualContext as _, WindowHandle,
|
||||
};
|
||||
use std::sync::{Arc, Weak};
|
||||
use ui::{h_stack, v_stack, Avatar, Button, Label};
|
||||
use util::ResultExt;
|
||||
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();
|
||||
while let Some(incoming_call) = incoming_call.next().await {
|
||||
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 {
|
||||
let window_size = cx.read(|cx| {
|
||||
let theme = &theme::current(cx).incoming_call_notification;
|
||||
vec2f(theme.window_width, theme.window_height)
|
||||
});
|
||||
let unique_screens = cx.update(|cx| cx.displays()).unwrap();
|
||||
let window_size = gpui::Size {
|
||||
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
|
||||
.add_window(notification_window_options(screen, window_size), |_| {
|
||||
IncomingCallNotification::new(incoming_call.clone(), app_state.clone())
|
||||
});
|
||||
|
||||
.open_window(options, |cx| {
|
||||
cx.build_view(|_| {
|
||||
IncomingCallNotification::new(
|
||||
incoming_call.clone(),
|
||||
app_state.clone(),
|
||||
)
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
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,
|
||||
}
|
||||
|
||||
pub struct IncomingCallNotification {
|
||||
struct IncomingCallNotificationState {
|
||||
call: IncomingCall,
|
||||
app_state: Weak<AppState>,
|
||||
}
|
||||
|
||||
impl IncomingCallNotification {
|
||||
pub struct IncomingCallNotification {
|
||||
state: Arc<IncomingCallNotificationState>,
|
||||
}
|
||||
impl IncomingCallNotificationState {
|
||||
pub fn new(call: IncomingCall, app_state: Weak<AppState>) -> Self {
|
||||
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);
|
||||
if accept {
|
||||
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 app_state = self.app_state.clone();
|
||||
cx.app_context()
|
||||
.spawn(|mut cx| async move {
|
||||
join.await?;
|
||||
if let Some(project_id) = initial_project_id {
|
||||
cx.update(|cx| {
|
||||
if let Some(app_state) = app_state.upgrade() {
|
||||
workspace::join_remote_project(
|
||||
project_id,
|
||||
caller_user_id,
|
||||
app_state,
|
||||
cx,
|
||||
)
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
let cx: &mut AppContext = cx;
|
||||
cx.spawn(|cx| async move {
|
||||
join.await?;
|
||||
if let Some(_project_id) = initial_project_id {
|
||||
cx.update(|_cx| {
|
||||
if let Some(_app_state) = app_state.upgrade() {
|
||||
// workspace::join_remote_project(
|
||||
// project_id,
|
||||
// caller_user_id,
|
||||
// app_state,
|
||||
// cx,
|
||||
// )
|
||||
// .detach_and_log_err(cx);
|
||||
}
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
} else {
|
||||
active_call.update(cx, |active_call, cx| {
|
||||
active_call.decline_incoming(cx).log_err();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_caller(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
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()
|
||||
impl IncomingCallNotification {
|
||||
pub fn new(call: IncomingCall, app_state: Weak<AppState>) -> Self {
|
||||
Self {
|
||||
state: Arc::new(IncomingCallNotificationState::new(call, app_state)),
|
||||
}
|
||||
}
|
||||
fn render_caller(&self, cx: &mut ViewContext<Self>) -> impl Element {
|
||||
h_stack()
|
||||
.children(
|
||||
self.state
|
||||
.call
|
||||
.calling_user
|
||||
.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 Decline {}
|
||||
// enum Accept {}
|
||||
// enum Decline {}
|
||||
|
||||
let theme = theme::current(cx);
|
||||
Flex::column()
|
||||
.with_child(
|
||||
MouseEventHandler::new::<Accept, _>(0, cx, |_, _| {
|
||||
let theme = &theme.incoming_call_notification;
|
||||
Label::new("Accept", theme.accept_button.text.clone())
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(theme.accept_button.container)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, |_, this, cx| {
|
||||
this.respond(true, cx);
|
||||
})
|
||||
.flex(1., true),
|
||||
)
|
||||
.with_child(
|
||||
MouseEventHandler::new::<Decline, _>(0, cx, |_, _| {
|
||||
let theme = &theme.incoming_call_notification;
|
||||
Label::new("Decline", theme.decline_button.text.clone())
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(theme.decline_button.container)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, |_, this, cx| {
|
||||
this.respond(false, cx);
|
||||
})
|
||||
.flex(1., true),
|
||||
)
|
||||
.constrained()
|
||||
.with_width(theme.incoming_call_notification.button_width)
|
||||
.into_any()
|
||||
// let theme = theme::current(cx);
|
||||
// Flex::column()
|
||||
// .with_child(
|
||||
// MouseEventHandler::new::<Accept, _>(0, cx, |_, _| {
|
||||
// let theme = &theme.incoming_call_notification;
|
||||
// Label::new("Accept", theme.accept_button.text.clone())
|
||||
// .aligned()
|
||||
// .contained()
|
||||
// .with_style(theme.accept_button.container)
|
||||
// })
|
||||
// .with_cursor_style(CursorStyle::PointingHand)
|
||||
// .on_click(MouseButton::Left, |_, this, cx| {
|
||||
// this.respond(true, cx);
|
||||
// })
|
||||
// .flex(1., true),
|
||||
// )
|
||||
// .with_child(
|
||||
// MouseEventHandler::new::<Decline, _>(0, cx, |_, _| {
|
||||
// let theme = &theme.incoming_call_notification;
|
||||
// Label::new("Decline", theme.decline_button.text.clone())
|
||||
// .aligned()
|
||||
// .contained()
|
||||
// .with_style(theme.decline_button.container)
|
||||
// })
|
||||
// .with_cursor_style(CursorStyle::PointingHand)
|
||||
// .on_click(MouseButton::Left, |_, this, cx| {
|
||||
// this.respond(false, cx);
|
||||
// })
|
||||
// .flex(1., true),
|
||||
// )
|
||||
// .constrained()
|
||||
// .with_width(theme.incoming_call_notification.button_width)
|
||||
// .into_any()
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for IncomingCallNotification {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for IncomingCallNotification {
|
||||
fn ui_name() -> &'static str {
|
||||
"IncomingCallNotification"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
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()
|
||||
impl Render for IncomingCallNotification {
|
||||
type Element = Div;
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
div().bg(red()).flex_none().child(self.render_caller(cx))
|
||||
// Flex::row()
|
||||
// .with_child()
|
||||
// .with_child(self.render_buttons(cx))
|
||||
// .contained()
|
||||
// .with_background_color(background)
|
||||
// .expanded()
|
||||
// .into_any()
|
||||
}
|
||||
}
|
||||
|
@ -1,17 +1,15 @@
|
||||
use collections::{CommandPaletteFilter, HashMap};
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
actions, div, prelude::*, Action, AppContext, Component, Div, EventEmitter, FocusHandle,
|
||||
FocusableView, Keystroke, Manager, ParentComponent, Render, Styled, View, ViewContext,
|
||||
VisualContext, WeakView,
|
||||
actions, Action, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView,
|
||||
Keystroke, ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use std::{
|
||||
cmp::{self, Reverse},
|
||||
sync::Arc,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
use ui::{h_stack, v_stack, HighlightedLabel, KeyBinding, StyledExt};
|
||||
use ui::{h_stack, v_stack, HighlightedLabel, KeyBinding, ListItem};
|
||||
use util::{
|
||||
channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL},
|
||||
ResultExt,
|
||||
@ -69,7 +67,7 @@ impl CommandPalette {
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<Manager> for CommandPalette {}
|
||||
impl EventEmitter<DismissEvent> for CommandPalette {}
|
||||
|
||||
impl FocusableView for CommandPalette {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
@ -78,7 +76,7 @@ impl FocusableView for CommandPalette {
|
||||
}
|
||||
|
||||
impl Render for CommandPalette {
|
||||
type Element = Div<Self>;
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
v_stack().w_96().child(self.picker.clone())
|
||||
@ -141,7 +139,7 @@ impl CommandPaletteDelegate {
|
||||
}
|
||||
|
||||
impl PickerDelegate for CommandPaletteDelegate {
|
||||
type ListItem = Div<Picker<Self>>;
|
||||
type ListItem = ListItem;
|
||||
|
||||
fn placeholder_text(&self) -> Arc<str> {
|
||||
"Execute a command...".into()
|
||||
@ -269,7 +267,7 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
|
||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.command_palette
|
||||
.update(cx, |_, cx| cx.emit(Manager::Dismiss))
|
||||
.update(cx, |_, cx| cx.emit(DismissEvent::Dismiss))
|
||||
.log_err();
|
||||
}
|
||||
|
||||
@ -294,24 +292,16 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Self::ListItem {
|
||||
let colors = cx.theme().colors();
|
||||
) -> Option<Self::ListItem> {
|
||||
let Some(r#match) = self.matches.get(ix) else {
|
||||
return div();
|
||||
return None;
|
||||
};
|
||||
let Some(command) = self.commands.get(r#match.candidate_id) else {
|
||||
return div();
|
||||
return None;
|
||||
};
|
||||
|
||||
div()
|
||||
.px_1()
|
||||
.text_color(colors.text)
|
||||
.text_ui()
|
||||
.bg(colors.ghost_element_background)
|
||||
.rounded_md()
|
||||
.when(selected, |this| this.bg(colors.ghost_element_selected))
|
||||
.hover(|this| this.bg(colors.ghost_element_hover))
|
||||
.child(
|
||||
Some(
|
||||
ListItem::new(ix).selected(selected).child(
|
||||
h_stack()
|
||||
.justify_between()
|
||||
.child(HighlightedLabel::new(
|
||||
@ -319,7 +309,8 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
r#match.positions.clone(),
|
||||
))
|
||||
.children(KeyBinding::for_action(&*command.action, cx)),
|
||||
)
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -13,10 +13,10 @@ use editor::{
|
||||
};
|
||||
use futures::future::try_join_all;
|
||||
use gpui::{
|
||||
actions, div, AnyElement, AnyView, AppContext, Component, Context, Div, EventEmitter,
|
||||
FocusEvent, FocusHandle, Focusable, FocusableComponent, FocusableView, InteractiveComponent,
|
||||
Model, ParentComponent, Render, SharedString, Styled, Subscription, Task, View, ViewContext,
|
||||
VisualContext, WeakView,
|
||||
actions, div, AnyElement, AnyView, AppContext, Context, Div, EventEmitter, FocusEvent,
|
||||
FocusHandle, Focusable, FocusableElement, FocusableView, InteractiveElement, IntoElement,
|
||||
Model, ParentElement, Render, SharedString, Styled, Subscription, Task, View, ViewContext,
|
||||
VisualContext, WeakView, WindowContext,
|
||||
};
|
||||
use language::{
|
||||
Anchor, Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Point, Selection,
|
||||
@ -36,7 +36,7 @@ use std::{
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
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 workspace::{
|
||||
item::{BreadcrumbText, Item, ItemEvent, ItemHandle},
|
||||
@ -91,7 +91,7 @@ struct DiagnosticGroupState {
|
||||
impl EventEmitter<ItemEvent> for ProjectDiagnosticsEditor {}
|
||||
|
||||
impl Render for ProjectDiagnosticsEditor {
|
||||
type Element = Focusable<Self, Div<Self>>;
|
||||
type Element = Focusable<Div>;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
let child = if self.path_states.is_empty() {
|
||||
@ -109,8 +109,8 @@ impl Render for ProjectDiagnosticsEditor {
|
||||
div()
|
||||
.track_focus(&self.focus_handle)
|
||||
.size_full()
|
||||
.on_focus_in(Self::focus_in)
|
||||
.on_action(Self::toggle_warnings)
|
||||
.on_focus_in(cx.listener(Self::focus_in))
|
||||
.on_action(cx.listener(Self::toggle_warnings))
|
||||
.child(child)
|
||||
}
|
||||
}
|
||||
@ -662,7 +662,7 @@ impl Item for ProjectDiagnosticsEditor {
|
||||
Some("Project Diagnostics".into())
|
||||
}
|
||||
|
||||
fn tab_content<T: 'static>(&self, _detail: Option<usize>, _: &AppContext) -> AnyElement<T> {
|
||||
fn tab_content(&self, _detail: Option<usize>, _: &WindowContext) -> AnyElement {
|
||||
render_summary(&self.summary)
|
||||
}
|
||||
|
||||
@ -742,7 +742,7 @@ impl Item for ProjectDiagnosticsEditor {
|
||||
}
|
||||
|
||||
fn breadcrumb_location(&self) -> ToolbarItemLocation {
|
||||
ToolbarItemLocation::PrimaryLeft { flex: None }
|
||||
ToolbarItemLocation::PrimaryLeft
|
||||
}
|
||||
|
||||
fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
|
||||
@ -778,27 +778,28 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
|
||||
.bg(gpui::red())
|
||||
.map(|stack| {
|
||||
let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
|
||||
IconElement::new(Icon::XCircle).color(TextColor::Error)
|
||||
IconElement::new(Icon::XCircle).color(Color::Error)
|
||||
} else {
|
||||
IconElement::new(Icon::ExclamationTriangle).color(TextColor::Warning)
|
||||
IconElement::new(Icon::ExclamationTriangle).color(Color::Warning)
|
||||
};
|
||||
|
||||
stack.child(div().pl_8().child(icon))
|
||||
})
|
||||
.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()))
|
||||
.when_some(diagnostic.code.as_ref(), |stack, code| {
|
||||
stack.child(Label::new(code.clone()))
|
||||
})
|
||||
.render()
|
||||
.into_any_element()
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn render_summary<T: 'static>(summary: &DiagnosticSummary) -> AnyElement<T> {
|
||||
pub(crate) fn render_summary(summary: &DiagnosticSummary) -> AnyElement {
|
||||
if summary.error_count == 0 && summary.warning_count == 0 {
|
||||
Label::new("No problems").render()
|
||||
let label = Label::new("No problems");
|
||||
label.into_any_element()
|
||||
} else {
|
||||
h_stack()
|
||||
.bg(gpui::red())
|
||||
@ -806,7 +807,7 @@ pub(crate) fn render_summary<T: 'static>(summary: &DiagnosticSummary) -> AnyElem
|
||||
.child(Label::new(summary.error_count.to_string()))
|
||||
.child(IconElement::new(Icon::ExclamationTriangle))
|
||||
.child(Label::new(summary.warning_count.to_string()))
|
||||
.render()
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
@ -1549,7 +1550,7 @@ mod tests {
|
||||
block_id: ix,
|
||||
editor_style: &editor::EditorStyle::default(),
|
||||
})
|
||||
.element_id()?
|
||||
.inner_id()?
|
||||
.try_into()
|
||||
.ok()?,
|
||||
|
||||
|
@ -1,13 +1,13 @@
|
||||
use collections::HashSet;
|
||||
use editor::{Editor, GoToDiagnostic};
|
||||
use gpui::{
|
||||
rems, Div, EventEmitter, InteractiveComponent, ParentComponent, Render, Stateful,
|
||||
StatefulInteractiveComponent, Styled, Subscription, View, ViewContext, WeakView,
|
||||
rems, Div, EventEmitter, InteractiveElement, ParentElement, Render, Stateful,
|
||||
StatefulInteractiveElement, Styled, Subscription, View, ViewContext, WeakView,
|
||||
};
|
||||
use language::Diagnostic;
|
||||
use lsp::LanguageServerId;
|
||||
use theme::ActiveTheme;
|
||||
use ui::{h_stack, Icon, IconElement, Label, TextColor, Tooltip};
|
||||
use ui::{h_stack, Color, Icon, IconElement, Label, Tooltip};
|
||||
use workspace::{item::ItemHandle, StatusItemView, ToolbarItemEvent, Workspace};
|
||||
|
||||
use crate::ProjectDiagnosticsEditor;
|
||||
@ -22,30 +22,30 @@ pub struct DiagnosticIndicator {
|
||||
}
|
||||
|
||||
impl Render for DiagnosticIndicator {
|
||||
type Element = Stateful<Self, Div<Self>>;
|
||||
type Element = Stateful<Div>;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
let diagnostic_indicator = match (self.summary.error_count, self.summary.warning_count) {
|
||||
(0, 0) => h_stack().child(IconElement::new(Icon::Check).color(TextColor::Success)),
|
||||
(0, 0) => h_stack().child(IconElement::new(Icon::Check).color(Color::Success)),
|
||||
(0, warning_count) => h_stack()
|
||||
.gap_1()
|
||||
.child(IconElement::new(Icon::ExclamationTriangle).color(TextColor::Warning))
|
||||
.child(IconElement::new(Icon::ExclamationTriangle).color(Color::Warning))
|
||||
.child(Label::new(warning_count.to_string())),
|
||||
(error_count, 0) => h_stack()
|
||||
.gap_1()
|
||||
.child(IconElement::new(Icon::XCircle).color(TextColor::Error))
|
||||
.child(IconElement::new(Icon::XCircle).color(Color::Error))
|
||||
.child(Label::new(error_count.to_string())),
|
||||
(error_count, warning_count) => h_stack()
|
||||
.gap_1()
|
||||
.child(IconElement::new(Icon::XCircle).color(TextColor::Error))
|
||||
.child(IconElement::new(Icon::XCircle).color(Color::Error))
|
||||
.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())),
|
||||
};
|
||||
|
||||
h_stack()
|
||||
.id(cx.entity_id())
|
||||
.on_action(Self::go_to_next_diagnostic)
|
||||
.id("diagnostic-indicator")
|
||||
.on_action(cx.listener(Self::go_to_next_diagnostic))
|
||||
.rounded_md()
|
||||
.flex_none()
|
||||
.h(rems(1.375))
|
||||
@ -54,14 +54,14 @@ impl Render for DiagnosticIndicator {
|
||||
.bg(cx.theme().colors().ghost_element_background)
|
||||
.hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
|
||||
.active(|style| style.bg(cx.theme().colors().ghost_element_active))
|
||||
.tooltip(|_, cx| Tooltip::text("Project Diagnostics", cx))
|
||||
.on_click(|this, _, cx| {
|
||||
.tooltip(|cx| Tooltip::text("Project Diagnostics", cx))
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
if let Some(workspace) = this.workspace.upgrade() {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
ProjectDiagnosticsEditor::deploy(workspace, &Default::default(), cx)
|
||||
})
|
||||
}
|
||||
})
|
||||
}))
|
||||
.child(diagnostic_indicator)
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
use crate::ProjectDiagnosticsEditor;
|
||||
use gpui::{div, Div, EventEmitter, ParentComponent, Render, ViewContext, WeakView};
|
||||
use gpui::{div, Div, EventEmitter, ParentElement, Render, ViewContext, WeakView};
|
||||
use ui::{Icon, IconButton, Tooltip};
|
||||
use workspace::{item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
|
||||
|
||||
@ -8,7 +8,7 @@ pub struct ToolbarControls {
|
||||
}
|
||||
|
||||
impl Render for ToolbarControls {
|
||||
type Element = Div<Self>;
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
let include_warnings = self
|
||||
@ -26,14 +26,14 @@ impl Render for ToolbarControls {
|
||||
|
||||
div().child(
|
||||
IconButton::new("toggle-warnings", Icon::ExclamationTriangle)
|
||||
.tooltip(move |_, cx| Tooltip::text(tooltip, cx))
|
||||
.on_click(|this: &mut Self, cx| {
|
||||
.tooltip(move |cx| Tooltip::text(tooltip, cx))
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
if let Some(editor) = this.editor.as_ref().and_then(|editor| editor.upgrade()) {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.toggle_warnings(&Default::default(), cx);
|
||||
});
|
||||
}
|
||||
}),
|
||||
})),
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -49,7 +49,7 @@ impl ToolbarItemView for ToolbarControls {
|
||||
if let Some(pane_item) = active_pane_item.as_ref() {
|
||||
if let Some(editor) = pane_item.downcast::<ProjectDiagnosticsEditor>() {
|
||||
self.editor = Some(editor.downgrade());
|
||||
ToolbarItemLocation::PrimaryRight { flex: None }
|
||||
ToolbarItemLocation::PrimaryRight
|
||||
} else {
|
||||
ToolbarItemLocation::Hidden
|
||||
}
|
||||
|
@ -1001,17 +1001,18 @@ impl CompletionsMenu {
|
||||
|
||||
fn pre_resolve_completion_documentation(
|
||||
&self,
|
||||
project: Option<ModelHandle<Project>>,
|
||||
editor: &Editor,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
) -> Option<Task<()>> {
|
||||
let settings = settings::get::<EditorSettings>(cx);
|
||||
if !settings.show_completion_documentation {
|
||||
return;
|
||||
return None;
|
||||
}
|
||||
|
||||
let Some(project) = project else {
|
||||
return;
|
||||
let Some(project) = editor.project.clone() else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let client = project.read(cx).client();
|
||||
let language_registry = project.read(cx).languages().clone();
|
||||
|
||||
@ -1021,7 +1022,7 @@ impl CompletionsMenu {
|
||||
let completions = self.completions.clone();
|
||||
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 {
|
||||
let Some(project_id) = project_id else {
|
||||
log::error!("Remote project without remote_id");
|
||||
@ -1083,8 +1084,7 @@ impl CompletionsMenu {
|
||||
_ = this.update(&mut cx, |_, cx| cx.notify());
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}))
|
||||
}
|
||||
|
||||
fn attempt_resolve_selected_completion_documentation(
|
||||
@ -3423,7 +3423,7 @@ impl Editor {
|
||||
to_insert,
|
||||
}) = self.inlay_hint_cache.spawn_hint_refresh(
|
||||
reason_description,
|
||||
self.excerpt_visible_offsets(required_languages.as_ref(), cx),
|
||||
self.excerpts_for_inlay_hints_query(required_languages.as_ref(), cx),
|
||||
invalidate_cache,
|
||||
cx,
|
||||
) {
|
||||
@ -3442,11 +3442,15 @@ impl Editor {
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn excerpt_visible_offsets(
|
||||
pub fn excerpts_for_inlay_hints_query(
|
||||
&self,
|
||||
restrict_to_languages: Option<&HashSet<Arc<Language>>>,
|
||||
cx: &mut ViewContext<'_, '_, Editor>,
|
||||
) -> 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_snapshot = multi_buffer.snapshot(cx);
|
||||
let multi_buffer_visible_start = self
|
||||
@ -3466,6 +3470,14 @@ impl Editor {
|
||||
.filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty())
|
||||
.filter_map(|(buffer_handle, excerpt_visible_range, excerpt_id)| {
|
||||
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()?;
|
||||
if let Some(restrict_to_languages) = restrict_to_languages {
|
||||
if !restrict_to_languages.contains(language) {
|
||||
@ -3580,7 +3592,8 @@ impl Editor {
|
||||
let id = post_inc(&mut self.next_completion_id);
|
||||
let task = cx.spawn(|this, mut cx| {
|
||||
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 {
|
||||
id,
|
||||
initial_position: position,
|
||||
@ -3601,21 +3614,26 @@ impl Editor {
|
||||
selected_item: 0,
|
||||
list: Default::default(),
|
||||
};
|
||||
|
||||
menu.filter(query.as_deref(), cx.background()).await;
|
||||
|
||||
if menu.matches.is_empty() {
|
||||
None
|
||||
(None, None)
|
||||
} else {
|
||||
_ = this.update(&mut cx, |editor, cx| {
|
||||
menu.pre_resolve_completion_documentation(editor.project.clone(), cx);
|
||||
});
|
||||
Some(menu)
|
||||
let pre_resolve_task = this
|
||||
.update(&mut cx, |editor, cx| {
|
||||
menu.pre_resolve_completion_documentation(editor, cx)
|
||||
})
|
||||
.ok()
|
||||
.flatten();
|
||||
(Some(menu), pre_resolve_task)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
(None, None)
|
||||
};
|
||||
|
||||
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();
|
||||
match context_menu.as_ref() {
|
||||
@ -3636,10 +3654,10 @@ impl Editor {
|
||||
drop(context_menu);
|
||||
this.discard_copilot_suggestion(cx);
|
||||
cx.notify();
|
||||
} else if this.completion_tasks.is_empty() {
|
||||
// If there are no more completion tasks and the last menu was
|
||||
// empty, we should hide it. If it was already hidden, we should
|
||||
// also show the copilot suggestion when available.
|
||||
} else if this.completion_tasks.len() <= 1 {
|
||||
// If there are no more completion tasks (omitting ourself) and
|
||||
// the last menu was empty, we should hide it. If it was already
|
||||
// hidden, we should also show the copilot suggestion when available.
|
||||
drop(context_menu);
|
||||
if this.hide_context_menu(cx).is_none() {
|
||||
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>(())
|
||||
}
|
||||
.log_err()
|
||||
});
|
||||
|
||||
self.completion_tasks.push((id, task));
|
||||
}
|
||||
|
||||
|
@ -861,7 +861,7 @@ async fn fetch_and_update_hints(
|
||||
let inlay_hints_fetch_task = editor
|
||||
.update(&mut cx, |editor, cx| {
|
||||
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)) => {
|
||||
let visible_offset_length = current_visible_range.len();
|
||||
let double_visible_range = current_visible_range
|
||||
@ -2237,7 +2237,9 @@ pub mod tests {
|
||||
editor: &ViewHandle<Editor>,
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) -> 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!(
|
||||
ranges.len(),
|
||||
1,
|
||||
|
@ -50,7 +50,7 @@ struct BlockRow(u32);
|
||||
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
|
||||
struct WrapRow(u32);
|
||||
|
||||
pub type RenderBlock = Arc<dyn Fn(&mut BlockContext) -> AnyElement<Editor>>;
|
||||
pub type RenderBlock = Arc<dyn Fn(&mut BlockContext) -> AnyElement>;
|
||||
|
||||
pub struct Block {
|
||||
id: BlockId,
|
||||
@ -69,7 +69,7 @@ where
|
||||
pub position: P,
|
||||
pub height: u8,
|
||||
pub style: BlockStyle,
|
||||
pub render: Arc<dyn Fn(&mut BlockContext) -> AnyElement<Editor>>,
|
||||
pub render: Arc<dyn Fn(&mut BlockContext) -> AnyElement>,
|
||||
pub disposition: BlockDisposition,
|
||||
}
|
||||
|
||||
@ -947,7 +947,7 @@ impl DerefMut for BlockContext<'_, '_> {
|
||||
}
|
||||
|
||||
impl Block {
|
||||
pub fn render(&self, cx: &mut BlockContext) -> AnyElement<Editor> {
|
||||
pub fn render(&self, cx: &mut BlockContext) -> AnyElement {
|
||||
self.render.lock()(cx)
|
||||
}
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -3048,7 +3048,7 @@ fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
|
||||
position: snapshot.anchor_after(Point::new(2, 0)),
|
||||
disposition: BlockDisposition::Below,
|
||||
height: 1,
|
||||
render: Arc::new(|_| div().render()),
|
||||
render: Arc::new(|_| div().into_any()),
|
||||
}],
|
||||
Some(Autoscroll::fit()),
|
||||
cx,
|
||||
@ -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]
|
||||
async fn go_to_prev_overlapping_diagnostic(
|
||||
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
|
||||
.update(&mut cx, |editor, cx| {
|
||||
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)) => {
|
||||
let visible_offset_length = current_visible_range.len();
|
||||
let double_visible_range = current_visible_range
|
||||
@ -2201,7 +2201,9 @@ pub mod tests {
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) -> Range<Point> {
|
||||
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();
|
||||
assert_eq!(
|
||||
ranges.len(),
|
||||
|
@ -9,8 +9,8 @@ use collections::HashSet;
|
||||
use futures::future::try_join_all;
|
||||
use gpui::{
|
||||
div, point, AnyElement, AppContext, AsyncAppContext, Entity, EntityId, EventEmitter,
|
||||
FocusHandle, Model, ParentComponent, Pixels, SharedString, Styled, Subscription, Task, View,
|
||||
ViewContext, VisualContext, WeakView,
|
||||
FocusHandle, Model, ParentElement, Pixels, SharedString, Styled, Subscription, Task, View,
|
||||
ViewContext, VisualContext, WeakView, WindowContext,
|
||||
};
|
||||
use language::{
|
||||
proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, CharKind, OffsetRangeExt,
|
||||
@ -30,7 +30,7 @@ use std::{
|
||||
};
|
||||
use text::Selection;
|
||||
use theme::{ActiveTheme, Theme};
|
||||
use ui::{Label, TextColor};
|
||||
use ui::{Color, Label};
|
||||
use util::{paths::PathExt, ResultExt, TryFutureExt};
|
||||
use workspace::item::{BreadcrumbText, FollowEvent, FollowableEvents, FollowableItemHandle};
|
||||
use workspace::{
|
||||
@ -584,7 +584,7 @@ impl Item for Editor {
|
||||
Some(path.to_string_lossy().to_string().into())
|
||||
}
|
||||
|
||||
fn tab_content<T: 'static>(&self, detail: Option<usize>, cx: &AppContext) -> AnyElement<T> {
|
||||
fn tab_content(&self, detail: Option<usize>, cx: &WindowContext) -> AnyElement {
|
||||
let theme = cx.theme();
|
||||
|
||||
AnyElement::new(
|
||||
@ -604,7 +604,7 @@ impl Item for Editor {
|
||||
&description,
|
||||
MAX_TAB_TITLE_LEN,
|
||||
))
|
||||
.color(TextColor::Muted),
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
})),
|
||||
@ -761,7 +761,7 @@ impl Item for Editor {
|
||||
}
|
||||
|
||||
fn breadcrumb_location(&self) -> ToolbarItemLocation {
|
||||
ToolbarItemLocation::PrimaryLeft { flex: None }
|
||||
ToolbarItemLocation::PrimaryLeft
|
||||
}
|
||||
|
||||
fn breadcrumbs(&self, variant: &Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
|
||||
@ -907,17 +907,15 @@ impl SearchableItem for Editor {
|
||||
type Match = Range<Anchor>;
|
||||
|
||||
fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
|
||||
todo!()
|
||||
// self.clear_background_highlights::<BufferSearchHighlights>(cx);
|
||||
self.clear_background_highlights::<BufferSearchHighlights>(cx);
|
||||
}
|
||||
|
||||
fn update_matches(&mut self, matches: Vec<Range<Anchor>>, cx: &mut ViewContext<Self>) {
|
||||
todo!()
|
||||
// self.highlight_background::<BufferSearchHighlights>(
|
||||
// matches,
|
||||
// |theme| theme.search.match_background,
|
||||
// cx,
|
||||
// );
|
||||
self.highlight_background::<BufferSearchHighlights>(
|
||||
matches,
|
||||
|theme| theme.title_bar_background, // todo: update theme
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
|
||||
@ -952,22 +950,20 @@ impl SearchableItem for Editor {
|
||||
matches: Vec<Range<Anchor>>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
todo!()
|
||||
// self.unfold_ranges([matches[index].clone()], false, true, cx);
|
||||
// let range = self.range_for_match(&matches[index]);
|
||||
// self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
// s.select_ranges([range]);
|
||||
// })
|
||||
self.unfold_ranges([matches[index].clone()], false, true, cx);
|
||||
let range = self.range_for_match(&matches[index]);
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.select_ranges([range]);
|
||||
})
|
||||
}
|
||||
|
||||
fn select_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>) {
|
||||
todo!()
|
||||
// self.unfold_ranges(matches.clone(), false, false, cx);
|
||||
// let mut ranges = Vec::new();
|
||||
// for m in &matches {
|
||||
// ranges.push(self.range_for_match(&m))
|
||||
// }
|
||||
// self.change_selections(None, cx, |s| s.select_ranges(ranges));
|
||||
self.unfold_ranges(matches.clone(), false, false, cx);
|
||||
let mut ranges = Vec::new();
|
||||
for m in &matches {
|
||||
ranges.push(self.range_for_match(&m))
|
||||
}
|
||||
self.change_selections(None, cx, |s| s.select_ranges(ranges));
|
||||
}
|
||||
fn replace(
|
||||
&mut self,
|
||||
|
@ -2,9 +2,8 @@ use collections::HashMap;
|
||||
use editor::{scroll::autoscroll::Autoscroll, Bias, Editor};
|
||||
use fuzzy::{CharBag, PathMatch, PathMatchCandidate};
|
||||
use gpui::{
|
||||
actions, div, AppContext, Component, Div, EventEmitter, FocusHandle, FocusableView,
|
||||
InteractiveComponent, Manager, Model, ParentComponent, Render, Styled, Task, View, ViewContext,
|
||||
VisualContext, WeakView,
|
||||
actions, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, Model,
|
||||
ParentElement, Render, Styled, Task, View, ViewContext, VisualContext, WeakView,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
|
||||
@ -16,8 +15,7 @@ use std::{
|
||||
},
|
||||
};
|
||||
use text::Point;
|
||||
use theme::ActiveTheme;
|
||||
use ui::{v_stack, HighlightedLabel, StyledExt};
|
||||
use ui::{v_stack, HighlightedLabel, ListItem};
|
||||
use util::{paths::PathLikeWithPosition, post_inc, ResultExt};
|
||||
use workspace::Workspace;
|
||||
|
||||
@ -111,14 +109,14 @@ impl FileFinder {
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<Manager> for FileFinder {}
|
||||
impl EventEmitter<DismissEvent> for FileFinder {}
|
||||
impl FocusableView for FileFinder {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
self.picker.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
impl Render for FileFinder {
|
||||
type Element = Div<Self>;
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
v_stack().w_96().child(self.picker.clone())
|
||||
@ -530,7 +528,7 @@ impl FileFinderDelegate {
|
||||
}
|
||||
|
||||
impl PickerDelegate for FileFinderDelegate {
|
||||
type ListItem = Div<Picker<Self>>;
|
||||
type ListItem = ListItem;
|
||||
|
||||
fn placeholder_text(&self) -> Arc<str> {
|
||||
"Search project files...".into()
|
||||
@ -690,7 +688,7 @@ impl PickerDelegate for FileFinderDelegate {
|
||||
}
|
||||
}
|
||||
finder
|
||||
.update(&mut cx, |_, cx| cx.emit(Manager::Dismiss))
|
||||
.update(&mut cx, |_, cx| cx.emit(DismissEvent::Dismiss))
|
||||
.ok()?;
|
||||
|
||||
Some(())
|
||||
@ -702,7 +700,7 @@ impl PickerDelegate for FileFinderDelegate {
|
||||
|
||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<FileFinderDelegate>>) {
|
||||
self.file_finder
|
||||
.update(cx, |_, cx| cx.emit(Manager::Dismiss))
|
||||
.update(cx, |_, cx| cx.emit(DismissEvent::Dismiss))
|
||||
.log_err();
|
||||
}
|
||||
|
||||
@ -711,30 +709,22 @@ impl PickerDelegate for FileFinderDelegate {
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Self::ListItem {
|
||||
) -> Option<Self::ListItem> {
|
||||
let path_match = self
|
||||
.matches
|
||||
.get(ix)
|
||||
.expect("Invalid matches state: no element for index {ix}");
|
||||
let theme = cx.theme();
|
||||
let colors = theme.colors();
|
||||
|
||||
let (file_name, file_name_positions, full_path, full_path_positions) =
|
||||
self.labels_for_match(path_match, cx, ix);
|
||||
|
||||
div()
|
||||
.px_1()
|
||||
.text_color(colors.text)
|
||||
.text_ui()
|
||||
.bg(colors.ghost_element_background)
|
||||
.rounded_md()
|
||||
.when(selected, |this| this.bg(colors.ghost_element_selected))
|
||||
.hover(|this| this.bg(colors.ghost_element_hover))
|
||||
.child(
|
||||
Some(
|
||||
ListItem::new(ix).selected(selected).child(
|
||||
v_stack()
|
||||
.child(HighlightedLabel::new(file_name, file_name_positions))
|
||||
.child(HighlightedLabel::new(full_path, full_path_positions)),
|
||||
)
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6,6 +6,8 @@ use gpui::BackgroundExecutor;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
cmp::{self, Ordering},
|
||||
iter,
|
||||
ops::Range,
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
@ -54,6 +56,32 @@ pub struct StringMatch {
|
||||
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 {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.cmp(other).is_eq()
|
||||
|
@ -1,14 +1,13 @@
|
||||
use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Editor};
|
||||
use gpui::{
|
||||
actions, div, prelude::*, AppContext, Div, EventEmitter, FocusHandle, FocusableView, Manager,
|
||||
ParentComponent, Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext,
|
||||
actions, div, prelude::*, AppContext, DismissEvent, Div, EventEmitter, FocusHandle,
|
||||
FocusableView, Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext,
|
||||
WindowContext,
|
||||
};
|
||||
use text::{Bias, Point};
|
||||
use theme::ActiveTheme;
|
||||
use ui::{h_stack, v_stack, Label, StyledExt, TextColor};
|
||||
use ui::{h_stack, v_stack, Color, Label, StyledExt};
|
||||
use util::paths::FILE_ROW_COLUMN_DELIMITER;
|
||||
use workspace::Workspace;
|
||||
|
||||
actions!(Toggle);
|
||||
|
||||
@ -26,22 +25,24 @@ pub struct GoToLine {
|
||||
|
||||
impl FocusableView for GoToLine {
|
||||
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 {
|
||||
fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
|
||||
workspace.register_action(|workspace, _: &Toggle, cx| {
|
||||
let Some(editor) = workspace
|
||||
.active_item(cx)
|
||||
.and_then(|active_item| active_item.downcast::<Editor>())
|
||||
else {
|
||||
fn register(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
|
||||
let handle = cx.view().downgrade();
|
||||
editor.register_action(move |_: &Toggle, cx| {
|
||||
let Some(editor) = handle.upgrade() else {
|
||||
return;
|
||||
};
|
||||
|
||||
workspace.toggle_modal(cx, move |cx| GoToLine::new(editor, cx));
|
||||
let Some(workspace) = editor.read(cx).workspace() else {
|
||||
return;
|
||||
};
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.toggle_modal(cx, move |cx| GoToLine::new(editor, cx));
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
@ -89,7 +90,7 @@ impl GoToLine {
|
||||
) {
|
||||
match event {
|
||||
// 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),
|
||||
_ => {}
|
||||
}
|
||||
@ -124,7 +125,7 @@ impl GoToLine {
|
||||
}
|
||||
|
||||
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>) {
|
||||
@ -141,19 +142,19 @@ impl GoToLine {
|
||||
self.prev_scroll_position.take();
|
||||
}
|
||||
|
||||
cx.emit(Manager::Dismiss);
|
||||
cx.emit(DismissEvent::Dismiss);
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for GoToLine {
|
||||
type Element = Div<Self>;
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
div()
|
||||
.elevation_2(cx)
|
||||
.key_context("GoToLine")
|
||||
.on_action(Self::cancel)
|
||||
.on_action(Self::confirm)
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.on_action(cx.listener(Self::confirm))
|
||||
.w_96()
|
||||
.child(
|
||||
v_stack()
|
||||
@ -177,7 +178,7 @@ impl Render for GoToLine {
|
||||
.justify_between()
|
||||
.px_2()
|
||||
.py_1()
|
||||
.child(Label::new(self.current_text.clone()).color(TextColor::Muted)),
|
||||
.child(Label::new(self.current_text.clone()).color(Color::Muted)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
@ -10,11 +10,12 @@ pub use entity_map::*;
|
||||
pub use model_context::*;
|
||||
use refineable::Refineable;
|
||||
use smallvec::SmallVec;
|
||||
use smol::future::FutureExt;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub use test_context::*;
|
||||
|
||||
use crate::{
|
||||
current_platform, image_cache::ImageCache, Action, ActionRegistry, AnyBox, AnyView,
|
||||
current_platform, image_cache::ImageCache, Action, ActionRegistry, Any, AnyView,
|
||||
AnyWindowHandle, AppMetadata, AssetSource, BackgroundExecutor, ClipboardItem, Context,
|
||||
DispatchPhase, DisplayId, Entity, EventEmitter, FocusEvent, FocusHandle, FocusId,
|
||||
ForegroundExecutor, KeyBinding, Keymap, LayoutId, PathPromptOptions, Pixels, Platform,
|
||||
@ -28,7 +29,7 @@ use futures::{channel::oneshot, future::LocalBoxFuture, Future};
|
||||
use parking_lot::Mutex;
|
||||
use slotmap::SlotMap;
|
||||
use std::{
|
||||
any::{type_name, Any, TypeId},
|
||||
any::{type_name, TypeId},
|
||||
cell::{Ref, RefCell, RefMut},
|
||||
marker::PhantomData,
|
||||
mem,
|
||||
@ -194,7 +195,7 @@ pub struct AppContext {
|
||||
asset_source: Arc<dyn AssetSource>,
|
||||
pub(crate) image_cache: ImageCache,
|
||||
pub(crate) text_style_stack: Vec<TextStyleRefinement>,
|
||||
pub(crate) globals_by_type: HashMap<TypeId, AnyBox>,
|
||||
pub(crate) globals_by_type: HashMap<TypeId, Box<dyn Any>>,
|
||||
pub(crate) entities: EntityMap,
|
||||
pub(crate) new_view_observers: SubscriberSet<TypeId, NewViewListener>,
|
||||
pub(crate) windows: SlotMap<WindowId, Option<Window>>,
|
||||
@ -424,7 +425,7 @@ impl AppContext {
|
||||
/// Opens a new window with the given option and the root view returned by the given function.
|
||||
/// The function is invoked with a `WindowContext`, which can be used to interact with window-specific
|
||||
/// functionality.
|
||||
pub fn open_window<V: Render>(
|
||||
pub fn open_window<V: 'static + Render>(
|
||||
&mut self,
|
||||
options: crate::WindowOptions,
|
||||
build_root_view: impl FnOnce(&mut WindowContext) -> View<V>,
|
||||
@ -579,7 +580,7 @@ impl AppContext {
|
||||
.windows
|
||||
.iter()
|
||||
.filter_map(|(_, window)| {
|
||||
let window = window.as_ref().unwrap();
|
||||
let window = window.as_ref()?;
|
||||
if window.dirty {
|
||||
Some(window.handle.clone())
|
||||
} else {
|
||||
@ -983,6 +984,22 @@ impl AppContext {
|
||||
pub fn all_action_names(&self) -> &[SharedString] {
|
||||
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 {
|
||||
@ -1032,7 +1049,9 @@ impl Context for AppContext {
|
||||
let root_view = window.root_view.clone().unwrap();
|
||||
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
|
||||
.get_mut(handle.id)
|
||||
.ok_or_else(|| anyhow!("window not found"))?
|
||||
@ -1104,12 +1123,12 @@ pub(crate) enum Effect {
|
||||
|
||||
/// Wraps a global variable value during `update_global` while the value has been moved to the stack.
|
||||
pub(crate) struct GlobalLease<G: 'static> {
|
||||
global: AnyBox,
|
||||
global: Box<dyn Any>,
|
||||
global_type: PhantomData<G>,
|
||||
}
|
||||
|
||||
impl<G: 'static> GlobalLease<G> {
|
||||
fn new(global: AnyBox) -> Self {
|
||||
fn new(global: Box<dyn Any>) -> Self {
|
||||
GlobalLease {
|
||||
global,
|
||||
global_type: PhantomData,
|
||||
|
@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
AnyView, AnyWindowHandle, AppCell, AppContext, BackgroundExecutor, Context, FocusableView,
|
||||
ForegroundExecutor, Manager, Model, ModelContext, Render, Result, Task, View, ViewContext,
|
||||
VisualContext, WindowContext, WindowHandle,
|
||||
AnyView, AnyWindowHandle, AppCell, AppContext, BackgroundExecutor, Context, DismissEvent,
|
||||
FocusableView, ForegroundExecutor, Model, ModelContext, Render, Result, Task, View,
|
||||
ViewContext, VisualContext, WindowContext, WindowHandle,
|
||||
};
|
||||
use anyhow::{anyhow, Context as _};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
@ -115,7 +115,7 @@ impl AsyncAppContext {
|
||||
build_root_view: impl FnOnce(&mut WindowContext) -> View<V>,
|
||||
) -> Result<WindowHandle<V>>
|
||||
where
|
||||
V: Render,
|
||||
V: 'static + Render,
|
||||
{
|
||||
let app = self
|
||||
.app
|
||||
@ -306,7 +306,7 @@ impl VisualContext for AsyncWindowContext {
|
||||
build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V,
|
||||
) -> Self::Result<View<V>>
|
||||
where
|
||||
V: Render,
|
||||
V: 'static + Render,
|
||||
{
|
||||
self.window
|
||||
.update(self, |_, cx| cx.replace_root_view(build_view))
|
||||
@ -326,7 +326,7 @@ impl VisualContext for AsyncWindowContext {
|
||||
V: crate::ManagedView,
|
||||
{
|
||||
self.window.update(self, |_, cx| {
|
||||
view.update(cx, |_, cx| cx.emit(Manager::Dismiss))
|
||||
view.update(cx, |_, cx| cx.emit(DismissEvent::Dismiss))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
use crate::{private::Sealed, AnyBox, AppContext, Context, Entity, ModelContext};
|
||||
use crate::{private::Sealed, AppContext, Context, Entity, ModelContext};
|
||||
use anyhow::{anyhow, Result};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use parking_lot::{RwLock, RwLockUpgradableReadGuard};
|
||||
use slotmap::{SecondaryMap, SlotMap};
|
||||
use std::{
|
||||
any::{type_name, TypeId},
|
||||
any::{type_name, Any, TypeId},
|
||||
fmt::{self, Display},
|
||||
hash::{Hash, Hasher},
|
||||
marker::PhantomData,
|
||||
@ -31,7 +31,7 @@ impl Display for EntityId {
|
||||
}
|
||||
|
||||
pub(crate) struct EntityMap {
|
||||
entities: SecondaryMap<EntityId, AnyBox>,
|
||||
entities: SecondaryMap<EntityId, Box<dyn Any>>,
|
||||
ref_counts: Arc<RwLock<EntityRefCounts>>,
|
||||
}
|
||||
|
||||
@ -102,7 +102,7 @@ impl EntityMap {
|
||||
);
|
||||
}
|
||||
|
||||
pub fn take_dropped(&mut self) -> Vec<(EntityId, AnyBox)> {
|
||||
pub fn take_dropped(&mut self) -> Vec<(EntityId, Box<dyn Any>)> {
|
||||
let mut ref_counts = self.ref_counts.write();
|
||||
let dropped_entity_ids = mem::take(&mut ref_counts.dropped_entity_ids);
|
||||
|
||||
@ -122,7 +122,7 @@ impl EntityMap {
|
||||
}
|
||||
|
||||
pub struct Lease<'a, T> {
|
||||
entity: Option<AnyBox>,
|
||||
entity: Option<Box<dyn Any>>,
|
||||
pub model: &'a Model<T>,
|
||||
entity_type: PhantomData<T>,
|
||||
}
|
||||
|
@ -1,8 +1,9 @@
|
||||
use crate::{
|
||||
div, Action, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext,
|
||||
BackgroundExecutor, Context, Div, EventEmitter, ForegroundExecutor, InputEvent, KeyDownEvent,
|
||||
Keystroke, Model, ModelContext, Render, Result, Task, TestDispatcher, TestPlatform, TestWindow,
|
||||
View, ViewContext, VisualContext, WindowContext, WindowHandle, WindowOptions,
|
||||
BackgroundExecutor, Context, Div, Entity, EventEmitter, ForegroundExecutor, InputEvent,
|
||||
KeyDownEvent, Keystroke, Model, ModelContext, Render, Result, Task, TestDispatcher,
|
||||
TestPlatform, TestWindow, View, ViewContext, VisualContext, WindowContext, WindowHandle,
|
||||
WindowOptions,
|
||||
};
|
||||
use anyhow::{anyhow, bail};
|
||||
use futures::{Stream, StreamExt};
|
||||
@ -126,7 +127,7 @@ impl TestAppContext {
|
||||
pub fn add_window<F, V>(&mut self, build_window: F) -> WindowHandle<V>
|
||||
where
|
||||
F: FnOnce(&mut ViewContext<V>) -> V,
|
||||
V: Render,
|
||||
V: 'static + Render,
|
||||
{
|
||||
let mut cx = self.app.borrow_mut();
|
||||
cx.open_window(WindowOptions::default(), |cx| cx.build_view(build_window))
|
||||
@ -143,7 +144,7 @@ impl TestAppContext {
|
||||
pub fn add_window_view<F, V>(&mut self, build_window: F) -> (View<V>, &mut VisualTestContext)
|
||||
where
|
||||
F: FnOnce(&mut ViewContext<V>) -> V,
|
||||
V: Render,
|
||||
V: 'static + Render,
|
||||
{
|
||||
let mut cx = self.app.borrow_mut();
|
||||
let window = cx.open_window(WindowOptions::default(), |cx| cx.build_view(build_window));
|
||||
@ -296,21 +297,19 @@ impl TestAppContext {
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn notifications<T: 'static>(&mut self, entity: &Model<T>) -> impl Stream<Item = ()> {
|
||||
pub fn notifications<T: 'static>(&mut self, entity: &impl Entity<T>) -> impl Stream<Item = ()> {
|
||||
let (tx, rx) = futures::channel::mpsc::unbounded();
|
||||
|
||||
entity.update(self, move |_, cx: &mut ModelContext<T>| {
|
||||
self.update(|cx| {
|
||||
cx.observe(entity, {
|
||||
let tx = tx.clone();
|
||||
move |_, _, _| {
|
||||
move |_, _| {
|
||||
let _ = tx.unbounded_send(());
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.on_release(move |_, _| tx.close_channel()).detach();
|
||||
cx.observe_release(entity, move |_, _| tx.close_channel())
|
||||
.detach()
|
||||
});
|
||||
|
||||
rx
|
||||
}
|
||||
|
||||
@ -591,7 +590,7 @@ impl<'a> VisualContext for VisualTestContext<'a> {
|
||||
build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V,
|
||||
) -> Self::Result<View<V>>
|
||||
where
|
||||
V: Render,
|
||||
V: 'static + Render,
|
||||
{
|
||||
self.window
|
||||
.update(self.cx, |_, cx| cx.replace_root_view(build_view))
|
||||
@ -612,7 +611,7 @@ impl<'a> VisualContext for VisualTestContext<'a> {
|
||||
{
|
||||
self.window
|
||||
.update(self.cx, |_, cx| {
|
||||
view.update(cx, |_, cx| cx.emit(crate::Manager::Dismiss))
|
||||
view.update(cx, |_, cx| cx.emit(crate::DismissEvent::Dismiss))
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
@ -631,7 +630,7 @@ impl AnyWindowHandle {
|
||||
pub struct EmptyView {}
|
||||
|
||||
impl Render for EmptyView {
|
||||
type Element = Div<Self>;
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, _cx: &mut crate::ViewContext<Self>) -> Self::Element {
|
||||
div()
|
||||
|
@ -1,317 +1,63 @@
|
||||
use crate::{
|
||||
AvailableSpace, BorrowWindow, Bounds, ElementId, LayoutId, Pixels, Point, Size, ViewContext,
|
||||
WindowContext,
|
||||
};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
pub(crate) use smallvec::SmallVec;
|
||||
use std::{any::Any, fmt::Debug, mem};
|
||||
use std::{any::Any, fmt::Debug};
|
||||
|
||||
pub trait Element<V: 'static> {
|
||||
type ElementState: 'static;
|
||||
pub trait Render: 'static + Sized {
|
||||
type Element: Element + 'static;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element;
|
||||
}
|
||||
|
||||
pub trait IntoElement: Sized {
|
||||
type Element: Element + 'static;
|
||||
|
||||
fn element_id(&self) -> Option<ElementId>;
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
view_state: &mut V,
|
||||
element_state: Option<Self::ElementState>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> (LayoutId, Self::ElementState);
|
||||
fn into_element(self) -> Self::Element;
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
view_state: &mut V,
|
||||
element_state: &mut Self::ElementState,
|
||||
cx: &mut ViewContext<V>,
|
||||
);
|
||||
fn into_any_element(self) -> AnyElement {
|
||||
self.into_element().into_any()
|
||||
}
|
||||
|
||||
fn draw<T, R>(
|
||||
self,
|
||||
origin: Point<Pixels>,
|
||||
available_space: Size<T>,
|
||||
view_state: &mut V,
|
||||
cx: &mut ViewContext<V>,
|
||||
f: impl FnOnce(&Self::ElementState, &mut ViewContext<V>) -> R,
|
||||
cx: &mut WindowContext,
|
||||
f: impl FnOnce(&mut <Self::Element as Element>::State, &mut WindowContext) -> R,
|
||||
) -> R
|
||||
where
|
||||
Self: Sized,
|
||||
T: Clone + Default + Debug + Into<AvailableSpace>,
|
||||
{
|
||||
let mut element = RenderedElement {
|
||||
element: self,
|
||||
phase: ElementRenderPhase::Start,
|
||||
let element = self.into_element();
|
||||
let element_id = element.element_id();
|
||||
let element = DrawableElement {
|
||||
element: Some(element),
|
||||
phase: ElementDrawPhase::Start,
|
||||
};
|
||||
element.draw(origin, available_space.map(Into::into), view_state, cx);
|
||||
if let ElementRenderPhase::Painted { frame_state } = &element.phase {
|
||||
if let Some(frame_state) = frame_state.as_ref() {
|
||||
f(&frame_state, cx)
|
||||
} else {
|
||||
let element_id = element
|
||||
.element
|
||||
.element_id()
|
||||
.expect("we either have some frame_state or some element_id");
|
||||
cx.with_element_state(element_id, |element_state, cx| {
|
||||
let element_state = element_state.unwrap();
|
||||
let result = f(&element_state, cx);
|
||||
(result, element_state)
|
||||
})
|
||||
}
|
||||
|
||||
let frame_state =
|
||||
DrawableElement::draw(element, origin, available_space.map(Into::into), cx);
|
||||
|
||||
if let Some(mut frame_state) = frame_state {
|
||||
f(&mut frame_state, cx)
|
||||
} else {
|
||||
unreachable!()
|
||||
cx.with_element_state(element_id.unwrap(), |element_state, cx| {
|
||||
let mut element_state = element_state.unwrap();
|
||||
let result = f(&mut element_state, cx);
|
||||
(result, element_state)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deref, DerefMut, Default, Clone, Debug, Eq, PartialEq, Hash)]
|
||||
pub struct GlobalElementId(SmallVec<[ElementId; 32]>);
|
||||
|
||||
pub trait ParentComponent<V: 'static> {
|
||||
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]>;
|
||||
|
||||
fn child(mut self, child: impl Component<V>) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.children_mut().push(child.render());
|
||||
self
|
||||
}
|
||||
|
||||
fn children(mut self, iter: impl IntoIterator<Item = impl Component<V>>) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.children_mut()
|
||||
.extend(iter.into_iter().map(|item| item.render()));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
trait ElementObject<V> {
|
||||
fn element_id(&self) -> Option<ElementId>;
|
||||
fn layout(&mut self, view_state: &mut V, cx: &mut ViewContext<V>) -> LayoutId;
|
||||
fn paint(&mut self, view_state: &mut V, cx: &mut ViewContext<V>);
|
||||
fn measure(
|
||||
&mut self,
|
||||
available_space: Size<AvailableSpace>,
|
||||
view_state: &mut V,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> Size<Pixels>;
|
||||
fn draw(
|
||||
&mut self,
|
||||
origin: Point<Pixels>,
|
||||
available_space: Size<AvailableSpace>,
|
||||
view_state: &mut V,
|
||||
cx: &mut ViewContext<V>,
|
||||
);
|
||||
}
|
||||
|
||||
struct RenderedElement<V: 'static, E: Element<V>> {
|
||||
element: E,
|
||||
phase: ElementRenderPhase<E::ElementState>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
enum ElementRenderPhase<V> {
|
||||
#[default]
|
||||
Start,
|
||||
LayoutRequested {
|
||||
layout_id: LayoutId,
|
||||
frame_state: Option<V>,
|
||||
},
|
||||
LayoutComputed {
|
||||
layout_id: LayoutId,
|
||||
available_space: Size<AvailableSpace>,
|
||||
frame_state: Option<V>,
|
||||
},
|
||||
Painted {
|
||||
frame_state: Option<V>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Internal struct that wraps an element to store Layout and ElementState after the element is rendered.
|
||||
/// It's allocated as a trait object to erase the element type and wrapped in AnyElement<E::State> for
|
||||
/// improved usability.
|
||||
impl<V, E: Element<V>> RenderedElement<V, E> {
|
||||
fn new(element: E) -> Self {
|
||||
RenderedElement {
|
||||
element,
|
||||
phase: ElementRenderPhase::Start,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<V, E> ElementObject<V> for RenderedElement<V, E>
|
||||
where
|
||||
E: Element<V>,
|
||||
E::ElementState: 'static,
|
||||
{
|
||||
fn element_id(&self) -> Option<ElementId> {
|
||||
self.element.element_id()
|
||||
}
|
||||
|
||||
fn layout(&mut self, state: &mut V, cx: &mut ViewContext<V>) -> LayoutId {
|
||||
let (layout_id, frame_state) = match mem::take(&mut self.phase) {
|
||||
ElementRenderPhase::Start => {
|
||||
if let Some(id) = self.element.element_id() {
|
||||
let layout_id = cx.with_element_state(id, |element_state, cx| {
|
||||
self.element.layout(state, element_state, cx)
|
||||
});
|
||||
(layout_id, None)
|
||||
} else {
|
||||
let (layout_id, frame_state) = self.element.layout(state, None, cx);
|
||||
(layout_id, Some(frame_state))
|
||||
}
|
||||
}
|
||||
ElementRenderPhase::LayoutRequested { .. }
|
||||
| ElementRenderPhase::LayoutComputed { .. }
|
||||
| ElementRenderPhase::Painted { .. } => {
|
||||
panic!("element rendered twice")
|
||||
}
|
||||
};
|
||||
|
||||
self.phase = ElementRenderPhase::LayoutRequested {
|
||||
layout_id,
|
||||
frame_state,
|
||||
};
|
||||
layout_id
|
||||
}
|
||||
|
||||
fn paint(&mut self, view_state: &mut V, cx: &mut ViewContext<V>) {
|
||||
self.phase = match mem::take(&mut self.phase) {
|
||||
ElementRenderPhase::LayoutRequested {
|
||||
layout_id,
|
||||
mut frame_state,
|
||||
}
|
||||
| ElementRenderPhase::LayoutComputed {
|
||||
layout_id,
|
||||
mut frame_state,
|
||||
..
|
||||
} => {
|
||||
let bounds = cx.layout_bounds(layout_id);
|
||||
if let Some(id) = self.element.element_id() {
|
||||
cx.with_element_state(id, |element_state, cx| {
|
||||
let mut element_state = element_state.unwrap();
|
||||
self.element
|
||||
.paint(bounds, view_state, &mut element_state, cx);
|
||||
((), element_state)
|
||||
});
|
||||
} else {
|
||||
self.element
|
||||
.paint(bounds, view_state, frame_state.as_mut().unwrap(), cx);
|
||||
}
|
||||
ElementRenderPhase::Painted { frame_state }
|
||||
}
|
||||
|
||||
_ => panic!("must call layout before paint"),
|
||||
};
|
||||
}
|
||||
|
||||
fn measure(
|
||||
&mut self,
|
||||
available_space: Size<AvailableSpace>,
|
||||
view_state: &mut V,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> Size<Pixels> {
|
||||
if matches!(&self.phase, ElementRenderPhase::Start) {
|
||||
self.layout(view_state, cx);
|
||||
}
|
||||
|
||||
let layout_id = match &mut self.phase {
|
||||
ElementRenderPhase::LayoutRequested {
|
||||
layout_id,
|
||||
frame_state,
|
||||
} => {
|
||||
cx.compute_layout(*layout_id, available_space);
|
||||
let layout_id = *layout_id;
|
||||
self.phase = ElementRenderPhase::LayoutComputed {
|
||||
layout_id,
|
||||
available_space,
|
||||
frame_state: frame_state.take(),
|
||||
};
|
||||
layout_id
|
||||
}
|
||||
ElementRenderPhase::LayoutComputed {
|
||||
layout_id,
|
||||
available_space: prev_available_space,
|
||||
..
|
||||
} => {
|
||||
if available_space != *prev_available_space {
|
||||
cx.compute_layout(*layout_id, available_space);
|
||||
*prev_available_space = available_space;
|
||||
}
|
||||
*layout_id
|
||||
}
|
||||
_ => panic!("cannot measure after painting"),
|
||||
};
|
||||
|
||||
cx.layout_bounds(layout_id).size
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&mut self,
|
||||
origin: Point<Pixels>,
|
||||
available_space: Size<AvailableSpace>,
|
||||
view_state: &mut V,
|
||||
cx: &mut ViewContext<V>,
|
||||
) {
|
||||
self.measure(available_space, view_state, cx);
|
||||
cx.with_absolute_element_offset(origin, |cx| self.paint(view_state, cx))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AnyElement<V>(Box<dyn ElementObject<V>>);
|
||||
|
||||
impl<V> AnyElement<V> {
|
||||
pub fn new<E>(element: E) -> Self
|
||||
where
|
||||
V: 'static,
|
||||
E: 'static + Element<V>,
|
||||
E::ElementState: Any,
|
||||
{
|
||||
AnyElement(Box::new(RenderedElement::new(element)))
|
||||
}
|
||||
|
||||
pub fn element_id(&self) -> Option<ElementId> {
|
||||
self.0.element_id()
|
||||
}
|
||||
|
||||
pub fn layout(&mut self, view_state: &mut V, cx: &mut ViewContext<V>) -> LayoutId {
|
||||
self.0.layout(view_state, cx)
|
||||
}
|
||||
|
||||
pub fn paint(&mut self, view_state: &mut V, cx: &mut ViewContext<V>) {
|
||||
self.0.paint(view_state, cx)
|
||||
}
|
||||
|
||||
/// Initializes this element and performs layout within the given available space to determine its size.
|
||||
pub fn measure(
|
||||
&mut self,
|
||||
available_space: Size<AvailableSpace>,
|
||||
view_state: &mut V,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> Size<Pixels> {
|
||||
self.0.measure(available_space, view_state, cx)
|
||||
}
|
||||
|
||||
/// Initializes this element and performs layout in the available space, then paints it at the given origin.
|
||||
pub fn draw(
|
||||
&mut self,
|
||||
origin: Point<Pixels>,
|
||||
available_space: Size<AvailableSpace>,
|
||||
view_state: &mut V,
|
||||
cx: &mut ViewContext<V>,
|
||||
) {
|
||||
self.0.draw(origin, available_space, view_state, cx)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Component<V> {
|
||||
fn render(self) -> AnyElement<V>;
|
||||
|
||||
fn map<U>(self, f: impl FnOnce(Self) -> U) -> U
|
||||
where
|
||||
Self: Sized,
|
||||
U: Component<V>,
|
||||
U: IntoElement,
|
||||
{
|
||||
f(self)
|
||||
}
|
||||
@ -337,65 +83,460 @@ pub trait Component<V> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> Component<V> for AnyElement<V> {
|
||||
fn render(self) -> AnyElement<V> {
|
||||
self
|
||||
pub trait Element: 'static + IntoElement {
|
||||
type State: 'static;
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
state: Option<Self::State>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::State);
|
||||
|
||||
fn paint(self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext);
|
||||
|
||||
fn into_any(self) -> AnyElement {
|
||||
AnyElement::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<V, E, F> Element<V> for Option<F>
|
||||
where
|
||||
V: 'static,
|
||||
E: 'static + Component<V>,
|
||||
F: FnOnce(&mut V, &mut ViewContext<'_, V>) -> E + 'static,
|
||||
{
|
||||
type ElementState = AnyElement<V>;
|
||||
pub trait RenderOnce: 'static {
|
||||
type Rendered: IntoElement;
|
||||
|
||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered;
|
||||
}
|
||||
|
||||
pub struct Component<C> {
|
||||
component: Option<C>,
|
||||
}
|
||||
|
||||
pub struct CompositeElementState<C: RenderOnce> {
|
||||
rendered_element: Option<<C::Rendered as IntoElement>::Element>,
|
||||
rendered_element_state: <<C::Rendered as IntoElement>::Element as Element>::State,
|
||||
}
|
||||
|
||||
impl<C> Component<C> {
|
||||
pub fn new(component: C) -> Self {
|
||||
Component {
|
||||
component: Some(component),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: RenderOnce> Element for Component<C> {
|
||||
type State = CompositeElementState<C>;
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
state: Option<Self::State>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::State) {
|
||||
let mut element = self.component.take().unwrap().render(cx).into_element();
|
||||
let (layout_id, state) = element.layout(state.map(|s| s.rendered_element_state), cx);
|
||||
let state = CompositeElementState {
|
||||
rendered_element: Some(element),
|
||||
rendered_element_state: state,
|
||||
};
|
||||
(layout_id, state)
|
||||
}
|
||||
|
||||
fn paint(self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
|
||||
state
|
||||
.rendered_element
|
||||
.take()
|
||||
.unwrap()
|
||||
.paint(bounds, &mut state.rendered_element_state, cx);
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: RenderOnce> IntoElement for Component<C> {
|
||||
type Element = Self;
|
||||
|
||||
fn element_id(&self) -> Option<ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
view_state: &mut V,
|
||||
_: Option<Self::ElementState>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> (LayoutId, Self::ElementState) {
|
||||
let render = self.take().unwrap();
|
||||
let mut rendered_element = (render)(view_state, cx).render();
|
||||
let layout_id = rendered_element.layout(view_state, cx);
|
||||
(layout_id, rendered_element)
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_bounds: Bounds<Pixels>,
|
||||
view_state: &mut V,
|
||||
rendered_element: &mut Self::ElementState,
|
||||
cx: &mut ViewContext<V>,
|
||||
) {
|
||||
rendered_element.paint(view_state, cx)
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<V, E, F> Component<V> for Option<F>
|
||||
#[derive(Deref, DerefMut, Default, Clone, Debug, Eq, PartialEq, Hash)]
|
||||
pub struct GlobalElementId(SmallVec<[ElementId; 32]>);
|
||||
|
||||
pub trait ParentElement {
|
||||
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]>;
|
||||
|
||||
fn child(mut self, child: impl IntoElement) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.children_mut().push(child.into_element().into_any());
|
||||
self
|
||||
}
|
||||
|
||||
fn children(mut self, children: impl IntoIterator<Item = impl IntoElement>) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.children_mut()
|
||||
.extend(children.into_iter().map(|child| child.into_any_element()));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
trait ElementObject {
|
||||
fn element_id(&self) -> Option<ElementId>;
|
||||
|
||||
fn layout(&mut self, cx: &mut WindowContext) -> LayoutId;
|
||||
|
||||
fn paint(&mut self, cx: &mut WindowContext);
|
||||
|
||||
fn measure(
|
||||
&mut self,
|
||||
available_space: Size<AvailableSpace>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Size<Pixels>;
|
||||
|
||||
fn draw(
|
||||
&mut self,
|
||||
origin: Point<Pixels>,
|
||||
available_space: Size<AvailableSpace>,
|
||||
cx: &mut WindowContext,
|
||||
);
|
||||
}
|
||||
|
||||
pub struct DrawableElement<E: Element> {
|
||||
element: Option<E>,
|
||||
phase: ElementDrawPhase<E::State>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
enum ElementDrawPhase<S> {
|
||||
#[default]
|
||||
Start,
|
||||
LayoutRequested {
|
||||
layout_id: LayoutId,
|
||||
frame_state: Option<S>,
|
||||
},
|
||||
LayoutComputed {
|
||||
layout_id: LayoutId,
|
||||
available_space: Size<AvailableSpace>,
|
||||
frame_state: Option<S>,
|
||||
},
|
||||
}
|
||||
|
||||
/// A wrapper around an implementer of [Element] that allows it to be drawn in a window.
|
||||
impl<E: Element> DrawableElement<E> {
|
||||
fn new(element: E) -> Self {
|
||||
DrawableElement {
|
||||
element: Some(element),
|
||||
phase: ElementDrawPhase::Start,
|
||||
}
|
||||
}
|
||||
|
||||
fn element_id(&self) -> Option<ElementId> {
|
||||
self.element.as_ref()?.element_id()
|
||||
}
|
||||
|
||||
fn layout(&mut self, cx: &mut WindowContext) -> LayoutId {
|
||||
let (layout_id, frame_state) = if let Some(id) = self.element.as_ref().unwrap().element_id()
|
||||
{
|
||||
let layout_id = cx.with_element_state(id, |element_state, cx| {
|
||||
self.element.as_mut().unwrap().layout(element_state, cx)
|
||||
});
|
||||
(layout_id, None)
|
||||
} else {
|
||||
let (layout_id, frame_state) = self.element.as_mut().unwrap().layout(None, cx);
|
||||
(layout_id, Some(frame_state))
|
||||
};
|
||||
|
||||
self.phase = ElementDrawPhase::LayoutRequested {
|
||||
layout_id,
|
||||
frame_state,
|
||||
};
|
||||
layout_id
|
||||
}
|
||||
|
||||
fn paint(mut self, cx: &mut WindowContext) -> Option<E::State> {
|
||||
match self.phase {
|
||||
ElementDrawPhase::LayoutRequested {
|
||||
layout_id,
|
||||
frame_state,
|
||||
}
|
||||
| ElementDrawPhase::LayoutComputed {
|
||||
layout_id,
|
||||
frame_state,
|
||||
..
|
||||
} => {
|
||||
let bounds = cx.layout_bounds(layout_id);
|
||||
|
||||
if let Some(mut frame_state) = frame_state {
|
||||
self.element
|
||||
.take()
|
||||
.unwrap()
|
||||
.paint(bounds, &mut frame_state, cx);
|
||||
Some(frame_state)
|
||||
} else {
|
||||
let element_id = self
|
||||
.element
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.element_id()
|
||||
.expect("if we don't have frame state, we should have element state");
|
||||
cx.with_element_state(element_id, |element_state, cx| {
|
||||
let mut element_state = element_state.unwrap();
|
||||
self.element
|
||||
.take()
|
||||
.unwrap()
|
||||
.paint(bounds, &mut element_state, cx);
|
||||
((), element_state)
|
||||
});
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
_ => panic!("must call layout before paint"),
|
||||
}
|
||||
}
|
||||
|
||||
fn measure(
|
||||
&mut self,
|
||||
available_space: Size<AvailableSpace>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Size<Pixels> {
|
||||
if matches!(&self.phase, ElementDrawPhase::Start) {
|
||||
self.layout(cx);
|
||||
}
|
||||
|
||||
let layout_id = match &mut self.phase {
|
||||
ElementDrawPhase::LayoutRequested {
|
||||
layout_id,
|
||||
frame_state,
|
||||
} => {
|
||||
cx.compute_layout(*layout_id, available_space);
|
||||
let layout_id = *layout_id;
|
||||
self.phase = ElementDrawPhase::LayoutComputed {
|
||||
layout_id,
|
||||
available_space,
|
||||
frame_state: frame_state.take(),
|
||||
};
|
||||
layout_id
|
||||
}
|
||||
ElementDrawPhase::LayoutComputed {
|
||||
layout_id,
|
||||
available_space: prev_available_space,
|
||||
..
|
||||
} => {
|
||||
if available_space != *prev_available_space {
|
||||
cx.compute_layout(*layout_id, available_space);
|
||||
*prev_available_space = available_space;
|
||||
}
|
||||
*layout_id
|
||||
}
|
||||
_ => panic!("cannot measure after painting"),
|
||||
};
|
||||
|
||||
cx.layout_bounds(layout_id).size
|
||||
}
|
||||
|
||||
fn draw(
|
||||
mut self,
|
||||
origin: Point<Pixels>,
|
||||
available_space: Size<AvailableSpace>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Option<E::State> {
|
||||
self.measure(available_space, cx);
|
||||
cx.with_absolute_element_offset(origin, |cx| self.paint(cx))
|
||||
}
|
||||
}
|
||||
|
||||
// impl<V: 'static, E: Element> Element for DrawableElement<V, E> {
|
||||
// type State = <E::Element as Element>::State;
|
||||
|
||||
// fn layout(
|
||||
// &mut self,
|
||||
// element_state: Option<Self::State>,
|
||||
// cx: &mut WindowContext,
|
||||
// ) -> (LayoutId, Self::State) {
|
||||
|
||||
// }
|
||||
|
||||
// fn paint(
|
||||
// self,
|
||||
// bounds: Bounds<Pixels>,
|
||||
// element_state: &mut Self::State,
|
||||
// cx: &mut WindowContext,
|
||||
// ) {
|
||||
// todo!()
|
||||
// }
|
||||
// }
|
||||
|
||||
// impl<V: 'static, E: 'static + Element> RenderOnce for DrawableElement<V, E> {
|
||||
// type Element = Self;
|
||||
|
||||
// fn element_id(&self) -> Option<ElementId> {
|
||||
// self.element.as_ref()?.element_id()
|
||||
// }
|
||||
|
||||
// fn render_once(self) -> Self::Element {
|
||||
// self
|
||||
// }
|
||||
// }
|
||||
|
||||
impl<E> ElementObject for Option<DrawableElement<E>>
|
||||
where
|
||||
V: 'static,
|
||||
E: 'static + Component<V>,
|
||||
F: FnOnce(&mut V, &mut ViewContext<'_, V>) -> E + 'static,
|
||||
E: Element,
|
||||
E::State: 'static,
|
||||
{
|
||||
fn render(self) -> AnyElement<V> {
|
||||
fn element_id(&self) -> Option<ElementId> {
|
||||
self.as_ref().unwrap().element_id()
|
||||
}
|
||||
|
||||
fn layout(&mut self, cx: &mut WindowContext) -> LayoutId {
|
||||
DrawableElement::layout(self.as_mut().unwrap(), cx)
|
||||
}
|
||||
|
||||
fn paint(&mut self, cx: &mut WindowContext) {
|
||||
DrawableElement::paint(self.take().unwrap(), cx);
|
||||
}
|
||||
|
||||
fn measure(
|
||||
&mut self,
|
||||
available_space: Size<AvailableSpace>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Size<Pixels> {
|
||||
DrawableElement::measure(self.as_mut().unwrap(), available_space, cx)
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&mut self,
|
||||
origin: Point<Pixels>,
|
||||
available_space: Size<AvailableSpace>,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
DrawableElement::draw(self.take().unwrap(), origin, available_space, cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AnyElement(Box<dyn ElementObject>);
|
||||
|
||||
impl AnyElement {
|
||||
pub fn new<E>(element: E) -> Self
|
||||
where
|
||||
E: 'static + Element,
|
||||
E::State: Any,
|
||||
{
|
||||
AnyElement(Box::new(Some(DrawableElement::new(element))) as Box<dyn ElementObject>)
|
||||
}
|
||||
|
||||
pub fn layout(&mut self, cx: &mut WindowContext) -> LayoutId {
|
||||
self.0.layout(cx)
|
||||
}
|
||||
|
||||
pub fn paint(mut self, cx: &mut WindowContext) {
|
||||
self.0.paint(cx)
|
||||
}
|
||||
|
||||
/// Initializes this element and performs layout within the given available space to determine its size.
|
||||
pub fn measure(
|
||||
&mut self,
|
||||
available_space: Size<AvailableSpace>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Size<Pixels> {
|
||||
self.0.measure(available_space, cx)
|
||||
}
|
||||
|
||||
/// Initializes this element and performs layout in the available space, then paints it at the given origin.
|
||||
pub fn draw(
|
||||
mut self,
|
||||
origin: Point<Pixels>,
|
||||
available_space: Size<AvailableSpace>,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
self.0.draw(origin, available_space, cx)
|
||||
}
|
||||
|
||||
/// Converts this `AnyElement` into a trait object that can be stored and manipulated.
|
||||
pub fn into_any(self) -> AnyElement {
|
||||
AnyElement::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<V, E, F> Component<V> for F
|
||||
where
|
||||
V: 'static,
|
||||
E: 'static + Component<V>,
|
||||
F: FnOnce(&mut V, &mut ViewContext<'_, V>) -> E + 'static,
|
||||
{
|
||||
fn render(self) -> AnyElement<V> {
|
||||
AnyElement::new(Some(self))
|
||||
pub fn inner_id(&self) -> Option<ElementId> {
|
||||
self.0.element_id()
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for AnyElement {
|
||||
type State = ();
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
_: Option<Self::State>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::State) {
|
||||
let layout_id = self.layout(cx);
|
||||
(layout_id, ())
|
||||
}
|
||||
|
||||
fn paint(self, _: Bounds<Pixels>, _: &mut Self::State, cx: &mut WindowContext) {
|
||||
self.paint(cx);
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoElement for AnyElement {
|
||||
type Element = Self;
|
||||
|
||||
fn element_id(&self) -> Option<ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// impl<V, E, F> Element for Option<F>
|
||||
// where
|
||||
// V: 'static,
|
||||
// E: Element,
|
||||
// F: FnOnce(&mut V, &mut WindowContext<'_, V>) -> E + 'static,
|
||||
// {
|
||||
// type State = Option<AnyElement>;
|
||||
|
||||
// fn element_id(&self) -> Option<ElementId> {
|
||||
// None
|
||||
// }
|
||||
|
||||
// fn layout(
|
||||
// &mut self,
|
||||
// _: Option<Self::State>,
|
||||
// cx: &mut WindowContext,
|
||||
// ) -> (LayoutId, Self::State) {
|
||||
// let render = self.take().unwrap();
|
||||
// let mut element = (render)(view_state, cx).into_any();
|
||||
// let layout_id = element.layout(view_state, cx);
|
||||
// (layout_id, Some(element))
|
||||
// }
|
||||
|
||||
// fn paint(
|
||||
// self,
|
||||
// _bounds: Bounds<Pixels>,
|
||||
// rendered_element: &mut Self::State,
|
||||
// cx: &mut WindowContext,
|
||||
// ) {
|
||||
// rendered_element.take().unwrap().paint(view_state, cx);
|
||||
// }
|
||||
// }
|
||||
|
||||
// impl<V, E, F> RenderOnce for Option<F>
|
||||
// where
|
||||
// V: 'static,
|
||||
// E: Element,
|
||||
// F: FnOnce(&mut V, &mut WindowContext) -> E + 'static,
|
||||
// {
|
||||
// type Element = Self;
|
||||
|
||||
// fn render(self) -> Self::Element {
|
||||
// self
|
||||
// }
|
||||
// }
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,70 +1,83 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
AnyElement, BorrowWindow, Bounds, Component, Element, InteractiveComponent,
|
||||
InteractiveElementState, Interactivity, LayoutId, Pixels, SharedString, StyleRefinement,
|
||||
Styled, ViewContext,
|
||||
Bounds, Element, ImageData, InteractiveElement, InteractiveElementState, Interactivity,
|
||||
IntoElement, LayoutId, Pixels, SharedString, StyleRefinement, Styled, WindowContext,
|
||||
};
|
||||
use futures::FutureExt;
|
||||
use util::ResultExt;
|
||||
|
||||
pub struct Img<V: 'static> {
|
||||
interactivity: Interactivity<V>,
|
||||
uri: Option<SharedString>,
|
||||
#[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 {
|
||||
interactivity: Interactivity,
|
||||
source: Option<ImageSource>,
|
||||
grayscale: bool,
|
||||
}
|
||||
|
||||
pub fn img<V: 'static>() -> Img<V> {
|
||||
pub fn img() -> Img {
|
||||
Img {
|
||||
interactivity: Interactivity::default(),
|
||||
uri: None,
|
||||
source: None,
|
||||
grayscale: false,
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> Img<V>
|
||||
where
|
||||
V: 'static,
|
||||
{
|
||||
impl Img {
|
||||
pub fn uri(mut self, uri: impl Into<SharedString>) -> Self {
|
||||
self.uri = Some(uri.into());
|
||||
self.source = Some(ImageSource::from(uri.into()));
|
||||
self
|
||||
}
|
||||
pub fn data(mut self, data: Arc<ImageData>) -> Self {
|
||||
self.source = Some(ImageSource::from(data));
|
||||
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 {
|
||||
self.grayscale = grayscale;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> Component<V> for Img<V> {
|
||||
fn render(self) -> AnyElement<V> {
|
||||
AnyElement::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> Element<V> for Img<V> {
|
||||
type ElementState = InteractiveElementState;
|
||||
|
||||
fn element_id(&self) -> Option<crate::ElementId> {
|
||||
self.interactivity.element_id.clone()
|
||||
}
|
||||
impl Element for Img {
|
||||
type State = InteractiveElementState;
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
_view_state: &mut V,
|
||||
element_state: Option<Self::ElementState>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> (LayoutId, Self::ElementState) {
|
||||
element_state: Option<Self::State>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::State) {
|
||||
self.interactivity.layout(element_state, cx, |style, cx| {
|
||||
cx.request_layout(&style, None)
|
||||
})
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
self,
|
||||
bounds: Bounds<Pixels>,
|
||||
_view_state: &mut V,
|
||||
element_state: &mut Self::ElementState,
|
||||
cx: &mut ViewContext<V>,
|
||||
element_state: &mut Self::State,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
self.interactivity.paint(
|
||||
bounds,
|
||||
@ -74,42 +87,59 @@ impl<V> Element<V> for Img<V> {
|
||||
|style, _scroll_offset, cx| {
|
||||
let corner_radii = style.corner_radii;
|
||||
|
||||
if let Some(uri) = self.uri.clone() {
|
||||
// eprintln!(">>> image_cache.get({uri}");
|
||||
let image_future = cx.image_cache.get(uri.clone());
|
||||
// eprintln!("<<< image_cache.get({uri}");
|
||||
if let Some(data) = image_future
|
||||
.clone()
|
||||
.now_or_never()
|
||||
.and_then(|result| result.ok())
|
||||
{
|
||||
let corner_radii = corner_radii.to_pixels(bounds.size, cx.rem_size());
|
||||
cx.with_z_index(1, |cx| {
|
||||
cx.paint_image(bounds, corner_radii, data, self.grayscale)
|
||||
.log_err()
|
||||
});
|
||||
} else {
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
if image_future.await.ok().is_some() {
|
||||
cx.on_next_frame(|cx| cx.notify());
|
||||
if let Some(source) = self.source {
|
||||
let image = match source {
|
||||
ImageSource::Uri(uri) => {
|
||||
let image_future = cx.image_cache.get(uri.clone());
|
||||
if let Some(data) = image_future
|
||||
.clone()
|
||||
.now_or_never()
|
||||
.and_then(|result| result.ok())
|
||||
{
|
||||
data
|
||||
} else {
|
||||
cx.spawn(|mut cx| async move {
|
||||
if image_future.await.ok().is_some() {
|
||||
cx.on_next_frame(|cx| cx.notify());
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
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<V> Styled for Img<V> {
|
||||
impl IntoElement for Img {
|
||||
type Element = Self;
|
||||
|
||||
fn element_id(&self) -> Option<crate::ElementId> {
|
||||
self.interactivity.element_id.clone()
|
||||
}
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Styled for Img {
|
||||
fn style(&mut self) -> &mut StyleRefinement {
|
||||
&mut self.interactivity.base_style
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> InteractiveComponent<V> for Img<V> {
|
||||
fn interactivity(&mut self) -> &mut Interactivity<V> {
|
||||
impl InteractiveElement for Img {
|
||||
fn interactivity(&mut self) -> &mut Interactivity {
|
||||
&mut self.interactivity
|
||||
}
|
||||
}
|
||||
|
@ -2,16 +2,16 @@ use smallvec::SmallVec;
|
||||
use taffy::style::{Display, Position};
|
||||
|
||||
use crate::{
|
||||
point, AnyElement, BorrowWindow, Bounds, Component, Element, LayoutId, ParentComponent, Pixels,
|
||||
Point, Size, Style,
|
||||
point, AnyElement, BorrowWindow, Bounds, Element, IntoElement, LayoutId, ParentElement, Pixels,
|
||||
Point, Size, Style, WindowContext,
|
||||
};
|
||||
|
||||
pub struct OverlayState {
|
||||
child_layout_ids: SmallVec<[LayoutId; 4]>,
|
||||
}
|
||||
|
||||
pub struct Overlay<V> {
|
||||
children: SmallVec<[AnyElement<V>; 2]>,
|
||||
pub struct Overlay {
|
||||
children: SmallVec<[AnyElement; 2]>,
|
||||
anchor_corner: AnchorCorner,
|
||||
fit_mode: OverlayFitMode,
|
||||
// todo!();
|
||||
@ -21,7 +21,7 @@ pub struct Overlay<V> {
|
||||
|
||||
/// overlay gives you a floating element that will avoid overflowing the window bounds.
|
||||
/// Its children should have no margin to avoid measurement issues.
|
||||
pub fn overlay<V: 'static>() -> Overlay<V> {
|
||||
pub fn overlay() -> Overlay {
|
||||
Overlay {
|
||||
children: SmallVec::new(),
|
||||
anchor_corner: AnchorCorner::TopLeft,
|
||||
@ -30,7 +30,7 @@ pub fn overlay<V: 'static>() -> Overlay<V> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> Overlay<V> {
|
||||
impl Overlay {
|
||||
/// Sets which corner of the overlay should be anchored to the current position.
|
||||
pub fn anchor(mut self, anchor: AnchorCorner) -> Self {
|
||||
self.anchor_corner = anchor;
|
||||
@ -51,35 +51,24 @@ impl<V> Overlay<V> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: 'static> ParentComponent<V> for Overlay<V> {
|
||||
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
|
||||
impl ParentElement for Overlay {
|
||||
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
|
||||
&mut self.children
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: 'static> Component<V> for Overlay<V> {
|
||||
fn render(self) -> AnyElement<V> {
|
||||
AnyElement::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: 'static> Element<V> for Overlay<V> {
|
||||
type ElementState = OverlayState;
|
||||
|
||||
fn element_id(&self) -> Option<crate::ElementId> {
|
||||
None
|
||||
}
|
||||
impl Element for Overlay {
|
||||
type State = OverlayState;
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
view_state: &mut V,
|
||||
_: Option<Self::ElementState>,
|
||||
cx: &mut crate::ViewContext<V>,
|
||||
) -> (crate::LayoutId, Self::ElementState) {
|
||||
_: Option<Self::State>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (crate::LayoutId, Self::State) {
|
||||
let child_layout_ids = self
|
||||
.children
|
||||
.iter_mut()
|
||||
.map(|child| child.layout(view_state, cx))
|
||||
.map(|child| child.layout(cx))
|
||||
.collect::<SmallVec<_>>();
|
||||
|
||||
let mut overlay_style = Style::default();
|
||||
@ -92,11 +81,10 @@ impl<V: 'static> Element<V> for Overlay<V> {
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
self,
|
||||
bounds: crate::Bounds<crate::Pixels>,
|
||||
view_state: &mut V,
|
||||
element_state: &mut Self::ElementState,
|
||||
cx: &mut crate::ViewContext<V>,
|
||||
element_state: &mut Self::State,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
if element_state.child_layout_ids.is_empty() {
|
||||
return;
|
||||
@ -117,6 +105,7 @@ impl<V: 'static> Element<V> for Overlay<V> {
|
||||
origin: Point::zero(),
|
||||
size: cx.viewport_size(),
|
||||
};
|
||||
dbg!(limits);
|
||||
|
||||
match self.fit_mode {
|
||||
OverlayFitMode::SnapToWindow => {
|
||||
@ -156,13 +145,27 @@ impl<V: 'static> Element<V> for Overlay<V> {
|
||||
}
|
||||
|
||||
cx.with_element_offset(desired.origin - bounds.origin, |cx| {
|
||||
for child in &mut self.children {
|
||||
child.paint(view_state, cx);
|
||||
}
|
||||
cx.break_content_mask(|cx| {
|
||||
for child in self.children {
|
||||
child.paint(cx);
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoElement for Overlay {
|
||||
type Element = Self;
|
||||
|
||||
fn element_id(&self) -> Option<crate::ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
enum Axis {
|
||||
Horizontal,
|
||||
Vertical,
|
||||
|
@ -1,60 +1,43 @@
|
||||
use crate::{
|
||||
AnyElement, Bounds, Component, Element, ElementId, InteractiveComponent,
|
||||
InteractiveElementState, Interactivity, LayoutId, Pixels, SharedString, StyleRefinement,
|
||||
Styled, ViewContext,
|
||||
Bounds, Element, ElementId, InteractiveElement, InteractiveElementState, Interactivity,
|
||||
IntoElement, LayoutId, Pixels, SharedString, StyleRefinement, Styled, WindowContext,
|
||||
};
|
||||
use util::ResultExt;
|
||||
|
||||
pub struct Svg<V: 'static> {
|
||||
interactivity: Interactivity<V>,
|
||||
pub struct Svg {
|
||||
interactivity: Interactivity,
|
||||
path: Option<SharedString>,
|
||||
}
|
||||
|
||||
pub fn svg<V: 'static>() -> Svg<V> {
|
||||
pub fn svg() -> Svg {
|
||||
Svg {
|
||||
interactivity: Interactivity::default(),
|
||||
path: None,
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> Svg<V> {
|
||||
impl Svg {
|
||||
pub fn path(mut self, path: impl Into<SharedString>) -> Self {
|
||||
self.path = Some(path.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> Component<V> for Svg<V> {
|
||||
fn render(self) -> AnyElement<V> {
|
||||
AnyElement::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> Element<V> for Svg<V> {
|
||||
type ElementState = InteractiveElementState;
|
||||
|
||||
fn element_id(&self) -> Option<ElementId> {
|
||||
self.interactivity.element_id.clone()
|
||||
}
|
||||
impl Element for Svg {
|
||||
type State = InteractiveElementState;
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
_view_state: &mut V,
|
||||
element_state: Option<Self::ElementState>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> (LayoutId, Self::ElementState) {
|
||||
element_state: Option<Self::State>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::State) {
|
||||
self.interactivity.layout(element_state, cx, |style, cx| {
|
||||
cx.request_layout(&style, None)
|
||||
})
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
_view_state: &mut V,
|
||||
element_state: &mut Self::ElementState,
|
||||
cx: &mut ViewContext<V>,
|
||||
) where
|
||||
fn paint(self, bounds: Bounds<Pixels>, element_state: &mut Self::State, cx: &mut WindowContext)
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.interactivity
|
||||
@ -66,14 +49,26 @@ impl<V> Element<V> for Svg<V> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> Styled for Svg<V> {
|
||||
impl IntoElement for Svg {
|
||||
type Element = Self;
|
||||
|
||||
fn element_id(&self) -> Option<ElementId> {
|
||||
self.interactivity.element_id.clone()
|
||||
}
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Styled for Svg {
|
||||
fn style(&mut self) -> &mut StyleRefinement {
|
||||
&mut self.interactivity.base_style
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> InteractiveComponent<V> for Svg<V> {
|
||||
fn interactivity(&mut self) -> &mut Interactivity<V> {
|
||||
impl InteractiveElement for Svg {
|
||||
fn interactivity(&mut self) -> &mut Interactivity {
|
||||
&mut self.interactivity
|
||||
}
|
||||
}
|
||||
|
@ -1,74 +1,177 @@
|
||||
use crate::{
|
||||
AnyElement, BorrowWindow, Bounds, Component, Element, ElementId, LayoutId, Pixels,
|
||||
SharedString, Size, TextRun, ViewContext, WrappedLine,
|
||||
Bounds, DispatchPhase, Element, ElementId, IntoElement, LayoutId, MouseDownEvent, MouseUpEvent,
|
||||
Pixels, Point, SharedString, Size, TextRun, WhiteSpace, WindowContext, WrappedLine,
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
use parking_lot::{Mutex, MutexGuard};
|
||||
use smallvec::SmallVec;
|
||||
use std::{cell::Cell, rc::Rc, sync::Arc};
|
||||
use std::{cell::Cell, ops::Range, rc::Rc, sync::Arc};
|
||||
use util::ResultExt;
|
||||
|
||||
pub struct Text {
|
||||
impl Element for &'static str {
|
||||
type State = TextState;
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
_: Option<Self::State>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::State) {
|
||||
let mut state = TextState::default();
|
||||
let layout_id = state.layout(SharedString::from(*self), None, cx);
|
||||
(layout_id, state)
|
||||
}
|
||||
|
||||
fn paint(self, bounds: Bounds<Pixels>, state: &mut TextState, cx: &mut WindowContext) {
|
||||
state.paint(bounds, self, cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoElement for &'static str {
|
||||
type Element = Self;
|
||||
|
||||
fn element_id(&self) -> Option<ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for SharedString {
|
||||
type State = TextState;
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
_: Option<Self::State>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::State) {
|
||||
let mut state = TextState::default();
|
||||
let layout_id = state.layout(self.clone(), None, cx);
|
||||
(layout_id, state)
|
||||
}
|
||||
|
||||
fn paint(self, bounds: Bounds<Pixels>, state: &mut TextState, cx: &mut WindowContext) {
|
||||
let text_str: &str = self.as_ref();
|
||||
state.paint(bounds, text_str, cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoElement for SharedString {
|
||||
type Element = Self;
|
||||
|
||||
fn element_id(&self) -> Option<ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
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 {
|
||||
text: SharedString,
|
||||
runs: Option<Vec<TextRun>>,
|
||||
}
|
||||
|
||||
impl Text {
|
||||
/// Renders text with runs of different styles.
|
||||
///
|
||||
/// Callers are responsible for setting the correct style for each run.
|
||||
/// For text with a uniform style, you can usually avoid calling this constructor
|
||||
/// and just pass text directly.
|
||||
pub fn styled(text: SharedString, runs: Vec<TextRun>) -> Self {
|
||||
Text {
|
||||
text,
|
||||
runs: Some(runs),
|
||||
impl StyledText {
|
||||
pub fn new(text: impl Into<SharedString>) -> Self {
|
||||
StyledText {
|
||||
text: text.into(),
|
||||
runs: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: 'static> Component<V> for Text {
|
||||
fn render(self) -> AnyElement<V> {
|
||||
AnyElement::new(self)
|
||||
pub fn with_runs(mut self, runs: Vec<TextRun>) -> Self {
|
||||
self.runs = Some(runs);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: 'static> Element<V> for Text {
|
||||
type ElementState = TextState;
|
||||
impl Element for StyledText {
|
||||
type State = TextState;
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
_: Option<Self::State>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::State) {
|
||||
let mut state = TextState::default();
|
||||
let layout_id = state.layout(self.text.clone(), self.runs.take(), cx);
|
||||
(layout_id, state)
|
||||
}
|
||||
|
||||
fn paint(self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
|
||||
state.paint(bounds, &self.text, cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoElement for StyledText {
|
||||
type Element = Self;
|
||||
|
||||
fn element_id(&self) -> Option<crate::ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct TextState(Arc<Mutex<Option<TextStateInner>>>);
|
||||
|
||||
struct TextStateInner {
|
||||
lines: SmallVec<[WrappedLine; 1]>,
|
||||
line_height: Pixels,
|
||||
wrap_width: Option<Pixels>,
|
||||
size: Option<Size<Pixels>>,
|
||||
}
|
||||
|
||||
impl TextState {
|
||||
fn lock(&self) -> MutexGuard<Option<TextStateInner>> {
|
||||
self.0.lock()
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
_view: &mut V,
|
||||
element_state: Option<Self::ElementState>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> (LayoutId, Self::ElementState) {
|
||||
let element_state = element_state.unwrap_or_default();
|
||||
text: SharedString,
|
||||
runs: Option<Vec<TextRun>>,
|
||||
cx: &mut WindowContext,
|
||||
) -> LayoutId {
|
||||
let text_system = cx.text_system().clone();
|
||||
let text_style = cx.text_style();
|
||||
let font_size = text_style.font_size.to_pixels(cx.rem_size());
|
||||
let line_height = text_style
|
||||
.line_height
|
||||
.to_pixels(font_size.into(), cx.rem_size());
|
||||
let text = self.text.clone();
|
||||
let text = SharedString::from(text);
|
||||
|
||||
let rem_size = cx.rem_size();
|
||||
|
||||
let runs = if let Some(runs) = self.runs.take() {
|
||||
let runs = if let Some(runs) = runs {
|
||||
runs
|
||||
} else {
|
||||
vec![text_style.to_run(text.len())]
|
||||
};
|
||||
|
||||
let layout_id = cx.request_measured_layout(Default::default(), rem_size, {
|
||||
let element_state = element_state.clone();
|
||||
let element_state = self.clone();
|
||||
|
||||
move |known_dimensions, available_space| {
|
||||
let wrap_width = known_dimensions.width.or(match available_space.width {
|
||||
crate::AvailableSpace::Definite(x) => Some(x),
|
||||
_ => None,
|
||||
});
|
||||
let wrap_width = if text_style.white_space == WhiteSpace::Normal {
|
||||
known_dimensions.width.or(match available_space.width {
|
||||
crate::AvailableSpace::Definite(x) => Some(x),
|
||||
_ => None,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(text_state) = element_state.0.lock().as_ref() {
|
||||
if text_state.size.is_some()
|
||||
@ -80,10 +183,7 @@ impl<V: 'static> Element<V> for Text {
|
||||
|
||||
let Some(lines) = text_system
|
||||
.shape_text(
|
||||
&text,
|
||||
font_size,
|
||||
&runs[..],
|
||||
wrap_width, // Wrap if we know the width.
|
||||
&text, font_size, &runs, wrap_width, // Wrap if we know the width.
|
||||
)
|
||||
.log_err()
|
||||
else {
|
||||
@ -100,7 +200,7 @@ impl<V: 'static> Element<V> for Text {
|
||||
for line in &lines {
|
||||
let line_size = line.size(line_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 {
|
||||
@ -114,20 +214,14 @@ impl<V: 'static> Element<V> for Text {
|
||||
}
|
||||
});
|
||||
|
||||
(layout_id, element_state)
|
||||
layout_id
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
_: &mut V,
|
||||
element_state: &mut Self::ElementState,
|
||||
cx: &mut ViewContext<V>,
|
||||
) {
|
||||
let element_state = element_state.lock();
|
||||
fn paint(&mut self, bounds: Bounds<Pixels>, text: &str, cx: &mut WindowContext) {
|
||||
let element_state = self.lock();
|
||||
let element_state = element_state
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("measurement has not been performed on {}", &self.text))
|
||||
.ok_or_else(|| anyhow!("measurement has not been performed on {}", text))
|
||||
.unwrap();
|
||||
|
||||
let line_height = element_state.line_height;
|
||||
@ -137,108 +231,156 @@ impl<V: 'static> Element<V> for Text {
|
||||
line_origin.y += line.size(line_height).height;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct TextState(Arc<Mutex<Option<TextStateInner>>>);
|
||||
fn index_for_position(&self, bounds: Bounds<Pixels>, position: Point<Pixels>) -> Option<usize> {
|
||||
if !bounds.contains_point(&position) {
|
||||
return None;
|
||||
}
|
||||
|
||||
impl TextState {
|
||||
fn lock(&self) -> MutexGuard<Option<TextStateInner>> {
|
||||
self.0.lock()
|
||||
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 TextStateInner {
|
||||
lines: SmallVec<[WrappedLine; 1]>,
|
||||
line_height: Pixels,
|
||||
wrap_width: Option<Pixels>,
|
||||
size: Option<Size<Pixels>>,
|
||||
pub struct InteractiveText {
|
||||
element_id: ElementId,
|
||||
text: StyledText,
|
||||
click_listener: Option<Box<dyn Fn(InteractiveTextClickEvent, &mut WindowContext<'_>)>>,
|
||||
}
|
||||
|
||||
struct InteractiveText {
|
||||
id: ElementId,
|
||||
text: Text,
|
||||
struct InteractiveTextClickEvent {
|
||||
mouse_down_index: usize,
|
||||
mouse_up_index: usize,
|
||||
}
|
||||
|
||||
struct InteractiveTextState {
|
||||
pub struct InteractiveTextState {
|
||||
text_state: TextState,
|
||||
clicked_range_ixs: Rc<Cell<SmallVec<[usize; 1]>>>,
|
||||
mouse_down_index: Rc<Cell<Option<usize>>>,
|
||||
}
|
||||
|
||||
impl<V: 'static> Element<V> for InteractiveText {
|
||||
type ElementState = InteractiveTextState;
|
||||
|
||||
fn element_id(&self) -> Option<ElementId> {
|
||||
Some(self.id.clone())
|
||||
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 {
|
||||
type State = InteractiveTextState;
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
view_state: &mut V,
|
||||
element_state: Option<Self::ElementState>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> (LayoutId, Self::ElementState) {
|
||||
state: Option<Self::State>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::State) {
|
||||
if let Some(InteractiveTextState {
|
||||
text_state,
|
||||
clicked_range_ixs,
|
||||
}) = element_state
|
||||
mouse_down_index, ..
|
||||
}) = state
|
||||
{
|
||||
let (layout_id, text_state) = self.text.layout(view_state, Some(text_state), cx);
|
||||
let (layout_id, text_state) = self.text.layout(None, cx);
|
||||
let element_state = InteractiveTextState {
|
||||
text_state,
|
||||
clicked_range_ixs,
|
||||
mouse_down_index,
|
||||
};
|
||||
(layout_id, element_state)
|
||||
} else {
|
||||
let (layout_id, text_state) = self.text.layout(view_state, None, cx);
|
||||
let (layout_id, text_state) = self.text.layout(None, cx);
|
||||
let element_state = InteractiveTextState {
|
||||
text_state,
|
||||
clicked_range_ixs: Rc::default(),
|
||||
mouse_down_index: Rc::default(),
|
||||
};
|
||||
(layout_id, element_state)
|
||||
}
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
view_state: &mut V,
|
||||
element_state: &mut Self::ElementState,
|
||||
cx: &mut ViewContext<V>,
|
||||
) {
|
||||
self.text
|
||||
.paint(bounds, view_state, &mut element_state.text_state, cx)
|
||||
fn paint(self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: 'static> Component<V> for SharedString {
|
||||
fn render(self) -> AnyElement<V> {
|
||||
Text {
|
||||
text: self,
|
||||
runs: None,
|
||||
}
|
||||
.render()
|
||||
}
|
||||
}
|
||||
impl IntoElement for InteractiveText {
|
||||
type Element = Self;
|
||||
|
||||
impl<V: 'static> Component<V> for &'static str {
|
||||
fn render(self) -> AnyElement<V> {
|
||||
Text {
|
||||
text: self.into(),
|
||||
runs: None,
|
||||
}
|
||||
.render()
|
||||
fn element_id(&self) -> Option<ElementId> {
|
||||
Some(self.element_id.clone())
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Figure out how to pass `String` to `child` without this.
|
||||
// This impl doesn't exist in the `gpui2` crate.
|
||||
impl<V: 'static> Component<V> for String {
|
||||
fn render(self) -> AnyElement<V> {
|
||||
Text {
|
||||
text: self.into(),
|
||||
runs: None,
|
||||
}
|
||||
.render()
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
@ -1,61 +1,60 @@
|
||||
use crate::{
|
||||
point, px, size, AnyElement, AvailableSpace, BorrowWindow, Bounds, Component, Element,
|
||||
ElementId, InteractiveComponent, InteractiveElementState, Interactivity, LayoutId, Pixels,
|
||||
Point, Size, StyleRefinement, Styled, ViewContext,
|
||||
point, px, size, AnyElement, AvailableSpace, BorrowWindow, Bounds, ContentMask, Element,
|
||||
ElementId, InteractiveElement, InteractiveElementState, Interactivity, IntoElement, LayoutId,
|
||||
Pixels, Point, Render, Size, StyleRefinement, Styled, View, ViewContext, WindowContext,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use std::{cell::RefCell, cmp, mem, ops::Range, rc::Rc};
|
||||
use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
|
||||
use taffy::style::Overflow;
|
||||
|
||||
/// uniform_list provides lazy rendering for a set of items that are of uniform height.
|
||||
/// When rendered into a container with overflow-y: hidden and a fixed (or max) height,
|
||||
/// uniform_list will only render the visibile subset of items.
|
||||
pub fn uniform_list<I, V, C>(
|
||||
/// uniform_list will only render the visible subset of items.
|
||||
pub fn uniform_list<I, R, V>(
|
||||
view: View<V>,
|
||||
id: I,
|
||||
item_count: usize,
|
||||
f: impl 'static + Fn(&mut V, Range<usize>, &mut ViewContext<V>) -> Vec<C>,
|
||||
) -> UniformList<V>
|
||||
f: impl 'static + Fn(&mut V, Range<usize>, &mut ViewContext<V>) -> Vec<R>,
|
||||
) -> UniformList
|
||||
where
|
||||
I: Into<ElementId>,
|
||||
V: 'static,
|
||||
C: Component<V>,
|
||||
R: IntoElement,
|
||||
V: Render,
|
||||
{
|
||||
let id = id.into();
|
||||
let mut style = StyleRefinement::default();
|
||||
style.overflow.y = Some(Overflow::Hidden);
|
||||
let mut base_style = StyleRefinement::default();
|
||||
base_style.overflow.y = Some(Overflow::Scroll);
|
||||
|
||||
let render_range = move |range, cx: &mut WindowContext| {
|
||||
view.update(cx, |this, cx| {
|
||||
f(this, range, cx)
|
||||
.into_iter()
|
||||
.map(|component| component.into_any_element())
|
||||
.collect()
|
||||
})
|
||||
};
|
||||
|
||||
UniformList {
|
||||
id: id.clone(),
|
||||
style,
|
||||
item_count,
|
||||
item_to_measure_index: 0,
|
||||
render_items: Box::new(move |view, visible_range, cx| {
|
||||
f(view, visible_range, cx)
|
||||
.into_iter()
|
||||
.map(|component| component.render())
|
||||
.collect()
|
||||
}),
|
||||
render_items: Box::new(render_range),
|
||||
interactivity: Interactivity {
|
||||
element_id: Some(id.into()),
|
||||
base_style,
|
||||
..Default::default()
|
||||
},
|
||||
scroll_handle: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UniformList<V: 'static> {
|
||||
pub struct UniformList {
|
||||
id: ElementId,
|
||||
style: StyleRefinement,
|
||||
item_count: usize,
|
||||
item_to_measure_index: usize,
|
||||
render_items: Box<
|
||||
dyn for<'a> Fn(
|
||||
&'a mut V,
|
||||
Range<usize>,
|
||||
&'a mut ViewContext<V>,
|
||||
) -> SmallVec<[AnyElement<V>; 64]>,
|
||||
>,
|
||||
interactivity: Interactivity<V>,
|
||||
render_items:
|
||||
Box<dyn for<'a> Fn(Range<usize>, &'a mut WindowContext) -> SmallVec<[AnyElement; 64]>>,
|
||||
interactivity: Interactivity,
|
||||
scroll_handle: Option<UniformListScrollHandle>,
|
||||
}
|
||||
|
||||
@ -89,9 +88,9 @@ impl UniformListScrollHandle {
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: 'static> Styled for UniformList<V> {
|
||||
impl Styled for UniformList {
|
||||
fn style(&mut self) -> &mut StyleRefinement {
|
||||
&mut self.style
|
||||
&mut self.interactivity.base_style
|
||||
}
|
||||
}
|
||||
|
||||
@ -101,29 +100,24 @@ pub struct UniformListState {
|
||||
item_size: Size<Pixels>,
|
||||
}
|
||||
|
||||
impl<V: 'static> Element<V> for UniformList<V> {
|
||||
type ElementState = UniformListState;
|
||||
|
||||
fn element_id(&self) -> Option<crate::ElementId> {
|
||||
Some(self.id.clone())
|
||||
}
|
||||
impl Element for UniformList {
|
||||
type State = UniformListState;
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
view_state: &mut V,
|
||||
element_state: Option<Self::ElementState>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> (LayoutId, Self::ElementState) {
|
||||
state: Option<Self::State>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::State) {
|
||||
let max_items = self.item_count;
|
||||
let rem_size = cx.rem_size();
|
||||
let item_size = element_state
|
||||
let item_size = state
|
||||
.as_ref()
|
||||
.map(|s| s.item_size)
|
||||
.unwrap_or_else(|| self.measure_item(view_state, None, cx));
|
||||
.unwrap_or_else(|| self.measure_item(None, cx));
|
||||
|
||||
let (layout_id, interactive) =
|
||||
self.interactivity
|
||||
.layout(element_state.map(|s| s.interactive), cx, |style, cx| {
|
||||
.layout(state.map(|s| s.interactive), cx, |style, cx| {
|
||||
cx.request_measured_layout(
|
||||
style,
|
||||
rem_size,
|
||||
@ -159,11 +153,10 @@ impl<V: 'static> Element<V> for UniformList<V> {
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
self,
|
||||
bounds: Bounds<crate::Pixels>,
|
||||
view_state: &mut V,
|
||||
element_state: &mut Self::ElementState,
|
||||
cx: &mut ViewContext<V>,
|
||||
element_state: &mut Self::State,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
let style =
|
||||
self.interactivity
|
||||
@ -183,14 +176,15 @@ impl<V: 'static> Element<V> for UniformList<V> {
|
||||
height: item_size.height * self.item_count,
|
||||
};
|
||||
|
||||
let mut interactivity = mem::take(&mut self.interactivity);
|
||||
let shared_scroll_offset = element_state
|
||||
.interactive
|
||||
.scroll_offset
|
||||
.get_or_insert_with(Rc::default)
|
||||
.clone();
|
||||
|
||||
interactivity.paint(
|
||||
let item_height = self.measure_item(Some(padded_bounds.size.width), cx).height;
|
||||
|
||||
self.interactivity.paint(
|
||||
bounds,
|
||||
content_size,
|
||||
&mut element_state.interactive,
|
||||
@ -209,9 +203,6 @@ impl<V: 'static> Element<V> for UniformList<V> {
|
||||
style.paint(bounds, cx);
|
||||
|
||||
if self.item_count > 0 {
|
||||
let item_height = self
|
||||
.measure_item(view_state, Some(padded_bounds.size.width), cx)
|
||||
.height;
|
||||
if let Some(scroll_handle) = self.scroll_handle.clone() {
|
||||
scroll_handle.0.borrow_mut().replace(ScrollHandleState {
|
||||
item_height,
|
||||
@ -219,58 +210,64 @@ impl<V: 'static> Element<V> for UniformList<V> {
|
||||
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 =
|
||||
(-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
|
||||
..cmp::min(
|
||||
first_visible_element_ix + visible_item_count,
|
||||
self.item_count,
|
||||
);
|
||||
..cmp::min(last_visible_element_ix, self.item_count);
|
||||
|
||||
let mut items = (self.render_items)(view_state, visible_range.clone(), cx);
|
||||
let items = (self.render_items)(visible_range.clone(), cx);
|
||||
cx.with_z_index(1, |cx| {
|
||||
for (item, ix) in items.iter_mut().zip(visible_range) {
|
||||
let item_origin = padded_bounds.origin
|
||||
+ point(px(0.), item_height * ix + scroll_offset.y);
|
||||
let available_space = size(
|
||||
AvailableSpace::Definite(padded_bounds.size.width),
|
||||
AvailableSpace::Definite(item_height),
|
||||
);
|
||||
item.draw(item_origin, available_space, view_state, cx);
|
||||
}
|
||||
let content_mask = ContentMask {
|
||||
bounds: padded_bounds,
|
||||
};
|
||||
cx.with_content_mask(Some(content_mask), |cx| {
|
||||
for (item, ix) in items.into_iter().zip(visible_range) {
|
||||
let item_origin = padded_bounds.origin
|
||||
+ point(px(0.), item_height * ix + scroll_offset.y);
|
||||
let available_space = size(
|
||||
AvailableSpace::Definite(padded_bounds.size.width),
|
||||
AvailableSpace::Definite(item_height),
|
||||
);
|
||||
item.draw(item_origin, available_space, cx);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
||||
},
|
||||
);
|
||||
self.interactivity = interactivity;
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> UniformList<V> {
|
||||
impl IntoElement for UniformList {
|
||||
type Element = Self;
|
||||
|
||||
fn element_id(&self) -> Option<crate::ElementId> {
|
||||
Some(self.id.clone())
|
||||
}
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl UniformList {
|
||||
pub fn with_width_from_item(mut self, item_index: Option<usize>) -> Self {
|
||||
self.item_to_measure_index = item_index.unwrap_or(0);
|
||||
self
|
||||
}
|
||||
|
||||
fn measure_item(
|
||||
&self,
|
||||
view_state: &mut V,
|
||||
list_width: Option<Pixels>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> Size<Pixels> {
|
||||
fn measure_item(&self, list_width: Option<Pixels>, cx: &mut WindowContext) -> Size<Pixels> {
|
||||
if self.item_count == 0 {
|
||||
return Size::default();
|
||||
}
|
||||
|
||||
let item_ix = cmp::min(self.item_to_measure_index, self.item_count - 1);
|
||||
let mut items = (self.render_items)(view_state, item_ix..item_ix + 1, cx);
|
||||
let mut items = (self.render_items)(item_ix..item_ix + 1, cx);
|
||||
let mut item_to_measure = items.pop().unwrap();
|
||||
let available_space = size(
|
||||
list_width.map_or(AvailableSpace::MinContent, |width| {
|
||||
@ -278,7 +275,7 @@ impl<V> UniformList<V> {
|
||||
}),
|
||||
AvailableSpace::MinContent,
|
||||
);
|
||||
item_to_measure.measure(available_space, view_state, cx)
|
||||
item_to_measure.measure(available_space, cx)
|
||||
}
|
||||
|
||||
pub fn track_scroll(mut self, handle: UniformListScrollHandle) -> Self {
|
||||
@ -287,14 +284,8 @@ impl<V> UniformList<V> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> InteractiveComponent<V> for UniformList<V> {
|
||||
fn interactivity(&mut self) -> &mut crate::Interactivity<V> {
|
||||
impl InteractiveElement for UniformList {
|
||||
fn interactivity(&mut self) -> &mut crate::Interactivity {
|
||||
&mut self.interactivity
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: 'static> Component<V> for UniformList<V> {
|
||||
fn render(self) -> AnyElement<V> {
|
||||
AnyElement::new(self)
|
||||
}
|
||||
}
|
||||
|
@ -78,8 +78,6 @@ use std::{
|
||||
};
|
||||
use taffy::TaffyLayoutEngine;
|
||||
|
||||
type AnyBox = Box<dyn Any>;
|
||||
|
||||
pub trait Context {
|
||||
type Result<T>;
|
||||
|
||||
@ -136,7 +134,7 @@ pub trait VisualContext: Context {
|
||||
build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V,
|
||||
) -> Self::Result<View<V>>
|
||||
where
|
||||
V: Render;
|
||||
V: 'static + Render;
|
||||
|
||||
fn focus_view<V>(&mut self, view: &View<V>) -> Self::Result<()>
|
||||
where
|
||||
|
@ -1,4 +1,6 @@
|
||||
use crate::{AsyncWindowContext, Bounds, Pixels, PlatformInputHandler, View, ViewContext};
|
||||
use crate::{
|
||||
AsyncWindowContext, Bounds, Pixels, PlatformInputHandler, View, ViewContext, WindowContext,
|
||||
};
|
||||
use std::ops::Range;
|
||||
|
||||
/// Implement this trait to allow views to handle textual input when implementing an editor, field, etc.
|
||||
@ -43,9 +45,9 @@ pub struct ElementInputHandler<V> {
|
||||
impl<V: 'static> ElementInputHandler<V> {
|
||||
/// Used in [Element::paint] with the element's bounds and a view context for its
|
||||
/// containing view.
|
||||
pub fn new(element_bounds: Bounds<Pixels>, cx: &mut ViewContext<V>) -> Self {
|
||||
pub fn new(element_bounds: Bounds<Pixels>, view: View<V>, cx: &mut WindowContext) -> Self {
|
||||
ElementInputHandler {
|
||||
view: cx.view().clone(),
|
||||
view,
|
||||
element_bounds,
|
||||
cx: cx.to_async(),
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
div, point, Component, Div, FocusHandle, Keystroke, Modifiers, Pixels, Point, Render,
|
||||
ViewContext,
|
||||
div, point, Div, Element, FocusHandle, IntoElement, Keystroke, Modifiers, Pixels, Point,
|
||||
Render, ViewContext,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use std::{any::Any, fmt::Debug, marker::PhantomData, ops::Deref, path::PathBuf};
|
||||
@ -64,24 +64,24 @@ pub struct Drag<S, R, V, E>
|
||||
where
|
||||
R: Fn(&mut V, &mut ViewContext<V>) -> E,
|
||||
V: 'static,
|
||||
E: Component<()>,
|
||||
E: IntoElement,
|
||||
{
|
||||
pub state: S,
|
||||
pub render_drag_handle: R,
|
||||
view_type: PhantomData<V>,
|
||||
view_element_types: PhantomData<(V, E)>,
|
||||
}
|
||||
|
||||
impl<S, R, V, E> Drag<S, R, V, E>
|
||||
where
|
||||
R: Fn(&mut V, &mut ViewContext<V>) -> E,
|
||||
V: 'static,
|
||||
E: Component<()>,
|
||||
E: Element,
|
||||
{
|
||||
pub fn new(state: S, render_drag_handle: R) -> Self {
|
||||
Drag {
|
||||
state,
|
||||
render_drag_handle,
|
||||
view_type: PhantomData,
|
||||
view_element_types: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -194,7 +194,7 @@ impl Deref for MouseExitEvent {
|
||||
pub struct ExternalPaths(pub(crate) SmallVec<[PathBuf; 2]>);
|
||||
|
||||
impl Render for ExternalPaths {
|
||||
type Element = Div<Self>;
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, _: &mut ViewContext<Self>) -> Self::Element {
|
||||
div() // Intentionally left empty because the platform will render icons for the dragged files
|
||||
@ -286,8 +286,8 @@ pub struct FocusEvent {
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{
|
||||
self as gpui, div, Component, Div, FocusHandle, InteractiveComponent, KeyBinding,
|
||||
Keystroke, ParentComponent, Render, Stateful, TestAppContext, ViewContext, VisualContext,
|
||||
self as gpui, div, Div, FocusHandle, InteractiveElement, IntoElement, KeyBinding,
|
||||
Keystroke, ParentElement, Render, Stateful, TestAppContext, VisualContext,
|
||||
};
|
||||
|
||||
struct TestView {
|
||||
@ -299,20 +299,24 @@ mod test {
|
||||
actions!(TestAction);
|
||||
|
||||
impl Render for TestView {
|
||||
type Element = Stateful<Self, Div<Self>>;
|
||||
type Element = Stateful<Div>;
|
||||
|
||||
fn render(&mut self, _: &mut gpui::ViewContext<Self>) -> Self::Element {
|
||||
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element {
|
||||
div().id("testview").child(
|
||||
div()
|
||||
.key_context("parent")
|
||||
.on_key_down(|this: &mut TestView, _, _, _| this.saw_key_down = true)
|
||||
.on_action(|this: &mut TestView, _: &TestAction, _| this.saw_action = true)
|
||||
.child(|this: &mut Self, _cx: &mut ViewContext<Self>| {
|
||||
.on_key_down(cx.listener(|this, _, _| this.saw_key_down = true))
|
||||
.on_action(
|
||||
cx.listener(|this: &mut TestView, _: &TestAction, _| {
|
||||
this.saw_action = true
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.key_context("nested")
|
||||
.track_focus(&this.focus_handle)
|
||||
.render()
|
||||
}),
|
||||
.track_focus(&self.focus_handle)
|
||||
.into_element(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -683,6 +683,9 @@ impl Drop for MacWindow {
|
||||
this.executor
|
||||
.spawn(async move {
|
||||
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();
|
||||
}
|
||||
})
|
||||
|
@ -1,4 +1,5 @@
|
||||
pub use crate::{
|
||||
BorrowAppContext, BorrowWindow, Component, Context, FocusableComponent, InteractiveComponent,
|
||||
ParentComponent, Refineable, Render, StatefulInteractiveComponent, Styled, VisualContext,
|
||||
BorrowAppContext, BorrowWindow, Context, Element, FocusableElement, InteractiveElement,
|
||||
IntoElement, ParentElement, Refineable, Render, RenderOnce, StatefulInteractiveElement, Styled,
|
||||
VisualContext,
|
||||
};
|
||||
|
@ -1,9 +1,12 @@
|
||||
use std::{iter, mem, ops::Range};
|
||||
|
||||
use crate::{
|
||||
black, phi, point, rems, AbsoluteLength, BorrowAppContext, BorrowWindow, Bounds, ContentMask,
|
||||
Corners, CornersRefinement, CursorStyle, DefiniteLength, Edges, EdgesRefinement, Font,
|
||||
FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Rgba,
|
||||
SharedString, Size, SizeRefinement, Styled, TextRun, ViewContext,
|
||||
SharedString, Size, SizeRefinement, Styled, TextRun, WindowContext,
|
||||
};
|
||||
use collections::HashSet;
|
||||
use refineable::{Cascade, Refineable};
|
||||
use smallvec::SmallVec;
|
||||
pub use taffy::style::{
|
||||
@ -128,6 +131,13 @@ pub struct BoxShadow {
|
||||
pub spread_radius: Pixels,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub enum WhiteSpace {
|
||||
#[default]
|
||||
Normal,
|
||||
Nowrap,
|
||||
}
|
||||
|
||||
#[derive(Refineable, Clone, Debug)]
|
||||
#[refineable(Debug)]
|
||||
pub struct TextStyle {
|
||||
@ -138,7 +148,9 @@ pub struct TextStyle {
|
||||
pub line_height: DefiniteLength,
|
||||
pub font_weight: FontWeight,
|
||||
pub font_style: FontStyle,
|
||||
pub background_color: Option<Hsla>,
|
||||
pub underline: Option<UnderlineStyle>,
|
||||
pub white_space: WhiteSpace,
|
||||
}
|
||||
|
||||
impl Default for TextStyle {
|
||||
@ -151,13 +163,16 @@ impl Default for TextStyle {
|
||||
line_height: phi(),
|
||||
font_weight: FontWeight::default(),
|
||||
font_style: FontStyle::default(),
|
||||
background_color: None,
|
||||
underline: None,
|
||||
white_space: WhiteSpace::Normal,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
self.font_weight = weight;
|
||||
}
|
||||
@ -173,6 +188,10 @@ impl TextStyle {
|
||||
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 {
|
||||
self.underline = Some(underline);
|
||||
}
|
||||
@ -203,7 +222,7 @@ impl TextStyle {
|
||||
style: self.font_style,
|
||||
},
|
||||
color: self.color,
|
||||
background_color: None,
|
||||
background_color: self.background_color,
|
||||
underline: self.underline.clone(),
|
||||
}
|
||||
}
|
||||
@ -214,6 +233,7 @@ pub struct HighlightStyle {
|
||||
pub color: Option<Hsla>,
|
||||
pub font_weight: Option<FontWeight>,
|
||||
pub font_style: Option<FontStyle>,
|
||||
pub background_color: Option<Hsla>,
|
||||
pub underline: Option<UnderlineStyle>,
|
||||
pub fade_out: Option<f32>,
|
||||
}
|
||||
@ -313,7 +333,7 @@ impl Style {
|
||||
}
|
||||
|
||||
/// Paints the background of an element styled with this style.
|
||||
pub fn paint<V: 'static>(&self, bounds: Bounds<Pixels>, cx: &mut ViewContext<V>) {
|
||||
pub fn paint(&self, bounds: Bounds<Pixels>, cx: &mut WindowContext) {
|
||||
let rem_size = cx.rem_size();
|
||||
|
||||
cx.with_z_index(0, |cx| {
|
||||
@ -432,6 +452,7 @@ impl From<&TextStyle> for HighlightStyle {
|
||||
color: Some(other.color),
|
||||
font_weight: Some(other.font_weight),
|
||||
font_style: Some(other.font_style),
|
||||
background_color: other.background_color,
|
||||
underline: other.underline.clone(),
|
||||
fade_out: None,
|
||||
}
|
||||
@ -458,6 +479,10 @@ impl HighlightStyle {
|
||||
self.font_style = other.font_style;
|
||||
}
|
||||
|
||||
if other.background_color.is_some() {
|
||||
self.background_color = other.background_color;
|
||||
}
|
||||
|
||||
if other.underline.is_some() {
|
||||
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 {
|
||||
fn from(color: Rgba) -> 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::{
|
||||
self as gpui, hsla, point, px, relative, rems, AbsoluteLength, AlignItems, CursorStyle,
|
||||
DefiniteLength, Display, Fill, FlexDirection, Hsla, JustifyContent, Length, Position,
|
||||
SharedString, StyleRefinement, Visibility,
|
||||
SharedString, StyleRefinement, Visibility, WhiteSpace,
|
||||
};
|
||||
use crate::{BoxShadow, TextStyleRefinement};
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
@ -101,6 +101,24 @@ pub trait Styled: Sized {
|
||||
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`.
|
||||
/// [Docs](https://tailwindcss.com/docs/flex-direction#column)
|
||||
fn flex_col(mut self) -> Self {
|
||||
@ -343,6 +361,13 @@ pub trait Styled: Sized {
|
||||
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 {
|
||||
self.text_style()
|
||||
.get_or_insert_with(Default::default)
|
||||
|
@ -196,7 +196,10 @@ impl TextSystem {
|
||||
let mut decoration_runs = SmallVec::<[DecorationRun; 32]>::new();
|
||||
for run in runs {
|
||||
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;
|
||||
continue;
|
||||
}
|
||||
@ -204,6 +207,7 @@ impl TextSystem {
|
||||
decoration_runs.push(DecorationRun {
|
||||
len: run.len as u32,
|
||||
color: run.color,
|
||||
background_color: run.background_color,
|
||||
underline: run.underline.clone(),
|
||||
});
|
||||
}
|
||||
@ -254,13 +258,16 @@ impl TextSystem {
|
||||
}
|
||||
|
||||
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;
|
||||
} else {
|
||||
decoration_runs.push(DecorationRun {
|
||||
len: run_len_within_line as u32,
|
||||
color: run.color,
|
||||
background_color: run.background_color,
|
||||
underline: run.underline.clone(),
|
||||
});
|
||||
}
|
||||
@ -283,7 +290,15 @@ impl TextSystem {
|
||||
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();
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
use crate::{
|
||||
black, point, px, BorrowWindow, Bounds, Hsla, LineLayout, Pixels, Point, Result, SharedString,
|
||||
UnderlineStyle, WindowContext, WrapBoundary, WrappedLineLayout,
|
||||
black, point, px, size, transparent_black, BorrowWindow, Bounds, Corners, Edges, Hsla,
|
||||
LineLayout, Pixels, Point, Result, SharedString, UnderlineStyle, WindowContext, WrapBoundary,
|
||||
WrappedLineLayout,
|
||||
};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use smallvec::SmallVec;
|
||||
@ -10,6 +11,7 @@ use std::sync::Arc;
|
||||
pub struct DecorationRun {
|
||||
pub len: u32,
|
||||
pub color: Hsla,
|
||||
pub background_color: Option<Hsla>,
|
||||
pub underline: Option<UnderlineStyle>,
|
||||
}
|
||||
|
||||
@ -38,7 +40,6 @@ impl ShapedLine {
|
||||
&self.layout,
|
||||
line_height,
|
||||
&self.decoration_runs,
|
||||
None,
|
||||
&[],
|
||||
cx,
|
||||
)?;
|
||||
@ -72,7 +73,6 @@ impl WrappedLine {
|
||||
&self.layout.unwrapped_layout,
|
||||
line_height,
|
||||
&self.decoration_runs,
|
||||
self.wrap_width,
|
||||
&self.wrap_boundaries,
|
||||
cx,
|
||||
)?;
|
||||
@ -86,7 +86,6 @@ fn paint_line(
|
||||
layout: &LineLayout,
|
||||
line_height: Pixels,
|
||||
decoration_runs: &[DecorationRun],
|
||||
wrap_width: Option<Pixels>,
|
||||
wrap_boundaries: &[WrapBoundary],
|
||||
cx: &mut WindowContext<'_>,
|
||||
) -> Result<()> {
|
||||
@ -97,6 +96,7 @@ fn paint_line(
|
||||
let mut run_end = 0;
|
||||
let mut color = black();
|
||||
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 mut glyph_origin = origin;
|
||||
let mut prev_glyph_position = Point::default();
|
||||
@ -110,12 +110,28 @@ fn paint_line(
|
||||
|
||||
if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) {
|
||||
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(
|
||||
underline_origin,
|
||||
*underline_origin,
|
||||
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;
|
||||
@ -123,9 +139,20 @@ fn paint_line(
|
||||
}
|
||||
prev_glyph_position = glyph.position;
|
||||
|
||||
let mut finished_background: Option<(Point<Pixels>, Hsla)> = None;
|
||||
let mut finished_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
|
||||
if glyph.index >= run_end {
|
||||
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 style_run.underline.as_ref() != Some(underline_style) {
|
||||
finished_underline = current_underline.take();
|
||||
@ -135,7 +162,7 @@ fn paint_line(
|
||||
current_underline.get_or_insert((
|
||||
point(
|
||||
glyph_origin.x,
|
||||
origin.y + baseline_offset.y + (layout.descent * 0.618),
|
||||
glyph_origin.y + baseline_offset.y + (layout.descent * 0.618),
|
||||
),
|
||||
UnderlineStyle {
|
||||
color: Some(run_underline.color.unwrap_or(style_run.color)),
|
||||
@ -149,16 +176,30 @@ fn paint_line(
|
||||
color = style_run.color;
|
||||
} else {
|
||||
run_end = layout.len;
|
||||
finished_background = current_background.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 {
|
||||
cx.paint_underline(
|
||||
underline_origin,
|
||||
glyph_origin.x - underline_origin.x,
|
||||
&underline_style,
|
||||
)?;
|
||||
);
|
||||
}
|
||||
|
||||
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() {
|
||||
let line_end_x = origin.x + wrap_width.unwrap_or(Pixels::MAX).min(layout.width);
|
||||
cx.paint_underline(
|
||||
underline_start,
|
||||
line_end_x - underline_start.x,
|
||||
last_line_end_x - underline_start.x,
|
||||
&underline_style,
|
||||
)?;
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
@ -198,6 +198,41 @@ impl WrappedLineLayout {
|
||||
pub fn runs(&self) -> &[ShapedRun] {
|
||||
&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 {
|
||||
|
@ -1,23 +1,17 @@
|
||||
use crate::{
|
||||
private::Sealed, AnyBox, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace,
|
||||
BorrowWindow, Bounds, Component, Element, ElementId, Entity, EntityId, Flatten, FocusHandle,
|
||||
FocusableView, LayoutId, Model, Pixels, Point, Size, ViewContext, VisualContext, WeakModel,
|
||||
private::Sealed, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace, BorrowWindow,
|
||||
Bounds, Element, ElementId, Entity, EntityId, Flatten, FocusHandle, FocusableView, IntoElement,
|
||||
LayoutId, Model, Pixels, Point, Render, Size, ViewContext, VisualContext, WeakModel,
|
||||
WindowContext,
|
||||
};
|
||||
use anyhow::{Context, Result};
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
any::TypeId,
|
||||
hash::{Hash, Hasher},
|
||||
};
|
||||
|
||||
pub trait Render: 'static + Sized {
|
||||
type Element: Element<Self> + 'static;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element;
|
||||
}
|
||||
|
||||
pub struct View<V> {
|
||||
pub(crate) model: Model<V>,
|
||||
pub model: Model<V>,
|
||||
}
|
||||
|
||||
impl<V> Sealed for View<V> {}
|
||||
@ -65,15 +59,15 @@ impl<V: 'static> View<V> {
|
||||
self.model.read(cx)
|
||||
}
|
||||
|
||||
pub fn render_with<C>(&self, component: C) -> RenderViewWith<C, V>
|
||||
where
|
||||
C: 'static + Component<V>,
|
||||
{
|
||||
RenderViewWith {
|
||||
view: self.clone(),
|
||||
component: Some(component),
|
||||
}
|
||||
}
|
||||
// pub fn render_with<E>(&self, component: E) -> RenderViewWith<E, V>
|
||||
// where
|
||||
// E: 'static + Element,
|
||||
// {
|
||||
// RenderViewWith {
|
||||
// view: self.clone(),
|
||||
// element: Some(component),
|
||||
// }
|
||||
// }
|
||||
|
||||
pub fn focus_handle(&self, cx: &AppContext) -> FocusHandle
|
||||
where
|
||||
@ -83,6 +77,24 @@ impl<V: 'static> View<V> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: Render> Element for View<V> {
|
||||
type State = Option<AnyElement>;
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
_state: Option<Self::State>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::State) {
|
||||
let mut element = self.update(cx, |view, cx| view.render(cx).into_any());
|
||||
let layout_id = element.layout(cx);
|
||||
(layout_id, Some(element))
|
||||
}
|
||||
|
||||
fn paint(self, _: Bounds<Pixels>, element: &mut Self::State, cx: &mut WindowContext) {
|
||||
element.take().unwrap().paint(cx);
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> Clone for View<V> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
@ -105,12 +117,6 @@ impl<V> PartialEq for View<V> {
|
||||
|
||||
impl<V> Eq for View<V> {}
|
||||
|
||||
impl<V: Render, ParentViewState: 'static> Component<ParentViewState> for View<V> {
|
||||
fn render(self) -> AnyElement<ParentViewState> {
|
||||
AnyElement::new(AnyView::from(self))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WeakView<V> {
|
||||
pub(crate) model: WeakModel<V>,
|
||||
}
|
||||
@ -163,8 +169,8 @@ impl<V> Eq for WeakView<V> {}
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AnyView {
|
||||
model: AnyModel,
|
||||
layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, Box<dyn Any>),
|
||||
paint: fn(&AnyView, &mut AnyBox, &mut WindowContext),
|
||||
layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, AnyElement),
|
||||
paint: fn(&AnyView, AnyElement, &mut WindowContext),
|
||||
}
|
||||
|
||||
impl AnyView {
|
||||
@ -202,21 +208,15 @@ impl AnyView {
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
cx.with_absolute_element_offset(origin, |cx| {
|
||||
let (layout_id, mut rendered_element) = (self.layout)(self, cx);
|
||||
let (layout_id, rendered_element) = (self.layout)(self, cx);
|
||||
cx.window
|
||||
.layout_engine
|
||||
.compute_layout(layout_id, available_space);
|
||||
(self.paint)(self, &mut rendered_element, cx);
|
||||
(self.paint)(self, rendered_element, cx);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: 'static> Component<V> for AnyView {
|
||||
fn render(self) -> AnyElement<V> {
|
||||
AnyElement::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: Render> From<View<V>> for AnyView {
|
||||
fn from(value: View<V>) -> Self {
|
||||
AnyView {
|
||||
@ -227,37 +227,51 @@ impl<V: Render> From<View<V>> for AnyView {
|
||||
}
|
||||
}
|
||||
|
||||
impl<ParentViewState: 'static> Element<ParentViewState> for AnyView {
|
||||
type ElementState = Box<dyn Any>;
|
||||
|
||||
fn element_id(&self) -> Option<ElementId> {
|
||||
Some(self.model.entity_id.into())
|
||||
}
|
||||
impl Element for AnyView {
|
||||
type State = Option<AnyElement>;
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
_view_state: &mut ParentViewState,
|
||||
_element_state: Option<Self::ElementState>,
|
||||
cx: &mut ViewContext<ParentViewState>,
|
||||
) -> (LayoutId, Self::ElementState) {
|
||||
(self.layout)(self, cx)
|
||||
_state: Option<Self::State>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::State) {
|
||||
let (layout_id, state) = (self.layout)(self, cx);
|
||||
(layout_id, Some(state))
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_bounds: Bounds<Pixels>,
|
||||
_view_state: &mut ParentViewState,
|
||||
rendered_element: &mut Self::ElementState,
|
||||
cx: &mut ViewContext<ParentViewState>,
|
||||
) {
|
||||
(self.paint)(self, rendered_element, cx)
|
||||
fn paint(self, _: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
|
||||
(self.paint)(&self, state.take().unwrap(), cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: 'static + Render> IntoElement for View<V> {
|
||||
type Element = View<V>;
|
||||
|
||||
fn element_id(&self) -> Option<ElementId> {
|
||||
Some(ElementId::from_entity_id(self.model.entity_id))
|
||||
}
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoElement for AnyView {
|
||||
type Element = Self;
|
||||
|
||||
fn element_id(&self) -> Option<ElementId> {
|
||||
Some(ElementId::from_entity_id(self.model.entity_id))
|
||||
}
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AnyWeakView {
|
||||
model: AnyWeakModel,
|
||||
layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, Box<dyn Any>),
|
||||
paint: fn(&AnyView, &mut AnyBox, &mut WindowContext),
|
||||
layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, AnyElement),
|
||||
paint: fn(&AnyView, AnyElement, &mut WindowContext),
|
||||
}
|
||||
|
||||
impl AnyWeakView {
|
||||
@ -271,7 +285,7 @@ impl AnyWeakView {
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: Render> From<WeakView<V>> for AnyWeakView {
|
||||
impl<V: 'static + Render> From<WeakView<V>> for AnyWeakView {
|
||||
fn from(view: WeakView<V>) -> Self {
|
||||
Self {
|
||||
model: view.model.into(),
|
||||
@ -281,97 +295,36 @@ impl<V: Render> From<WeakView<V>> for AnyWeakView {
|
||||
}
|
||||
}
|
||||
|
||||
// impl<T, E> Render for T
|
||||
// where
|
||||
// T: 'static + FnMut(&mut WindowContext) -> E,
|
||||
// E: 'static + Send + Element<T>,
|
||||
// {
|
||||
// type Element = E;
|
||||
|
||||
// fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
// (self)(cx)
|
||||
// }
|
||||
// }
|
||||
|
||||
pub struct RenderViewWith<C, V> {
|
||||
view: View<V>,
|
||||
component: Option<C>,
|
||||
}
|
||||
|
||||
impl<C, ParentViewState, ViewState> Component<ParentViewState> for RenderViewWith<C, ViewState>
|
||||
impl<T, E> Render for T
|
||||
where
|
||||
C: 'static + Component<ViewState>,
|
||||
ParentViewState: 'static,
|
||||
ViewState: 'static,
|
||||
T: 'static + FnMut(&mut WindowContext) -> E,
|
||||
E: 'static + Send + Element,
|
||||
{
|
||||
fn render(self) -> AnyElement<ParentViewState> {
|
||||
AnyElement::new(self)
|
||||
}
|
||||
}
|
||||
type Element = E;
|
||||
|
||||
impl<C, ParentViewState, ViewState> Element<ParentViewState> for RenderViewWith<C, ViewState>
|
||||
where
|
||||
C: 'static + Component<ViewState>,
|
||||
ParentViewState: 'static,
|
||||
ViewState: 'static,
|
||||
{
|
||||
type ElementState = AnyElement<ViewState>;
|
||||
|
||||
fn element_id(&self) -> Option<ElementId> {
|
||||
Some(self.view.entity_id().into())
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
_: &mut ParentViewState,
|
||||
_: Option<Self::ElementState>,
|
||||
cx: &mut ViewContext<ParentViewState>,
|
||||
) -> (LayoutId, Self::ElementState) {
|
||||
self.view.update(cx, |view, cx| {
|
||||
let mut element = self.component.take().unwrap().render();
|
||||
let layout_id = element.layout(view, cx);
|
||||
(layout_id, element)
|
||||
})
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_: Bounds<Pixels>,
|
||||
_: &mut ParentViewState,
|
||||
element: &mut Self::ElementState,
|
||||
cx: &mut ViewContext<ParentViewState>,
|
||||
) {
|
||||
self.view.update(cx, |view, cx| element.paint(view, cx))
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
(self)(cx)
|
||||
}
|
||||
}
|
||||
|
||||
mod any_view {
|
||||
use crate::{AnyElement, AnyView, BorrowWindow, LayoutId, Render, WindowContext};
|
||||
use std::any::Any;
|
||||
use crate::{AnyElement, AnyView, Element, LayoutId, Render, WindowContext};
|
||||
|
||||
pub(crate) fn layout<V: Render>(
|
||||
pub(crate) fn layout<V: 'static + Render>(
|
||||
view: &AnyView,
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Box<dyn Any>) {
|
||||
cx.with_element_id(Some(view.model.entity_id), |cx| {
|
||||
let view = view.clone().downcast::<V>().unwrap();
|
||||
view.update(cx, |view, cx| {
|
||||
let mut element = AnyElement::new(view.render(cx));
|
||||
let layout_id = element.layout(view, cx);
|
||||
(layout_id, Box::new(element) as Box<dyn Any>)
|
||||
})
|
||||
})
|
||||
) -> (LayoutId, AnyElement) {
|
||||
let view = view.clone().downcast::<V>().unwrap();
|
||||
let mut element = view.update(cx, |view, cx| view.render(cx).into_any());
|
||||
let layout_id = element.layout(cx);
|
||||
(layout_id, element)
|
||||
}
|
||||
|
||||
pub(crate) fn paint<V: Render>(
|
||||
view: &AnyView,
|
||||
element: &mut Box<dyn Any>,
|
||||
pub(crate) fn paint<V: 'static + Render>(
|
||||
_view: &AnyView,
|
||||
element: AnyElement,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
cx.with_element_id(Some(view.model.entity_id), |cx| {
|
||||
let view = view.clone().downcast::<V>().unwrap();
|
||||
let element = element.downcast_mut::<AnyElement<V>>().unwrap();
|
||||
view.update(cx, |view, cx| element.paint(view, cx))
|
||||
})
|
||||
element.paint(cx);
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
key_dispatch::DispatchActionListener, px, size, Action, AnyBox, AnyDrag, AnyView, AppContext,
|
||||
key_dispatch::DispatchActionListener, px, size, Action, AnyDrag, AnyView, AppContext,
|
||||
AsyncWindowContext, AvailableSpace, Bounds, BoxShadow, Context, Corners, CursorStyle,
|
||||
DevicePixels, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId,
|
||||
EventEmitter, FileDropEvent, Flatten, FocusEvent, FontId, GlobalElementId, GlyphId, Hsla,
|
||||
@ -187,17 +187,17 @@ impl Drop for FocusHandle {
|
||||
|
||||
/// FocusableView allows users of your view to easily
|
||||
/// focus it (using cx.focus_view(view))
|
||||
pub trait FocusableView: Render {
|
||||
pub trait FocusableView: 'static + Render {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle;
|
||||
}
|
||||
|
||||
/// ManagedView is a view (like a Modal, Popover, Menu, etc.)
|
||||
/// where the lifecycle of the view is handled by another view.
|
||||
pub trait ManagedView: 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,
|
||||
}
|
||||
|
||||
@ -230,9 +230,15 @@ pub struct Window {
|
||||
pub(crate) focus: Option<FocusId>,
|
||||
}
|
||||
|
||||
pub(crate) struct ElementStateBox {
|
||||
inner: Box<dyn Any>,
|
||||
#[cfg(debug_assertions)]
|
||||
type_name: &'static str,
|
||||
}
|
||||
|
||||
// #[derive(Default)]
|
||||
pub(crate) struct Frame {
|
||||
pub(crate) element_states: HashMap<GlobalElementId, AnyBox>,
|
||||
pub(crate) element_states: HashMap<GlobalElementId, ElementStateBox>,
|
||||
mouse_listeners: HashMap<TypeId, Vec<(StackingOrder, AnyMouseListener)>>,
|
||||
pub(crate) dispatch_tree: DispatchTree,
|
||||
pub(crate) focus_listeners: Vec<AnyFocusListener>,
|
||||
@ -875,7 +881,7 @@ impl<'a> WindowContext<'a> {
|
||||
origin: Point<Pixels>,
|
||||
width: Pixels,
|
||||
style: &UnderlineStyle,
|
||||
) -> Result<()> {
|
||||
) {
|
||||
let scale_factor = self.scale_factor();
|
||||
let height = if style.wavy {
|
||||
style.thickness * 3.
|
||||
@ -899,7 +905,6 @@ impl<'a> WindowContext<'a> {
|
||||
wavy: style.wavy,
|
||||
},
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Paint a monochrome (non-emoji) glyph into the scene for the current frame at the current z-index.
|
||||
@ -1436,6 +1441,89 @@ impl<'a> WindowContext<'a> {
|
||||
.dispatch_tree
|
||||
.bindings_for_action(action)
|
||||
}
|
||||
|
||||
pub fn listener_for<V: Render, E>(
|
||||
&self,
|
||||
view: &View<V>,
|
||||
f: impl Fn(&mut V, &E, &mut ViewContext<V>) + 'static,
|
||||
) -> impl Fn(&E, &mut WindowContext) + 'static {
|
||||
let view = view.downgrade();
|
||||
move |e: &E, cx: &mut WindowContext| {
|
||||
view.update(cx, |view, cx| f(view, e, cx)).ok();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn constructor_for<V: Render, R>(
|
||||
&self,
|
||||
view: &View<V>,
|
||||
f: impl Fn(&mut V, &mut ViewContext<V>) -> R + 'static,
|
||||
) -> impl Fn(&mut WindowContext) -> R + 'static {
|
||||
let view = view.clone();
|
||||
move |cx: &mut WindowContext| view.update(cx, |view, cx| f(view, cx))
|
||||
}
|
||||
|
||||
//========== ELEMENT RELATED FUNCTIONS ===========
|
||||
pub fn with_key_dispatch<R>(
|
||||
&mut self,
|
||||
context: KeyContext,
|
||||
focus_handle: Option<FocusHandle>,
|
||||
f: impl FnOnce(Option<FocusHandle>, &mut Self) -> R,
|
||||
) -> R {
|
||||
let window = &mut self.window;
|
||||
window
|
||||
.current_frame
|
||||
.dispatch_tree
|
||||
.push_node(context.clone());
|
||||
if let Some(focus_handle) = focus_handle.as_ref() {
|
||||
window
|
||||
.current_frame
|
||||
.dispatch_tree
|
||||
.make_focusable(focus_handle.id);
|
||||
}
|
||||
let result = f(focus_handle, self);
|
||||
|
||||
self.window.current_frame.dispatch_tree.pop_node();
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Register a focus listener for the current frame only. It will be cleared
|
||||
/// on the next frame render. You should use this method only from within elements,
|
||||
/// and we may want to enforce that better via a different context type.
|
||||
// todo!() Move this to `FrameContext` to emphasize its individuality?
|
||||
pub fn on_focus_changed(
|
||||
&mut self,
|
||||
listener: impl Fn(&FocusEvent, &mut WindowContext) + 'static,
|
||||
) {
|
||||
self.window
|
||||
.current_frame
|
||||
.focus_listeners
|
||||
.push(Box::new(move |event, cx| {
|
||||
listener(event, cx);
|
||||
}));
|
||||
}
|
||||
|
||||
/// Set an input handler, such as [ElementInputHandler], which interfaces with the
|
||||
/// platform to receive textual input with proper integration with concerns such
|
||||
/// as IME interactions.
|
||||
pub fn handle_input(
|
||||
&mut self,
|
||||
focus_handle: &FocusHandle,
|
||||
input_handler: impl PlatformInputHandler,
|
||||
) {
|
||||
if focus_handle.is_focused(self) {
|
||||
self.window
|
||||
.platform_window
|
||||
.set_input_handler(Box::new(input_handler));
|
||||
}
|
||||
}
|
||||
|
||||
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<'_> {
|
||||
@ -1559,7 +1647,7 @@ impl VisualContext for WindowContext<'_> {
|
||||
build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V,
|
||||
) -> Self::Result<View<V>>
|
||||
where
|
||||
V: Render,
|
||||
V: 'static + Render,
|
||||
{
|
||||
let slot = self.app.entities.reserve();
|
||||
let view = View {
|
||||
@ -1582,7 +1670,7 @@ impl VisualContext for WindowContext<'_> {
|
||||
where
|
||||
V: ManagedView,
|
||||
{
|
||||
self.update_view(view, |_, cx| cx.emit(Manager::Dismiss))
|
||||
self.update_view(view, |_, cx| cx.emit(DismissEvent::Dismiss))
|
||||
}
|
||||
}
|
||||
|
||||
@ -1617,6 +1705,10 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
|
||||
self.borrow_mut()
|
||||
}
|
||||
|
||||
fn app(&self) -> &AppContext {
|
||||
self.borrow()
|
||||
}
|
||||
|
||||
fn window(&self) -> &Window {
|
||||
self.borrow()
|
||||
}
|
||||
@ -1667,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
|
||||
/// scrolling.
|
||||
fn with_element_offset<R>(
|
||||
@ -1735,10 +1845,37 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
|
||||
.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.
|
||||
let mut state_box = any
|
||||
let mut state_box = inner
|
||||
.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
|
||||
.take()
|
||||
.expect("element state is already on the stack");
|
||||
@ -1747,14 +1884,27 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
|
||||
cx.window_mut()
|
||||
.current_frame
|
||||
.element_states
|
||||
.insert(global_id, state_box);
|
||||
.insert(global_id, ElementStateBox {
|
||||
inner: state_box,
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
type_name
|
||||
});
|
||||
result
|
||||
} else {
|
||||
let (result, state) = f(None, cx);
|
||||
cx.window_mut()
|
||||
.current_frame
|
||||
.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
|
||||
}
|
||||
})
|
||||
@ -2124,49 +2274,6 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
||||
)
|
||||
}
|
||||
|
||||
/// Register a focus listener for the current frame only. It will be cleared
|
||||
/// on the next frame render. You should use this method only from within elements,
|
||||
/// and we may want to enforce that better via a different context type.
|
||||
// todo!() Move this to `FrameContext` to emphasize its individuality?
|
||||
pub fn on_focus_changed(
|
||||
&mut self,
|
||||
listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + 'static,
|
||||
) {
|
||||
let handle = self.view().downgrade();
|
||||
self.window
|
||||
.current_frame
|
||||
.focus_listeners
|
||||
.push(Box::new(move |event, cx| {
|
||||
handle
|
||||
.update(cx, |view, cx| listener(view, event, cx))
|
||||
.log_err();
|
||||
}));
|
||||
}
|
||||
|
||||
pub fn with_key_dispatch<R>(
|
||||
&mut self,
|
||||
context: KeyContext,
|
||||
focus_handle: Option<FocusHandle>,
|
||||
f: impl FnOnce(Option<FocusHandle>, &mut Self) -> R,
|
||||
) -> R {
|
||||
let window = &mut self.window;
|
||||
window
|
||||
.current_frame
|
||||
.dispatch_tree
|
||||
.push_node(context.clone());
|
||||
if let Some(focus_handle) = focus_handle.as_ref() {
|
||||
window
|
||||
.current_frame
|
||||
.dispatch_tree
|
||||
.make_focusable(focus_handle.id);
|
||||
}
|
||||
let result = f(focus_handle, self);
|
||||
|
||||
self.window.current_frame.dispatch_tree.pop_node();
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
pub fn spawn<Fut, R>(
|
||||
&mut self,
|
||||
f: impl FnOnce(WeakView<V>, AsyncWindowContext) -> Fut,
|
||||
@ -2243,21 +2350,6 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
||||
});
|
||||
}
|
||||
|
||||
/// Set an input handler, such as [ElementInputHandler], which interfaces with the
|
||||
/// platform to receive textual input with proper integration with concerns such
|
||||
/// as IME interactions.
|
||||
pub fn handle_input(
|
||||
&mut self,
|
||||
focus_handle: &FocusHandle,
|
||||
input_handler: impl PlatformInputHandler,
|
||||
) {
|
||||
if focus_handle.is_focused(self) {
|
||||
self.window
|
||||
.platform_window
|
||||
.set_input_handler(Box::new(input_handler));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn emit<Evt>(&mut self, event: Evt)
|
||||
where
|
||||
Evt: 'static,
|
||||
@ -2282,7 +2374,17 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
||||
where
|
||||
V: ManagedView,
|
||||
{
|
||||
self.defer(|_, cx| cx.emit(Manager::Dismiss))
|
||||
self.defer(|_, cx| cx.emit(DismissEvent::Dismiss))
|
||||
}
|
||||
|
||||
pub fn listener<E>(
|
||||
&self,
|
||||
f: impl Fn(&mut V, &E, &mut ViewContext<V>) + 'static,
|
||||
) -> impl Fn(&E, &mut WindowContext) + 'static {
|
||||
let view = self.view().downgrade();
|
||||
move |e: &E, cx: &mut WindowContext| {
|
||||
view.update(cx, |view, cx| f(view, e, cx)).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2355,7 +2457,7 @@ impl<V: 'static> VisualContext for ViewContext<'_, V> {
|
||||
build_view: impl FnOnce(&mut ViewContext<'_, W>) -> W,
|
||||
) -> Self::Result<View<W>>
|
||||
where
|
||||
W: Render,
|
||||
W: 'static + Render,
|
||||
{
|
||||
self.window_cx.replace_root_view(build_view)
|
||||
}
|
||||
@ -2567,6 +2669,12 @@ pub enum ElementId {
|
||||
FocusHandle(FocusId),
|
||||
}
|
||||
|
||||
impl ElementId {
|
||||
pub(crate) fn from_entity_id(entity_id: EntityId) -> Self {
|
||||
ElementId::View(entity_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryInto<SharedString> for ElementId {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
@ -2579,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 {
|
||||
fn from(id: usize) -> Self {
|
||||
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
|
||||
}
|
||||
}
|
27
crates/gpui2_macros/src/derive_into_element.rs
Normal file
27
crates/gpui2_macros/src/derive_into_element.rs
Normal file
@ -0,0 +1,27 @@
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::{parse_macro_input, DeriveInput};
|
||||
|
||||
pub fn derive_into_element(input: TokenStream) -> TokenStream {
|
||||
let ast = parse_macro_input!(input as DeriveInput);
|
||||
let type_name = &ast.ident;
|
||||
let (impl_generics, type_generics, where_clause) = ast.generics.split_for_impl();
|
||||
|
||||
let gen = quote! {
|
||||
impl #impl_generics gpui::IntoElement for #type_name #type_generics
|
||||
#where_clause
|
||||
{
|
||||
type Element = gpui::Component<Self>;
|
||||
|
||||
fn element_id(&self) -> Option<ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
gpui::Component::new(self)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
gen.into()
|
||||
}
|
@ -1,16 +1,11 @@
|
||||
mod action;
|
||||
mod derive_component;
|
||||
mod derive_into_element;
|
||||
mod register_action;
|
||||
mod style_helpers;
|
||||
mod test;
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
|
||||
#[proc_macro]
|
||||
pub fn style_helpers(args: TokenStream) -> TokenStream {
|
||||
style_helpers::style_helpers(args)
|
||||
}
|
||||
|
||||
#[proc_macro_derive(Action)]
|
||||
pub fn action(input: TokenStream) -> TokenStream {
|
||||
action::action(input)
|
||||
@ -21,9 +16,14 @@ pub fn register_action(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
register_action::register_action_macro(attr, item)
|
||||
}
|
||||
|
||||
#[proc_macro_derive(Component, attributes(component))]
|
||||
pub fn derive_component(input: TokenStream) -> TokenStream {
|
||||
derive_component::derive_component(input)
|
||||
#[proc_macro_derive(IntoElement)]
|
||||
pub fn derive_into_element(input: TokenStream) -> TokenStream {
|
||||
derive_into_element::derive_into_element(input)
|
||||
}
|
||||
|
||||
#[proc_macro]
|
||||
pub fn style_helpers(input: TokenStream) -> TokenStream {
|
||||
style_helpers::style_helpers(input)
|
||||
}
|
||||
|
||||
#[proc_macro_attribute]
|
||||
|
@ -11,7 +11,7 @@ pub struct HighlightId(pub u32);
|
||||
const DEFAULT_SYNTAX_HIGHLIGHT_ID: HighlightId = HighlightId(u32::MAX);
|
||||
|
||||
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
|
||||
// key in the theme's syntax styles that matches all of the
|
||||
// dot-separated components of the capture name.
|
||||
@ -98,9 +98,9 @@ mod tests {
|
||||
);
|
||||
|
||||
let capture_names = &[
|
||||
"function.special".to_string(),
|
||||
"function.async.rust".to_string(),
|
||||
"variable.builtin.self".to_string(),
|
||||
"function.special",
|
||||
"function.async.rust",
|
||||
"variable.builtin.self",
|
||||
];
|
||||
|
||||
let map = HighlightMap::new(capture_names, &theme);
|
||||
|
@ -1383,7 +1383,7 @@ impl Language {
|
||||
let query = Query::new(self.grammar_mut().ts_language, source)?;
|
||||
|
||||
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('_') {
|
||||
let value = self.config.overrides.remove(name).unwrap_or_default();
|
||||
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<_>>();
|
||||
for capture in captures {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ pub use crate::{
|
||||
use crate::{
|
||||
diagnostic_set::{DiagnosticEntry, DiagnosticGroup},
|
||||
language_settings::{language_settings, LanguageSettings},
|
||||
markdown::parse_markdown,
|
||||
outline::OutlineItem,
|
||||
syntax_map::{
|
||||
SyntaxLayerInfo, SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures, SyntaxMapMatches,
|
||||
@ -155,12 +156,52 @@ pub struct Diagnostic {
|
||||
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)]
|
||||
pub struct Completion {
|
||||
pub old_range: Range<Anchor>,
|
||||
pub new_text: String,
|
||||
pub label: CodeLabel,
|
||||
pub server_id: LanguageServerId,
|
||||
pub documentation: Option<Documentation>,
|
||||
pub lsp_completion: lsp::CompletionItem,
|
||||
}
|
||||
|
||||
|
@ -11,7 +11,7 @@ pub struct HighlightId(pub u32);
|
||||
const DEFAULT_SYNTAX_HIGHLIGHT_ID: HighlightId = HighlightId(u32::MAX);
|
||||
|
||||
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
|
||||
// key in the theme's syntax styles that matches all of the
|
||||
// dot-separated components of the capture name.
|
||||
@ -100,9 +100,9 @@ mod tests {
|
||||
};
|
||||
|
||||
let capture_names = &[
|
||||
"function.special".to_string(),
|
||||
"function.async.rust".to_string(),
|
||||
"variable.builtin.self".to_string(),
|
||||
"function.special",
|
||||
"function.async.rust",
|
||||
"variable.builtin.self",
|
||||
];
|
||||
|
||||
let map = HighlightMap::new(capture_names, &theme);
|
||||
|
@ -1391,7 +1391,7 @@ impl Language {
|
||||
let mut override_configs_by_id = HashMap::default();
|
||||
for (ix, name) in query.capture_names().iter().enumerate() {
|
||||
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 {
|
||||
if !self
|
||||
.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(),
|
||||
)
|
||||
}),
|
||||
documentation: None,
|
||||
server_id: LanguageServerId(completion.server_id as usize),
|
||||
lsp_completion,
|
||||
})
|
||||
|
@ -1300,7 +1300,7 @@ fn assert_capture_ranges(
|
||||
.collect::<Vec<_>>();
|
||||
for capture in captures {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
@ -61,12 +61,14 @@ fn build_bridge(swift_target: &SwiftTarget) {
|
||||
|
||||
let swift_package_root = swift_package_root();
|
||||
let swift_target_folder = swift_target_folder();
|
||||
let swift_cache_folder = swift_cache_folder();
|
||||
if !Command::new("swift")
|
||||
.arg("build")
|
||||
.arg("--disable-automatic-resolution")
|
||||
.args(["--configuration", &env::var("PROFILE").unwrap()])
|
||||
.args(["--triple", &swift_target.target.triple])
|
||||
.args(["--build-path".into(), swift_target_folder])
|
||||
.args(["--cache-path".into(), swift_cache_folder])
|
||||
.current_dir(&swift_package_root)
|
||||
.status()
|
||||
.unwrap()
|
||||
@ -133,9 +135,17 @@ fn swift_package_root() -> PathBuf {
|
||||
}
|
||||
|
||||
fn swift_target_folder() -> PathBuf {
|
||||
let target = env::var("TARGET").unwrap();
|
||||
env::current_dir()
|
||||
.unwrap()
|
||||
.join(format!("../../target/{SWIFT_PACKAGE_NAME}"))
|
||||
.join(format!("../../target/{target}/{SWIFT_PACKAGE_NAME}_target"))
|
||||
}
|
||||
|
||||
fn swift_cache_folder() -> PathBuf {
|
||||
let target = env::var("TARGET").unwrap();
|
||||
env::current_dir()
|
||||
.unwrap()
|
||||
.join(format!("../../target/{target}/{SWIFT_PACKAGE_NAME}_cache"))
|
||||
}
|
||||
|
||||
fn copy_dir(source: &Path, destination: &Path) {
|
||||
|
@ -73,6 +73,7 @@ impl RealNodeRuntime {
|
||||
let npm_file = node_dir.join("bin/npm");
|
||||
|
||||
let result = Command::new(&node_binary)
|
||||
.env_clear()
|
||||
.arg(npm_file)
|
||||
.arg("--version")
|
||||
.stdin(Stdio::null())
|
||||
@ -149,6 +150,7 @@ impl NodeRuntime for RealNodeRuntime {
|
||||
}
|
||||
|
||||
let mut command = Command::new(node_binary);
|
||||
command.env_clear();
|
||||
command.env("PATH", env_path);
|
||||
command.arg(npm_file).arg(subcommand);
|
||||
command.args(["--cache".into(), installation_path.join("cache")]);
|
||||
@ -200,11 +202,11 @@ impl NodeRuntime for RealNodeRuntime {
|
||||
&[
|
||||
name,
|
||||
"--json",
|
||||
"-fetch-retry-mintimeout",
|
||||
"--fetch-retry-mintimeout",
|
||||
"2000",
|
||||
"-fetch-retry-maxtimeout",
|
||||
"--fetch-retry-maxtimeout",
|
||||
"5000",
|
||||
"-fetch-timeout",
|
||||
"--fetch-timeout",
|
||||
"5000",
|
||||
],
|
||||
)
|
||||
@ -229,11 +231,11 @@ impl NodeRuntime for RealNodeRuntime {
|
||||
|
||||
let mut arguments: Vec<_> = packages.iter().map(|p| p.as_str()).collect();
|
||||
arguments.extend_from_slice(&[
|
||||
"-fetch-retry-mintimeout",
|
||||
"--fetch-retry-mintimeout",
|
||||
"2000",
|
||||
"-fetch-retry-maxtimeout",
|
||||
"--fetch-retry-maxtimeout",
|
||||
"5000",
|
||||
"-fetch-timeout",
|
||||
"--fetch-timeout",
|
||||
"5000",
|
||||
]);
|
||||
|
||||
|
@ -1,10 +1,10 @@
|
||||
use editor::Editor;
|
||||
use gpui::{
|
||||
div, prelude::*, uniform_list, AppContext, Component, Div, FocusHandle, FocusableView,
|
||||
MouseButton, Render, Task, UniformListScrollHandle, View, ViewContext, WindowContext,
|
||||
div, prelude::*, uniform_list, AppContext, Div, FocusHandle, FocusableView, MouseButton,
|
||||
MouseDownEvent, Render, Task, UniformListScrollHandle, View, ViewContext, WindowContext,
|
||||
};
|
||||
use std::{cmp, sync::Arc};
|
||||
use ui::{prelude::*, v_stack, Divider, Label, TextColor};
|
||||
use ui::{prelude::*, v_stack, Color, Divider, Label};
|
||||
|
||||
pub struct Picker<D: PickerDelegate> {
|
||||
pub delegate: D,
|
||||
@ -15,7 +15,7 @@ pub struct Picker<D: PickerDelegate> {
|
||||
}
|
||||
|
||||
pub trait PickerDelegate: Sized + 'static {
|
||||
type ListItem: Component<Picker<Self>>;
|
||||
type ListItem: IntoElement;
|
||||
|
||||
fn match_count(&self) -> usize;
|
||||
fn selected_index(&self) -> usize;
|
||||
@ -32,7 +32,7 @@ pub trait PickerDelegate: Sized + 'static {
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Self::ListItem;
|
||||
) -> Option<Self::ListItem>;
|
||||
}
|
||||
|
||||
impl<D: PickerDelegate> FocusableView for Picker<D> {
|
||||
@ -114,6 +114,7 @@ impl<D: PickerDelegate> Picker<D> {
|
||||
}
|
||||
|
||||
fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||
dbg!("canceling!");
|
||||
self.delegate.dismissed(cx);
|
||||
}
|
||||
|
||||
@ -181,20 +182,20 @@ impl<D: PickerDelegate> Picker<D> {
|
||||
}
|
||||
|
||||
impl<D: PickerDelegate> Render for Picker<D> {
|
||||
type Element = Div<Self>;
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
div()
|
||||
.key_context("picker")
|
||||
.size_full()
|
||||
.elevation_2(cx)
|
||||
.on_action(Self::select_next)
|
||||
.on_action(Self::select_prev)
|
||||
.on_action(Self::select_first)
|
||||
.on_action(Self::select_last)
|
||||
.on_action(Self::cancel)
|
||||
.on_action(Self::confirm)
|
||||
.on_action(Self::secondary_confirm)
|
||||
.on_action(cx.listener(Self::select_next))
|
||||
.on_action(cx.listener(Self::select_prev))
|
||||
.on_action(cx.listener(Self::select_first))
|
||||
.on_action(cx.listener(Self::select_last))
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.on_action(cx.listener(Self::confirm))
|
||||
.on_action(cx.listener(Self::secondary_confirm))
|
||||
.child(
|
||||
v_stack()
|
||||
.py_0p5()
|
||||
@ -208,31 +209,37 @@ impl<D: PickerDelegate> Render for Picker<D> {
|
||||
.p_1()
|
||||
.grow()
|
||||
.child(
|
||||
uniform_list("candidates", self.delegate.match_count(), {
|
||||
move |this: &mut Self, visible_range, cx| {
|
||||
let selected_ix = this.delegate.selected_index();
|
||||
visible_range
|
||||
.map(|ix| {
|
||||
div()
|
||||
.on_mouse_down(
|
||||
MouseButton::Left,
|
||||
move |this: &mut Self, event, cx| {
|
||||
this.handle_click(
|
||||
ix,
|
||||
event.modifiers.command,
|
||||
cx,
|
||||
)
|
||||
},
|
||||
)
|
||||
.child(this.delegate.render_match(
|
||||
ix,
|
||||
ix == selected_ix,
|
||||
cx,
|
||||
))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
})
|
||||
uniform_list(
|
||||
cx.view().clone(),
|
||||
"candidates",
|
||||
self.delegate.match_count(),
|
||||
{
|
||||
let selected_index = self.delegate.selected_index();
|
||||
|
||||
move |picker, visible_range, cx| {
|
||||
visible_range
|
||||
.map(|ix| {
|
||||
div()
|
||||
.on_mouse_down(
|
||||
MouseButton::Left,
|
||||
cx.listener(move |this, event: &MouseDownEvent, cx| {
|
||||
this.handle_click(
|
||||
ix,
|
||||
event.modifiers.command,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
)
|
||||
.children(picker.delegate.render_match(
|
||||
ix,
|
||||
ix == selected_index,
|
||||
cx,
|
||||
))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
},
|
||||
)
|
||||
.track_scroll(self.scroll_handle.clone()),
|
||||
)
|
||||
.max_h_72()
|
||||
@ -244,7 +251,7 @@ impl<D: PickerDelegate> Render for Picker<D> {
|
||||
v_stack().p_1().grow().child(
|
||||
div()
|
||||
.px_1()
|
||||
.child(Label::new("No matches").color(TextColor::Muted)),
|
||||
.child(Label::new("No matches").color(Color::Muted)),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
@ -13,7 +13,7 @@ mod worktree_tests;
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use client::{proto, Client, Collaborator, TypedEnvelope, UserStore};
|
||||
use clock::ReplicaId;
|
||||
use collections::{hash_map, BTreeMap, HashMap, HashSet};
|
||||
use collections::{hash_map, BTreeMap, HashMap, HashSet, VecDeque};
|
||||
use copilot::Copilot;
|
||||
use futures::{
|
||||
channel::{
|
||||
@ -62,7 +62,10 @@ use serde::Serialize;
|
||||
use settings::SettingsStore;
|
||||
use sha2::{Digest, Sha256};
|
||||
use similar::{ChangeTag, TextDiff};
|
||||
use smol::channel::{Receiver, Sender};
|
||||
use smol::{
|
||||
channel::{Receiver, Sender},
|
||||
lock::Semaphore,
|
||||
};
|
||||
use std::{
|
||||
cmp::{self, Ordering},
|
||||
convert::TryInto,
|
||||
@ -557,6 +560,7 @@ enum SearchMatchCandidate {
|
||||
},
|
||||
Path {
|
||||
worktree_id: WorktreeId,
|
||||
is_ignored: bool,
|
||||
path: Arc<Path>,
|
||||
},
|
||||
}
|
||||
@ -5742,13 +5746,18 @@ impl Project {
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
|
||||
background
|
||||
.scoped(|scope| {
|
||||
let max_concurrent_workers = Arc::new(Semaphore::new(workers));
|
||||
|
||||
for worker_ix in 0..workers {
|
||||
let worker_start_ix = worker_ix * paths_per_worker;
|
||||
let worker_end_ix = worker_start_ix + paths_per_worker;
|
||||
let unnamed_buffers = opened_buffers.clone();
|
||||
let limiter = Arc::clone(&max_concurrent_workers);
|
||||
scope.spawn(async move {
|
||||
let _guard = limiter.acquire().await;
|
||||
let mut snapshot_start_ix = 0;
|
||||
let mut abs_path = PathBuf::new();
|
||||
for snapshot in snapshots {
|
||||
@ -5797,6 +5806,7 @@ impl Project {
|
||||
let project_path = SearchMatchCandidate::Path {
|
||||
worktree_id: snapshot.id(),
|
||||
path: entry.path.clone(),
|
||||
is_ignored: entry.is_ignored,
|
||||
};
|
||||
if matching_paths_tx.send(project_path).await.is_err() {
|
||||
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;
|
||||
}
|
||||
@ -5917,11 +6015,24 @@ impl Project {
|
||||
let (buffers_tx, buffers_rx) = smol::channel::bounded(1024);
|
||||
let (sorted_buffers_tx, sorted_buffers_rx) = futures::channel::oneshot::channel();
|
||||
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 {
|
||||
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());
|
||||
ignored_buffers.sort_by_key(|candidate| candidate.path());
|
||||
buffers.extend(ignored_buffers);
|
||||
let matching_paths = buffers.clone();
|
||||
let _ = sorted_buffers_tx.send(buffers);
|
||||
for (index, candidate) in matching_paths.into_iter().enumerate() {
|
||||
@ -5933,7 +6044,9 @@ impl Project {
|
||||
cx.spawn(|mut cx| async move {
|
||||
let buffer = match candidate {
|
||||
SearchMatchCandidate::OpenBuffer { buffer, .. } => Some(buffer),
|
||||
SearchMatchCandidate::Path { worktree_id, path } => this
|
||||
SearchMatchCandidate::Path {
|
||||
worktree_id, path, ..
|
||||
} => this
|
||||
.update(&mut cx, |this, cx| {
|
||||
this.open_buffer((worktree_id, path), cx)
|
||||
})
|
||||
|
@ -2226,7 +2226,7 @@ impl LocalSnapshot {
|
||||
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
|
||||
.iter()
|
||||
.any(|exclude_matcher| exclude_matcher.is_match(abs_path))
|
||||
@ -2399,26 +2399,9 @@ impl BackgroundScannerState {
|
||||
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;
|
||||
|
||||
// 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);
|
||||
|
||||
for dot_git_dir in dot_git_dirs_to_reload {
|
||||
// If there is already a repository for this .git directory, reload
|
||||
// the status for all of its files.
|
||||
let repository = self
|
||||
@ -2430,7 +2413,7 @@ impl BackgroundScannerState {
|
||||
});
|
||||
match repository {
|
||||
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)) => {
|
||||
if repository.git_dir_scan_id == scan_id {
|
||||
@ -2444,7 +2427,7 @@ impl BackgroundScannerState {
|
||||
continue;
|
||||
};
|
||||
|
||||
log::info!("reload git repository {:?}", dot_git_dir);
|
||||
log::info!("reload git repository {dot_git_dir:?}");
|
||||
let repository = repository.repo_ptr.lock();
|
||||
let branch = repository.branch_name();
|
||||
repository.reload_index();
|
||||
@ -2475,7 +2458,9 @@ impl BackgroundScannerState {
|
||||
ids_to_preserve.insert(work_directory_id);
|
||||
} else {
|
||||
let git_dir_abs_path = snapshot.abs_path().join(&entry.git_dir_path);
|
||||
if snapshot.is_abs_path_excluded(&git_dir_abs_path)
|
||||
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))
|
||||
{
|
||||
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 dot_git_paths_to_reload = HashSet::default();
|
||||
abs_paths.sort_unstable();
|
||||
abs_paths.dedup_by(|a, b| a.starts_with(&b));
|
||||
abs_paths.retain(|abs_path| {
|
||||
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> =
|
||||
if let Ok(path) = abs_path.strip_prefix(&root_canonical_path) {
|
||||
path.into()
|
||||
@ -3328,23 +3328,30 @@ impl BackgroundScanner {
|
||||
);
|
||||
return false;
|
||||
};
|
||||
let parent_dir_is_loaded = relative_path.parent().map_or(true, |parent| {
|
||||
snapshot
|
||||
.entry_for_path(parent)
|
||||
.map_or(false, |entry| entry.kind == EntryKind::Dir)
|
||||
});
|
||||
if !parent_dir_is_loaded {
|
||||
log::debug!("ignoring event {relative_path:?} within unloaded directory");
|
||||
return false;
|
||||
}
|
||||
|
||||
if !is_git_related(&abs_path) {
|
||||
let parent_dir_is_loaded = relative_path.parent().map_or(true, |parent| {
|
||||
snapshot
|
||||
.entry_for_path(parent)
|
||||
.map_or(false, |entry| entry.kind == EntryKind::Dir)
|
||||
});
|
||||
if !parent_dir_is_loaded {
|
||||
log::debug!("ignoring event {relative_path:?} within unloaded directory");
|
||||
return false;
|
||||
// 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!(
|
||||
"ignoring FS event for path {relative_path:?} within excluded directory"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if excluded_file_event {
|
||||
if !is_git_related {
|
||||
log::debug!("ignoring FS event for excluded path {relative_path:?}");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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();
|
||||
self.reload_entries_for_paths(
|
||||
root_path,
|
||||
root_canonical_path,
|
||||
&relative_paths,
|
||||
abs_paths,
|
||||
Some(scan_job_tx.clone()),
|
||||
)
|
||||
.await;
|
||||
drop(scan_job_tx);
|
||||
self.scan_dirs(false, scan_job_rx).await;
|
||||
let (scan_job_tx, scan_job_rx) = channel::unbounded();
|
||||
self.reload_entries_for_paths(
|
||||
root_path,
|
||||
root_canonical_path,
|
||||
&relative_paths,
|
||||
abs_paths,
|
||||
Some(scan_job_tx.clone()),
|
||||
)
|
||||
.await;
|
||||
drop(scan_job_tx);
|
||||
self.scan_dirs(false, scan_job_rx).await;
|
||||
|
||||
let (scan_job_tx, scan_job_rx) = channel::unbounded();
|
||||
self.update_ignore_statuses(scan_job_tx).await;
|
||||
self.scan_dirs(false, scan_job_rx).await;
|
||||
let (scan_job_tx, scan_job_rx) = channel::unbounded();
|
||||
self.update_ignore_statuses(scan_job_tx).await;
|
||||
self.scan_dirs(false, scan_job_rx).await;
|
||||
}
|
||||
|
||||
{
|
||||
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;
|
||||
for (_, entry_id) in mem::take(&mut state.removed_entry_ids) {
|
||||
state.scanned_dirs.remove(&entry_id);
|
||||
@ -3516,7 +3531,7 @@ impl BackgroundScanner {
|
||||
let state = self.state.lock();
|
||||
let snapshot = &state.snapshot;
|
||||
root_abs_path = snapshot.abs_path().clone();
|
||||
if snapshot.is_abs_path_excluded(&job.abs_path) {
|
||||
if snapshot.is_path_excluded(&job.abs_path) {
|
||||
log::error!("skipping excluded directory {:?}", job.path);
|
||||
return Ok(());
|
||||
}
|
||||
@ -3588,7 +3603,7 @@ impl BackgroundScanner {
|
||||
|
||||
{
|
||||
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);
|
||||
log::debug!("skipping excluded child entry {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 {
|
||||
let mut result = root_char_bag;
|
||||
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)]
|
||||
async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
@ -10,7 +10,7 @@ use futures::future;
|
||||
use gpui::{AppContext, AsyncAppContext, Model};
|
||||
use language::{
|
||||
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},
|
||||
range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CharKind,
|
||||
CodeAction, Completion, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction,
|
||||
@ -1339,7 +1339,7 @@ impl LspCommand for GetCompletions {
|
||||
async fn response_from_lsp(
|
||||
self,
|
||||
completions: Option<lsp::CompletionResponse>,
|
||||
_: Model<Project>,
|
||||
project: Model<Project>,
|
||||
buffer: Model<Buffer>,
|
||||
server_id: LanguageServerId,
|
||||
mut cx: AsyncAppContext,
|
||||
@ -1359,7 +1359,8 @@ impl LspCommand for GetCompletions {
|
||||
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 snapshot = buffer.snapshot();
|
||||
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();
|
||||
LineEnding::normalize(&mut new_text);
|
||||
Some(async move {
|
||||
let mut label = None;
|
||||
if let Some(language) = language {
|
||||
if let Some(language) = language.as_ref() {
|
||||
language.process_completion(&mut 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 {
|
||||
old_range,
|
||||
new_text,
|
||||
@ -1460,6 +1476,7 @@ impl LspCommand for GetCompletions {
|
||||
lsp_completion.filter_text.as_deref(),
|
||||
)
|
||||
}),
|
||||
documentation,
|
||||
server_id,
|
||||
lsp_completion,
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ mod worktree_tests;
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use client::{proto, Client, Collaborator, TypedEnvelope, UserStore};
|
||||
use clock::ReplicaId;
|
||||
use collections::{hash_map, BTreeMap, HashMap, HashSet};
|
||||
use collections::{hash_map, BTreeMap, HashMap, HashSet, VecDeque};
|
||||
use copilot::Copilot;
|
||||
use futures::{
|
||||
channel::{
|
||||
@ -63,6 +63,7 @@ use settings::{Settings, SettingsStore};
|
||||
use sha2::{Digest, Sha256};
|
||||
use similar::{ChangeTag, TextDiff};
|
||||
use smol::channel::{Receiver, Sender};
|
||||
use smol::lock::Semaphore;
|
||||
use std::{
|
||||
cmp::{self, Ordering},
|
||||
convert::TryInto,
|
||||
@ -557,6 +558,7 @@ enum SearchMatchCandidate {
|
||||
},
|
||||
Path {
|
||||
worktree_id: WorktreeId,
|
||||
is_ignored: bool,
|
||||
path: Arc<Path>,
|
||||
},
|
||||
}
|
||||
@ -5815,11 +5817,15 @@ impl Project {
|
||||
}
|
||||
executor
|
||||
.scoped(|scope| {
|
||||
let max_concurrent_workers = Arc::new(Semaphore::new(workers));
|
||||
|
||||
for worker_ix in 0..workers {
|
||||
let worker_start_ix = worker_ix * paths_per_worker;
|
||||
let worker_end_ix = worker_start_ix + paths_per_worker;
|
||||
let unnamed_buffers = opened_buffers.clone();
|
||||
let limiter = Arc::clone(&max_concurrent_workers);
|
||||
scope.spawn(async move {
|
||||
let _guard = limiter.acquire().await;
|
||||
let mut snapshot_start_ix = 0;
|
||||
let mut abs_path = PathBuf::new();
|
||||
for snapshot in snapshots {
|
||||
@ -5868,6 +5874,7 @@ impl Project {
|
||||
let project_path = SearchMatchCandidate::Path {
|
||||
worktree_id: snapshot.id(),
|
||||
path: entry.path.clone(),
|
||||
is_ignored: entry.is_ignored,
|
||||
};
|
||||
if matching_paths_tx.send(project_path).await.is_err() {
|
||||
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;
|
||||
}
|
||||
@ -5986,11 +6081,24 @@ impl Project {
|
||||
let (buffers_tx, buffers_rx) = smol::channel::bounded(1024);
|
||||
let (sorted_buffers_tx, sorted_buffers_rx) = futures::channel::oneshot::channel();
|
||||
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 {
|
||||
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());
|
||||
ignored_buffers.sort_by_key(|candidate| candidate.path());
|
||||
buffers.extend(ignored_buffers);
|
||||
let matching_paths = buffers.clone();
|
||||
let _ = sorted_buffers_tx.send(buffers);
|
||||
for (index, candidate) in matching_paths.into_iter().enumerate() {
|
||||
@ -6002,7 +6110,9 @@ impl Project {
|
||||
cx.spawn(move |mut cx| async move {
|
||||
let buffer = match candidate {
|
||||
SearchMatchCandidate::OpenBuffer { buffer, .. } => Some(buffer),
|
||||
SearchMatchCandidate::Path { worktree_id, path } => this
|
||||
SearchMatchCandidate::Path {
|
||||
worktree_id, path, ..
|
||||
} => this
|
||||
.update(&mut cx, |this, cx| {
|
||||
this.open_buffer((worktree_id, path), cx)
|
||||
})?
|
||||
|
@ -2222,7 +2222,7 @@ impl LocalSnapshot {
|
||||
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
|
||||
.iter()
|
||||
.any(|exclude_matcher| exclude_matcher.is_match(abs_path))
|
||||
@ -2395,26 +2395,10 @@ impl BackgroundScannerState {
|
||||
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;
|
||||
|
||||
// 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);
|
||||
|
||||
for dot_git_dir in dot_git_dirs_to_reload {
|
||||
// If there is already a repository for this .git directory, reload
|
||||
// the status for all of its files.
|
||||
let repository = self
|
||||
@ -2426,7 +2410,7 @@ impl BackgroundScannerState {
|
||||
});
|
||||
match repository {
|
||||
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)) => {
|
||||
if repository.git_dir_scan_id == scan_id {
|
||||
@ -2440,7 +2424,7 @@ impl BackgroundScannerState {
|
||||
continue;
|
||||
};
|
||||
|
||||
log::info!("reload git repository {:?}", dot_git_dir);
|
||||
log::info!("reload git repository {dot_git_dir:?}");
|
||||
let repository = repository.repo_ptr.lock();
|
||||
let branch = repository.branch_name();
|
||||
repository.reload_index();
|
||||
@ -2471,7 +2455,9 @@ impl BackgroundScannerState {
|
||||
ids_to_preserve.insert(work_directory_id);
|
||||
} else {
|
||||
let git_dir_abs_path = snapshot.abs_path().join(&entry.git_dir_path);
|
||||
if snapshot.is_abs_path_excluded(&git_dir_abs_path)
|
||||
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))
|
||||
{
|
||||
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 dot_git_paths_to_reload = HashSet::default();
|
||||
abs_paths.sort_unstable();
|
||||
abs_paths.dedup_by(|a, b| a.starts_with(&b));
|
||||
abs_paths.retain(|abs_path| {
|
||||
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> =
|
||||
if let Ok(path) = abs_path.strip_prefix(&root_canonical_path) {
|
||||
path.into()
|
||||
@ -3318,22 +3319,30 @@ impl BackgroundScanner {
|
||||
return false;
|
||||
};
|
||||
|
||||
if !is_git_related(&abs_path) {
|
||||
let parent_dir_is_loaded = relative_path.parent().map_or(true, |parent| {
|
||||
snapshot
|
||||
.entry_for_path(parent)
|
||||
.map_or(false, |entry| entry.kind == EntryKind::Dir)
|
||||
});
|
||||
if !parent_dir_is_loaded {
|
||||
log::debug!("ignoring event {relative_path:?} within unloaded directory");
|
||||
return false;
|
||||
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;
|
||||
}
|
||||
|
||||
// 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!(
|
||||
"ignoring FS event for path {relative_path:?} within excluded directory"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if excluded_file_event {
|
||||
if !is_git_related {
|
||||
log::debug!("ignoring FS event for excluded path {relative_path:?}");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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();
|
||||
self.reload_entries_for_paths(
|
||||
root_path,
|
||||
root_canonical_path,
|
||||
&relative_paths,
|
||||
abs_paths,
|
||||
Some(scan_job_tx.clone()),
|
||||
)
|
||||
.await;
|
||||
drop(scan_job_tx);
|
||||
self.scan_dirs(false, scan_job_rx).await;
|
||||
let (scan_job_tx, scan_job_rx) = channel::unbounded();
|
||||
self.reload_entries_for_paths(
|
||||
root_path,
|
||||
root_canonical_path,
|
||||
&relative_paths,
|
||||
abs_paths,
|
||||
Some(scan_job_tx.clone()),
|
||||
)
|
||||
.await;
|
||||
drop(scan_job_tx);
|
||||
self.scan_dirs(false, scan_job_rx).await;
|
||||
|
||||
let (scan_job_tx, scan_job_rx) = channel::unbounded();
|
||||
self.update_ignore_statuses(scan_job_tx).await;
|
||||
self.scan_dirs(false, scan_job_rx).await;
|
||||
let (scan_job_tx, scan_job_rx) = channel::unbounded();
|
||||
self.update_ignore_statuses(scan_job_tx).await;
|
||||
self.scan_dirs(false, scan_job_rx).await;
|
||||
}
|
||||
|
||||
{
|
||||
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;
|
||||
for (_, entry_id) in mem::take(&mut state.removed_entry_ids) {
|
||||
state.scanned_dirs.remove(&entry_id);
|
||||
@ -3505,7 +3522,7 @@ impl BackgroundScanner {
|
||||
let state = self.state.lock();
|
||||
let snapshot = &state.snapshot;
|
||||
root_abs_path = snapshot.abs_path().clone();
|
||||
if snapshot.is_abs_path_excluded(&job.abs_path) {
|
||||
if snapshot.is_path_excluded(&job.abs_path) {
|
||||
log::error!("skipping excluded directory {:?}", job.path);
|
||||
return Ok(());
|
||||
}
|
||||
@ -3577,7 +3594,7 @@ impl BackgroundScanner {
|
||||
|
||||
{
|
||||
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);
|
||||
log::debug!("skipping excluded child entry {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 {
|
||||
let mut result = root_char_bag;
|
||||
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)]
|
||||
async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
|
||||
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) {
|
||||
init_test(cx);
|
||||
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());
|
||||
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());
|
||||
});
|
||||
|
||||
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 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> {
|
||||
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]
|
||||
|
@ -9,7 +9,6 @@ path = "src/project_panel.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
context_menu = { path = "../context_menu" }
|
||||
collections = { path = "../collections" }
|
||||
db = { path = "../db2", package = "db2" }
|
||||
editor = { path = "../editor2", package = "editor2" }
|
||||
|
@ -9,10 +9,9 @@ use file_associations::FileAssociations;
|
||||
use anyhow::{anyhow, Result};
|
||||
use gpui::{
|
||||
actions, div, px, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext,
|
||||
ClipboardItem, Component, Div, EventEmitter, FocusHandle, Focusable, FocusableView,
|
||||
InteractiveComponent, Model, MouseButton, ParentComponent, Pixels, Point, PromptLevel, Render,
|
||||
Stateful, StatefulInteractiveComponent, Styled, Task, UniformListScrollHandle, View,
|
||||
ViewContext, VisualContext as _, WeakView, WindowContext,
|
||||
ClipboardItem, Div, EventEmitter, FocusHandle, Focusable, FocusableView, InteractiveElement,
|
||||
Model, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render, Stateful, Styled,
|
||||
Task, UniformListScrollHandle, View, ViewContext, VisualContext as _, WeakView, WindowContext,
|
||||
};
|
||||
use menu::{Confirm, SelectNext, SelectPrev};
|
||||
use project::{
|
||||
@ -30,7 +29,7 @@ use std::{
|
||||
sync::Arc,
|
||||
};
|
||||
use theme::ActiveTheme as _;
|
||||
use ui::{h_stack, v_stack, IconElement, Label};
|
||||
use ui::{v_stack, IconElement, Label, ListItem};
|
||||
use unicase::UniCase;
|
||||
use util::{maybe, ResultExt, TryFutureExt};
|
||||
use workspace::{
|
||||
@ -247,7 +246,6 @@ impl ProjectPanel {
|
||||
let mut old_dock_position = this.position(cx);
|
||||
ProjectPanelSettings::register(cx);
|
||||
cx.observe_global::<SettingsStore>(move |this, cx| {
|
||||
dbg!("OLA!");
|
||||
let new_dock_position = this.position(cx);
|
||||
if new_dock_position != old_dock_position {
|
||||
old_dock_position = new_dock_position;
|
||||
@ -372,7 +370,7 @@ impl ProjectPanel {
|
||||
_entry_id: ProjectEntryId,
|
||||
_cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
todo!()
|
||||
// todo!()
|
||||
// let project = self.project.read(cx);
|
||||
|
||||
// let worktree_id = if let Some(id) = project.worktree_id_for_entry(entry_id, cx) {
|
||||
@ -645,6 +643,7 @@ impl ProjectPanel {
|
||||
}
|
||||
|
||||
fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
|
||||
dbg!("odd");
|
||||
self.edit_state = None;
|
||||
self.update_visible_entries(None, cx);
|
||||
cx.focus(&self.focus_handle);
|
||||
@ -1335,13 +1334,19 @@ impl ProjectPanel {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_entry_visual_element(
|
||||
details: &EntryDetails,
|
||||
editor: Option<&View<Editor>>,
|
||||
padding: Pixels,
|
||||
fn render_entry(
|
||||
&self,
|
||||
entry_id: ProjectEntryId,
|
||||
details: EntryDetails,
|
||||
// dragged_entry_destination: &mut Option<Arc<Path>>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Div<Self> {
|
||||
) -> ListItem {
|
||||
let kind = details.kind;
|
||||
let settings = ProjectPanelSettings::get_global(cx);
|
||||
let show_editor = details.is_editing && !details.is_processing;
|
||||
let is_selected = self
|
||||
.selection
|
||||
.map_or(false, |selection| selection.entry_id == entry_id);
|
||||
|
||||
let theme = cx.theme();
|
||||
let filename_text_color = details
|
||||
@ -1354,14 +1359,17 @@ impl ProjectPanel {
|
||||
})
|
||||
.unwrap_or(theme.status().info);
|
||||
|
||||
h_stack()
|
||||
ListItem::new(entry_id.to_proto() as usize)
|
||||
.indent_level(details.depth)
|
||||
.indent_step_size(px(settings.indent_size))
|
||||
.selected(is_selected)
|
||||
.child(if let Some(icon) = &details.icon {
|
||||
div().child(IconElement::from_path(icon.to_string()))
|
||||
} else {
|
||||
div()
|
||||
})
|
||||
.child(
|
||||
if let (Some(editor), true) = (editor, show_editor) {
|
||||
if let (Some(editor), true) = (Some(&self.filename_editor), show_editor) {
|
||||
div().w_full().child(editor.clone())
|
||||
} else {
|
||||
div()
|
||||
@ -1370,34 +1378,7 @@ impl ProjectPanel {
|
||||
}
|
||||
.ml_1(),
|
||||
)
|
||||
.pl(padding)
|
||||
}
|
||||
|
||||
fn render_entry(
|
||||
&self,
|
||||
entry_id: ProjectEntryId,
|
||||
details: EntryDetails,
|
||||
// dragged_entry_destination: &mut Option<Arc<Path>>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Stateful<Self, Div<Self>> {
|
||||
let kind = details.kind;
|
||||
let settings = ProjectPanelSettings::get_global(cx);
|
||||
const INDENT_SIZE: Pixels = px(16.0);
|
||||
let padding = INDENT_SIZE + details.depth as f32 * px(settings.indent_size);
|
||||
let show_editor = details.is_editing && !details.is_processing;
|
||||
let is_selected = self
|
||||
.selection
|
||||
.map_or(false, |selection| selection.entry_id == entry_id);
|
||||
|
||||
Self::render_entry_visual_element(&details, Some(&self.filename_editor), padding, cx)
|
||||
.id(entry_id.to_proto() as usize)
|
||||
.w_full()
|
||||
.cursor_pointer()
|
||||
.when(is_selected, |this| {
|
||||
this.bg(cx.theme().colors().element_selected)
|
||||
})
|
||||
.hover(|style| style.bg(cx.theme().colors().element_hover))
|
||||
.on_click(move |this, event, cx| {
|
||||
.on_click(cx.listener(move |this, event: &gpui::ClickEvent, cx| {
|
||||
if !show_editor {
|
||||
if kind.is_dir() {
|
||||
this.toggle_expanded(entry_id, cx);
|
||||
@ -1409,10 +1390,10 @@ impl ProjectPanel {
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.on_mouse_down(MouseButton::Right, move |this, event, cx| {
|
||||
}))
|
||||
.on_secondary_mouse_down(cx.listener(move |this, event: &MouseDownEvent, cx| {
|
||||
this.deploy_context_menu(event.position, entry_id, cx);
|
||||
})
|
||||
}))
|
||||
// .on_drop::<ProjectEntryId>(|this, event, cx| {
|
||||
// this.move_entry(
|
||||
// *dragged_entry,
|
||||
@ -1425,9 +1406,9 @@ impl ProjectPanel {
|
||||
}
|
||||
|
||||
impl Render for ProjectPanel {
|
||||
type Element = Focusable<Self, Stateful<Self, Div<Self>>>;
|
||||
type Element = Focusable<Stateful<Div>>;
|
||||
|
||||
fn render(&mut self, _cx: &mut gpui::ViewContext<Self>) -> Self::Element {
|
||||
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element {
|
||||
let has_worktree = self.visible_entries.len() != 0;
|
||||
|
||||
if has_worktree {
|
||||
@ -1435,40 +1416,43 @@ impl Render for ProjectPanel {
|
||||
.id("project-panel")
|
||||
.size_full()
|
||||
.key_context("ProjectPanel")
|
||||
.on_action(Self::select_next)
|
||||
.on_action(Self::select_prev)
|
||||
.on_action(Self::expand_selected_entry)
|
||||
.on_action(Self::collapse_selected_entry)
|
||||
.on_action(Self::collapse_all_entries)
|
||||
.on_action(Self::new_file)
|
||||
.on_action(Self::new_directory)
|
||||
.on_action(Self::rename)
|
||||
.on_action(Self::delete)
|
||||
.on_action(Self::confirm)
|
||||
.on_action(Self::open_file)
|
||||
.on_action(Self::cancel)
|
||||
.on_action(Self::cut)
|
||||
.on_action(Self::copy)
|
||||
.on_action(Self::copy_path)
|
||||
.on_action(Self::copy_relative_path)
|
||||
.on_action(Self::paste)
|
||||
.on_action(Self::reveal_in_finder)
|
||||
.on_action(Self::open_in_terminal)
|
||||
.on_action(Self::new_search_in_directory)
|
||||
.on_action(cx.listener(Self::select_next))
|
||||
.on_action(cx.listener(Self::select_prev))
|
||||
.on_action(cx.listener(Self::expand_selected_entry))
|
||||
.on_action(cx.listener(Self::collapse_selected_entry))
|
||||
.on_action(cx.listener(Self::collapse_all_entries))
|
||||
.on_action(cx.listener(Self::new_file))
|
||||
.on_action(cx.listener(Self::new_directory))
|
||||
.on_action(cx.listener(Self::rename))
|
||||
.on_action(cx.listener(Self::delete))
|
||||
.on_action(cx.listener(Self::confirm))
|
||||
.on_action(cx.listener(Self::open_file))
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.on_action(cx.listener(Self::cut))
|
||||
.on_action(cx.listener(Self::copy))
|
||||
.on_action(cx.listener(Self::copy_path))
|
||||
.on_action(cx.listener(Self::copy_relative_path))
|
||||
.on_action(cx.listener(Self::paste))
|
||||
.on_action(cx.listener(Self::reveal_in_finder))
|
||||
.on_action(cx.listener(Self::open_in_terminal))
|
||||
.on_action(cx.listener(Self::new_search_in_directory))
|
||||
.track_focus(&self.focus_handle)
|
||||
.child(
|
||||
uniform_list(
|
||||
cx.view().clone(),
|
||||
"entries",
|
||||
self.visible_entries
|
||||
.iter()
|
||||
.map(|(_, worktree_entries)| worktree_entries.len())
|
||||
.sum(),
|
||||
|this: &mut Self, range, cx| {
|
||||
let mut items = Vec::new();
|
||||
this.for_each_visible_entry(range, cx, |id, details, cx| {
|
||||
items.push(this.render_entry(id, details, cx));
|
||||
});
|
||||
items
|
||||
{
|
||||
|this, range, cx| {
|
||||
let mut items = Vec::new();
|
||||
this.for_each_visible_entry(range, cx, |id, details, cx| {
|
||||
items.push(this.render_entry(id, details, cx));
|
||||
});
|
||||
items
|
||||
}
|
||||
},
|
||||
)
|
||||
.size_full()
|
||||
|
@ -56,12 +56,12 @@ pub struct Mention {
|
||||
}
|
||||
|
||||
impl RichText {
|
||||
pub fn element<V: 'static>(
|
||||
pub fn element(
|
||||
&self,
|
||||
// syntax: Arc<SyntaxTheme>,
|
||||
// style: RichTextStyle,
|
||||
// cx: &mut ViewContext<V>,
|
||||
) -> AnyElement<V> {
|
||||
) -> AnyElement {
|
||||
todo!();
|
||||
|
||||
// let mut region_id = 0;
|
||||
|
@ -1767,16 +1767,13 @@ impl View for ProjectSearchBar {
|
||||
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(
|
||||
// TODO proper icon
|
||||
"icons/case_insensitive.svg",
|
||||
"icons/file_icons/git.svg",
|
||||
SearchOptions::INCLUDE_IGNORED,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
// TODO not implemented yet
|
||||
let _ = include_ignored.take();
|
||||
|
||||
let search_button_for_mode = |mode, side, cx: &mut ViewContext<ProjectSearchBar>| {
|
||||
let is_active = if let Some(search) = self.active_project_search.as_ref() {
|
||||
|
40
crates/search2/Cargo.toml
Normal file
40
crates/search2/Cargo.toml
Normal file
@ -0,0 +1,40 @@
|
||||
[package]
|
||||
name = "search2"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/search.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
bitflags = "1"
|
||||
collections = { path = "../collections" }
|
||||
editor = { package = "editor2", path = "../editor2" }
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
language = { package = "language2", path = "../language2" }
|
||||
menu = { package = "menu2", path = "../menu2" }
|
||||
project = { package = "project2", path = "../project2" }
|
||||
settings = { package = "settings2", path = "../settings2" }
|
||||
theme = { package = "theme2", path = "../theme2" }
|
||||
util = { path = "../util" }
|
||||
ui = {package = "ui2", path = "../ui2"}
|
||||
workspace = { package = "workspace2", path = "../workspace2" }
|
||||
#semantic_index = { path = "../semantic_index" }
|
||||
anyhow.workspace = true
|
||||
futures.workspace = true
|
||||
log.workspace = true
|
||||
postage.workspace = true
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
smallvec.workspace = true
|
||||
smol.workspace = true
|
||||
serde_json.workspace = true
|
||||
[dev-dependencies]
|
||||
client = { package = "client2", path = "../client2", features = ["test-support"] }
|
||||
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
|
||||
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
|
||||
|
||||
workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
|
||||
unindent.workspace = true
|
1747
crates/search2/src/buffer_search.rs
Normal file
1747
crates/search2/src/buffer_search.rs
Normal file
File diff suppressed because it is too large
Load Diff
184
crates/search2/src/history.rs
Normal file
184
crates/search2/src/history.rs
Normal file
@ -0,0 +1,184 @@
|
||||
use smallvec::SmallVec;
|
||||
const SEARCH_HISTORY_LIMIT: usize = 20;
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct SearchHistory {
|
||||
history: SmallVec<[String; SEARCH_HISTORY_LIMIT]>,
|
||||
selected: Option<usize>,
|
||||
}
|
||||
|
||||
impl SearchHistory {
|
||||
pub fn add(&mut self, search_string: String) {
|
||||
if let Some(i) = self.selected {
|
||||
if search_string == self.history[i] {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(previously_searched) = self.history.last_mut() {
|
||||
if search_string.find(previously_searched.as_str()).is_some() {
|
||||
*previously_searched = search_string;
|
||||
self.selected = Some(self.history.len() - 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
self.history.push(search_string);
|
||||
if self.history.len() > SEARCH_HISTORY_LIMIT {
|
||||
self.history.remove(0);
|
||||
}
|
||||
self.selected = Some(self.history.len() - 1);
|
||||
}
|
||||
|
||||
pub fn next(&mut self) -> Option<&str> {
|
||||
let history_size = self.history.len();
|
||||
if history_size == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let selected = self.selected?;
|
||||
if selected == history_size - 1 {
|
||||
return None;
|
||||
}
|
||||
let next_index = selected + 1;
|
||||
self.selected = Some(next_index);
|
||||
Some(&self.history[next_index])
|
||||
}
|
||||
|
||||
pub fn current(&self) -> Option<&str> {
|
||||
Some(&self.history[self.selected?])
|
||||
}
|
||||
|
||||
pub fn previous(&mut self) -> Option<&str> {
|
||||
let history_size = self.history.len();
|
||||
if history_size == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let prev_index = match self.selected {
|
||||
Some(selected_index) => {
|
||||
if selected_index == 0 {
|
||||
return None;
|
||||
} else {
|
||||
selected_index - 1
|
||||
}
|
||||
}
|
||||
None => history_size - 1,
|
||||
};
|
||||
|
||||
self.selected = Some(prev_index);
|
||||
Some(&self.history[prev_index])
|
||||
}
|
||||
|
||||
pub fn reset_selection(&mut self) {
|
||||
self.selected = None;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_add() {
|
||||
let mut search_history = SearchHistory::default();
|
||||
assert_eq!(
|
||||
search_history.current(),
|
||||
None,
|
||||
"No current selection should be set fo the default search history"
|
||||
);
|
||||
|
||||
search_history.add("rust".to_string());
|
||||
assert_eq!(
|
||||
search_history.current(),
|
||||
Some("rust"),
|
||||
"Newly added item should be selected"
|
||||
);
|
||||
|
||||
// check if duplicates are not added
|
||||
search_history.add("rust".to_string());
|
||||
assert_eq!(
|
||||
search_history.history.len(),
|
||||
1,
|
||||
"Should not add a duplicate"
|
||||
);
|
||||
assert_eq!(search_history.current(), Some("rust"));
|
||||
|
||||
// check if new string containing the previous string replaces it
|
||||
search_history.add("rustlang".to_string());
|
||||
assert_eq!(
|
||||
search_history.history.len(),
|
||||
1,
|
||||
"Should replace previous item if it's a substring"
|
||||
);
|
||||
assert_eq!(search_history.current(), Some("rustlang"));
|
||||
|
||||
// push enough items to test SEARCH_HISTORY_LIMIT
|
||||
for i in 0..SEARCH_HISTORY_LIMIT * 2 {
|
||||
search_history.add(format!("item{i}"));
|
||||
}
|
||||
assert!(search_history.history.len() <= SEARCH_HISTORY_LIMIT);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_next_and_previous() {
|
||||
let mut search_history = SearchHistory::default();
|
||||
assert_eq!(
|
||||
search_history.next(),
|
||||
None,
|
||||
"Default search history should not have a next item"
|
||||
);
|
||||
|
||||
search_history.add("Rust".to_string());
|
||||
assert_eq!(search_history.next(), None);
|
||||
search_history.add("JavaScript".to_string());
|
||||
assert_eq!(search_history.next(), None);
|
||||
search_history.add("TypeScript".to_string());
|
||||
assert_eq!(search_history.next(), None);
|
||||
|
||||
assert_eq!(search_history.current(), Some("TypeScript"));
|
||||
|
||||
assert_eq!(search_history.previous(), Some("JavaScript"));
|
||||
assert_eq!(search_history.current(), Some("JavaScript"));
|
||||
|
||||
assert_eq!(search_history.previous(), Some("Rust"));
|
||||
assert_eq!(search_history.current(), Some("Rust"));
|
||||
|
||||
assert_eq!(search_history.previous(), None);
|
||||
assert_eq!(search_history.current(), Some("Rust"));
|
||||
|
||||
assert_eq!(search_history.next(), Some("JavaScript"));
|
||||
assert_eq!(search_history.current(), Some("JavaScript"));
|
||||
|
||||
assert_eq!(search_history.next(), Some("TypeScript"));
|
||||
assert_eq!(search_history.current(), Some("TypeScript"));
|
||||
|
||||
assert_eq!(search_history.next(), None);
|
||||
assert_eq!(search_history.current(), Some("TypeScript"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reset_selection() {
|
||||
let mut search_history = SearchHistory::default();
|
||||
search_history.add("Rust".to_string());
|
||||
search_history.add("JavaScript".to_string());
|
||||
search_history.add("TypeScript".to_string());
|
||||
|
||||
assert_eq!(search_history.current(), Some("TypeScript"));
|
||||
search_history.reset_selection();
|
||||
assert_eq!(search_history.current(), None);
|
||||
assert_eq!(
|
||||
search_history.previous(),
|
||||
Some("TypeScript"),
|
||||
"Should start from the end after reset on previous item query"
|
||||
);
|
||||
|
||||
search_history.previous();
|
||||
assert_eq!(search_history.current(), Some("JavaScript"));
|
||||
search_history.previous();
|
||||
assert_eq!(search_history.current(), Some("Rust"));
|
||||
|
||||
search_history.reset_selection();
|
||||
assert_eq!(search_history.current(), None);
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user