Merge branch 'main' into welcome

This commit is contained in:
Mikayla 2023-11-27 18:51:19 -08:00
commit 8faa1f6e58
No known key found for this signature in database
188 changed files with 6979 additions and 6715 deletions

12
.cargo/ci-config.toml Normal file
View 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"]

View File

@ -19,16 +19,12 @@ runs:
- name: Limit target directory size - name: Limit target directory size
shell: bash -euxo pipefail {0} shell: bash -euxo pipefail {0}
run: script/clear-target-dir-if-larger-than 70 run: script/clear-target-dir-if-larger-than 100
- name: Run check - name: Run check
env:
RUSTFLAGS: -D warnings
shell: bash -euxo pipefail {0} shell: bash -euxo pipefail {0}
run: cargo check --tests --workspace run: cargo check --tests --workspace
- name: Run tests - name: Run tests
env:
RUSTFLAGS: -D warnings
shell: bash -euxo pipefail {0} shell: bash -euxo pipefail {0}
run: cargo nextest run --workspace --no-fail-fast run: cargo nextest run --workspace --no-fail-fast

View File

@ -29,6 +29,9 @@ jobs:
clean: false clean: false
submodules: "recursive" submodules: "recursive"
- name: Set up default .cargo/config.toml
run: cp ./.cargo/ci-config.toml ~/.cargo/config.toml
- name: Run rustfmt - name: Run rustfmt
uses: ./.github/actions/check_formatting uses: ./.github/actions/check_formatting
@ -87,7 +90,7 @@ jobs:
submodules: "recursive" submodules: "recursive"
- name: Limit target directory size - name: Limit target directory size
run: script/clear-target-dir-if-larger-than 70 run: script/clear-target-dir-if-larger-than 100
- name: Determine version and release channel - name: Determine version and release channel
if: ${{ startsWith(github.ref, 'refs/tags/v') }} if: ${{ startsWith(github.ref, 'refs/tags/v') }}
@ -131,8 +134,6 @@ jobs:
- uses: softprops/action-gh-release@v1 - uses: softprops/action-gh-release@v1
name: Upload app bundle to release name: Upload app bundle to release
# TODO kb seems that zed.dev relies on GitHub releases for release version tracking.
# Find alternatives for `nightly` or just go on with more releases?
if: ${{ env.RELEASE_CHANNEL == 'preview' || env.RELEASE_CHANNEL == 'stable' }} if: ${{ env.RELEASE_CHANNEL == 'preview' || env.RELEASE_CHANNEL == 'stable' }}
with: with:
draft: true draft: true

View File

@ -79,7 +79,7 @@ jobs:
submodules: "recursive" submodules: "recursive"
- name: Limit target directory size - name: Limit target directory size
run: script/clear-target-dir-if-larger-than 70 run: script/clear-target-dir-if-larger-than 100
- name: Set release channel to nightly - name: Set release channel to nightly
run: | run: |

75
Cargo.lock generated
View File

@ -841,6 +841,17 @@ dependencies = [
"rustc-demangle", "rustc-demangle",
] ]
[[package]]
name = "backtrace-on-stack-overflow"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fd2d70527f3737a1ad17355e260706c1badebabd1fa06a7a053407380df841b"
dependencies = [
"backtrace",
"libc",
"nix 0.23.2",
]
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.13.1" version = "0.13.1"
@ -1175,12 +1186,14 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-broadcast", "async-broadcast",
"async-trait",
"audio2", "audio2",
"client2", "client2",
"collections", "collections",
"fs2", "fs2",
"futures 0.3.28", "futures 0.3.28",
"gpui2", "gpui2",
"image",
"language2", "language2",
"live_kit_client2", "live_kit_client2",
"log", "log",
@ -1192,7 +1205,10 @@ dependencies = [
"serde_derive", "serde_derive",
"serde_json", "serde_json",
"settings2", "settings2",
"smallvec",
"ui2",
"util", "util",
"workspace2",
] ]
[[package]] [[package]]
@ -1653,7 +1669,7 @@ dependencies = [
[[package]] [[package]]
name = "collab" name = "collab"
version = "0.28.0" version = "0.29.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -5559,6 +5575,19 @@ dependencies = [
"winapi 0.3.9", "winapi 0.3.9",
] ]
[[package]]
name = "nix"
version = "0.23.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c"
dependencies = [
"bitflags 1.3.2",
"cc",
"cfg-if 1.0.0",
"libc",
"memoffset 0.6.5",
]
[[package]] [[package]]
name = "nix" name = "nix"
version = "0.24.3" version = "0.24.3"
@ -8859,6 +8888,42 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "story"
version = "0.1.0"
dependencies = [
"gpui2",
]
[[package]]
name = "storybook2"
version = "0.1.0"
dependencies = [
"anyhow",
"backtrace-on-stack-overflow",
"chrono",
"clap 4.4.4",
"editor2",
"fuzzy2",
"gpui2",
"itertools 0.11.0",
"language2",
"log",
"menu2",
"picker2",
"rust-embed",
"serde",
"settings2",
"simplelog",
"smallvec",
"story",
"strum",
"theme",
"theme2",
"ui2",
"util",
]
[[package]] [[package]]
name = "stringprep" name = "stringprep"
version = "0.1.4" version = "0.1.4"
@ -9362,6 +9427,7 @@ dependencies = [
"serde_derive", "serde_derive",
"serde_json", "serde_json",
"settings2", "settings2",
"story",
"toml 0.5.11", "toml 0.5.11",
"util", "util",
"uuid 1.4.1", "uuid 1.4.1",
@ -9884,7 +9950,7 @@ dependencies = [
[[package]] [[package]]
name = "tree-sitter" name = "tree-sitter"
version = "0.20.10" version = "0.20.10"
source = "git+https://github.com/tree-sitter/tree-sitter?rev=35a6052fbcafc5e5fc0f9415b8652be7dcaf7222#35a6052fbcafc5e5fc0f9415b8652be7dcaf7222" source = "git+https://github.com/tree-sitter/tree-sitter?rev=3b0159d25559b603af566ade3c83d930bf466db1#3b0159d25559b603af566ade3c83d930bf466db1"
dependencies = [ dependencies = [
"cc", "cc",
"regex", "regex",
@ -10225,6 +10291,7 @@ dependencies = [
"serde", "serde",
"settings2", "settings2",
"smallvec", "smallvec",
"story",
"strum", "strum",
"theme2", "theme2",
] ]
@ -11363,6 +11430,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-recursion 1.0.5", "async-recursion 1.0.5",
"async-trait",
"bincode", "bincode",
"call2", "call2",
"client2", "client2",
@ -11475,7 +11543,7 @@ dependencies = [
[[package]] [[package]]
name = "zed" name = "zed"
version = "0.114.0" version = "0.115.0"
dependencies = [ dependencies = [
"activity_indicator", "activity_indicator",
"ai", "ai",
@ -11623,6 +11691,7 @@ dependencies = [
"async-recursion 0.3.2", "async-recursion 0.3.2",
"async-tar", "async-tar",
"async-trait", "async-trait",
"audio2",
"auto_update2", "auto_update2",
"backtrace", "backtrace",
"call2", "call2",

View File

@ -97,8 +97,7 @@ members = [
"crates/sqlez", "crates/sqlez",
"crates/sqlez_macros", "crates/sqlez_macros",
"crates/rich_text", "crates/rich_text",
# "crates/storybook2", "crates/storybook2",
# "crates/storybook3",
"crates/sum_tree", "crates/sum_tree",
"crates/terminal", "crates/terminal",
"crates/terminal2", "crates/terminal2",
@ -112,6 +111,7 @@ members = [
"crates/ui2", "crates/ui2",
"crates/util", "crates/util",
"crates/semantic_index", "crates/semantic_index",
"crates/story",
"crates/vim", "crates/vim",
"crates/vcs_menu", "crates/vcs_menu",
"crates/workspace2", "crates/workspace2",
@ -197,8 +197,9 @@ tree-sitter-lua = "0.0.14"
tree-sitter-nix = { git = "https://github.com/nix-community/tree-sitter-nix", rev = "66e3e9ce9180ae08fc57372061006ef83f0abde7" } tree-sitter-nix = { git = "https://github.com/nix-community/tree-sitter-nix", rev = "66e3e9ce9180ae08fc57372061006ef83f0abde7" }
tree-sitter-nu = { git = "https://github.com/nushell/tree-sitter-nu", rev = "786689b0562b9799ce53e824cb45a1a2a04dc673"} tree-sitter-nu = { git = "https://github.com/nushell/tree-sitter-nu", rev = "786689b0562b9799ce53e824cb45a1a2a04dc673"}
tree-sitter-vue = {git = "https://github.com/zed-industries/tree-sitter-vue", rev = "9b6cb221ccb8d0b956fcb17e9a1efac2feefeb58"} tree-sitter-vue = {git = "https://github.com/zed-industries/tree-sitter-vue", rev = "9b6cb221ccb8d0b956fcb17e9a1efac2feefeb58"}
[patch.crates-io] [patch.crates-io]
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "35a6052fbcafc5e5fc0f9415b8652be7dcaf7222" } tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "3b0159d25559b603af566ade3c83d930bf466db1" }
async-task = { git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e" } async-task = { git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e" }
# TODO - Remove when a version is released with this PR: https://github.com/servo/core-foundation-rs/pull/457 # TODO - Remove when a version is released with this PR: https://github.com/servo/core-foundation-rs/pull/457
@ -210,11 +211,12 @@ core-graphics = { git = "https://github.com/servo/core-foundation-rs", rev = "07
[profile.dev] [profile.dev]
split-debuginfo = "unpacked" split-debuginfo = "unpacked"
debug = "limited"
[profile.dev.package.taffy] [profile.dev.package.taffy]
opt-level = 3 opt-level = 3
[profile.release] [profile.release]
debug = true debug = "limited"
lto = "thin" lto = "thin"
codegen-units = 1 codegen-units = 1

4
Procfile.zed2 Normal file
View 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

View File

@ -43,7 +43,7 @@
"calt": false "calt": false
}, },
// The default font size for text in the UI // The default font size for text in the UI
"ui_font_size": 14, "ui_font_size": 16,
// The factor to grow the active pane by. Defaults to 1.0 // The factor to grow the active pane by. Defaults to 1.0
// which gives the same size as all other panes. // which gives the same size as all other panes.
"active_pane_magnification": 1.0, "active_pane_magnification": 1.0,

View File

@ -7,5 +7,6 @@
// custom settings, run the `open default settings` command // custom settings, run the `open default settings` command
// from the command palette or from `Zed` application menu. // from the command palette or from `Zed` application menu.
{ {
"buffer_font_size": 15 "ui_font_size": 16,
"buffer_font_size": 16
} }

View File

@ -1,12 +1,13 @@
use gpui::{div, Div, EventEmitter, ParentElement, Render, SemanticVersion, ViewContext}; use gpui::{
div, DismissEvent, Div, EventEmitter, ParentElement, Render, SemanticVersion, ViewContext,
};
use menu::Cancel; use menu::Cancel;
use workspace::notifications::NotificationEvent;
pub struct UpdateNotification { pub struct UpdateNotification {
_version: SemanticVersion, _version: SemanticVersion,
} }
impl EventEmitter<NotificationEvent> for UpdateNotification {} impl EventEmitter<DismissEvent> for UpdateNotification {}
impl Render for UpdateNotification { impl Render for UpdateNotification {
type Element = Div; type Element = Div;
@ -82,6 +83,6 @@ impl UpdateNotification {
} }
pub fn _dismiss(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) { pub fn _dismiss(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
cx.emit(NotificationEvent::Dismiss); cx.emit(DismissEvent::Dismiss);
} }
} }

View File

@ -31,15 +31,19 @@ media = { path = "../media" }
project = { package = "project2", path = "../project2" } project = { package = "project2", path = "../project2" }
settings = { package = "settings2", path = "../settings2" } settings = { package = "settings2", path = "../settings2" }
util = { path = "../util" } util = { path = "../util" }
ui = {package = "ui2", path = "../ui2"}
workspace = {package = "workspace2", path = "../workspace2"}
async-trait.workspace = true
anyhow.workspace = true anyhow.workspace = true
async-broadcast = "0.4" async-broadcast = "0.4"
futures.workspace = true futures.workspace = true
image = "0.23"
postage.workspace = true postage.workspace = true
schemars.workspace = true schemars.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
serde_derive.workspace = true serde_derive.workspace = true
smallvec.workspace = true
[dev-dependencies] [dev-dependencies]
client = { package = "client2", path = "../client2", features = ["test-support"] } client = { package = "client2", path = "../client2", features = ["test-support"] }

View File

@ -1,25 +1,32 @@
pub mod call_settings; pub mod call_settings;
pub mod participant; pub mod participant;
pub mod room; pub mod room;
mod shared_screen;
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use async_trait::async_trait;
use audio::Audio; use audio::Audio;
use call_settings::CallSettings; use call_settings::CallSettings;
use client::{proto, Client, TelemetrySettings, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE}; use client::{
proto::{self, PeerId},
Client, TelemetrySettings, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE,
};
use collections::HashSet; use collections::HashSet;
use futures::{channel::oneshot, future::Shared, Future, FutureExt}; use futures::{channel::oneshot, future::Shared, Future, FutureExt};
use gpui::{ use gpui::{
AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Subscription, Task, AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Subscription, Task,
WeakModel, View, ViewContext, VisualContext, WeakModel, WeakView,
}; };
pub use participant::ParticipantLocation;
use postage::watch; use postage::watch;
use project::Project; use project::Project;
use room::Event; use room::Event;
use settings::Settings;
use std::sync::Arc;
pub use participant::ParticipantLocation;
pub use room::Room; pub use room::Room;
use settings::Settings;
use shared_screen::SharedScreen;
use std::sync::Arc;
use util::ResultExt;
use workspace::{item::ItemHandle, CallHandler, Pane, Workspace};
pub fn init(client: Arc<Client>, user_store: Model<UserStore>, cx: &mut AppContext) { pub fn init(client: Arc<Client>, user_store: Model<UserStore>, cx: &mut AppContext) {
CallSettings::register(cx); CallSettings::register(cx);
@ -464,7 +471,7 @@ impl ActiveCall {
&self.pending_invites &self.pending_invites
} }
pub fn report_call_event(&self, operation: &'static str, cx: &AppContext) { pub fn report_call_event(&self, operation: &'static str, cx: &mut AppContext) {
if let Some(room) = self.room() { if let Some(room) = self.room() {
let room = room.read(cx); let room = room.read(cx);
report_call_event_for_room(operation, room.id(), room.channel_id(), &self.client, cx); report_call_event_for_room(operation, room.id(), room.channel_id(), &self.client, cx);
@ -477,7 +484,7 @@ pub fn report_call_event_for_room(
room_id: u64, room_id: u64,
channel_id: Option<u64>, channel_id: Option<u64>,
client: &Arc<Client>, client: &Arc<Client>,
cx: &AppContext, cx: &mut AppContext,
) { ) {
let telemetry = client.telemetry(); let telemetry = client.telemetry();
let telemetry_settings = *TelemetrySettings::get_global(cx); let telemetry_settings = *TelemetrySettings::get_global(cx);
@ -505,6 +512,205 @@ pub fn report_call_event_for_channel(
) )
} }
pub struct Call {
active_call: Option<(Model<ActiveCall>, Vec<Subscription>)>,
parent_workspace: WeakView<Workspace>,
}
impl Call {
pub fn new(
parent_workspace: WeakView<Workspace>,
cx: &mut ViewContext<'_, Workspace>,
) -> Box<dyn CallHandler> {
let mut active_call = None;
if cx.has_global::<Model<ActiveCall>>() {
let call = cx.global::<Model<ActiveCall>>().clone();
let subscriptions = vec![cx.subscribe(&call, Self::on_active_call_event)];
active_call = Some((call, subscriptions));
}
Box::new(Self {
active_call,
parent_workspace,
})
}
fn on_active_call_event(
workspace: &mut Workspace,
_: Model<ActiveCall>,
event: &room::Event,
cx: &mut ViewContext<Workspace>,
) {
match event {
room::Event::ParticipantLocationChanged { participant_id }
| room::Event::RemoteVideoTracksChanged { participant_id } => {
workspace.leader_updated(*participant_id, cx);
}
_ => {}
}
}
}
#[async_trait(?Send)]
impl CallHandler for Call {
fn peer_state(
&mut self,
leader_id: PeerId,
cx: &mut ViewContext<Workspace>,
) -> Option<(bool, bool)> {
let (call, _) = self.active_call.as_ref()?;
let room = call.read(cx).room()?.read(cx);
let participant = room.remote_participant_for_peer_id(leader_id)?;
let leader_in_this_app;
let leader_in_this_project;
match participant.location {
ParticipantLocation::SharedProject { project_id } => {
leader_in_this_app = true;
leader_in_this_project = Some(project_id)
== self
.parent_workspace
.update(cx, |this, cx| this.project().read(cx).remote_id())
.log_err()
.flatten();
}
ParticipantLocation::UnsharedProject => {
leader_in_this_app = true;
leader_in_this_project = false;
}
ParticipantLocation::External => {
leader_in_this_app = false;
leader_in_this_project = false;
}
};
Some((leader_in_this_project, leader_in_this_app))
}
fn shared_screen_for_peer(
&self,
peer_id: PeerId,
pane: &View<Pane>,
cx: &mut ViewContext<Workspace>,
) -> Option<Box<dyn ItemHandle>> {
let (call, _) = self.active_call.as_ref()?;
let room = call.read(cx).room()?.read(cx);
let participant = room.remote_participant_for_peer_id(peer_id)?;
let track = participant.video_tracks.values().next()?.clone();
let user = participant.user.clone();
for item in pane.read(cx).items_of_type::<SharedScreen>() {
if item.read(cx).peer_id == peer_id {
return Some(Box::new(item));
}
}
Some(Box::new(cx.build_view(|cx| {
SharedScreen::new(&track, peer_id, user.clone(), cx)
})))
}
fn room_id(&self, cx: &AppContext) -> Option<u64> {
Some(self.active_call.as_ref()?.0.read(cx).room()?.read(cx).id())
}
fn hang_up(&self, cx: &mut AppContext) -> Task<Result<()>> {
let Some((call, _)) = self.active_call.as_ref() else {
return Task::ready(Err(anyhow!("Cannot exit a call; not in a call")));
};
call.update(cx, |this, cx| this.hang_up(cx))
}
fn active_project(&self, cx: &AppContext) -> Option<WeakModel<Project>> {
ActiveCall::global(cx).read(cx).location().cloned()
}
fn invite(
&mut self,
called_user_id: u64,
initial_project: Option<Model<Project>>,
cx: &mut AppContext,
) -> Task<Result<()>> {
ActiveCall::global(cx).update(cx, |this, cx| {
this.invite(called_user_id, initial_project, cx)
})
}
fn remote_participants(&self, cx: &AppContext) -> Option<Vec<(Arc<User>, PeerId)>> {
self.active_call
.as_ref()
.map(|call| {
call.0.read(cx).room().map(|room| {
room.read(cx)
.remote_participants()
.iter()
.map(|participant| {
(participant.1.user.clone(), participant.1.peer_id.clone())
})
.collect()
})
})
.flatten()
}
fn is_muted(&self, cx: &AppContext) -> Option<bool> {
self.active_call
.as_ref()
.map(|call| {
call.0
.read(cx)
.room()
.map(|room| room.read(cx).is_muted(cx))
})
.flatten()
}
fn toggle_mute(&self, cx: &mut AppContext) {
self.active_call.as_ref().map(|call| {
call.0.update(cx, |this, cx| {
this.room().map(|room| {
room.update(cx, |this, cx| {
this.toggle_mute(cx).log_err();
})
})
})
});
}
fn toggle_screen_share(&self, cx: &mut AppContext) {
self.active_call.as_ref().map(|call| {
call.0.update(cx, |this, cx| {
this.room().map(|room| {
room.update(cx, |this, cx| {
if this.is_screen_sharing() {
this.unshare_screen(cx).log_err();
} else {
let t = this.share_screen(cx);
cx.spawn(move |_, _| async move {
t.await.log_err();
})
.detach();
}
})
})
})
});
}
fn toggle_deafen(&self, cx: &mut AppContext) {
self.active_call.as_ref().map(|call| {
call.0.update(cx, |this, cx| {
this.room().map(|room| {
room.update(cx, |this, cx| {
this.toggle_deafen(cx).log_err();
})
})
})
});
}
fn is_deafened(&self, cx: &AppContext) -> Option<bool> {
self.active_call
.as_ref()
.map(|call| {
call.0
.read(cx)
.room()
.map(|room| room.read(cx).is_deafened())
})
.flatten()
.flatten()
}
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use gpui::TestAppContext; use gpui::TestAppContext;

View File

@ -4,7 +4,7 @@ use client::{proto, User};
use collections::HashMap; use collections::HashMap;
use gpui::WeakModel; use gpui::WeakModel;
pub use live_kit_client::Frame; pub use live_kit_client::Frame;
use live_kit_client::{RemoteAudioTrack, RemoteVideoTrack}; pub(crate) use live_kit_client::{RemoteAudioTrack, RemoteVideoTrack};
use project::Project; use project::Project;
use std::sync::Arc; use std::sync::Arc;

View File

@ -1,7 +1,4 @@
use crate::{ use crate::participant::{LocalParticipant, ParticipantLocation, RemoteParticipant};
call_settings::CallSettings,
participant::{LocalParticipant, ParticipantLocation, RemoteParticipant},
};
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use audio::{Audio, Sound}; use audio::{Audio, Sound};
use client::{ use client::{
@ -21,7 +18,6 @@ use live_kit_client::{
}; };
use postage::{sink::Sink, stream::Stream, watch}; use postage::{sink::Sink, stream::Stream, watch};
use project::Project; use project::Project;
use settings::Settings;
use std::{future::Future, mem, sync::Arc, time::Duration}; use std::{future::Future, mem, sync::Arc, time::Duration};
use util::{post_inc, ResultExt, TryFutureExt}; use util::{post_inc, ResultExt, TryFutureExt};
@ -332,8 +328,10 @@ impl Room {
} }
} }
pub fn mute_on_join(cx: &AppContext) -> bool { pub fn mute_on_join(_cx: &AppContext) -> bool {
CallSettings::get_global(cx).mute_on_join || client::IMPERSONATE_LOGIN.is_some() // todo!() po: This should be uncommented, though then unmuting does not work
false
//CallSettings::get_global(cx).mute_on_join || client::IMPERSONATE_LOGIN.is_some()
} }
fn from_join_response( fn from_join_response(

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

View File

@ -109,6 +109,10 @@ pub enum ClickhouseEvent {
virtual_memory_in_bytes: u64, virtual_memory_in_bytes: u64,
milliseconds_since_first_event: i64, milliseconds_since_first_event: i64,
}, },
App {
operation: &'static str,
milliseconds_since_first_event: i64,
},
} }
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
@ -168,13 +172,8 @@ impl Telemetry {
let mut state = self.state.lock(); let mut state = self.state.lock();
state.installation_id = installation_id.map(|id| id.into()); state.installation_id = installation_id.map(|id| id.into());
state.session_id = Some(session_id.into()); state.session_id = Some(session_id.into());
let has_clickhouse_events = !state.clickhouse_events_queue.is_empty();
drop(state); drop(state);
if has_clickhouse_events {
self.flush_clickhouse_events();
}
let this = self.clone(); let this = self.clone();
cx.spawn(|mut cx| async move { cx.spawn(|mut cx| async move {
// Avoiding calling `System::new_all()`, as there have been crashes related to it // Avoiding calling `System::new_all()`, as there have been crashes related to it
@ -256,7 +255,7 @@ impl Telemetry {
milliseconds_since_first_event: self.milliseconds_since_first_event(), milliseconds_since_first_event: self.milliseconds_since_first_event(),
}; };
self.report_clickhouse_event(event, telemetry_settings) self.report_clickhouse_event(event, telemetry_settings, false)
} }
pub fn report_copilot_event( pub fn report_copilot_event(
@ -273,7 +272,7 @@ impl Telemetry {
milliseconds_since_first_event: self.milliseconds_since_first_event(), milliseconds_since_first_event: self.milliseconds_since_first_event(),
}; };
self.report_clickhouse_event(event, telemetry_settings) self.report_clickhouse_event(event, telemetry_settings, false)
} }
pub fn report_assistant_event( pub fn report_assistant_event(
@ -290,7 +289,7 @@ impl Telemetry {
milliseconds_since_first_event: self.milliseconds_since_first_event(), milliseconds_since_first_event: self.milliseconds_since_first_event(),
}; };
self.report_clickhouse_event(event, telemetry_settings) self.report_clickhouse_event(event, telemetry_settings, false)
} }
pub fn report_call_event( pub fn report_call_event(
@ -307,7 +306,7 @@ impl Telemetry {
milliseconds_since_first_event: self.milliseconds_since_first_event(), milliseconds_since_first_event: self.milliseconds_since_first_event(),
}; };
self.report_clickhouse_event(event, telemetry_settings) self.report_clickhouse_event(event, telemetry_settings, false)
} }
pub fn report_cpu_event( pub fn report_cpu_event(
@ -322,7 +321,7 @@ impl Telemetry {
milliseconds_since_first_event: self.milliseconds_since_first_event(), milliseconds_since_first_event: self.milliseconds_since_first_event(),
}; };
self.report_clickhouse_event(event, telemetry_settings) self.report_clickhouse_event(event, telemetry_settings, false)
} }
pub fn report_memory_event( pub fn report_memory_event(
@ -337,7 +336,21 @@ impl Telemetry {
milliseconds_since_first_event: self.milliseconds_since_first_event(), milliseconds_since_first_event: self.milliseconds_since_first_event(),
}; };
self.report_clickhouse_event(event, telemetry_settings) self.report_clickhouse_event(event, telemetry_settings, false)
}
// app_events are called at app open and app close, so flush is set to immediately send
pub fn report_app_event(
self: &Arc<Self>,
telemetry_settings: TelemetrySettings,
operation: &'static str,
) {
let event = ClickhouseEvent::App {
operation,
milliseconds_since_first_event: self.milliseconds_since_first_event(),
};
self.report_clickhouse_event(event, telemetry_settings, true)
} }
fn milliseconds_since_first_event(&self) -> i64 { fn milliseconds_since_first_event(&self) -> i64 {
@ -358,6 +371,7 @@ impl Telemetry {
self: &Arc<Self>, self: &Arc<Self>,
event: ClickhouseEvent, event: ClickhouseEvent,
telemetry_settings: TelemetrySettings, telemetry_settings: TelemetrySettings,
immediate_flush: bool,
) { ) {
if !telemetry_settings.metrics { if !telemetry_settings.metrics {
return; return;
@ -370,7 +384,7 @@ impl Telemetry {
.push(ClickhouseEventWrapper { signed_in, event }); .push(ClickhouseEventWrapper { signed_in, event });
if state.installation_id.is_some() { if state.installation_id.is_some() {
if state.clickhouse_events_queue.len() >= MAX_QUEUE_LEN { if immediate_flush || state.clickhouse_events_queue.len() >= MAX_QUEUE_LEN {
drop(state); drop(state);
self.flush_clickhouse_events(); self.flush_clickhouse_events();
} else { } else {

View File

@ -382,7 +382,7 @@ impl settings::Settings for TelemetrySettings {
} }
impl Client { impl Client {
pub fn new(http: Arc<dyn HttpClient>, cx: &AppContext) -> Arc<Self> { pub fn new(http: Arc<dyn HttpClient>, cx: &mut AppContext) -> Arc<Self> {
Arc::new(Self { Arc::new(Self {
id: AtomicU64::new(0), id: AtomicU64::new(0),
peer: Peer::new(0), peer: Peer::new(0),
@ -551,7 +551,6 @@ impl Client {
F: 'static + Future<Output = Result<()>>, F: 'static + Future<Output = Result<()>>,
{ {
let message_type_id = TypeId::of::<M>(); let message_type_id = TypeId::of::<M>();
let mut state = self.state.write(); let mut state = self.state.write();
state state
.models_by_message_type .models_by_message_type

View File

@ -1,5 +1,6 @@
use crate::{TelemetrySettings, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL}; use crate::{TelemetrySettings, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use futures::Future;
use gpui::{serde_json, AppContext, AppMetadata, BackgroundExecutor, Task}; use gpui::{serde_json, AppContext, AppMetadata, BackgroundExecutor, Task};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use parking_lot::Mutex; use parking_lot::Mutex;
@ -107,6 +108,10 @@ pub enum ClickhouseEvent {
virtual_memory_in_bytes: u64, virtual_memory_in_bytes: u64,
milliseconds_since_first_event: i64, milliseconds_since_first_event: i64,
}, },
App {
operation: &'static str,
milliseconds_since_first_event: i64,
},
} }
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
@ -122,12 +127,13 @@ const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(1);
const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(30); const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(30);
impl Telemetry { impl Telemetry {
pub fn new(client: Arc<dyn HttpClient>, cx: &AppContext) -> Arc<Self> { pub fn new(client: Arc<dyn HttpClient>, cx: &mut AppContext) -> Arc<Self> {
let release_channel = if cx.has_global::<ReleaseChannel>() { let release_channel = if cx.has_global::<ReleaseChannel>() {
Some(cx.global::<ReleaseChannel>().display_name()) Some(cx.global::<ReleaseChannel>().display_name())
} else { } else {
None None
}; };
// TODO: Replace all hardware stuff with nested SystemSpecs json // TODO: Replace all hardware stuff with nested SystemSpecs json
let this = Arc::new(Self { let this = Arc::new(Self {
http_client: client, http_client: client,
@ -147,9 +153,30 @@ impl Telemetry {
}), }),
}); });
// We should only ever have one instance of Telemetry, leak the subscription to keep it alive
// rather than store in TelemetryState, complicating spawn as subscriptions are not Send
std::mem::forget(cx.on_app_quit({
let this = this.clone();
move |cx| this.shutdown_telemetry(cx)
}));
this this
} }
#[cfg(any(test, feature = "test-support"))]
fn shutdown_telemetry(self: &Arc<Self>, _: &mut AppContext) -> impl Future<Output = ()> {
Task::ready(())
}
// Skip calling this function in tests.
// TestAppContext ends up calling this function on shutdown and it panics when trying to find the TelemetrySettings
#[cfg(not(any(test, feature = "test-support")))]
fn shutdown_telemetry(self: &Arc<Self>, cx: &mut AppContext) -> impl Future<Output = ()> {
let telemetry_settings = TelemetrySettings::get_global(cx).clone();
self.report_app_event(telemetry_settings, "close");
Task::ready(())
}
pub fn log_file_path(&self) -> Option<PathBuf> { pub fn log_file_path(&self) -> Option<PathBuf> {
Some(self.state.lock().log_file.as_ref()?.path().to_path_buf()) Some(self.state.lock().log_file.as_ref()?.path().to_path_buf())
} }
@ -163,13 +190,8 @@ impl Telemetry {
let mut state = self.state.lock(); let mut state = self.state.lock();
state.installation_id = installation_id.map(|id| id.into()); state.installation_id = installation_id.map(|id| id.into());
state.session_id = Some(session_id.into()); state.session_id = Some(session_id.into());
let has_clickhouse_events = !state.clickhouse_events_queue.is_empty();
drop(state); drop(state);
if has_clickhouse_events {
self.flush_clickhouse_events();
}
let this = self.clone(); let this = self.clone();
cx.spawn(|cx| async move { cx.spawn(|cx| async move {
// Avoiding calling `System::new_all()`, as there have been crashes related to it // Avoiding calling `System::new_all()`, as there have been crashes related to it
@ -257,7 +279,7 @@ impl Telemetry {
milliseconds_since_first_event: self.milliseconds_since_first_event(), milliseconds_since_first_event: self.milliseconds_since_first_event(),
}; };
self.report_clickhouse_event(event, telemetry_settings) self.report_clickhouse_event(event, telemetry_settings, false)
} }
pub fn report_copilot_event( pub fn report_copilot_event(
@ -274,7 +296,7 @@ impl Telemetry {
milliseconds_since_first_event: self.milliseconds_since_first_event(), milliseconds_since_first_event: self.milliseconds_since_first_event(),
}; };
self.report_clickhouse_event(event, telemetry_settings) self.report_clickhouse_event(event, telemetry_settings, false)
} }
pub fn report_assistant_event( pub fn report_assistant_event(
@ -291,7 +313,7 @@ impl Telemetry {
milliseconds_since_first_event: self.milliseconds_since_first_event(), milliseconds_since_first_event: self.milliseconds_since_first_event(),
}; };
self.report_clickhouse_event(event, telemetry_settings) self.report_clickhouse_event(event, telemetry_settings, false)
} }
pub fn report_call_event( pub fn report_call_event(
@ -308,7 +330,7 @@ impl Telemetry {
milliseconds_since_first_event: self.milliseconds_since_first_event(), milliseconds_since_first_event: self.milliseconds_since_first_event(),
}; };
self.report_clickhouse_event(event, telemetry_settings) self.report_clickhouse_event(event, telemetry_settings, false)
} }
pub fn report_cpu_event( pub fn report_cpu_event(
@ -323,7 +345,7 @@ impl Telemetry {
milliseconds_since_first_event: self.milliseconds_since_first_event(), milliseconds_since_first_event: self.milliseconds_since_first_event(),
}; };
self.report_clickhouse_event(event, telemetry_settings) self.report_clickhouse_event(event, telemetry_settings, false)
} }
pub fn report_memory_event( pub fn report_memory_event(
@ -338,7 +360,21 @@ impl Telemetry {
milliseconds_since_first_event: self.milliseconds_since_first_event(), milliseconds_since_first_event: self.milliseconds_since_first_event(),
}; };
self.report_clickhouse_event(event, telemetry_settings) self.report_clickhouse_event(event, telemetry_settings, false)
}
// app_events are called at app open and app close, so flush is set to immediately send
pub fn report_app_event(
self: &Arc<Self>,
telemetry_settings: TelemetrySettings,
operation: &'static str,
) {
let event = ClickhouseEvent::App {
operation,
milliseconds_since_first_event: self.milliseconds_since_first_event(),
};
self.report_clickhouse_event(event, telemetry_settings, true)
} }
fn milliseconds_since_first_event(&self) -> i64 { fn milliseconds_since_first_event(&self) -> i64 {
@ -359,6 +395,7 @@ impl Telemetry {
self: &Arc<Self>, self: &Arc<Self>,
event: ClickhouseEvent, event: ClickhouseEvent,
telemetry_settings: TelemetrySettings, telemetry_settings: TelemetrySettings,
immediate_flush: bool,
) { ) {
if !telemetry_settings.metrics { if !telemetry_settings.metrics {
return; return;
@ -371,7 +408,7 @@ impl Telemetry {
.push(ClickhouseEventWrapper { signed_in, event }); .push(ClickhouseEventWrapper { signed_in, event });
if state.installation_id.is_some() { if state.installation_id.is_some() {
if state.clickhouse_events_queue.len() >= MAX_QUEUE_LEN { if immediate_flush || state.clickhouse_events_queue.len() >= MAX_QUEUE_LEN {
drop(state); drop(state);
self.flush_clickhouse_events(); self.flush_clickhouse_events();
} else { } else {

View File

@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
default-run = "collab" default-run = "collab"
edition = "2021" edition = "2021"
name = "collab" name = "collab"
version = "0.28.0" version = "0.29.0"
publish = false publish = false
[[bin]] [[bin]]

View File

@ -10,7 +10,7 @@ publish = false
name = "collab2" name = "collab2"
[[bin]] [[bin]]
name = "seed" name = "seed2"
required-features = ["seed-support"] required-features = ["seed-support"]
[dependencies] [dependencies]

View File

@ -149,7 +149,7 @@ impl TestServer {
.user_id .user_id
}; };
let client_name = name.to_string(); let client_name = name.to_string();
let mut client = cx.read(|cx| Client::new(http.clone(), cx)); let mut client = cx.update(|cx| Client::new(http.clone(), cx));
let server = self.server.clone(); let server = self.server.clone();
let db = self.app_state.db.clone(); let db = self.app_state.db.clone();
let connection_killers = self.connection_killers.clone(); let connection_killers = self.connection_killers.clone();
@ -221,6 +221,7 @@ impl TestServer {
fs: fs.clone(), fs: fs.clone(),
build_window_options: |_, _, _| Default::default(), build_window_options: |_, _, _| Default::default(),
node_runtime: FakeNodeRuntime::new(), node_runtime: FakeNodeRuntime::new(),
call_factory: |_, _| Box::new(workspace::TestCallHandler),
}); });
cx.update(|cx| { cx.update(|cx| {

View File

@ -157,15 +157,17 @@ const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel";
use std::sync::Arc; use std::sync::Arc;
use client::{Client, Contact, UserStore};
use db::kvp::KEY_VALUE_STORE; use db::kvp::KEY_VALUE_STORE;
use gpui::{ use gpui::{
actions, div, serde_json, AppContext, AsyncWindowContext, Div, EventEmitter, FocusHandle, actions, div, serde_json, AppContext, AsyncWindowContext, Div, EventEmitter, FocusHandle,
Focusable, FocusableView, InteractiveElement, ParentElement, Render, View, ViewContext, Focusable, FocusableView, InteractiveElement, Model, ParentElement, Render, Styled, View,
VisualContext, WeakView, ViewContext, VisualContext, WeakView,
}; };
use project::Fs; use project::Fs;
use serde_derive::{Deserialize, Serialize}; use serde_derive::{Deserialize, Serialize};
use settings::Settings; use settings::Settings;
use ui::{h_stack, Avatar, Label};
use util::ResultExt; use util::ResultExt;
use workspace::{ use workspace::{
dock::{DockPosition, Panel, PanelEvent}, dock::{DockPosition, Panel, PanelEvent},
@ -299,8 +301,8 @@ pub struct CollabPanel {
// channel_editing_state: Option<ChannelEditingState>, // channel_editing_state: Option<ChannelEditingState>,
// entries: Vec<ListEntry>, // entries: Vec<ListEntry>,
// selection: Option<usize>, // selection: Option<usize>,
// user_store: ModelHandle<UserStore>, user_store: Model<UserStore>,
// client: Arc<Client>, _client: Arc<Client>,
// channel_store: ModelHandle<ChannelStore>, // channel_store: ModelHandle<ChannelStore>,
// project: ModelHandle<Project>, // project: ModelHandle<Project>,
// match_candidates: Vec<StringMatchCandidate>, // match_candidates: Vec<StringMatchCandidate>,
@ -595,7 +597,7 @@ impl CollabPanel {
// entries: Vec::default(), // entries: Vec::default(),
// channel_editing_state: None, // channel_editing_state: None,
// selection: None, // selection: None,
// user_store: workspace.user_store().clone(), user_store: workspace.user_store().clone(),
// channel_store: ChannelStore::global(cx), // channel_store: ChannelStore::global(cx),
// project: workspace.project().clone(), // project: workspace.project().clone(),
// subscriptions: Vec::default(), // subscriptions: Vec::default(),
@ -603,7 +605,7 @@ impl CollabPanel {
// collapsed_sections: vec![Section::Offline], // collapsed_sections: vec![Section::Offline],
// collapsed_channels: Vec::default(), // collapsed_channels: Vec::default(),
_workspace: workspace.weak_handle(), _workspace: workspace.weak_handle(),
// client: workspace.app_state().client.clone(), _client: workspace.app_state().client.clone(),
// context_menu_on_selected: true, // context_menu_on_selected: true,
// drag_target_channel: ChannelDragTarget::None, // drag_target_channel: ChannelDragTarget::None,
// list_state, // list_state,
@ -663,6 +665,9 @@ impl CollabPanel {
}) })
} }
fn contacts(&self, cx: &AppContext) -> Option<Vec<Arc<Contact>>> {
Some(self.user_store.read(cx).contacts().to_owned())
}
pub async fn load( pub async fn load(
workspace: WeakView<Workspace>, workspace: WeakView<Workspace>,
mut cx: AsyncWindowContext, mut cx: AsyncWindowContext,
@ -3297,11 +3302,38 @@ impl CollabPanel {
impl Render for CollabPanel { impl Render for CollabPanel {
type Element = Focusable<Div>; type Element = Focusable<Div>;
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element { fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
let contacts = self.contacts(cx).unwrap_or_default();
let workspace = self._workspace.clone();
div() div()
.key_context("CollabPanel") .key_context("CollabPanel")
.track_focus(&self.focus_handle) .track_focus(&self.focus_handle)
.child("COLLAB PANEL") .children(contacts.into_iter().map(|contact| {
let id = contact.user.id;
h_stack()
.p_2()
.gap_2()
.children(
contact
.user
.avatar
.as_ref()
.map(|avatar| Avatar::data(avatar.clone())),
)
.child(Label::new(contact.user.github_login.clone()))
.on_mouse_down(gpui::MouseButton::Left, {
let workspace = workspace.clone();
move |_, cx| {
workspace
.update(cx, |this, cx| {
this.call_state()
.invite(id, None, cx)
.detach_and_log_err(cx)
})
.log_err();
}
})
}))
} }
} }

View File

@ -31,15 +31,18 @@ use std::sync::Arc;
use call::ActiveCall; use call::ActiveCall;
use client::{Client, UserStore}; use client::{Client, UserStore};
use gpui::{ use gpui::{
div, px, rems, AppContext, Div, InteractiveElement, Model, ParentElement, Render, RenderOnce, div, px, rems, AppContext, Div, Element, InteractiveElement, IntoElement, Model, MouseButton,
Stateful, StatefulInteractiveElement, Styled, Subscription, ViewContext, VisualContext, ParentElement, Render, RenderOnce, Stateful, StatefulInteractiveElement, Styled, Subscription,
WeakView, WindowBounds, ViewContext, VisualContext, WeakView, WindowBounds,
}; };
use project::Project; use project::Project;
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::{h_stack, Button, ButtonVariant, KeyBinding, Label, TextColor, Tooltip}; use ui::{h_stack, Avatar, Button, ButtonVariant, Color, IconButton, KeyBinding, Tooltip};
use util::ResultExt;
use workspace::Workspace; use workspace::Workspace;
use crate::face_pile::FacePile;
// const MAX_PROJECT_NAME_LENGTH: usize = 40; // const MAX_PROJECT_NAME_LENGTH: usize = 40;
// const MAX_BRANCH_NAME_LENGTH: usize = 40; // const MAX_BRANCH_NAME_LENGTH: usize = 40;
@ -85,6 +88,41 @@ impl Render for CollabTitlebarItem {
type Element = Stateful<Div>; type Element = Stateful<Div>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element { fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
let is_in_room = self
.workspace
.update(cx, |this, cx| this.call_state().is_in_room(cx))
.unwrap_or_default();
let is_shared = is_in_room && self.project.read(cx).is_shared();
let current_user = self.user_store.read(cx).current_user();
let client = self.client.clone();
let users = self
.workspace
.update(cx, |this, cx| this.call_state().remote_participants(cx))
.log_err()
.flatten();
let mic_icon = if self
.workspace
.update(cx, |this, cx| this.call_state().is_muted(cx))
.log_err()
.flatten()
.unwrap_or_default()
{
ui::Icon::MicMute
} else {
ui::Icon::Mic
};
let speakers_icon = if self
.workspace
.update(cx, |this, cx| this.call_state().is_deafened(cx))
.log_err()
.flatten()
.unwrap_or_default()
{
ui::Icon::AudioOff
} else {
ui::Icon::AudioOn
};
let workspace = self.workspace.clone();
h_stack() h_stack()
.id("titlebar") .id("titlebar")
.justify_between() .justify_between()
@ -111,17 +149,21 @@ impl Render for CollabTitlebarItem {
// TODO - Add player menu // TODO - Add player menu
.child( .child(
div() div()
.border()
.border_color(gpui::red())
.id("project_owner_indicator") .id("project_owner_indicator")
.child( .child(
Button::new("player") Button::new("player")
.variant(ButtonVariant::Ghost) .variant(ButtonVariant::Ghost)
.color(Some(TextColor::Player(0))), .color(Some(Color::Player(0))),
) )
.tooltip(move |cx| Tooltip::text("Toggle following", cx)), .tooltip(move |cx| Tooltip::text("Toggle following", cx)),
) )
// TODO - Add project menu // TODO - Add project menu
.child( .child(
div() div()
.border()
.border_color(gpui::red())
.id("titlebar_project_menu_button") .id("titlebar_project_menu_button")
.child(Button::new("project_name").variant(ButtonVariant::Ghost)) .child(Button::new("project_name").variant(ButtonVariant::Ghost))
.tooltip(move |cx| Tooltip::text("Recent Projects", cx)), .tooltip(move |cx| Tooltip::text("Recent Projects", cx)),
@ -129,11 +171,13 @@ impl Render for CollabTitlebarItem {
// TODO - Add git menu // TODO - Add git menu
.child( .child(
div() div()
.border()
.border_color(gpui::red())
.id("titlebar_git_menu_button") .id("titlebar_git_menu_button")
.child( .child(
Button::new("branch_name") Button::new("branch_name")
.variant(ButtonVariant::Ghost) .variant(ButtonVariant::Ghost)
.color(Some(TextColor::Muted)), .color(Some(Color::Muted)),
) )
.tooltip(move |cx| { .tooltip(move |cx| {
cx.build_view(|_| { cx.build_view(|_| {
@ -149,8 +193,111 @@ impl Render for CollabTitlebarItem {
.into() .into()
}), }),
), ),
) // self.titlebar_item )
.child(h_stack().child(Label::new("Right side titlebar item"))) .when_some(
users.zip(current_user.clone()),
|this, (remote_participants, current_user)| {
let mut pile = FacePile::default();
pile.extend(
current_user
.avatar
.clone()
.map(|avatar| {
div().child(Avatar::data(avatar.clone())).into_any_element()
})
.into_iter()
.chain(remote_participants.into_iter().flat_map(|(user, peer_id)| {
user.avatar.as_ref().map(|avatar| {
div()
.child(
Avatar::data(avatar.clone()).into_element().into_any(),
)
.on_mouse_down(MouseButton::Left, {
let workspace = workspace.clone();
move |_, cx| {
workspace
.update(cx, |this, cx| {
this.open_shared_screen(peer_id, cx);
})
.log_err();
}
})
.into_any_element()
})
})),
);
this.child(pile.render(cx))
},
)
.child(div().flex_1())
.when(is_in_room, |this| {
this.child(
h_stack()
.child(
h_stack()
.child(Button::new(if is_shared { "Unshare" } else { "Share" }))
.child(IconButton::new("leave-call", ui::Icon::Exit).on_click({
let workspace = workspace.clone();
move |_, cx| {
workspace
.update(cx, |this, cx| {
this.call_state().hang_up(cx).detach();
})
.log_err();
}
})),
)
.child(
h_stack()
.child(IconButton::new("mute-microphone", mic_icon).on_click({
let workspace = workspace.clone();
move |_, cx| {
workspace
.update(cx, |this, cx| {
this.call_state().toggle_mute(cx);
})
.log_err();
}
}))
.child(IconButton::new("mute-sound", speakers_icon).on_click({
let workspace = workspace.clone();
move |_, cx| {
workspace
.update(cx, |this, cx| {
this.call_state().toggle_deafen(cx);
})
.log_err();
}
}))
.child(IconButton::new("screen-share", ui::Icon::Screen).on_click(
move |_, cx| {
workspace
.update(cx, |this, cx| {
this.call_state().toggle_screen_share(cx);
})
.log_err();
},
))
.pl_2(),
),
)
})
.map(|this| {
if let Some(user) = current_user {
this.when_some(user.avatar.clone(), |this, avatar| {
this.child(ui::Avatar::data(avatar))
})
} else {
this.child(Button::new("Sign in").on_click(move |_, cx| {
let client = client.clone();
cx.spawn(move |cx| async move {
client.authenticate_and_connect(true, &cx).await?;
Ok::<(), anyhow::Error>(())
})
.detach_and_log_err(cx);
}))
}
})
} }
} }

View File

@ -7,11 +7,14 @@ pub mod notification_panel;
pub mod notifications; pub mod notifications;
mod panel_settings; mod panel_settings;
use std::sync::Arc; use std::{rc::Rc, sync::Arc};
pub use collab_panel::CollabPanel; pub use collab_panel::CollabPanel;
pub use collab_titlebar_item::CollabTitlebarItem; pub use collab_titlebar_item::CollabTitlebarItem;
use gpui::AppContext; use gpui::{
point, AppContext, GlobalPixels, Pixels, PlatformDisplay, Size, WindowBounds, WindowKind,
WindowOptions,
};
pub use panel_settings::{ pub use panel_settings::{
ChatPanelSettings, CollaborationPanelSettings, NotificationPanelSettings, ChatPanelSettings, CollaborationPanelSettings, NotificationPanelSettings,
}; };
@ -23,7 +26,7 @@ use workspace::AppState;
// [ToggleScreenSharing, ToggleMute, ToggleDeafen, LeaveCall] // [ToggleScreenSharing, ToggleMute, ToggleDeafen, LeaveCall]
// ); // );
pub fn init(_app_state: &Arc<AppState>, cx: &mut AppContext) { pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
CollaborationPanelSettings::register(cx); CollaborationPanelSettings::register(cx);
ChatPanelSettings::register(cx); ChatPanelSettings::register(cx);
NotificationPanelSettings::register(cx); NotificationPanelSettings::register(cx);
@ -32,7 +35,7 @@ pub fn init(_app_state: &Arc<AppState>, cx: &mut AppContext) {
collab_titlebar_item::init(cx); collab_titlebar_item::init(cx);
collab_panel::init(cx); collab_panel::init(cx);
// chat_panel::init(cx); // chat_panel::init(cx);
// notifications::init(&app_state, cx); notifications::init(&app_state, cx);
// cx.add_global_action(toggle_screen_sharing); // cx.add_global_action(toggle_screen_sharing);
// cx.add_global_action(toggle_mute); // cx.add_global_action(toggle_mute);
@ -95,31 +98,36 @@ pub fn init(_app_state: &Arc<AppState>, cx: &mut AppContext) {
// } // }
// } // }
// fn notification_window_options( fn notification_window_options(
// screen: Rc<dyn Screen>, screen: Rc<dyn PlatformDisplay>,
// window_size: Vector2F, window_size: Size<Pixels>,
// ) -> WindowOptions<'static> { ) -> WindowOptions {
// const NOTIFICATION_PADDING: f32 = 16.; let notification_margin_width = GlobalPixels::from(16.);
let notification_margin_height = GlobalPixels::from(-0.) - GlobalPixels::from(48.);
// let screen_bounds = screen.content_bounds(); let screen_bounds = screen.bounds();
// WindowOptions { let size: Size<GlobalPixels> = window_size.into();
// bounds: WindowBounds::Fixed(RectF::new(
// screen_bounds.upper_right() // todo!() use content bounds instead of screen.bounds and get rid of magics in point's 2nd argument.
// + vec2f( let bounds = gpui::Bounds::<GlobalPixels> {
// -NOTIFICATION_PADDING - window_size.x(), origin: screen_bounds.upper_right()
// NOTIFICATION_PADDING, - point(
// ), size.width + notification_margin_width,
// window_size, notification_margin_height,
// )), ),
// titlebar: None, size: window_size.into(),
// center: false, };
// focus: false, WindowOptions {
// show: true, bounds: WindowBounds::Fixed(bounds),
// kind: WindowKind::PopUp, titlebar: None,
// is_movable: false, center: false,
// screen: Some(screen), focus: false,
// } show: true,
// } kind: WindowKind::PopUp,
is_movable: false,
display_id: Some(screen.id()),
}
}
// fn render_avatar<T: 'static>( // fn render_avatar<T: 'static>(
// avatar: Option<Arc<ImageData>>, // avatar: Option<Arc<ImageData>>,

View File

@ -1,54 +1,48 @@
// use std::ops::Range; use gpui::{
div, AnyElement, Div, IntoElement as _, ParentElement as _, RenderOnce, Styled, WindowContext,
};
// use gpui::{ #[derive(Default)]
// geometry::{ pub(crate) struct FacePile {
// rect::RectF, faces: Vec<AnyElement>,
// vector::{vec2f, Vector2F}, }
// },
// json::ToJson,
// serde_json::{self, json},
// AnyElement, Axis, Element, View, ViewContext,
// };
// pub(crate) struct FacePile<V: View> { impl RenderOnce for FacePile {
// overlap: f32, type Rendered = Div;
// faces: Vec<AnyElement<V>>,
// }
// impl<V: View> FacePile<V> { fn render(self, _: &mut WindowContext) -> Self::Rendered {
// pub fn new(overlap: f32) -> Self { let player_count = self.faces.len();
// Self { let player_list = self.faces.into_iter().enumerate().map(|(ix, player)| {
// overlap, let isnt_last = ix < player_count - 1;
// faces: Vec::new(),
// }
// }
// }
// impl<V: View> Element<V> for FacePile<V> { div().when(isnt_last, |div| div.neg_mr_1()).child(player)
// type LayoutState = (); });
// type PaintState = (); div().p_1().flex().items_center().children(player_list)
}
}
// impl Element for FacePile {
// type State = ();
// fn layout( // fn layout(
// &mut self, // &mut self,
// constraint: gpui::SizeConstraint, // state: Option<Self::State>,
// view: &mut V, // cx: &mut WindowContext,
// cx: &mut ViewContext<V>, // ) -> (LayoutId, Self::State) {
// ) -> (Vector2F, Self::LayoutState) {
// debug_assert!(constraint.max_along(Axis::Horizontal) == f32::INFINITY);
// let mut width = 0.; // let mut width = 0.;
// let mut max_height = 0.; // let mut max_height = 0.;
// let mut faces = Vec::with_capacity(self.faces.len());
// for face in &mut self.faces { // for face in &mut self.faces {
// let layout = face.layout(constraint, view, cx); // let layout = face.layout(cx);
// width += layout.x(); // width += layout.x();
// max_height = f32::max(max_height, layout.y()); // max_height = f32::max(max_height, layout.y());
// faces.push(layout);
// } // }
// width -= self.overlap * self.faces.len().saturating_sub(1) as f32; // width -= self.overlap * self.faces.len().saturating_sub(1) as f32;
// (cx.request_layout(&Style::default(), faces), ())
// ( // // (
// Vector2F::new(width, max_height.clamp(1., constraint.max.y())), // // Vector2F::new(width, max_height.clamp(1., constraint.max.y())),
// (), // // (),
// ) // // ))
// } // }
// fn paint( // fn paint(
@ -77,37 +71,10 @@
// () // ()
// } // }
// fn rect_for_text_range(
// &self,
// _: Range<usize>,
// _: RectF,
// _: RectF,
// _: &Self::LayoutState,
// _: &Self::PaintState,
// _: &V,
// _: &ViewContext<V>,
// ) -> Option<RectF> {
// None
// }
// fn debug(
// &self,
// bounds: RectF,
// _: &Self::LayoutState,
// _: &Self::PaintState,
// _: &V,
// _: &ViewContext<V>,
// ) -> serde_json::Value {
// json!({
// "type": "FacePile",
// "bounds": bounds.to_json()
// })
// }
// } // }
// impl<V: View> Extend<AnyElement<V>> for FacePile<V> { impl Extend<AnyElement> for FacePile {
// fn extend<T: IntoIterator<Item = AnyElement<V>>>(&mut self, children: T) { fn extend<T: IntoIterator<Item = AnyElement>>(&mut self, children: T) {
// self.faces.extend(children); self.faces.extend(children);
// } }
// } }

View File

@ -1,11 +1,11 @@
// use gpui::AppContext; use gpui::AppContext;
// use std::sync::Arc; use std::sync::Arc;
// use workspace::AppState; use workspace::AppState;
// pub mod incoming_call_notification; pub mod incoming_call_notification;
// pub mod project_shared_notification; // pub mod project_shared_notification;
// pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) { pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
// incoming_call_notification::init(app_state, cx); incoming_call_notification::init(app_state, cx);
// project_shared_notification::init(app_state, cx); //project_shared_notification::init(app_state, cx);
// } }

View File

@ -1,14 +1,12 @@
use crate::notification_window_options; use crate::notification_window_options;
use call::{ActiveCall, IncomingCall}; use call::{ActiveCall, IncomingCall};
use client::proto;
use futures::StreamExt; use futures::StreamExt;
use gpui::{ use gpui::{
elements::*, div, green, px, red, AppContext, Div, Element, ParentElement, Render, RenderOnce,
geometry::vector::vec2f, StatefulInteractiveElement, Styled, ViewContext, VisualContext as _, WindowHandle,
platform::{CursorStyle, MouseButton},
AnyElement, AppContext, Entity, View, ViewContext, WindowHandle,
}; };
use std::sync::{Arc, Weak}; use std::sync::{Arc, Weak};
use ui::{h_stack, v_stack, Avatar, Button, Label};
use util::ResultExt; use util::ResultExt;
use workspace::AppState; use workspace::AppState;
@ -19,23 +17,44 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
let mut notification_windows: Vec<WindowHandle<IncomingCallNotification>> = Vec::new(); let mut notification_windows: Vec<WindowHandle<IncomingCallNotification>> = Vec::new();
while let Some(incoming_call) = incoming_call.next().await { while let Some(incoming_call) = incoming_call.next().await {
for window in notification_windows.drain(..) { for window in notification_windows.drain(..) {
window.remove(&mut cx); window
.update(&mut cx, |_, cx| {
// todo!()
cx.remove_window();
})
.log_err();
} }
if let Some(incoming_call) = incoming_call { if let Some(incoming_call) = incoming_call {
let window_size = cx.read(|cx| { let unique_screens = cx.update(|cx| cx.displays()).unwrap();
let theme = &theme::current(cx).incoming_call_notification; let window_size = gpui::Size {
vec2f(theme.window_width, theme.window_height) width: px(380.),
}); height: px(64.),
};
for screen in cx.platform().screens() { for window in unique_screens {
let options = notification_window_options(window, window_size);
let window = cx let window = cx
.add_window(notification_window_options(screen, window_size), |_| { .open_window(options, |cx| {
IncomingCallNotification::new(incoming_call.clone(), app_state.clone()) cx.build_view(|_| {
}); IncomingCallNotification::new(
incoming_call.clone(),
app_state.clone(),
)
})
})
.unwrap();
notification_windows.push(window); notification_windows.push(window);
} }
// for screen in cx.platform().screens() {
// let window = cx
// .add_window(notification_window_options(screen, window_size), |_| {
// IncomingCallNotification::new(incoming_call.clone(), app_state.clone())
// });
// notification_windows.push(window);
// }
} }
} }
}) })
@ -47,167 +66,196 @@ struct RespondToCall {
accept: bool, accept: bool,
} }
pub struct IncomingCallNotification { struct IncomingCallNotificationState {
call: IncomingCall, call: IncomingCall,
app_state: Weak<AppState>, app_state: Weak<AppState>,
} }
impl IncomingCallNotification { pub struct IncomingCallNotification {
state: Arc<IncomingCallNotificationState>,
}
impl IncomingCallNotificationState {
pub fn new(call: IncomingCall, app_state: Weak<AppState>) -> Self { pub fn new(call: IncomingCall, app_state: Weak<AppState>) -> Self {
Self { call, app_state } Self { call, app_state }
} }
fn respond(&mut self, accept: bool, cx: &mut ViewContext<Self>) { fn respond(&self, accept: bool, cx: &mut AppContext) {
let active_call = ActiveCall::global(cx); let active_call = ActiveCall::global(cx);
if accept { if accept {
let join = active_call.update(cx, |active_call, cx| active_call.accept_incoming(cx)); let join = active_call.update(cx, |active_call, cx| active_call.accept_incoming(cx));
let caller_user_id = self.call.calling_user.id;
let initial_project_id = self.call.initial_project.as_ref().map(|project| project.id); let initial_project_id = self.call.initial_project.as_ref().map(|project| project.id);
let app_state = self.app_state.clone(); let app_state = self.app_state.clone();
cx.app_context() let cx: &mut AppContext = cx;
.spawn(|mut cx| async move { cx.spawn(|cx| async move {
join.await?; join.await?;
if let Some(project_id) = initial_project_id { if let Some(_project_id) = initial_project_id {
cx.update(|cx| { cx.update(|_cx| {
if let Some(app_state) = app_state.upgrade() { if let Some(_app_state) = app_state.upgrade() {
workspace::join_remote_project( // workspace::join_remote_project(
project_id, // project_id,
caller_user_id, // caller_user_id,
app_state, // app_state,
cx, // cx,
) // )
.detach_and_log_err(cx); // .detach_and_log_err(cx);
} }
}); })
} .log_err();
anyhow::Ok(()) }
}) anyhow::Ok(())
.detach_and_log_err(cx); })
.detach_and_log_err(cx);
} else { } else {
active_call.update(cx, |active_call, cx| { active_call.update(cx, |active_call, cx| {
active_call.decline_incoming(cx).log_err(); active_call.decline_incoming(cx).log_err();
}); });
} }
} }
}
fn render_caller(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> { impl IncomingCallNotification {
let theme = &theme::current(cx).incoming_call_notification; pub fn new(call: IncomingCall, app_state: Weak<AppState>) -> Self {
let default_project = proto::ParticipantProject::default(); Self {
let initial_project = self state: Arc::new(IncomingCallNotificationState::new(call, app_state)),
.call }
.initial_project }
.as_ref() fn render_caller(&self, cx: &mut ViewContext<Self>) -> impl Element {
.unwrap_or(&default_project); h_stack()
Flex::row() .children(
.with_children(self.call.calling_user.avatar.clone().map(|avatar| { self.state
Image::from_data(avatar) .call
.with_style(theme.caller_avatar) .calling_user
.aligned() .avatar
.as_ref()
.map(|avatar| Avatar::data(avatar.clone())),
)
.child(
v_stack()
.child(Label::new(format!(
"{} is sharing a project in Zed",
self.state.call.calling_user.github_login
)))
.child(self.render_buttons(cx)),
)
// let theme = &theme::current(cx).incoming_call_notification;
// let default_project = proto::ParticipantProject::default();
// let initial_project = self
// .call
// .initial_project
// .as_ref()
// .unwrap_or(&default_project);
// Flex::row()
// .with_children(self.call.calling_user.avatar.clone().map(|avatar| {
// Image::from_data(avatar)
// .with_style(theme.caller_avatar)
// .aligned()
// }))
// .with_child(
// Flex::column()
// .with_child(
// Label::new(
// self.call.calling_user.github_login.clone(),
// theme.caller_username.text.clone(),
// )
// .contained()
// .with_style(theme.caller_username.container),
// )
// .with_child(
// Label::new(
// format!(
// "is sharing a project in Zed{}",
// if initial_project.worktree_root_names.is_empty() {
// ""
// } else {
// ":"
// }
// ),
// theme.caller_message.text.clone(),
// )
// .contained()
// .with_style(theme.caller_message.container),
// )
// .with_children(if initial_project.worktree_root_names.is_empty() {
// None
// } else {
// Some(
// Label::new(
// initial_project.worktree_root_names.join(", "),
// theme.worktree_roots.text.clone(),
// )
// .contained()
// .with_style(theme.worktree_roots.container),
// )
// })
// .contained()
// .with_style(theme.caller_metadata)
// .aligned(),
// )
// .contained()
// .with_style(theme.caller_container)
// .flex(1., true)
// .into_any()
}
fn render_buttons(&self, cx: &mut ViewContext<Self>) -> impl Element {
h_stack()
.child(Button::new("Accept").render(cx).bg(green()).on_click({
let state = self.state.clone();
move |_, cx| state.respond(true, cx)
}))
.child(Button::new("Decline").render(cx).bg(red()).on_click({
let state = self.state.clone();
move |_, cx| state.respond(false, cx)
})) }))
.with_child(
Flex::column()
.with_child(
Label::new(
self.call.calling_user.github_login.clone(),
theme.caller_username.text.clone(),
)
.contained()
.with_style(theme.caller_username.container),
)
.with_child(
Label::new(
format!(
"is sharing a project in Zed{}",
if initial_project.worktree_root_names.is_empty() {
""
} else {
":"
}
),
theme.caller_message.text.clone(),
)
.contained()
.with_style(theme.caller_message.container),
)
.with_children(if initial_project.worktree_root_names.is_empty() {
None
} else {
Some(
Label::new(
initial_project.worktree_root_names.join(", "),
theme.worktree_roots.text.clone(),
)
.contained()
.with_style(theme.worktree_roots.container),
)
})
.contained()
.with_style(theme.caller_metadata)
.aligned(),
)
.contained()
.with_style(theme.caller_container)
.flex(1., true)
.into_any()
}
fn render_buttons(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> { // enum Accept {}
enum Accept {} // enum Decline {}
enum Decline {}
let theme = theme::current(cx); // let theme = theme::current(cx);
Flex::column() // Flex::column()
.with_child( // .with_child(
MouseEventHandler::new::<Accept, _>(0, cx, |_, _| { // MouseEventHandler::new::<Accept, _>(0, cx, |_, _| {
let theme = &theme.incoming_call_notification; // let theme = &theme.incoming_call_notification;
Label::new("Accept", theme.accept_button.text.clone()) // Label::new("Accept", theme.accept_button.text.clone())
.aligned() // .aligned()
.contained() // .contained()
.with_style(theme.accept_button.container) // .with_style(theme.accept_button.container)
}) // })
.with_cursor_style(CursorStyle::PointingHand) // .with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, this, cx| { // .on_click(MouseButton::Left, |_, this, cx| {
this.respond(true, cx); // this.respond(true, cx);
}) // })
.flex(1., true), // .flex(1., true),
) // )
.with_child( // .with_child(
MouseEventHandler::new::<Decline, _>(0, cx, |_, _| { // MouseEventHandler::new::<Decline, _>(0, cx, |_, _| {
let theme = &theme.incoming_call_notification; // let theme = &theme.incoming_call_notification;
Label::new("Decline", theme.decline_button.text.clone()) // Label::new("Decline", theme.decline_button.text.clone())
.aligned() // .aligned()
.contained() // .contained()
.with_style(theme.decline_button.container) // .with_style(theme.decline_button.container)
}) // })
.with_cursor_style(CursorStyle::PointingHand) // .with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, this, cx| { // .on_click(MouseButton::Left, |_, this, cx| {
this.respond(false, cx); // this.respond(false, cx);
}) // })
.flex(1., true), // .flex(1., true),
) // )
.constrained() // .constrained()
.with_width(theme.incoming_call_notification.button_width) // .with_width(theme.incoming_call_notification.button_width)
.into_any() // .into_any()
} }
} }
impl Render for IncomingCallNotification {
impl Entity for IncomingCallNotification { type Element = Div;
type Event = (); fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
} div().bg(red()).flex_none().child(self.render_caller(cx))
// Flex::row()
impl View for IncomingCallNotification { // .with_child()
fn ui_name() -> &'static str { // .with_child(self.render_buttons(cx))
"IncomingCallNotification" // .contained()
} // .with_background_color(background)
// .expanded()
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> { // .into_any()
let background = theme::current(cx).incoming_call_notification.background;
Flex::row()
.with_child(self.render_caller(cx))
.with_child(self.render_buttons(cx))
.contained()
.with_background_color(background)
.expanded()
.into_any()
} }
} }

View File

@ -1,8 +1,9 @@
use collections::{CommandPaletteFilter, HashMap}; use collections::{CommandPaletteFilter, HashMap};
use fuzzy::{StringMatch, StringMatchCandidate}; use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{ use gpui::{
actions, div, prelude::*, Action, AppContext, Div, EventEmitter, FocusHandle, FocusableView, actions, div, prelude::*, Action, AppContext, DismissEvent, Div, EventEmitter, FocusHandle,
Keystroke, Manager, ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView, FocusableView, Keystroke, ParentElement, Render, Styled, View, ViewContext, VisualContext,
WeakView,
}; };
use picker::{Picker, PickerDelegate}; use picker::{Picker, PickerDelegate};
use std::{ use std::{
@ -68,7 +69,7 @@ impl CommandPalette {
} }
} }
impl EventEmitter<Manager> for CommandPalette {} impl EventEmitter<DismissEvent> for CommandPalette {}
impl FocusableView for CommandPalette { impl FocusableView for CommandPalette {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle { fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
@ -268,7 +269,7 @@ impl PickerDelegate for CommandPaletteDelegate {
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) { fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
self.command_palette self.command_palette
.update(cx, |_, cx| cx.emit(Manager::Dismiss)) .update(cx, |_, cx| cx.emit(DismissEvent::Dismiss))
.log_err(); .log_err();
} }

View File

@ -14,8 +14,8 @@ use editor::{
use futures::future::try_join_all; use futures::future::try_join_all;
use gpui::{ use gpui::{
actions, div, AnyElement, AnyView, AppContext, Context, Div, EventEmitter, FocusEvent, actions, div, AnyElement, AnyView, AppContext, Context, Div, EventEmitter, FocusEvent,
FocusHandle, Focusable, FocusableElement, FocusableView, InteractiveElement, Model, FocusHandle, Focusable, FocusableElement, FocusableView, InteractiveElement, IntoElement,
ParentElement, Render, RenderOnce, SharedString, Styled, Subscription, Task, View, ViewContext, Model, ParentElement, Render, SharedString, Styled, Subscription, Task, View, ViewContext,
VisualContext, WeakView, WindowContext, VisualContext, WeakView, WindowContext,
}; };
use language::{ use language::{
@ -36,7 +36,7 @@ use std::{
}; };
use theme::ActiveTheme; use theme::ActiveTheme;
pub use toolbar_controls::ToolbarControls; pub use toolbar_controls::ToolbarControls;
use ui::{h_stack, HighlightedLabel, Icon, IconElement, Label, TextColor}; use ui::{h_stack, Color, HighlightedLabel, Icon, IconElement, Label};
use util::TryFutureExt; use util::TryFutureExt;
use workspace::{ use workspace::{
item::{BreadcrumbText, Item, ItemEvent, ItemHandle}, item::{BreadcrumbText, Item, ItemEvent, ItemHandle},
@ -778,28 +778,28 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
.bg(gpui::red()) .bg(gpui::red())
.map(|stack| { .map(|stack| {
let icon = if diagnostic.severity == DiagnosticSeverity::ERROR { let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
IconElement::new(Icon::XCircle).color(TextColor::Error) IconElement::new(Icon::XCircle).color(Color::Error)
} else { } else {
IconElement::new(Icon::ExclamationTriangle).color(TextColor::Warning) IconElement::new(Icon::ExclamationTriangle).color(Color::Warning)
}; };
stack.child(div().pl_8().child(icon)) stack.child(div().pl_8().child(icon))
}) })
.when_some(diagnostic.source.as_ref(), |stack, source| { .when_some(diagnostic.source.as_ref(), |stack, source| {
stack.child(Label::new(format!("{source}:")).color(TextColor::Accent)) stack.child(Label::new(format!("{source}:")).color(Color::Accent))
}) })
.child(HighlightedLabel::new(message.clone(), highlights.clone())) .child(HighlightedLabel::new(message.clone(), highlights.clone()))
.when_some(diagnostic.code.as_ref(), |stack, code| { .when_some(diagnostic.code.as_ref(), |stack, code| {
stack.child(Label::new(code.clone())) stack.child(Label::new(code.clone()))
}) })
.render_into_any() .into_any_element()
}) })
} }
pub(crate) fn render_summary(summary: &DiagnosticSummary) -> AnyElement { pub(crate) fn render_summary(summary: &DiagnosticSummary) -> AnyElement {
if summary.error_count == 0 && summary.warning_count == 0 { if summary.error_count == 0 && summary.warning_count == 0 {
let label = Label::new("No problems"); let label = Label::new("No problems");
label.render_into_any() label.into_any_element()
} else { } else {
h_stack() h_stack()
.bg(gpui::red()) .bg(gpui::red())
@ -807,7 +807,7 @@ pub(crate) fn render_summary(summary: &DiagnosticSummary) -> AnyElement {
.child(Label::new(summary.error_count.to_string())) .child(Label::new(summary.error_count.to_string()))
.child(IconElement::new(Icon::ExclamationTriangle)) .child(IconElement::new(Icon::ExclamationTriangle))
.child(Label::new(summary.warning_count.to_string())) .child(Label::new(summary.warning_count.to_string()))
.render_into_any() .into_any_element()
} }
} }
@ -1550,7 +1550,7 @@ mod tests {
block_id: ix, block_id: ix,
editor_style: &editor::EditorStyle::default(), editor_style: &editor::EditorStyle::default(),
}) })
.element_id()? .inner_id()?
.try_into() .try_into()
.ok()?, .ok()?,

View File

@ -7,7 +7,7 @@ use gpui::{
use language::Diagnostic; use language::Diagnostic;
use lsp::LanguageServerId; use lsp::LanguageServerId;
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::{h_stack, Icon, IconElement, Label, TextColor, Tooltip}; use ui::{h_stack, Color, Icon, IconElement, Label, Tooltip};
use workspace::{item::ItemHandle, StatusItemView, ToolbarItemEvent, Workspace}; use workspace::{item::ItemHandle, StatusItemView, ToolbarItemEvent, Workspace};
use crate::ProjectDiagnosticsEditor; use crate::ProjectDiagnosticsEditor;
@ -26,25 +26,25 @@ impl Render for DiagnosticIndicator {
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element { fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
let diagnostic_indicator = match (self.summary.error_count, self.summary.warning_count) { let diagnostic_indicator = match (self.summary.error_count, self.summary.warning_count) {
(0, 0) => h_stack().child(IconElement::new(Icon::Check).color(TextColor::Success)), (0, 0) => h_stack().child(IconElement::new(Icon::Check).color(Color::Success)),
(0, warning_count) => h_stack() (0, warning_count) => h_stack()
.gap_1() .gap_1()
.child(IconElement::new(Icon::ExclamationTriangle).color(TextColor::Warning)) .child(IconElement::new(Icon::ExclamationTriangle).color(Color::Warning))
.child(Label::new(warning_count.to_string())), .child(Label::new(warning_count.to_string())),
(error_count, 0) => h_stack() (error_count, 0) => h_stack()
.gap_1() .gap_1()
.child(IconElement::new(Icon::XCircle).color(TextColor::Error)) .child(IconElement::new(Icon::XCircle).color(Color::Error))
.child(Label::new(error_count.to_string())), .child(Label::new(error_count.to_string())),
(error_count, warning_count) => h_stack() (error_count, warning_count) => h_stack()
.gap_1() .gap_1()
.child(IconElement::new(Icon::XCircle).color(TextColor::Error)) .child(IconElement::new(Icon::XCircle).color(Color::Error))
.child(Label::new(error_count.to_string())) .child(Label::new(error_count.to_string()))
.child(IconElement::new(Icon::ExclamationTriangle).color(TextColor::Warning)) .child(IconElement::new(Icon::ExclamationTriangle).color(Color::Warning))
.child(Label::new(warning_count.to_string())), .child(Label::new(warning_count.to_string())),
}; };
h_stack() h_stack()
.id(cx.entity_id()) .id("diagnostic-indicator")
.on_action(cx.listener(Self::go_to_next_diagnostic)) .on_action(cx.listener(Self::go_to_next_diagnostic))
.rounded_md() .rounded_md()
.flex_none() .flex_none()

View File

@ -1001,17 +1001,18 @@ impl CompletionsMenu {
fn pre_resolve_completion_documentation( fn pre_resolve_completion_documentation(
&self, &self,
project: Option<ModelHandle<Project>>, editor: &Editor,
cx: &mut ViewContext<Editor>, cx: &mut ViewContext<Editor>,
) { ) -> Option<Task<()>> {
let settings = settings::get::<EditorSettings>(cx); let settings = settings::get::<EditorSettings>(cx);
if !settings.show_completion_documentation { if !settings.show_completion_documentation {
return; return None;
} }
let Some(project) = project else { let Some(project) = editor.project.clone() else {
return; return None;
}; };
let client = project.read(cx).client(); let client = project.read(cx).client();
let language_registry = project.read(cx).languages().clone(); let language_registry = project.read(cx).languages().clone();
@ -1021,7 +1022,7 @@ impl CompletionsMenu {
let completions = self.completions.clone(); let completions = self.completions.clone();
let completion_indices: Vec<_> = self.matches.iter().map(|m| m.candidate_id).collect(); let completion_indices: Vec<_> = self.matches.iter().map(|m| m.candidate_id).collect();
cx.spawn(move |this, mut cx| async move { Some(cx.spawn(move |this, mut cx| async move {
if is_remote { if is_remote {
let Some(project_id) = project_id else { let Some(project_id) = project_id else {
log::error!("Remote project without remote_id"); log::error!("Remote project without remote_id");
@ -1083,8 +1084,7 @@ impl CompletionsMenu {
_ = this.update(&mut cx, |_, cx| cx.notify()); _ = this.update(&mut cx, |_, cx| cx.notify());
} }
} }
}) }))
.detach();
} }
fn attempt_resolve_selected_completion_documentation( fn attempt_resolve_selected_completion_documentation(
@ -3423,7 +3423,7 @@ impl Editor {
to_insert, to_insert,
}) = self.inlay_hint_cache.spawn_hint_refresh( }) = self.inlay_hint_cache.spawn_hint_refresh(
reason_description, reason_description,
self.excerpt_visible_offsets(required_languages.as_ref(), cx), self.excerpts_for_inlay_hints_query(required_languages.as_ref(), cx),
invalidate_cache, invalidate_cache,
cx, cx,
) { ) {
@ -3442,11 +3442,15 @@ impl Editor {
.collect() .collect()
} }
pub fn excerpt_visible_offsets( pub fn excerpts_for_inlay_hints_query(
&self, &self,
restrict_to_languages: Option<&HashSet<Arc<Language>>>, restrict_to_languages: Option<&HashSet<Arc<Language>>>,
cx: &mut ViewContext<'_, '_, Editor>, cx: &mut ViewContext<'_, '_, Editor>,
) -> HashMap<ExcerptId, (ModelHandle<Buffer>, Global, Range<usize>)> { ) -> HashMap<ExcerptId, (ModelHandle<Buffer>, Global, Range<usize>)> {
let Some(project) = self.project.as_ref() else {
return HashMap::default();
};
let project = project.read(cx);
let multi_buffer = self.buffer().read(cx); let multi_buffer = self.buffer().read(cx);
let multi_buffer_snapshot = multi_buffer.snapshot(cx); let multi_buffer_snapshot = multi_buffer.snapshot(cx);
let multi_buffer_visible_start = self let multi_buffer_visible_start = self
@ -3466,6 +3470,14 @@ impl Editor {
.filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty()) .filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty())
.filter_map(|(buffer_handle, excerpt_visible_range, excerpt_id)| { .filter_map(|(buffer_handle, excerpt_visible_range, excerpt_id)| {
let buffer = buffer_handle.read(cx); let buffer = buffer_handle.read(cx);
let buffer_file = project::worktree::File::from_dyn(buffer.file())?;
let buffer_worktree = project.worktree_for_id(buffer_file.worktree_id(cx), cx)?;
let worktree_entry = buffer_worktree
.read(cx)
.entry_for_id(buffer_file.project_entry_id(cx)?)?;
if worktree_entry.is_ignored {
return None;
}
let language = buffer.language()?; let language = buffer.language()?;
if let Some(restrict_to_languages) = restrict_to_languages { if let Some(restrict_to_languages) = restrict_to_languages {
if !restrict_to_languages.contains(language) { if !restrict_to_languages.contains(language) {
@ -3580,7 +3592,8 @@ impl Editor {
let id = post_inc(&mut self.next_completion_id); let id = post_inc(&mut self.next_completion_id);
let task = cx.spawn(|this, mut cx| { let task = cx.spawn(|this, mut cx| {
async move { async move {
let menu = if let Some(completions) = completions.await.log_err() { let completions = completions.await.log_err();
let (menu, pre_resolve_task) = if let Some(completions) = completions {
let mut menu = CompletionsMenu { let mut menu = CompletionsMenu {
id, id,
initial_position: position, initial_position: position,
@ -3601,21 +3614,26 @@ impl Editor {
selected_item: 0, selected_item: 0,
list: Default::default(), list: Default::default(),
}; };
menu.filter(query.as_deref(), cx.background()).await; menu.filter(query.as_deref(), cx.background()).await;
if menu.matches.is_empty() { if menu.matches.is_empty() {
None (None, None)
} else { } else {
_ = this.update(&mut cx, |editor, cx| { let pre_resolve_task = this
menu.pre_resolve_completion_documentation(editor.project.clone(), cx); .update(&mut cx, |editor, cx| {
}); menu.pre_resolve_completion_documentation(editor, cx)
Some(menu) })
.ok()
.flatten();
(Some(menu), pre_resolve_task)
} }
} else { } else {
None (None, None)
}; };
this.update(&mut cx, |this, cx| { this.update(&mut cx, |this, cx| {
this.completion_tasks.retain(|(task_id, _)| *task_id > id); this.completion_tasks.retain(|(task_id, _)| *task_id >= id);
let mut context_menu = this.context_menu.write(); let mut context_menu = this.context_menu.write();
match context_menu.as_ref() { match context_menu.as_ref() {
@ -3636,10 +3654,10 @@ impl Editor {
drop(context_menu); drop(context_menu);
this.discard_copilot_suggestion(cx); this.discard_copilot_suggestion(cx);
cx.notify(); cx.notify();
} else if this.completion_tasks.is_empty() { } else if this.completion_tasks.len() <= 1 {
// If there are no more completion tasks and the last menu was // If there are no more completion tasks (omitting ourself) and
// empty, we should hide it. If it was already hidden, we should // the last menu was empty, we should hide it. If it was already
// also show the copilot suggestion when available. // hidden, we should also show the copilot suggestion when available.
drop(context_menu); drop(context_menu);
if this.hide_context_menu(cx).is_none() { if this.hide_context_menu(cx).is_none() {
this.update_visible_copilot_suggestion(cx); this.update_visible_copilot_suggestion(cx);
@ -3647,10 +3665,15 @@ impl Editor {
} }
})?; })?;
if let Some(pre_resolve_task) = pre_resolve_task {
pre_resolve_task.await;
}
Ok::<_, anyhow::Error>(()) Ok::<_, anyhow::Error>(())
} }
.log_err() .log_err()
}); });
self.completion_tasks.push((id, task)); self.completion_tasks.push((id, task));
} }

View File

@ -861,7 +861,7 @@ async fn fetch_and_update_hints(
let inlay_hints_fetch_task = editor let inlay_hints_fetch_task = editor
.update(&mut cx, |editor, cx| { .update(&mut cx, |editor, cx| {
if got_throttled { if got_throttled {
let query_not_around_visible_range = match editor.excerpt_visible_offsets(None, cx).remove(&query.excerpt_id) { let query_not_around_visible_range = match editor.excerpts_for_inlay_hints_query(None, cx).remove(&query.excerpt_id) {
Some((_, _, current_visible_range)) => { Some((_, _, current_visible_range)) => {
let visible_offset_length = current_visible_range.len(); let visible_offset_length = current_visible_range.len();
let double_visible_range = current_visible_range let double_visible_range = current_visible_range
@ -2237,7 +2237,9 @@ pub mod tests {
editor: &ViewHandle<Editor>, editor: &ViewHandle<Editor>,
cx: &mut gpui::TestAppContext, cx: &mut gpui::TestAppContext,
) -> Range<Point> { ) -> Range<Point> {
let ranges = editor.update(cx, |editor, cx| editor.excerpt_visible_offsets(None, cx)); let ranges = editor.update(cx, |editor, cx| {
editor.excerpts_for_inlay_hints_query(None, cx)
});
assert_eq!( assert_eq!(
ranges.len(), ranges.len(),
1, 1,

File diff suppressed because it is too large Load Diff

View File

@ -6740,75 +6740,6 @@ async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) {
// ); // );
// } // }
#[test]
fn test_combine_syntax_and_fuzzy_match_highlights() {
let string = "abcdefghijklmnop";
let syntax_ranges = [
(
0..3,
HighlightStyle {
color: Some(Hsla::red()),
..Default::default()
},
),
(
4..8,
HighlightStyle {
color: Some(Hsla::green()),
..Default::default()
},
),
];
let match_indices = [4, 6, 7, 8];
assert_eq!(
combine_syntax_and_fuzzy_match_highlights(
string,
Default::default(),
syntax_ranges.into_iter(),
&match_indices,
),
&[
(
0..3,
HighlightStyle {
color: Some(Hsla::red()),
..Default::default()
},
),
(
4..5,
HighlightStyle {
color: Some(Hsla::green()),
font_weight: Some(gpui::FontWeight::BOLD),
..Default::default()
},
),
(
5..6,
HighlightStyle {
color: Some(Hsla::green()),
..Default::default()
},
),
(
6..8,
HighlightStyle {
color: Some(Hsla::green()),
font_weight: Some(gpui::FontWeight::BOLD),
..Default::default()
},
),
(
8..9,
HighlightStyle {
font_weight: Some(gpui::FontWeight::BOLD),
..Default::default()
},
),
]
);
}
#[gpui::test] #[gpui::test]
async fn go_to_prev_overlapping_diagnostic( async fn go_to_prev_overlapping_diagnostic(
executor: BackgroundExecutor, executor: BackgroundExecutor,

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -861,7 +861,7 @@ async fn fetch_and_update_hints(
let inlay_hints_fetch_task = editor let inlay_hints_fetch_task = editor
.update(&mut cx, |editor, cx| { .update(&mut cx, |editor, cx| {
if got_throttled { if got_throttled {
let query_not_around_visible_range = match editor.excerpt_visible_offsets(None, cx).remove(&query.excerpt_id) { let query_not_around_visible_range = match editor.excerpts_for_inlay_hints_query(None, cx).remove(&query.excerpt_id) {
Some((_, _, current_visible_range)) => { Some((_, _, current_visible_range)) => {
let visible_offset_length = current_visible_range.len(); let visible_offset_length = current_visible_range.len();
let double_visible_range = current_visible_range let double_visible_range = current_visible_range
@ -2201,7 +2201,9 @@ pub mod tests {
cx: &mut gpui::TestAppContext, cx: &mut gpui::TestAppContext,
) -> Range<Point> { ) -> Range<Point> {
let ranges = editor let ranges = editor
.update(cx, |editor, cx| editor.excerpt_visible_offsets(None, cx)) .update(cx, |editor, cx| {
editor.excerpts_for_inlay_hints_query(None, cx)
})
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
ranges.len(), ranges.len(),

View File

@ -30,7 +30,7 @@ use std::{
}; };
use text::Selection; use text::Selection;
use theme::{ActiveTheme, Theme}; use theme::{ActiveTheme, Theme};
use ui::{Label, TextColor}; use ui::{Color, Label};
use util::{paths::PathExt, ResultExt, TryFutureExt}; use util::{paths::PathExt, ResultExt, TryFutureExt};
use workspace::item::{BreadcrumbText, FollowEvent, FollowableEvents, FollowableItemHandle}; use workspace::item::{BreadcrumbText, FollowEvent, FollowableEvents, FollowableItemHandle};
use workspace::{ use workspace::{
@ -604,7 +604,7 @@ impl Item for Editor {
&description, &description,
MAX_TAB_TITLE_LEN, MAX_TAB_TITLE_LEN,
)) ))
.color(TextColor::Muted), .color(Color::Muted),
), ),
) )
})), })),

View File

@ -2,8 +2,8 @@ use collections::HashMap;
use editor::{scroll::autoscroll::Autoscroll, Bias, Editor}; use editor::{scroll::autoscroll::Autoscroll, Bias, Editor};
use fuzzy::{CharBag, PathMatch, PathMatchCandidate}; use fuzzy::{CharBag, PathMatch, PathMatchCandidate};
use gpui::{ use gpui::{
actions, div, AppContext, Div, EventEmitter, FocusHandle, FocusableView, InteractiveElement, actions, div, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView,
Manager, Model, ParentElement, Render, RenderOnce, Styled, Task, View, ViewContext, InteractiveElement, IntoElement, Model, ParentElement, Render, Styled, Task, View, ViewContext,
VisualContext, WeakView, VisualContext, WeakView,
}; };
use picker::{Picker, PickerDelegate}; use picker::{Picker, PickerDelegate};
@ -111,7 +111,7 @@ impl FileFinder {
} }
} }
impl EventEmitter<Manager> for FileFinder {} impl EventEmitter<DismissEvent> for FileFinder {}
impl FocusableView for FileFinder { impl FocusableView for FileFinder {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle { fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.picker.focus_handle(cx) self.picker.focus_handle(cx)
@ -690,7 +690,7 @@ impl PickerDelegate for FileFinderDelegate {
} }
} }
finder finder
.update(&mut cx, |_, cx| cx.emit(Manager::Dismiss)) .update(&mut cx, |_, cx| cx.emit(DismissEvent::Dismiss))
.ok()?; .ok()?;
Some(()) Some(())
@ -702,7 +702,7 @@ impl PickerDelegate for FileFinderDelegate {
fn dismissed(&mut self, cx: &mut ViewContext<Picker<FileFinderDelegate>>) { fn dismissed(&mut self, cx: &mut ViewContext<Picker<FileFinderDelegate>>) {
self.file_finder self.file_finder
.update(cx, |_, cx| cx.emit(Manager::Dismiss)) .update(cx, |_, cx| cx.emit(DismissEvent::Dismiss))
.log_err(); .log_err();
} }

View File

@ -6,6 +6,8 @@ use gpui::BackgroundExecutor;
use std::{ use std::{
borrow::Cow, borrow::Cow,
cmp::{self, Ordering}, cmp::{self, Ordering},
iter,
ops::Range,
sync::atomic::AtomicBool, sync::atomic::AtomicBool,
}; };
@ -54,6 +56,32 @@ pub struct StringMatch {
pub string: String, pub string: String,
} }
impl StringMatch {
pub fn ranges<'a>(&'a self) -> impl 'a + Iterator<Item = Range<usize>> {
let mut positions = self.positions.iter().peekable();
iter::from_fn(move || {
while let Some(start) = positions.next().copied() {
let mut end = start + self.char_len_at_index(start);
while let Some(next_start) = positions.peek() {
if end == **next_start {
end += self.char_len_at_index(end);
positions.next();
} else {
break;
}
}
return Some(start..end);
}
None
})
}
fn char_len_at_index(&self, ix: usize) -> usize {
self.string[ix..].chars().next().unwrap().len_utf8()
}
}
impl PartialEq for StringMatch { impl PartialEq for StringMatch {
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
self.cmp(other).is_eq() self.cmp(other).is_eq()

View File

@ -1,13 +1,13 @@
use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Editor}; use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Editor};
use gpui::{ use gpui::{
actions, div, prelude::*, AppContext, Div, EventEmitter, FocusHandle, FocusableView, Manager, actions, div, prelude::*, AppContext, DismissEvent, Div, EventEmitter, FocusHandle,
Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext, WindowContext, FocusableView, Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext,
WindowContext,
}; };
use text::{Bias, Point}; use text::{Bias, Point};
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::{h_stack, v_stack, Label, StyledExt, TextColor}; use ui::{h_stack, v_stack, Color, Label, StyledExt};
use util::paths::FILE_ROW_COLUMN_DELIMITER; use util::paths::FILE_ROW_COLUMN_DELIMITER;
use workspace::Workspace;
actions!(Toggle); actions!(Toggle);
@ -25,22 +25,24 @@ pub struct GoToLine {
impl FocusableView for GoToLine { impl FocusableView for GoToLine {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle { fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.active_editor.focus_handle(cx) self.line_editor.focus_handle(cx)
} }
} }
impl EventEmitter<Manager> for GoToLine {} impl EventEmitter<DismissEvent> for GoToLine {}
impl GoToLine { impl GoToLine {
fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) { fn register(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
workspace.register_action(|workspace, _: &Toggle, cx| { let handle = cx.view().downgrade();
let Some(editor) = workspace editor.register_action(move |_: &Toggle, cx| {
.active_item(cx) let Some(editor) = handle.upgrade() else {
.and_then(|active_item| active_item.downcast::<Editor>())
else {
return; return;
}; };
let Some(workspace) = editor.read(cx).workspace() else {
workspace.toggle_modal(cx, move |cx| GoToLine::new(editor, cx)); return;
};
workspace.update(cx, |workspace, cx| {
workspace.toggle_modal(cx, move |cx| GoToLine::new(editor, cx));
})
}); });
} }
@ -88,7 +90,7 @@ impl GoToLine {
) { ) {
match event { match event {
// todo!() this isn't working... // todo!() this isn't working...
editor::EditorEvent::Blurred => cx.emit(Manager::Dismiss), editor::EditorEvent::Blurred => cx.emit(DismissEvent::Dismiss),
editor::EditorEvent::BufferEdited { .. } => self.highlight_current_line(cx), editor::EditorEvent::BufferEdited { .. } => self.highlight_current_line(cx),
_ => {} _ => {}
} }
@ -123,7 +125,7 @@ impl GoToLine {
} }
fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) { fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
cx.emit(Manager::Dismiss); cx.emit(DismissEvent::Dismiss);
} }
fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) { fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
@ -140,7 +142,7 @@ impl GoToLine {
self.prev_scroll_position.take(); self.prev_scroll_position.take();
} }
cx.emit(Manager::Dismiss); cx.emit(DismissEvent::Dismiss);
} }
} }
@ -176,7 +178,7 @@ impl Render for GoToLine {
.justify_between() .justify_between()
.px_2() .px_2()
.py_1() .py_1()
.child(Label::new(self.current_text.clone()).color(TextColor::Muted)), .child(Label::new(self.current_text.clone()).color(Color::Muted)),
), ),
) )
} }

View File

@ -10,6 +10,7 @@ pub use entity_map::*;
pub use model_context::*; pub use model_context::*;
use refineable::Refineable; use refineable::Refineable;
use smallvec::SmallVec; use smallvec::SmallVec;
use smol::future::FutureExt;
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
pub use test_context::*; pub use test_context::*;
@ -579,7 +580,7 @@ impl AppContext {
.windows .windows
.iter() .iter()
.filter_map(|(_, window)| { .filter_map(|(_, window)| {
let window = window.as_ref().unwrap(); let window = window.as_ref()?;
if window.dirty { if window.dirty {
Some(window.handle.clone()) Some(window.handle.clone())
} else { } else {
@ -983,6 +984,22 @@ impl AppContext {
pub fn all_action_names(&self) -> &[SharedString] { pub fn all_action_names(&self) -> &[SharedString] {
self.actions.all_action_names() self.actions.all_action_names()
} }
pub fn on_app_quit<Fut>(
&mut self,
mut on_quit: impl FnMut(&mut AppContext) -> Fut + 'static,
) -> Subscription
where
Fut: 'static + Future<Output = ()>,
{
self.quit_observers.insert(
(),
Box::new(move |cx| {
let future = on_quit(cx);
async move { future.await }.boxed_local()
}),
)
}
} }
impl Context for AppContext { impl Context for AppContext {
@ -1032,7 +1049,9 @@ impl Context for AppContext {
let root_view = window.root_view.clone().unwrap(); let root_view = window.root_view.clone().unwrap();
let result = update(root_view, &mut WindowContext::new(cx, &mut window)); let result = update(root_view, &mut WindowContext::new(cx, &mut window));
if !window.removed { if window.removed {
cx.windows.remove(handle.id);
} else {
cx.windows cx.windows
.get_mut(handle.id) .get_mut(handle.id)
.ok_or_else(|| anyhow!("window not found"))? .ok_or_else(|| anyhow!("window not found"))?

View File

@ -1,7 +1,7 @@
use crate::{ use crate::{
AnyView, AnyWindowHandle, AppCell, AppContext, BackgroundExecutor, Context, FocusableView, AnyView, AnyWindowHandle, AppCell, AppContext, BackgroundExecutor, Context, DismissEvent,
ForegroundExecutor, Manager, Model, ModelContext, Render, Result, Task, View, ViewContext, FocusableView, ForegroundExecutor, Model, ModelContext, Render, Result, Task, View,
VisualContext, WindowContext, WindowHandle, ViewContext, VisualContext, WindowContext, WindowHandle,
}; };
use anyhow::{anyhow, Context as _}; use anyhow::{anyhow, Context as _};
use derive_more::{Deref, DerefMut}; use derive_more::{Deref, DerefMut};
@ -326,7 +326,7 @@ impl VisualContext for AsyncWindowContext {
V: crate::ManagedView, V: crate::ManagedView,
{ {
self.window.update(self, |_, cx| { self.window.update(self, |_, cx| {
view.update(cx, |_, cx| cx.emit(Manager::Dismiss)) view.update(cx, |_, cx| cx.emit(DismissEvent::Dismiss))
}) })
} }
} }

View File

@ -611,7 +611,7 @@ impl<'a> VisualContext for VisualTestContext<'a> {
{ {
self.window self.window
.update(self.cx, |_, cx| { .update(self.cx, |_, cx| {
view.update(cx, |_, cx| cx.emit(crate::Manager::Dismiss)) view.update(cx, |_, cx| cx.emit(crate::DismissEvent::Dismiss))
}) })
.unwrap() .unwrap()
} }

View File

@ -12,15 +12,15 @@ pub trait Render: 'static + Sized {
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element; fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element;
} }
pub trait RenderOnce: Sized { pub trait IntoElement: Sized {
type Element: Element + 'static; type Element: Element + 'static;
fn element_id(&self) -> Option<ElementId>; fn element_id(&self) -> Option<ElementId>;
fn render_once(self) -> Self::Element; fn into_element(self) -> Self::Element;
fn render_into_any(self) -> AnyElement { fn into_any_element(self) -> AnyElement {
self.render_once().into_any() self.into_element().into_any()
} }
fn draw<T, R>( fn draw<T, R>(
@ -33,7 +33,7 @@ pub trait RenderOnce: Sized {
where where
T: Clone + Default + Debug + Into<AvailableSpace>, T: Clone + Default + Debug + Into<AvailableSpace>,
{ {
let element = self.render_once(); let element = self.into_element();
let element_id = element.element_id(); let element_id = element.element_id();
let element = DrawableElement { let element = DrawableElement {
element: Some(element), element: Some(element),
@ -57,7 +57,7 @@ pub trait RenderOnce: Sized {
fn map<U>(self, f: impl FnOnce(Self) -> U) -> U fn map<U>(self, f: impl FnOnce(Self) -> U) -> U
where where
Self: Sized, Self: Sized,
U: RenderOnce, U: IntoElement,
{ {
f(self) f(self)
} }
@ -83,7 +83,7 @@ pub trait RenderOnce: Sized {
} }
} }
pub trait Element: 'static + RenderOnce { pub trait Element: 'static + IntoElement {
type State: 'static; type State: 'static;
fn layout( fn layout(
@ -99,30 +99,30 @@ pub trait Element: 'static + RenderOnce {
} }
} }
pub trait Component: 'static { pub trait RenderOnce: 'static {
type Rendered: RenderOnce; type Rendered: IntoElement;
fn render(self, cx: &mut WindowContext) -> Self::Rendered; fn render(self, cx: &mut WindowContext) -> Self::Rendered;
} }
pub struct CompositeElement<C> { pub struct Component<C> {
component: Option<C>, component: Option<C>,
} }
pub struct CompositeElementState<C: Component> { pub struct CompositeElementState<C: RenderOnce> {
rendered_element: Option<<C::Rendered as RenderOnce>::Element>, rendered_element: Option<<C::Rendered as IntoElement>::Element>,
rendered_element_state: <<C::Rendered as RenderOnce>::Element as Element>::State, rendered_element_state: <<C::Rendered as IntoElement>::Element as Element>::State,
} }
impl<C> CompositeElement<C> { impl<C> Component<C> {
pub fn new(component: C) -> Self { pub fn new(component: C) -> Self {
CompositeElement { Component {
component: Some(component), component: Some(component),
} }
} }
} }
impl<C: Component> Element for CompositeElement<C> { impl<C: RenderOnce> Element for Component<C> {
type State = CompositeElementState<C>; type State = CompositeElementState<C>;
fn layout( fn layout(
@ -130,7 +130,7 @@ impl<C: Component> Element for CompositeElement<C> {
state: Option<Self::State>, state: Option<Self::State>,
cx: &mut WindowContext, cx: &mut WindowContext,
) -> (LayoutId, Self::State) { ) -> (LayoutId, Self::State) {
let mut element = self.component.take().unwrap().render(cx).render_once(); let mut element = self.component.take().unwrap().render(cx).into_element();
let (layout_id, state) = element.layout(state.map(|s| s.rendered_element_state), cx); let (layout_id, state) = element.layout(state.map(|s| s.rendered_element_state), cx);
let state = CompositeElementState { let state = CompositeElementState {
rendered_element: Some(element), rendered_element: Some(element),
@ -148,14 +148,14 @@ impl<C: Component> Element for CompositeElement<C> {
} }
} }
impl<C: Component> RenderOnce for CompositeElement<C> { impl<C: RenderOnce> IntoElement for Component<C> {
type Element = Self; type Element = Self;
fn element_id(&self) -> Option<ElementId> { fn element_id(&self) -> Option<ElementId> {
None None
} }
fn render_once(self) -> Self::Element { fn into_element(self) -> Self::Element {
self self
} }
} }
@ -166,23 +166,20 @@ pub struct GlobalElementId(SmallVec<[ElementId; 32]>);
pub trait ParentElement { pub trait ParentElement {
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]>; fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]>;
fn child(mut self, child: impl RenderOnce) -> Self fn child(mut self, child: impl IntoElement) -> Self
where where
Self: Sized, Self: Sized,
{ {
self.children_mut().push(child.render_once().into_any()); self.children_mut().push(child.into_element().into_any());
self self
} }
fn children(mut self, children: impl IntoIterator<Item = impl RenderOnce>) -> Self fn children(mut self, children: impl IntoIterator<Item = impl IntoElement>) -> Self
where where
Self: Sized, Self: Sized,
{ {
self.children_mut().extend( self.children_mut()
children .extend(children.into_iter().map(|child| child.into_any_element()));
.into_iter()
.map(|child| child.render_once().into_any()),
);
self self
} }
} }
@ -432,10 +429,6 @@ impl AnyElement {
AnyElement(Box::new(Some(DrawableElement::new(element))) as Box<dyn ElementObject>) AnyElement(Box::new(Some(DrawableElement::new(element))) as Box<dyn ElementObject>)
} }
pub fn element_id(&self) -> Option<ElementId> {
self.0.element_id()
}
pub fn layout(&mut self, cx: &mut WindowContext) -> LayoutId { pub fn layout(&mut self, cx: &mut WindowContext) -> LayoutId {
self.0.layout(cx) self.0.layout(cx)
} }
@ -467,6 +460,10 @@ impl AnyElement {
pub fn into_any(self) -> AnyElement { pub fn into_any(self) -> AnyElement {
AnyElement::new(self) AnyElement::new(self)
} }
pub fn inner_id(&self) -> Option<ElementId> {
self.0.element_id()
}
} }
impl Element for AnyElement { impl Element for AnyElement {
@ -486,14 +483,14 @@ impl Element for AnyElement {
} }
} }
impl RenderOnce for AnyElement { impl IntoElement for AnyElement {
type Element = Self; type Element = Self;
fn element_id(&self) -> Option<ElementId> { fn element_id(&self) -> Option<ElementId> {
AnyElement::element_id(self) None
} }
fn render_once(self) -> Self::Element { fn into_element(self) -> Self::Element {
self self
} }
} }

View File

@ -1,9 +1,9 @@
use crate::{ use crate::{
point, px, Action, AnyDrag, AnyElement, AnyTooltip, AnyView, AppContext, BorrowAppContext, point, px, Action, AnyDrag, AnyElement, AnyTooltip, AnyView, AppContext, BorrowAppContext,
BorrowWindow, Bounds, ClickEvent, DispatchPhase, Element, ElementId, FocusEvent, FocusHandle, BorrowWindow, Bounds, ClickEvent, DispatchPhase, Element, ElementId, FocusEvent, FocusHandle,
KeyContext, KeyDownEvent, KeyUpEvent, LayoutId, MouseButton, MouseDownEvent, MouseMoveEvent, IntoElement, KeyContext, KeyDownEvent, KeyUpEvent, LayoutId, MouseButton, MouseDownEvent,
MouseUpEvent, ParentElement, Pixels, Point, Render, RenderOnce, ScrollWheelEvent, SharedString, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Point, Render, ScrollWheelEvent,
Size, Style, StyleRefinement, Styled, Task, View, Visibility, WindowContext, SharedString, Size, Style, StyleRefinement, Styled, Task, View, Visibility, WindowContext,
}; };
use collections::HashMap; use collections::HashMap;
use refineable::Refineable; use refineable::Refineable;
@ -666,14 +666,14 @@ impl Element for Div {
} }
} }
impl RenderOnce for Div { impl IntoElement for Div {
type Element = Self; type Element = Self;
fn element_id(&self) -> Option<ElementId> { fn element_id(&self) -> Option<ElementId> {
self.interactivity.element_id.clone() self.interactivity.element_id.clone()
} }
fn render_once(self) -> Self::Element { fn into_element(self) -> Self::Element {
self self
} }
} }
@ -1278,7 +1278,7 @@ where
} }
} }
impl<E> RenderOnce for Focusable<E> impl<E> IntoElement for Focusable<E>
where where
E: Element, E: Element,
{ {
@ -1288,7 +1288,7 @@ where
self.element.element_id() self.element.element_id()
} }
fn render_once(self) -> Self::Element { fn into_element(self) -> Self::Element {
self.element self.element
} }
} }
@ -1352,7 +1352,7 @@ where
} }
} }
impl<E> RenderOnce for Stateful<E> impl<E> IntoElement for Stateful<E>
where where
E: Element, E: Element,
{ {
@ -1362,7 +1362,7 @@ where
self.element.element_id() self.element.element_id()
} }
fn render_once(self) -> Self::Element { fn into_element(self) -> Self::Element {
self self
} }
} }

View File

@ -1,30 +1,59 @@
use std::sync::Arc;
use crate::{ use crate::{
Bounds, Element, InteractiveElement, InteractiveElementState, Interactivity, LayoutId, Pixels, Bounds, Element, ImageData, InteractiveElement, InteractiveElementState, Interactivity,
RenderOnce, SharedString, StyleRefinement, Styled, WindowContext, IntoElement, LayoutId, Pixels, SharedString, StyleRefinement, Styled, WindowContext,
}; };
use futures::FutureExt; use futures::FutureExt;
use util::ResultExt; use util::ResultExt;
#[derive(Clone, Debug)]
pub enum ImageSource {
/// Image content will be loaded from provided URI at render time.
Uri(SharedString),
Data(Arc<ImageData>),
}
impl From<SharedString> for ImageSource {
fn from(value: SharedString) -> Self {
Self::Uri(value)
}
}
impl From<Arc<ImageData>> for ImageSource {
fn from(value: Arc<ImageData>) -> Self {
Self::Data(value)
}
}
pub struct Img { pub struct Img {
interactivity: Interactivity, interactivity: Interactivity,
uri: Option<SharedString>, source: Option<ImageSource>,
grayscale: bool, grayscale: bool,
} }
pub fn img() -> Img { pub fn img() -> Img {
Img { Img {
interactivity: Interactivity::default(), interactivity: Interactivity::default(),
uri: None, source: None,
grayscale: false, grayscale: false,
} }
} }
impl Img { impl Img {
pub fn uri(mut self, uri: impl Into<SharedString>) -> Self { pub fn uri(mut self, uri: impl Into<SharedString>) -> Self {
self.uri = Some(uri.into()); self.source = Some(ImageSource::from(uri.into()));
self
}
pub fn data(mut self, data: Arc<ImageData>) -> Self {
self.source = Some(ImageSource::from(data));
self self
} }
pub fn source(mut self, source: impl Into<ImageSource>) -> Self {
self.source = Some(source.into());
self
}
pub fn grayscale(mut self, grayscale: bool) -> Self { pub fn grayscale(mut self, grayscale: bool) -> Self {
self.grayscale = grayscale; self.grayscale = grayscale;
self self
@ -58,42 +87,47 @@ impl Element for Img {
|style, _scroll_offset, cx| { |style, _scroll_offset, cx| {
let corner_radii = style.corner_radii; let corner_radii = style.corner_radii;
if let Some(uri) = self.uri.clone() { if let Some(source) = self.source {
// eprintln!(">>> image_cache.get({uri}"); let image = match source {
let image_future = cx.image_cache.get(uri.clone()); ImageSource::Uri(uri) => {
// eprintln!("<<< image_cache.get({uri}"); let image_future = cx.image_cache.get(uri.clone());
if let Some(data) = image_future if let Some(data) = image_future
.clone() .clone()
.now_or_never() .now_or_never()
.and_then(|result| result.ok()) .and_then(|result| result.ok())
{ {
let corner_radii = corner_radii.to_pixels(bounds.size, cx.rem_size()); data
cx.with_z_index(1, |cx| { } else {
cx.paint_image(bounds, corner_radii, data, self.grayscale) cx.spawn(|mut cx| async move {
.log_err() if image_future.await.ok().is_some() {
}); cx.on_next_frame(|cx| cx.notify());
} else { }
cx.spawn(|mut cx| async move { })
if image_future.await.ok().is_some() { .detach();
cx.on_next_frame(|cx| cx.notify()); return;
} }
}) }
.detach() ImageSource::Data(image) => image,
} };
let corner_radii = corner_radii.to_pixels(bounds.size, cx.rem_size());
cx.with_z_index(1, |cx| {
cx.paint_image(bounds, corner_radii, image, self.grayscale)
.log_err()
});
} }
}, },
) )
} }
} }
impl RenderOnce for Img { impl IntoElement for Img {
type Element = Self; type Element = Self;
fn element_id(&self) -> Option<crate::ElementId> { fn element_id(&self) -> Option<crate::ElementId> {
self.interactivity.element_id.clone() self.interactivity.element_id.clone()
} }
fn render_once(self) -> Self::Element { fn into_element(self) -> Self::Element {
self self
} }
} }

View File

@ -2,8 +2,8 @@ use smallvec::SmallVec;
use taffy::style::{Display, Position}; use taffy::style::{Display, Position};
use crate::{ use crate::{
point, AnyElement, BorrowWindow, Bounds, Element, LayoutId, ParentElement, Pixels, Point, point, AnyElement, BorrowWindow, Bounds, Element, IntoElement, LayoutId, ParentElement, Pixels,
RenderOnce, Size, Style, WindowContext, Point, Size, Style, WindowContext,
}; };
pub struct OverlayState { pub struct OverlayState {
@ -144,21 +144,23 @@ impl Element for Overlay {
} }
cx.with_element_offset(desired.origin - bounds.origin, |cx| { cx.with_element_offset(desired.origin - bounds.origin, |cx| {
for child in self.children { cx.break_content_mask(|cx| {
child.paint(cx); for child in self.children {
} child.paint(cx);
}
})
}) })
} }
} }
impl RenderOnce for Overlay { impl IntoElement for Overlay {
type Element = Self; type Element = Self;
fn element_id(&self) -> Option<crate::ElementId> { fn element_id(&self) -> Option<crate::ElementId> {
None None
} }
fn render_once(self) -> Self::Element { fn into_element(self) -> Self::Element {
self self
} }
} }

View File

@ -1,6 +1,6 @@
use crate::{ use crate::{
Bounds, Element, ElementId, InteractiveElement, InteractiveElementState, Interactivity, Bounds, Element, ElementId, InteractiveElement, InteractiveElementState, Interactivity,
LayoutId, Pixels, RenderOnce, SharedString, StyleRefinement, Styled, WindowContext, IntoElement, LayoutId, Pixels, SharedString, StyleRefinement, Styled, WindowContext,
}; };
use util::ResultExt; use util::ResultExt;
@ -49,14 +49,14 @@ impl Element for Svg {
} }
} }
impl RenderOnce for Svg { impl IntoElement for Svg {
type Element = Self; type Element = Self;
fn element_id(&self) -> Option<ElementId> { fn element_id(&self) -> Option<ElementId> {
self.interactivity.element_id.clone() self.interactivity.element_id.clone()
} }
fn render_once(self) -> Self::Element { fn into_element(self) -> Self::Element {
self self
} }
} }

View File

@ -1,11 +1,11 @@
use crate::{ use crate::{
Bounds, Element, ElementId, LayoutId, Pixels, RenderOnce, SharedString, Size, TextRun, Bounds, DispatchPhase, Element, ElementId, IntoElement, LayoutId, MouseDownEvent, MouseUpEvent,
WindowContext, WrappedLine, Pixels, Point, SharedString, Size, TextRun, WhiteSpace, WindowContext, WrappedLine,
}; };
use anyhow::anyhow; use anyhow::anyhow;
use parking_lot::{Mutex, MutexGuard}; use parking_lot::{Mutex, MutexGuard};
use smallvec::SmallVec; use smallvec::SmallVec;
use std::{cell::Cell, rc::Rc, sync::Arc}; use std::{cell::Cell, ops::Range, rc::Rc, sync::Arc};
use util::ResultExt; use util::ResultExt;
impl Element for &'static str { impl Element for &'static str {
@ -26,14 +26,14 @@ impl Element for &'static str {
} }
} }
impl RenderOnce for &'static str { impl IntoElement for &'static str {
type Element = Self; type Element = Self;
fn element_id(&self) -> Option<ElementId> { fn element_id(&self) -> Option<ElementId> {
None None
} }
fn render_once(self) -> Self::Element { fn into_element(self) -> Self::Element {
self self
} }
} }
@ -57,35 +57,40 @@ impl Element for SharedString {
} }
} }
impl RenderOnce for SharedString { impl IntoElement for SharedString {
type Element = Self; type Element = Self;
fn element_id(&self) -> Option<ElementId> { fn element_id(&self) -> Option<ElementId> {
None None
} }
fn render_once(self) -> Self::Element { fn into_element(self) -> Self::Element {
self self
} }
} }
/// Renders text with runs of different styles.
///
/// Callers are responsible for setting the correct style for each run.
/// For text with a uniform style, you can usually avoid calling this constructor
/// and just pass text directly.
pub struct StyledText { pub struct StyledText {
text: SharedString, text: SharedString,
runs: Option<Vec<TextRun>>, runs: Option<Vec<TextRun>>,
} }
impl StyledText { impl StyledText {
/// Renders text with runs of different styles. pub fn new(text: impl Into<SharedString>) -> Self {
///
/// Callers are responsible for setting the correct style for each run.
/// For text with a uniform style, you can usually avoid calling this constructor
/// and just pass text directly.
pub fn new(text: SharedString, runs: Vec<TextRun>) -> Self {
StyledText { StyledText {
text, text: text.into(),
runs: Some(runs), runs: None,
} }
} }
pub fn with_runs(mut self, runs: Vec<TextRun>) -> Self {
self.runs = Some(runs);
self
}
} }
impl Element for StyledText { impl Element for StyledText {
@ -106,14 +111,14 @@ impl Element for StyledText {
} }
} }
impl RenderOnce for StyledText { impl IntoElement for StyledText {
type Element = Self; type Element = Self;
fn element_id(&self) -> Option<crate::ElementId> { fn element_id(&self) -> Option<crate::ElementId> {
None None
} }
fn render_once(self) -> Self::Element { fn into_element(self) -> Self::Element {
self self
} }
} }
@ -159,10 +164,14 @@ impl TextState {
let element_state = self.clone(); let element_state = self.clone();
move |known_dimensions, available_space| { move |known_dimensions, available_space| {
let wrap_width = known_dimensions.width.or(match available_space.width { let wrap_width = if text_style.white_space == WhiteSpace::Normal {
crate::AvailableSpace::Definite(x) => Some(x), known_dimensions.width.or(match available_space.width {
_ => None, crate::AvailableSpace::Definite(x) => Some(x),
}); _ => None,
})
} else {
None
};
if let Some(text_state) = element_state.0.lock().as_ref() { if let Some(text_state) = element_state.0.lock().as_ref() {
if text_state.size.is_some() if text_state.size.is_some()
@ -174,10 +183,7 @@ impl TextState {
let Some(lines) = text_system let Some(lines) = text_system
.shape_text( .shape_text(
&text, &text, font_size, &runs, wrap_width, // Wrap if we know the width.
font_size,
&runs[..],
wrap_width, // Wrap if we know the width.
) )
.log_err() .log_err()
else { else {
@ -194,7 +200,7 @@ impl TextState {
for line in &lines { for line in &lines {
let line_size = line.size(line_height); let line_size = line.size(line_height);
size.height += line_size.height; size.height += line_size.height;
size.width = size.width.max(line_size.width); size.width = size.width.max(line_size.width).ceil();
} }
element_state.lock().replace(TextStateInner { element_state.lock().replace(TextStateInner {
@ -225,16 +231,77 @@ impl TextState {
line_origin.y += line.size(line_height).height; line_origin.y += line.size(line_height).height;
} }
} }
fn index_for_position(&self, bounds: Bounds<Pixels>, position: Point<Pixels>) -> Option<usize> {
if !bounds.contains_point(&position) {
return None;
}
let element_state = self.lock();
let element_state = element_state
.as_ref()
.expect("measurement has not been performed");
let line_height = element_state.line_height;
let mut line_origin = bounds.origin;
let mut line_start_ix = 0;
for line in &element_state.lines {
let line_bottom = line_origin.y + line.size(line_height).height;
if position.y > line_bottom {
line_origin.y = line_bottom;
line_start_ix += line.len() + 1;
} else {
let position_within_line = position - line_origin;
let index_within_line =
line.index_for_position(position_within_line, line_height)?;
return Some(line_start_ix + index_within_line);
}
}
None
}
} }
struct InteractiveText { pub struct InteractiveText {
element_id: ElementId, element_id: ElementId,
text: StyledText, text: StyledText,
click_listener: Option<Box<dyn Fn(InteractiveTextClickEvent, &mut WindowContext<'_>)>>,
} }
struct InteractiveTextState { struct InteractiveTextClickEvent {
mouse_down_index: usize,
mouse_up_index: usize,
}
pub struct InteractiveTextState {
text_state: TextState, text_state: TextState,
clicked_range_ixs: Rc<Cell<SmallVec<[usize; 1]>>>, mouse_down_index: Rc<Cell<Option<usize>>>,
}
impl InteractiveText {
pub fn new(id: impl Into<ElementId>, text: StyledText) -> Self {
Self {
element_id: id.into(),
text,
click_listener: None,
}
}
pub fn on_click(
mut self,
ranges: Vec<Range<usize>>,
listener: impl Fn(usize, &mut WindowContext<'_>) + 'static,
) -> Self {
self.click_listener = Some(Box::new(move |event, cx| {
for (range_ix, range) in ranges.iter().enumerate() {
if range.contains(&event.mouse_down_index) && range.contains(&event.mouse_up_index)
{
listener(range_ix, cx);
}
}
}));
self
}
} }
impl Element for InteractiveText { impl Element for InteractiveText {
@ -246,39 +313,74 @@ impl Element for InteractiveText {
cx: &mut WindowContext, cx: &mut WindowContext,
) -> (LayoutId, Self::State) { ) -> (LayoutId, Self::State) {
if let Some(InteractiveTextState { if let Some(InteractiveTextState {
text_state, mouse_down_index, ..
clicked_range_ixs,
}) = state }) = state
{ {
let (layout_id, text_state) = self.text.layout(Some(text_state), cx); let (layout_id, text_state) = self.text.layout(None, cx);
let element_state = InteractiveTextState { let element_state = InteractiveTextState {
text_state, text_state,
clicked_range_ixs, mouse_down_index,
}; };
(layout_id, element_state) (layout_id, element_state)
} else { } else {
let (layout_id, text_state) = self.text.layout(None, cx); let (layout_id, text_state) = self.text.layout(None, cx);
let element_state = InteractiveTextState { let element_state = InteractiveTextState {
text_state, text_state,
clicked_range_ixs: Rc::default(), mouse_down_index: Rc::default(),
}; };
(layout_id, element_state) (layout_id, element_state)
} }
} }
fn paint(self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) { fn paint(self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
if let Some(click_listener) = self.click_listener {
let text_state = state.text_state.clone();
let mouse_down = state.mouse_down_index.clone();
if let Some(mouse_down_index) = mouse_down.get() {
cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| {
if phase == DispatchPhase::Bubble {
if let Some(mouse_up_index) =
text_state.index_for_position(bounds, event.position)
{
click_listener(
InteractiveTextClickEvent {
mouse_down_index,
mouse_up_index,
},
cx,
)
}
mouse_down.take();
cx.notify();
}
});
} else {
cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| {
if phase == DispatchPhase::Bubble {
if let Some(mouse_down_index) =
text_state.index_for_position(bounds, event.position)
{
mouse_down.set(Some(mouse_down_index));
cx.notify();
}
}
});
}
}
self.text.paint(bounds, &mut state.text_state, cx) self.text.paint(bounds, &mut state.text_state, cx)
} }
} }
impl RenderOnce for InteractiveText { impl IntoElement for InteractiveText {
type Element = Self; type Element = Self;
fn element_id(&self) -> Option<ElementId> { fn element_id(&self) -> Option<ElementId> {
Some(self.element_id.clone()) Some(self.element_id.clone())
} }
fn render_once(self) -> Self::Element { fn into_element(self) -> Self::Element {
self self
} }
} }

View File

@ -1,7 +1,7 @@
use crate::{ use crate::{
point, px, size, AnyElement, AvailableSpace, Bounds, Element, ElementId, InteractiveElement, point, px, size, AnyElement, AvailableSpace, BorrowWindow, Bounds, ContentMask, Element,
InteractiveElementState, Interactivity, LayoutId, Pixels, Point, Render, RenderOnce, Size, ElementId, InteractiveElement, InteractiveElementState, Interactivity, IntoElement, LayoutId,
StyleRefinement, Styled, View, ViewContext, WindowContext, Pixels, Point, Render, Size, StyleRefinement, Styled, View, ViewContext, WindowContext,
}; };
use smallvec::SmallVec; use smallvec::SmallVec;
use std::{cell::RefCell, cmp, ops::Range, rc::Rc}; use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
@ -9,7 +9,7 @@ use taffy::style::Overflow;
/// uniform_list provides lazy rendering for a set of items that are of uniform height. /// uniform_list provides lazy rendering for a set of items that are of uniform height.
/// When rendered into a container with overflow-y: hidden and a fixed (or max) height, /// When rendered into a container with overflow-y: hidden and a fixed (or max) height,
/// uniform_list will only render the visibile subset of items. /// uniform_list will only render the visible subset of items.
pub fn uniform_list<I, R, V>( pub fn uniform_list<I, R, V>(
view: View<V>, view: View<V>,
id: I, id: I,
@ -18,30 +18,30 @@ pub fn uniform_list<I, R, V>(
) -> UniformList ) -> UniformList
where where
I: Into<ElementId>, I: Into<ElementId>,
R: RenderOnce, R: IntoElement,
V: Render, V: Render,
{ {
let id = id.into(); let id = id.into();
let mut style = StyleRefinement::default(); let mut base_style = StyleRefinement::default();
style.overflow.y = Some(Overflow::Hidden); base_style.overflow.y = Some(Overflow::Scroll);
let render_range = move |range, cx: &mut WindowContext| { let render_range = move |range, cx: &mut WindowContext| {
view.update(cx, |this, cx| { view.update(cx, |this, cx| {
f(this, range, cx) f(this, range, cx)
.into_iter() .into_iter()
.map(|component| component.render_into_any()) .map(|component| component.into_any_element())
.collect() .collect()
}) })
}; };
UniformList { UniformList {
id: id.clone(), id: id.clone(),
style,
item_count, item_count,
item_to_measure_index: 0, item_to_measure_index: 0,
render_items: Box::new(render_range), render_items: Box::new(render_range),
interactivity: Interactivity { interactivity: Interactivity {
element_id: Some(id.into()), element_id: Some(id.into()),
base_style,
..Default::default() ..Default::default()
}, },
scroll_handle: None, scroll_handle: None,
@ -50,7 +50,6 @@ where
pub struct UniformList { pub struct UniformList {
id: ElementId, id: ElementId,
style: StyleRefinement,
item_count: usize, item_count: usize,
item_to_measure_index: usize, item_to_measure_index: usize,
render_items: render_items:
@ -91,7 +90,7 @@ impl UniformListScrollHandle {
impl Styled for UniformList { impl Styled for UniformList {
fn style(&mut self) -> &mut StyleRefinement { fn style(&mut self) -> &mut StyleRefinement {
&mut self.style &mut self.interactivity.base_style
} }
} }
@ -211,31 +210,31 @@ impl Element for UniformList {
scroll_offset: shared_scroll_offset, scroll_offset: shared_scroll_offset,
}); });
} }
let visible_item_count = if item_height > px(0.) {
(padded_bounds.size.height / item_height).ceil() as usize + 1
} else {
0
};
let first_visible_element_ix = let first_visible_element_ix =
(-scroll_offset.y / item_height).floor() as usize; (-scroll_offset.y / item_height).floor() as usize;
let last_visible_element_ix =
((-scroll_offset.y + padded_bounds.size.height) / item_height).ceil()
as usize;
let visible_range = first_visible_element_ix let visible_range = first_visible_element_ix
..cmp::min( ..cmp::min(last_visible_element_ix, self.item_count);
first_visible_element_ix + visible_item_count,
self.item_count,
);
let items = (self.render_items)(visible_range.clone(), cx); let items = (self.render_items)(visible_range.clone(), cx);
cx.with_z_index(1, |cx| { cx.with_z_index(1, |cx| {
for (item, ix) in items.into_iter().zip(visible_range) { let content_mask = ContentMask {
let item_origin = padded_bounds.origin bounds: padded_bounds,
+ point(px(0.), item_height * ix + scroll_offset.y); };
let available_space = size( cx.with_content_mask(Some(content_mask), |cx| {
AvailableSpace::Definite(padded_bounds.size.width), for (item, ix) in items.into_iter().zip(visible_range) {
AvailableSpace::Definite(item_height), let item_origin = padded_bounds.origin
); + point(px(0.), item_height * ix + scroll_offset.y);
item.draw(item_origin, available_space, cx); let available_space = size(
} AvailableSpace::Definite(padded_bounds.size.width),
AvailableSpace::Definite(item_height),
);
item.draw(item_origin, available_space, cx);
}
});
}); });
} }
}) })
@ -244,14 +243,14 @@ impl Element for UniformList {
} }
} }
impl RenderOnce for UniformList { impl IntoElement for UniformList {
type Element = Self; type Element = Self;
fn element_id(&self) -> Option<crate::ElementId> { fn element_id(&self) -> Option<crate::ElementId> {
Some(self.id.clone()) Some(self.id.clone())
} }
fn render_once(self) -> Self::Element { fn into_element(self) -> Self::Element {
self self
} }
} }

View File

@ -1,6 +1,6 @@
use crate::{ use crate::{
div, point, Div, Element, FocusHandle, Keystroke, Modifiers, Pixels, Point, Render, RenderOnce, div, point, Div, Element, FocusHandle, IntoElement, Keystroke, Modifiers, Pixels, Point,
ViewContext, Render, ViewContext,
}; };
use smallvec::SmallVec; use smallvec::SmallVec;
use std::{any::Any, fmt::Debug, marker::PhantomData, ops::Deref, path::PathBuf}; use std::{any::Any, fmt::Debug, marker::PhantomData, ops::Deref, path::PathBuf};
@ -64,7 +64,7 @@ pub struct Drag<S, R, V, E>
where where
R: Fn(&mut V, &mut ViewContext<V>) -> E, R: Fn(&mut V, &mut ViewContext<V>) -> E,
V: 'static, V: 'static,
E: RenderOnce, E: IntoElement,
{ {
pub state: S, pub state: S,
pub render_drag_handle: R, pub render_drag_handle: R,
@ -286,8 +286,8 @@ pub struct FocusEvent {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::{ use crate::{
self as gpui, div, Div, FocusHandle, InteractiveElement, KeyBinding, Keystroke, self as gpui, div, Div, FocusHandle, InteractiveElement, IntoElement, KeyBinding,
ParentElement, Render, RenderOnce, Stateful, TestAppContext, VisualContext, Keystroke, ParentElement, Render, Stateful, TestAppContext, VisualContext,
}; };
struct TestView { struct TestView {
@ -315,7 +315,7 @@ mod test {
div() div()
.key_context("nested") .key_context("nested")
.track_focus(&self.focus_handle) .track_focus(&self.focus_handle)
.render_once(), .into_element(),
), ),
) )
} }

View File

@ -683,6 +683,9 @@ impl Drop for MacWindow {
this.executor this.executor
.spawn(async move { .spawn(async move {
unsafe { unsafe {
// todo!() this panic()s when you click the red close button
// unless should_close returns false.
// (luckliy in zed it always returns false)
window.close(); window.close();
} }
}) })

View File

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

View File

@ -1,9 +1,12 @@
use std::{iter, mem, ops::Range};
use crate::{ use crate::{
black, phi, point, rems, AbsoluteLength, BorrowAppContext, BorrowWindow, Bounds, ContentMask, black, phi, point, rems, AbsoluteLength, BorrowAppContext, BorrowWindow, Bounds, ContentMask,
Corners, CornersRefinement, CursorStyle, DefiniteLength, Edges, EdgesRefinement, Font, Corners, CornersRefinement, CursorStyle, DefiniteLength, Edges, EdgesRefinement, Font,
FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Rgba, FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Rgba,
SharedString, Size, SizeRefinement, Styled, TextRun, WindowContext, SharedString, Size, SizeRefinement, Styled, TextRun, WindowContext,
}; };
use collections::HashSet;
use refineable::{Cascade, Refineable}; use refineable::{Cascade, Refineable};
use smallvec::SmallVec; use smallvec::SmallVec;
pub use taffy::style::{ pub use taffy::style::{
@ -128,6 +131,13 @@ pub struct BoxShadow {
pub spread_radius: Pixels, pub spread_radius: Pixels,
} }
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub enum WhiteSpace {
#[default]
Normal,
Nowrap,
}
#[derive(Refineable, Clone, Debug)] #[derive(Refineable, Clone, Debug)]
#[refineable(Debug)] #[refineable(Debug)]
pub struct TextStyle { pub struct TextStyle {
@ -138,7 +148,9 @@ pub struct TextStyle {
pub line_height: DefiniteLength, pub line_height: DefiniteLength,
pub font_weight: FontWeight, pub font_weight: FontWeight,
pub font_style: FontStyle, pub font_style: FontStyle,
pub background_color: Option<Hsla>,
pub underline: Option<UnderlineStyle>, pub underline: Option<UnderlineStyle>,
pub white_space: WhiteSpace,
} }
impl Default for TextStyle { impl Default for TextStyle {
@ -151,13 +163,16 @@ impl Default for TextStyle {
line_height: phi(), line_height: phi(),
font_weight: FontWeight::default(), font_weight: FontWeight::default(),
font_style: FontStyle::default(), font_style: FontStyle::default(),
background_color: None,
underline: None, underline: None,
white_space: WhiteSpace::Normal,
} }
} }
} }
impl TextStyle { impl TextStyle {
pub fn highlight(mut self, style: HighlightStyle) -> Self { pub fn highlight(mut self, style: impl Into<HighlightStyle>) -> Self {
let style = style.into();
if let Some(weight) = style.font_weight { if let Some(weight) = style.font_weight {
self.font_weight = weight; self.font_weight = weight;
} }
@ -173,6 +188,10 @@ impl TextStyle {
self.color.fade_out(factor); self.color.fade_out(factor);
} }
if let Some(background_color) = style.background_color {
self.background_color = Some(background_color);
}
if let Some(underline) = style.underline { if let Some(underline) = style.underline {
self.underline = Some(underline); self.underline = Some(underline);
} }
@ -203,7 +222,7 @@ impl TextStyle {
style: self.font_style, style: self.font_style,
}, },
color: self.color, color: self.color,
background_color: None, background_color: self.background_color,
underline: self.underline.clone(), underline: self.underline.clone(),
} }
} }
@ -214,6 +233,7 @@ pub struct HighlightStyle {
pub color: Option<Hsla>, pub color: Option<Hsla>,
pub font_weight: Option<FontWeight>, pub font_weight: Option<FontWeight>,
pub font_style: Option<FontStyle>, pub font_style: Option<FontStyle>,
pub background_color: Option<Hsla>,
pub underline: Option<UnderlineStyle>, pub underline: Option<UnderlineStyle>,
pub fade_out: Option<f32>, pub fade_out: Option<f32>,
} }
@ -432,6 +452,7 @@ impl From<&TextStyle> for HighlightStyle {
color: Some(other.color), color: Some(other.color),
font_weight: Some(other.font_weight), font_weight: Some(other.font_weight),
font_style: Some(other.font_style), font_style: Some(other.font_style),
background_color: other.background_color,
underline: other.underline.clone(), underline: other.underline.clone(),
fade_out: None, fade_out: None,
} }
@ -458,6 +479,10 @@ impl HighlightStyle {
self.font_style = other.font_style; self.font_style = other.font_style;
} }
if other.background_color.is_some() {
self.background_color = other.background_color;
}
if other.underline.is_some() { if other.underline.is_some() {
self.underline = other.underline; self.underline = other.underline;
} }
@ -481,6 +506,24 @@ impl From<Hsla> for HighlightStyle {
} }
} }
impl From<FontWeight> for HighlightStyle {
fn from(font_weight: FontWeight) -> Self {
Self {
font_weight: Some(font_weight),
..Default::default()
}
}
}
impl From<FontStyle> for HighlightStyle {
fn from(font_style: FontStyle) -> Self {
Self {
font_style: Some(font_style),
..Default::default()
}
}
}
impl From<Rgba> for HighlightStyle { impl From<Rgba> for HighlightStyle {
fn from(color: Rgba) -> Self { fn from(color: Rgba) -> Self {
Self { Self {
@ -489,3 +532,140 @@ impl From<Rgba> for HighlightStyle {
} }
} }
} }
pub fn combine_highlights(
a: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
b: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
) -> impl Iterator<Item = (Range<usize>, HighlightStyle)> {
let mut endpoints = Vec::new();
let mut highlights = Vec::new();
for (range, highlight) in a.into_iter().chain(b) {
if !range.is_empty() {
let highlight_id = highlights.len();
endpoints.push((range.start, highlight_id, true));
endpoints.push((range.end, highlight_id, false));
highlights.push(highlight);
}
}
endpoints.sort_unstable_by_key(|(position, _, _)| *position);
let mut endpoints = endpoints.into_iter().peekable();
let mut active_styles = HashSet::default();
let mut ix = 0;
iter::from_fn(move || {
while let Some((endpoint_ix, highlight_id, is_start)) = endpoints.peek() {
let prev_index = mem::replace(&mut ix, *endpoint_ix);
if ix > prev_index && !active_styles.is_empty() {
let mut current_style = HighlightStyle::default();
for highlight_id in &active_styles {
current_style.highlight(highlights[*highlight_id]);
}
return Some((prev_index..ix, current_style));
}
if *is_start {
active_styles.insert(*highlight_id);
} else {
active_styles.remove(highlight_id);
}
endpoints.next();
}
None
})
}
#[cfg(test)]
mod tests {
use crate::{blue, green, red, yellow};
use super::*;
#[test]
fn test_combine_highlights() {
assert_eq!(
combine_highlights(
[
(0..5, green().into()),
(4..10, FontWeight::BOLD.into()),
(15..20, yellow().into()),
],
[
(2..6, FontStyle::Italic.into()),
(1..3, blue().into()),
(21..23, red().into()),
]
)
.collect::<Vec<_>>(),
[
(
0..1,
HighlightStyle {
color: Some(green()),
..Default::default()
}
),
(
1..2,
HighlightStyle {
color: Some(blue()),
..Default::default()
}
),
(
2..3,
HighlightStyle {
color: Some(blue()),
font_style: Some(FontStyle::Italic),
..Default::default()
}
),
(
3..4,
HighlightStyle {
color: Some(green()),
font_style: Some(FontStyle::Italic),
..Default::default()
}
),
(
4..5,
HighlightStyle {
color: Some(green()),
font_weight: Some(FontWeight::BOLD),
font_style: Some(FontStyle::Italic),
..Default::default()
}
),
(
5..6,
HighlightStyle {
font_weight: Some(FontWeight::BOLD),
font_style: Some(FontStyle::Italic),
..Default::default()
}
),
(
6..10,
HighlightStyle {
font_weight: Some(FontWeight::BOLD),
..Default::default()
}
),
(
15..20,
HighlightStyle {
color: Some(yellow()),
..Default::default()
}
),
(
21..23,
HighlightStyle {
color: Some(red()),
..Default::default()
}
)
]
);
}
}

View File

@ -1,7 +1,7 @@
use crate::{ use crate::{
self as gpui, hsla, point, px, relative, rems, AbsoluteLength, AlignItems, CursorStyle, self as gpui, hsla, point, px, relative, rems, AbsoluteLength, AlignItems, CursorStyle,
DefiniteLength, Display, Fill, FlexDirection, Hsla, JustifyContent, Length, Position, DefiniteLength, Display, Fill, FlexDirection, Hsla, JustifyContent, Length, Position,
SharedString, StyleRefinement, Visibility, SharedString, StyleRefinement, Visibility, WhiteSpace,
}; };
use crate::{BoxShadow, TextStyleRefinement}; use crate::{BoxShadow, TextStyleRefinement};
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
@ -101,6 +101,24 @@ pub trait Styled: Sized {
self self
} }
/// Sets the whitespace of the element to `normal`.
/// [Docs](https://tailwindcss.com/docs/whitespace#normal)
fn whitespace_normal(mut self) -> Self {
self.text_style()
.get_or_insert_with(Default::default)
.white_space = Some(WhiteSpace::Normal);
self
}
/// Sets the whitespace of the element to `nowrap`.
/// [Docs](https://tailwindcss.com/docs/whitespace#nowrap)
fn whitespace_nowrap(mut self) -> Self {
self.text_style()
.get_or_insert_with(Default::default)
.white_space = Some(WhiteSpace::Nowrap);
self
}
/// Sets the flex direction of the element to `column`. /// Sets the flex direction of the element to `column`.
/// [Docs](https://tailwindcss.com/docs/flex-direction#column) /// [Docs](https://tailwindcss.com/docs/flex-direction#column)
fn flex_col(mut self) -> Self { fn flex_col(mut self) -> Self {
@ -343,6 +361,13 @@ pub trait Styled: Sized {
self self
} }
fn text_bg(mut self, bg: impl Into<Hsla>) -> Self {
self.text_style()
.get_or_insert_with(Default::default)
.background_color = Some(bg.into());
self
}
fn text_size(mut self, size: impl Into<AbsoluteLength>) -> Self { fn text_size(mut self, size: impl Into<AbsoluteLength>) -> Self {
self.text_style() self.text_style()
.get_or_insert_with(Default::default) .get_or_insert_with(Default::default)

View File

@ -196,7 +196,10 @@ impl TextSystem {
let mut decoration_runs = SmallVec::<[DecorationRun; 32]>::new(); let mut decoration_runs = SmallVec::<[DecorationRun; 32]>::new();
for run in runs { for run in runs {
if let Some(last_run) = decoration_runs.last_mut() { if let Some(last_run) = decoration_runs.last_mut() {
if last_run.color == run.color && last_run.underline == run.underline { if last_run.color == run.color
&& last_run.underline == run.underline
&& last_run.background_color == run.background_color
{
last_run.len += run.len as u32; last_run.len += run.len as u32;
continue; continue;
} }
@ -204,6 +207,7 @@ impl TextSystem {
decoration_runs.push(DecorationRun { decoration_runs.push(DecorationRun {
len: run.len as u32, len: run.len as u32,
color: run.color, color: run.color,
background_color: run.background_color,
underline: run.underline.clone(), underline: run.underline.clone(),
}); });
} }
@ -254,13 +258,16 @@ impl TextSystem {
} }
if decoration_runs.last().map_or(false, |last_run| { if decoration_runs.last().map_or(false, |last_run| {
last_run.color == run.color && last_run.underline == run.underline last_run.color == run.color
&& last_run.underline == run.underline
&& last_run.background_color == run.background_color
}) { }) {
decoration_runs.last_mut().unwrap().len += run_len_within_line as u32; decoration_runs.last_mut().unwrap().len += run_len_within_line as u32;
} else { } else {
decoration_runs.push(DecorationRun { decoration_runs.push(DecorationRun {
len: run_len_within_line as u32, len: run_len_within_line as u32,
color: run.color, color: run.color,
background_color: run.background_color,
underline: run.underline.clone(), underline: run.underline.clone(),
}); });
} }
@ -283,7 +290,15 @@ impl TextSystem {
text: SharedString::from(line_text), text: SharedString::from(line_text),
}); });
line_start = line_end + 1; // Skip `\n` character. // Skip `\n` character.
line_start = line_end + 1;
if let Some(run) = runs.peek_mut() {
run.len = run.len.saturating_sub(1);
if run.len == 0 {
runs.next();
}
}
font_runs.clear(); font_runs.clear();
} }

View File

@ -1,6 +1,7 @@
use crate::{ use crate::{
black, point, px, BorrowWindow, Bounds, Hsla, LineLayout, Pixels, Point, Result, SharedString, black, point, px, size, transparent_black, BorrowWindow, Bounds, Corners, Edges, Hsla,
UnderlineStyle, WindowContext, WrapBoundary, WrappedLineLayout, LineLayout, Pixels, Point, Result, SharedString, UnderlineStyle, WindowContext, WrapBoundary,
WrappedLineLayout,
}; };
use derive_more::{Deref, DerefMut}; use derive_more::{Deref, DerefMut};
use smallvec::SmallVec; use smallvec::SmallVec;
@ -10,6 +11,7 @@ use std::sync::Arc;
pub struct DecorationRun { pub struct DecorationRun {
pub len: u32, pub len: u32,
pub color: Hsla, pub color: Hsla,
pub background_color: Option<Hsla>,
pub underline: Option<UnderlineStyle>, pub underline: Option<UnderlineStyle>,
} }
@ -38,7 +40,6 @@ impl ShapedLine {
&self.layout, &self.layout,
line_height, line_height,
&self.decoration_runs, &self.decoration_runs,
None,
&[], &[],
cx, cx,
)?; )?;
@ -72,7 +73,6 @@ impl WrappedLine {
&self.layout.unwrapped_layout, &self.layout.unwrapped_layout,
line_height, line_height,
&self.decoration_runs, &self.decoration_runs,
self.wrap_width,
&self.wrap_boundaries, &self.wrap_boundaries,
cx, cx,
)?; )?;
@ -86,7 +86,6 @@ fn paint_line(
layout: &LineLayout, layout: &LineLayout,
line_height: Pixels, line_height: Pixels,
decoration_runs: &[DecorationRun], decoration_runs: &[DecorationRun],
wrap_width: Option<Pixels>,
wrap_boundaries: &[WrapBoundary], wrap_boundaries: &[WrapBoundary],
cx: &mut WindowContext<'_>, cx: &mut WindowContext<'_>,
) -> Result<()> { ) -> Result<()> {
@ -97,6 +96,7 @@ fn paint_line(
let mut run_end = 0; let mut run_end = 0;
let mut color = black(); let mut color = black();
let mut current_underline: Option<(Point<Pixels>, UnderlineStyle)> = None; let mut current_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
let mut current_background: Option<(Point<Pixels>, Hsla)> = None;
let text_system = cx.text_system().clone(); let text_system = cx.text_system().clone();
let mut glyph_origin = origin; let mut glyph_origin = origin;
let mut prev_glyph_position = Point::default(); let mut prev_glyph_position = Point::default();
@ -110,12 +110,28 @@ fn paint_line(
if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) { if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) {
wraps.next(); wraps.next();
if let Some((underline_origin, underline_style)) = current_underline.take() { if let Some((background_origin, background_color)) = current_background.as_mut() {
cx.paint_quad(
Bounds {
origin: *background_origin,
size: size(glyph_origin.x - background_origin.x, line_height),
},
Corners::default(),
*background_color,
Edges::default(),
transparent_black(),
);
background_origin.x = origin.x;
background_origin.y += line_height;
}
if let Some((underline_origin, underline_style)) = current_underline.as_mut() {
cx.paint_underline( cx.paint_underline(
underline_origin, *underline_origin,
glyph_origin.x - underline_origin.x, glyph_origin.x - underline_origin.x,
&underline_style, underline_style,
)?; );
underline_origin.x = origin.x;
underline_origin.y += line_height;
} }
glyph_origin.x = origin.x; glyph_origin.x = origin.x;
@ -123,9 +139,20 @@ fn paint_line(
} }
prev_glyph_position = glyph.position; prev_glyph_position = glyph.position;
let mut finished_background: Option<(Point<Pixels>, Hsla)> = None;
let mut finished_underline: Option<(Point<Pixels>, UnderlineStyle)> = None; let mut finished_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
if glyph.index >= run_end { if glyph.index >= run_end {
if let Some(style_run) = decoration_runs.next() { if let Some(style_run) = decoration_runs.next() {
if let Some((_, background_color)) = &mut current_background {
if style_run.background_color.as_ref() != Some(background_color) {
finished_background = current_background.take();
}
}
if let Some(run_background) = style_run.background_color {
current_background
.get_or_insert((point(glyph_origin.x, glyph_origin.y), run_background));
}
if let Some((_, underline_style)) = &mut current_underline { if let Some((_, underline_style)) = &mut current_underline {
if style_run.underline.as_ref() != Some(underline_style) { if style_run.underline.as_ref() != Some(underline_style) {
finished_underline = current_underline.take(); finished_underline = current_underline.take();
@ -135,7 +162,7 @@ fn paint_line(
current_underline.get_or_insert(( current_underline.get_or_insert((
point( point(
glyph_origin.x, glyph_origin.x,
origin.y + baseline_offset.y + (layout.descent * 0.618), glyph_origin.y + baseline_offset.y + (layout.descent * 0.618),
), ),
UnderlineStyle { UnderlineStyle {
color: Some(run_underline.color.unwrap_or(style_run.color)), color: Some(run_underline.color.unwrap_or(style_run.color)),
@ -149,16 +176,30 @@ fn paint_line(
color = style_run.color; color = style_run.color;
} else { } else {
run_end = layout.len; run_end = layout.len;
finished_background = current_background.take();
finished_underline = current_underline.take(); finished_underline = current_underline.take();
} }
} }
if let Some((background_origin, background_color)) = finished_background {
cx.paint_quad(
Bounds {
origin: background_origin,
size: size(glyph_origin.x - background_origin.x, line_height),
},
Corners::default(),
background_color,
Edges::default(),
transparent_black(),
);
}
if let Some((underline_origin, underline_style)) = finished_underline { if let Some((underline_origin, underline_style)) = finished_underline {
cx.paint_underline( cx.paint_underline(
underline_origin, underline_origin,
glyph_origin.x - underline_origin.x, glyph_origin.x - underline_origin.x,
&underline_style, &underline_style,
)?; );
} }
let max_glyph_bounds = Bounds { let max_glyph_bounds = Bounds {
@ -188,13 +229,32 @@ fn paint_line(
} }
} }
let mut last_line_end_x = origin.x + layout.width;
if let Some(boundary) = wrap_boundaries.last() {
let run = &layout.runs[boundary.run_ix];
let glyph = &run.glyphs[boundary.glyph_ix];
last_line_end_x -= glyph.position.x;
}
if let Some((background_origin, background_color)) = current_background.take() {
cx.paint_quad(
Bounds {
origin: background_origin,
size: size(last_line_end_x - background_origin.x, line_height),
},
Corners::default(),
background_color,
Edges::default(),
transparent_black(),
);
}
if let Some((underline_start, underline_style)) = current_underline.take() { if let Some((underline_start, underline_style)) = current_underline.take() {
let line_end_x = origin.x + wrap_width.unwrap_or(Pixels::MAX).min(layout.width);
cx.paint_underline( cx.paint_underline(
underline_start, underline_start,
line_end_x - underline_start.x, last_line_end_x - underline_start.x,
&underline_style, &underline_style,
)?; );
} }
Ok(()) Ok(())

View File

@ -198,6 +198,41 @@ impl WrappedLineLayout {
pub fn runs(&self) -> &[ShapedRun] { pub fn runs(&self) -> &[ShapedRun] {
&self.unwrapped_layout.runs &self.unwrapped_layout.runs
} }
pub fn index_for_position(
&self,
position: Point<Pixels>,
line_height: Pixels,
) -> Option<usize> {
let wrapped_line_ix = (position.y / line_height) as usize;
let wrapped_line_start_x = if wrapped_line_ix > 0 {
let wrap_boundary_ix = wrapped_line_ix - 1;
let wrap_boundary = self.wrap_boundaries[wrap_boundary_ix];
let run = &self.unwrapped_layout.runs[wrap_boundary.run_ix];
run.glyphs[wrap_boundary.glyph_ix].position.x
} else {
Pixels::ZERO
};
let wrapped_line_end_x = if wrapped_line_ix < self.wrap_boundaries.len() {
let next_wrap_boundary_ix = wrapped_line_ix;
let next_wrap_boundary = self.wrap_boundaries[next_wrap_boundary_ix];
let run = &self.unwrapped_layout.runs[next_wrap_boundary.run_ix];
run.glyphs[next_wrap_boundary.glyph_ix].position.x
} else {
self.unwrapped_layout.width
};
let mut position_in_unwrapped_line = position;
position_in_unwrapped_line.x += wrapped_line_start_x;
if position_in_unwrapped_line.x > wrapped_line_end_x {
None
} else {
self.unwrapped_layout
.index_for_x(position_in_unwrapped_line.x)
}
}
} }
pub(crate) struct LineLayoutCache { pub(crate) struct LineLayoutCache {

View File

@ -1,7 +1,7 @@
use crate::{ use crate::{
private::Sealed, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace, BorrowWindow, private::Sealed, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace, BorrowWindow,
Bounds, Element, ElementId, Entity, EntityId, Flatten, FocusHandle, FocusableView, LayoutId, Bounds, Element, ElementId, Entity, EntityId, Flatten, FocusHandle, FocusableView, IntoElement,
Model, Pixels, Point, Render, RenderOnce, Size, ViewContext, VisualContext, WeakModel, LayoutId, Model, Pixels, Point, Render, Size, ViewContext, VisualContext, WeakModel,
WindowContext, WindowContext,
}; };
use anyhow::{Context, Result}; use anyhow::{Context, Result};
@ -244,26 +244,26 @@ impl Element for AnyView {
} }
} }
impl<V: 'static + Render> RenderOnce for View<V> { impl<V: 'static + Render> IntoElement for View<V> {
type Element = View<V>; type Element = View<V>;
fn element_id(&self) -> Option<ElementId> { fn element_id(&self) -> Option<ElementId> {
Some(self.model.entity_id.into()) Some(ElementId::from_entity_id(self.model.entity_id))
} }
fn render_once(self) -> Self::Element { fn into_element(self) -> Self::Element {
self self
} }
} }
impl RenderOnce for AnyView { impl IntoElement for AnyView {
type Element = Self; type Element = Self;
fn element_id(&self) -> Option<ElementId> { fn element_id(&self) -> Option<ElementId> {
Some(self.model.entity_id.into()) Some(ElementId::from_entity_id(self.model.entity_id))
} }
fn render_once(self) -> Self::Element { fn into_element(self) -> Self::Element {
self self
} }
} }
@ -308,27 +308,23 @@ where
} }
mod any_view { mod any_view {
use crate::{AnyElement, AnyView, BorrowWindow, Element, LayoutId, Render, WindowContext}; use crate::{AnyElement, AnyView, Element, LayoutId, Render, WindowContext};
pub(crate) fn layout<V: 'static + Render>( pub(crate) fn layout<V: 'static + Render>(
view: &AnyView, view: &AnyView,
cx: &mut WindowContext, cx: &mut WindowContext,
) -> (LayoutId, AnyElement) { ) -> (LayoutId, AnyElement) {
cx.with_element_id(Some(view.model.entity_id), |cx| { let view = view.clone().downcast::<V>().unwrap();
let view = view.clone().downcast::<V>().unwrap(); let mut element = view.update(cx, |view, cx| view.render(cx).into_any());
let mut element = view.update(cx, |view, cx| view.render(cx).into_any()); let layout_id = element.layout(cx);
let layout_id = element.layout(cx); (layout_id, element)
(layout_id, element)
})
} }
pub(crate) fn paint<V: 'static + Render>( pub(crate) fn paint<V: 'static + Render>(
view: &AnyView, _view: &AnyView,
element: AnyElement, element: AnyElement,
cx: &mut WindowContext, cx: &mut WindowContext,
) { ) {
cx.with_element_id(Some(view.model.entity_id), |cx| { element.paint(cx);
element.paint(cx);
})
} }
} }

View File

@ -193,11 +193,11 @@ pub trait FocusableView: 'static + Render {
/// ManagedView is a view (like a Modal, Popover, Menu, etc.) /// ManagedView is a view (like a Modal, Popover, Menu, etc.)
/// where the lifecycle of the view is handled by another view. /// where the lifecycle of the view is handled by another view.
pub trait ManagedView: FocusableView + EventEmitter<Manager> {} pub trait ManagedView: FocusableView + EventEmitter<DismissEvent> {}
impl<M: FocusableView + EventEmitter<Manager>> ManagedView for M {} impl<M: FocusableView + EventEmitter<DismissEvent>> ManagedView for M {}
pub enum Manager { pub enum DismissEvent {
Dismiss, Dismiss,
} }
@ -230,9 +230,15 @@ pub struct Window {
pub(crate) focus: Option<FocusId>, pub(crate) focus: Option<FocusId>,
} }
pub(crate) struct ElementStateBox {
inner: Box<dyn Any>,
#[cfg(debug_assertions)]
type_name: &'static str,
}
// #[derive(Default)] // #[derive(Default)]
pub(crate) struct Frame { pub(crate) struct Frame {
pub(crate) element_states: HashMap<GlobalElementId, Box<dyn Any>>, pub(crate) element_states: HashMap<GlobalElementId, ElementStateBox>,
mouse_listeners: HashMap<TypeId, Vec<(StackingOrder, AnyMouseListener)>>, mouse_listeners: HashMap<TypeId, Vec<(StackingOrder, AnyMouseListener)>>,
pub(crate) dispatch_tree: DispatchTree, pub(crate) dispatch_tree: DispatchTree,
pub(crate) focus_listeners: Vec<AnyFocusListener>, pub(crate) focus_listeners: Vec<AnyFocusListener>,
@ -875,7 +881,7 @@ impl<'a> WindowContext<'a> {
origin: Point<Pixels>, origin: Point<Pixels>,
width: Pixels, width: Pixels,
style: &UnderlineStyle, style: &UnderlineStyle,
) -> Result<()> { ) {
let scale_factor = self.scale_factor(); let scale_factor = self.scale_factor();
let height = if style.wavy { let height = if style.wavy {
style.thickness * 3. style.thickness * 3.
@ -899,7 +905,6 @@ impl<'a> WindowContext<'a> {
wavy: style.wavy, wavy: style.wavy,
}, },
); );
Ok(())
} }
/// Paint a monochrome (non-emoji) glyph into the scene for the current frame at the current z-index. /// Paint a monochrome (non-emoji) glyph into the scene for the current frame at the current z-index.
@ -1512,6 +1517,13 @@ impl<'a> WindowContext<'a> {
.set_input_handler(Box::new(input_handler)); .set_input_handler(Box::new(input_handler));
} }
} }
pub fn on_window_should_close(&mut self, f: impl Fn(&mut WindowContext) -> bool + 'static) {
let mut this = self.to_async();
self.window
.platform_window
.on_should_close(Box::new(move || this.update(|_, cx| f(cx)).unwrap_or(true)))
}
} }
impl Context for WindowContext<'_> { impl Context for WindowContext<'_> {
@ -1658,7 +1670,7 @@ impl VisualContext for WindowContext<'_> {
where where
V: ManagedView, V: ManagedView,
{ {
self.update_view(view, |_, cx| cx.emit(Manager::Dismiss)) self.update_view(view, |_, cx| cx.emit(DismissEvent::Dismiss))
} }
} }
@ -1747,6 +1759,24 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
} }
} }
/// Invoke the given function with the content mask reset to that
/// of the window.
fn break_content_mask<R>(&mut self, f: impl FnOnce(&mut Self) -> R) -> R {
let mask = ContentMask {
bounds: Bounds {
origin: Point::default(),
size: self.window().viewport_size,
},
};
self.window_mut()
.current_frame
.content_mask_stack
.push(mask);
let result = f(self);
self.window_mut().current_frame.content_mask_stack.pop();
result
}
/// Update the global element offset relative to the current offset. This is used to implement /// Update the global element offset relative to the current offset. This is used to implement
/// scrolling. /// scrolling.
fn with_element_offset<R>( fn with_element_offset<R>(
@ -1815,10 +1845,37 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
.remove(&global_id) .remove(&global_id)
}) })
{ {
let ElementStateBox {
inner,
#[cfg(debug_assertions)]
type_name
} = any;
// Using the extra inner option to avoid needing to reallocate a new box. // Using the extra inner option to avoid needing to reallocate a new box.
let mut state_box = any let mut state_box = inner
.downcast::<Option<S>>() .downcast::<Option<S>>()
.expect("invalid element state type for id"); .map_err(|_| {
#[cfg(debug_assertions)]
{
anyhow!(
"invalid element state type for id, requested_type {:?}, actual type: {:?}",
std::any::type_name::<S>(),
type_name
)
}
#[cfg(not(debug_assertions))]
{
anyhow!(
"invalid element state type for id, requested_type {:?}",
std::any::type_name::<S>(),
)
}
})
.unwrap();
// Actual: Option<AnyElement> <- View
// Requested: () <- AnyElemet
let state = state_box let state = state_box
.take() .take()
.expect("element state is already on the stack"); .expect("element state is already on the stack");
@ -1827,14 +1884,27 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
cx.window_mut() cx.window_mut()
.current_frame .current_frame
.element_states .element_states
.insert(global_id, state_box); .insert(global_id, ElementStateBox {
inner: state_box,
#[cfg(debug_assertions)]
type_name
});
result result
} else { } else {
let (result, state) = f(None, cx); let (result, state) = f(None, cx);
cx.window_mut() cx.window_mut()
.current_frame .current_frame
.element_states .element_states
.insert(global_id, Box::new(Some(state))); .insert(global_id,
ElementStateBox {
inner: Box::new(Some(state)),
#[cfg(debug_assertions)]
type_name: std::any::type_name::<S>()
}
);
result result
} }
}) })
@ -2304,7 +2374,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
where where
V: ManagedView, V: ManagedView,
{ {
self.defer(|_, cx| cx.emit(Manager::Dismiss)) self.defer(|_, cx| cx.emit(DismissEvent::Dismiss))
} }
pub fn listener<E>( pub fn listener<E>(
@ -2599,6 +2669,12 @@ pub enum ElementId {
FocusHandle(FocusId), FocusHandle(FocusId),
} }
impl ElementId {
pub(crate) fn from_entity_id(entity_id: EntityId) -> Self {
ElementId::View(entity_id)
}
}
impl TryInto<SharedString> for ElementId { impl TryInto<SharedString> for ElementId {
type Error = anyhow::Error; type Error = anyhow::Error;
@ -2611,12 +2687,6 @@ impl TryInto<SharedString> for ElementId {
} }
} }
impl From<EntityId> for ElementId {
fn from(id: EntityId) -> Self {
ElementId::View(id)
}
}
impl From<usize> for ElementId { impl From<usize> for ElementId {
fn from(id: usize) -> Self { fn from(id: usize) -> Self {
ElementId::Integer(id) ElementId::Integer(id)

View File

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

View File

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

View File

@ -1,6 +1,5 @@
mod action; mod action;
mod derive_component; mod derive_into_element;
mod derive_render_once;
mod register_action; mod register_action;
mod style_helpers; mod style_helpers;
mod test; mod test;
@ -17,14 +16,9 @@ pub fn register_action(attr: TokenStream, item: TokenStream) -> TokenStream {
register_action::register_action_macro(attr, item) register_action::register_action_macro(attr, item)
} }
#[proc_macro_derive(Component, attributes(component))] #[proc_macro_derive(IntoElement)]
pub fn derive_component(input: TokenStream) -> TokenStream { pub fn derive_into_element(input: TokenStream) -> TokenStream {
derive_component::derive_component(input) derive_into_element::derive_into_element(input)
}
#[proc_macro_derive(RenderOnce, attributes(view))]
pub fn derive_render_once(input: TokenStream) -> TokenStream {
derive_render_once::derive_render_once(input)
} }
#[proc_macro] #[proc_macro]

View File

@ -11,7 +11,7 @@ pub struct HighlightId(pub u32);
const DEFAULT_SYNTAX_HIGHLIGHT_ID: HighlightId = HighlightId(u32::MAX); const DEFAULT_SYNTAX_HIGHLIGHT_ID: HighlightId = HighlightId(u32::MAX);
impl HighlightMap { impl HighlightMap {
pub fn new(capture_names: &[String], theme: &SyntaxTheme) -> Self { pub fn new(capture_names: &[&str], theme: &SyntaxTheme) -> Self {
// For each capture name in the highlight query, find the longest // For each capture name in the highlight query, find the longest
// key in the theme's syntax styles that matches all of the // key in the theme's syntax styles that matches all of the
// dot-separated components of the capture name. // dot-separated components of the capture name.
@ -98,9 +98,9 @@ mod tests {
); );
let capture_names = &[ let capture_names = &[
"function.special".to_string(), "function.special",
"function.async.rust".to_string(), "function.async.rust",
"variable.builtin.self".to_string(), "variable.builtin.self",
]; ];
let map = HighlightMap::new(capture_names, &theme); let map = HighlightMap::new(capture_names, &theme);

View File

@ -1383,7 +1383,7 @@ impl Language {
let query = Query::new(self.grammar_mut().ts_language, source)?; let query = Query::new(self.grammar_mut().ts_language, source)?;
let mut override_configs_by_id = HashMap::default(); let mut override_configs_by_id = HashMap::default();
for (ix, name) in query.capture_names().iter().enumerate() { for (ix, name) in query.capture_names().iter().copied().enumerate() {
if !name.starts_with('_') { if !name.starts_with('_') {
let value = self.config.overrides.remove(name).unwrap_or_default(); let value = self.config.overrides.remove(name).unwrap_or_default();
for server_name in &value.opt_into_language_servers { for server_name in &value.opt_into_language_servers {
@ -1396,7 +1396,7 @@ impl Language {
} }
} }
override_configs_by_id.insert(ix as u32, (name.clone(), value)); override_configs_by_id.insert(ix as u32, (name.into(), value));
} }
} }

View File

@ -1300,7 +1300,7 @@ fn assert_capture_ranges(
.collect::<Vec<_>>(); .collect::<Vec<_>>();
for capture in captures { for capture in captures {
let name = &queries[capture.grammar_index].capture_names()[capture.index as usize]; let name = &queries[capture.grammar_index].capture_names()[capture.index as usize];
if highlight_query_capture_names.contains(&name.as_str()) { if highlight_query_capture_names.contains(&name) {
actual_ranges.push(capture.node.byte_range()); actual_ranges.push(capture.node.byte_range());
} }
} }

View File

@ -7,6 +7,7 @@ pub use crate::{
use crate::{ use crate::{
diagnostic_set::{DiagnosticEntry, DiagnosticGroup}, diagnostic_set::{DiagnosticEntry, DiagnosticGroup},
language_settings::{language_settings, LanguageSettings}, language_settings::{language_settings, LanguageSettings},
markdown::parse_markdown,
outline::OutlineItem, outline::OutlineItem,
syntax_map::{ syntax_map::{
SyntaxLayerInfo, SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures, SyntaxMapMatches, SyntaxLayerInfo, SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures, SyntaxMapMatches,
@ -155,12 +156,52 @@ pub struct Diagnostic {
pub is_unnecessary: bool, pub is_unnecessary: bool,
} }
pub async fn prepare_completion_documentation(
documentation: &lsp::Documentation,
language_registry: &Arc<LanguageRegistry>,
language: Option<Arc<Language>>,
) -> Documentation {
match documentation {
lsp::Documentation::String(text) => {
if text.lines().count() <= 1 {
Documentation::SingleLine(text.clone())
} else {
Documentation::MultiLinePlainText(text.clone())
}
}
lsp::Documentation::MarkupContent(lsp::MarkupContent { kind, value }) => match kind {
lsp::MarkupKind::PlainText => {
if value.lines().count() <= 1 {
Documentation::SingleLine(value.clone())
} else {
Documentation::MultiLinePlainText(value.clone())
}
}
lsp::MarkupKind::Markdown => {
let parsed = parse_markdown(value, language_registry, language).await;
Documentation::MultiLineMarkdown(parsed)
}
},
}
}
#[derive(Clone, Debug)]
pub enum Documentation {
Undocumented,
SingleLine(String),
MultiLinePlainText(String),
MultiLineMarkdown(ParsedMarkdown),
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Completion { pub struct Completion {
pub old_range: Range<Anchor>, pub old_range: Range<Anchor>,
pub new_text: String, pub new_text: String,
pub label: CodeLabel, pub label: CodeLabel,
pub server_id: LanguageServerId, pub server_id: LanguageServerId,
pub documentation: Option<Documentation>,
pub lsp_completion: lsp::CompletionItem, pub lsp_completion: lsp::CompletionItem,
} }

View File

@ -11,7 +11,7 @@ pub struct HighlightId(pub u32);
const DEFAULT_SYNTAX_HIGHLIGHT_ID: HighlightId = HighlightId(u32::MAX); const DEFAULT_SYNTAX_HIGHLIGHT_ID: HighlightId = HighlightId(u32::MAX);
impl HighlightMap { impl HighlightMap {
pub fn new(capture_names: &[String], theme: &SyntaxTheme) -> Self { pub fn new(capture_names: &[&str], theme: &SyntaxTheme) -> Self {
// For each capture name in the highlight query, find the longest // For each capture name in the highlight query, find the longest
// key in the theme's syntax styles that matches all of the // key in the theme's syntax styles that matches all of the
// dot-separated components of the capture name. // dot-separated components of the capture name.
@ -100,9 +100,9 @@ mod tests {
}; };
let capture_names = &[ let capture_names = &[
"function.special".to_string(), "function.special",
"function.async.rust".to_string(), "function.async.rust",
"variable.builtin.self".to_string(), "variable.builtin.self",
]; ];
let map = HighlightMap::new(capture_names, &theme); let map = HighlightMap::new(capture_names, &theme);

View File

@ -1391,7 +1391,7 @@ impl Language {
let mut override_configs_by_id = HashMap::default(); let mut override_configs_by_id = HashMap::default();
for (ix, name) in query.capture_names().iter().enumerate() { for (ix, name) in query.capture_names().iter().enumerate() {
if !name.starts_with('_') { if !name.starts_with('_') {
let value = self.config.overrides.remove(name).unwrap_or_default(); let value = self.config.overrides.remove(*name).unwrap_or_default();
for server_name in &value.opt_into_language_servers { for server_name in &value.opt_into_language_servers {
if !self if !self
.config .config
@ -1402,7 +1402,7 @@ impl Language {
} }
} }
override_configs_by_id.insert(ix as u32, (name.clone(), value)); override_configs_by_id.insert(ix as u32, (name.to_string(), value));
} }
} }

View File

@ -482,6 +482,7 @@ pub async fn deserialize_completion(
lsp_completion.filter_text.as_deref(), lsp_completion.filter_text.as_deref(),
) )
}), }),
documentation: None,
server_id: LanguageServerId(completion.server_id as usize), server_id: LanguageServerId(completion.server_id as usize),
lsp_completion, lsp_completion,
}) })

View File

@ -1300,7 +1300,7 @@ fn assert_capture_ranges(
.collect::<Vec<_>>(); .collect::<Vec<_>>();
for capture in captures { for capture in captures {
let name = &queries[capture.grammar_index].capture_names()[capture.index as usize]; let name = &queries[capture.grammar_index].capture_names()[capture.index as usize];
if highlight_query_capture_names.contains(&name.as_str()) { if highlight_query_capture_names.contains(&name) {
actual_ranges.push(capture.node.byte_range()); actual_ranges.push(capture.node.byte_range());
} }
} }

View File

@ -4,7 +4,7 @@ use gpui::{
MouseDownEvent, Render, Task, UniformListScrollHandle, View, ViewContext, WindowContext, MouseDownEvent, Render, Task, UniformListScrollHandle, View, ViewContext, WindowContext,
}; };
use std::{cmp, sync::Arc}; use std::{cmp, sync::Arc};
use ui::{prelude::*, v_stack, Divider, Label, TextColor}; use ui::{prelude::*, v_stack, Color, Divider, Label};
pub struct Picker<D: PickerDelegate> { pub struct Picker<D: PickerDelegate> {
pub delegate: D, pub delegate: D,
@ -15,7 +15,7 @@ pub struct Picker<D: PickerDelegate> {
} }
pub trait PickerDelegate: Sized + 'static { pub trait PickerDelegate: Sized + 'static {
type ListItem: RenderOnce; type ListItem: IntoElement;
fn match_count(&self) -> usize; fn match_count(&self) -> usize;
fn selected_index(&self) -> usize; fn selected_index(&self) -> usize;
@ -114,6 +114,7 @@ impl<D: PickerDelegate> Picker<D> {
} }
fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) { fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
dbg!("canceling!");
self.delegate.dismissed(cx); self.delegate.dismissed(cx);
} }
@ -250,7 +251,7 @@ impl<D: PickerDelegate> Render for Picker<D> {
v_stack().p_1().grow().child( v_stack().p_1().grow().child(
div() div()
.px_1() .px_1()
.child(Label::new("No matches").color(TextColor::Muted)), .child(Label::new("No matches").color(Color::Muted)),
), ),
) )
}) })

View File

@ -13,7 +13,7 @@ mod worktree_tests;
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use client::{proto, Client, Collaborator, TypedEnvelope, UserStore}; use client::{proto, Client, Collaborator, TypedEnvelope, UserStore};
use clock::ReplicaId; use clock::ReplicaId;
use collections::{hash_map, BTreeMap, HashMap, HashSet}; use collections::{hash_map, BTreeMap, HashMap, HashSet, VecDeque};
use copilot::Copilot; use copilot::Copilot;
use futures::{ use futures::{
channel::{ channel::{
@ -62,7 +62,10 @@ use serde::Serialize;
use settings::SettingsStore; use settings::SettingsStore;
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use similar::{ChangeTag, TextDiff}; use similar::{ChangeTag, TextDiff};
use smol::channel::{Receiver, Sender}; use smol::{
channel::{Receiver, Sender},
lock::Semaphore,
};
use std::{ use std::{
cmp::{self, Ordering}, cmp::{self, Ordering},
convert::TryInto, convert::TryInto,
@ -557,6 +560,7 @@ enum SearchMatchCandidate {
}, },
Path { Path {
worktree_id: WorktreeId, worktree_id: WorktreeId,
is_ignored: bool,
path: Arc<Path>, path: Arc<Path>,
}, },
} }
@ -5742,13 +5746,18 @@ impl Project {
.await .await
.log_err(); .log_err();
} }
background background
.scoped(|scope| { .scoped(|scope| {
let max_concurrent_workers = Arc::new(Semaphore::new(workers));
for worker_ix in 0..workers { for worker_ix in 0..workers {
let worker_start_ix = worker_ix * paths_per_worker; let worker_start_ix = worker_ix * paths_per_worker;
let worker_end_ix = worker_start_ix + paths_per_worker; let worker_end_ix = worker_start_ix + paths_per_worker;
let unnamed_buffers = opened_buffers.clone(); let unnamed_buffers = opened_buffers.clone();
let limiter = Arc::clone(&max_concurrent_workers);
scope.spawn(async move { scope.spawn(async move {
let _guard = limiter.acquire().await;
let mut snapshot_start_ix = 0; let mut snapshot_start_ix = 0;
let mut abs_path = PathBuf::new(); let mut abs_path = PathBuf::new();
for snapshot in snapshots { for snapshot in snapshots {
@ -5797,6 +5806,7 @@ impl Project {
let project_path = SearchMatchCandidate::Path { let project_path = SearchMatchCandidate::Path {
worktree_id: snapshot.id(), worktree_id: snapshot.id(),
path: entry.path.clone(), path: entry.path.clone(),
is_ignored: entry.is_ignored,
}; };
if matching_paths_tx.send(project_path).await.is_err() { if matching_paths_tx.send(project_path).await.is_err() {
break; break;
@ -5809,6 +5819,94 @@ impl Project {
} }
}); });
} }
if query.include_ignored() {
for snapshot in snapshots {
for ignored_entry in snapshot
.entries(query.include_ignored())
.filter(|e| e.is_ignored)
{
let limiter = Arc::clone(&max_concurrent_workers);
scope.spawn(async move {
let _guard = limiter.acquire().await;
let mut ignored_paths_to_process =
VecDeque::from([snapshot.abs_path().join(&ignored_entry.path)]);
while let Some(ignored_abs_path) =
ignored_paths_to_process.pop_front()
{
if !query.file_matches(Some(&ignored_abs_path))
|| snapshot.is_path_excluded(&ignored_abs_path)
{
continue;
}
if let Some(fs_metadata) = fs
.metadata(&ignored_abs_path)
.await
.with_context(|| {
format!("fetching fs metadata for {ignored_abs_path:?}")
})
.log_err()
.flatten()
{
if fs_metadata.is_dir {
if let Some(mut subfiles) = fs
.read_dir(&ignored_abs_path)
.await
.with_context(|| {
format!(
"listing ignored path {ignored_abs_path:?}"
)
})
.log_err()
{
while let Some(subfile) = subfiles.next().await {
if let Some(subfile) = subfile.log_err() {
ignored_paths_to_process.push_back(subfile);
}
}
}
} else if !fs_metadata.is_symlink {
let matches = if let Some(file) = fs
.open_sync(&ignored_abs_path)
.await
.with_context(|| {
format!(
"Opening ignored path {ignored_abs_path:?}"
)
})
.log_err()
{
query.detect(file).unwrap_or(false)
} else {
false
};
if matches {
let project_path = SearchMatchCandidate::Path {
worktree_id: snapshot.id(),
path: Arc::from(
ignored_abs_path
.strip_prefix(snapshot.abs_path())
.expect(
"scanning worktree-related files",
),
),
is_ignored: true,
};
if matching_paths_tx
.send(project_path)
.await
.is_err()
{
return;
}
}
}
}
}
});
}
}
}
}) })
.await; .await;
} }
@ -5917,11 +6015,24 @@ impl Project {
let (buffers_tx, buffers_rx) = smol::channel::bounded(1024); let (buffers_tx, buffers_rx) = smol::channel::bounded(1024);
let (sorted_buffers_tx, sorted_buffers_rx) = futures::channel::oneshot::channel(); let (sorted_buffers_tx, sorted_buffers_rx) = futures::channel::oneshot::channel();
cx.spawn(|this, cx| async move { cx.spawn(|this, cx| async move {
let mut buffers = vec![]; let mut buffers = Vec::new();
let mut ignored_buffers = Vec::new();
while let Some(entry) = matching_paths_rx.next().await { while let Some(entry) = matching_paths_rx.next().await {
buffers.push(entry); if matches!(
entry,
SearchMatchCandidate::Path {
is_ignored: true,
..
}
) {
ignored_buffers.push(entry);
} else {
buffers.push(entry);
}
} }
buffers.sort_by_key(|candidate| candidate.path()); buffers.sort_by_key(|candidate| candidate.path());
ignored_buffers.sort_by_key(|candidate| candidate.path());
buffers.extend(ignored_buffers);
let matching_paths = buffers.clone(); let matching_paths = buffers.clone();
let _ = sorted_buffers_tx.send(buffers); let _ = sorted_buffers_tx.send(buffers);
for (index, candidate) in matching_paths.into_iter().enumerate() { for (index, candidate) in matching_paths.into_iter().enumerate() {
@ -5933,7 +6044,9 @@ impl Project {
cx.spawn(|mut cx| async move { cx.spawn(|mut cx| async move {
let buffer = match candidate { let buffer = match candidate {
SearchMatchCandidate::OpenBuffer { buffer, .. } => Some(buffer), SearchMatchCandidate::OpenBuffer { buffer, .. } => Some(buffer),
SearchMatchCandidate::Path { worktree_id, path } => this SearchMatchCandidate::Path {
worktree_id, path, ..
} => this
.update(&mut cx, |this, cx| { .update(&mut cx, |this, cx| {
this.open_buffer((worktree_id, path), cx) this.open_buffer((worktree_id, path), cx)
}) })

View File

@ -2226,7 +2226,7 @@ impl LocalSnapshot {
paths paths
} }
fn is_abs_path_excluded(&self, abs_path: &Path) -> bool { pub fn is_path_excluded(&self, abs_path: &Path) -> bool {
self.file_scan_exclusions self.file_scan_exclusions
.iter() .iter()
.any(|exclude_matcher| exclude_matcher.is_match(abs_path)) .any(|exclude_matcher| exclude_matcher.is_match(abs_path))
@ -2399,26 +2399,9 @@ impl BackgroundScannerState {
self.snapshot.check_invariants(false); self.snapshot.check_invariants(false);
} }
fn reload_repositories(&mut self, changed_paths: &[Arc<Path>], fs: &dyn Fs) { fn reload_repositories(&mut self, dot_git_dirs_to_reload: &HashSet<PathBuf>, fs: &dyn Fs) {
let scan_id = self.snapshot.scan_id; let scan_id = self.snapshot.scan_id;
for dot_git_dir in dot_git_dirs_to_reload {
// Find each of the .git directories that contain any of the given paths.
let mut prev_dot_git_dir = None;
for changed_path in changed_paths {
let Some(dot_git_dir) = changed_path
.ancestors()
.find(|ancestor| ancestor.file_name() == Some(&*DOT_GIT))
else {
continue;
};
// Avoid processing the same repository multiple times, if multiple paths
// within it have changed.
if prev_dot_git_dir == Some(dot_git_dir) {
continue;
}
prev_dot_git_dir = Some(dot_git_dir);
// If there is already a repository for this .git directory, reload // If there is already a repository for this .git directory, reload
// the status for all of its files. // the status for all of its files.
let repository = self let repository = self
@ -2430,7 +2413,7 @@ impl BackgroundScannerState {
}); });
match repository { match repository {
None => { None => {
self.build_git_repository(dot_git_dir.into(), fs); self.build_git_repository(Arc::from(dot_git_dir.as_path()), fs);
} }
Some((entry_id, repository)) => { Some((entry_id, repository)) => {
if repository.git_dir_scan_id == scan_id { if repository.git_dir_scan_id == scan_id {
@ -2444,7 +2427,7 @@ impl BackgroundScannerState {
continue; continue;
}; };
log::info!("reload git repository {:?}", dot_git_dir); log::info!("reload git repository {dot_git_dir:?}");
let repository = repository.repo_ptr.lock(); let repository = repository.repo_ptr.lock();
let branch = repository.branch_name(); let branch = repository.branch_name();
repository.reload_index(); repository.reload_index();
@ -2475,7 +2458,9 @@ impl BackgroundScannerState {
ids_to_preserve.insert(work_directory_id); ids_to_preserve.insert(work_directory_id);
} else { } else {
let git_dir_abs_path = snapshot.abs_path().join(&entry.git_dir_path); let git_dir_abs_path = snapshot.abs_path().join(&entry.git_dir_path);
if snapshot.is_abs_path_excluded(&git_dir_abs_path) let git_dir_excluded = snapshot.is_path_excluded(&entry.git_dir_path)
|| snapshot.is_path_excluded(&git_dir_abs_path);
if git_dir_excluded
&& !matches!(smol::block_on(fs.metadata(&git_dir_abs_path)), Ok(None)) && !matches!(smol::block_on(fs.metadata(&git_dir_abs_path)), Ok(None))
{ {
ids_to_preserve.insert(work_directory_id); ids_to_preserve.insert(work_directory_id);
@ -3314,11 +3299,26 @@ impl BackgroundScanner {
}; };
let mut relative_paths = Vec::with_capacity(abs_paths.len()); let mut relative_paths = Vec::with_capacity(abs_paths.len());
let mut dot_git_paths_to_reload = HashSet::default();
abs_paths.sort_unstable(); abs_paths.sort_unstable();
abs_paths.dedup_by(|a, b| a.starts_with(&b)); abs_paths.dedup_by(|a, b| a.starts_with(&b));
abs_paths.retain(|abs_path| { abs_paths.retain(|abs_path| {
let snapshot = &self.state.lock().snapshot; let snapshot = &self.state.lock().snapshot;
{ {
let mut is_git_related = false;
if let Some(dot_git_dir) = abs_path
.ancestors()
.find(|ancestor| ancestor.file_name() == Some(&*DOT_GIT))
{
let dot_git_path = dot_git_dir
.strip_prefix(&root_canonical_path)
.ok()
.map(|path| path.to_path_buf())
.unwrap_or_else(|| dot_git_dir.to_path_buf());
dot_git_paths_to_reload.insert(dot_git_path.to_path_buf());
is_git_related = true;
}
let relative_path: Arc<Path> = let relative_path: Arc<Path> =
if let Ok(path) = abs_path.strip_prefix(&root_canonical_path) { if let Ok(path) = abs_path.strip_prefix(&root_canonical_path) {
path.into() path.into()
@ -3328,23 +3328,30 @@ impl BackgroundScanner {
); );
return false; return false;
}; };
let parent_dir_is_loaded = relative_path.parent().map_or(true, |parent| {
snapshot
.entry_for_path(parent)
.map_or(false, |entry| entry.kind == EntryKind::Dir)
});
if !parent_dir_is_loaded {
log::debug!("ignoring event {relative_path:?} within unloaded directory");
return false;
}
if !is_git_related(&abs_path) { // FS events may come for files which parent directory is excluded, need to check ignore those.
let parent_dir_is_loaded = relative_path.parent().map_or(true, |parent| { let mut path_to_test = abs_path.clone();
snapshot let mut excluded_file_event = snapshot.is_path_excluded(abs_path)
.entry_for_path(parent) || snapshot.is_path_excluded(&relative_path);
.map_or(false, |entry| entry.kind == EntryKind::Dir) while !excluded_file_event && path_to_test.pop() {
}); if snapshot.is_path_excluded(&path_to_test) {
if !parent_dir_is_loaded { excluded_file_event = true;
log::debug!("ignoring event {relative_path:?} within unloaded directory");
return false;
} }
if snapshot.is_abs_path_excluded(abs_path) { }
log::debug!( if excluded_file_event {
"ignoring FS event for path {relative_path:?} within excluded directory" if !is_git_related {
); log::debug!("ignoring FS event for excluded path {relative_path:?}");
return false;
} }
return false;
} }
relative_paths.push(relative_path); relative_paths.push(relative_path);
@ -3352,31 +3359,39 @@ impl BackgroundScanner {
} }
}); });
if relative_paths.is_empty() { if dot_git_paths_to_reload.is_empty() && relative_paths.is_empty() {
return; return;
} }
log::debug!("received fs events {:?}", relative_paths); if !relative_paths.is_empty() {
log::debug!("received fs events {:?}", relative_paths);
let (scan_job_tx, scan_job_rx) = channel::unbounded(); let (scan_job_tx, scan_job_rx) = channel::unbounded();
self.reload_entries_for_paths( self.reload_entries_for_paths(
root_path, root_path,
root_canonical_path, root_canonical_path,
&relative_paths, &relative_paths,
abs_paths, abs_paths,
Some(scan_job_tx.clone()), Some(scan_job_tx.clone()),
) )
.await; .await;
drop(scan_job_tx); drop(scan_job_tx);
self.scan_dirs(false, scan_job_rx).await; self.scan_dirs(false, scan_job_rx).await;
let (scan_job_tx, scan_job_rx) = channel::unbounded(); let (scan_job_tx, scan_job_rx) = channel::unbounded();
self.update_ignore_statuses(scan_job_tx).await; self.update_ignore_statuses(scan_job_tx).await;
self.scan_dirs(false, scan_job_rx).await; self.scan_dirs(false, scan_job_rx).await;
}
{ {
let mut state = self.state.lock(); let mut state = self.state.lock();
state.reload_repositories(&relative_paths, self.fs.as_ref()); if !dot_git_paths_to_reload.is_empty() {
if relative_paths.is_empty() {
state.snapshot.scan_id += 1;
}
log::debug!("reloading repositories: {dot_git_paths_to_reload:?}");
state.reload_repositories(&dot_git_paths_to_reload, self.fs.as_ref());
}
state.snapshot.completed_scan_id = state.snapshot.scan_id; state.snapshot.completed_scan_id = state.snapshot.scan_id;
for (_, entry_id) in mem::take(&mut state.removed_entry_ids) { for (_, entry_id) in mem::take(&mut state.removed_entry_ids) {
state.scanned_dirs.remove(&entry_id); state.scanned_dirs.remove(&entry_id);
@ -3516,7 +3531,7 @@ impl BackgroundScanner {
let state = self.state.lock(); let state = self.state.lock();
let snapshot = &state.snapshot; let snapshot = &state.snapshot;
root_abs_path = snapshot.abs_path().clone(); root_abs_path = snapshot.abs_path().clone();
if snapshot.is_abs_path_excluded(&job.abs_path) { if snapshot.is_path_excluded(&job.abs_path) {
log::error!("skipping excluded directory {:?}", job.path); log::error!("skipping excluded directory {:?}", job.path);
return Ok(()); return Ok(());
} }
@ -3588,7 +3603,7 @@ impl BackgroundScanner {
{ {
let mut state = self.state.lock(); let mut state = self.state.lock();
if state.snapshot.is_abs_path_excluded(&child_abs_path) { if state.snapshot.is_path_excluded(&child_abs_path) {
let relative_path = job.path.join(child_name); let relative_path = job.path.join(child_name);
log::debug!("skipping excluded child entry {relative_path:?}"); log::debug!("skipping excluded child entry {relative_path:?}");
state.remove_path(&relative_path); state.remove_path(&relative_path);
@ -4130,12 +4145,6 @@ impl BackgroundScanner {
} }
} }
fn is_git_related(abs_path: &Path) -> bool {
abs_path
.components()
.any(|c| c.as_os_str() == *DOT_GIT || c.as_os_str() == *GITIGNORE)
}
fn char_bag_for_path(root_char_bag: CharBag, path: &Path) -> CharBag { fn char_bag_for_path(root_char_bag: CharBag, path: &Path) -> CharBag {
let mut result = root_char_bag; let mut result = root_char_bag;
result.extend( result.extend(

View File

@ -990,6 +990,145 @@ async fn test_file_scan_exclusions(cx: &mut TestAppContext) {
}); });
} }
#[gpui::test]
async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) {
init_test(cx);
let dir = temp_tree(json!({
".git": {
"HEAD": "ref: refs/heads/main\n",
"foo": "bar",
},
".gitignore": "**/target\n/node_modules\ntest_output\n",
"target": {
"index": "blah2"
},
"node_modules": {
".DS_Store": "",
"prettier": {
"package.json": "{}",
},
},
"src": {
".DS_Store": "",
"foo": {
"foo.rs": "mod another;\n",
"another.rs": "// another",
},
"bar": {
"bar.rs": "// bar",
},
"lib.rs": "mod foo;\nmod bar;\n",
},
".DS_Store": "",
}));
cx.update(|cx| {
cx.update_global::<SettingsStore, _, _>(|store, cx| {
store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
project_settings.file_scan_exclusions = Some(vec![
"**/.git".to_string(),
"node_modules/".to_string(),
"build_output".to_string(),
]);
});
});
});
let tree = Worktree::local(
build_client(cx),
dir.path(),
true,
Arc::new(RealFs),
Default::default(),
&mut cx.to_async(),
)
.await
.unwrap();
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
.await;
tree.flush_fs_events(cx).await;
tree.read_with(cx, |tree, _| {
check_worktree_entries(
tree,
&[
".git/HEAD",
".git/foo",
"node_modules/.DS_Store",
"node_modules/prettier",
"node_modules/prettier/package.json",
],
&["target", "node_modules"],
&[
".DS_Store",
"src/.DS_Store",
"src/lib.rs",
"src/foo/foo.rs",
"src/foo/another.rs",
"src/bar/bar.rs",
".gitignore",
],
)
});
let new_excluded_dir = dir.path().join("build_output");
let new_ignored_dir = dir.path().join("test_output");
std::fs::create_dir_all(&new_excluded_dir)
.unwrap_or_else(|e| panic!("Failed to create a {new_excluded_dir:?} directory: {e}"));
std::fs::create_dir_all(&new_ignored_dir)
.unwrap_or_else(|e| panic!("Failed to create a {new_ignored_dir:?} directory: {e}"));
let node_modules_dir = dir.path().join("node_modules");
let dot_git_dir = dir.path().join(".git");
let src_dir = dir.path().join("src");
for existing_dir in [&node_modules_dir, &dot_git_dir, &src_dir] {
assert!(
existing_dir.is_dir(),
"Expect {existing_dir:?} to be present in the FS already"
);
}
for directory_for_new_file in [
new_excluded_dir,
new_ignored_dir,
node_modules_dir,
dot_git_dir,
src_dir,
] {
std::fs::write(directory_for_new_file.join("new_file"), "new file contents")
.unwrap_or_else(|e| {
panic!("Failed to create in {directory_for_new_file:?} a new file: {e}")
});
}
tree.flush_fs_events(cx).await;
tree.read_with(cx, |tree, _| {
check_worktree_entries(
tree,
&[
".git/HEAD",
".git/foo",
".git/new_file",
"node_modules/.DS_Store",
"node_modules/prettier",
"node_modules/prettier/package.json",
"node_modules/new_file",
"build_output",
"build_output/new_file",
"test_output/new_file",
],
&["target", "node_modules", "test_output"],
&[
".DS_Store",
"src/.DS_Store",
"src/lib.rs",
"src/foo/foo.rs",
"src/foo/another.rs",
"src/bar/bar.rs",
"src/new_file",
".gitignore",
],
)
});
}
#[gpui::test(iterations = 30)] #[gpui::test(iterations = 30)]
async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) { async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
init_test(cx); init_test(cx);

View File

@ -10,7 +10,7 @@ use futures::future;
use gpui::{AppContext, AsyncAppContext, Model}; use gpui::{AppContext, AsyncAppContext, Model};
use language::{ use language::{
language_settings::{language_settings, InlayHintKind}, language_settings::{language_settings, InlayHintKind},
point_from_lsp, point_to_lsp, point_from_lsp, point_to_lsp, prepare_completion_documentation,
proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version}, proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version},
range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CharKind, range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CharKind,
CodeAction, Completion, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction, CodeAction, Completion, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction,
@ -1339,7 +1339,7 @@ impl LspCommand for GetCompletions {
async fn response_from_lsp( async fn response_from_lsp(
self, self,
completions: Option<lsp::CompletionResponse>, completions: Option<lsp::CompletionResponse>,
_: Model<Project>, project: Model<Project>,
buffer: Model<Buffer>, buffer: Model<Buffer>,
server_id: LanguageServerId, server_id: LanguageServerId,
mut cx: AsyncAppContext, mut cx: AsyncAppContext,
@ -1359,7 +1359,8 @@ impl LspCommand for GetCompletions {
Default::default() Default::default()
}; };
let completions = buffer.update(&mut cx, |buffer, _| { let completions = buffer.update(&mut cx, |buffer, cx| {
let language_registry = project.read(cx).languages().clone();
let language = buffer.language().cloned(); let language = buffer.language().cloned();
let snapshot = buffer.snapshot(); let snapshot = buffer.snapshot();
let clipped_position = buffer.clip_point_utf16(Unclipped(self.position), Bias::Left); let clipped_position = buffer.clip_point_utf16(Unclipped(self.position), Bias::Left);
@ -1443,14 +1444,29 @@ impl LspCommand for GetCompletions {
} }
}; };
let language_registry = language_registry.clone();
let language = language.clone(); let language = language.clone();
LineEnding::normalize(&mut new_text); LineEnding::normalize(&mut new_text);
Some(async move { Some(async move {
let mut label = None; let mut label = None;
if let Some(language) = language { if let Some(language) = language.as_ref() {
language.process_completion(&mut lsp_completion).await; language.process_completion(&mut lsp_completion).await;
label = language.label_for_completion(&lsp_completion).await; label = language.label_for_completion(&lsp_completion).await;
} }
let documentation = if let Some(lsp_docs) = &lsp_completion.documentation {
Some(
prepare_completion_documentation(
lsp_docs,
&language_registry,
language.clone(),
)
.await,
)
} else {
None
};
Completion { Completion {
old_range, old_range,
new_text, new_text,
@ -1460,6 +1476,7 @@ impl LspCommand for GetCompletions {
lsp_completion.filter_text.as_deref(), lsp_completion.filter_text.as_deref(),
) )
}), }),
documentation,
server_id, server_id,
lsp_completion, lsp_completion,
} }

View File

@ -13,7 +13,7 @@ mod worktree_tests;
use anyhow::{anyhow, Context as _, Result}; use anyhow::{anyhow, Context as _, Result};
use client::{proto, Client, Collaborator, TypedEnvelope, UserStore}; use client::{proto, Client, Collaborator, TypedEnvelope, UserStore};
use clock::ReplicaId; use clock::ReplicaId;
use collections::{hash_map, BTreeMap, HashMap, HashSet}; use collections::{hash_map, BTreeMap, HashMap, HashSet, VecDeque};
use copilot::Copilot; use copilot::Copilot;
use futures::{ use futures::{
channel::{ channel::{
@ -63,6 +63,7 @@ use settings::{Settings, SettingsStore};
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use similar::{ChangeTag, TextDiff}; use similar::{ChangeTag, TextDiff};
use smol::channel::{Receiver, Sender}; use smol::channel::{Receiver, Sender};
use smol::lock::Semaphore;
use std::{ use std::{
cmp::{self, Ordering}, cmp::{self, Ordering},
convert::TryInto, convert::TryInto,
@ -557,6 +558,7 @@ enum SearchMatchCandidate {
}, },
Path { Path {
worktree_id: WorktreeId, worktree_id: WorktreeId,
is_ignored: bool,
path: Arc<Path>, path: Arc<Path>,
}, },
} }
@ -5815,11 +5817,15 @@ impl Project {
} }
executor executor
.scoped(|scope| { .scoped(|scope| {
let max_concurrent_workers = Arc::new(Semaphore::new(workers));
for worker_ix in 0..workers { for worker_ix in 0..workers {
let worker_start_ix = worker_ix * paths_per_worker; let worker_start_ix = worker_ix * paths_per_worker;
let worker_end_ix = worker_start_ix + paths_per_worker; let worker_end_ix = worker_start_ix + paths_per_worker;
let unnamed_buffers = opened_buffers.clone(); let unnamed_buffers = opened_buffers.clone();
let limiter = Arc::clone(&max_concurrent_workers);
scope.spawn(async move { scope.spawn(async move {
let _guard = limiter.acquire().await;
let mut snapshot_start_ix = 0; let mut snapshot_start_ix = 0;
let mut abs_path = PathBuf::new(); let mut abs_path = PathBuf::new();
for snapshot in snapshots { for snapshot in snapshots {
@ -5868,6 +5874,7 @@ impl Project {
let project_path = SearchMatchCandidate::Path { let project_path = SearchMatchCandidate::Path {
worktree_id: snapshot.id(), worktree_id: snapshot.id(),
path: entry.path.clone(), path: entry.path.clone(),
is_ignored: entry.is_ignored,
}; };
if matching_paths_tx.send(project_path).await.is_err() { if matching_paths_tx.send(project_path).await.is_err() {
break; break;
@ -5880,6 +5887,94 @@ impl Project {
} }
}); });
} }
if query.include_ignored() {
for snapshot in snapshots {
for ignored_entry in snapshot
.entries(query.include_ignored())
.filter(|e| e.is_ignored)
{
let limiter = Arc::clone(&max_concurrent_workers);
scope.spawn(async move {
let _guard = limiter.acquire().await;
let mut ignored_paths_to_process =
VecDeque::from([snapshot.abs_path().join(&ignored_entry.path)]);
while let Some(ignored_abs_path) =
ignored_paths_to_process.pop_front()
{
if !query.file_matches(Some(&ignored_abs_path))
|| snapshot.is_path_excluded(&ignored_abs_path)
{
continue;
}
if let Some(fs_metadata) = fs
.metadata(&ignored_abs_path)
.await
.with_context(|| {
format!("fetching fs metadata for {ignored_abs_path:?}")
})
.log_err()
.flatten()
{
if fs_metadata.is_dir {
if let Some(mut subfiles) = fs
.read_dir(&ignored_abs_path)
.await
.with_context(|| {
format!(
"listing ignored path {ignored_abs_path:?}"
)
})
.log_err()
{
while let Some(subfile) = subfiles.next().await {
if let Some(subfile) = subfile.log_err() {
ignored_paths_to_process.push_back(subfile);
}
}
}
} else if !fs_metadata.is_symlink {
let matches = if let Some(file) = fs
.open_sync(&ignored_abs_path)
.await
.with_context(|| {
format!(
"Opening ignored path {ignored_abs_path:?}"
)
})
.log_err()
{
query.detect(file).unwrap_or(false)
} else {
false
};
if matches {
let project_path = SearchMatchCandidate::Path {
worktree_id: snapshot.id(),
path: Arc::from(
ignored_abs_path
.strip_prefix(snapshot.abs_path())
.expect(
"scanning worktree-related files",
),
),
is_ignored: true,
};
if matching_paths_tx
.send(project_path)
.await
.is_err()
{
return;
}
}
}
}
}
});
}
}
}
}) })
.await; .await;
} }
@ -5986,11 +6081,24 @@ impl Project {
let (buffers_tx, buffers_rx) = smol::channel::bounded(1024); let (buffers_tx, buffers_rx) = smol::channel::bounded(1024);
let (sorted_buffers_tx, sorted_buffers_rx) = futures::channel::oneshot::channel(); let (sorted_buffers_tx, sorted_buffers_rx) = futures::channel::oneshot::channel();
cx.spawn(move |this, cx| async move { cx.spawn(move |this, cx| async move {
let mut buffers = vec![]; let mut buffers = Vec::new();
let mut ignored_buffers = Vec::new();
while let Some(entry) = matching_paths_rx.next().await { while let Some(entry) = matching_paths_rx.next().await {
buffers.push(entry); if matches!(
entry,
SearchMatchCandidate::Path {
is_ignored: true,
..
}
) {
ignored_buffers.push(entry);
} else {
buffers.push(entry);
}
} }
buffers.sort_by_key(|candidate| candidate.path()); buffers.sort_by_key(|candidate| candidate.path());
ignored_buffers.sort_by_key(|candidate| candidate.path());
buffers.extend(ignored_buffers);
let matching_paths = buffers.clone(); let matching_paths = buffers.clone();
let _ = sorted_buffers_tx.send(buffers); let _ = sorted_buffers_tx.send(buffers);
for (index, candidate) in matching_paths.into_iter().enumerate() { for (index, candidate) in matching_paths.into_iter().enumerate() {
@ -6002,7 +6110,9 @@ impl Project {
cx.spawn(move |mut cx| async move { cx.spawn(move |mut cx| async move {
let buffer = match candidate { let buffer = match candidate {
SearchMatchCandidate::OpenBuffer { buffer, .. } => Some(buffer), SearchMatchCandidate::OpenBuffer { buffer, .. } => Some(buffer),
SearchMatchCandidate::Path { worktree_id, path } => this SearchMatchCandidate::Path {
worktree_id, path, ..
} => this
.update(&mut cx, |this, cx| { .update(&mut cx, |this, cx| {
this.open_buffer((worktree_id, path), cx) this.open_buffer((worktree_id, path), cx)
})? })?

View File

@ -2222,7 +2222,7 @@ impl LocalSnapshot {
paths paths
} }
fn is_abs_path_excluded(&self, abs_path: &Path) -> bool { pub fn is_path_excluded(&self, abs_path: &Path) -> bool {
self.file_scan_exclusions self.file_scan_exclusions
.iter() .iter()
.any(|exclude_matcher| exclude_matcher.is_match(abs_path)) .any(|exclude_matcher| exclude_matcher.is_match(abs_path))
@ -2395,26 +2395,10 @@ impl BackgroundScannerState {
self.snapshot.check_invariants(false); self.snapshot.check_invariants(false);
} }
fn reload_repositories(&mut self, changed_paths: &[Arc<Path>], fs: &dyn Fs) { fn reload_repositories(&mut self, dot_git_dirs_to_reload: &HashSet<PathBuf>, fs: &dyn Fs) {
let scan_id = self.snapshot.scan_id; let scan_id = self.snapshot.scan_id;
// Find each of the .git directories that contain any of the given paths. for dot_git_dir in dot_git_dirs_to_reload {
let mut prev_dot_git_dir = None;
for changed_path in changed_paths {
let Some(dot_git_dir) = changed_path
.ancestors()
.find(|ancestor| ancestor.file_name() == Some(&*DOT_GIT))
else {
continue;
};
// Avoid processing the same repository multiple times, if multiple paths
// within it have changed.
if prev_dot_git_dir == Some(dot_git_dir) {
continue;
}
prev_dot_git_dir = Some(dot_git_dir);
// If there is already a repository for this .git directory, reload // If there is already a repository for this .git directory, reload
// the status for all of its files. // the status for all of its files.
let repository = self let repository = self
@ -2426,7 +2410,7 @@ impl BackgroundScannerState {
}); });
match repository { match repository {
None => { None => {
self.build_git_repository(dot_git_dir.into(), fs); self.build_git_repository(Arc::from(dot_git_dir.as_path()), fs);
} }
Some((entry_id, repository)) => { Some((entry_id, repository)) => {
if repository.git_dir_scan_id == scan_id { if repository.git_dir_scan_id == scan_id {
@ -2440,7 +2424,7 @@ impl BackgroundScannerState {
continue; continue;
}; };
log::info!("reload git repository {:?}", dot_git_dir); log::info!("reload git repository {dot_git_dir:?}");
let repository = repository.repo_ptr.lock(); let repository = repository.repo_ptr.lock();
let branch = repository.branch_name(); let branch = repository.branch_name();
repository.reload_index(); repository.reload_index();
@ -2471,7 +2455,9 @@ impl BackgroundScannerState {
ids_to_preserve.insert(work_directory_id); ids_to_preserve.insert(work_directory_id);
} else { } else {
let git_dir_abs_path = snapshot.abs_path().join(&entry.git_dir_path); let git_dir_abs_path = snapshot.abs_path().join(&entry.git_dir_path);
if snapshot.is_abs_path_excluded(&git_dir_abs_path) let git_dir_excluded = snapshot.is_path_excluded(&entry.git_dir_path)
|| snapshot.is_path_excluded(&git_dir_abs_path);
if git_dir_excluded
&& !matches!(smol::block_on(fs.metadata(&git_dir_abs_path)), Ok(None)) && !matches!(smol::block_on(fs.metadata(&git_dir_abs_path)), Ok(None))
{ {
ids_to_preserve.insert(work_directory_id); ids_to_preserve.insert(work_directory_id);
@ -3303,11 +3289,26 @@ impl BackgroundScanner {
}; };
let mut relative_paths = Vec::with_capacity(abs_paths.len()); let mut relative_paths = Vec::with_capacity(abs_paths.len());
let mut dot_git_paths_to_reload = HashSet::default();
abs_paths.sort_unstable(); abs_paths.sort_unstable();
abs_paths.dedup_by(|a, b| a.starts_with(&b)); abs_paths.dedup_by(|a, b| a.starts_with(&b));
abs_paths.retain(|abs_path| { abs_paths.retain(|abs_path| {
let snapshot = &self.state.lock().snapshot; let snapshot = &self.state.lock().snapshot;
{ {
let mut is_git_related = false;
if let Some(dot_git_dir) = abs_path
.ancestors()
.find(|ancestor| ancestor.file_name() == Some(&*DOT_GIT))
{
let dot_git_path = dot_git_dir
.strip_prefix(&root_canonical_path)
.ok()
.map(|path| path.to_path_buf())
.unwrap_or_else(|| dot_git_dir.to_path_buf());
dot_git_paths_to_reload.insert(dot_git_path.to_path_buf());
is_git_related = true;
}
let relative_path: Arc<Path> = let relative_path: Arc<Path> =
if let Ok(path) = abs_path.strip_prefix(&root_canonical_path) { if let Ok(path) = abs_path.strip_prefix(&root_canonical_path) {
path.into() path.into()
@ -3318,22 +3319,30 @@ impl BackgroundScanner {
return false; return false;
}; };
if !is_git_related(&abs_path) { let parent_dir_is_loaded = relative_path.parent().map_or(true, |parent| {
let parent_dir_is_loaded = relative_path.parent().map_or(true, |parent| { snapshot
snapshot .entry_for_path(parent)
.entry_for_path(parent) .map_or(false, |entry| entry.kind == EntryKind::Dir)
.map_or(false, |entry| entry.kind == EntryKind::Dir) });
}); if !parent_dir_is_loaded {
if !parent_dir_is_loaded { log::debug!("ignoring event {relative_path:?} within unloaded directory");
log::debug!("ignoring event {relative_path:?} within unloaded directory"); return false;
return false; }
// FS events may come for files which parent directory is excluded, need to check ignore those.
let mut path_to_test = abs_path.clone();
let mut excluded_file_event = snapshot.is_path_excluded(abs_path)
|| snapshot.is_path_excluded(&relative_path);
while !excluded_file_event && path_to_test.pop() {
if snapshot.is_path_excluded(&path_to_test) {
excluded_file_event = true;
} }
if snapshot.is_abs_path_excluded(abs_path) { }
log::debug!( if excluded_file_event {
"ignoring FS event for path {relative_path:?} within excluded directory" if !is_git_related {
); log::debug!("ignoring FS event for excluded path {relative_path:?}");
return false;
} }
return false;
} }
relative_paths.push(relative_path); relative_paths.push(relative_path);
@ -3341,31 +3350,39 @@ impl BackgroundScanner {
} }
}); });
if relative_paths.is_empty() { if dot_git_paths_to_reload.is_empty() && relative_paths.is_empty() {
return; return;
} }
log::debug!("received fs events {:?}", relative_paths); if !relative_paths.is_empty() {
log::debug!("received fs events {:?}", relative_paths);
let (scan_job_tx, scan_job_rx) = channel::unbounded(); let (scan_job_tx, scan_job_rx) = channel::unbounded();
self.reload_entries_for_paths( self.reload_entries_for_paths(
root_path, root_path,
root_canonical_path, root_canonical_path,
&relative_paths, &relative_paths,
abs_paths, abs_paths,
Some(scan_job_tx.clone()), Some(scan_job_tx.clone()),
) )
.await; .await;
drop(scan_job_tx); drop(scan_job_tx);
self.scan_dirs(false, scan_job_rx).await; self.scan_dirs(false, scan_job_rx).await;
let (scan_job_tx, scan_job_rx) = channel::unbounded(); let (scan_job_tx, scan_job_rx) = channel::unbounded();
self.update_ignore_statuses(scan_job_tx).await; self.update_ignore_statuses(scan_job_tx).await;
self.scan_dirs(false, scan_job_rx).await; self.scan_dirs(false, scan_job_rx).await;
}
{ {
let mut state = self.state.lock(); let mut state = self.state.lock();
state.reload_repositories(&relative_paths, self.fs.as_ref()); if !dot_git_paths_to_reload.is_empty() {
if relative_paths.is_empty() {
state.snapshot.scan_id += 1;
}
log::debug!("reloading repositories: {dot_git_paths_to_reload:?}");
state.reload_repositories(&dot_git_paths_to_reload, self.fs.as_ref());
}
state.snapshot.completed_scan_id = state.snapshot.scan_id; state.snapshot.completed_scan_id = state.snapshot.scan_id;
for (_, entry_id) in mem::take(&mut state.removed_entry_ids) { for (_, entry_id) in mem::take(&mut state.removed_entry_ids) {
state.scanned_dirs.remove(&entry_id); state.scanned_dirs.remove(&entry_id);
@ -3505,7 +3522,7 @@ impl BackgroundScanner {
let state = self.state.lock(); let state = self.state.lock();
let snapshot = &state.snapshot; let snapshot = &state.snapshot;
root_abs_path = snapshot.abs_path().clone(); root_abs_path = snapshot.abs_path().clone();
if snapshot.is_abs_path_excluded(&job.abs_path) { if snapshot.is_path_excluded(&job.abs_path) {
log::error!("skipping excluded directory {:?}", job.path); log::error!("skipping excluded directory {:?}", job.path);
return Ok(()); return Ok(());
} }
@ -3577,7 +3594,7 @@ impl BackgroundScanner {
{ {
let mut state = self.state.lock(); let mut state = self.state.lock();
if state.snapshot.is_abs_path_excluded(&child_abs_path) { if state.snapshot.is_path_excluded(&child_abs_path) {
let relative_path = job.path.join(child_name); let relative_path = job.path.join(child_name);
log::debug!("skipping excluded child entry {relative_path:?}"); log::debug!("skipping excluded child entry {relative_path:?}");
state.remove_path(&relative_path); state.remove_path(&relative_path);
@ -4119,12 +4136,6 @@ impl BackgroundScanner {
} }
} }
fn is_git_related(abs_path: &Path) -> bool {
abs_path
.components()
.any(|c| c.as_os_str() == *DOT_GIT || c.as_os_str() == *GITIGNORE)
}
fn char_bag_for_path(root_char_bag: CharBag, path: &Path) -> CharBag { fn char_bag_for_path(root_char_bag: CharBag, path: &Path) -> CharBag {
let mut result = root_char_bag; let mut result = root_char_bag;
result.extend( result.extend(

View File

@ -992,6 +992,146 @@ async fn test_file_scan_exclusions(cx: &mut TestAppContext) {
}); });
} }
#[gpui::test]
async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) {
init_test(cx);
cx.executor().allow_parking();
let dir = temp_tree(json!({
".git": {
"HEAD": "ref: refs/heads/main\n",
"foo": "bar",
},
".gitignore": "**/target\n/node_modules\ntest_output\n",
"target": {
"index": "blah2"
},
"node_modules": {
".DS_Store": "",
"prettier": {
"package.json": "{}",
},
},
"src": {
".DS_Store": "",
"foo": {
"foo.rs": "mod another;\n",
"another.rs": "// another",
},
"bar": {
"bar.rs": "// bar",
},
"lib.rs": "mod foo;\nmod bar;\n",
},
".DS_Store": "",
}));
cx.update(|cx| {
cx.update_global::<SettingsStore, _>(|store, cx| {
store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
project_settings.file_scan_exclusions = Some(vec![
"**/.git".to_string(),
"node_modules/".to_string(),
"build_output".to_string(),
]);
});
});
});
let tree = Worktree::local(
build_client(cx),
dir.path(),
true,
Arc::new(RealFs),
Default::default(),
&mut cx.to_async(),
)
.await
.unwrap();
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
.await;
tree.flush_fs_events(cx).await;
tree.read_with(cx, |tree, _| {
check_worktree_entries(
tree,
&[
".git/HEAD",
".git/foo",
"node_modules/.DS_Store",
"node_modules/prettier",
"node_modules/prettier/package.json",
],
&["target", "node_modules"],
&[
".DS_Store",
"src/.DS_Store",
"src/lib.rs",
"src/foo/foo.rs",
"src/foo/another.rs",
"src/bar/bar.rs",
".gitignore",
],
)
});
let new_excluded_dir = dir.path().join("build_output");
let new_ignored_dir = dir.path().join("test_output");
std::fs::create_dir_all(&new_excluded_dir)
.unwrap_or_else(|e| panic!("Failed to create a {new_excluded_dir:?} directory: {e}"));
std::fs::create_dir_all(&new_ignored_dir)
.unwrap_or_else(|e| panic!("Failed to create a {new_ignored_dir:?} directory: {e}"));
let node_modules_dir = dir.path().join("node_modules");
let dot_git_dir = dir.path().join(".git");
let src_dir = dir.path().join("src");
for existing_dir in [&node_modules_dir, &dot_git_dir, &src_dir] {
assert!(
existing_dir.is_dir(),
"Expect {existing_dir:?} to be present in the FS already"
);
}
for directory_for_new_file in [
new_excluded_dir,
new_ignored_dir,
node_modules_dir,
dot_git_dir,
src_dir,
] {
std::fs::write(directory_for_new_file.join("new_file"), "new file contents")
.unwrap_or_else(|e| {
panic!("Failed to create in {directory_for_new_file:?} a new file: {e}")
});
}
tree.flush_fs_events(cx).await;
tree.read_with(cx, |tree, _| {
check_worktree_entries(
tree,
&[
".git/HEAD",
".git/foo",
".git/new_file",
"node_modules/.DS_Store",
"node_modules/prettier",
"node_modules/prettier/package.json",
"node_modules/new_file",
"build_output",
"build_output/new_file",
"test_output/new_file",
],
&["target", "node_modules", "test_output"],
&[
".DS_Store",
"src/.DS_Store",
"src/lib.rs",
"src/foo/foo.rs",
"src/foo/another.rs",
"src/bar/bar.rs",
"src/new_file",
".gitignore",
],
)
});
}
#[gpui::test(iterations = 30)] #[gpui::test(iterations = 30)]
async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) { async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
init_test(cx); init_test(cx);
@ -1056,7 +1196,7 @@ async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) { async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
init_test(cx); init_test(cx);
cx.executor().allow_parking(); cx.executor().allow_parking();
let client_fake = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); let client_fake = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
let fs_fake = FakeFs::new(cx.background_executor.clone()); let fs_fake = FakeFs::new(cx.background_executor.clone());
fs_fake fs_fake
@ -1096,7 +1236,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
assert!(tree.entry_for_path("a/b/").unwrap().is_dir()); assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
}); });
let client_real = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); let client_real = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
let fs_real = Arc::new(RealFs); let fs_real = Arc::new(RealFs);
let temp_root = temp_tree(json!({ let temp_root = temp_tree(json!({
@ -2181,7 +2321,7 @@ async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
fn build_client(cx: &mut TestAppContext) -> Arc<Client> { fn build_client(cx: &mut TestAppContext) -> Arc<Client> {
let http_client = FakeHttpClient::with_404_response(); let http_client = FakeHttpClient::with_404_response();
cx.read(|cx| Client::new(http_client, cx)) cx.update(|cx| Client::new(http_client, cx))
} }
#[track_caller] #[track_caller]

View File

@ -10,8 +10,8 @@ use anyhow::{anyhow, Result};
use gpui::{ use gpui::{
actions, div, px, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext, actions, div, px, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext,
ClipboardItem, Div, EventEmitter, FocusHandle, Focusable, FocusableView, InteractiveElement, ClipboardItem, Div, EventEmitter, FocusHandle, Focusable, FocusableView, InteractiveElement,
Model, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render, IntoElement, Model, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel,
RenderOnce, Stateful, StatefulInteractiveElement, Styled, Task, UniformListScrollHandle, View, Render, Stateful, StatefulInteractiveElement, Styled, Task, UniformListScrollHandle, View,
ViewContext, VisualContext as _, WeakView, WindowContext, ViewContext, VisualContext as _, WeakView, WindowContext,
}; };
use menu::{Confirm, SelectNext, SelectPrev}; use menu::{Confirm, SelectNext, SelectPrev};
@ -371,7 +371,7 @@ impl ProjectPanel {
_entry_id: ProjectEntryId, _entry_id: ProjectEntryId,
_cx: &mut ViewContext<Self>, _cx: &mut ViewContext<Self>,
) { ) {
todo!() // todo!()
// let project = self.project.read(cx); // let project = self.project.read(cx);
// let worktree_id = if let Some(id) = project.worktree_id_for_entry(entry_id, cx) { // let worktree_id = if let Some(id) = project.worktree_id_for_entry(entry_id, cx) {
@ -644,6 +644,7 @@ impl ProjectPanel {
} }
fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) { fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
dbg!("odd");
self.edit_state = None; self.edit_state = None;
self.update_visible_entries(None, cx); self.update_visible_entries(None, cx);
cx.focus(&self.focus_handle); cx.focus(&self.focus_handle);

View File

@ -1767,16 +1767,13 @@ impl View for ProjectSearchBar {
render_option_button_icon("icons/word_search.svg", SearchOptions::WHOLE_WORD, cx) render_option_button_icon("icons/word_search.svg", SearchOptions::WHOLE_WORD, cx)
}); });
let mut include_ignored = is_semantic_disabled.then(|| { let include_ignored = is_semantic_disabled.then(|| {
render_option_button_icon( render_option_button_icon(
// TODO proper icon "icons/file_icons/git.svg",
"icons/case_insensitive.svg",
SearchOptions::INCLUDE_IGNORED, SearchOptions::INCLUDE_IGNORED,
cx, cx,
) )
}); });
// TODO not implemented yet
let _ = include_ignored.take();
let search_button_for_mode = |mode, side, cx: &mut ViewContext<ProjectSearchBar>| { let search_button_for_mode = |mode, side, cx: &mut ViewContext<ProjectSearchBar>| {
let is_active = if let Some(search) = self.active_project_search.as_ref() { let is_active = if let Some(search) = self.active_project_search.as_ref() {

View File

@ -7,12 +7,12 @@ use crate::{
ToggleCaseSensitive, ToggleReplace, ToggleWholeWord, ToggleCaseSensitive, ToggleReplace, ToggleWholeWord,
}; };
use collections::HashMap; use collections::HashMap;
use editor::Editor; use editor::{Editor, EditorMode};
use futures::channel::oneshot; use futures::channel::oneshot;
use gpui::{ use gpui::{
actions, div, red, Action, AppContext, Div, EventEmitter, InteractiveElement as _, actions, div, red, Action, AppContext, Div, EventEmitter, InteractiveElement as _, IntoElement,
ParentElement as _, Render, RenderOnce, Styled, Subscription, Task, View, ViewContext, ParentElement as _, Render, Styled, Subscription, Task, View, ViewContext, VisualContext as _,
VisualContext as _, WindowContext, WeakView, WindowContext,
}; };
use project::search::SearchQuery; use project::search::SearchQuery;
use serde::Deserialize; use serde::Deserialize;
@ -23,7 +23,7 @@ use util::ResultExt;
use workspace::{ use workspace::{
item::ItemHandle, item::ItemHandle,
searchable::{Direction, SearchEvent, SearchableItemHandle, WeakSearchableItemHandle}, searchable::{Direction, SearchEvent, SearchableItemHandle, WeakSearchableItemHandle},
ToolbarItemLocation, ToolbarItemView, Workspace, ToolbarItemLocation, ToolbarItemView,
}; };
#[derive(PartialEq, Clone, Deserialize, Default, Action)] #[derive(PartialEq, Clone, Deserialize, Default, Action)]
@ -38,7 +38,7 @@ pub enum Event {
} }
pub fn init(cx: &mut AppContext) { pub fn init(cx: &mut AppContext) {
cx.observe_new_views(|workspace: &mut Workspace, _| BufferSearchBar::register(workspace)) cx.observe_new_views(|editor: &mut Editor, cx| BufferSearchBar::register(editor, cx))
.detach(); .detach();
} }
@ -187,6 +187,7 @@ impl Render for BufferSearchBar {
}) })
.on_action(cx.listener(Self::previous_history_query)) .on_action(cx.listener(Self::previous_history_query))
.on_action(cx.listener(Self::next_history_query)) .on_action(cx.listener(Self::next_history_query))
.on_action(cx.listener(Self::dismiss))
.w_full() .w_full()
.p_1() .p_1()
.child( .child(
@ -294,9 +295,19 @@ impl ToolbarItemView for BufferSearchBar {
} }
impl BufferSearchBar { impl BufferSearchBar {
pub fn register(workspace: &mut Workspace) { pub fn register(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
workspace.register_action(|workspace, a: &Deploy, cx| { if editor.mode() != EditorMode::Full {
workspace.active_pane().update(cx, |this, cx| { return;
};
let handle = cx.view().downgrade();
editor.register_action(move |a: &Deploy, cx| {
let Some(pane) = handle.upgrade().and_then(|editor| editor.read(cx).pane(cx)) else {
return;
};
pane.update(cx, |this, cx| {
this.toolbar().update(cx, |this, cx| { this.toolbar().update(cx, |this, cx| {
if let Some(search_bar) = this.item_of_type::<BufferSearchBar>() { if let Some(search_bar) = this.item_of_type::<BufferSearchBar>() {
search_bar.update(cx, |this, cx| { search_bar.update(cx, |this, cx| {
@ -316,11 +327,16 @@ impl BufferSearchBar {
}); });
}); });
fn register_action<A: Action>( fn register_action<A: Action>(
workspace: &mut Workspace, editor: &mut Editor,
handle: WeakView<Editor>,
update: fn(&mut BufferSearchBar, &A, &mut ViewContext<BufferSearchBar>), update: fn(&mut BufferSearchBar, &A, &mut ViewContext<BufferSearchBar>),
) { ) {
workspace.register_action(move |workspace, action: &A, cx| { editor.register_action(move |action: &A, cx| {
workspace.active_pane().update(cx, move |this, cx| { let Some(pane) = handle.upgrade().and_then(|editor| editor.read(cx).pane(cx))
else {
return;
};
pane.update(cx, move |this, cx| {
this.toolbar().update(cx, move |this, cx| { this.toolbar().update(cx, move |this, cx| {
if let Some(search_bar) = this.item_of_type::<BufferSearchBar>() { if let Some(search_bar) = this.item_of_type::<BufferSearchBar>() {
search_bar.update(cx, move |this, cx| update(this, action, cx)); search_bar.update(cx, move |this, cx| update(this, action, cx));
@ -331,49 +347,76 @@ impl BufferSearchBar {
}); });
} }
register_action(workspace, |this, action: &ToggleCaseSensitive, cx| { let handle = cx.view().downgrade();
if this.supported_options().case { register_action(
this.toggle_case_sensitive(action, cx); editor,
} handle.clone(),
}); |this, action: &ToggleCaseSensitive, cx| {
register_action(workspace, |this, action: &ToggleWholeWord, cx| { if this.supported_options().case {
if this.supported_options().word { this.toggle_case_sensitive(action, cx);
this.toggle_whole_word(action, cx); }
} },
}); );
register_action(workspace, |this, action: &ToggleReplace, cx| { register_action(
if this.supported_options().replacement { editor,
this.toggle_replace(action, cx); handle.clone(),
} |this, action: &ToggleWholeWord, cx| {
}); if this.supported_options().word {
register_action(workspace, |this, _: &ActivateRegexMode, cx| { this.toggle_whole_word(action, cx);
}
},
);
register_action(
editor,
handle.clone(),
|this, action: &ToggleReplace, cx| {
if this.supported_options().replacement {
this.toggle_replace(action, cx);
}
},
);
register_action(editor, handle.clone(), |this, _: &ActivateRegexMode, cx| {
if this.supported_options().regex { if this.supported_options().regex {
this.activate_search_mode(SearchMode::Regex, cx); this.activate_search_mode(SearchMode::Regex, cx);
} }
}); });
register_action(workspace, |this, _: &ActivateTextMode, cx| { register_action(editor, handle.clone(), |this, _: &ActivateTextMode, cx| {
this.activate_search_mode(SearchMode::Text, cx); this.activate_search_mode(SearchMode::Text, cx);
}); });
register_action(workspace, |this, action: &CycleMode, cx| { register_action(editor, handle.clone(), |this, action: &CycleMode, cx| {
if this.supported_options().regex { if this.supported_options().regex {
// If regex is not supported then search has just one mode (text) - in that case there's no point in supporting // If regex is not supported then search has just one mode (text) - in that case there's no point in supporting
// cycling. // cycling.
this.cycle_mode(action, cx) this.cycle_mode(action, cx)
} }
}); });
register_action(workspace, |this, action: &SelectNextMatch, cx| { register_action(
this.select_next_match(action, cx); editor,
}); handle.clone(),
register_action(workspace, |this, action: &SelectPrevMatch, cx| { |this, action: &SelectNextMatch, cx| {
this.select_prev_match(action, cx); this.select_next_match(action, cx);
}); },
register_action(workspace, |this, action: &SelectAllMatches, cx| { );
this.select_all_matches(action, cx); register_action(
}); editor,
register_action(workspace, |this, _: &editor::Cancel, cx| { handle.clone(),
|this, action: &SelectPrevMatch, cx| {
this.select_prev_match(action, cx);
},
);
register_action(
editor,
handle.clone(),
|this, action: &SelectAllMatches, cx| {
this.select_all_matches(action, cx);
},
);
register_action(editor, handle.clone(), |this, _: &editor::Cancel, cx| {
if !this.dismissed { if !this.dismissed {
this.dismiss(&Dismiss, cx); this.dismiss(&Dismiss, cx);
return;
} }
cx.propagate();
}); });
} }
pub fn new(cx: &mut ViewContext<Self>) -> Self { pub fn new(cx: &mut ViewContext<Self>) -> Self {
@ -538,7 +581,7 @@ impl BufferSearchBar {
self.update_matches(cx) self.update_matches(cx)
} }
fn render_action_button(&self) -> impl RenderOnce { fn render_action_button(&self) -> impl IntoElement {
// let tooltip_style = theme.tooltip.clone(); // let tooltip_style = theme.tooltip.clone();
// let style = theme.search.action_button.clone(); // let style = theme.search.action_button.clone();

View File

@ -85,6 +85,7 @@ pub fn init(cx: &mut AppContext) {
cx.capture_action(ProjectSearchView::replace_next); cx.capture_action(ProjectSearchView::replace_next);
add_toggle_option_action::<ToggleCaseSensitive>(SearchOptions::CASE_SENSITIVE, cx); add_toggle_option_action::<ToggleCaseSensitive>(SearchOptions::CASE_SENSITIVE, cx);
add_toggle_option_action::<ToggleWholeWord>(SearchOptions::WHOLE_WORD, cx); add_toggle_option_action::<ToggleWholeWord>(SearchOptions::WHOLE_WORD, cx);
add_toggle_option_action::<ToggleIncludeIgnored>(SearchOptions::INCLUDE_IGNORED, cx);
add_toggle_filters_action::<ToggleFilters>(cx); add_toggle_filters_action::<ToggleFilters>(cx);
} }
@ -1192,6 +1193,7 @@ impl ProjectSearchView {
text, text,
self.search_options.contains(SearchOptions::WHOLE_WORD), self.search_options.contains(SearchOptions::WHOLE_WORD),
self.search_options.contains(SearchOptions::CASE_SENSITIVE), self.search_options.contains(SearchOptions::CASE_SENSITIVE),
self.search_options.contains(SearchOptions::INCLUDE_IGNORED),
included_files, included_files,
excluded_files, excluded_files,
) { ) {
@ -1210,6 +1212,7 @@ impl ProjectSearchView {
text, text,
self.search_options.contains(SearchOptions::WHOLE_WORD), self.search_options.contains(SearchOptions::WHOLE_WORD),
self.search_options.contains(SearchOptions::CASE_SENSITIVE), self.search_options.contains(SearchOptions::CASE_SENSITIVE),
self.search_options.contains(SearchOptions::INCLUDE_IGNORED),
included_files, included_files,
excluded_files, excluded_files,
) { ) {
@ -1764,6 +1767,14 @@ impl View for ProjectSearchBar {
render_option_button_icon("icons/word_search.svg", SearchOptions::WHOLE_WORD, cx) render_option_button_icon("icons/word_search.svg", SearchOptions::WHOLE_WORD, cx)
}); });
let include_ignored = is_semantic_disabled.then(|| {
render_option_button_icon(
"icons/file_icons/git.svg",
SearchOptions::INCLUDE_IGNORED,
cx,
)
});
let search_button_for_mode = |mode, side, cx: &mut ViewContext<ProjectSearchBar>| { let search_button_for_mode = |mode, side, cx: &mut ViewContext<ProjectSearchBar>| {
let is_active = if let Some(search) = self.active_project_search.as_ref() { let is_active = if let Some(search) = self.active_project_search.as_ref() {
let search = search.read(cx); let search = search.read(cx);
@ -1879,7 +1890,15 @@ impl View for ProjectSearchBar {
.with_children(search.filters_enabled.then(|| { .with_children(search.filters_enabled.then(|| {
Flex::row() Flex::row()
.with_child( .with_child(
ChildView::new(&search.included_files_editor, cx) Flex::row()
.with_child(
ChildView::new(&search.included_files_editor, cx)
.contained()
.constrained()
.with_height(theme.search.search_bar_row_height)
.flex(1., true),
)
.with_children(include_ignored)
.contained() .contained()
.with_style(include_container_style) .with_style(include_container_style)
.constrained() .constrained()

View File

@ -1,6 +1,6 @@
use bitflags::bitflags; use bitflags::bitflags;
pub use buffer_search::BufferSearchBar; pub use buffer_search::BufferSearchBar;
use gpui::{actions, Action, AppContext, RenderOnce}; use gpui::{actions, Action, AppContext, IntoElement};
pub use mode::SearchMode; pub use mode::SearchMode;
use project::search::SearchQuery; use project::search::SearchQuery;
use ui::ButtonVariant; use ui::ButtonVariant;
@ -82,7 +82,7 @@ impl SearchOptions {
options options
} }
pub fn as_button(&self, active: bool) -> impl RenderOnce { pub fn as_button(&self, active: bool) -> impl IntoElement {
ui::IconButton::new(0, self.icon()) ui::IconButton::new(0, self.icon())
.on_click({ .on_click({
let action = self.to_toggle_action(); let action = self.to_toggle_action();
@ -95,7 +95,7 @@ impl SearchOptions {
} }
} }
fn toggle_replace_button(active: bool) -> impl RenderOnce { fn toggle_replace_button(active: bool) -> impl IntoElement {
// todo: add toggle_replace button // todo: add toggle_replace button
ui::IconButton::new(0, ui::Icon::Replace) ui::IconButton::new(0, ui::Icon::Replace)
.on_click(|_, cx| { .on_click(|_, cx| {
@ -109,7 +109,7 @@ fn toggle_replace_button(active: bool) -> impl RenderOnce {
fn render_replace_button( fn render_replace_button(
action: impl Action + 'static + Send + Sync, action: impl Action + 'static + Send + Sync,
icon: ui::Icon, icon: ui::Icon,
) -> impl RenderOnce { ) -> impl IntoElement {
// todo: add tooltip // todo: add tooltip
ui::IconButton::new(0, icon).on_click(move |_, cx| { ui::IconButton::new(0, icon).on_click(move |_, cx| {
cx.dispatch_action(action.boxed_clone()); cx.dispatch_action(action.boxed_clone());

View File

@ -1,4 +1,4 @@
use gpui::{MouseDownEvent, RenderOnce, WindowContext}; use gpui::{IntoElement, MouseDownEvent, WindowContext};
use ui::{Button, ButtonVariant, IconButton}; use ui::{Button, ButtonVariant, IconButton};
use crate::mode::SearchMode; use crate::mode::SearchMode;
@ -7,7 +7,7 @@ pub(super) fn render_nav_button(
icon: ui::Icon, icon: ui::Icon,
_active: bool, _active: bool,
on_click: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static, on_click: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
) -> impl RenderOnce { ) -> impl IntoElement {
// let tooltip_style = cx.theme().tooltip.clone(); // let tooltip_style = cx.theme().tooltip.clone();
// let cursor_style = if active { // let cursor_style = if active {
// CursorStyle::PointingHand // CursorStyle::PointingHand

View File

@ -1659,13 +1659,13 @@ fn elixir_lang() -> Arc<Language> {
target: (identifier) @name) target: (identifier) @name)
operator: "when") operator: "when")
]) ])
(#match? @name "^(def|defp|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp)$")) @item (#any-match? @name "^(def|defp|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp)$")) @item
) )
(call (call
target: (identifier) @name target: (identifier) @name
(arguments (alias) @name) (arguments (alias) @name)
(#match? @name "^(defmodule|defprotocol)$")) @item (#any-match? @name "^(defmodule|defprotocol)$")) @item
"#, "#,
) )
.unwrap(), .unwrap(),

10
crates/story/Cargo.toml Normal file
View 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
View File

@ -0,0 +1,3 @@
mod story;
pub use story::*;

35
crates/story/src/story.rs Normal file
View 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())
}
}

View File

@ -25,6 +25,7 @@ serde.workspace = true
settings2 = { path = "../settings2" } settings2 = { path = "../settings2" }
simplelog = "0.9" simplelog = "0.9"
smallvec.workspace = true smallvec.workspace = true
story = { path = "../story" }
strum = { version = "0.25.0", features = ["derive"] } strum = { version = "0.25.0", features = ["derive"] }
theme = { path = "../theme" } theme = { path = "../theme" }
theme2 = { path = "../theme2" } theme2 = { path = "../theme2" }

View File

@ -1,4 +1,3 @@
mod colors;
mod focus; mod focus;
mod kitchen_sink; mod kitchen_sink;
mod picker; mod picker;
@ -6,7 +5,6 @@ mod scroll;
mod text; mod text;
mod z_index; mod z_index;
pub use colors::*;
pub use focus::*; pub use focus::*;
pub use kitchen_sink::*; pub use kitchen_sink::*;
pub use picker::*; pub use picker::*;

View File

@ -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))
})),
)
})),
)
}
}

View File

@ -26,7 +26,7 @@ impl FocusStory {
} }
} }
impl Render<Self> for FocusStory { impl Render for FocusStory {
type Element = Focusable<Stateful<Div>>; type Element = Focusable<Stateful<Div>>;
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element { fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element {
@ -52,10 +52,8 @@ impl Render<Self> for FocusStory {
.on_blur(cx.listener(|_, _, _| println!("Parent blurred"))) .on_blur(cx.listener(|_, _, _| println!("Parent blurred")))
.on_focus_in(cx.listener(|_, _, _| println!("Parent focus_in"))) .on_focus_in(cx.listener(|_, _, _| println!("Parent focus_in")))
.on_focus_out(cx.listener(|_, _, _| println!("Parent focus_out"))) .on_focus_out(cx.listener(|_, _, _| println!("Parent focus_out")))
.on_key_down( .on_key_down(cx.listener(|_, event, _| println!("Key down on parent {:?}", event)))
cx.listener(|_, event, phase, _| println!("Key down on parent {:?}", event)), .on_key_up(cx.listener(|_, event, _| println!("Key up on parent {:?}", event)))
)
.on_key_up(cx.listener(|_, event, phase, _| println!("Key up on parent {:?}", event)))
.size_full() .size_full()
.bg(color_1) .bg(color_1)
.focus(|style| style.bg(color_2)) .focus(|style| style.bg(color_2))

View File

@ -1,8 +1,10 @@
use crate::{story::Story, story_selector::ComponentStory};
use gpui::{prelude::*, Div, Render, Stateful, View}; use gpui::{prelude::*, Div, Render, Stateful, View};
use story::Story;
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
use ui::prelude::*; use ui::prelude::*;
use crate::story_selector::ComponentStory;
pub struct KitchenSinkStory; pub struct KitchenSinkStory;
impl KitchenSinkStory { impl KitchenSinkStory {
@ -19,11 +21,11 @@ impl Render for KitchenSinkStory {
.map(|selector| selector.story(cx)) .map(|selector| selector.story(cx))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
Story::container(cx) Story::container()
.id("kitchen-sink") .id("kitchen-sink")
.overflow_y_scroll() .overflow_y_scroll()
.child(Story::title(cx, "Kitchen Sink")) .child(Story::title("Kitchen Sink"))
.child(Story::label(cx, "Components")) .child(Story::label("Components"))
.child(div().flex().flex_col().children(component_stories)) .child(div().flex().flex_col().children(component_stories))
// Add a bit of space at the bottom of the kitchen sink so elements // Add a bit of space at the bottom of the kitchen sink so elements
// don't end up squished right up against the bottom of the screen. // don't end up squished right up against the bottom of the screen.

View File

@ -36,7 +36,7 @@ impl Delegate {
} }
impl PickerDelegate for Delegate { impl PickerDelegate for Delegate {
type ListItem = Div<Picker<Self>>; type ListItem = Div;
fn match_count(&self) -> usize { fn match_count(&self) -> usize {
self.candidates.len() self.candidates.len()
@ -205,8 +205,8 @@ impl PickerStory {
} }
} }
impl Render<Self> for PickerStory { impl Render for PickerStory {
type Element = Div<Self>; type Element = Div;
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element { fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element {
div() div()

View File

@ -10,8 +10,8 @@ impl ScrollStory {
} }
} }
impl Render<Self> for ScrollStory { impl Render for ScrollStory {
type Element = Stateful<Self, Div<Self>>; type Element = Stateful<Div>;
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element { fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element {
let theme = cx.theme(); let theme = cx.theme();
@ -38,7 +38,7 @@ impl Render<Self> for ScrollStory {
}; };
div() div()
.id(id) .id(id)
.tooltip(move |_, cx| Tooltip::text(format!("{}, {}", row, column), cx)) .tooltip(move |cx| Tooltip::text(format!("{}, {}", row, column), cx))
.bg(bg) .bg(bg)
.size(px(100. as f32)) .size(px(100. as f32))
.when(row >= 5 && column >= 5, |d| { .when(row >= 5 && column >= 5, |d| {

View File

@ -1,5 +1,6 @@
use gpui::{ use gpui::{
blue, div, red, white, Div, ParentElement, Render, Styled, View, VisualContext, WindowContext, blue, div, green, red, white, Div, InteractiveText, ParentElement, Render, Styled, StyledText,
TextRun, View, VisualContext, WindowContext,
}; };
use ui::v_stack; use ui::v_stack;
@ -55,6 +56,21 @@ impl Render for TextStory {
"flex-row. width 96. The quick brown fox jumps over the lazy dog. ", "flex-row. width 96. The quick brown fox jumps over the lazy dog. ",
"Meanwhile, the lazy dog decided it was time for a change. ", "Meanwhile, the lazy dog decided it was time for a change. ",
"He started daily workout routines, ate healthier and became the fastest dog in town.", "He started daily workout routines, ate healthier and became the fastest dog in town.",
))) ))).child(
InteractiveText::new(
"interactive",
StyledText::new("Hello world, how is it going?").with_runs(vec![
cx.text_style().to_run(6),
TextRun {
background_color: Some(green()),
..cx.text_style().to_run(5)
},
cx.text_style().to_run(18),
]),
)
.on_click(vec![2..4, 1..3, 7..9], |range_ix, cx| {
println!("Clicked range {range_ix}");
})
)
} }
} }

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