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
shell: bash -euxo pipefail {0}
run: script/clear-target-dir-if-larger-than 70
run: script/clear-target-dir-if-larger-than 100
- name: Run check
env:
RUSTFLAGS: -D warnings
shell: bash -euxo pipefail {0}
run: cargo check --tests --workspace
- name: Run tests
env:
RUSTFLAGS: -D warnings
shell: bash -euxo pipefail {0}
run: cargo nextest run --workspace --no-fail-fast

View File

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

View File

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

75
Cargo.lock generated
View File

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

View File

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

4
Procfile.zed2 Normal file
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
},
// The default font size for text in the UI
"ui_font_size": 14,
"ui_font_size": 16,
// The factor to grow the active pane by. Defaults to 1.0
// which gives the same size as all other panes.
"active_pane_magnification": 1.0,

View File

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

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 workspace::notifications::NotificationEvent;
pub struct UpdateNotification {
_version: SemanticVersion,
}
impl EventEmitter<NotificationEvent> for UpdateNotification {}
impl EventEmitter<DismissEvent> for UpdateNotification {}
impl Render for UpdateNotification {
type Element = Div;
@ -82,6 +83,6 @@ impl UpdateNotification {
}
pub fn _dismiss(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
cx.emit(NotificationEvent::Dismiss);
cx.emit(DismissEvent::Dismiss);
}
}

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

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]
async fn go_to_prev_overlapping_diagnostic(
executor: BackgroundExecutor,

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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)]
async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
init_test(cx);

View File

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

View File

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

View File

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

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)]
async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
init_test(cx);
@ -1056,7 +1196,7 @@ async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
init_test(cx);
cx.executor().allow_parking();
let client_fake = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
let client_fake = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
let fs_fake = FakeFs::new(cx.background_executor.clone());
fs_fake
@ -1096,7 +1236,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
});
let client_real = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
let client_real = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
let fs_real = Arc::new(RealFs);
let temp_root = temp_tree(json!({
@ -2181,7 +2321,7 @@ async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
fn build_client(cx: &mut TestAppContext) -> Arc<Client> {
let http_client = FakeHttpClient::with_404_response();
cx.read(|cx| Client::new(http_client, cx))
cx.update(|cx| Client::new(http_client, cx))
}
#[track_caller]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

10
crates/story/Cargo.toml Normal file
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" }
simplelog = "0.9"
smallvec.workspace = true
story = { path = "../story" }
strum = { version = "0.25.0", features = ["derive"] }
theme = { path = "../theme" }
theme2 = { path = "../theme2" }

View File

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

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

View File

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

View File

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

View File

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

View File

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

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