mirror of
https://github.com/zed-industries/zed.git
synced 2024-12-27 10:21:55 +03:00
Merge branch 'main' into welcome
This commit is contained in:
commit
8faa1f6e58
12
.cargo/ci-config.toml
Normal file
12
.cargo/ci-config.toml
Normal file
@ -0,0 +1,12 @@
|
||||
# This config is different from config.toml in this directory, as the latter is recognized by Cargo.
|
||||
# This file is placed in $HOME/.cargo/config.toml on CI runs. Cargo then merges Zeds .cargo/config.toml with $HOME/.cargo/config.toml
|
||||
# with preference for settings from Zeds config.toml.
|
||||
# TL;DR: If a value is set in both ci-config.toml and config.toml, config.toml value takes precedence.
|
||||
# Arrays are merged together though. See: https://doc.rust-lang.org/cargo/reference/config.html#hierarchical-structure
|
||||
# The intent for this file is to configure CI build process with a divergance from Zed developers experience; for example, in this config file
|
||||
# we use `-D warnings` for rustflags (which makes compilation fail in presence of warnings during build process). Placing that in developers `config.toml`
|
||||
# would be incovenient.
|
||||
# We *could* override things like RUSTFLAGS manually by setting them as environment variables, but that is less DRY; worse yet, if you forget to set proper environment variables
|
||||
# in one spot, that's going to trigger a rebuild of all of the artifacts. Using ci-config.toml we can define these overrides for CI in one spot and not worry about it.
|
||||
[build]
|
||||
rustflags = ["-D", "warnings"]
|
6
.github/actions/run_tests/action.yml
vendored
6
.github/actions/run_tests/action.yml
vendored
@ -19,16 +19,12 @@ runs:
|
||||
|
||||
- name: Limit target directory size
|
||||
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: |
|
||||
|
75
Cargo.lock
generated
75
Cargo.lock
generated
@ -841,6 +841,17 @@ dependencies = [
|
||||
"rustc-demangle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "backtrace-on-stack-overflow"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fd2d70527f3737a1ad17355e260706c1badebabd1fa06a7a053407380df841b"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"libc",
|
||||
"nix 0.23.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.13.1"
|
||||
@ -1175,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",
|
||||
@ -1192,7 +1205,10 @@ dependencies = [
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"settings2",
|
||||
"smallvec",
|
||||
"ui2",
|
||||
"util",
|
||||
"workspace2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1653,7 +1669,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "collab"
|
||||
version = "0.28.0"
|
||||
version = "0.29.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@ -5559,6 +5575,19 @@ dependencies = [
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.23.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"cc",
|
||||
"cfg-if 1.0.0",
|
||||
"libc",
|
||||
"memoffset 0.6.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.24.3"
|
||||
@ -8859,6 +8888,42 @@ 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"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"backtrace-on-stack-overflow",
|
||||
"chrono",
|
||||
"clap 4.4.4",
|
||||
"editor2",
|
||||
"fuzzy2",
|
||||
"gpui2",
|
||||
"itertools 0.11.0",
|
||||
"language2",
|
||||
"log",
|
||||
"menu2",
|
||||
"picker2",
|
||||
"rust-embed",
|
||||
"serde",
|
||||
"settings2",
|
||||
"simplelog",
|
||||
"smallvec",
|
||||
"story",
|
||||
"strum",
|
||||
"theme",
|
||||
"theme2",
|
||||
"ui2",
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stringprep"
|
||||
version = "0.1.4"
|
||||
@ -9362,6 +9427,7 @@ dependencies = [
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"settings2",
|
||||
"story",
|
||||
"toml 0.5.11",
|
||||
"util",
|
||||
"uuid 1.4.1",
|
||||
@ -9884,7 +9950,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",
|
||||
@ -10225,6 +10291,7 @@ dependencies = [
|
||||
"serde",
|
||||
"settings2",
|
||||
"smallvec",
|
||||
"story",
|
||||
"strum",
|
||||
"theme2",
|
||||
]
|
||||
@ -11363,6 +11430,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion 1.0.5",
|
||||
"async-trait",
|
||||
"bincode",
|
||||
"call2",
|
||||
"client2",
|
||||
@ -11475,7 +11543,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.114.0"
|
||||
version = "0.115.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"ai",
|
||||
@ -11623,6 +11691,7 @@ dependencies = [
|
||||
"async-recursion 0.3.2",
|
||||
"async-tar",
|
||||
"async-trait",
|
||||
"audio2",
|
||||
"auto_update2",
|
||||
"backtrace",
|
||||
"call2",
|
||||
|
10
Cargo.toml
10
Cargo.toml
@ -97,8 +97,7 @@ members = [
|
||||
"crates/sqlez",
|
||||
"crates/sqlez_macros",
|
||||
"crates/rich_text",
|
||||
# "crates/storybook2",
|
||||
# "crates/storybook3",
|
||||
"crates/storybook2",
|
||||
"crates/sum_tree",
|
||||
"crates/terminal",
|
||||
"crates/terminal2",
|
||||
@ -112,6 +111,7 @@ members = [
|
||||
"crates/ui2",
|
||||
"crates/util",
|
||||
"crates/semantic_index",
|
||||
"crates/story",
|
||||
"crates/vim",
|
||||
"crates/vcs_menu",
|
||||
"crates/workspace2",
|
||||
@ -197,8 +197,9 @@ tree-sitter-lua = "0.0.14"
|
||||
tree-sitter-nix = { git = "https://github.com/nix-community/tree-sitter-nix", rev = "66e3e9ce9180ae08fc57372061006ef83f0abde7" }
|
||||
tree-sitter-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"}
|
||||
|
||||
[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
|
||||
@ -210,11 +211,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
|
||||
}
|
||||
|
@ -1,12 +1,13 @@
|
||||
use gpui::{div, Div, EventEmitter, ParentElement, Render, SemanticVersion, ViewContext};
|
||||
use gpui::{
|
||||
div, DismissEvent, Div, EventEmitter, ParentElement, Render, SemanticVersion, ViewContext,
|
||||
};
|
||||
use menu::Cancel;
|
||||
use workspace::notifications::NotificationEvent;
|
||||
|
||||
pub struct UpdateNotification {
|
||||
_version: SemanticVersion,
|
||||
}
|
||||
|
||||
impl EventEmitter<NotificationEvent> for UpdateNotification {}
|
||||
impl EventEmitter<DismissEvent> for UpdateNotification {}
|
||||
|
||||
impl Render for UpdateNotification {
|
||||
type Element = Div;
|
||||
@ -82,6 +83,6 @@ impl UpdateNotification {
|
||||
}
|
||||
|
||||
pub fn _dismiss(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(NotificationEvent::Dismiss);
|
||||
cx.emit(DismissEvent::Dismiss);
|
||||
}
|
||||
}
|
||||
|
@ -31,15 +31,19 @@ media = { path = "../media" }
|
||||
project = { package = "project2", path = "../project2" }
|
||||
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,205 @@ pub fn report_call_event_for_channel(
|
||||
)
|
||||
}
|
||||
|
||||
pub struct Call {
|
||||
active_call: Option<(Model<ActiveCall>, Vec<Subscription>)>,
|
||||
parent_workspace: WeakView<Workspace>,
|
||||
}
|
||||
|
||||
impl Call {
|
||||
pub fn new(
|
||||
parent_workspace: WeakView<Workspace>,
|
||||
cx: &mut ViewContext<'_, Workspace>,
|
||||
) -> Box<dyn CallHandler> {
|
||||
let mut active_call = None;
|
||||
if cx.has_global::<Model<ActiveCall>>() {
|
||||
let call = cx.global::<Model<ActiveCall>>().clone();
|
||||
let subscriptions = vec![cx.subscribe(&call, Self::on_active_call_event)];
|
||||
active_call = Some((call, subscriptions));
|
||||
}
|
||||
Box::new(Self {
|
||||
active_call,
|
||||
parent_workspace,
|
||||
})
|
||||
}
|
||||
fn on_active_call_event(
|
||||
workspace: &mut Workspace,
|
||||
_: Model<ActiveCall>,
|
||||
event: &room::Event,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
match event {
|
||||
room::Event::ParticipantLocationChanged { participant_id }
|
||||
| room::Event::RemoteVideoTracksChanged { participant_id } => {
|
||||
workspace.leader_updated(*participant_id, cx);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl CallHandler for Call {
|
||||
fn peer_state(
|
||||
&mut self,
|
||||
leader_id: PeerId,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) -> Option<(bool, bool)> {
|
||||
let (call, _) = self.active_call.as_ref()?;
|
||||
let room = call.read(cx).room()?.read(cx);
|
||||
let participant = room.remote_participant_for_peer_id(leader_id)?;
|
||||
|
||||
let leader_in_this_app;
|
||||
let leader_in_this_project;
|
||||
match participant.location {
|
||||
ParticipantLocation::SharedProject { project_id } => {
|
||||
leader_in_this_app = true;
|
||||
leader_in_this_project = Some(project_id)
|
||||
== self
|
||||
.parent_workspace
|
||||
.update(cx, |this, cx| this.project().read(cx).remote_id())
|
||||
.log_err()
|
||||
.flatten();
|
||||
}
|
||||
ParticipantLocation::UnsharedProject => {
|
||||
leader_in_this_app = true;
|
||||
leader_in_this_project = false;
|
||||
}
|
||||
ParticipantLocation::External => {
|
||||
leader_in_this_app = false;
|
||||
leader_in_this_project = false;
|
||||
}
|
||||
};
|
||||
|
||||
Some((leader_in_this_project, leader_in_this_app))
|
||||
}
|
||||
|
||||
fn shared_screen_for_peer(
|
||||
&self,
|
||||
peer_id: PeerId,
|
||||
pane: &View<Pane>,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) -> Option<Box<dyn ItemHandle>> {
|
||||
let (call, _) = self.active_call.as_ref()?;
|
||||
let room = call.read(cx).room()?.read(cx);
|
||||
let participant = room.remote_participant_for_peer_id(peer_id)?;
|
||||
let track = participant.video_tracks.values().next()?.clone();
|
||||
let user = participant.user.clone();
|
||||
for item in pane.read(cx).items_of_type::<SharedScreen>() {
|
||||
if item.read(cx).peer_id == peer_id {
|
||||
return Some(Box::new(item));
|
||||
}
|
||||
}
|
||||
|
||||
Some(Box::new(cx.build_view(|cx| {
|
||||
SharedScreen::new(&track, peer_id, user.clone(), cx)
|
||||
})))
|
||||
}
|
||||
fn room_id(&self, cx: &AppContext) -> Option<u64> {
|
||||
Some(self.active_call.as_ref()?.0.read(cx).room()?.read(cx).id())
|
||||
}
|
||||
fn hang_up(&self, cx: &mut AppContext) -> Task<Result<()>> {
|
||||
let Some((call, _)) = self.active_call.as_ref() else {
|
||||
return Task::ready(Err(anyhow!("Cannot exit a call; not in a call")));
|
||||
};
|
||||
|
||||
call.update(cx, |this, cx| this.hang_up(cx))
|
||||
}
|
||||
fn active_project(&self, cx: &AppContext) -> Option<WeakModel<Project>> {
|
||||
ActiveCall::global(cx).read(cx).location().cloned()
|
||||
}
|
||||
fn invite(
|
||||
&mut self,
|
||||
called_user_id: u64,
|
||||
initial_project: Option<Model<Project>>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
ActiveCall::global(cx).update(cx, |this, cx| {
|
||||
this.invite(called_user_id, initial_project, cx)
|
||||
})
|
||||
}
|
||||
fn remote_participants(&self, cx: &AppContext) -> Option<Vec<(Arc<User>, PeerId)>> {
|
||||
self.active_call
|
||||
.as_ref()
|
||||
.map(|call| {
|
||||
call.0.read(cx).room().map(|room| {
|
||||
room.read(cx)
|
||||
.remote_participants()
|
||||
.iter()
|
||||
.map(|participant| {
|
||||
(participant.1.user.clone(), participant.1.peer_id.clone())
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
})
|
||||
.flatten()
|
||||
}
|
||||
fn is_muted(&self, cx: &AppContext) -> Option<bool> {
|
||||
self.active_call
|
||||
.as_ref()
|
||||
.map(|call| {
|
||||
call.0
|
||||
.read(cx)
|
||||
.room()
|
||||
.map(|room| room.read(cx).is_muted(cx))
|
||||
})
|
||||
.flatten()
|
||||
}
|
||||
fn toggle_mute(&self, cx: &mut AppContext) {
|
||||
self.active_call.as_ref().map(|call| {
|
||||
call.0.update(cx, |this, cx| {
|
||||
this.room().map(|room| {
|
||||
room.update(cx, |this, cx| {
|
||||
this.toggle_mute(cx).log_err();
|
||||
})
|
||||
})
|
||||
})
|
||||
});
|
||||
}
|
||||
fn toggle_screen_share(&self, cx: &mut AppContext) {
|
||||
self.active_call.as_ref().map(|call| {
|
||||
call.0.update(cx, |this, cx| {
|
||||
this.room().map(|room| {
|
||||
room.update(cx, |this, cx| {
|
||||
if this.is_screen_sharing() {
|
||||
this.unshare_screen(cx).log_err();
|
||||
} else {
|
||||
let t = this.share_screen(cx);
|
||||
cx.spawn(move |_, _| async move {
|
||||
t.await.log_err();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
});
|
||||
}
|
||||
fn toggle_deafen(&self, cx: &mut AppContext) {
|
||||
self.active_call.as_ref().map(|call| {
|
||||
call.0.update(cx, |this, cx| {
|
||||
this.room().map(|room| {
|
||||
room.update(cx, |this, cx| {
|
||||
this.toggle_deafen(cx).log_err();
|
||||
})
|
||||
})
|
||||
})
|
||||
});
|
||||
}
|
||||
fn is_deafened(&self, cx: &AppContext) -> Option<bool> {
|
||||
self.active_call
|
||||
.as_ref()
|
||||
.map(|call| {
|
||||
call.0
|
||||
.read(cx)
|
||||
.room()
|
||||
.map(|room| room.read(cx).is_deafened())
|
||||
})
|
||||
.flatten()
|
||||
.flatten()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
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;
|
||||
|
||||
|
@ -1,7 +1,4 @@
|
||||
use crate::{
|
||||
call_settings::CallSettings,
|
||||
participant::{LocalParticipant, ParticipantLocation, RemoteParticipant},
|
||||
};
|
||||
use crate::participant::{LocalParticipant, ParticipantLocation, RemoteParticipant};
|
||||
use anyhow::{anyhow, Result};
|
||||
use audio::{Audio, Sound};
|
||||
use client::{
|
||||
@ -21,7 +18,6 @@ use live_kit_client::{
|
||||
};
|
||||
use postage::{sink::Sink, stream::Stream, watch};
|
||||
use project::Project;
|
||||
use settings::Settings;
|
||||
use std::{future::Future, mem, sync::Arc, time::Duration};
|
||||
use util::{post_inc, ResultExt, TryFutureExt};
|
||||
|
||||
@ -332,8 +328,10 @@ impl Room {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mute_on_join(cx: &AppContext) -> bool {
|
||||
CallSettings::get_global(cx).mute_on_join || client::IMPERSONATE_LOGIN.is_some()
|
||||
pub fn mute_on_join(_cx: &AppContext) -> bool {
|
||||
// todo!() po: This should be uncommented, though then unmuting does not work
|
||||
false
|
||||
//CallSettings::get_global(cx).mute_on_join || client::IMPERSONATE_LOGIN.is_some()
|
||||
}
|
||||
|
||||
fn from_join_response(
|
||||
|
157
crates/call2/src/shared_screen.rs
Normal file
157
crates/call2/src/shared_screen.rs
Normal file
@ -0,0 +1,157 @@
|
||||
use crate::participant::{Frame, RemoteVideoTrack};
|
||||
use anyhow::Result;
|
||||
use client::{proto::PeerId, User};
|
||||
use futures::StreamExt;
|
||||
use gpui::{
|
||||
div, AppContext, Div, Element, EventEmitter, FocusHandle, FocusableView, ParentElement, Render,
|
||||
SharedString, Task, View, ViewContext, VisualContext, WindowContext,
|
||||
};
|
||||
use std::sync::{Arc, Weak};
|
||||
use workspace::{item::Item, ItemNavHistory, WorkspaceId};
|
||||
|
||||
pub enum Event {
|
||||
Close,
|
||||
}
|
||||
|
||||
pub struct SharedScreen {
|
||||
track: Weak<RemoteVideoTrack>,
|
||||
frame: Option<Frame>,
|
||||
// temporary addition just to render something interactive.
|
||||
current_frame_id: usize,
|
||||
pub peer_id: PeerId,
|
||||
user: Arc<User>,
|
||||
nav_history: Option<ItemNavHistory>,
|
||||
_maintain_frame: Task<Result<()>>,
|
||||
focus: FocusHandle,
|
||||
}
|
||||
|
||||
impl SharedScreen {
|
||||
pub fn new(
|
||||
track: &Arc<RemoteVideoTrack>,
|
||||
peer_id: PeerId,
|
||||
user: Arc<User>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
cx.focus_handle();
|
||||
let mut frames = track.frames();
|
||||
Self {
|
||||
track: Arc::downgrade(track),
|
||||
frame: None,
|
||||
peer_id,
|
||||
user,
|
||||
nav_history: Default::default(),
|
||||
_maintain_frame: cx.spawn(|this, mut cx| async move {
|
||||
while let Some(frame) = frames.next().await {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.frame = Some(frame);
|
||||
cx.notify();
|
||||
})?;
|
||||
}
|
||||
this.update(&mut cx, |_, cx| cx.emit(Event::Close))?;
|
||||
Ok(())
|
||||
}),
|
||||
focus: cx.focus_handle(),
|
||||
current_frame_id: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<Event> for SharedScreen {}
|
||||
impl EventEmitter<workspace::item::ItemEvent> for SharedScreen {}
|
||||
|
||||
impl FocusableView for SharedScreen {
|
||||
fn focus_handle(&self, _: &AppContext) -> FocusHandle {
|
||||
self.focus.clone()
|
||||
}
|
||||
}
|
||||
impl Render for SharedScreen {
|
||||
type Element = Div;
|
||||
fn render(&mut self, _: &mut ViewContext<Self>) -> Self::Element {
|
||||
let frame = self.frame.clone();
|
||||
let frame_id = self.current_frame_id;
|
||||
self.current_frame_id = self.current_frame_id.wrapping_add(1);
|
||||
div().children(frame.map(|_| {
|
||||
ui::Label::new(frame_id.to_string()).color(ui::Color::Error)
|
||||
// img().data(Arc::new(ImageData::new(image::ImageBuffer::new(
|
||||
// frame.width() as u32,
|
||||
// frame.height() as u32,
|
||||
// ))))
|
||||
}))
|
||||
}
|
||||
}
|
||||
// impl View for SharedScreen {
|
||||
// fn ui_name() -> &'static str {
|
||||
// "SharedScreen"
|
||||
// }
|
||||
|
||||
// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
// enum Focus {}
|
||||
|
||||
// let frame = self.frame.clone();
|
||||
// MouseEventHandler::new::<Focus, _>(0, cx, |_, cx| {
|
||||
// Canvas::new(move |bounds, _, _, cx| {
|
||||
// if let Some(frame) = frame.clone() {
|
||||
// let size = constrain_size_preserving_aspect_ratio(
|
||||
// bounds.size(),
|
||||
// vec2f(frame.width() as f32, frame.height() as f32),
|
||||
// );
|
||||
// let origin = bounds.origin() + (bounds.size() / 2.) - size / 2.;
|
||||
// cx.scene().push_surface(gpui::platform::mac::Surface {
|
||||
// bounds: RectF::new(origin, size),
|
||||
// image_buffer: frame.image(),
|
||||
// });
|
||||
// }
|
||||
// })
|
||||
// .contained()
|
||||
// .with_style(theme::current(cx).shared_screen)
|
||||
// })
|
||||
// .on_down(MouseButton::Left, |_, _, cx| cx.focus_parent())
|
||||
// .into_any()
|
||||
// }
|
||||
// }
|
||||
|
||||
impl Item for SharedScreen {
|
||||
fn tab_tooltip_text(&self, _: &AppContext) -> Option<SharedString> {
|
||||
Some(format!("{}'s screen", self.user.github_login).into())
|
||||
}
|
||||
fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if let Some(nav_history) = self.nav_history.as_mut() {
|
||||
nav_history.push::<()>(None, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn tab_content(&self, _: Option<usize>, _: &WindowContext<'_>) -> gpui::AnyElement {
|
||||
div().child("Shared screen").into_any()
|
||||
// Flex::row()
|
||||
// .with_child(
|
||||
// Svg::new("icons/desktop.svg")
|
||||
// .with_color(style.label.text.color)
|
||||
// .constrained()
|
||||
// .with_width(style.type_icon_width)
|
||||
// .aligned()
|
||||
// .contained()
|
||||
// .with_margin_right(style.spacing),
|
||||
// )
|
||||
// .with_child(
|
||||
// Label::new(
|
||||
// format!("{}'s screen", self.user.github_login),
|
||||
// style.label.clone(),
|
||||
// )
|
||||
// .aligned(),
|
||||
// )
|
||||
// .into_any()
|
||||
}
|
||||
|
||||
fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext<Self>) {
|
||||
self.nav_history = Some(history);
|
||||
}
|
||||
|
||||
fn clone_on_split(
|
||||
&self,
|
||||
_workspace_id: WorkspaceId,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<View<Self>> {
|
||||
let track = self.track.upgrade()?;
|
||||
Some(cx.build_view(|cx| Self::new(&track, self.peer_id, self.user.clone(), cx)))
|
||||
}
|
||||
}
|
@ -109,6 +109,10 @@ pub enum ClickhouseEvent {
|
||||
virtual_memory_in_bytes: u64,
|
||||
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| {
|
||||
|
@ -157,15 +157,17 @@ const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel";
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use client::{Client, Contact, UserStore};
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use gpui::{
|
||||
actions, div, serde_json, AppContext, AsyncWindowContext, Div, EventEmitter, FocusHandle,
|
||||
Focusable, FocusableView, InteractiveElement, ParentElement, Render, View, ViewContext,
|
||||
VisualContext, WeakView,
|
||||
Focusable, FocusableView, InteractiveElement, Model, ParentElement, Render, Styled, View,
|
||||
ViewContext, VisualContext, WeakView,
|
||||
};
|
||||
use project::Fs;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
use ui::{h_stack, Avatar, Label};
|
||||
use util::ResultExt;
|
||||
use workspace::{
|
||||
dock::{DockPosition, Panel, PanelEvent},
|
||||
@ -299,8 +301,8 @@ pub struct CollabPanel {
|
||||
// channel_editing_state: Option<ChannelEditingState>,
|
||||
// entries: Vec<ListEntry>,
|
||||
// selection: Option<usize>,
|
||||
// user_store: ModelHandle<UserStore>,
|
||||
// client: Arc<Client>,
|
||||
user_store: Model<UserStore>,
|
||||
_client: Arc<Client>,
|
||||
// channel_store: ModelHandle<ChannelStore>,
|
||||
// project: ModelHandle<Project>,
|
||||
// match_candidates: Vec<StringMatchCandidate>,
|
||||
@ -595,7 +597,7 @@ impl CollabPanel {
|
||||
// entries: Vec::default(),
|
||||
// channel_editing_state: None,
|
||||
// selection: None,
|
||||
// user_store: workspace.user_store().clone(),
|
||||
user_store: workspace.user_store().clone(),
|
||||
// channel_store: ChannelStore::global(cx),
|
||||
// project: workspace.project().clone(),
|
||||
// subscriptions: Vec::default(),
|
||||
@ -603,7 +605,7 @@ impl CollabPanel {
|
||||
// collapsed_sections: vec![Section::Offline],
|
||||
// collapsed_channels: Vec::default(),
|
||||
_workspace: workspace.weak_handle(),
|
||||
// client: workspace.app_state().client.clone(),
|
||||
_client: workspace.app_state().client.clone(),
|
||||
// context_menu_on_selected: true,
|
||||
// drag_target_channel: ChannelDragTarget::None,
|
||||
// list_state,
|
||||
@ -663,6 +665,9 @@ impl CollabPanel {
|
||||
})
|
||||
}
|
||||
|
||||
fn contacts(&self, cx: &AppContext) -> Option<Vec<Arc<Contact>>> {
|
||||
Some(self.user_store.read(cx).contacts().to_owned())
|
||||
}
|
||||
pub async fn load(
|
||||
workspace: WeakView<Workspace>,
|
||||
mut cx: AsyncWindowContext,
|
||||
@ -3297,11 +3302,38 @@ impl CollabPanel {
|
||||
impl Render for CollabPanel {
|
||||
type Element = Focusable<Div>;
|
||||
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
let contacts = self.contacts(cx).unwrap_or_default();
|
||||
let workspace = self._workspace.clone();
|
||||
div()
|
||||
.key_context("CollabPanel")
|
||||
.track_focus(&self.focus_handle)
|
||||
.child("COLLAB PANEL")
|
||||
.children(contacts.into_iter().map(|contact| {
|
||||
let id = contact.user.id;
|
||||
h_stack()
|
||||
.p_2()
|
||||
.gap_2()
|
||||
.children(
|
||||
contact
|
||||
.user
|
||||
.avatar
|
||||
.as_ref()
|
||||
.map(|avatar| Avatar::data(avatar.clone())),
|
||||
)
|
||||
.child(Label::new(contact.user.github_login.clone()))
|
||||
.on_mouse_down(gpui::MouseButton::Left, {
|
||||
let workspace = workspace.clone();
|
||||
move |_, cx| {
|
||||
workspace
|
||||
.update(cx, |this, cx| {
|
||||
this.call_state()
|
||||
.invite(id, None, cx)
|
||||
.detach_and_log_err(cx)
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
})
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -31,15 +31,18 @@ use std::sync::Arc;
|
||||
use call::ActiveCall;
|
||||
use client::{Client, UserStore};
|
||||
use gpui::{
|
||||
div, px, rems, AppContext, Div, InteractiveElement, Model, ParentElement, Render, RenderOnce,
|
||||
Stateful, StatefulInteractiveElement, 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 ui::{h_stack, Avatar, Button, ButtonVariant, Color, IconButton, KeyBinding, Tooltip};
|
||||
use util::ResultExt;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::face_pile::FacePile;
|
||||
|
||||
// const MAX_PROJECT_NAME_LENGTH: usize = 40;
|
||||
// const MAX_BRANCH_NAME_LENGTH: usize = 40;
|
||||
|
||||
@ -85,6 +88,41 @@ impl Render for CollabTitlebarItem {
|
||||
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()
|
||||
@ -111,17 +149,21 @@ 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)),
|
||||
)
|
||||
// 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)),
|
||||
@ -129,11 +171,13 @@ impl Render for CollabTitlebarItem {
|
||||
// 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| {
|
||||
cx.build_view(|_| {
|
||||
@ -149,8 +193,111 @@ 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 |cx| async move {
|
||||
client.authenticate_and_connect(true, &cx).await?;
|
||||
Ok::<(), anyhow::Error>(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,11 +7,14 @@ pub mod notification_panel;
|
||||
pub mod notifications;
|
||||
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,8 +1,9 @@
|
||||
use collections::{CommandPaletteFilter, HashMap};
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
actions, div, prelude::*, Action, AppContext, Div, EventEmitter, FocusHandle, FocusableView,
|
||||
Keystroke, Manager, ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView,
|
||||
actions, div, prelude::*, Action, AppContext, DismissEvent, Div, EventEmitter, FocusHandle,
|
||||
FocusableView, Keystroke, ParentElement, Render, Styled, View, ViewContext, VisualContext,
|
||||
WeakView,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use std::{
|
||||
@ -68,7 +69,7 @@ impl CommandPalette {
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<Manager> for CommandPalette {}
|
||||
impl EventEmitter<DismissEvent> for CommandPalette {}
|
||||
|
||||
impl FocusableView for CommandPalette {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
@ -268,7 +269,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();
|
||||
}
|
||||
|
||||
|
@ -14,8 +14,8 @@ use editor::{
|
||||
use futures::future::try_join_all;
|
||||
use gpui::{
|
||||
actions, div, AnyElement, AnyView, AppContext, Context, Div, EventEmitter, FocusEvent,
|
||||
FocusHandle, Focusable, FocusableElement, FocusableView, InteractiveElement, Model,
|
||||
ParentElement, Render, RenderOnce, SharedString, Styled, Subscription, Task, View, ViewContext,
|
||||
FocusHandle, Focusable, FocusableElement, FocusableView, InteractiveElement, IntoElement,
|
||||
Model, ParentElement, Render, SharedString, Styled, Subscription, Task, View, ViewContext,
|
||||
VisualContext, WeakView, WindowContext,
|
||||
};
|
||||
use language::{
|
||||
@ -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},
|
||||
@ -778,28 +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()
|
||||
.into_any_element()
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn render_summary(summary: &DiagnosticSummary) -> AnyElement {
|
||||
if summary.error_count == 0 && summary.warning_count == 0 {
|
||||
let label = Label::new("No problems");
|
||||
label.render_into_any()
|
||||
label.into_any_element()
|
||||
} else {
|
||||
h_stack()
|
||||
.bg(gpui::red())
|
||||
@ -807,7 +807,7 @@ pub(crate) fn render_summary(summary: &DiagnosticSummary) -> AnyElement {
|
||||
.child(Label::new(summary.error_count.to_string()))
|
||||
.child(IconElement::new(Icon::ExclamationTriangle))
|
||||
.child(Label::new(summary.warning_count.to_string()))
|
||||
.render_into_any()
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
@ -1550,7 +1550,7 @@ mod tests {
|
||||
block_id: ix,
|
||||
editor_style: &editor::EditorStyle::default(),
|
||||
})
|
||||
.element_id()?
|
||||
.inner_id()?
|
||||
.try_into()
|
||||
.ok()?,
|
||||
|
||||
|
@ -7,7 +7,7 @@ use gpui::{
|
||||
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;
|
||||
@ -26,25 +26,25 @@ impl Render for DiagnosticIndicator {
|
||||
|
||||
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())
|
||||
.id("diagnostic-indicator")
|
||||
.on_action(cx.listener(Self::go_to_next_diagnostic))
|
||||
.rounded_md()
|
||||
.flex_none()
|
||||
|
@ -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,
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -6740,75 +6740,6 @@ async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) {
|
||||
// );
|
||||
// }
|
||||
|
||||
#[test]
|
||||
fn test_combine_syntax_and_fuzzy_match_highlights() {
|
||||
let string = "abcdefghijklmnop";
|
||||
let syntax_ranges = [
|
||||
(
|
||||
0..3,
|
||||
HighlightStyle {
|
||||
color: Some(Hsla::red()),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
(
|
||||
4..8,
|
||||
HighlightStyle {
|
||||
color: Some(Hsla::green()),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
];
|
||||
let match_indices = [4, 6, 7, 8];
|
||||
assert_eq!(
|
||||
combine_syntax_and_fuzzy_match_highlights(
|
||||
string,
|
||||
Default::default(),
|
||||
syntax_ranges.into_iter(),
|
||||
&match_indices,
|
||||
),
|
||||
&[
|
||||
(
|
||||
0..3,
|
||||
HighlightStyle {
|
||||
color: Some(Hsla::red()),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
(
|
||||
4..5,
|
||||
HighlightStyle {
|
||||
color: Some(Hsla::green()),
|
||||
font_weight: Some(gpui::FontWeight::BOLD),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
(
|
||||
5..6,
|
||||
HighlightStyle {
|
||||
color: Some(Hsla::green()),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
(
|
||||
6..8,
|
||||
HighlightStyle {
|
||||
color: Some(Hsla::green()),
|
||||
font_weight: Some(gpui::FontWeight::BOLD),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
(
|
||||
8..9,
|
||||
HighlightStyle {
|
||||
font_weight: Some(gpui::FontWeight::BOLD),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
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(),
|
||||
|
@ -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::{
|
||||
@ -604,7 +604,7 @@ impl Item for Editor {
|
||||
&description,
|
||||
MAX_TAB_TITLE_LEN,
|
||||
))
|
||||
.color(TextColor::Muted),
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
})),
|
||||
|
@ -2,8 +2,8 @@ use collections::HashMap;
|
||||
use editor::{scroll::autoscroll::Autoscroll, Bias, Editor};
|
||||
use fuzzy::{CharBag, PathMatch, PathMatchCandidate};
|
||||
use gpui::{
|
||||
actions, div, AppContext, Div, EventEmitter, FocusHandle, FocusableView, InteractiveElement,
|
||||
Manager, Model, ParentElement, Render, RenderOnce, Styled, Task, View, ViewContext,
|
||||
actions, div, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView,
|
||||
InteractiveElement, IntoElement, Model, ParentElement, Render, Styled, Task, View, ViewContext,
|
||||
VisualContext, WeakView,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
@ -111,7 +111,7 @@ 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)
|
||||
@ -690,7 +690,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 +702,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();
|
||||
}
|
||||
|
||||
|
@ -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,13 +1,13 @@
|
||||
use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Editor};
|
||||
use gpui::{
|
||||
actions, div, prelude::*, AppContext, Div, EventEmitter, FocusHandle, FocusableView, Manager,
|
||||
Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext, WindowContext,
|
||||
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);
|
||||
|
||||
@ -25,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));
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
@ -88,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),
|
||||
_ => {}
|
||||
}
|
||||
@ -123,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>) {
|
||||
@ -140,7 +142,7 @@ impl GoToLine {
|
||||
self.prev_scroll_position.take();
|
||||
}
|
||||
|
||||
cx.emit(Manager::Dismiss);
|
||||
cx.emit(DismissEvent::Dismiss);
|
||||
}
|
||||
}
|
||||
|
||||
@ -176,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,6 +10,7 @@ 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::*;
|
||||
|
||||
@ -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"))?
|
||||
|
@ -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};
|
||||
@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -611,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()
|
||||
}
|
||||
|
@ -12,15 +12,15 @@ pub trait Render: 'static + Sized {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element;
|
||||
}
|
||||
|
||||
pub trait RenderOnce: Sized {
|
||||
pub trait IntoElement: Sized {
|
||||
type Element: Element + 'static;
|
||||
|
||||
fn element_id(&self) -> Option<ElementId>;
|
||||
|
||||
fn render_once(self) -> Self::Element;
|
||||
fn into_element(self) -> Self::Element;
|
||||
|
||||
fn render_into_any(self) -> AnyElement {
|
||||
self.render_once().into_any()
|
||||
fn into_any_element(self) -> AnyElement {
|
||||
self.into_element().into_any()
|
||||
}
|
||||
|
||||
fn draw<T, R>(
|
||||
@ -33,7 +33,7 @@ pub trait RenderOnce: Sized {
|
||||
where
|
||||
T: Clone + Default + Debug + Into<AvailableSpace>,
|
||||
{
|
||||
let element = self.render_once();
|
||||
let element = self.into_element();
|
||||
let element_id = element.element_id();
|
||||
let element = DrawableElement {
|
||||
element: Some(element),
|
||||
@ -57,7 +57,7 @@ pub trait RenderOnce: Sized {
|
||||
fn map<U>(self, f: impl FnOnce(Self) -> U) -> U
|
||||
where
|
||||
Self: Sized,
|
||||
U: RenderOnce,
|
||||
U: IntoElement,
|
||||
{
|
||||
f(self)
|
||||
}
|
||||
@ -83,7 +83,7 @@ pub trait RenderOnce: Sized {
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Element: 'static + RenderOnce {
|
||||
pub trait Element: 'static + IntoElement {
|
||||
type State: 'static;
|
||||
|
||||
fn layout(
|
||||
@ -99,30 +99,30 @@ pub trait Element: 'static + RenderOnce {
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Component: 'static {
|
||||
type Rendered: RenderOnce;
|
||||
pub trait RenderOnce: 'static {
|
||||
type Rendered: IntoElement;
|
||||
|
||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered;
|
||||
}
|
||||
|
||||
pub struct CompositeElement<C> {
|
||||
pub struct Component<C> {
|
||||
component: Option<C>,
|
||||
}
|
||||
|
||||
pub struct CompositeElementState<C: Component> {
|
||||
rendered_element: Option<<C::Rendered as RenderOnce>::Element>,
|
||||
rendered_element_state: <<C::Rendered as RenderOnce>::Element as Element>::State,
|
||||
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> CompositeElement<C> {
|
||||
impl<C> Component<C> {
|
||||
pub fn new(component: C) -> Self {
|
||||
CompositeElement {
|
||||
Component {
|
||||
component: Some(component),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Component> Element for CompositeElement<C> {
|
||||
impl<C: RenderOnce> Element for Component<C> {
|
||||
type State = CompositeElementState<C>;
|
||||
|
||||
fn layout(
|
||||
@ -130,7 +130,7 @@ impl<C: Component> Element for CompositeElement<C> {
|
||||
state: Option<Self::State>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::State) {
|
||||
let mut element = self.component.take().unwrap().render(cx).render_once();
|
||||
let mut element = self.component.take().unwrap().render(cx).into_element();
|
||||
let (layout_id, state) = element.layout(state.map(|s| s.rendered_element_state), cx);
|
||||
let state = CompositeElementState {
|
||||
rendered_element: Some(element),
|
||||
@ -148,14 +148,14 @@ impl<C: Component> Element for CompositeElement<C> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Component> RenderOnce for CompositeElement<C> {
|
||||
impl<C: RenderOnce> IntoElement for Component<C> {
|
||||
type Element = Self;
|
||||
|
||||
fn element_id(&self) -> Option<ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn render_once(self) -> Self::Element {
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
@ -166,23 +166,20 @@ pub struct GlobalElementId(SmallVec<[ElementId; 32]>);
|
||||
pub trait ParentElement {
|
||||
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]>;
|
||||
|
||||
fn child(mut self, child: impl RenderOnce) -> Self
|
||||
fn child(mut self, child: impl IntoElement) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.children_mut().push(child.render_once().into_any());
|
||||
self.children_mut().push(child.into_element().into_any());
|
||||
self
|
||||
}
|
||||
|
||||
fn children(mut self, children: impl IntoIterator<Item = impl RenderOnce>) -> 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.render_once().into_any()),
|
||||
);
|
||||
self.children_mut()
|
||||
.extend(children.into_iter().map(|child| child.into_any_element()));
|
||||
self
|
||||
}
|
||||
}
|
||||
@ -432,10 +429,6 @@ impl AnyElement {
|
||||
AnyElement(Box::new(Some(DrawableElement::new(element))) as Box<dyn ElementObject>)
|
||||
}
|
||||
|
||||
pub fn element_id(&self) -> Option<ElementId> {
|
||||
self.0.element_id()
|
||||
}
|
||||
|
||||
pub fn layout(&mut self, cx: &mut WindowContext) -> LayoutId {
|
||||
self.0.layout(cx)
|
||||
}
|
||||
@ -467,6 +460,10 @@ impl AnyElement {
|
||||
pub fn into_any(self) -> AnyElement {
|
||||
AnyElement::new(self)
|
||||
}
|
||||
|
||||
pub fn inner_id(&self) -> Option<ElementId> {
|
||||
self.0.element_id()
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for AnyElement {
|
||||
@ -486,14 +483,14 @@ impl Element for AnyElement {
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for AnyElement {
|
||||
impl IntoElement for AnyElement {
|
||||
type Element = Self;
|
||||
|
||||
fn element_id(&self) -> Option<ElementId> {
|
||||
AnyElement::element_id(self)
|
||||
None
|
||||
}
|
||||
|
||||
fn render_once(self) -> Self::Element {
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
use crate::{
|
||||
point, px, Action, AnyDrag, AnyElement, AnyTooltip, AnyView, AppContext, BorrowAppContext,
|
||||
BorrowWindow, Bounds, ClickEvent, DispatchPhase, Element, ElementId, FocusEvent, FocusHandle,
|
||||
KeyContext, KeyDownEvent, KeyUpEvent, LayoutId, MouseButton, MouseDownEvent, MouseMoveEvent,
|
||||
MouseUpEvent, ParentElement, Pixels, Point, Render, RenderOnce, ScrollWheelEvent, SharedString,
|
||||
Size, Style, StyleRefinement, Styled, Task, View, Visibility, WindowContext,
|
||||
IntoElement, KeyContext, KeyDownEvent, KeyUpEvent, LayoutId, MouseButton, MouseDownEvent,
|
||||
MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Point, Render, ScrollWheelEvent,
|
||||
SharedString, Size, Style, StyleRefinement, Styled, Task, View, Visibility, WindowContext,
|
||||
};
|
||||
use collections::HashMap;
|
||||
use refineable::Refineable;
|
||||
@ -666,14 +666,14 @@ impl Element for Div {
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Div {
|
||||
impl IntoElement for Div {
|
||||
type Element = Self;
|
||||
|
||||
fn element_id(&self) -> Option<ElementId> {
|
||||
self.interactivity.element_id.clone()
|
||||
}
|
||||
|
||||
fn render_once(self) -> Self::Element {
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
@ -1278,7 +1278,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> RenderOnce for Focusable<E>
|
||||
impl<E> IntoElement for Focusable<E>
|
||||
where
|
||||
E: Element,
|
||||
{
|
||||
@ -1288,7 +1288,7 @@ where
|
||||
self.element.element_id()
|
||||
}
|
||||
|
||||
fn render_once(self) -> Self::Element {
|
||||
fn into_element(self) -> Self::Element {
|
||||
self.element
|
||||
}
|
||||
}
|
||||
@ -1352,7 +1352,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> RenderOnce for Stateful<E>
|
||||
impl<E> IntoElement for Stateful<E>
|
||||
where
|
||||
E: Element,
|
||||
{
|
||||
@ -1362,7 +1362,7 @@ where
|
||||
self.element.element_id()
|
||||
}
|
||||
|
||||
fn render_once(self) -> Self::Element {
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
@ -1,30 +1,59 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
Bounds, Element, InteractiveElement, InteractiveElementState, Interactivity, LayoutId, Pixels,
|
||||
RenderOnce, SharedString, StyleRefinement, Styled, WindowContext,
|
||||
Bounds, Element, ImageData, InteractiveElement, InteractiveElementState, Interactivity,
|
||||
IntoElement, LayoutId, Pixels, SharedString, StyleRefinement, Styled, WindowContext,
|
||||
};
|
||||
use futures::FutureExt;
|
||||
use util::ResultExt;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum ImageSource {
|
||||
/// Image content will be loaded from provided URI at render time.
|
||||
Uri(SharedString),
|
||||
Data(Arc<ImageData>),
|
||||
}
|
||||
|
||||
impl From<SharedString> for ImageSource {
|
||||
fn from(value: SharedString) -> Self {
|
||||
Self::Uri(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Arc<ImageData>> for ImageSource {
|
||||
fn from(value: Arc<ImageData>) -> Self {
|
||||
Self::Data(value)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Img {
|
||||
interactivity: Interactivity,
|
||||
uri: Option<SharedString>,
|
||||
source: Option<ImageSource>,
|
||||
grayscale: bool,
|
||||
}
|
||||
|
||||
pub fn img() -> Img {
|
||||
Img {
|
||||
interactivity: Interactivity::default(),
|
||||
uri: None,
|
||||
source: None,
|
||||
grayscale: false,
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@ -58,42 +87,47 @@ impl Element for Img {
|
||||
|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 RenderOnce for Img {
|
||||
impl IntoElement for Img {
|
||||
type Element = Self;
|
||||
|
||||
fn element_id(&self) -> Option<crate::ElementId> {
|
||||
self.interactivity.element_id.clone()
|
||||
}
|
||||
|
||||
fn render_once(self) -> Self::Element {
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
@ -2,8 +2,8 @@ use smallvec::SmallVec;
|
||||
use taffy::style::{Display, Position};
|
||||
|
||||
use crate::{
|
||||
point, AnyElement, BorrowWindow, Bounds, Element, LayoutId, ParentElement, Pixels, Point,
|
||||
RenderOnce, Size, Style, WindowContext,
|
||||
point, AnyElement, BorrowWindow, Bounds, Element, IntoElement, LayoutId, ParentElement, Pixels,
|
||||
Point, Size, Style, WindowContext,
|
||||
};
|
||||
|
||||
pub struct OverlayState {
|
||||
@ -144,21 +144,23 @@ impl Element for Overlay {
|
||||
}
|
||||
|
||||
cx.with_element_offset(desired.origin - bounds.origin, |cx| {
|
||||
for child in self.children {
|
||||
child.paint(cx);
|
||||
}
|
||||
cx.break_content_mask(|cx| {
|
||||
for child in self.children {
|
||||
child.paint(cx);
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Overlay {
|
||||
impl IntoElement for Overlay {
|
||||
type Element = Self;
|
||||
|
||||
fn element_id(&self) -> Option<crate::ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn render_once(self) -> Self::Element {
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
Bounds, Element, ElementId, InteractiveElement, InteractiveElementState, Interactivity,
|
||||
LayoutId, Pixels, RenderOnce, SharedString, StyleRefinement, Styled, WindowContext,
|
||||
IntoElement, LayoutId, Pixels, SharedString, StyleRefinement, Styled, WindowContext,
|
||||
};
|
||||
use util::ResultExt;
|
||||
|
||||
@ -49,14 +49,14 @@ impl Element for Svg {
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Svg {
|
||||
impl IntoElement for Svg {
|
||||
type Element = Self;
|
||||
|
||||
fn element_id(&self) -> Option<ElementId> {
|
||||
self.interactivity.element_id.clone()
|
||||
}
|
||||
|
||||
fn render_once(self) -> Self::Element {
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
use crate::{
|
||||
Bounds, Element, ElementId, LayoutId, Pixels, RenderOnce, SharedString, Size, TextRun,
|
||||
WindowContext, 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;
|
||||
|
||||
impl Element for &'static str {
|
||||
@ -26,14 +26,14 @@ impl Element for &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for &'static str {
|
||||
impl IntoElement for &'static str {
|
||||
type Element = Self;
|
||||
|
||||
fn element_id(&self) -> Option<ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn render_once(self) -> Self::Element {
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
@ -57,35 +57,40 @@ impl Element for SharedString {
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for SharedString {
|
||||
impl IntoElement for SharedString {
|
||||
type Element = Self;
|
||||
|
||||
fn element_id(&self) -> Option<ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn render_once(self) -> Self::Element {
|
||||
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 StyledText {
|
||||
/// Renders text with runs of different styles.
|
||||
///
|
||||
/// Callers are responsible for setting the correct style for each run.
|
||||
/// For text with a uniform style, you can usually avoid calling this constructor
|
||||
/// and just pass text directly.
|
||||
pub fn new(text: SharedString, runs: Vec<TextRun>) -> Self {
|
||||
pub fn new(text: impl Into<SharedString>) -> Self {
|
||||
StyledText {
|
||||
text,
|
||||
runs: Some(runs),
|
||||
text: text.into(),
|
||||
runs: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_runs(mut self, runs: Vec<TextRun>) -> Self {
|
||||
self.runs = Some(runs);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for StyledText {
|
||||
@ -106,14 +111,14 @@ impl Element for StyledText {
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for StyledText {
|
||||
impl IntoElement for StyledText {
|
||||
type Element = Self;
|
||||
|
||||
fn element_id(&self) -> Option<crate::ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn render_once(self) -> Self::Element {
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
@ -159,10 +164,14 @@ impl TextState {
|
||||
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()
|
||||
@ -174,10 +183,7 @@ impl TextState {
|
||||
|
||||
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 {
|
||||
@ -194,7 +200,7 @@ impl TextState {
|
||||
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 {
|
||||
@ -225,16 +231,77 @@ impl TextState {
|
||||
line_origin.y += line.size(line_height).height;
|
||||
}
|
||||
}
|
||||
|
||||
fn index_for_position(&self, bounds: Bounds<Pixels>, position: Point<Pixels>) -> Option<usize> {
|
||||
if !bounds.contains_point(&position) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let element_state = self.lock();
|
||||
let element_state = element_state
|
||||
.as_ref()
|
||||
.expect("measurement has not been performed");
|
||||
|
||||
let line_height = element_state.line_height;
|
||||
let mut line_origin = bounds.origin;
|
||||
let mut line_start_ix = 0;
|
||||
for line in &element_state.lines {
|
||||
let line_bottom = line_origin.y + line.size(line_height).height;
|
||||
if position.y > line_bottom {
|
||||
line_origin.y = line_bottom;
|
||||
line_start_ix += line.len() + 1;
|
||||
} else {
|
||||
let position_within_line = position - line_origin;
|
||||
let index_within_line =
|
||||
line.index_for_position(position_within_line, line_height)?;
|
||||
return Some(line_start_ix + index_within_line);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
struct InteractiveText {
|
||||
pub struct InteractiveText {
|
||||
element_id: ElementId,
|
||||
text: StyledText,
|
||||
click_listener: Option<Box<dyn Fn(InteractiveTextClickEvent, &mut WindowContext<'_>)>>,
|
||||
}
|
||||
|
||||
struct InteractiveTextState {
|
||||
struct InteractiveTextClickEvent {
|
||||
mouse_down_index: usize,
|
||||
mouse_up_index: usize,
|
||||
}
|
||||
|
||||
pub struct InteractiveTextState {
|
||||
text_state: TextState,
|
||||
clicked_range_ixs: Rc<Cell<SmallVec<[usize; 1]>>>,
|
||||
mouse_down_index: Rc<Cell<Option<usize>>>,
|
||||
}
|
||||
|
||||
impl InteractiveText {
|
||||
pub fn new(id: impl Into<ElementId>, text: StyledText) -> Self {
|
||||
Self {
|
||||
element_id: id.into(),
|
||||
text,
|
||||
click_listener: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_click(
|
||||
mut self,
|
||||
ranges: Vec<Range<usize>>,
|
||||
listener: impl Fn(usize, &mut WindowContext<'_>) + 'static,
|
||||
) -> Self {
|
||||
self.click_listener = Some(Box::new(move |event, cx| {
|
||||
for (range_ix, range) in ranges.iter().enumerate() {
|
||||
if range.contains(&event.mouse_down_index) && range.contains(&event.mouse_up_index)
|
||||
{
|
||||
listener(range_ix, cx);
|
||||
}
|
||||
}
|
||||
}));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for InteractiveText {
|
||||
@ -246,39 +313,74 @@ impl Element for InteractiveText {
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::State) {
|
||||
if let Some(InteractiveTextState {
|
||||
text_state,
|
||||
clicked_range_ixs,
|
||||
mouse_down_index, ..
|
||||
}) = state
|
||||
{
|
||||
let (layout_id, text_state) = self.text.layout(Some(text_state), cx);
|
||||
let (layout_id, text_state) = self.text.layout(None, cx);
|
||||
let element_state = InteractiveTextState {
|
||||
text_state,
|
||||
clicked_range_ixs,
|
||||
mouse_down_index,
|
||||
};
|
||||
(layout_id, element_state)
|
||||
} else {
|
||||
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(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 RenderOnce for InteractiveText {
|
||||
impl IntoElement for InteractiveText {
|
||||
type Element = Self;
|
||||
|
||||
fn element_id(&self) -> Option<ElementId> {
|
||||
Some(self.element_id.clone())
|
||||
}
|
||||
|
||||
fn render_once(self) -> Self::Element {
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
point, px, size, AnyElement, AvailableSpace, Bounds, Element, ElementId, InteractiveElement,
|
||||
InteractiveElementState, Interactivity, LayoutId, Pixels, Point, Render, RenderOnce, Size,
|
||||
StyleRefinement, Styled, View, ViewContext, WindowContext,
|
||||
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, ops::Range, rc::Rc};
|
||||
@ -9,7 +9,7 @@ use taffy::style::Overflow;
|
||||
|
||||
/// uniform_list provides lazy rendering for a set of items that are of uniform height.
|
||||
/// When rendered into a container with overflow-y: hidden and a fixed (or max) height,
|
||||
/// uniform_list will only render the visibile subset of items.
|
||||
/// uniform_list will only render the visible subset of items.
|
||||
pub fn uniform_list<I, R, V>(
|
||||
view: View<V>,
|
||||
id: I,
|
||||
@ -18,30 +18,30 @@ pub fn uniform_list<I, R, V>(
|
||||
) -> UniformList
|
||||
where
|
||||
I: Into<ElementId>,
|
||||
R: RenderOnce,
|
||||
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.render_into_any())
|
||||
.map(|component| component.into_any_element())
|
||||
.collect()
|
||||
})
|
||||
};
|
||||
|
||||
UniformList {
|
||||
id: id.clone(),
|
||||
style,
|
||||
item_count,
|
||||
item_to_measure_index: 0,
|
||||
render_items: Box::new(render_range),
|
||||
interactivity: Interactivity {
|
||||
element_id: Some(id.into()),
|
||||
base_style,
|
||||
..Default::default()
|
||||
},
|
||||
scroll_handle: None,
|
||||
@ -50,7 +50,6 @@ where
|
||||
|
||||
pub struct UniformList {
|
||||
id: ElementId,
|
||||
style: StyleRefinement,
|
||||
item_count: usize,
|
||||
item_to_measure_index: usize,
|
||||
render_items:
|
||||
@ -91,7 +90,7 @@ impl UniformListScrollHandle {
|
||||
|
||||
impl Styled for UniformList {
|
||||
fn style(&mut self) -> &mut StyleRefinement {
|
||||
&mut self.style
|
||||
&mut self.interactivity.base_style
|
||||
}
|
||||
}
|
||||
|
||||
@ -211,31 +210,31 @@ impl Element for UniformList {
|
||||
scroll_offset: shared_scroll_offset,
|
||||
});
|
||||
}
|
||||
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 items = (self.render_items)(visible_range.clone(), cx);
|
||||
cx.with_z_index(1, |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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
||||
@ -244,14 +243,14 @@ impl Element for UniformList {
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for UniformList {
|
||||
impl IntoElement for UniformList {
|
||||
type Element = Self;
|
||||
|
||||
fn element_id(&self) -> Option<crate::ElementId> {
|
||||
Some(self.id.clone())
|
||||
}
|
||||
|
||||
fn render_once(self) -> Self::Element {
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
div, point, Div, Element, FocusHandle, Keystroke, Modifiers, Pixels, Point, Render, RenderOnce,
|
||||
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,7 +64,7 @@ pub struct Drag<S, R, V, E>
|
||||
where
|
||||
R: Fn(&mut V, &mut ViewContext<V>) -> E,
|
||||
V: 'static,
|
||||
E: RenderOnce,
|
||||
E: IntoElement,
|
||||
{
|
||||
pub state: S,
|
||||
pub render_drag_handle: R,
|
||||
@ -286,8 +286,8 @@ pub struct FocusEvent {
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{
|
||||
self as gpui, div, Div, FocusHandle, InteractiveElement, KeyBinding, Keystroke,
|
||||
ParentElement, Render, RenderOnce, Stateful, TestAppContext, VisualContext,
|
||||
self as gpui, div, Div, FocusHandle, InteractiveElement, IntoElement, KeyBinding,
|
||||
Keystroke, ParentElement, Render, Stateful, TestAppContext, VisualContext,
|
||||
};
|
||||
|
||||
struct TestView {
|
||||
@ -315,7 +315,7 @@ mod test {
|
||||
div()
|
||||
.key_context("nested")
|
||||
.track_focus(&self.focus_handle)
|
||||
.render_once(),
|
||||
.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,5 +1,5 @@
|
||||
pub use crate::{
|
||||
BorrowAppContext, BorrowWindow, Component, Context, Element, FocusableElement,
|
||||
InteractiveElement, ParentElement, Refineable, Render, RenderOnce, StatefulInteractiveElement,
|
||||
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, 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>,
|
||||
}
|
||||
@ -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,7 +1,7 @@
|
||||
use crate::{
|
||||
private::Sealed, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace, BorrowWindow,
|
||||
Bounds, Element, ElementId, Entity, EntityId, Flatten, FocusHandle, FocusableView, LayoutId,
|
||||
Model, Pixels, Point, Render, RenderOnce, Size, ViewContext, VisualContext, WeakModel,
|
||||
Bounds, Element, ElementId, Entity, EntityId, Flatten, FocusHandle, FocusableView, IntoElement,
|
||||
LayoutId, Model, Pixels, Point, Render, Size, ViewContext, VisualContext, WeakModel,
|
||||
WindowContext,
|
||||
};
|
||||
use anyhow::{Context, Result};
|
||||
@ -244,26 +244,26 @@ impl Element for AnyView {
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: 'static + Render> RenderOnce for View<V> {
|
||||
impl<V: 'static + Render> IntoElement for View<V> {
|
||||
type Element = View<V>;
|
||||
|
||||
fn element_id(&self) -> Option<ElementId> {
|
||||
Some(self.model.entity_id.into())
|
||||
Some(ElementId::from_entity_id(self.model.entity_id))
|
||||
}
|
||||
|
||||
fn render_once(self) -> Self::Element {
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for AnyView {
|
||||
impl IntoElement for AnyView {
|
||||
type Element = Self;
|
||||
|
||||
fn element_id(&self) -> Option<ElementId> {
|
||||
Some(self.model.entity_id.into())
|
||||
Some(ElementId::from_entity_id(self.model.entity_id))
|
||||
}
|
||||
|
||||
fn render_once(self) -> Self::Element {
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
@ -308,27 +308,23 @@ where
|
||||
}
|
||||
|
||||
mod any_view {
|
||||
use crate::{AnyElement, AnyView, BorrowWindow, Element, LayoutId, Render, WindowContext};
|
||||
use crate::{AnyElement, AnyView, Element, LayoutId, Render, WindowContext};
|
||||
|
||||
pub(crate) fn layout<V: 'static + Render>(
|
||||
view: &AnyView,
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, AnyElement) {
|
||||
cx.with_element_id(Some(view.model.entity_id), |cx| {
|
||||
let view = view.clone().downcast::<V>().unwrap();
|
||||
let mut element = view.update(cx, |view, cx| view.render(cx).into_any());
|
||||
let layout_id = element.layout(cx);
|
||||
(layout_id, element)
|
||||
})
|
||||
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: 'static + Render>(
|
||||
view: &AnyView,
|
||||
_view: &AnyView,
|
||||
element: AnyElement,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
cx.with_element_id(Some(view.model.entity_id), |cx| {
|
||||
element.paint(cx);
|
||||
})
|
||||
element.paint(cx);
|
||||
}
|
||||
}
|
||||
|
@ -193,11 +193,11 @@ pub trait FocusableView: 'static + Render {
|
||||
|
||||
/// ManagedView is a view (like a Modal, Popover, Menu, etc.)
|
||||
/// 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, Box<dyn Any>>,
|
||||
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.
|
||||
@ -1512,6 +1517,13 @@ impl<'a> WindowContext<'a> {
|
||||
.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<'_> {
|
||||
@ -1658,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))
|
||||
}
|
||||
}
|
||||
|
||||
@ -1747,6 +1759,24 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Invoke the given function with the content mask reset to that
|
||||
/// of the window.
|
||||
fn break_content_mask<R>(&mut self, f: impl FnOnce(&mut Self) -> R) -> R {
|
||||
let mask = ContentMask {
|
||||
bounds: Bounds {
|
||||
origin: Point::default(),
|
||||
size: self.window().viewport_size,
|
||||
},
|
||||
};
|
||||
self.window_mut()
|
||||
.current_frame
|
||||
.content_mask_stack
|
||||
.push(mask);
|
||||
let result = f(self);
|
||||
self.window_mut().current_frame.content_mask_stack.pop();
|
||||
result
|
||||
}
|
||||
|
||||
/// Update the global element offset relative to the current offset. This is used to implement
|
||||
/// scrolling.
|
||||
fn with_element_offset<R>(
|
||||
@ -1815,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");
|
||||
@ -1827,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
|
||||
}
|
||||
})
|
||||
@ -2304,7 +2374,7 @@ 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>(
|
||||
@ -2599,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;
|
||||
|
||||
@ -2611,12 +2687,6 @@ impl TryInto<SharedString> for ElementId {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EntityId> for ElementId {
|
||||
fn from(id: EntityId) -> Self {
|
||||
ElementId::View(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<usize> for ElementId {
|
||||
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
|
||||
}
|
||||
}
|
@ -2,23 +2,23 @@ use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::{parse_macro_input, DeriveInput};
|
||||
|
||||
pub fn derive_render_once(input: TokenStream) -> TokenStream {
|
||||
pub fn derive_into_element(input: TokenStream) -> TokenStream {
|
||||
let ast = parse_macro_input!(input as DeriveInput);
|
||||
let type_name = &ast.ident;
|
||||
let (impl_generics, type_generics, where_clause) = ast.generics.split_for_impl();
|
||||
|
||||
let gen = quote! {
|
||||
impl #impl_generics gpui::RenderOnce for #type_name #type_generics
|
||||
impl #impl_generics gpui::IntoElement for #type_name #type_generics
|
||||
#where_clause
|
||||
{
|
||||
type Element = gpui::CompositeElement<Self>;
|
||||
type Element = gpui::Component<Self>;
|
||||
|
||||
fn element_id(&self) -> Option<ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn render_once(self) -> Self::Element {
|
||||
gpui::CompositeElement::new(self)
|
||||
fn into_element(self) -> Self::Element {
|
||||
gpui::Component::new(self)
|
||||
}
|
||||
}
|
||||
};
|
@ -1,6 +1,5 @@
|
||||
mod action;
|
||||
mod derive_component;
|
||||
mod derive_render_once;
|
||||
mod derive_into_element;
|
||||
mod register_action;
|
||||
mod style_helpers;
|
||||
mod test;
|
||||
@ -17,14 +16,9 @@ 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(RenderOnce, attributes(view))]
|
||||
pub fn derive_render_once(input: TokenStream) -> TokenStream {
|
||||
derive_render_once::derive_render_once(input)
|
||||
#[proc_macro_derive(IntoElement)]
|
||||
pub fn derive_into_element(input: TokenStream) -> TokenStream {
|
||||
derive_into_element::derive_into_element(input)
|
||||
}
|
||||
|
||||
#[proc_macro]
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ use gpui::{
|
||||
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: RenderOnce;
|
||||
type ListItem: IntoElement;
|
||||
|
||||
fn match_count(&self) -> usize;
|
||||
fn selected_index(&self) -> usize;
|
||||
@ -114,6 +114,7 @@ impl<D: PickerDelegate> Picker<D> {
|
||||
}
|
||||
|
||||
fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||
dbg!("canceling!");
|
||||
self.delegate.dismissed(cx);
|
||||
}
|
||||
|
||||
@ -250,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]
|
||||
|
@ -10,8 +10,8 @@ use anyhow::{anyhow, Result};
|
||||
use gpui::{
|
||||
actions, div, px, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext,
|
||||
ClipboardItem, Div, EventEmitter, FocusHandle, Focusable, FocusableView, InteractiveElement,
|
||||
Model, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render,
|
||||
RenderOnce, Stateful, StatefulInteractiveElement, Styled, Task, UniformListScrollHandle, View,
|
||||
IntoElement, Model, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel,
|
||||
Render, Stateful, StatefulInteractiveElement, Styled, Task, UniformListScrollHandle, View,
|
||||
ViewContext, VisualContext as _, WeakView, WindowContext,
|
||||
};
|
||||
use menu::{Confirm, SelectNext, SelectPrev};
|
||||
@ -371,7 +371,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) {
|
||||
@ -644,6 +644,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);
|
||||
|
@ -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() {
|
||||
|
@ -7,12 +7,12 @@ use crate::{
|
||||
ToggleCaseSensitive, ToggleReplace, ToggleWholeWord,
|
||||
};
|
||||
use collections::HashMap;
|
||||
use editor::Editor;
|
||||
use editor::{Editor, EditorMode};
|
||||
use futures::channel::oneshot;
|
||||
use gpui::{
|
||||
actions, div, red, Action, AppContext, Div, EventEmitter, InteractiveElement as _,
|
||||
ParentElement as _, Render, RenderOnce, Styled, Subscription, Task, View, ViewContext,
|
||||
VisualContext as _, WindowContext,
|
||||
actions, div, red, Action, AppContext, Div, EventEmitter, InteractiveElement as _, IntoElement,
|
||||
ParentElement as _, Render, Styled, Subscription, Task, View, ViewContext, VisualContext as _,
|
||||
WeakView, WindowContext,
|
||||
};
|
||||
use project::search::SearchQuery;
|
||||
use serde::Deserialize;
|
||||
@ -23,7 +23,7 @@ use util::ResultExt;
|
||||
use workspace::{
|
||||
item::ItemHandle,
|
||||
searchable::{Direction, SearchEvent, SearchableItemHandle, WeakSearchableItemHandle},
|
||||
ToolbarItemLocation, ToolbarItemView, Workspace,
|
||||
ToolbarItemLocation, ToolbarItemView,
|
||||
};
|
||||
|
||||
#[derive(PartialEq, Clone, Deserialize, Default, Action)]
|
||||
@ -38,7 +38,7 @@ pub enum Event {
|
||||
}
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.observe_new_views(|workspace: &mut Workspace, _| BufferSearchBar::register(workspace))
|
||||
cx.observe_new_views(|editor: &mut Editor, cx| BufferSearchBar::register(editor, cx))
|
||||
.detach();
|
||||
}
|
||||
|
||||
@ -187,6 +187,7 @@ impl Render for BufferSearchBar {
|
||||
})
|
||||
.on_action(cx.listener(Self::previous_history_query))
|
||||
.on_action(cx.listener(Self::next_history_query))
|
||||
.on_action(cx.listener(Self::dismiss))
|
||||
.w_full()
|
||||
.p_1()
|
||||
.child(
|
||||
@ -294,9 +295,19 @@ impl ToolbarItemView for BufferSearchBar {
|
||||
}
|
||||
|
||||
impl BufferSearchBar {
|
||||
pub fn register(workspace: &mut Workspace) {
|
||||
workspace.register_action(|workspace, a: &Deploy, cx| {
|
||||
workspace.active_pane().update(cx, |this, cx| {
|
||||
pub fn register(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
|
||||
if editor.mode() != EditorMode::Full {
|
||||
return;
|
||||
};
|
||||
|
||||
let handle = cx.view().downgrade();
|
||||
|
||||
editor.register_action(move |a: &Deploy, cx| {
|
||||
let Some(pane) = handle.upgrade().and_then(|editor| editor.read(cx).pane(cx)) else {
|
||||
return;
|
||||
};
|
||||
|
||||
pane.update(cx, |this, cx| {
|
||||
this.toolbar().update(cx, |this, cx| {
|
||||
if let Some(search_bar) = this.item_of_type::<BufferSearchBar>() {
|
||||
search_bar.update(cx, |this, cx| {
|
||||
@ -316,11 +327,16 @@ impl BufferSearchBar {
|
||||
});
|
||||
});
|
||||
fn register_action<A: Action>(
|
||||
workspace: &mut Workspace,
|
||||
editor: &mut Editor,
|
||||
handle: WeakView<Editor>,
|
||||
update: fn(&mut BufferSearchBar, &A, &mut ViewContext<BufferSearchBar>),
|
||||
) {
|
||||
workspace.register_action(move |workspace, action: &A, cx| {
|
||||
workspace.active_pane().update(cx, move |this, cx| {
|
||||
editor.register_action(move |action: &A, cx| {
|
||||
let Some(pane) = handle.upgrade().and_then(|editor| editor.read(cx).pane(cx))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
pane.update(cx, move |this, cx| {
|
||||
this.toolbar().update(cx, move |this, cx| {
|
||||
if let Some(search_bar) = this.item_of_type::<BufferSearchBar>() {
|
||||
search_bar.update(cx, move |this, cx| update(this, action, cx));
|
||||
@ -331,49 +347,76 @@ impl BufferSearchBar {
|
||||
});
|
||||
}
|
||||
|
||||
register_action(workspace, |this, action: &ToggleCaseSensitive, cx| {
|
||||
if this.supported_options().case {
|
||||
this.toggle_case_sensitive(action, cx);
|
||||
}
|
||||
});
|
||||
register_action(workspace, |this, action: &ToggleWholeWord, cx| {
|
||||
if this.supported_options().word {
|
||||
this.toggle_whole_word(action, cx);
|
||||
}
|
||||
});
|
||||
register_action(workspace, |this, action: &ToggleReplace, cx| {
|
||||
if this.supported_options().replacement {
|
||||
this.toggle_replace(action, cx);
|
||||
}
|
||||
});
|
||||
register_action(workspace, |this, _: &ActivateRegexMode, cx| {
|
||||
let handle = cx.view().downgrade();
|
||||
register_action(
|
||||
editor,
|
||||
handle.clone(),
|
||||
|this, action: &ToggleCaseSensitive, cx| {
|
||||
if this.supported_options().case {
|
||||
this.toggle_case_sensitive(action, cx);
|
||||
}
|
||||
},
|
||||
);
|
||||
register_action(
|
||||
editor,
|
||||
handle.clone(),
|
||||
|this, action: &ToggleWholeWord, cx| {
|
||||
if this.supported_options().word {
|
||||
this.toggle_whole_word(action, cx);
|
||||
}
|
||||
},
|
||||
);
|
||||
register_action(
|
||||
editor,
|
||||
handle.clone(),
|
||||
|this, action: &ToggleReplace, cx| {
|
||||
if this.supported_options().replacement {
|
||||
this.toggle_replace(action, cx);
|
||||
}
|
||||
},
|
||||
);
|
||||
register_action(editor, handle.clone(), |this, _: &ActivateRegexMode, cx| {
|
||||
if this.supported_options().regex {
|
||||
this.activate_search_mode(SearchMode::Regex, cx);
|
||||
}
|
||||
});
|
||||
register_action(workspace, |this, _: &ActivateTextMode, cx| {
|
||||
register_action(editor, handle.clone(), |this, _: &ActivateTextMode, cx| {
|
||||
this.activate_search_mode(SearchMode::Text, cx);
|
||||
});
|
||||
register_action(workspace, |this, action: &CycleMode, cx| {
|
||||
register_action(editor, handle.clone(), |this, action: &CycleMode, cx| {
|
||||
if this.supported_options().regex {
|
||||
// If regex is not supported then search has just one mode (text) - in that case there's no point in supporting
|
||||
// cycling.
|
||||
this.cycle_mode(action, cx)
|
||||
}
|
||||
});
|
||||
register_action(workspace, |this, action: &SelectNextMatch, cx| {
|
||||
this.select_next_match(action, cx);
|
||||
});
|
||||
register_action(workspace, |this, action: &SelectPrevMatch, cx| {
|
||||
this.select_prev_match(action, cx);
|
||||
});
|
||||
register_action(workspace, |this, action: &SelectAllMatches, cx| {
|
||||
this.select_all_matches(action, cx);
|
||||
});
|
||||
register_action(workspace, |this, _: &editor::Cancel, cx| {
|
||||
register_action(
|
||||
editor,
|
||||
handle.clone(),
|
||||
|this, action: &SelectNextMatch, cx| {
|
||||
this.select_next_match(action, cx);
|
||||
},
|
||||
);
|
||||
register_action(
|
||||
editor,
|
||||
handle.clone(),
|
||||
|this, action: &SelectPrevMatch, cx| {
|
||||
this.select_prev_match(action, cx);
|
||||
},
|
||||
);
|
||||
register_action(
|
||||
editor,
|
||||
handle.clone(),
|
||||
|this, action: &SelectAllMatches, cx| {
|
||||
this.select_all_matches(action, cx);
|
||||
},
|
||||
);
|
||||
register_action(editor, handle.clone(), |this, _: &editor::Cancel, cx| {
|
||||
if !this.dismissed {
|
||||
this.dismiss(&Dismiss, cx);
|
||||
return;
|
||||
}
|
||||
cx.propagate();
|
||||
});
|
||||
}
|
||||
pub fn new(cx: &mut ViewContext<Self>) -> Self {
|
||||
@ -538,7 +581,7 @@ impl BufferSearchBar {
|
||||
self.update_matches(cx)
|
||||
}
|
||||
|
||||
fn render_action_button(&self) -> impl RenderOnce {
|
||||
fn render_action_button(&self) -> impl IntoElement {
|
||||
// let tooltip_style = theme.tooltip.clone();
|
||||
|
||||
// let style = theme.search.action_button.clone();
|
||||
|
@ -85,6 +85,7 @@ pub fn init(cx: &mut AppContext) {
|
||||
cx.capture_action(ProjectSearchView::replace_next);
|
||||
add_toggle_option_action::<ToggleCaseSensitive>(SearchOptions::CASE_SENSITIVE, cx);
|
||||
add_toggle_option_action::<ToggleWholeWord>(SearchOptions::WHOLE_WORD, cx);
|
||||
add_toggle_option_action::<ToggleIncludeIgnored>(SearchOptions::INCLUDE_IGNORED, cx);
|
||||
add_toggle_filters_action::<ToggleFilters>(cx);
|
||||
}
|
||||
|
||||
@ -1192,6 +1193,7 @@ impl ProjectSearchView {
|
||||
text,
|
||||
self.search_options.contains(SearchOptions::WHOLE_WORD),
|
||||
self.search_options.contains(SearchOptions::CASE_SENSITIVE),
|
||||
self.search_options.contains(SearchOptions::INCLUDE_IGNORED),
|
||||
included_files,
|
||||
excluded_files,
|
||||
) {
|
||||
@ -1210,6 +1212,7 @@ impl ProjectSearchView {
|
||||
text,
|
||||
self.search_options.contains(SearchOptions::WHOLE_WORD),
|
||||
self.search_options.contains(SearchOptions::CASE_SENSITIVE),
|
||||
self.search_options.contains(SearchOptions::INCLUDE_IGNORED),
|
||||
included_files,
|
||||
excluded_files,
|
||||
) {
|
||||
@ -1764,6 +1767,14 @@ impl View for ProjectSearchBar {
|
||||
render_option_button_icon("icons/word_search.svg", SearchOptions::WHOLE_WORD, cx)
|
||||
});
|
||||
|
||||
let include_ignored = is_semantic_disabled.then(|| {
|
||||
render_option_button_icon(
|
||||
"icons/file_icons/git.svg",
|
||||
SearchOptions::INCLUDE_IGNORED,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let search_button_for_mode = |mode, side, cx: &mut ViewContext<ProjectSearchBar>| {
|
||||
let is_active = if let Some(search) = self.active_project_search.as_ref() {
|
||||
let search = search.read(cx);
|
||||
@ -1879,7 +1890,15 @@ impl View for ProjectSearchBar {
|
||||
.with_children(search.filters_enabled.then(|| {
|
||||
Flex::row()
|
||||
.with_child(
|
||||
ChildView::new(&search.included_files_editor, cx)
|
||||
Flex::row()
|
||||
.with_child(
|
||||
ChildView::new(&search.included_files_editor, cx)
|
||||
.contained()
|
||||
.constrained()
|
||||
.with_height(theme.search.search_bar_row_height)
|
||||
.flex(1., true),
|
||||
)
|
||||
.with_children(include_ignored)
|
||||
.contained()
|
||||
.with_style(include_container_style)
|
||||
.constrained()
|
||||
|
@ -1,6 +1,6 @@
|
||||
use bitflags::bitflags;
|
||||
pub use buffer_search::BufferSearchBar;
|
||||
use gpui::{actions, Action, AppContext, RenderOnce};
|
||||
use gpui::{actions, Action, AppContext, IntoElement};
|
||||
pub use mode::SearchMode;
|
||||
use project::search::SearchQuery;
|
||||
use ui::ButtonVariant;
|
||||
@ -82,7 +82,7 @@ impl SearchOptions {
|
||||
options
|
||||
}
|
||||
|
||||
pub fn as_button(&self, active: bool) -> impl RenderOnce {
|
||||
pub fn as_button(&self, active: bool) -> impl IntoElement {
|
||||
ui::IconButton::new(0, self.icon())
|
||||
.on_click({
|
||||
let action = self.to_toggle_action();
|
||||
@ -95,7 +95,7 @@ impl SearchOptions {
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_replace_button(active: bool) -> impl RenderOnce {
|
||||
fn toggle_replace_button(active: bool) -> impl IntoElement {
|
||||
// todo: add toggle_replace button
|
||||
ui::IconButton::new(0, ui::Icon::Replace)
|
||||
.on_click(|_, cx| {
|
||||
@ -109,7 +109,7 @@ fn toggle_replace_button(active: bool) -> impl RenderOnce {
|
||||
fn render_replace_button(
|
||||
action: impl Action + 'static + Send + Sync,
|
||||
icon: ui::Icon,
|
||||
) -> impl RenderOnce {
|
||||
) -> impl IntoElement {
|
||||
// todo: add tooltip
|
||||
ui::IconButton::new(0, icon).on_click(move |_, cx| {
|
||||
cx.dispatch_action(action.boxed_clone());
|
||||
|
@ -1,4 +1,4 @@
|
||||
use gpui::{MouseDownEvent, RenderOnce, WindowContext};
|
||||
use gpui::{IntoElement, MouseDownEvent, WindowContext};
|
||||
use ui::{Button, ButtonVariant, IconButton};
|
||||
|
||||
use crate::mode::SearchMode;
|
||||
@ -7,7 +7,7 @@ pub(super) fn render_nav_button(
|
||||
icon: ui::Icon,
|
||||
_active: bool,
|
||||
on_click: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
|
||||
) -> impl RenderOnce {
|
||||
) -> impl IntoElement {
|
||||
// let tooltip_style = cx.theme().tooltip.clone();
|
||||
// let cursor_style = if active {
|
||||
// CursorStyle::PointingHand
|
||||
|
@ -1659,13 +1659,13 @@ fn elixir_lang() -> Arc<Language> {
|
||||
target: (identifier) @name)
|
||||
operator: "when")
|
||||
])
|
||||
(#match? @name "^(def|defp|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp)$")) @item
|
||||
(#any-match? @name "^(def|defp|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp)$")) @item
|
||||
)
|
||||
|
||||
(call
|
||||
target: (identifier) @name
|
||||
(arguments (alias) @name)
|
||||
(#match? @name "^(defmodule|defprotocol)$")) @item
|
||||
(#any-match? @name "^(defmodule|defprotocol)$")) @item
|
||||
"#,
|
||||
)
|
||||
.unwrap(),
|
||||
|
10
crates/story/Cargo.toml
Normal file
10
crates/story/Cargo.toml
Normal file
@ -0,0 +1,10 @@
|
||||
[package]
|
||||
name = "story"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
3
crates/story/src/lib.rs
Normal file
3
crates/story/src/lib.rs
Normal file
@ -0,0 +1,3 @@
|
||||
mod story;
|
||||
|
||||
pub use story::*;
|
35
crates/story/src/story.rs
Normal file
35
crates/story/src/story.rs
Normal file
@ -0,0 +1,35 @@
|
||||
use gpui::prelude::*;
|
||||
use gpui::{div, hsla, Div, SharedString};
|
||||
|
||||
pub struct Story {}
|
||||
|
||||
impl Story {
|
||||
pub fn container() -> Div {
|
||||
div().size_full().flex().flex_col().pt_2().px_4().bg(hsla(
|
||||
0. / 360.,
|
||||
0. / 100.,
|
||||
100. / 100.,
|
||||
1.,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn title(title: impl Into<SharedString>) -> impl Element {
|
||||
div()
|
||||
.text_xl()
|
||||
.text_color(hsla(0. / 360., 0. / 100., 0. / 100., 1.))
|
||||
.child(title.into())
|
||||
}
|
||||
|
||||
pub fn title_for<T>() -> impl Element {
|
||||
Self::title(std::any::type_name::<T>())
|
||||
}
|
||||
|
||||
pub fn label(label: impl Into<SharedString>) -> impl Element {
|
||||
div()
|
||||
.mt_4()
|
||||
.mb_2()
|
||||
.text_xs()
|
||||
.text_color(hsla(0. / 360., 0. / 100., 0. / 100., 1.))
|
||||
.child(label.into())
|
||||
}
|
||||
}
|
@ -25,6 +25,7 @@ serde.workspace = true
|
||||
settings2 = { path = "../settings2" }
|
||||
simplelog = "0.9"
|
||||
smallvec.workspace = true
|
||||
story = { path = "../story" }
|
||||
strum = { version = "0.25.0", features = ["derive"] }
|
||||
theme = { path = "../theme" }
|
||||
theme2 = { path = "../theme2" }
|
||||
|
@ -1,4 +1,3 @@
|
||||
mod colors;
|
||||
mod focus;
|
||||
mod kitchen_sink;
|
||||
mod picker;
|
||||
@ -6,7 +5,6 @@ mod scroll;
|
||||
mod text;
|
||||
mod z_index;
|
||||
|
||||
pub use colors::*;
|
||||
pub use focus::*;
|
||||
pub use kitchen_sink::*;
|
||||
pub use picker::*;
|
||||
|
@ -1,44 +0,0 @@
|
||||
use crate::story::Story;
|
||||
use gpui::{prelude::*, px, Div, Render};
|
||||
use theme2::{default_color_scales, ColorScaleStep};
|
||||
use ui::prelude::*;
|
||||
|
||||
pub struct ColorsStory;
|
||||
|
||||
impl Render for ColorsStory {
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
let color_scales = default_color_scales();
|
||||
|
||||
Story::container(cx)
|
||||
.child(Story::title(cx, "Colors"))
|
||||
.child(
|
||||
div()
|
||||
.id("colors")
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.overflow_y_scroll()
|
||||
.text_color(gpui::white())
|
||||
.children(color_scales.into_iter().map(|scale| {
|
||||
div()
|
||||
.flex()
|
||||
.child(
|
||||
div()
|
||||
.w(px(75.))
|
||||
.line_height(px(24.))
|
||||
.child(scale.name().clone()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_1()
|
||||
.children(ColorScaleStep::ALL.map(|step| {
|
||||
div().flex().size_6().bg(scale.step(cx, step))
|
||||
})),
|
||||
)
|
||||
})),
|
||||
)
|
||||
}
|
||||
}
|
@ -26,7 +26,7 @@ impl FocusStory {
|
||||
}
|
||||
}
|
||||
|
||||
impl Render<Self> for FocusStory {
|
||||
impl Render for FocusStory {
|
||||
type Element = Focusable<Stateful<Div>>;
|
||||
|
||||
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element {
|
||||
@ -52,10 +52,8 @@ impl Render<Self> for FocusStory {
|
||||
.on_blur(cx.listener(|_, _, _| println!("Parent blurred")))
|
||||
.on_focus_in(cx.listener(|_, _, _| println!("Parent focus_in")))
|
||||
.on_focus_out(cx.listener(|_, _, _| println!("Parent focus_out")))
|
||||
.on_key_down(
|
||||
cx.listener(|_, event, phase, _| println!("Key down on parent {:?}", event)),
|
||||
)
|
||||
.on_key_up(cx.listener(|_, event, phase, _| println!("Key up on parent {:?}", event)))
|
||||
.on_key_down(cx.listener(|_, event, _| println!("Key down on parent {:?}", event)))
|
||||
.on_key_up(cx.listener(|_, event, _| println!("Key up on parent {:?}", event)))
|
||||
.size_full()
|
||||
.bg(color_1)
|
||||
.focus(|style| style.bg(color_2))
|
||||
|
@ -1,8 +1,10 @@
|
||||
use crate::{story::Story, story_selector::ComponentStory};
|
||||
use gpui::{prelude::*, Div, Render, Stateful, View};
|
||||
use story::Story;
|
||||
use strum::IntoEnumIterator;
|
||||
use ui::prelude::*;
|
||||
|
||||
use crate::story_selector::ComponentStory;
|
||||
|
||||
pub struct KitchenSinkStory;
|
||||
|
||||
impl KitchenSinkStory {
|
||||
@ -19,11 +21,11 @@ impl Render for KitchenSinkStory {
|
||||
.map(|selector| selector.story(cx))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Story::container(cx)
|
||||
Story::container()
|
||||
.id("kitchen-sink")
|
||||
.overflow_y_scroll()
|
||||
.child(Story::title(cx, "Kitchen Sink"))
|
||||
.child(Story::label(cx, "Components"))
|
||||
.child(Story::title("Kitchen Sink"))
|
||||
.child(Story::label("Components"))
|
||||
.child(div().flex().flex_col().children(component_stories))
|
||||
// Add a bit of space at the bottom of the kitchen sink so elements
|
||||
// don't end up squished right up against the bottom of the screen.
|
||||
|
@ -36,7 +36,7 @@ impl Delegate {
|
||||
}
|
||||
|
||||
impl PickerDelegate for Delegate {
|
||||
type ListItem = Div<Picker<Self>>;
|
||||
type ListItem = Div;
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
self.candidates.len()
|
||||
@ -205,8 +205,8 @@ impl PickerStory {
|
||||
}
|
||||
}
|
||||
|
||||
impl Render<Self> for PickerStory {
|
||||
type Element = Div<Self>;
|
||||
impl Render for PickerStory {
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element {
|
||||
div()
|
||||
|
@ -10,8 +10,8 @@ impl ScrollStory {
|
||||
}
|
||||
}
|
||||
|
||||
impl Render<Self> for ScrollStory {
|
||||
type Element = Stateful<Self, Div<Self>>;
|
||||
impl Render for ScrollStory {
|
||||
type Element = Stateful<Div>;
|
||||
|
||||
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element {
|
||||
let theme = cx.theme();
|
||||
@ -38,7 +38,7 @@ impl Render<Self> for ScrollStory {
|
||||
};
|
||||
div()
|
||||
.id(id)
|
||||
.tooltip(move |_, cx| Tooltip::text(format!("{}, {}", row, column), cx))
|
||||
.tooltip(move |cx| Tooltip::text(format!("{}, {}", row, column), cx))
|
||||
.bg(bg)
|
||||
.size(px(100. as f32))
|
||||
.when(row >= 5 && column >= 5, |d| {
|
||||
|
@ -1,5 +1,6 @@
|
||||
use gpui::{
|
||||
blue, div, red, white, Div, ParentElement, Render, Styled, View, VisualContext, WindowContext,
|
||||
blue, div, green, red, white, Div, InteractiveText, ParentElement, Render, Styled, StyledText,
|
||||
TextRun, View, VisualContext, WindowContext,
|
||||
};
|
||||
use ui::v_stack;
|
||||
|
||||
@ -55,6 +56,21 @@ impl Render for TextStory {
|
||||
"flex-row. width 96. The quick brown fox jumps over the lazy dog. ",
|
||||
"Meanwhile, the lazy dog decided it was time for a change. ",
|
||||
"He started daily workout routines, ate healthier and became the fastest dog in town.",
|
||||
)))
|
||||
))).child(
|
||||
InteractiveText::new(
|
||||
"interactive",
|
||||
StyledText::new("Hello world, how is it going?").with_runs(vec![
|
||||
cx.text_style().to_run(6),
|
||||
TextRun {
|
||||
background_color: Some(green()),
|
||||
..cx.text_style().to_run(5)
|
||||
},
|
||||
cx.text_style().to_run(18),
|
||||
]),
|
||||
)
|
||||
.on_click(vec![2..4, 1..3, 7..9], |range_ix, cx| {
|
||||
println!("Clicked range {range_ix}");
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user