Merge branch 'main' into instrument-keyboard-events
This commit is contained in:
commit
b26a468820
188 changed files with 3391 additions and 1997 deletions
83
Cargo.lock
generated
83
Cargo.lock
generated
|
@ -292,6 +292,15 @@ version = "1.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16"
|
||||
|
||||
[[package]]
|
||||
name = "assets"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"gpui",
|
||||
"rust-embed",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "assistant"
|
||||
version = "0.1.0"
|
||||
|
@ -1443,7 +1452,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "collab"
|
||||
version = "0.34.0"
|
||||
version = "0.36.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
|
@ -1464,6 +1473,7 @@ dependencies = [
|
|||
"editor",
|
||||
"env_logger",
|
||||
"envy",
|
||||
"file_finder",
|
||||
"fs",
|
||||
"futures 0.3.28",
|
||||
"git",
|
||||
|
@ -1477,6 +1487,7 @@ dependencies = [
|
|||
"live_kit_server",
|
||||
"log",
|
||||
"lsp",
|
||||
"menu",
|
||||
"nanoid",
|
||||
"node_runtime",
|
||||
"notifications",
|
||||
|
@ -1550,6 +1561,7 @@ dependencies = [
|
|||
"serde_json",
|
||||
"settings",
|
||||
"smallvec",
|
||||
"story",
|
||||
"theme",
|
||||
"theme_selector",
|
||||
"time",
|
||||
|
@ -3428,6 +3440,40 @@ dependencies = [
|
|||
"tiff",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "include-flate"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2e11569346406931d20276cc460215ee2826e7cad43aa986999cb244dd7adb0"
|
||||
dependencies = [
|
||||
"include-flate-codegen-exports",
|
||||
"lazy_static",
|
||||
"libflate",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "include-flate-codegen"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a7d6e1419fa3129eb0802b4c99603c0d425c79fb5d76191d5a20d0ab0d664e8"
|
||||
dependencies = [
|
||||
"libflate",
|
||||
"proc-macro-hack",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "include-flate-codegen-exports"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75657043ffe3d8280f1cb8aef0f505532b392ed7758e0baeac22edadcee31a03"
|
||||
dependencies = [
|
||||
"include-flate-codegen",
|
||||
"proc-macro-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.9.3"
|
||||
|
@ -3819,6 +3865,26 @@ version = "0.2.148"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b"
|
||||
|
||||
[[package]]
|
||||
name = "libflate"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ff4ae71b685bbad2f2f391fe74f6b7659a34871c08b210fdc039e43bee07d18"
|
||||
dependencies = [
|
||||
"adler32",
|
||||
"crc32fast",
|
||||
"libflate_lz77",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libflate_lz77"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a52d3a8bfc85f250440e4424db7d857e241a3aebbbe301f3eb606ab15c39acbf"
|
||||
dependencies = [
|
||||
"rle-decode-fast",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libgit2-sys"
|
||||
version = "0.14.2+1.5.1"
|
||||
|
@ -5396,6 +5462,12 @@ dependencies = [
|
|||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-hack"
|
||||
version = "0.5.20+deprecated"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.67"
|
||||
|
@ -6090,6 +6162,12 @@ dependencies = [
|
|||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rle-decode-fast"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3582f63211428f83597b51b2ddb88e2a91a9d52d12831f9d08f5e624e8977422"
|
||||
|
||||
[[package]]
|
||||
name = "rmp"
|
||||
version = "0.8.12"
|
||||
|
@ -6237,6 +6315,7 @@ version = "8.0.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1e7d90385b59f0a6bf3d3b757f3ca4ece2048265d70db20a2016043d4509a40"
|
||||
dependencies = [
|
||||
"include-flate",
|
||||
"rust-embed-impl",
|
||||
"rust-embed-utils",
|
||||
"walkdir",
|
||||
|
@ -7438,6 +7517,7 @@ dependencies = [
|
|||
"backtrace-on-stack-overflow",
|
||||
"chrono",
|
||||
"clap 4.4.4",
|
||||
"collab_ui",
|
||||
"dialoguer",
|
||||
"editor",
|
||||
"fuzzy",
|
||||
|
@ -9529,6 +9609,7 @@ dependencies = [
|
|||
"activity_indicator",
|
||||
"ai",
|
||||
"anyhow",
|
||||
"assets",
|
||||
"assistant",
|
||||
"async-compression",
|
||||
"async-recursion 0.3.2",
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
[workspace]
|
||||
members = [
|
||||
"crates/assets",
|
||||
"crates/activity_indicator",
|
||||
"crates/ai",
|
||||
"crates/assistant",
|
||||
|
@ -109,7 +110,7 @@ prost = { version = "0.8" }
|
|||
rand = { version = "0.8.5" }
|
||||
refineable = { path = "./crates/refineable" }
|
||||
regex = { version = "1.5" }
|
||||
rust-embed = { version = "8.0", features = ["include-exclude"] }
|
||||
rust-embed = { version = "8.0", features = ["include-exclude", "compression"] }
|
||||
rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] }
|
||||
schemars = { version = "0.8" }
|
||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||
|
|
|
@ -77,9 +77,6 @@ impl ActivityIndicator {
|
|||
cx.observe(auto_updater, |_, _, cx| cx.notify()).detach();
|
||||
}
|
||||
|
||||
// cx.observe_active_labeled_tasks(|_, cx| cx.notify())
|
||||
// .detach();
|
||||
|
||||
Self {
|
||||
statuses: Default::default(),
|
||||
project: project.clone(),
|
||||
|
@ -288,15 +285,6 @@ impl ActivityIndicator {
|
|||
};
|
||||
}
|
||||
|
||||
// todo!(show active tasks)
|
||||
// if let Some(most_recent_active_task) = cx.active_labeled_tasks().last() {
|
||||
// return Content {
|
||||
// icon: None,
|
||||
// message: most_recent_active_task.to_string(),
|
||||
// on_click: None,
|
||||
// };
|
||||
// }
|
||||
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
|
|
12
crates/assets/Cargo.toml
Normal file
12
crates/assets/Cargo.toml
Normal file
|
@ -0,0 +1,12 @@
|
|||
[package]
|
||||
name = "assets"
|
||||
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 = {path = "../gpui"}
|
||||
rust-embed.workspace = true
|
||||
anyhow.workspace = true
|
|
@ -1,3 +1,4 @@
|
|||
// This crate was essentially pulled out verbatim from main `zed` crate to avoid having to run RustEmbed macro whenever zed has to be rebuilt. It saves a second or two on an incremental build.
|
||||
use anyhow::anyhow;
|
||||
|
||||
use gpui::{AssetSource, Result, SharedString};
|
|
@ -933,7 +933,7 @@ impl AssistantPanel {
|
|||
}
|
||||
|
||||
fn render_hamburger_button(cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
IconButton::new("hamburger_button", Icon::Menu)
|
||||
IconButton::new("hamburger_button", IconName::Menu)
|
||||
.on_click(cx.listener(|this, _event, cx| {
|
||||
if this.active_editor().is_some() {
|
||||
this.set_active_editor_index(None, cx);
|
||||
|
@ -957,7 +957,7 @@ impl AssistantPanel {
|
|||
}
|
||||
|
||||
fn render_split_button(cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
IconButton::new("split_button", Icon::Snip)
|
||||
IconButton::new("split_button", IconName::Snip)
|
||||
.on_click(cx.listener(|this, _event, cx| {
|
||||
if let Some(active_editor) = this.active_editor() {
|
||||
active_editor.update(cx, |editor, cx| editor.split(&Default::default(), cx));
|
||||
|
@ -968,7 +968,7 @@ impl AssistantPanel {
|
|||
}
|
||||
|
||||
fn render_assist_button(cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
IconButton::new("assist_button", Icon::MagicWand)
|
||||
IconButton::new("assist_button", IconName::MagicWand)
|
||||
.on_click(cx.listener(|this, _event, cx| {
|
||||
if let Some(active_editor) = this.active_editor() {
|
||||
active_editor.update(cx, |editor, cx| editor.assist(&Default::default(), cx));
|
||||
|
@ -979,7 +979,7 @@ impl AssistantPanel {
|
|||
}
|
||||
|
||||
fn render_quote_button(cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
IconButton::new("quote_button", Icon::Quote)
|
||||
IconButton::new("quote_button", IconName::Quote)
|
||||
.on_click(cx.listener(|this, _event, cx| {
|
||||
if let Some(workspace) = this.workspace.upgrade() {
|
||||
cx.window_context().defer(move |cx| {
|
||||
|
@ -994,7 +994,7 @@ impl AssistantPanel {
|
|||
}
|
||||
|
||||
fn render_plus_button(cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
IconButton::new("plus_button", Icon::Plus)
|
||||
IconButton::new("plus_button", IconName::Plus)
|
||||
.on_click(cx.listener(|this, _event, cx| {
|
||||
this.new_conversation(cx);
|
||||
}))
|
||||
|
@ -1004,12 +1004,12 @@ impl AssistantPanel {
|
|||
|
||||
fn render_zoom_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let zoomed = self.zoomed;
|
||||
IconButton::new("zoom_button", Icon::Maximize)
|
||||
IconButton::new("zoom_button", IconName::Maximize)
|
||||
.on_click(cx.listener(|this, _event, cx| {
|
||||
this.toggle_zoom(&ToggleZoom, cx);
|
||||
}))
|
||||
.selected(zoomed)
|
||||
.selected_icon(Icon::Minimize)
|
||||
.selected_icon(IconName::Minimize)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::for_action(if zoomed { "Zoom Out" } else { "Zoom In" }, &ToggleZoom, cx)
|
||||
|
@ -1286,8 +1286,8 @@ impl Panel for AssistantPanel {
|
|||
}
|
||||
}
|
||||
|
||||
fn icon(&self, cx: &WindowContext) -> Option<Icon> {
|
||||
Some(Icon::Ai).filter(|_| AssistantSettings::get_global(cx).button)
|
||||
fn icon(&self, cx: &WindowContext) -> Option<IconName> {
|
||||
Some(IconName::Ai).filter(|_| AssistantSettings::get_global(cx).button)
|
||||
}
|
||||
|
||||
fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
|
||||
|
@ -2349,7 +2349,7 @@ impl ConversationEditor {
|
|||
div()
|
||||
.id("error")
|
||||
.tooltip(move |cx| Tooltip::text(error.clone(), cx))
|
||||
.child(IconElement::new(Icon::XCircle)),
|
||||
.child(Icon::new(IconName::XCircle)),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
|
@ -2645,7 +2645,7 @@ impl Render for InlineAssistant {
|
|||
.justify_center()
|
||||
.w(measurements.gutter_width)
|
||||
.child(
|
||||
IconButton::new("include_conversation", Icon::Ai)
|
||||
IconButton::new("include_conversation", IconName::Ai)
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
this.toggle_include_conversation(&ToggleIncludeConversation, cx)
|
||||
}))
|
||||
|
@ -2660,7 +2660,7 @@ impl Render for InlineAssistant {
|
|||
)
|
||||
.children(if SemanticIndex::enabled(cx) {
|
||||
Some(
|
||||
IconButton::new("retrieve_context", Icon::MagnifyingGlass)
|
||||
IconButton::new("retrieve_context", IconName::MagnifyingGlass)
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
this.toggle_retrieve_context(&ToggleRetrieveContext, cx)
|
||||
}))
|
||||
|
@ -2682,7 +2682,7 @@ impl Render for InlineAssistant {
|
|||
div()
|
||||
.id("error")
|
||||
.tooltip(move |cx| Tooltip::text(error_message.clone(), cx))
|
||||
.child(IconElement::new(Icon::XCircle).color(Color::Error)),
|
||||
.child(Icon::new(IconName::XCircle).color(Color::Error)),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
|
@ -2957,7 +2957,7 @@ impl InlineAssistant {
|
|||
div()
|
||||
.id("error")
|
||||
.tooltip(|cx| Tooltip::text("Not Authenticated. Please ensure you have a valid 'OPENAI_API_KEY' in your environment variables.", cx))
|
||||
.child(IconElement::new(Icon::XCircle))
|
||||
.child(Icon::new(IconName::XCircle))
|
||||
.into_any_element()
|
||||
),
|
||||
|
||||
|
@ -2965,7 +2965,7 @@ impl InlineAssistant {
|
|||
div()
|
||||
.id("error")
|
||||
.tooltip(|cx| Tooltip::text("Not Indexed", cx))
|
||||
.child(IconElement::new(Icon::XCircle))
|
||||
.child(Icon::new(IconName::XCircle))
|
||||
.into_any_element()
|
||||
),
|
||||
|
||||
|
@ -2996,7 +2996,7 @@ impl InlineAssistant {
|
|||
div()
|
||||
.id("update")
|
||||
.tooltip(move |cx| Tooltip::text(status_text.clone(), cx))
|
||||
.child(IconElement::new(Icon::Update).color(Color::Info))
|
||||
.child(Icon::new(IconName::Update).color(Color::Info))
|
||||
.into_any_element()
|
||||
)
|
||||
}
|
||||
|
@ -3005,7 +3005,7 @@ impl InlineAssistant {
|
|||
div()
|
||||
.id("check")
|
||||
.tooltip(|cx| Tooltip::text("Index up to date", cx))
|
||||
.child(IconElement::new(Icon::Check).color(Color::Success))
|
||||
.child(Icon::new(IconName::Check).color(Color::Success))
|
||||
.into_any_element()
|
||||
),
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ use gpui::{
|
|||
};
|
||||
use menu::Cancel;
|
||||
use util::channel::ReleaseChannel;
|
||||
use workspace::ui::{h_stack, v_stack, Icon, IconElement, Label, StyledExt};
|
||||
use workspace::ui::{h_stack, v_stack, Icon, IconName, Label, StyledExt};
|
||||
|
||||
pub struct UpdateNotification {
|
||||
version: SemanticVersion,
|
||||
|
@ -30,7 +30,7 @@ impl Render for UpdateNotification {
|
|||
.child(
|
||||
div()
|
||||
.id("cancel")
|
||||
.child(IconElement::new(Icon::Close))
|
||||
.child(Icon::new(IconName::Close))
|
||||
.cursor_pointer()
|
||||
.on_click(cx.listener(|this, _, cx| this.dismiss(&menu::Cancel, cx))),
|
||||
),
|
||||
|
|
|
@ -9,9 +9,12 @@ pub struct CallSettings {
|
|||
pub mute_on_join: bool,
|
||||
}
|
||||
|
||||
/// Configuration of voice calls in Zed.
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
pub struct CallSettingsContent {
|
||||
/// Whether the microphone should be muted when joining a channel or a call.
|
||||
///
|
||||
/// Default: false
|
||||
pub mute_on_join: Option<bool>,
|
||||
}
|
||||
|
||||
|
|
|
@ -173,7 +173,11 @@ impl Room {
|
|||
cx.spawn(|this, mut cx| async move {
|
||||
connect.await?;
|
||||
|
||||
if !cx.update(|cx| Self::mute_on_join(cx))? {
|
||||
let is_read_only = this
|
||||
.update(&mut cx, |room, _| room.read_only())
|
||||
.unwrap_or(true);
|
||||
|
||||
if !cx.update(|cx| Self::mute_on_join(cx))? && !is_read_only {
|
||||
this.update(&mut cx, |this, cx| this.share_microphone(cx))?
|
||||
.await?;
|
||||
}
|
||||
|
@ -620,6 +624,27 @@ impl Room {
|
|||
self.local_participant.role == proto::ChannelRole::Admin
|
||||
}
|
||||
|
||||
pub fn set_participant_role(
|
||||
&mut self,
|
||||
user_id: u64,
|
||||
role: proto::ChannelRole,
|
||||
cx: &ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let client = self.client.clone();
|
||||
let room_id = self.id;
|
||||
let role = role.into();
|
||||
cx.spawn(|_, _| async move {
|
||||
client
|
||||
.request(proto::SetRoomParticipantRole {
|
||||
room_id,
|
||||
user_id,
|
||||
role,
|
||||
})
|
||||
.await
|
||||
.map(|_| ())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn pending_participants(&self) -> &[Arc<User>] {
|
||||
&self.pending_participants
|
||||
}
|
||||
|
@ -729,9 +754,21 @@ impl Room {
|
|||
if this.local_participant.role != role {
|
||||
this.local_participant.role = role;
|
||||
|
||||
if role == proto::ChannelRole::Guest {
|
||||
for project in mem::take(&mut this.shared_projects) {
|
||||
if let Some(project) = project.upgrade() {
|
||||
this.unshare_project(project, cx).log_err();
|
||||
}
|
||||
}
|
||||
this.local_participant.projects.clear();
|
||||
if let Some(live_kit_room) = &mut this.live_kit {
|
||||
live_kit_room.stop_publishing(cx);
|
||||
}
|
||||
}
|
||||
|
||||
this.joined_projects.retain(|project| {
|
||||
if let Some(project) = project.upgrade() {
|
||||
project.update(cx, |project, _| project.set_role(role));
|
||||
project.update(cx, |project, cx| project.set_role(role, cx));
|
||||
true
|
||||
} else {
|
||||
false
|
||||
|
@ -1607,6 +1644,24 @@ impl LiveKitRoom {
|
|||
|
||||
Ok((result, old_muted))
|
||||
}
|
||||
|
||||
fn stop_publishing(&mut self, cx: &mut ModelContext<Room>) {
|
||||
if let LocalTrack::Published {
|
||||
track_publication, ..
|
||||
} = mem::replace(&mut self.microphone_track, LocalTrack::None)
|
||||
{
|
||||
self.room.unpublish_track(track_publication);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
if let LocalTrack::Published {
|
||||
track_publication, ..
|
||||
} = mem::replace(&mut self.screen_track, LocalTrack::None)
|
||||
{
|
||||
self.room.unpublish_track(track_publication);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum LocalTrack {
|
||||
|
|
|
@ -3,7 +3,7 @@ use anyhow::{anyhow, Context, Result};
|
|||
use collections::{hash_map::Entry, HashMap, HashSet};
|
||||
use feature_flags::FeatureFlagAppExt;
|
||||
use futures::{channel::mpsc, Future, StreamExt};
|
||||
use gpui::{AsyncAppContext, EventEmitter, Model, ModelContext, SharedString, Task};
|
||||
use gpui::{AsyncAppContext, EventEmitter, Model, ModelContext, SharedUrl, Task};
|
||||
use postage::{sink::Sink, watch};
|
||||
use rpc::proto::{RequestMessage, UsersResponse};
|
||||
use std::sync::{Arc, Weak};
|
||||
|
@ -19,7 +19,7 @@ pub struct ParticipantIndex(pub u32);
|
|||
pub struct User {
|
||||
pub id: UserId,
|
||||
pub github_login: String,
|
||||
pub avatar_uri: SharedString,
|
||||
pub avatar_uri: SharedUrl,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
|
|
|
@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
|
|||
default-run = "collab"
|
||||
edition = "2021"
|
||||
name = "collab"
|
||||
version = "0.34.0"
|
||||
version = "0.36.0"
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
|
@ -74,6 +74,8 @@ live_kit_client = { path = "../live_kit_client", features = ["test-support"] }
|
|||
lsp = { path = "../lsp", features = ["test-support"] }
|
||||
node_runtime = { path = "../node_runtime" }
|
||||
notifications = { path = "../notifications", features = ["test-support"] }
|
||||
file_finder = { path = "../file_finder"}
|
||||
menu = { path = "../menu"}
|
||||
|
||||
project = { path = "../project", features = ["test-support"] }
|
||||
rpc = { path = "../rpc", features = ["test-support"] }
|
||||
|
|
|
@ -133,13 +133,29 @@ impl ChannelRole {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn can_share_projects(&self) -> bool {
|
||||
pub fn can_publish_to_rooms(&self) -> bool {
|
||||
use ChannelRole::*;
|
||||
match self {
|
||||
Admin | Member => true,
|
||||
Guest | Banned => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn can_edit_projects(&self) -> bool {
|
||||
use ChannelRole::*;
|
||||
match self {
|
||||
Admin | Member => true,
|
||||
Guest | Banned => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn can_read_projects(&self) -> bool {
|
||||
use ChannelRole::*;
|
||||
match self {
|
||||
Admin | Member | Guest => true,
|
||||
Banned => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<proto::ChannelRole> for ChannelRole {
|
||||
|
|
|
@ -49,7 +49,7 @@ impl Database {
|
|||
if !participant
|
||||
.role
|
||||
.unwrap_or(ChannelRole::Member)
|
||||
.can_share_projects()
|
||||
.can_publish_to_rooms()
|
||||
{
|
||||
return Err(anyhow!("guests cannot share projects"))?;
|
||||
}
|
||||
|
@ -777,13 +777,131 @@ impl Database {
|
|||
.await
|
||||
}
|
||||
|
||||
pub async fn project_collaborators(
|
||||
pub async fn check_user_is_project_host(
|
||||
&self,
|
||||
project_id: ProjectId,
|
||||
connection_id: ConnectionId,
|
||||
) -> Result<()> {
|
||||
let room_id = self.room_id_for_project(project_id).await?;
|
||||
self.room_transaction(room_id, |tx| async move {
|
||||
project_collaborator::Entity::find()
|
||||
.filter(
|
||||
Condition::all()
|
||||
.add(project_collaborator::Column::ProjectId.eq(project_id))
|
||||
.add(project_collaborator::Column::IsHost.eq(true))
|
||||
.add(project_collaborator::Column::ConnectionId.eq(connection_id.id))
|
||||
.add(
|
||||
project_collaborator::Column::ConnectionServerId
|
||||
.eq(connection_id.owner_id),
|
||||
),
|
||||
)
|
||||
.one(&*tx)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("failed to read project host"))?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.map(|guard| guard.into_inner())
|
||||
}
|
||||
|
||||
pub async fn host_for_read_only_project_request(
|
||||
&self,
|
||||
project_id: ProjectId,
|
||||
connection_id: ConnectionId,
|
||||
) -> Result<ConnectionId> {
|
||||
let room_id = self.room_id_for_project(project_id).await?;
|
||||
self.room_transaction(room_id, |tx| async move {
|
||||
let current_participant = room_participant::Entity::find()
|
||||
.filter(room_participant::Column::RoomId.eq(room_id))
|
||||
.filter(room_participant::Column::AnsweringConnectionId.eq(connection_id.id))
|
||||
.one(&*tx)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("no such room"))?;
|
||||
|
||||
if !current_participant
|
||||
.role
|
||||
.map_or(false, |role| role.can_read_projects())
|
||||
{
|
||||
Err(anyhow!("not authorized to read projects"))?;
|
||||
}
|
||||
|
||||
let host = project_collaborator::Entity::find()
|
||||
.filter(
|
||||
project_collaborator::Column::ProjectId
|
||||
.eq(project_id)
|
||||
.and(project_collaborator::Column::IsHost.eq(true)),
|
||||
)
|
||||
.one(&*tx)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("failed to read project host"))?;
|
||||
|
||||
Ok(host.connection())
|
||||
})
|
||||
.await
|
||||
.map(|guard| guard.into_inner())
|
||||
}
|
||||
|
||||
pub async fn host_for_mutating_project_request(
|
||||
&self,
|
||||
project_id: ProjectId,
|
||||
connection_id: ConnectionId,
|
||||
) -> Result<ConnectionId> {
|
||||
let room_id = self.room_id_for_project(project_id).await?;
|
||||
self.room_transaction(room_id, |tx| async move {
|
||||
let current_participant = room_participant::Entity::find()
|
||||
.filter(room_participant::Column::RoomId.eq(room_id))
|
||||
.filter(room_participant::Column::AnsweringConnectionId.eq(connection_id.id))
|
||||
.one(&*tx)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("no such room"))?;
|
||||
|
||||
if !current_participant
|
||||
.role
|
||||
.map_or(false, |role| role.can_edit_projects())
|
||||
{
|
||||
Err(anyhow!("not authorized to edit projects"))?;
|
||||
}
|
||||
|
||||
let host = project_collaborator::Entity::find()
|
||||
.filter(
|
||||
project_collaborator::Column::ProjectId
|
||||
.eq(project_id)
|
||||
.and(project_collaborator::Column::IsHost.eq(true)),
|
||||
)
|
||||
.one(&*tx)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("failed to read project host"))?;
|
||||
|
||||
Ok(host.connection())
|
||||
})
|
||||
.await
|
||||
.map(|guard| guard.into_inner())
|
||||
}
|
||||
|
||||
pub async fn project_collaborators_for_buffer_update(
|
||||
&self,
|
||||
project_id: ProjectId,
|
||||
connection_id: ConnectionId,
|
||||
requires_write: bool,
|
||||
) -> Result<RoomGuard<Vec<ProjectCollaborator>>> {
|
||||
let room_id = self.room_id_for_project(project_id).await?;
|
||||
self.room_transaction(room_id, |tx| async move {
|
||||
let current_participant = room_participant::Entity::find()
|
||||
.filter(room_participant::Column::RoomId.eq(room_id))
|
||||
.filter(room_participant::Column::AnsweringConnectionId.eq(connection_id.id))
|
||||
.one(&*tx)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("no such room"))?;
|
||||
|
||||
if requires_write
|
||||
&& !current_participant
|
||||
.role
|
||||
.map_or(false, |role| role.can_edit_projects())
|
||||
{
|
||||
Err(anyhow!("not authorized to edit projects"))?;
|
||||
}
|
||||
|
||||
let collaborators = project_collaborator::Entity::find()
|
||||
.filter(project_collaborator::Column::ProjectId.eq(project_id))
|
||||
.all(&*tx)
|
||||
|
|
|
@ -1004,6 +1004,46 @@ impl Database {
|
|||
.await
|
||||
}
|
||||
|
||||
pub async fn set_room_participant_role(
|
||||
&self,
|
||||
admin_id: UserId,
|
||||
room_id: RoomId,
|
||||
user_id: UserId,
|
||||
role: ChannelRole,
|
||||
) -> Result<RoomGuard<proto::Room>> {
|
||||
self.room_transaction(room_id, |tx| async move {
|
||||
room_participant::Entity::find()
|
||||
.filter(
|
||||
Condition::all()
|
||||
.add(room_participant::Column::RoomId.eq(room_id))
|
||||
.add(room_participant::Column::UserId.eq(admin_id))
|
||||
.add(room_participant::Column::Role.eq(ChannelRole::Admin)),
|
||||
)
|
||||
.one(&*tx)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("only admins can set participant role"))?;
|
||||
|
||||
let result = room_participant::Entity::update_many()
|
||||
.filter(
|
||||
Condition::all()
|
||||
.add(room_participant::Column::RoomId.eq(room_id))
|
||||
.add(room_participant::Column::UserId.eq(user_id)),
|
||||
)
|
||||
.set(room_participant::ActiveModel {
|
||||
role: ActiveValue::set(Some(ChannelRole::from(role))),
|
||||
..Default::default()
|
||||
})
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
|
||||
if result.rows_affected != 1 {
|
||||
Err(anyhow!("could not update room participant role"))?;
|
||||
}
|
||||
Ok(self.get_room(room_id, &tx).await?)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn connection_lost(&self, connection: ConnectionId) -> Result<()> {
|
||||
self.transaction(|tx| async move {
|
||||
self.room_connection_lost(connection, &*tx).await?;
|
||||
|
|
|
@ -103,6 +103,12 @@ pub struct Config {
|
|||
pub zed_environment: Arc<str>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn is_development(&self) -> bool {
|
||||
self.zed_environment == "development".into()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize)]
|
||||
pub struct MigrateConfig {
|
||||
pub database_url: String,
|
||||
|
|
|
@ -53,6 +53,25 @@ async fn main() -> Result<()> {
|
|||
let config = envy::from_env::<Config>().expect("error loading config");
|
||||
init_tracing(&config);
|
||||
|
||||
if config.is_development() {
|
||||
// sanity check database url so even if we deploy a busted ZED_ENVIRONMENT to production
|
||||
// we do not run
|
||||
if config.database_url != "postgres://postgres@localhost/zed" {
|
||||
panic!("about to run development migrations on a non-development database?")
|
||||
}
|
||||
let migrations_path = Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/migrations"));
|
||||
let db_options = db::ConnectOptions::new(config.database_url.clone());
|
||||
let db = Database::new(db_options, Executor::Production).await?;
|
||||
|
||||
let migrations = db.migrate(&migrations_path, false).await?;
|
||||
for (migration, duration) in migrations {
|
||||
println!(
|
||||
"Ran {} {} {:?}",
|
||||
migration.version, migration.description, duration
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let state = AppState::new(config).await?;
|
||||
|
||||
let listener = TcpListener::bind(&format!("0.0.0.0:{}", state.config.http_port))
|
||||
|
|
|
@ -42,7 +42,7 @@ use prometheus::{register_int_gauge, IntGauge};
|
|||
use rpc::{
|
||||
proto::{
|
||||
self, Ack, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, LiveKitConnectionInfo,
|
||||
RequestMessage, UpdateChannelBufferCollaborators,
|
||||
RequestMessage, ShareProject, UpdateChannelBufferCollaborators,
|
||||
},
|
||||
Connection, ConnectionId, Peer, Receipt, TypedEnvelope,
|
||||
};
|
||||
|
@ -202,6 +202,7 @@ impl Server {
|
|||
.add_request_handler(join_room)
|
||||
.add_request_handler(rejoin_room)
|
||||
.add_request_handler(leave_room)
|
||||
.add_request_handler(set_room_participant_role)
|
||||
.add_request_handler(call)
|
||||
.add_request_handler(cancel_call)
|
||||
.add_message_handler(decline_call)
|
||||
|
@ -216,40 +217,45 @@ impl Server {
|
|||
.add_message_handler(update_language_server)
|
||||
.add_message_handler(update_diagnostic_summary)
|
||||
.add_message_handler(update_worktree_settings)
|
||||
.add_message_handler(refresh_inlay_hints)
|
||||
.add_request_handler(forward_project_request::<proto::GetHover>)
|
||||
.add_request_handler(forward_project_request::<proto::GetDefinition>)
|
||||
.add_request_handler(forward_project_request::<proto::GetTypeDefinition>)
|
||||
.add_request_handler(forward_project_request::<proto::GetReferences>)
|
||||
.add_request_handler(forward_project_request::<proto::SearchProject>)
|
||||
.add_request_handler(forward_project_request::<proto::GetDocumentHighlights>)
|
||||
.add_request_handler(forward_project_request::<proto::GetProjectSymbols>)
|
||||
.add_request_handler(forward_project_request::<proto::OpenBufferForSymbol>)
|
||||
.add_request_handler(forward_project_request::<proto::OpenBufferById>)
|
||||
.add_request_handler(forward_project_request::<proto::OpenBufferByPath>)
|
||||
.add_request_handler(forward_project_request::<proto::GetCompletions>)
|
||||
.add_request_handler(forward_project_request::<proto::ApplyCompletionAdditionalEdits>)
|
||||
.add_request_handler(forward_project_request::<proto::ResolveCompletionDocumentation>)
|
||||
.add_request_handler(forward_project_request::<proto::GetCodeActions>)
|
||||
.add_request_handler(forward_project_request::<proto::ApplyCodeAction>)
|
||||
.add_request_handler(forward_project_request::<proto::PrepareRename>)
|
||||
.add_request_handler(forward_project_request::<proto::PerformRename>)
|
||||
.add_request_handler(forward_project_request::<proto::ReloadBuffers>)
|
||||
.add_request_handler(forward_project_request::<proto::SynchronizeBuffers>)
|
||||
.add_request_handler(forward_project_request::<proto::FormatBuffers>)
|
||||
.add_request_handler(forward_project_request::<proto::CreateProjectEntry>)
|
||||
.add_request_handler(forward_project_request::<proto::RenameProjectEntry>)
|
||||
.add_request_handler(forward_project_request::<proto::CopyProjectEntry>)
|
||||
.add_request_handler(forward_project_request::<proto::DeleteProjectEntry>)
|
||||
.add_request_handler(forward_project_request::<proto::ExpandProjectEntry>)
|
||||
.add_request_handler(forward_project_request::<proto::OnTypeFormatting>)
|
||||
.add_request_handler(forward_project_request::<proto::InlayHints>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::GetHover>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::GetDefinition>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::GetTypeDefinition>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::GetReferences>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::SearchProject>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::GetDocumentHighlights>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::GetProjectSymbols>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::OpenBufferForSymbol>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::OpenBufferById>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::SynchronizeBuffers>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::InlayHints>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::OpenBufferByPath>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::GetCompletions>)
|
||||
.add_request_handler(
|
||||
forward_mutating_project_request::<proto::ApplyCompletionAdditionalEdits>,
|
||||
)
|
||||
.add_request_handler(
|
||||
forward_mutating_project_request::<proto::ResolveCompletionDocumentation>,
|
||||
)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::GetCodeActions>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::ApplyCodeAction>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::PrepareRename>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::PerformRename>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::ReloadBuffers>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::FormatBuffers>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::CreateProjectEntry>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::RenameProjectEntry>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::CopyProjectEntry>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::DeleteProjectEntry>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::ExpandProjectEntry>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::OnTypeFormatting>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::SaveBuffer>)
|
||||
.add_message_handler(create_buffer_for_peer)
|
||||
.add_request_handler(update_buffer)
|
||||
.add_message_handler(update_buffer_file)
|
||||
.add_message_handler(buffer_reloaded)
|
||||
.add_message_handler(buffer_saved)
|
||||
.add_request_handler(forward_project_request::<proto::SaveBuffer>)
|
||||
.add_message_handler(broadcast_project_message_from_host::<proto::RefreshInlayHints>)
|
||||
.add_message_handler(broadcast_project_message_from_host::<proto::UpdateBufferFile>)
|
||||
.add_message_handler(broadcast_project_message_from_host::<proto::BufferReloaded>)
|
||||
.add_message_handler(broadcast_project_message_from_host::<proto::BufferSaved>)
|
||||
.add_message_handler(broadcast_project_message_from_host::<proto::UpdateDiffBase>)
|
||||
.add_request_handler(get_users)
|
||||
.add_request_handler(fuzzy_search_users)
|
||||
.add_request_handler(request_contact)
|
||||
|
@ -281,7 +287,6 @@ impl Server {
|
|||
.add_request_handler(follow)
|
||||
.add_message_handler(unfollow)
|
||||
.add_message_handler(update_followers)
|
||||
.add_message_handler(update_diff_base)
|
||||
.add_request_handler(get_private_user_info)
|
||||
.add_message_handler(acknowledge_channel_message)
|
||||
.add_message_handler(acknowledge_buffer_version);
|
||||
|
@ -1254,6 +1259,50 @@ async fn leave_room(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
async fn set_room_participant_role(
|
||||
request: proto::SetRoomParticipantRole,
|
||||
response: Response<proto::SetRoomParticipantRole>,
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
let (live_kit_room, can_publish) = {
|
||||
let room = session
|
||||
.db()
|
||||
.await
|
||||
.set_room_participant_role(
|
||||
session.user_id,
|
||||
RoomId::from_proto(request.room_id),
|
||||
UserId::from_proto(request.user_id),
|
||||
ChannelRole::from(request.role()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let live_kit_room = room.live_kit_room.clone();
|
||||
let can_publish = ChannelRole::from(request.role()).can_publish_to_rooms();
|
||||
room_updated(&room, &session.peer);
|
||||
(live_kit_room, can_publish)
|
||||
};
|
||||
|
||||
if let Some(live_kit) = session.live_kit_client.as_ref() {
|
||||
live_kit
|
||||
.update_participant(
|
||||
live_kit_room.clone(),
|
||||
request.user_id.to_string(),
|
||||
live_kit_server::proto::ParticipantPermission {
|
||||
can_subscribe: true,
|
||||
can_publish,
|
||||
can_publish_data: can_publish,
|
||||
hidden: false,
|
||||
recorder: false,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.trace_err();
|
||||
}
|
||||
|
||||
response.send(proto::Ack {})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn call(
|
||||
request: proto::Call,
|
||||
response: Response<proto::Call>,
|
||||
|
@ -1694,10 +1743,6 @@ async fn update_worktree_settings(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
async fn refresh_inlay_hints(request: proto::RefreshInlayHints, session: Session) -> Result<()> {
|
||||
broadcast_project_message(request.project_id, request, session).await
|
||||
}
|
||||
|
||||
async fn start_language_server(
|
||||
request: proto::StartLanguageServer,
|
||||
session: Session,
|
||||
|
@ -1742,7 +1787,7 @@ async fn update_language_server(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
async fn forward_project_request<T>(
|
||||
async fn forward_read_only_project_request<T>(
|
||||
request: T,
|
||||
response: Response<T>,
|
||||
session: Session,
|
||||
|
@ -1751,24 +1796,37 @@ where
|
|||
T: EntityMessage + RequestMessage,
|
||||
{
|
||||
let project_id = ProjectId::from_proto(request.remote_entity_id());
|
||||
let host_connection_id = {
|
||||
let collaborators = session
|
||||
.db()
|
||||
.await
|
||||
.project_collaborators(project_id, session.connection_id)
|
||||
.await?;
|
||||
collaborators
|
||||
.iter()
|
||||
.find(|collaborator| collaborator.is_host)
|
||||
.ok_or_else(|| anyhow!("host not found"))?
|
||||
.connection_id
|
||||
};
|
||||
|
||||
let host_connection_id = session
|
||||
.db()
|
||||
.await
|
||||
.host_for_read_only_project_request(project_id, session.connection_id)
|
||||
.await?;
|
||||
let payload = session
|
||||
.peer
|
||||
.forward_request(session.connection_id, host_connection_id, request)
|
||||
.await?;
|
||||
response.send(payload)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn forward_mutating_project_request<T>(
|
||||
request: T,
|
||||
response: Response<T>,
|
||||
session: Session,
|
||||
) -> Result<()>
|
||||
where
|
||||
T: EntityMessage + RequestMessage,
|
||||
{
|
||||
let project_id = ProjectId::from_proto(request.remote_entity_id());
|
||||
let host_connection_id = session
|
||||
.db()
|
||||
.await
|
||||
.host_for_mutating_project_request(project_id, session.connection_id)
|
||||
.await?;
|
||||
let payload = session
|
||||
.peer
|
||||
.forward_request(session.connection_id, host_connection_id, request)
|
||||
.await?;
|
||||
response.send(payload)?;
|
||||
Ok(())
|
||||
}
|
||||
|
@ -1777,6 +1835,14 @@ async fn create_buffer_for_peer(
|
|||
request: proto::CreateBufferForPeer,
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
session
|
||||
.db()
|
||||
.await
|
||||
.check_user_is_project_host(
|
||||
ProjectId::from_proto(request.project_id),
|
||||
session.connection_id,
|
||||
)
|
||||
.await?;
|
||||
let peer_id = request.peer_id.ok_or_else(|| anyhow!("invalid peer id"))?;
|
||||
session
|
||||
.peer
|
||||
|
@ -1792,11 +1858,25 @@ async fn update_buffer(
|
|||
let project_id = ProjectId::from_proto(request.project_id);
|
||||
let mut guest_connection_ids;
|
||||
let mut host_connection_id = None;
|
||||
|
||||
let mut requires_write_permission = false;
|
||||
|
||||
for op in request.operations.iter() {
|
||||
match op.variant {
|
||||
None | Some(proto::operation::Variant::UpdateSelections(_)) => {}
|
||||
Some(_) => requires_write_permission = true,
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let collaborators = session
|
||||
.db()
|
||||
.await
|
||||
.project_collaborators(project_id, session.connection_id)
|
||||
.project_collaborators_for_buffer_update(
|
||||
project_id,
|
||||
session.connection_id,
|
||||
requires_write_permission,
|
||||
)
|
||||
.await?;
|
||||
guest_connection_ids = Vec::with_capacity(collaborators.len() - 1);
|
||||
for collaborator in collaborators.iter() {
|
||||
|
@ -1829,60 +1909,17 @@ async fn update_buffer(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_buffer_file(request: proto::UpdateBufferFile, session: Session) -> Result<()> {
|
||||
let project_id = ProjectId::from_proto(request.project_id);
|
||||
let project_connection_ids = session
|
||||
.db()
|
||||
.await
|
||||
.project_connection_ids(project_id, session.connection_id)
|
||||
.await?;
|
||||
|
||||
broadcast(
|
||||
Some(session.connection_id),
|
||||
project_connection_ids.iter().copied(),
|
||||
|connection_id| {
|
||||
session
|
||||
.peer
|
||||
.forward_send(session.connection_id, connection_id, request.clone())
|
||||
},
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn buffer_reloaded(request: proto::BufferReloaded, session: Session) -> Result<()> {
|
||||
let project_id = ProjectId::from_proto(request.project_id);
|
||||
let project_connection_ids = session
|
||||
.db()
|
||||
.await
|
||||
.project_connection_ids(project_id, session.connection_id)
|
||||
.await?;
|
||||
broadcast(
|
||||
Some(session.connection_id),
|
||||
project_connection_ids.iter().copied(),
|
||||
|connection_id| {
|
||||
session
|
||||
.peer
|
||||
.forward_send(session.connection_id, connection_id, request.clone())
|
||||
},
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn buffer_saved(request: proto::BufferSaved, session: Session) -> Result<()> {
|
||||
broadcast_project_message(request.project_id, request, session).await
|
||||
}
|
||||
|
||||
async fn broadcast_project_message<T: EnvelopedMessage>(
|
||||
project_id: u64,
|
||||
async fn broadcast_project_message_from_host<T: EntityMessage<Entity = ShareProject>>(
|
||||
request: T,
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
let project_id = ProjectId::from_proto(project_id);
|
||||
let project_id = ProjectId::from_proto(request.remote_entity_id());
|
||||
let project_connection_ids = session
|
||||
.db()
|
||||
.await
|
||||
.project_connection_ids(project_id, session.connection_id)
|
||||
.await?;
|
||||
|
||||
broadcast(
|
||||
Some(session.connection_id),
|
||||
project_connection_ids.iter().copied(),
|
||||
|
@ -3111,25 +3148,6 @@ async fn mark_notification_as_read(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_diff_base(request: proto::UpdateDiffBase, session: Session) -> Result<()> {
|
||||
let project_id = ProjectId::from_proto(request.project_id);
|
||||
let project_connection_ids = session
|
||||
.db()
|
||||
.await
|
||||
.project_connection_ids(project_id, session.connection_id)
|
||||
.await?;
|
||||
broadcast(
|
||||
Some(session.connection_id),
|
||||
project_connection_ids.iter().copied(),
|
||||
|connection_id| {
|
||||
session
|
||||
.peer
|
||||
.forward_send(session.connection_id, connection_id, request.clone())
|
||||
},
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_private_user_info(
|
||||
_request: proto::GetPrivateUserInfo,
|
||||
response: Response<proto::GetPrivateUserInfo>,
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
use crate::tests::TestServer;
|
||||
use call::ActiveCall;
|
||||
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
|
||||
use editor::Editor;
|
||||
use gpui::{BackgroundExecutor, TestAppContext};
|
||||
use rpc::proto;
|
||||
use workspace::Workspace;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_channel_guests(
|
||||
|
@ -13,37 +13,18 @@ async fn test_channel_guests(
|
|||
let mut server = TestServer::start(executor.clone()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
|
||||
let channel_id = server
|
||||
.make_channel("the-channel", None, (&client_a, cx_a), &mut [])
|
||||
.await;
|
||||
|
||||
client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |channel_store, cx| {
|
||||
channel_store.set_channel_visibility(channel_id, proto::ChannelVisibility::Public, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
client_a
|
||||
.fs()
|
||||
.insert_tree(
|
||||
"/a",
|
||||
serde_json::json!({
|
||||
"a.txt": "a-contents",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
|
||||
let channel_id = server
|
||||
.make_public_channel("the-channel", &client_a, cx_a)
|
||||
.await;
|
||||
|
||||
// Client A shares a project in the channel
|
||||
let project_a = client_a.build_test_project(cx_a).await;
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| call.join_channel(channel_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
|
||||
let project_id = active_call_a
|
||||
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||
.await
|
||||
|
@ -57,30 +38,122 @@ async fn test_channel_guests(
|
|||
|
||||
// b should be following a in the shared project.
|
||||
// B is a guest,
|
||||
cx_a.executor().run_until_parked();
|
||||
executor.run_until_parked();
|
||||
|
||||
// todo!() the test window does not call activation handlers
|
||||
// correctly yet, so this API does not work.
|
||||
// let project_b = active_call_b.read_with(cx_b, |call, _| {
|
||||
// call.location()
|
||||
// .unwrap()
|
||||
// .upgrade()
|
||||
// .expect("should not be weak")
|
||||
// });
|
||||
|
||||
let window_b = cx_b.update(|cx| cx.active_window().unwrap());
|
||||
let cx_b = &mut VisualTestContext::from_window(window_b, cx_b);
|
||||
|
||||
let workspace_b = window_b
|
||||
.downcast::<Workspace>()
|
||||
.unwrap()
|
||||
.root_view(cx_b)
|
||||
.unwrap();
|
||||
let project_b = workspace_b.update(cx_b, |workspace, _| workspace.project().clone());
|
||||
let active_call_b = cx_b.read(ActiveCall::global);
|
||||
let project_b =
|
||||
active_call_b.read_with(cx_b, |call, _| call.location().unwrap().upgrade().unwrap());
|
||||
let room_b = active_call_b.update(cx_b, |call, _| call.room().unwrap().clone());
|
||||
|
||||
assert_eq!(
|
||||
project_b.read_with(cx_b, |project, _| project.remote_id()),
|
||||
Some(project_id),
|
||||
);
|
||||
assert!(project_b.read_with(cx_b, |project, _| project.is_read_only()))
|
||||
assert!(project_b.read_with(cx_b, |project, _| project.is_read_only()));
|
||||
assert!(project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
let worktree_id = project.worktrees().next().unwrap().read(cx).id();
|
||||
project.create_entry((worktree_id, "b.txt"), false, cx)
|
||||
})
|
||||
.await
|
||||
.is_err());
|
||||
assert!(room_b.read_with(cx_b, |room, _| !room.is_sharing_mic()));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
let mut server = TestServer::start(cx_a.executor()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
|
||||
let channel_id = server
|
||||
.make_public_channel("the-channel", &client_a, cx_a)
|
||||
.await;
|
||||
|
||||
let project_a = client_a.build_test_project(cx_a).await;
|
||||
cx_a.update(|cx| workspace::join_channel(channel_id, client_a.app_state.clone(), None, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Client A shares a project in the channel
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
cx_a.run_until_parked();
|
||||
|
||||
// Client B joins channel A as a guest
|
||||
cx_b.update(|cx| workspace::join_channel(channel_id, client_b.app_state.clone(), None, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
cx_a.run_until_parked();
|
||||
|
||||
// client B opens 1.txt as a guest
|
||||
let (workspace_b, cx_b) = client_b.active_workspace(cx_b);
|
||||
let room_b = cx_b
|
||||
.read(ActiveCall::global)
|
||||
.update(cx_b, |call, _| call.room().unwrap().clone());
|
||||
cx_b.simulate_keystrokes("cmd-p 1 enter");
|
||||
|
||||
let (project_b, editor_b) = workspace_b.update(cx_b, |workspace, cx| {
|
||||
(
|
||||
workspace.project().clone(),
|
||||
workspace.active_item_as::<Editor>(cx).unwrap(),
|
||||
)
|
||||
});
|
||||
assert!(project_b.read_with(cx_b, |project, _| project.is_read_only()));
|
||||
assert!(editor_b.update(cx_b, |e, cx| e.read_only(cx)));
|
||||
assert!(dbg!(
|
||||
room_b
|
||||
.update(cx_b, |room, cx| room.share_microphone(cx))
|
||||
.await
|
||||
)
|
||||
.is_err());
|
||||
|
||||
// B is promoted
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| {
|
||||
call.room().unwrap().update(cx, |room, cx| {
|
||||
room.set_participant_role(
|
||||
client_b.user_id().unwrap(),
|
||||
proto::ChannelRole::Member,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx_a.run_until_parked();
|
||||
|
||||
// project and buffers are now editable
|
||||
assert!(project_b.read_with(cx_b, |project, _| !project.is_read_only()));
|
||||
assert!(editor_b.update(cx_b, |editor, cx| !editor.read_only(cx)));
|
||||
room_b
|
||||
.update(cx_b, |room, cx| room.share_microphone(cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// B is demoted
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| {
|
||||
call.room().unwrap().update(cx, |room, cx| {
|
||||
room.set_participant_role(
|
||||
client_b.user_id().unwrap(),
|
||||
proto::ChannelRole::Guest,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx_a.run_until_parked();
|
||||
|
||||
// project and buffers are no longer editable
|
||||
assert!(project_b.read_with(cx_b, |project, _| project.is_read_only()));
|
||||
assert!(editor_b.update(cx_b, |editor, cx| editor.read_only(cx)));
|
||||
assert!(room_b
|
||||
.update(cx_b, |room, cx| room.share_microphone(cx))
|
||||
.await
|
||||
.is_err());
|
||||
}
|
||||
|
|
|
@ -262,7 +262,6 @@ async fn test_remove_channel_message(
|
|||
|
||||
#[track_caller]
|
||||
fn assert_messages(chat: &Model<ChannelChat>, messages: &[&str], cx: &mut TestAppContext) {
|
||||
// todo!(don't directly borrow here)
|
||||
assert_eq!(
|
||||
chat.read_with(cx, |chat, _| {
|
||||
chat.messages()
|
||||
|
|
|
@ -1337,6 +1337,7 @@ async fn test_guest_access(
|
|||
})
|
||||
.await
|
||||
.unwrap();
|
||||
executor.run_until_parked();
|
||||
|
||||
assert_channels_list_shape(client_b.channel_store(), cx_b, &[]);
|
||||
|
||||
|
|
|
@ -71,6 +71,7 @@ async fn test_host_disconnect(
|
|||
let workspace_b =
|
||||
cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.clone(), cx));
|
||||
let cx_b = &mut VisualTestContext::from_window(*workspace_b, cx_b);
|
||||
let workspace_b_view = workspace_b.root_view(cx_b).unwrap();
|
||||
|
||||
let editor_b = workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
|
@ -85,8 +86,10 @@ async fn test_host_disconnect(
|
|||
//TODO: focus
|
||||
assert!(cx_b.update_view(&editor_b, |editor, cx| editor.is_focused(cx)));
|
||||
editor_b.update(cx_b, |editor, cx| editor.insert("X", cx));
|
||||
//todo(is_edited)
|
||||
// assert!(workspace_b.is_edited(cx_b));
|
||||
|
||||
cx_b.update(|cx| {
|
||||
assert!(workspace_b_view.read(cx).is_edited());
|
||||
});
|
||||
|
||||
// Drop client A's connection. Collaborators should disappear and the project should not be shown as shared.
|
||||
server.forbid_connections();
|
||||
|
@ -105,11 +108,11 @@ async fn test_host_disconnect(
|
|||
// Ensure client B's edited state is reset and that the whole window is blurred.
|
||||
|
||||
workspace_b
|
||||
.update(cx_b, |_, cx| {
|
||||
.update(cx_b, |workspace, cx| {
|
||||
assert_eq!(cx.focused(), None);
|
||||
assert!(!workspace.is_edited())
|
||||
})
|
||||
.unwrap();
|
||||
// assert!(!workspace_b.is_edited(cx_b));
|
||||
|
||||
// Ensure client B is not prompted to save edits when closing window after disconnecting.
|
||||
let can_close = workspace_b
|
||||
|
|
|
@ -76,6 +76,10 @@ async fn test_basic_following(
|
|||
let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
|
||||
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
|
||||
|
||||
cx_b.update(|cx| {
|
||||
assert!(cx.is_window_active());
|
||||
});
|
||||
|
||||
// Client A opens some editors.
|
||||
let pane_a = workspace_a.update(cx_a, |workspace, _| workspace.active_pane().clone());
|
||||
let editor_a1 = workspace_a
|
||||
|
@ -157,7 +161,6 @@ async fn test_basic_following(
|
|||
.update(cx_c, |call, cx| call.set_location(Some(&project_c), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let weak_project_c = project_c.downgrade();
|
||||
drop(project_c);
|
||||
|
||||
// Client C also follows client A.
|
||||
|
@ -234,17 +237,16 @@ async fn test_basic_following(
|
|||
workspace_c.update(cx_c, |workspace, cx| {
|
||||
workspace.close_window(&Default::default(), cx);
|
||||
});
|
||||
cx_c.update(|_| {
|
||||
drop(workspace_c);
|
||||
});
|
||||
cx_b.executor().run_until_parked();
|
||||
executor.run_until_parked();
|
||||
// are you sure you want to leave the call?
|
||||
cx_c.simulate_prompt_answer(0);
|
||||
cx_b.executor().run_until_parked();
|
||||
cx_c.cx.update(|_| {
|
||||
drop(workspace_c);
|
||||
});
|
||||
executor.run_until_parked();
|
||||
cx_c.cx.update(|_| {});
|
||||
|
||||
weak_workspace_c.assert_dropped();
|
||||
weak_project_c.assert_dropped();
|
||||
|
||||
// Clients A and B see that client B is following A, and client C is not present in the followers.
|
||||
executor.run_until_parked();
|
||||
|
@ -1363,8 +1365,6 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut
|
|||
let mut server = TestServer::start(executor.clone()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
cx_a.update(editor::init);
|
||||
cx_b.update(editor::init);
|
||||
|
||||
client_a
|
||||
.fs()
|
||||
|
@ -1400,9 +1400,6 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut
|
|||
let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
|
||||
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
|
||||
|
||||
cx_a.update(|cx| collab_ui::init(&client_a.app_state, cx));
|
||||
cx_b.update(|cx| collab_ui::init(&client_b.app_state, cx));
|
||||
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||
.await
|
||||
|
|
|
@ -3065,6 +3065,7 @@ async fn test_local_settings(
|
|||
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
executor.run_until_parked();
|
||||
|
||||
// As client B, join that project and observe the local settings.
|
||||
let project_b = client_b.build_remote_project(project_id, cx_b).await;
|
||||
|
@ -4936,10 +4937,10 @@ async fn test_project_symbols(
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
buffer_b_2.read_with(cx_b, |buffer, _| {
|
||||
buffer_b_2.read_with(cx_b, |buffer, cx| {
|
||||
assert_eq!(
|
||||
buffer.file().unwrap().path().as_ref(),
|
||||
Path::new("../crate-2/two.rs")
|
||||
buffer.file().unwrap().full_path(cx),
|
||||
Path::new("/code/crate-2/two.rs")
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -20,7 +20,11 @@ use node_runtime::FakeNodeRuntime;
|
|||
use notifications::NotificationStore;
|
||||
use parking_lot::Mutex;
|
||||
use project::{Project, WorktreeId};
|
||||
use rpc::{proto::ChannelRole, RECEIVE_TIMEOUT};
|
||||
use rpc::{
|
||||
proto::{self, ChannelRole},
|
||||
RECEIVE_TIMEOUT,
|
||||
};
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use std::{
|
||||
cell::{Ref, RefCell, RefMut},
|
||||
|
@ -228,12 +232,16 @@ impl TestServer {
|
|||
Project::init(&client, cx);
|
||||
client::init(&client, cx);
|
||||
language::init(cx);
|
||||
editor::init_settings(cx);
|
||||
editor::init(cx);
|
||||
workspace::init(app_state.clone(), cx);
|
||||
audio::init((), cx);
|
||||
call::init(client.clone(), user_store.clone(), cx);
|
||||
channel::init(&client, user_store.clone(), cx);
|
||||
notifications::init(client.clone(), user_store, cx);
|
||||
collab_ui::init(&app_state, cx);
|
||||
file_finder::init(cx);
|
||||
menu::init();
|
||||
settings::KeymapFile::load_asset("keymaps/default.json", cx).unwrap();
|
||||
});
|
||||
|
||||
client
|
||||
|
@ -351,6 +359,31 @@ impl TestServer {
|
|||
channel_id
|
||||
}
|
||||
|
||||
pub async fn make_public_channel(
|
||||
&self,
|
||||
channel: &str,
|
||||
client: &TestClient,
|
||||
cx: &mut TestAppContext,
|
||||
) -> u64 {
|
||||
let channel_id = self
|
||||
.make_channel(channel, None, (client, cx), &mut [])
|
||||
.await;
|
||||
|
||||
client
|
||||
.channel_store()
|
||||
.update(cx, |channel_store, cx| {
|
||||
channel_store.set_channel_visibility(
|
||||
channel_id,
|
||||
proto::ChannelVisibility::Public,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
channel_id
|
||||
}
|
||||
|
||||
pub async fn make_channel_tree(
|
||||
&self,
|
||||
channels: &[(&str, Option<&str>)],
|
||||
|
@ -580,6 +613,20 @@ impl TestClient {
|
|||
(project, worktree.read_with(cx, |tree, _| tree.id()))
|
||||
}
|
||||
|
||||
pub async fn build_test_project(&self, cx: &mut TestAppContext) -> Model<Project> {
|
||||
self.fs()
|
||||
.insert_tree(
|
||||
"/a",
|
||||
json!({
|
||||
"1.txt": "one\none\none",
|
||||
"2.txt": "two\ntwo\ntwo",
|
||||
"3.txt": "three\nthree\nthree",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
self.build_local_project("/a", cx).await.0
|
||||
}
|
||||
|
||||
pub fn build_empty_local_project(&self, cx: &mut TestAppContext) -> Model<Project> {
|
||||
cx.update(|cx| {
|
||||
Project::local(
|
||||
|
@ -617,7 +664,22 @@ impl TestClient {
|
|||
project: &Model<Project>,
|
||||
cx: &'a mut TestAppContext,
|
||||
) -> (View<Workspace>, &'a mut VisualTestContext) {
|
||||
cx.add_window_view(|cx| Workspace::new(0, project.clone(), self.app_state.clone(), cx))
|
||||
cx.add_window_view(|cx| {
|
||||
cx.activate_window();
|
||||
Workspace::new(0, project.clone(), self.app_state.clone(), cx)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn active_workspace<'a>(
|
||||
&'a self,
|
||||
cx: &'a mut TestAppContext,
|
||||
) -> (View<Workspace>, &'a mut VisualTestContext) {
|
||||
let window = cx.update(|cx| cx.active_window().unwrap().downcast::<Workspace>().unwrap());
|
||||
|
||||
let view = window.root_view(cx).unwrap();
|
||||
let cx = Box::new(VisualTestContext::from_window(*window.deref(), cx));
|
||||
// it might be nice to try and cleanup these at the end of each test.
|
||||
(view, Box::leak(cx))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,6 +9,8 @@ path = "src/collab_ui.rs"
|
|||
doctest = false
|
||||
|
||||
[features]
|
||||
default = []
|
||||
stories = ["dep:story"]
|
||||
test-support = [
|
||||
"call/test-support",
|
||||
"client/test-support",
|
||||
|
@ -44,6 +46,7 @@ project = { path = "../project" }
|
|||
recent_projects = { path = "../recent_projects" }
|
||||
rpc = { path = "../rpc" }
|
||||
settings = { path = "../settings" }
|
||||
story = { path = "../story", optional = true }
|
||||
feature_flags = { path = "../feature_flags"}
|
||||
theme = { path = "../theme" }
|
||||
theme_selector = { path = "../theme_selector" }
|
||||
|
|
|
@ -19,9 +19,8 @@ use rich_text::RichText;
|
|||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsStore};
|
||||
use std::sync::Arc;
|
||||
use theme::ActiveTheme as _;
|
||||
use time::{OffsetDateTime, UtcOffset};
|
||||
use ui::{prelude::*, Avatar, Button, Icon, IconButton, Label, TabBar, Tooltip};
|
||||
use ui::{prelude::*, Avatar, Button, IconButton, IconName, Label, TabBar, Tooltip};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
use workspace::{
|
||||
dock::{DockPosition, Panel, PanelEvent},
|
||||
|
@ -48,7 +47,7 @@ pub struct ChatPanel {
|
|||
languages: Arc<LanguageRegistry>,
|
||||
message_list: ListState,
|
||||
active_chat: Option<(Model<ChannelChat>, Subscription)>,
|
||||
input_editor: View<MessageEditor>,
|
||||
message_editor: View<MessageEditor>,
|
||||
local_timezone: UtcOffset,
|
||||
fs: Arc<dyn Fs>,
|
||||
width: Option<Pixels>,
|
||||
|
@ -120,7 +119,7 @@ impl ChatPanel {
|
|||
message_list,
|
||||
active_chat: Default::default(),
|
||||
pending_serialization: Task::ready(None),
|
||||
input_editor,
|
||||
message_editor: input_editor,
|
||||
local_timezone: cx.local_timezone(),
|
||||
subscriptions: Vec::new(),
|
||||
workspace: workspace_handle,
|
||||
|
@ -209,7 +208,7 @@ impl ChatPanel {
|
|||
self.message_list.reset(chat.message_count());
|
||||
|
||||
let channel_name = chat.channel(cx).map(|channel| channel.name.clone());
|
||||
self.input_editor.update(cx, |editor, cx| {
|
||||
self.message_editor.update(cx, |editor, cx| {
|
||||
editor.set_channel(channel_id, channel_name, cx);
|
||||
});
|
||||
};
|
||||
|
@ -282,12 +281,12 @@ impl ChatPanel {
|
|||
)),
|
||||
)
|
||||
.end_child(
|
||||
IconButton::new("notes", Icon::File)
|
||||
IconButton::new("notes", IconName::File)
|
||||
.on_click(cx.listener(Self::open_notes))
|
||||
.tooltip(|cx| Tooltip::text("Open notes", cx)),
|
||||
)
|
||||
.end_child(
|
||||
IconButton::new("call", Icon::AudioOn)
|
||||
IconButton::new("call", IconName::AudioOn)
|
||||
.on_click(cx.listener(Self::join_call))
|
||||
.tooltip(|cx| Tooltip::text("Join call", cx)),
|
||||
),
|
||||
|
@ -300,13 +299,7 @@ impl ChatPanel {
|
|||
this
|
||||
}
|
||||
}))
|
||||
.child(
|
||||
div()
|
||||
.z_index(1)
|
||||
.p_2()
|
||||
.bg(cx.theme().colors().background)
|
||||
.child(self.input_editor.clone()),
|
||||
)
|
||||
.child(h_stack().p_2().child(self.message_editor.clone()))
|
||||
.into_any()
|
||||
}
|
||||
|
||||
|
@ -402,7 +395,7 @@ impl ChatPanel {
|
|||
.w_8()
|
||||
.visible_on_hover("")
|
||||
.children(message_id_to_remove.map(|message_id| {
|
||||
IconButton::new(("remove", message_id), Icon::XCircle).on_click(
|
||||
IconButton::new(("remove", message_id), IconName::XCircle).on_click(
|
||||
cx.listener(move |this, _, cx| {
|
||||
this.remove_message(message_id, cx);
|
||||
}),
|
||||
|
@ -436,7 +429,7 @@ impl ChatPanel {
|
|||
Button::new("sign-in", "Sign in")
|
||||
.style(ButtonStyle::Filled)
|
||||
.icon_color(Color::Muted)
|
||||
.icon(Icon::Github)
|
||||
.icon(IconName::Github)
|
||||
.icon_position(IconPosition::Start)
|
||||
.full_width()
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
|
@ -469,7 +462,7 @@ impl ChatPanel {
|
|||
fn send(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
|
||||
if let Some((chat, _)) = self.active_chat.as_ref() {
|
||||
let message = self
|
||||
.input_editor
|
||||
.message_editor
|
||||
.update(cx, |editor, cx| editor.take_message(cx));
|
||||
|
||||
if let Some(task) = chat
|
||||
|
@ -585,7 +578,7 @@ impl Render for ChatPanel {
|
|||
|
||||
impl FocusableView for ChatPanel {
|
||||
fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
|
||||
self.input_editor.read(cx).focus_handle(cx)
|
||||
self.message_editor.read(cx).focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -629,12 +622,12 @@ impl Panel for ChatPanel {
|
|||
"ChatPanel"
|
||||
}
|
||||
|
||||
fn icon(&self, cx: &WindowContext) -> Option<ui::Icon> {
|
||||
fn icon(&self, cx: &WindowContext) -> Option<ui::IconName> {
|
||||
if !is_channels_feature_enabled(cx) {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(ui::Icon::MessageBubbles).filter(|_| ChatPanelSettings::get_global(cx).button)
|
||||
Some(ui::IconName::MessageBubbles).filter(|_| ChatPanelSettings::get_global(cx).button)
|
||||
}
|
||||
|
||||
fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
|
||||
|
|
|
@ -1,16 +1,19 @@
|
|||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use channel::{ChannelId, ChannelMembership, ChannelStore, MessageParams};
|
||||
use client::UserId;
|
||||
use collections::HashMap;
|
||||
use editor::{AnchorRangeExt, Editor};
|
||||
use editor::{AnchorRangeExt, Editor, EditorElement, EditorStyle};
|
||||
use gpui::{
|
||||
AsyncWindowContext, FocusableView, IntoElement, Model, Render, SharedString, Task, View,
|
||||
ViewContext, WeakView,
|
||||
AsyncWindowContext, FocusableView, FontStyle, FontWeight, HighlightStyle, IntoElement, Model,
|
||||
Render, SharedString, Task, TextStyle, View, ViewContext, WeakView, WhiteSpace,
|
||||
};
|
||||
use language::{language_settings::SoftWrap, Buffer, BufferSnapshot, LanguageRegistry};
|
||||
use lazy_static::lazy_static;
|
||||
use project::search::SearchQuery;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use workspace::item::ItemHandle;
|
||||
use settings::Settings;
|
||||
use theme::ThemeSettings;
|
||||
use ui::prelude::*;
|
||||
|
||||
const MENTIONS_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(50);
|
||||
|
||||
|
@ -181,7 +184,14 @@ impl MessageEditor {
|
|||
}
|
||||
|
||||
editor.clear_highlights::<Self>(cx);
|
||||
editor.highlight_text::<Self>(anchor_ranges, gpui::red().into(), cx)
|
||||
editor.highlight_text::<Self>(
|
||||
anchor_ranges,
|
||||
HighlightStyle {
|
||||
font_weight: Some(FontWeight::BOLD),
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
this.mentions = mentioned_user_ids;
|
||||
|
@ -196,8 +206,39 @@ impl MessageEditor {
|
|||
}
|
||||
|
||||
impl Render for MessageEditor {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
self.editor.to_any()
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let text_style = TextStyle {
|
||||
color: if self.editor.read(cx).read_only(cx) {
|
||||
cx.theme().colors().text_disabled
|
||||
} else {
|
||||
cx.theme().colors().text
|
||||
},
|
||||
font_family: settings.ui_font.family.clone(),
|
||||
font_features: settings.ui_font.features,
|
||||
font_size: rems(0.875).into(),
|
||||
font_weight: FontWeight::NORMAL,
|
||||
font_style: FontStyle::Normal,
|
||||
line_height: relative(1.3).into(),
|
||||
background_color: None,
|
||||
underline: None,
|
||||
white_space: WhiteSpace::Normal,
|
||||
};
|
||||
|
||||
div()
|
||||
.w_full()
|
||||
.px_2()
|
||||
.py_1()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.rounded_md()
|
||||
.child(EditorElement::new(
|
||||
&self.editor,
|
||||
EditorStyle {
|
||||
local_player: cx.theme().players().local(),
|
||||
text: text_style,
|
||||
..Default::default()
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -205,7 +246,7 @@ impl Render for MessageEditor {
|
|||
mod tests {
|
||||
use super::*;
|
||||
use client::{Client, User, UserStore};
|
||||
use gpui::{Context as _, TestAppContext, VisualContext as _};
|
||||
use gpui::TestAppContext;
|
||||
use language::{Language, LanguageConfig};
|
||||
use rpc::proto;
|
||||
use settings::SettingsStore;
|
||||
|
|
|
@ -31,13 +31,13 @@ use smallvec::SmallVec;
|
|||
use std::{mem, sync::Arc};
|
||||
use theme::{ActiveTheme, ThemeSettings};
|
||||
use ui::{
|
||||
prelude::*, Avatar, Button, Color, ContextMenu, Icon, IconButton, IconElement, IconSize, Label,
|
||||
prelude::*, Avatar, Button, Color, ContextMenu, Icon, IconButton, IconName, IconSize, Label,
|
||||
ListHeader, ListItem, Tooltip,
|
||||
};
|
||||
use util::{maybe, ResultExt, TryFutureExt};
|
||||
use workspace::{
|
||||
dock::{DockPosition, Panel, PanelEvent},
|
||||
notifications::NotifyResultExt,
|
||||
notifications::{NotifyResultExt, NotifyTaskExt},
|
||||
Workspace,
|
||||
};
|
||||
|
||||
|
@ -140,6 +140,7 @@ enum ListEntry {
|
|||
user: Arc<User>,
|
||||
peer_id: Option<PeerId>,
|
||||
is_pending: bool,
|
||||
role: proto::ChannelRole,
|
||||
},
|
||||
ParticipantProject {
|
||||
project_id: u64,
|
||||
|
@ -151,10 +152,6 @@ enum ListEntry {
|
|||
peer_id: Option<PeerId>,
|
||||
is_last: bool,
|
||||
},
|
||||
GuestCount {
|
||||
count: usize,
|
||||
has_visible_participants: bool,
|
||||
},
|
||||
IncomingRequest(Arc<User>),
|
||||
OutgoingRequest(Arc<User>),
|
||||
ChannelInvite(Arc<Channel>),
|
||||
|
@ -384,14 +381,10 @@ impl CollabPanel {
|
|||
|
||||
if !self.collapsed_sections.contains(&Section::ActiveCall) {
|
||||
let room = room.read(cx);
|
||||
let mut guest_count_ix = 0;
|
||||
let mut guest_count = if room.read_only() { 1 } else { 0 };
|
||||
let mut non_guest_count = if room.read_only() { 0 } else { 1 };
|
||||
|
||||
if let Some(channel_id) = room.channel_id() {
|
||||
self.entries.push(ListEntry::ChannelNotes { channel_id });
|
||||
self.entries.push(ListEntry::ChannelChat { channel_id });
|
||||
guest_count_ix = self.entries.len();
|
||||
}
|
||||
|
||||
// Populate the active user.
|
||||
|
@ -410,12 +403,13 @@ impl CollabPanel {
|
|||
&Default::default(),
|
||||
executor.clone(),
|
||||
));
|
||||
if !matches.is_empty() && !room.read_only() {
|
||||
if !matches.is_empty() {
|
||||
let user_id = user.id;
|
||||
self.entries.push(ListEntry::CallParticipant {
|
||||
user,
|
||||
peer_id: None,
|
||||
is_pending: false,
|
||||
role: room.local_participant().role,
|
||||
});
|
||||
let mut projects = room.local_participant().projects.iter().peekable();
|
||||
while let Some(project) = projects.next() {
|
||||
|
@ -442,12 +436,6 @@ impl CollabPanel {
|
|||
room.remote_participants()
|
||||
.iter()
|
||||
.filter_map(|(_, participant)| {
|
||||
if participant.role == proto::ChannelRole::Guest {
|
||||
guest_count += 1;
|
||||
return None;
|
||||
} else {
|
||||
non_guest_count += 1;
|
||||
}
|
||||
Some(StringMatchCandidate {
|
||||
id: participant.user.id as usize,
|
||||
string: participant.user.github_login.clone(),
|
||||
|
@ -455,7 +443,7 @@ impl CollabPanel {
|
|||
})
|
||||
}),
|
||||
);
|
||||
let matches = executor.block(match_strings(
|
||||
let mut matches = executor.block(match_strings(
|
||||
&self.match_candidates,
|
||||
&query,
|
||||
true,
|
||||
|
@ -463,6 +451,15 @@ impl CollabPanel {
|
|||
&Default::default(),
|
||||
executor.clone(),
|
||||
));
|
||||
matches.sort_by(|a, b| {
|
||||
let a_is_guest = room.role_for_user(a.candidate_id as u64)
|
||||
== Some(proto::ChannelRole::Guest);
|
||||
let b_is_guest = room.role_for_user(b.candidate_id as u64)
|
||||
== Some(proto::ChannelRole::Guest);
|
||||
a_is_guest
|
||||
.cmp(&b_is_guest)
|
||||
.then_with(|| a.string.cmp(&b.string))
|
||||
});
|
||||
for mat in matches {
|
||||
let user_id = mat.candidate_id as u64;
|
||||
let participant = &room.remote_participants()[&user_id];
|
||||
|
@ -470,6 +467,7 @@ impl CollabPanel {
|
|||
user: participant.user.clone(),
|
||||
peer_id: Some(participant.peer_id),
|
||||
is_pending: false,
|
||||
role: participant.role,
|
||||
});
|
||||
let mut projects = participant.projects.iter().peekable();
|
||||
while let Some(project) = projects.next() {
|
||||
|
@ -488,15 +486,6 @@ impl CollabPanel {
|
|||
});
|
||||
}
|
||||
}
|
||||
if guest_count > 0 {
|
||||
self.entries.insert(
|
||||
guest_count_ix,
|
||||
ListEntry::GuestCount {
|
||||
count: guest_count,
|
||||
has_visible_participants: non_guest_count > 0,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Populate pending participants.
|
||||
self.match_candidates.clear();
|
||||
|
@ -521,6 +510,7 @@ impl CollabPanel {
|
|||
user: room.pending_participants()[mat.candidate_id].clone(),
|
||||
peer_id: None,
|
||||
is_pending: true,
|
||||
role: proto::ChannelRole::Member,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
@ -834,13 +824,19 @@ impl CollabPanel {
|
|||
user: &Arc<User>,
|
||||
peer_id: Option<PeerId>,
|
||||
is_pending: bool,
|
||||
role: proto::ChannelRole,
|
||||
is_selected: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> ListItem {
|
||||
let user_id = user.id;
|
||||
let is_current_user =
|
||||
self.user_store.read(cx).current_user().map(|user| user.id) == Some(user.id);
|
||||
self.user_store.read(cx).current_user().map(|user| user.id) == Some(user_id);
|
||||
let tooltip = format!("Follow {}", user.github_login);
|
||||
|
||||
let is_call_admin = ActiveCall::global(cx).read(cx).room().is_some_and(|room| {
|
||||
room.read(cx).local_participant().role == proto::ChannelRole::Admin
|
||||
});
|
||||
|
||||
ListItem::new(SharedString::from(user.github_login.clone()))
|
||||
.start_slot(Avatar::new(user.avatar_uri.clone()))
|
||||
.child(Label::new(user.github_login.clone()))
|
||||
|
@ -848,22 +844,32 @@ impl CollabPanel {
|
|||
.end_slot(if is_pending {
|
||||
Label::new("Calling").color(Color::Muted).into_any_element()
|
||||
} else if is_current_user {
|
||||
IconButton::new("leave-call", Icon::Exit)
|
||||
IconButton::new("leave-call", IconName::Exit)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.on_click(move |_, cx| Self::leave_call(cx))
|
||||
.tooltip(|cx| Tooltip::text("Leave Call", cx))
|
||||
.into_any_element()
|
||||
} else if role == proto::ChannelRole::Guest {
|
||||
Label::new("Guest").color(Color::Muted).into_any_element()
|
||||
} else {
|
||||
div().into_any_element()
|
||||
})
|
||||
.when_some(peer_id, |this, peer_id| {
|
||||
this.tooltip(move |cx| Tooltip::text(tooltip.clone(), cx))
|
||||
.when_some(peer_id, |el, peer_id| {
|
||||
if role == proto::ChannelRole::Guest {
|
||||
return el;
|
||||
}
|
||||
el.tooltip(move |cx| Tooltip::text(tooltip.clone(), cx))
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.workspace
|
||||
.update(cx, |workspace, cx| workspace.follow(peer_id, cx))
|
||||
.ok();
|
||||
}))
|
||||
})
|
||||
.when(is_call_admin, |el| {
|
||||
el.on_secondary_mouse_down(cx.listener(move |this, event: &MouseDownEvent, cx| {
|
||||
this.deploy_participant_context_menu(event.position, user_id, role, cx)
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
fn render_participant_project(
|
||||
|
@ -897,7 +903,7 @@ impl CollabPanel {
|
|||
h_stack()
|
||||
.gap_1()
|
||||
.child(render_tree_branch(is_last, false, cx))
|
||||
.child(IconButton::new(0, Icon::Folder)),
|
||||
.child(IconButton::new(0, IconName::Folder)),
|
||||
)
|
||||
.child(Label::new(project_name.clone()))
|
||||
.tooltip(move |cx| Tooltip::text(format!("Open {}", project_name), cx))
|
||||
|
@ -918,7 +924,7 @@ impl CollabPanel {
|
|||
h_stack()
|
||||
.gap_1()
|
||||
.child(render_tree_branch(is_last, false, cx))
|
||||
.child(IconButton::new(0, Icon::Screen)),
|
||||
.child(IconButton::new(0, IconName::Screen)),
|
||||
)
|
||||
.child(Label::new("Screen"))
|
||||
.when_some(peer_id, |this, _| {
|
||||
|
@ -959,7 +965,7 @@ impl CollabPanel {
|
|||
h_stack()
|
||||
.gap_1()
|
||||
.child(render_tree_branch(false, true, cx))
|
||||
.child(IconButton::new(0, Icon::File)),
|
||||
.child(IconButton::new(0, IconName::File)),
|
||||
)
|
||||
.child(div().h_7().w_full().child(Label::new("notes")))
|
||||
.tooltip(move |cx| Tooltip::text("Open Channel Notes", cx))
|
||||
|
@ -980,47 +986,12 @@ impl CollabPanel {
|
|||
h_stack()
|
||||
.gap_1()
|
||||
.child(render_tree_branch(false, false, cx))
|
||||
.child(IconButton::new(0, Icon::MessageBubbles)),
|
||||
.child(IconButton::new(0, IconName::MessageBubbles)),
|
||||
)
|
||||
.child(Label::new("chat"))
|
||||
.tooltip(move |cx| Tooltip::text("Open Chat", cx))
|
||||
}
|
||||
|
||||
fn render_guest_count(
|
||||
&self,
|
||||
count: usize,
|
||||
has_visible_participants: bool,
|
||||
is_selected: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> impl IntoElement {
|
||||
let manageable_channel_id = ActiveCall::global(cx).read(cx).room().and_then(|room| {
|
||||
let room = room.read(cx);
|
||||
if room.local_participant_is_admin() {
|
||||
room.channel_id()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
ListItem::new("guest_count")
|
||||
.selected(is_selected)
|
||||
.start_slot(
|
||||
h_stack()
|
||||
.gap_1()
|
||||
.child(render_tree_branch(!has_visible_participants, false, cx))
|
||||
.child(""),
|
||||
)
|
||||
.child(Label::new(if count == 1 {
|
||||
format!("{} guest", count)
|
||||
} else {
|
||||
format!("{} guests", count)
|
||||
}))
|
||||
.when_some(manageable_channel_id, |el, channel_id| {
|
||||
el.tooltip(move |cx| Tooltip::text("Manage Members", cx))
|
||||
.on_click(cx.listener(move |this, _, cx| this.manage_members(channel_id, cx)))
|
||||
})
|
||||
}
|
||||
|
||||
fn has_subchannels(&self, ix: usize) -> bool {
|
||||
self.entries.get(ix).map_or(false, |entry| {
|
||||
if let ListEntry::Channel { has_children, .. } = entry {
|
||||
|
@ -1031,6 +1002,80 @@ impl CollabPanel {
|
|||
})
|
||||
}
|
||||
|
||||
fn deploy_participant_context_menu(
|
||||
&mut self,
|
||||
position: Point<Pixels>,
|
||||
user_id: u64,
|
||||
role: proto::ChannelRole,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let this = cx.view().clone();
|
||||
if !(role == proto::ChannelRole::Guest || role == proto::ChannelRole::Member) {
|
||||
return;
|
||||
}
|
||||
|
||||
let context_menu = ContextMenu::build(cx, |context_menu, cx| {
|
||||
if role == proto::ChannelRole::Guest {
|
||||
context_menu.entry(
|
||||
"Grant Write Access",
|
||||
None,
|
||||
cx.handler_for(&this, move |_, cx| {
|
||||
ActiveCall::global(cx)
|
||||
.update(cx, |call, cx| {
|
||||
let Some(room) = call.room() else {
|
||||
return Task::ready(Ok(()));
|
||||
};
|
||||
room.update(cx, |room, cx| {
|
||||
room.set_participant_role(
|
||||
user_id,
|
||||
proto::ChannelRole::Member,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
.detach_and_notify_err(cx)
|
||||
}),
|
||||
)
|
||||
} else if role == proto::ChannelRole::Member {
|
||||
context_menu.entry(
|
||||
"Revoke Write Access",
|
||||
None,
|
||||
cx.handler_for(&this, move |_, cx| {
|
||||
ActiveCall::global(cx)
|
||||
.update(cx, |call, cx| {
|
||||
let Some(room) = call.room() else {
|
||||
return Task::ready(Ok(()));
|
||||
};
|
||||
room.update(cx, |room, cx| {
|
||||
room.set_participant_role(
|
||||
user_id,
|
||||
proto::ChannelRole::Guest,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
.detach_and_notify_err(cx)
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
});
|
||||
|
||||
cx.focus_view(&context_menu);
|
||||
let subscription =
|
||||
cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| {
|
||||
if this.context_menu.as_ref().is_some_and(|context_menu| {
|
||||
context_menu.0.focus_handle(cx).contains_focused(cx)
|
||||
}) {
|
||||
cx.focus_self();
|
||||
}
|
||||
this.context_menu.take();
|
||||
cx.notify();
|
||||
});
|
||||
self.context_menu = Some((context_menu, position, subscription));
|
||||
}
|
||||
|
||||
fn deploy_channel_context_menu(
|
||||
&mut self,
|
||||
position: Point<Pixels>,
|
||||
|
@ -1242,18 +1287,6 @@ impl CollabPanel {
|
|||
});
|
||||
}
|
||||
}
|
||||
ListEntry::GuestCount { .. } => {
|
||||
let Some(room) = ActiveCall::global(cx).read(cx).room() else {
|
||||
return;
|
||||
};
|
||||
let room = room.read(cx);
|
||||
let Some(channel_id) = room.channel_id() else {
|
||||
return;
|
||||
};
|
||||
if room.local_participant_is_admin() {
|
||||
self.manage_members(channel_id, cx)
|
||||
}
|
||||
}
|
||||
ListEntry::Channel { channel, .. } => {
|
||||
let is_active = maybe!({
|
||||
let call_channel = ActiveCall::global(cx)
|
||||
|
@ -1724,7 +1757,7 @@ impl CollabPanel {
|
|||
.child(
|
||||
Button::new("sign_in", "Sign in")
|
||||
.icon_color(Color::Muted)
|
||||
.icon(Icon::Github)
|
||||
.icon(IconName::Github)
|
||||
.icon_position(IconPosition::Start)
|
||||
.style(ButtonStyle::Filled)
|
||||
.full_width()
|
||||
|
@ -1788,8 +1821,9 @@ impl CollabPanel {
|
|||
user,
|
||||
peer_id,
|
||||
is_pending,
|
||||
role,
|
||||
} => self
|
||||
.render_call_participant(user, *peer_id, *is_pending, is_selected, cx)
|
||||
.render_call_participant(user, *peer_id, *is_pending, *role, is_selected, cx)
|
||||
.into_any_element(),
|
||||
ListEntry::ParticipantProject {
|
||||
project_id,
|
||||
|
@ -1809,12 +1843,6 @@ impl CollabPanel {
|
|||
ListEntry::ParticipantScreen { peer_id, is_last } => self
|
||||
.render_participant_screen(*peer_id, *is_last, is_selected, cx)
|
||||
.into_any_element(),
|
||||
ListEntry::GuestCount {
|
||||
count,
|
||||
has_visible_participants,
|
||||
} => self
|
||||
.render_guest_count(*count, *has_visible_participants, is_selected, cx)
|
||||
.into_any_element(),
|
||||
ListEntry::ChannelNotes { channel_id } => self
|
||||
.render_channel_notes(*channel_id, is_selected, cx)
|
||||
.into_any_element(),
|
||||
|
@ -1921,7 +1949,7 @@ impl CollabPanel {
|
|||
let button = match section {
|
||||
Section::ActiveCall => channel_link.map(|channel_link| {
|
||||
let channel_link_copy = channel_link.clone();
|
||||
IconButton::new("channel-link", Icon::Copy)
|
||||
IconButton::new("channel-link", IconName::Copy)
|
||||
.icon_size(IconSize::Small)
|
||||
.size(ButtonSize::None)
|
||||
.visible_on_hover("section-header")
|
||||
|
@ -1933,13 +1961,13 @@ impl CollabPanel {
|
|||
.into_any_element()
|
||||
}),
|
||||
Section::Contacts => Some(
|
||||
IconButton::new("add-contact", Icon::Plus)
|
||||
IconButton::new("add-contact", IconName::Plus)
|
||||
.on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx)))
|
||||
.tooltip(|cx| Tooltip::text("Search for new contact", cx))
|
||||
.into_any_element(),
|
||||
),
|
||||
Section::Channels => Some(
|
||||
IconButton::new("add-channel", Icon::Plus)
|
||||
IconButton::new("add-channel", IconName::Plus)
|
||||
.on_click(cx.listener(|this, _, cx| this.new_root_channel(cx)))
|
||||
.tooltip(|cx| Tooltip::text("Create a channel", cx))
|
||||
.into_any_element(),
|
||||
|
@ -2010,7 +2038,7 @@ impl CollabPanel {
|
|||
})
|
||||
.when(!calling, |el| {
|
||||
el.child(
|
||||
IconButton::new("remove_contact", Icon::Close)
|
||||
IconButton::new("remove_contact", IconName::Close)
|
||||
.icon_color(Color::Muted)
|
||||
.visible_on_hover("")
|
||||
.tooltip(|cx| Tooltip::text("Remove Contact", cx))
|
||||
|
@ -2071,13 +2099,13 @@ impl CollabPanel {
|
|||
|
||||
let controls = if is_incoming {
|
||||
vec![
|
||||
IconButton::new("decline-contact", Icon::Close)
|
||||
IconButton::new("decline-contact", IconName::Close)
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.respond_to_contact_request(user_id, false, cx);
|
||||
}))
|
||||
.icon_color(color)
|
||||
.tooltip(|cx| Tooltip::text("Decline invite", cx)),
|
||||
IconButton::new("accept-contact", Icon::Check)
|
||||
IconButton::new("accept-contact", IconName::Check)
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.respond_to_contact_request(user_id, true, cx);
|
||||
}))
|
||||
|
@ -2086,7 +2114,7 @@ impl CollabPanel {
|
|||
]
|
||||
} else {
|
||||
let github_login = github_login.clone();
|
||||
vec![IconButton::new("remove_contact", Icon::Close)
|
||||
vec![IconButton::new("remove_contact", IconName::Close)
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.remove_contact(user_id, &github_login, cx);
|
||||
}))
|
||||
|
@ -2126,13 +2154,13 @@ impl CollabPanel {
|
|||
};
|
||||
|
||||
let controls = [
|
||||
IconButton::new("reject-invite", Icon::Close)
|
||||
IconButton::new("reject-invite", IconName::Close)
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.respond_to_channel_invite(channel_id, false, cx);
|
||||
}))
|
||||
.icon_color(color)
|
||||
.tooltip(|cx| Tooltip::text("Decline invite", cx)),
|
||||
IconButton::new("accept-invite", Icon::Check)
|
||||
IconButton::new("accept-invite", IconName::Check)
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.respond_to_channel_invite(channel_id, true, cx);
|
||||
}))
|
||||
|
@ -2150,7 +2178,7 @@ impl CollabPanel {
|
|||
.child(h_stack().children(controls)),
|
||||
)
|
||||
.start_slot(
|
||||
IconElement::new(Icon::Hash)
|
||||
Icon::new(IconName::Hash)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
|
@ -2162,7 +2190,7 @@ impl CollabPanel {
|
|||
cx: &mut ViewContext<Self>,
|
||||
) -> ListItem {
|
||||
ListItem::new("contact-placeholder")
|
||||
.child(IconElement::new(Icon::Plus))
|
||||
.child(Icon::new(IconName::Plus))
|
||||
.child(Label::new("Add a Contact"))
|
||||
.selected(is_selected)
|
||||
.on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx)))
|
||||
|
@ -2228,47 +2256,6 @@ impl CollabPanel {
|
|||
None
|
||||
};
|
||||
|
||||
let button_container = |cx: &mut ViewContext<Self>| {
|
||||
h_stack()
|
||||
.absolute()
|
||||
// We're using a negative coordinate for the right anchor to
|
||||
// counteract the padding of the `ListItem`.
|
||||
//
|
||||
// This prevents a gap from showing up between the background
|
||||
// of this element and the edge of the collab panel.
|
||||
.right(rems(-0.5))
|
||||
// HACK: Without this the channel name clips on top of the icons, but I'm not sure why.
|
||||
.z_index(10)
|
||||
.bg(cx.theme().colors().panel_background)
|
||||
.when(is_selected || is_active, |this| {
|
||||
this.bg(cx.theme().colors().ghost_element_selected)
|
||||
})
|
||||
};
|
||||
|
||||
let messages_button = |cx: &mut ViewContext<Self>| {
|
||||
IconButton::new("channel_chat", Icon::MessageBubbles)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(if has_messages_notification {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Muted
|
||||
})
|
||||
.on_click(cx.listener(move |this, _, cx| this.join_channel_chat(channel_id, cx)))
|
||||
.tooltip(|cx| Tooltip::text("Open channel chat", cx))
|
||||
};
|
||||
|
||||
let notes_button = |cx: &mut ViewContext<Self>| {
|
||||
IconButton::new("channel_notes", Icon::File)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(if has_notes_notification {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Muted
|
||||
})
|
||||
.on_click(cx.listener(move |this, _, cx| this.open_channel_notes(channel_id, cx)))
|
||||
.tooltip(|cx| Tooltip::text("Open channel notes", cx))
|
||||
};
|
||||
|
||||
let width = self.width.unwrap_or(px(240.));
|
||||
|
||||
div()
|
||||
|
@ -2315,65 +2302,69 @@ impl CollabPanel {
|
|||
},
|
||||
))
|
||||
.start_slot(
|
||||
IconElement::new(if is_public { Icon::Public } else { Icon::Hash })
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
Icon::new(if is_public {
|
||||
IconName::Public
|
||||
} else {
|
||||
IconName::Hash
|
||||
})
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
h_stack()
|
||||
.id(channel_id as usize)
|
||||
// HACK: This is a dirty hack to help with the positioning of the button container.
|
||||
//
|
||||
// We're using a pixel width for the elements but then allowing the contents to
|
||||
// overflow. This means that the label and facepile will be shown, but will not
|
||||
// push the button container off the edge of the panel.
|
||||
.w_px()
|
||||
.child(Label::new(channel.name.clone()))
|
||||
.children(face_pile.map(|face_pile| face_pile.render(cx))),
|
||||
)
|
||||
.end_slot::<Div>(
|
||||
// If we have a notification for either button, we want to show the corresponding
|
||||
// button(s) as indicators.
|
||||
if has_messages_notification || has_notes_notification {
|
||||
Some(
|
||||
button_container(cx).child(
|
||||
h_stack()
|
||||
.px_1()
|
||||
.children(
|
||||
// We only want to render the messages button if there are unseen messages.
|
||||
// This way we don't take up any space that might overlap the channel name
|
||||
// when there are no notifications.
|
||||
has_messages_notification.then(|| messages_button(cx)),
|
||||
)
|
||||
.child(
|
||||
// We always want the notes button to take up space to prevent layout
|
||||
// shift when hovering over the channel.
|
||||
// However, if there are is no notes notification we just show an empty slot.
|
||||
notes_button(cx)
|
||||
.when(!has_notes_notification, |this| {
|
||||
this.visible_on_hover("")
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_stack()
|
||||
.absolute()
|
||||
.right(rems(0.))
|
||||
.h_full()
|
||||
// HACK: Without this the channel name clips on top of the icons, but I'm not sure why.
|
||||
.z_index(10)
|
||||
.child(
|
||||
h_stack()
|
||||
.h_full()
|
||||
.gap_1()
|
||||
.px_1()
|
||||
.child(
|
||||
IconButton::new("channel_chat", IconName::MessageBubbles)
|
||||
.style(ButtonStyle::Filled)
|
||||
.size(ButtonSize::Compact)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(if has_messages_notification {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Muted
|
||||
})
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.join_channel_chat(channel_id, cx)
|
||||
}))
|
||||
.tooltip(|cx| Tooltip::text("Open channel chat", cx))
|
||||
.when(!has_messages_notification, |this| {
|
||||
this.visible_on_hover("")
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
)
|
||||
.end_hover_slot(
|
||||
// When we hover the channel entry we want to always show both buttons.
|
||||
button_container(cx).child(
|
||||
h_stack()
|
||||
.px_1()
|
||||
// The element hover background has a slight transparency to it, so we
|
||||
// need to apply it to the inner element so that it blends with the solid
|
||||
// background color of the absolutely-positioned element.
|
||||
.group_hover("", |style| {
|
||||
style.bg(cx.theme().colors().ghost_element_hover)
|
||||
})
|
||||
.child(messages_button(cx))
|
||||
.child(notes_button(cx)),
|
||||
),
|
||||
.child(
|
||||
IconButton::new("channel_notes", IconName::File)
|
||||
.style(ButtonStyle::Filled)
|
||||
.size(ButtonSize::Compact)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(if has_notes_notification {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Muted
|
||||
})
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.open_channel_notes(channel_id, cx)
|
||||
}))
|
||||
.tooltip(|cx| Tooltip::text("Open channel notes", cx))
|
||||
.when(!has_notes_notification, |this| {
|
||||
this.visible_on_hover("")
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
.tooltip(|cx| Tooltip::text("Join channel", cx))
|
||||
|
@ -2386,7 +2377,7 @@ impl CollabPanel {
|
|||
.indent_level(depth + 1)
|
||||
.indent_step_size(px(20.))
|
||||
.start_slot(
|
||||
IconElement::new(Icon::Hash)
|
||||
Icon::new(IconName::Hash)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
);
|
||||
|
@ -2500,10 +2491,10 @@ impl Panel for CollabPanel {
|
|||
cx.notify();
|
||||
}
|
||||
|
||||
fn icon(&self, cx: &gpui::WindowContext) -> Option<ui::Icon> {
|
||||
fn icon(&self, cx: &gpui::WindowContext) -> Option<ui::IconName> {
|
||||
CollaborationPanelSettings::get_global(cx)
|
||||
.button
|
||||
.then(|| ui::Icon::Collab)
|
||||
.then(|| ui::IconName::Collab)
|
||||
}
|
||||
|
||||
fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
|
||||
|
@ -2621,11 +2612,6 @@ impl PartialEq for ListEntry {
|
|||
return true;
|
||||
}
|
||||
}
|
||||
ListEntry::GuestCount { .. } => {
|
||||
if let ListEntry::GuestCount { .. } = other {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
@ -2646,11 +2632,11 @@ impl Render for DraggedChannelView {
|
|||
.p_1()
|
||||
.gap_1()
|
||||
.child(
|
||||
IconElement::new(
|
||||
Icon::new(
|
||||
if self.channel.visibility == proto::ChannelVisibility::Public {
|
||||
Icon::Public
|
||||
IconName::Public
|
||||
} else {
|
||||
Icon::Hash
|
||||
IconName::Hash
|
||||
},
|
||||
)
|
||||
.size(IconSize::Small)
|
||||
|
|
|
@ -168,7 +168,7 @@ impl Render for ChannelModal {
|
|||
.w_px()
|
||||
.flex_1()
|
||||
.gap_1()
|
||||
.child(IconElement::new(Icon::Hash).size(IconSize::Medium))
|
||||
.child(Icon::new(IconName::Hash).size(IconSize::Medium))
|
||||
.child(Label::new(channel_name)),
|
||||
)
|
||||
.child(
|
||||
|
@ -406,7 +406,7 @@ impl PickerDelegate for ChannelModalDelegate {
|
|||
Some(ChannelRole::Guest) => Some(Label::new("Guest")),
|
||||
_ => None,
|
||||
})
|
||||
.child(IconButton::new("ellipsis", Icon::Ellipsis))
|
||||
.child(IconButton::new("ellipsis", IconName::Ellipsis))
|
||||
.children(
|
||||
if let (Some((menu, _)), true) = (&self.context_menu, selected) {
|
||||
Some(
|
||||
|
|
|
@ -155,9 +155,7 @@ impl PickerDelegate for ContactFinderDelegate {
|
|||
.selected(selected)
|
||||
.start_slot(Avatar::new(user.avatar_uri.clone()))
|
||||
.child(Label::new(user.github_login.clone()))
|
||||
.end_slot::<IconElement>(
|
||||
icon_path.map(|icon_path| IconElement::from_path(icon_path)),
|
||||
),
|
||||
.end_slot::<Icon>(icon_path.map(|icon_path| Icon::from_path(icon_path))),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ use std::sync::Arc;
|
|||
use theme::{ActiveTheme, PlayerColors};
|
||||
use ui::{
|
||||
h_stack, popover_menu, prelude::*, Avatar, Button, ButtonLike, ButtonStyle, ContextMenu, Icon,
|
||||
IconButton, IconElement, TintColor, Tooltip,
|
||||
IconButton, IconName, TintColor, Tooltip,
|
||||
};
|
||||
use util::ResultExt;
|
||||
use vcs_menu::{build_branch_list, BranchList, OpenRecent as ToggleVcsMenu};
|
||||
|
@ -41,12 +41,6 @@ pub fn init(cx: &mut AppContext) {
|
|||
workspace.set_titlebar_item(titlebar_item.into(), cx)
|
||||
})
|
||||
.detach();
|
||||
// todo!()
|
||||
// cx.add_action(CollabTitlebarItem::share_project);
|
||||
// cx.add_action(CollabTitlebarItem::unshare_project);
|
||||
// cx.add_action(CollabTitlebarItem::toggle_user_menu);
|
||||
// cx.add_action(CollabTitlebarItem::toggle_vcs_menu);
|
||||
// cx.add_action(CollabTitlebarItem::toggle_project_menu);
|
||||
}
|
||||
|
||||
pub struct CollabTitlebarItem {
|
||||
|
@ -213,7 +207,7 @@ impl Render for CollabTitlebarItem {
|
|||
.child(
|
||||
div()
|
||||
.child(
|
||||
IconButton::new("leave-call", ui::Icon::Exit)
|
||||
IconButton::new("leave-call", ui::IconName::Exit)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.tooltip(|cx| Tooltip::text("Leave call", cx))
|
||||
.icon_size(IconSize::Small)
|
||||
|
@ -230,9 +224,9 @@ impl Render for CollabTitlebarItem {
|
|||
IconButton::new(
|
||||
"mute-microphone",
|
||||
if is_muted {
|
||||
ui::Icon::MicMute
|
||||
ui::IconName::MicMute
|
||||
} else {
|
||||
ui::Icon::Mic
|
||||
ui::IconName::Mic
|
||||
},
|
||||
)
|
||||
.tooltip(move |cx| {
|
||||
|
@ -256,9 +250,9 @@ impl Render for CollabTitlebarItem {
|
|||
IconButton::new(
|
||||
"mute-sound",
|
||||
if is_deafened {
|
||||
ui::Icon::AudioOff
|
||||
ui::IconName::AudioOff
|
||||
} else {
|
||||
ui::Icon::AudioOn
|
||||
ui::IconName::AudioOn
|
||||
},
|
||||
)
|
||||
.style(ButtonStyle::Subtle)
|
||||
|
@ -281,7 +275,7 @@ impl Render for CollabTitlebarItem {
|
|||
)
|
||||
.when(!read_only, |this| {
|
||||
this.child(
|
||||
IconButton::new("screen-share", ui::Icon::Screen)
|
||||
IconButton::new("screen-share", ui::IconName::Screen)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.icon_size(IconSize::Small)
|
||||
.selected(is_screen_sharing)
|
||||
|
@ -573,7 +567,7 @@ impl CollabTitlebarItem {
|
|||
| client::Status::ReconnectionError { .. } => Some(
|
||||
div()
|
||||
.id("disconnected")
|
||||
.child(IconElement::new(Icon::Disconnected).size(IconSize::Small))
|
||||
.child(Icon::new(IconName::Disconnected).size(IconSize::Small))
|
||||
.tooltip(|cx| Tooltip::text("Disconnected", cx))
|
||||
.into_any_element(),
|
||||
),
|
||||
|
@ -643,7 +637,7 @@ impl CollabTitlebarItem {
|
|||
h_stack()
|
||||
.gap_0p5()
|
||||
.child(Avatar::new(user.avatar_uri.clone()))
|
||||
.child(IconElement::new(Icon::ChevronDown).color(Color::Muted)),
|
||||
.child(Icon::new(IconName::ChevronDown).color(Color::Muted)),
|
||||
)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)),
|
||||
|
@ -665,7 +659,7 @@ impl CollabTitlebarItem {
|
|||
.child(
|
||||
h_stack()
|
||||
.gap_0p5()
|
||||
.child(IconElement::new(Icon::ChevronDown).color(Color::Muted)),
|
||||
.child(Icon::new(IconName::ChevronDown).color(Color::Muted)),
|
||||
)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)),
|
||||
|
|
|
@ -19,7 +19,7 @@ use serde::{Deserialize, Serialize};
|
|||
use settings::{Settings, SettingsStore};
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use time::{OffsetDateTime, UtcOffset};
|
||||
use ui::{h_stack, prelude::*, v_stack, Avatar, Button, Icon, IconButton, IconElement, Label};
|
||||
use ui::{h_stack, prelude::*, v_stack, Avatar, Button, Icon, IconButton, IconName, Label};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
use workspace::{
|
||||
dock::{DockPosition, Panel, PanelEvent},
|
||||
|
@ -553,7 +553,7 @@ impl Render for NotificationPanel {
|
|||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(Label::new("Notifications"))
|
||||
.child(IconElement::new(Icon::Envelope)),
|
||||
.child(Icon::new(IconName::Envelope)),
|
||||
)
|
||||
.map(|this| {
|
||||
if self.client.user_id().is_none() {
|
||||
|
@ -564,7 +564,7 @@ impl Render for NotificationPanel {
|
|||
.child(
|
||||
Button::new("sign_in_prompt_button", "Sign in")
|
||||
.icon_color(Color::Muted)
|
||||
.icon(Icon::Github)
|
||||
.icon(IconName::Github)
|
||||
.icon_position(IconPosition::Start)
|
||||
.style(ButtonStyle::Filled)
|
||||
.full_width()
|
||||
|
@ -655,10 +655,10 @@ impl Panel for NotificationPanel {
|
|||
}
|
||||
}
|
||||
|
||||
fn icon(&self, cx: &gpui::WindowContext) -> Option<Icon> {
|
||||
fn icon(&self, cx: &gpui::WindowContext) -> Option<IconName> {
|
||||
(NotificationPanelSettings::get_global(cx).button
|
||||
&& self.notification_store.read(cx).notification_count() > 0)
|
||||
.then(|| Icon::Bell)
|
||||
.then(|| IconName::Bell)
|
||||
}
|
||||
|
||||
fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
|
||||
|
@ -716,7 +716,7 @@ impl Render for NotificationToast {
|
|||
.children(user.map(|user| Avatar::new(user.avatar_uri.clone())))
|
||||
.child(Label::new(self.text.clone()))
|
||||
.child(
|
||||
IconButton::new("close", Icon::Close)
|
||||
IconButton::new("close", IconName::Close)
|
||||
.on_click(cx.listener(|_, _, cx| cx.emit(DismissEvent))),
|
||||
)
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
|
|
|
@ -1,9 +1,16 @@
|
|||
mod collab_notification;
|
||||
pub mod incoming_call_notification;
|
||||
pub mod project_shared_notification;
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
mod stories;
|
||||
|
||||
use gpui::AppContext;
|
||||
use std::sync::Arc;
|
||||
use workspace::AppState;
|
||||
|
||||
pub mod incoming_call_notification;
|
||||
pub mod project_shared_notification;
|
||||
#[cfg(feature = "stories")]
|
||||
pub use stories::*;
|
||||
|
||||
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
incoming_call_notification::init(app_state, cx);
|
||||
|
|
52
crates/collab_ui/src/notifications/collab_notification.rs
Normal file
52
crates/collab_ui/src/notifications/collab_notification.rs
Normal file
|
@ -0,0 +1,52 @@
|
|||
use gpui::{img, prelude::*, AnyElement, SharedUrl};
|
||||
use smallvec::SmallVec;
|
||||
use ui::prelude::*;
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct CollabNotification {
|
||||
avatar_uri: SharedUrl,
|
||||
accept_button: Button,
|
||||
dismiss_button: Button,
|
||||
children: SmallVec<[AnyElement; 2]>,
|
||||
}
|
||||
|
||||
impl CollabNotification {
|
||||
pub fn new(
|
||||
avatar_uri: impl Into<SharedUrl>,
|
||||
accept_button: Button,
|
||||
dismiss_button: Button,
|
||||
) -> Self {
|
||||
Self {
|
||||
avatar_uri: avatar_uri.into(),
|
||||
accept_button,
|
||||
dismiss_button,
|
||||
children: SmallVec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ParentElement for CollabNotification {
|
||||
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
|
||||
&mut self.children
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for CollabNotification {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
h_stack()
|
||||
.text_ui()
|
||||
.justify_between()
|
||||
.size_full()
|
||||
.overflow_hidden()
|
||||
.elevation_3(cx)
|
||||
.p_2()
|
||||
.gap_2()
|
||||
.child(img(self.avatar_uri).w_12().h_12().rounded_full())
|
||||
.child(v_stack().overflow_hidden().children(self.children))
|
||||
.child(
|
||||
v_stack()
|
||||
.child(self.accept_button)
|
||||
.child(self.dismiss_button),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,15 +1,12 @@
|
|||
use crate::notification_window_options;
|
||||
use crate::notifications::collab_notification::CollabNotification;
|
||||
use call::{ActiveCall, IncomingCall};
|
||||
use futures::StreamExt;
|
||||
use gpui::{
|
||||
img, px, AppContext, ParentElement, Render, RenderOnce, Styled, ViewContext,
|
||||
VisualContext as _, WindowHandle,
|
||||
};
|
||||
use gpui::{prelude::*, AppContext, WindowHandle};
|
||||
use settings::Settings;
|
||||
use std::sync::{Arc, Weak};
|
||||
use theme::ThemeSettings;
|
||||
use ui::prelude::*;
|
||||
use ui::{h_stack, v_stack, Button, Label};
|
||||
use ui::{prelude::*, Button, Label};
|
||||
use util::ResultExt;
|
||||
use workspace::AppState;
|
||||
|
||||
|
@ -22,7 +19,6 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
|||
for window in notification_windows.drain(..) {
|
||||
window
|
||||
.update(&mut cx, |_, cx| {
|
||||
// todo!()
|
||||
cx.remove_window();
|
||||
})
|
||||
.log_err();
|
||||
|
@ -31,8 +27,8 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
|||
if let Some(incoming_call) = incoming_call {
|
||||
let unique_screens = cx.update(|cx| cx.displays()).unwrap();
|
||||
let window_size = gpui::Size {
|
||||
width: px(380.),
|
||||
height: px(64.),
|
||||
width: px(400.),
|
||||
height: px(72.),
|
||||
};
|
||||
|
||||
for screen in unique_screens {
|
||||
|
@ -129,35 +125,22 @@ impl Render for IncomingCallNotification {
|
|||
|
||||
cx.set_rem_size(ui_font_size);
|
||||
|
||||
h_stack()
|
||||
.font(ui_font)
|
||||
.text_ui()
|
||||
.justify_between()
|
||||
.size_full()
|
||||
.overflow_hidden()
|
||||
.elevation_3(cx)
|
||||
.p_2()
|
||||
.gap_2()
|
||||
.child(
|
||||
img(self.state.call.calling_user.avatar_uri.clone())
|
||||
.w_12()
|
||||
.h_12()
|
||||
.rounded_full(),
|
||||
div().size_full().font(ui_font).child(
|
||||
CollabNotification::new(
|
||||
self.state.call.calling_user.avatar_uri.clone(),
|
||||
Button::new("accept", "Accept").on_click({
|
||||
let state = self.state.clone();
|
||||
move |_, cx| state.respond(true, cx)
|
||||
}),
|
||||
Button::new("decline", "Decline").on_click({
|
||||
let state = self.state.clone();
|
||||
move |_, cx| state.respond(false, cx)
|
||||
}),
|
||||
)
|
||||
.child(v_stack().overflow_hidden().child(Label::new(format!(
|
||||
"{} is sharing a project in Zed",
|
||||
self.state.call.calling_user.github_login
|
||||
))))
|
||||
.child(
|
||||
v_stack()
|
||||
.child(Button::new("accept", "Accept").render(cx).on_click({
|
||||
let state = self.state.clone();
|
||||
move |_, cx| state.respond(true, cx)
|
||||
}))
|
||||
.child(Button::new("decline", "Decline").render(cx).on_click({
|
||||
let state = self.state.clone();
|
||||
move |_, cx| state.respond(false, cx)
|
||||
})),
|
||||
)
|
||||
)))),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
use crate::notification_window_options;
|
||||
use crate::notifications::collab_notification::CollabNotification;
|
||||
use call::{room, ActiveCall};
|
||||
use client::User;
|
||||
use collections::HashMap;
|
||||
use gpui::{img, px, AppContext, ParentElement, Render, Size, Styled, ViewContext, VisualContext};
|
||||
use gpui::{AppContext, Size};
|
||||
use settings::Settings;
|
||||
use std::sync::{Arc, Weak};
|
||||
use theme::ThemeSettings;
|
||||
use ui::{h_stack, prelude::*, v_stack, Button, Label};
|
||||
use ui::{prelude::*, Button, Label};
|
||||
use workspace::AppState;
|
||||
|
||||
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
|
@ -50,7 +51,6 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
|||
for window in windows {
|
||||
window
|
||||
.update(cx, |_, cx| {
|
||||
// todo!()
|
||||
cx.remove_window();
|
||||
})
|
||||
.ok();
|
||||
|
@ -63,7 +63,6 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
|||
for window in windows {
|
||||
window
|
||||
.update(cx, |_, cx| {
|
||||
// todo!()
|
||||
cx.remove_window();
|
||||
})
|
||||
.ok();
|
||||
|
@ -130,51 +129,30 @@ impl Render for ProjectSharedNotification {
|
|||
|
||||
cx.set_rem_size(ui_font_size);
|
||||
|
||||
h_stack()
|
||||
.font(ui_font)
|
||||
.text_ui()
|
||||
.justify_between()
|
||||
.size_full()
|
||||
.overflow_hidden()
|
||||
.elevation_3(cx)
|
||||
.p_2()
|
||||
.gap_2()
|
||||
.child(
|
||||
img(self.owner.avatar_uri.clone())
|
||||
.w_12()
|
||||
.h_12()
|
||||
.rounded_full(),
|
||||
)
|
||||
.child(
|
||||
v_stack()
|
||||
.overflow_hidden()
|
||||
.child(Label::new(self.owner.github_login.clone()))
|
||||
.child(Label::new(format!(
|
||||
"is sharing a project in Zed{}",
|
||||
if self.worktree_root_names.is_empty() {
|
||||
""
|
||||
} else {
|
||||
":"
|
||||
}
|
||||
)))
|
||||
.children(if self.worktree_root_names.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(Label::new(self.worktree_root_names.join(", ")))
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
v_stack()
|
||||
.child(Button::new("open", "Open").on_click(cx.listener(
|
||||
move |this, _event, cx| {
|
||||
this.join(cx);
|
||||
},
|
||||
)))
|
||||
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|
||||
move |this, _event, cx| {
|
||||
this.dismiss(cx);
|
||||
},
|
||||
))),
|
||||
div().size_full().font(ui_font).child(
|
||||
CollabNotification::new(
|
||||
self.owner.avatar_uri.clone(),
|
||||
Button::new("open", "Open").on_click(cx.listener(move |this, _event, cx| {
|
||||
this.join(cx);
|
||||
})),
|
||||
Button::new("dismiss", "Dismiss").on_click(cx.listener(move |this, _event, cx| {
|
||||
this.dismiss(cx);
|
||||
})),
|
||||
)
|
||||
.child(Label::new(self.owner.github_login.clone()))
|
||||
.child(Label::new(format!(
|
||||
"is sharing a project in Zed{}",
|
||||
if self.worktree_root_names.is_empty() {
|
||||
""
|
||||
} else {
|
||||
":"
|
||||
}
|
||||
)))
|
||||
.children(if self.worktree_root_names.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(Label::new(self.worktree_root_names.join(", ")))
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
3
crates/collab_ui/src/notifications/stories.rs
Normal file
3
crates/collab_ui/src/notifications/stories.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
mod collab_notification;
|
||||
|
||||
pub use collab_notification::*;
|
|
@ -0,0 +1,50 @@
|
|||
use gpui::prelude::*;
|
||||
use story::{StoryContainer, StoryItem, StorySection};
|
||||
use ui::prelude::*;
|
||||
|
||||
use crate::notifications::collab_notification::CollabNotification;
|
||||
|
||||
pub struct CollabNotificationStory;
|
||||
|
||||
impl Render for CollabNotificationStory {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let window_container = |width, height| div().w(px(width)).h(px(height));
|
||||
|
||||
StoryContainer::new(
|
||||
"CollabNotification Story",
|
||||
"crates/collab_ui/src/notifications/stories/collab_notification.rs",
|
||||
)
|
||||
.child(
|
||||
StorySection::new().child(StoryItem::new(
|
||||
"Incoming Call Notification",
|
||||
window_container(400., 72.).child(
|
||||
CollabNotification::new(
|
||||
"https://avatars.githubusercontent.com/u/1486634?v=4",
|
||||
Button::new("accept", "Accept"),
|
||||
Button::new("decline", "Decline"),
|
||||
)
|
||||
.child(
|
||||
v_stack()
|
||||
.overflow_hidden()
|
||||
.child(Label::new("maxdeviant is sharing a project in Zed")),
|
||||
),
|
||||
),
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
StorySection::new().child(StoryItem::new(
|
||||
"Project Shared Notification",
|
||||
window_container(400., 72.).child(
|
||||
CollabNotification::new(
|
||||
"https://avatars.githubusercontent.com/u/1714999?v=4",
|
||||
Button::new("open", "Open"),
|
||||
Button::new("dismiss", "Dismiss"),
|
||||
)
|
||||
.child(Label::new("iamnbutler"))
|
||||
.child(Label::new("is sharing a project in Zed:"))
|
||||
.child(Label::new("zed")),
|
||||
),
|
||||
)),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -17,7 +17,9 @@ use util::{paths, ResultExt};
|
|||
use workspace::{
|
||||
create_and_open_local_file,
|
||||
item::ItemHandle,
|
||||
ui::{popover_menu, ButtonCommon, Clickable, ContextMenu, Icon, IconButton, IconSize, Tooltip},
|
||||
ui::{
|
||||
popover_menu, ButtonCommon, Clickable, ContextMenu, IconButton, IconName, IconSize, Tooltip,
|
||||
},
|
||||
StatusItemView, Toast, Workspace,
|
||||
};
|
||||
use zed_actions::OpenBrowser;
|
||||
|
@ -51,15 +53,15 @@ impl Render for CopilotButton {
|
|||
.unwrap_or_else(|| all_language_settings.copilot_enabled(None, None));
|
||||
|
||||
let icon = match status {
|
||||
Status::Error(_) => Icon::CopilotError,
|
||||
Status::Error(_) => IconName::CopilotError,
|
||||
Status::Authorized => {
|
||||
if enabled {
|
||||
Icon::Copilot
|
||||
IconName::Copilot
|
||||
} else {
|
||||
Icon::CopilotDisabled
|
||||
IconName::CopilotDisabled
|
||||
}
|
||||
}
|
||||
_ => Icon::CopilotInit,
|
||||
_ => IconName::CopilotInit,
|
||||
};
|
||||
|
||||
if let Status::Error(e) = status {
|
||||
|
|
|
@ -4,7 +4,7 @@ use gpui::{
|
|||
FocusableView, InteractiveElement, IntoElement, Model, ParentElement, Render, Styled,
|
||||
Subscription, ViewContext,
|
||||
};
|
||||
use ui::{prelude::*, Button, Icon, Label};
|
||||
use ui::{prelude::*, Button, IconName, Label};
|
||||
use workspace::ModalView;
|
||||
|
||||
const COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot";
|
||||
|
@ -175,7 +175,7 @@ impl Render for CopilotCodeVerification {
|
|||
.w_32()
|
||||
.h_16()
|
||||
.flex_none()
|
||||
.path(Icon::ZedXCopilot.path())
|
||||
.path(IconName::ZedXCopilot.path())
|
||||
.text_color(cx.theme().colors().icon),
|
||||
)
|
||||
.child(prompt)
|
||||
|
|
|
@ -36,7 +36,7 @@ use std::{
|
|||
};
|
||||
use theme::ActiveTheme;
|
||||
pub use toolbar_controls::ToolbarControls;
|
||||
use ui::{h_stack, prelude::*, Icon, IconElement, Label};
|
||||
use ui::{h_stack, prelude::*, Icon, IconName, Label};
|
||||
use util::TryFutureExt;
|
||||
use workspace::{
|
||||
item::{BreadcrumbText, Item, ItemEvent, ItemHandle},
|
||||
|
@ -660,7 +660,7 @@ impl Item for ProjectDiagnosticsEditor {
|
|||
then.child(
|
||||
h_stack()
|
||||
.gap_1()
|
||||
.child(IconElement::new(Icon::XCircle).color(Color::Error))
|
||||
.child(Icon::new(IconName::XCircle).color(Color::Error))
|
||||
.child(Label::new(self.summary.error_count.to_string()).color(
|
||||
if selected {
|
||||
Color::Default
|
||||
|
@ -674,9 +674,7 @@ impl Item for ProjectDiagnosticsEditor {
|
|||
then.child(
|
||||
h_stack()
|
||||
.gap_1()
|
||||
.child(
|
||||
IconElement::new(Icon::ExclamationTriangle).color(Color::Warning),
|
||||
)
|
||||
.child(Icon::new(IconName::ExclamationTriangle).color(Color::Warning))
|
||||
.child(Label::new(self.summary.warning_count.to_string()).color(
|
||||
if selected {
|
||||
Color::Default
|
||||
|
@ -816,10 +814,10 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
|
|||
.flex_none()
|
||||
.map(|icon| {
|
||||
if diagnostic.severity == DiagnosticSeverity::ERROR {
|
||||
icon.path(Icon::XCircle.path())
|
||||
icon.path(IconName::XCircle.path())
|
||||
.text_color(Color::Error.color(cx))
|
||||
} else {
|
||||
icon.path(Icon::ExclamationTriangle.path())
|
||||
icon.path(IconName::ExclamationTriangle.path())
|
||||
.text_color(Color::Warning.color(cx))
|
||||
}
|
||||
}),
|
||||
|
|
|
@ -6,7 +6,7 @@ use gpui::{
|
|||
};
|
||||
use language::Diagnostic;
|
||||
use lsp::LanguageServerId;
|
||||
use ui::{h_stack, prelude::*, Button, ButtonLike, Color, Icon, IconElement, Label, Tooltip};
|
||||
use ui::{h_stack, prelude::*, Button, ButtonLike, Color, Icon, IconName, Label, Tooltip};
|
||||
use workspace::{item::ItemHandle, StatusItemView, ToolbarItemEvent, Workspace};
|
||||
|
||||
use crate::{Deploy, ProjectDiagnosticsEditor};
|
||||
|
@ -25,7 +25,7 @@ impl Render for DiagnosticIndicator {
|
|||
let diagnostic_indicator = match (self.summary.error_count, self.summary.warning_count) {
|
||||
(0, 0) => h_stack().map(|this| {
|
||||
this.child(
|
||||
IconElement::new(Icon::Check)
|
||||
Icon::new(IconName::Check)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Default),
|
||||
)
|
||||
|
@ -33,7 +33,7 @@ impl Render for DiagnosticIndicator {
|
|||
(0, warning_count) => h_stack()
|
||||
.gap_1()
|
||||
.child(
|
||||
IconElement::new(Icon::ExclamationTriangle)
|
||||
Icon::new(IconName::ExclamationTriangle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Warning),
|
||||
)
|
||||
|
@ -41,7 +41,7 @@ impl Render for DiagnosticIndicator {
|
|||
(error_count, 0) => h_stack()
|
||||
.gap_1()
|
||||
.child(
|
||||
IconElement::new(Icon::XCircle)
|
||||
Icon::new(IconName::XCircle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Error),
|
||||
)
|
||||
|
@ -49,13 +49,13 @@ impl Render for DiagnosticIndicator {
|
|||
(error_count, warning_count) => h_stack()
|
||||
.gap_1()
|
||||
.child(
|
||||
IconElement::new(Icon::XCircle)
|
||||
Icon::new(IconName::XCircle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Error),
|
||||
)
|
||||
.child(Label::new(error_count.to_string()).size(LabelSize::Small))
|
||||
.child(
|
||||
IconElement::new(Icon::ExclamationTriangle)
|
||||
Icon::new(IconName::ExclamationTriangle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Warning),
|
||||
)
|
||||
|
@ -66,7 +66,7 @@ impl Render for DiagnosticIndicator {
|
|||
Some(
|
||||
h_stack()
|
||||
.gap_2()
|
||||
.child(IconElement::new(Icon::ArrowCircle).size(IconSize::Small))
|
||||
.child(Icon::new(IconName::ArrowCircle).size(IconSize::Small))
|
||||
.child(
|
||||
Label::new("Checking…")
|
||||
.size(LabelSize::Small)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use crate::ProjectDiagnosticsEditor;
|
||||
use gpui::{div, EventEmitter, ParentElement, Render, ViewContext, WeakView};
|
||||
use ui::prelude::*;
|
||||
use ui::{Icon, IconButton, Tooltip};
|
||||
use ui::{IconButton, IconName, Tooltip};
|
||||
use workspace::{item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
|
||||
|
||||
pub struct ToolbarControls {
|
||||
|
@ -24,7 +24,7 @@ impl Render for ToolbarControls {
|
|||
};
|
||||
|
||||
div().child(
|
||||
IconButton::new("toggle-warnings", Icon::ExclamationTriangle)
|
||||
IconButton::new("toggle-warnings", IconName::ExclamationTriangle)
|
||||
.tooltip(move |cx| Tooltip::text(tooltip, cx))
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
if let Some(editor) = this.editor.as_ref().and_then(|editor| editor.upgrade()) {
|
||||
|
|
|
@ -1015,7 +1015,6 @@ pub mod tests {
|
|||
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
|
||||
.unwrap_or(10);
|
||||
|
||||
let _test_platform = &cx.test_platform;
|
||||
let mut tab_size = rng.gen_range(1..=4);
|
||||
let buffer_start_excerpt_header_height = rng.gen_range(1..=5);
|
||||
let excerpt_header_height = rng.gen_range(1..=5);
|
||||
|
|
|
@ -99,8 +99,8 @@ use sum_tree::TreeMap;
|
|||
use text::{OffsetUtf16, Rope};
|
||||
use theme::{ActiveTheme, PlayerColor, StatusColors, SyntaxTheme, ThemeColors, ThemeSettings};
|
||||
use ui::{
|
||||
h_stack, prelude::*, ButtonSize, ButtonStyle, Icon, IconButton, IconSize, ListItem, Popover,
|
||||
Tooltip,
|
||||
h_stack, prelude::*, ButtonSize, ButtonStyle, IconButton, IconName, IconSize, ListItem,
|
||||
Popover, Tooltip,
|
||||
};
|
||||
use util::{post_inc, RangeExt, ResultExt, TryFutureExt};
|
||||
use workspace::{searchable::SearchEvent, ItemNavHistory, Pane, SplitDirection, ViewId, Workspace};
|
||||
|
@ -507,7 +507,7 @@ pub enum SoftWrap {
|
|||
Column(u32),
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
#[derive(Clone)]
|
||||
pub struct EditorStyle {
|
||||
pub background: Hsla,
|
||||
pub local_player: PlayerColor,
|
||||
|
@ -519,6 +519,24 @@ pub struct EditorStyle {
|
|||
pub suggestions_style: HighlightStyle,
|
||||
}
|
||||
|
||||
impl Default for EditorStyle {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
background: Hsla::default(),
|
||||
local_player: PlayerColor::default(),
|
||||
text: TextStyle::default(),
|
||||
scrollbar_width: Pixels::default(),
|
||||
syntax: Default::default(),
|
||||
// HACK: Status colors don't have a real default.
|
||||
// We should look into removing the status colors from the editor
|
||||
// style and retrieve them directly from the theme.
|
||||
status: StatusColors::dark(),
|
||||
inlays_style: HighlightStyle::default(),
|
||||
suggestions_style: HighlightStyle::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type CompletionId = usize;
|
||||
|
||||
// type GetFieldEditorTheme = dyn Fn(&theme::Theme) -> theme::FieldEditor;
|
||||
|
@ -1811,10 +1829,6 @@ impl Editor {
|
|||
this.end_selection(cx);
|
||||
this.scroll_manager.show_scrollbar(cx);
|
||||
|
||||
// todo!("use a different mechanism")
|
||||
// let editor_created_event = EditorCreated(cx.handle());
|
||||
// cx.emit_global(editor_created_event);
|
||||
|
||||
if mode == EditorMode::Full {
|
||||
let should_auto_hide_scrollbars = cx.should_auto_hide_scrollbars();
|
||||
cx.set_global(ScrollbarAutoHide(should_auto_hide_scrollbars));
|
||||
|
@ -4223,7 +4237,7 @@ impl Editor {
|
|||
) -> Option<IconButton> {
|
||||
if self.available_code_actions.is_some() {
|
||||
Some(
|
||||
IconButton::new("code_actions_indicator", ui::Icon::Bolt)
|
||||
IconButton::new("code_actions_indicator", ui::IconName::Bolt)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.selected(is_active)
|
||||
|
@ -4257,7 +4271,7 @@ impl Editor {
|
|||
fold_data
|
||||
.map(|(fold_status, buffer_row, active)| {
|
||||
(active || gutter_hovered || fold_status == FoldStatus::Folded).then(|| {
|
||||
IconButton::new(ix as usize, ui::Icon::ChevronDown)
|
||||
IconButton::new(ix as usize, ui::IconName::ChevronDown)
|
||||
.on_click(cx.listener(move |editor, _e, cx| match fold_status {
|
||||
FoldStatus::Folded => {
|
||||
editor.unfold_at(&UnfoldAt { buffer_row }, cx);
|
||||
|
@ -4269,7 +4283,7 @@ impl Editor {
|
|||
.icon_color(ui::Color::Muted)
|
||||
.icon_size(ui::IconSize::Small)
|
||||
.selected(fold_status == FoldStatus::Folded)
|
||||
.selected_icon(ui::Icon::ChevronRight)
|
||||
.selected_icon(ui::IconName::ChevronRight)
|
||||
.size(ui::ButtonSize::None)
|
||||
})
|
||||
})
|
||||
|
@ -7036,7 +7050,7 @@ impl Editor {
|
|||
let buffer = self.buffer.read(cx).snapshot(cx);
|
||||
let selection = self.selections.newest::<usize>(cx);
|
||||
|
||||
// If there is an active Diagnostic Popover. Jump to it's diagnostic instead.
|
||||
// If there is an active Diagnostic Popover jump to its diagnostic instead.
|
||||
if direction == Direction::Next {
|
||||
if let Some(popover) = self.hover_state.diagnostic_popover.as_ref() {
|
||||
let (group_id, jump_to) = popover.activation_info();
|
||||
|
@ -9743,7 +9757,7 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, _is_valid: bool) -> Ren
|
|||
),
|
||||
)
|
||||
.child(
|
||||
IconButton::new(("copy-block", cx.block_id), Icon::Copy)
|
||||
IconButton::new(("copy-block", cx.block_id), IconName::Copy)
|
||||
.icon_color(Color::Muted)
|
||||
.size(ButtonSize::Compact)
|
||||
.style(ButtonStyle::Transparent)
|
||||
|
|
|
@ -539,7 +539,6 @@ fn test_clone(cx: &mut TestAppContext) {
|
|||
);
|
||||
}
|
||||
|
||||
//todo!(editor navigate)
|
||||
#[gpui::test]
|
||||
async fn test_navigation_history(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
@ -993,7 +992,6 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
|
|||
});
|
||||
}
|
||||
|
||||
//todo!(finish editor tests)
|
||||
#[gpui::test]
|
||||
fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
@ -1259,7 +1257,6 @@ fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
|
|||
});
|
||||
}
|
||||
|
||||
//todo!(finish editor tests)
|
||||
#[gpui::test]
|
||||
fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
@ -1318,7 +1315,6 @@ fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
|
|||
});
|
||||
}
|
||||
|
||||
//todo!(simulate_resize)
|
||||
#[gpui::test]
|
||||
async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
@ -2546,7 +2542,6 @@ fn test_delete_line(cx: &mut TestAppContext) {
|
|||
});
|
||||
}
|
||||
|
||||
//todo!(select_anchor_ranges)
|
||||
#[gpui::test]
|
||||
fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
@ -3114,7 +3109,6 @@ fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
|
|||
});
|
||||
}
|
||||
|
||||
//todo!(test_transpose)
|
||||
#[gpui::test]
|
||||
fn test_transpose(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
@ -4860,7 +4854,6 @@ async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) {
|
|||
});
|
||||
}
|
||||
|
||||
// todo!(select_anchor_ranges)
|
||||
#[gpui::test]
|
||||
async fn test_snippets(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
@ -6455,7 +6448,6 @@ fn test_highlighted_ranges(cx: &mut TestAppContext) {
|
|||
});
|
||||
}
|
||||
|
||||
// todo!(following)
|
||||
#[gpui::test]
|
||||
async fn test_following(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
@ -7094,7 +7086,6 @@ async fn test_move_to_enclosing_bracket(cx: &mut gpui::TestAppContext) {
|
|||
);
|
||||
}
|
||||
|
||||
// todo!(completions)
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_copilot(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) {
|
||||
// flaky
|
||||
|
|
|
@ -28,7 +28,7 @@ use gpui::{
|
|||
AnchorCorner, AnyElement, AvailableSpace, BorrowWindow, Bounds, ContentMask, Corners,
|
||||
CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler, Hsla, InteractiveBounds,
|
||||
InteractiveElement, IntoElement, ModifiersChangedEvent, MouseButton, MouseDownEvent,
|
||||
MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, ScrollWheelEvent, ShapedLine,
|
||||
MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine,
|
||||
SharedString, Size, StackingOrder, StatefulInteractiveElement, Style, Styled, TextRun,
|
||||
TextStyle, View, ViewContext, WindowContext,
|
||||
};
|
||||
|
@ -581,41 +581,6 @@ impl EditorElement {
|
|||
}
|
||||
}
|
||||
|
||||
fn scroll(
|
||||
editor: &mut Editor,
|
||||
event: &ScrollWheelEvent,
|
||||
position_map: &PositionMap,
|
||||
bounds: &InteractiveBounds,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
if !bounds.visibly_contains(&event.position, cx) {
|
||||
return;
|
||||
}
|
||||
|
||||
let line_height = position_map.line_height;
|
||||
let max_glyph_width = position_map.em_width;
|
||||
let (delta, axis) = match event.delta {
|
||||
gpui::ScrollDelta::Pixels(mut pixels) => {
|
||||
//Trackpad
|
||||
let axis = position_map.snapshot.ongoing_scroll.filter(&mut pixels);
|
||||
(pixels, axis)
|
||||
}
|
||||
|
||||
gpui::ScrollDelta::Lines(lines) => {
|
||||
//Not trackpad
|
||||
let pixels = point(lines.x * max_glyph_width, lines.y * line_height);
|
||||
(pixels, None)
|
||||
}
|
||||
};
|
||||
|
||||
let scroll_position = position_map.snapshot.scroll_position();
|
||||
let x = f32::from((scroll_position.x * max_glyph_width - delta.x) / max_glyph_width);
|
||||
let y = f32::from((scroll_position.y * line_height - delta.y) / line_height);
|
||||
let scroll_position = point(x, y).clamp(&point(0., 0.), &position_map.scroll_max);
|
||||
editor.scroll(scroll_position, axis, cx);
|
||||
cx.stop_propagation();
|
||||
}
|
||||
|
||||
fn paint_background(
|
||||
&self,
|
||||
gutter_bounds: Bounds<Pixels>,
|
||||
|
@ -839,9 +804,22 @@ impl EditorElement {
|
|||
|
||||
let start_row = display_row_range.start;
|
||||
let end_row = display_row_range.end;
|
||||
// If we're in a multibuffer, row range span might include an
|
||||
// excerpt header, so if we were to draw the marker straight away,
|
||||
// the hunk might include the rows of that header.
|
||||
// Making the range inclusive doesn't quite cut it, as we rely on the exclusivity for the soft wrap.
|
||||
// Instead, we simply check whether the range we're dealing with includes
|
||||
// any custom elements and if so, we stop painting the diff hunk on the first row of that custom element.
|
||||
let end_row_in_current_excerpt = layout
|
||||
.position_map
|
||||
.snapshot
|
||||
.blocks_in_range(start_row..end_row)
|
||||
.next()
|
||||
.map(|(start_row, _)| start_row)
|
||||
.unwrap_or(end_row);
|
||||
|
||||
let start_y = start_row as f32 * line_height - scroll_top;
|
||||
let end_y = end_row as f32 * line_height - scroll_top;
|
||||
let end_y = end_row_in_current_excerpt as f32 * line_height - scroll_top;
|
||||
|
||||
let width = 0.275 * line_height;
|
||||
let highlight_origin = bounds.origin + point(-width, start_y);
|
||||
|
@ -2450,6 +2428,64 @@ impl EditorElement {
|
|||
)
|
||||
}
|
||||
|
||||
fn paint_scroll_wheel_listener(
|
||||
&mut self,
|
||||
interactive_bounds: &InteractiveBounds,
|
||||
layout: &LayoutState,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
cx.on_mouse_event({
|
||||
let position_map = layout.position_map.clone();
|
||||
let editor = self.editor.clone();
|
||||
let interactive_bounds = interactive_bounds.clone();
|
||||
let mut delta = ScrollDelta::default();
|
||||
|
||||
move |event: &ScrollWheelEvent, phase, cx| {
|
||||
if phase == DispatchPhase::Bubble
|
||||
&& interactive_bounds.visibly_contains(&event.position, cx)
|
||||
{
|
||||
delta = delta.coalesce(event.delta);
|
||||
editor.update(cx, |editor, cx| {
|
||||
let position = event.position;
|
||||
let position_map: &PositionMap = &position_map;
|
||||
let bounds = &interactive_bounds;
|
||||
if !bounds.visibly_contains(&position, cx) {
|
||||
return;
|
||||
}
|
||||
|
||||
let line_height = position_map.line_height;
|
||||
let max_glyph_width = position_map.em_width;
|
||||
let (delta, axis) = match delta {
|
||||
gpui::ScrollDelta::Pixels(mut pixels) => {
|
||||
//Trackpad
|
||||
let axis = position_map.snapshot.ongoing_scroll.filter(&mut pixels);
|
||||
(pixels, axis)
|
||||
}
|
||||
|
||||
gpui::ScrollDelta::Lines(lines) => {
|
||||
//Not trackpad
|
||||
let pixels =
|
||||
point(lines.x * max_glyph_width, lines.y * line_height);
|
||||
(pixels, None)
|
||||
}
|
||||
};
|
||||
|
||||
let scroll_position = position_map.snapshot.scroll_position();
|
||||
let x = f32::from(
|
||||
(scroll_position.x * max_glyph_width - delta.x) / max_glyph_width,
|
||||
);
|
||||
let y =
|
||||
f32::from((scroll_position.y * line_height - delta.y) / line_height);
|
||||
let scroll_position =
|
||||
point(x, y).clamp(&point(0., 0.), &position_map.scroll_max);
|
||||
editor.scroll(scroll_position, axis, cx);
|
||||
cx.stop_propagation();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn paint_mouse_listeners(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
|
@ -2463,21 +2499,7 @@ impl EditorElement {
|
|||
stacking_order: cx.stacking_order().clone(),
|
||||
};
|
||||
|
||||
cx.on_mouse_event({
|
||||
let position_map = layout.position_map.clone();
|
||||
let editor = self.editor.clone();
|
||||
let interactive_bounds = interactive_bounds.clone();
|
||||
|
||||
move |event: &ScrollWheelEvent, phase, cx| {
|
||||
if phase == DispatchPhase::Bubble
|
||||
&& interactive_bounds.visibly_contains(&event.position, cx)
|
||||
{
|
||||
editor.update(cx, |editor, cx| {
|
||||
Self::scroll(editor, event, &position_map, &interactive_bounds, cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
self.paint_scroll_wheel_listener(&interactive_bounds, layout, cx);
|
||||
|
||||
cx.on_mouse_event({
|
||||
let position_map = layout.position_map.clone();
|
||||
|
|
|
@ -16,7 +16,7 @@ use lsp::DiagnosticSeverity;
|
|||
use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project};
|
||||
use settings::Settings;
|
||||
use std::{ops::Range, sync::Arc, time::Duration};
|
||||
use ui::{StyledExt, Tooltip};
|
||||
use ui::{prelude::*, Tooltip};
|
||||
use util::TryFutureExt;
|
||||
use workspace::Workspace;
|
||||
|
||||
|
@ -514,6 +514,8 @@ impl DiagnosticPopover {
|
|||
None => self.local_diagnostic.diagnostic.message.clone(),
|
||||
};
|
||||
|
||||
let status_colors = cx.theme().status();
|
||||
|
||||
struct DiagnosticColors {
|
||||
pub background: Hsla,
|
||||
pub border: Hsla,
|
||||
|
@ -521,24 +523,24 @@ impl DiagnosticPopover {
|
|||
|
||||
let diagnostic_colors = match self.local_diagnostic.diagnostic.severity {
|
||||
DiagnosticSeverity::ERROR => DiagnosticColors {
|
||||
background: style.status.error_background,
|
||||
border: style.status.error_border,
|
||||
background: status_colors.error_background,
|
||||
border: status_colors.error_border,
|
||||
},
|
||||
DiagnosticSeverity::WARNING => DiagnosticColors {
|
||||
background: style.status.warning_background,
|
||||
border: style.status.warning_border,
|
||||
background: status_colors.warning_background,
|
||||
border: status_colors.warning_border,
|
||||
},
|
||||
DiagnosticSeverity::INFORMATION => DiagnosticColors {
|
||||
background: style.status.info_background,
|
||||
border: style.status.info_border,
|
||||
background: status_colors.info_background,
|
||||
border: status_colors.info_border,
|
||||
},
|
||||
DiagnosticSeverity::HINT => DiagnosticColors {
|
||||
background: style.status.hint_background,
|
||||
border: style.status.hint_border,
|
||||
background: status_colors.hint_background,
|
||||
border: status_colors.hint_border,
|
||||
},
|
||||
_ => DiagnosticColors {
|
||||
background: style.status.ignored_background,
|
||||
border: style.status.ignored_border,
|
||||
background: status_colors.ignored_background,
|
||||
border: status_colors.ignored_border,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -95,7 +95,7 @@ pub fn up_by_rows(
|
|||
text_layout_details: &TextLayoutDetails,
|
||||
) -> (DisplayPoint, SelectionGoal) {
|
||||
let mut goal_x = match goal {
|
||||
SelectionGoal::HorizontalPosition(x) => x.into(), // todo!("Can the fields in SelectionGoal by Pixels? We should extract a geometry crate and depend on that.")
|
||||
SelectionGoal::HorizontalPosition(x) => x.into(),
|
||||
SelectionGoal::WrappedHorizontalPosition((_, x)) => x.into(),
|
||||
SelectionGoal::HorizontalRange { end, .. } => end.into(),
|
||||
_ => map.x_for_display_point(start, text_layout_details),
|
||||
|
|
|
@ -384,10 +384,12 @@ impl Editor {
|
|||
) {
|
||||
hide_hover(self, cx);
|
||||
let workspace_id = self.workspace.as_ref().map(|workspace| workspace.1);
|
||||
let top_row = scroll_anchor
|
||||
.anchor
|
||||
.to_point(&self.buffer().read(cx).snapshot(cx))
|
||||
.row;
|
||||
let snapshot = &self.buffer().read(cx).snapshot(cx);
|
||||
if !scroll_anchor.anchor.is_valid(snapshot) {
|
||||
log::warn!("Invalid scroll anchor: {:?}", scroll_anchor);
|
||||
return;
|
||||
}
|
||||
let top_row = scroll_anchor.anchor.to_point(snapshot).row;
|
||||
self.scroll_manager
|
||||
.set_anchor(scroll_anchor, top_row, false, false, workspace_id, cx);
|
||||
}
|
||||
|
|
|
@ -11,10 +11,9 @@ impl Editor {
|
|||
return;
|
||||
}
|
||||
|
||||
// todo!()
|
||||
// if self.mouse_context_menu.read(cx).visible() {
|
||||
// return None;
|
||||
// }
|
||||
if self.mouse_context_menu.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
if matches!(self.mode, EditorMode::SingleLine) {
|
||||
cx.propagate();
|
||||
|
|
|
@ -60,8 +60,7 @@ pub fn assert_text_with_selections(
|
|||
#[allow(dead_code)]
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub(crate) fn build_editor(buffer: Model<MultiBuffer>, cx: &mut ViewContext<Editor>) -> Editor {
|
||||
// todo!()
|
||||
Editor::new(EditorMode::Full, buffer, None, /*None,*/ cx)
|
||||
Editor::new(EditorMode::Full, buffer, None, cx)
|
||||
}
|
||||
|
||||
pub(crate) fn build_editor_with_project(
|
||||
|
@ -69,6 +68,5 @@ pub(crate) fn build_editor_with_project(
|
|||
buffer: Model<MultiBuffer>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> Editor {
|
||||
// todo!()
|
||||
Editor::new(EditorMode::Full, buffer, Some(project), /*None,*/ cx)
|
||||
Editor::new(EditorMode::Full, buffer, Some(project), cx)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use gpui::{Render, ViewContext, WeakView};
|
||||
use ui::{prelude::*, ButtonCommon, Icon, IconButton, Tooltip};
|
||||
use ui::{prelude::*, ButtonCommon, IconButton, IconName, Tooltip};
|
||||
use workspace::{item::ItemHandle, StatusItemView, Workspace};
|
||||
|
||||
use crate::{feedback_modal::FeedbackModal, GiveFeedback};
|
||||
|
@ -27,7 +27,7 @@ impl Render for DeployFeedbackButton {
|
|||
})
|
||||
})
|
||||
.is_some();
|
||||
IconButton::new("give-feedback", Icon::Envelope)
|
||||
IconButton::new("give-feedback", IconName::Envelope)
|
||||
.style(ui::ButtonStyle::Subtle)
|
||||
.icon_size(IconSize::Small)
|
||||
.selected(is_open)
|
||||
|
|
|
@ -7,7 +7,7 @@ use db::kvp::KEY_VALUE_STORE;
|
|||
use editor::{Editor, EditorEvent};
|
||||
use futures::AsyncReadExt;
|
||||
use gpui::{
|
||||
div, red, rems, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model,
|
||||
div, rems, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model,
|
||||
PromptLevel, Render, Task, View, ViewContext,
|
||||
};
|
||||
use isahc::Request;
|
||||
|
@ -179,14 +179,13 @@ impl FeedbackModal {
|
|||
editor
|
||||
});
|
||||
|
||||
// Moved here because providing it inline breaks rustfmt
|
||||
let placeholder_text =
|
||||
"You can use markdown to organize your feedback with code and links.";
|
||||
|
||||
let feedback_editor = cx.new_view(|cx| {
|
||||
let mut editor = Editor::for_buffer(buffer, Some(project.clone()), cx);
|
||||
editor.set_placeholder_text(placeholder_text, cx);
|
||||
// editor.set_show_gutter(false, cx);
|
||||
editor.set_placeholder_text(
|
||||
"You can use markdown to organize your feedback with code and links.",
|
||||
cx,
|
||||
);
|
||||
editor.set_show_gutter(false, cx);
|
||||
editor.set_vertical_scroll_margin(5, cx);
|
||||
editor
|
||||
});
|
||||
|
@ -422,10 +421,6 @@ impl Render for FeedbackModal {
|
|||
let open_community_repo =
|
||||
cx.listener(|_, _, cx| cx.dispatch_action(Box::new(OpenZedCommunityRepo)));
|
||||
|
||||
// Moved this here because providing it inline breaks rustfmt
|
||||
let provide_an_email_address =
|
||||
"Provide an email address if you want us to be able to reply.";
|
||||
|
||||
v_stack()
|
||||
.elevation_3(cx)
|
||||
.key_context("GiveFeedback")
|
||||
|
@ -434,11 +429,8 @@ impl Render for FeedbackModal {
|
|||
.max_w(rems(96.))
|
||||
.h(rems(32.))
|
||||
.p_4()
|
||||
.gap_4()
|
||||
.child(v_stack().child(
|
||||
// TODO: Add Headline component to `ui2`
|
||||
div().text_xl().child("Share Feedback"),
|
||||
))
|
||||
.gap_2()
|
||||
.child(Headline::new("Share Feedback"))
|
||||
.child(
|
||||
Label::new(if self.character_count < *FEEDBACK_CHAR_LIMIT.start() {
|
||||
format!(
|
||||
|
@ -468,17 +460,26 @@ impl Render for FeedbackModal {
|
|||
.child(self.feedback_editor.clone()),
|
||||
)
|
||||
.child(
|
||||
h_stack()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.p_2()
|
||||
.border()
|
||||
.rounded_md()
|
||||
.border_color(if self.valid_email_address() {
|
||||
cx.theme().colors().border
|
||||
} else {
|
||||
red()
|
||||
})
|
||||
.child(self.email_address_editor.clone()),
|
||||
v_stack()
|
||||
.gap_1()
|
||||
.child(
|
||||
h_stack()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.p_2()
|
||||
.border()
|
||||
.rounded_md()
|
||||
.border_color(if self.valid_email_address() {
|
||||
cx.theme().colors().border
|
||||
} else {
|
||||
cx.theme().status().error_border
|
||||
})
|
||||
.child(self.email_address_editor.clone()),
|
||||
)
|
||||
.child(
|
||||
Label::new("Provide an email address if you want us to be able to reply.")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_stack()
|
||||
|
@ -487,7 +488,7 @@ impl Render for FeedbackModal {
|
|||
.child(
|
||||
Button::new("community_repository", "Community Repository")
|
||||
.style(ButtonStyle::Transparent)
|
||||
.icon(Icon::ExternalLink)
|
||||
.icon(IconName::ExternalLink)
|
||||
.icon_position(IconPosition::End)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(open_community_repo),
|
||||
|
@ -515,12 +516,7 @@ impl Render for FeedbackModal {
|
|||
this.submit(cx).detach();
|
||||
}))
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::with_meta(
|
||||
"Submit feedback to the Zed team.",
|
||||
None,
|
||||
provide_an_email_address,
|
||||
cx,
|
||||
)
|
||||
Tooltip::text("Submit feedback to the Zed team.", cx)
|
||||
})
|
||||
.when(!self.can_submit(), |this| this.disabled(true)),
|
||||
),
|
||||
|
|
|
@ -1297,7 +1297,7 @@ mod tests {
|
|||
// so that one should be sorted earlier
|
||||
let b_path = ProjectPath {
|
||||
worktree_id,
|
||||
path: Arc::from(Path::new("/root/dir2/b.txt")),
|
||||
path: Arc::from(Path::new("dir2/b.txt")),
|
||||
};
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
|
|
|
@ -50,7 +50,7 @@ impl Render for Menu {
|
|||
.on_action(|this, move: &MoveDown, cx| {
|
||||
// ...
|
||||
})
|
||||
.children(todo!())
|
||||
.children(unimplemented!())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
@ -68,7 +68,7 @@ impl Render for Menu {
|
|||
.on_action(|this, move: &MoveDown, cx| {
|
||||
// ...
|
||||
})
|
||||
.children(todo!())
|
||||
.children(unimplemented!())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
@ -104,7 +104,7 @@ pub struct ActionData {
|
|||
}
|
||||
|
||||
/// This constant must be public to be accessible from other crates.
|
||||
/// But it's existence is an implementation detail and should not be used directly.
|
||||
/// But its existence is an implementation detail and should not be used directly.
|
||||
#[doc(hidden)]
|
||||
#[linkme::distributed_slice]
|
||||
pub static __GPUI_ACTIONS: [MacroActionBuilder];
|
||||
|
@ -114,14 +114,26 @@ impl ActionRegistry {
|
|||
pub(crate) fn load_actions(&mut self) {
|
||||
for builder in __GPUI_ACTIONS {
|
||||
let action = builder();
|
||||
//todo(remove)
|
||||
let name: SharedString = action.name.into();
|
||||
self.builders_by_name.insert(name.clone(), action.build);
|
||||
self.names_by_type_id.insert(action.type_id, name.clone());
|
||||
self.all_names.push(name);
|
||||
self.insert_action(action);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn load_action<A: Action>(&mut self) {
|
||||
self.insert_action(ActionData {
|
||||
name: A::debug_name(),
|
||||
type_id: TypeId::of::<A>(),
|
||||
build: A::build,
|
||||
});
|
||||
}
|
||||
|
||||
fn insert_action(&mut self, action: ActionData) {
|
||||
let name: SharedString = action.name.into();
|
||||
self.builders_by_name.insert(name.clone(), action.build);
|
||||
self.names_by_type_id.insert(action.type_id, name.clone());
|
||||
self.all_names.push(name);
|
||||
}
|
||||
|
||||
/// Construct an action based on its name and optional JSON parameters sourced from the keymap.
|
||||
pub fn build_action_type(&self, type_id: &TypeId) -> Result<Box<dyn Action>> {
|
||||
let name = self
|
||||
|
@ -203,7 +215,6 @@ macro_rules! __impl_action {
|
|||
)
|
||||
}
|
||||
|
||||
// todo!() why is this needed in addition to name?
|
||||
fn debug_name() -> &'static str
|
||||
where
|
||||
Self: ::std::marker::Sized
|
||||
|
|
|
@ -45,11 +45,13 @@ use util::{
|
|||
|
||||
/// Temporary(?) wrapper around [`RefCell<AppContext>`] to help us debug any double borrows.
|
||||
/// Strongly consider removing after stabilization.
|
||||
#[doc(hidden)]
|
||||
pub struct AppCell {
|
||||
app: RefCell<AppContext>,
|
||||
}
|
||||
|
||||
impl AppCell {
|
||||
#[doc(hidden)]
|
||||
#[track_caller]
|
||||
pub fn borrow(&self) -> AppRef {
|
||||
if option_env!("TRACK_THREAD_BORROWS").is_some() {
|
||||
|
@ -59,6 +61,7 @@ impl AppCell {
|
|||
AppRef(self.app.borrow())
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[track_caller]
|
||||
pub fn borrow_mut(&self) -> AppRefMut {
|
||||
if option_env!("TRACK_THREAD_BORROWS").is_some() {
|
||||
|
@ -69,6 +72,7 @@ impl AppCell {
|
|||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[derive(Deref, DerefMut)]
|
||||
pub struct AppRef<'a>(Ref<'a, AppContext>);
|
||||
|
||||
|
@ -81,6 +85,7 @@ impl<'a> Drop for AppRef<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[derive(Deref, DerefMut)]
|
||||
pub struct AppRefMut<'a>(RefMut<'a, AppContext>);
|
||||
|
||||
|
@ -93,6 +98,8 @@ impl<'a> Drop for AppRefMut<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
/// A reference to a GPUI application, typically constructed in the `main` function of your app.
|
||||
/// You won't interact with this type much outside of initial configuration and startup.
|
||||
pub struct App(Rc<AppCell>);
|
||||
|
||||
/// Represents an application before it is fully launched. Once your app is
|
||||
|
@ -136,6 +143,8 @@ impl App {
|
|||
self
|
||||
}
|
||||
|
||||
/// Invokes a handler when an already-running application is launched.
|
||||
/// On macOS, this can occur when the application icon is double-clicked or the app is launched via the dock.
|
||||
pub fn on_reopen<F>(&self, mut callback: F) -> &Self
|
||||
where
|
||||
F: 'static + FnMut(&mut AppContext),
|
||||
|
@ -149,18 +158,22 @@ impl App {
|
|||
self
|
||||
}
|
||||
|
||||
/// Returns metadata associated with the application
|
||||
pub fn metadata(&self) -> AppMetadata {
|
||||
self.0.borrow().app_metadata.clone()
|
||||
}
|
||||
|
||||
/// Returns a handle to the [`BackgroundExecutor`] associated with this app, which can be used to spawn futures in the background.
|
||||
pub fn background_executor(&self) -> BackgroundExecutor {
|
||||
self.0.borrow().background_executor.clone()
|
||||
}
|
||||
|
||||
/// Returns a handle to the [`ForegroundExecutor`] associated with this app, which can be used to spawn futures in the foreground.
|
||||
pub fn foreground_executor(&self) -> ForegroundExecutor {
|
||||
self.0.borrow().foreground_executor.clone()
|
||||
}
|
||||
|
||||
/// Returns a reference to the [`TextSystem`] associated with this app.
|
||||
pub fn text_system(&self) -> Arc<TextSystem> {
|
||||
self.0.borrow().text_system.clone()
|
||||
}
|
||||
|
@ -174,12 +187,6 @@ type QuitHandler = Box<dyn FnOnce(&mut AppContext) -> LocalBoxFuture<'static, ()
|
|||
type ReleaseListener = Box<dyn FnOnce(&mut dyn Any, &mut AppContext) + 'static>;
|
||||
type NewViewListener = Box<dyn FnMut(AnyView, &mut WindowContext) + 'static>;
|
||||
|
||||
// struct FrameConsumer {
|
||||
// next_frame_callbacks: Vec<FrameCallback>,
|
||||
// task: Task<()>,
|
||||
// display_linker
|
||||
// }
|
||||
|
||||
pub struct AppContext {
|
||||
pub(crate) this: Weak<AppCell>,
|
||||
pub(crate) platform: Rc<dyn Platform>,
|
||||
|
@ -292,7 +299,7 @@ impl AppContext {
|
|||
app
|
||||
}
|
||||
|
||||
/// Quit the application gracefully. Handlers registered with `ModelContext::on_app_quit`
|
||||
/// Quit the application gracefully. Handlers registered with [`ModelContext::on_app_quit`]
|
||||
/// will be given 100ms to complete before exiting.
|
||||
pub fn shutdown(&mut self) {
|
||||
let mut futures = Vec::new();
|
||||
|
@ -314,10 +321,12 @@ impl AppContext {
|
|||
}
|
||||
}
|
||||
|
||||
/// Gracefully quit the application via the platform's standard routine.
|
||||
pub fn quit(&mut self) {
|
||||
self.platform.quit();
|
||||
}
|
||||
|
||||
/// Get metadata about the app and platform.
|
||||
pub fn app_metadata(&self) -> AppMetadata {
|
||||
self.app_metadata.clone()
|
||||
}
|
||||
|
@ -340,6 +349,7 @@ impl AppContext {
|
|||
result
|
||||
}
|
||||
|
||||
/// Arrange a callback to be invoked when the given model or view calls `notify` on its respective context.
|
||||
pub fn observe<W, E>(
|
||||
&mut self,
|
||||
entity: &E,
|
||||
|
@ -355,7 +365,7 @@ impl AppContext {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn observe_internal<W, E>(
|
||||
pub(crate) fn observe_internal<W, E>(
|
||||
&mut self,
|
||||
entity: &E,
|
||||
mut on_notify: impl FnMut(E, &mut AppContext) -> bool + 'static,
|
||||
|
@ -380,15 +390,17 @@ impl AppContext {
|
|||
subscription
|
||||
}
|
||||
|
||||
pub fn subscribe<T, E, Evt>(
|
||||
/// Arrange for the given callback to be invoked whenever the given model or view emits an event of a given type.
|
||||
/// The callback is provided a handle to the emitting entity and a reference to the emitted event.
|
||||
pub fn subscribe<T, E, Event>(
|
||||
&mut self,
|
||||
entity: &E,
|
||||
mut on_event: impl FnMut(E, &Evt, &mut AppContext) + 'static,
|
||||
mut on_event: impl FnMut(E, &Event, &mut AppContext) + 'static,
|
||||
) -> Subscription
|
||||
where
|
||||
T: 'static + EventEmitter<Evt>,
|
||||
T: 'static + EventEmitter<Event>,
|
||||
E: Entity<T>,
|
||||
Evt: 'static,
|
||||
Event: 'static,
|
||||
{
|
||||
self.subscribe_internal(entity, move |entity, event, cx| {
|
||||
on_event(entity, event, cx);
|
||||
|
@ -426,6 +438,9 @@ impl AppContext {
|
|||
subscription
|
||||
}
|
||||
|
||||
/// Returns handles to all open windows in the application.
|
||||
/// Each handle could be downcast to a handle typed for the root view of that window.
|
||||
/// To find all windows of a given type, you could filter on
|
||||
pub fn windows(&self) -> Vec<AnyWindowHandle> {
|
||||
self.windows
|
||||
.values()
|
||||
|
@ -565,7 +580,7 @@ impl AppContext {
|
|||
self.pending_effects.push_back(effect);
|
||||
}
|
||||
|
||||
/// Called at the end of AppContext::update to complete any side effects
|
||||
/// Called at the end of [`AppContext::update`] to complete any side effects
|
||||
/// such as notifying observers, emitting events, etc. Effects can themselves
|
||||
/// cause effects, so we continue looping until all effects are processed.
|
||||
fn flush_effects(&mut self) {
|
||||
|
|
|
@ -82,6 +82,7 @@ impl Context for AsyncAppContext {
|
|||
}
|
||||
|
||||
impl AsyncAppContext {
|
||||
/// Schedules all windows in the application to be redrawn.
|
||||
pub fn refresh(&mut self) -> Result<()> {
|
||||
let app = self
|
||||
.app
|
||||
|
@ -92,14 +93,17 @@ impl AsyncAppContext {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Get an executor which can be used to spawn futures in the background.
|
||||
pub fn background_executor(&self) -> &BackgroundExecutor {
|
||||
&self.background_executor
|
||||
}
|
||||
|
||||
/// Get an executor which can be used to spawn futures in the foreground.
|
||||
pub fn foreground_executor(&self) -> &ForegroundExecutor {
|
||||
&self.foreground_executor
|
||||
}
|
||||
|
||||
/// Invoke the given function in the context of the app, then flush any effects produced during its invocation.
|
||||
pub fn update<R>(&self, f: impl FnOnce(&mut AppContext) -> R) -> Result<R> {
|
||||
let app = self
|
||||
.app
|
||||
|
@ -109,6 +113,7 @@ impl AsyncAppContext {
|
|||
Ok(f(&mut lock))
|
||||
}
|
||||
|
||||
/// Open a window with the given options based on the root view returned by the given function.
|
||||
pub fn open_window<V>(
|
||||
&self,
|
||||
options: crate::WindowOptions,
|
||||
|
@ -125,6 +130,7 @@ impl AsyncAppContext {
|
|||
Ok(lock.open_window(options, build_root_view))
|
||||
}
|
||||
|
||||
/// Schedule a future to be polled in the background.
|
||||
pub fn spawn<Fut, R>(&self, f: impl FnOnce(AsyncAppContext) -> Fut) -> Task<R>
|
||||
where
|
||||
Fut: Future<Output = R> + 'static,
|
||||
|
|
|
@ -19,7 +19,10 @@ use std::{
|
|||
#[cfg(any(test, feature = "test-support"))]
|
||||
use collections::HashMap;
|
||||
|
||||
slotmap::new_key_type! { pub struct EntityId; }
|
||||
slotmap::new_key_type! {
|
||||
/// A unique identifier for a model or view across the application.
|
||||
pub struct EntityId;
|
||||
}
|
||||
|
||||
impl EntityId {
|
||||
pub fn as_u64(self) -> u64 {
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#![deny(missing_docs)]
|
||||
|
||||
use crate::{
|
||||
div, Action, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext,
|
||||
BackgroundExecutor, ClipboardItem, Context, Entity, EventEmitter, ForegroundExecutor,
|
||||
|
@ -9,14 +11,21 @@ use anyhow::{anyhow, bail};
|
|||
use futures::{Stream, StreamExt};
|
||||
use std::{future::Future, ops::Deref, rc::Rc, sync::Arc, time::Duration};
|
||||
|
||||
/// A TestAppContext is provided to tests created with `#[gpui::test]`, it provides
|
||||
/// an implementation of `Context` with additional methods that are useful in tests.
|
||||
#[derive(Clone)]
|
||||
pub struct TestAppContext {
|
||||
#[doc(hidden)]
|
||||
pub app: Rc<AppCell>,
|
||||
#[doc(hidden)]
|
||||
pub background_executor: BackgroundExecutor,
|
||||
#[doc(hidden)]
|
||||
pub foreground_executor: ForegroundExecutor,
|
||||
#[doc(hidden)]
|
||||
pub dispatcher: TestDispatcher,
|
||||
pub test_platform: Rc<TestPlatform>,
|
||||
test_platform: Rc<TestPlatform>,
|
||||
text_system: Arc<TextSystem>,
|
||||
fn_name: Option<&'static str>,
|
||||
}
|
||||
|
||||
impl Context for TestAppContext {
|
||||
|
@ -76,7 +85,8 @@ impl Context for TestAppContext {
|
|||
}
|
||||
|
||||
impl TestAppContext {
|
||||
pub fn new(dispatcher: TestDispatcher) -> Self {
|
||||
/// Creates a new `TestAppContext`. Usually you can rely on `#[gpui::test]` to do this for you.
|
||||
pub fn new(dispatcher: TestDispatcher, fn_name: Option<&'static str>) -> Self {
|
||||
let arc_dispatcher = Arc::new(dispatcher.clone());
|
||||
let background_executor = BackgroundExecutor::new(arc_dispatcher.clone());
|
||||
let foreground_executor = ForegroundExecutor::new(arc_dispatcher);
|
||||
|
@ -92,41 +102,61 @@ impl TestAppContext {
|
|||
dispatcher: dispatcher.clone(),
|
||||
test_platform: platform,
|
||||
text_system,
|
||||
fn_name,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_app(&self) -> TestAppContext {
|
||||
Self::new(self.dispatcher.clone())
|
||||
/// The name of the test function that created this `TestAppContext`
|
||||
pub fn test_function_name(&self) -> Option<&'static str> {
|
||||
self.fn_name
|
||||
}
|
||||
|
||||
/// Checks whether there have been any new path prompts received by the platform.
|
||||
pub fn did_prompt_for_new_path(&self) -> bool {
|
||||
self.test_platform.did_prompt_for_new_path()
|
||||
}
|
||||
|
||||
/// returns a new `TestAppContext` re-using the same executors to interleave tasks.
|
||||
pub fn new_app(&self) -> TestAppContext {
|
||||
Self::new(self.dispatcher.clone(), self.fn_name)
|
||||
}
|
||||
|
||||
/// Simulates quitting the app.
|
||||
pub fn quit(&self) {
|
||||
self.app.borrow_mut().shutdown();
|
||||
}
|
||||
|
||||
/// Schedules all windows to be redrawn on the next effect cycle.
|
||||
pub fn refresh(&mut self) -> Result<()> {
|
||||
let mut app = self.app.borrow_mut();
|
||||
app.refresh();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns an executor (for running tasks in the background)
|
||||
pub fn executor(&self) -> BackgroundExecutor {
|
||||
self.background_executor.clone()
|
||||
}
|
||||
|
||||
/// Returns an executor (for running tasks on the main thread)
|
||||
pub fn foreground_executor(&self) -> &ForegroundExecutor {
|
||||
&self.foreground_executor
|
||||
}
|
||||
|
||||
/// Gives you an `&mut AppContext` for the duration of the closure
|
||||
pub fn update<R>(&self, f: impl FnOnce(&mut AppContext) -> R) -> R {
|
||||
let mut cx = self.app.borrow_mut();
|
||||
cx.update(f)
|
||||
}
|
||||
|
||||
/// Gives you an `&AppContext` for the duration of the closure
|
||||
pub fn read<R>(&self, f: impl FnOnce(&AppContext) -> R) -> R {
|
||||
let cx = self.app.borrow();
|
||||
f(&*cx)
|
||||
}
|
||||
|
||||
/// Adds a new window. The Window will always be backed by a `TestWindow` which
|
||||
/// can be retrieved with `self.test_window(handle)`
|
||||
pub fn add_window<F, V>(&mut self, build_window: F) -> WindowHandle<V>
|
||||
where
|
||||
F: FnOnce(&mut ViewContext<V>) -> V,
|
||||
|
@ -136,12 +166,16 @@ impl TestAppContext {
|
|||
cx.open_window(WindowOptions::default(), |cx| cx.new_view(build_window))
|
||||
}
|
||||
|
||||
/// Adds a new window with no content.
|
||||
pub fn add_empty_window(&mut self) -> AnyWindowHandle {
|
||||
let mut cx = self.app.borrow_mut();
|
||||
cx.open_window(WindowOptions::default(), |cx| cx.new_view(|_| EmptyView {}))
|
||||
.any_handle
|
||||
}
|
||||
|
||||
/// Adds a new window, and returns its root view and a `VisualTestContext` which can be used
|
||||
/// as a `WindowContext` for the rest of the test. Typically you would shadow this context with
|
||||
/// the returned one. `let (view, cx) = cx.add_window_view(...);`
|
||||
pub fn add_window_view<F, V>(&mut self, build_window: F) -> (View<V>, &mut VisualTestContext)
|
||||
where
|
||||
F: FnOnce(&mut ViewContext<V>) -> V,
|
||||
|
@ -152,22 +186,28 @@ impl TestAppContext {
|
|||
drop(cx);
|
||||
let view = window.root_view(self).unwrap();
|
||||
let cx = Box::new(VisualTestContext::from_window(*window.deref(), self));
|
||||
cx.run_until_parked();
|
||||
// it might be nice to try and cleanup these at the end of each test.
|
||||
(view, Box::leak(cx))
|
||||
}
|
||||
|
||||
/// returns the TextSystem
|
||||
pub fn text_system(&self) -> &Arc<TextSystem> {
|
||||
&self.text_system
|
||||
}
|
||||
|
||||
/// Simulates writing to the platform clipboard
|
||||
pub fn write_to_clipboard(&self, item: ClipboardItem) {
|
||||
self.test_platform.write_to_clipboard(item)
|
||||
}
|
||||
|
||||
/// Simulates reading from the platform clipboard.
|
||||
/// This will return the most recent value from `write_to_clipboard`.
|
||||
pub fn read_from_clipboard(&self) -> Option<ClipboardItem> {
|
||||
self.test_platform.read_from_clipboard()
|
||||
}
|
||||
|
||||
/// Simulates choosing a File in the platform's "Open" dialog.
|
||||
pub fn simulate_new_path_selection(
|
||||
&self,
|
||||
select_path: impl FnOnce(&std::path::Path) -> Option<std::path::PathBuf>,
|
||||
|
@ -175,22 +215,27 @@ impl TestAppContext {
|
|||
self.test_platform.simulate_new_path_selection(select_path);
|
||||
}
|
||||
|
||||
/// Simulates clicking a button in an platform-level alert dialog.
|
||||
pub fn simulate_prompt_answer(&self, button_ix: usize) {
|
||||
self.test_platform.simulate_prompt_answer(button_ix);
|
||||
}
|
||||
|
||||
/// Returns true if there's an alert dialog open.
|
||||
pub fn has_pending_prompt(&self) -> bool {
|
||||
self.test_platform.has_pending_prompt()
|
||||
}
|
||||
|
||||
/// Simulates the user resizing the window to the new size.
|
||||
pub fn simulate_window_resize(&self, window_handle: AnyWindowHandle, size: Size<Pixels>) {
|
||||
self.test_window(window_handle).simulate_resize(size);
|
||||
}
|
||||
|
||||
/// Returns all windows open in the test.
|
||||
pub fn windows(&self) -> Vec<AnyWindowHandle> {
|
||||
self.app.borrow().windows().clone()
|
||||
}
|
||||
|
||||
/// Run the given task on the main thread.
|
||||
pub fn spawn<Fut, R>(&self, f: impl FnOnce(AsyncAppContext) -> Fut) -> Task<R>
|
||||
where
|
||||
Fut: Future<Output = R> + 'static,
|
||||
|
@ -199,16 +244,20 @@ impl TestAppContext {
|
|||
self.foreground_executor.spawn(f(self.to_async()))
|
||||
}
|
||||
|
||||
/// true if the given global is defined
|
||||
pub fn has_global<G: 'static>(&self) -> bool {
|
||||
let app = self.app.borrow();
|
||||
app.has_global::<G>()
|
||||
}
|
||||
|
||||
/// runs the given closure with a reference to the global
|
||||
/// panics if `has_global` would return false.
|
||||
pub fn read_global<G: 'static, R>(&self, read: impl FnOnce(&G, &AppContext) -> R) -> R {
|
||||
let app = self.app.borrow();
|
||||
read(app.global(), &app)
|
||||
}
|
||||
|
||||
/// runs the given closure with a reference to the global (if set)
|
||||
pub fn try_read_global<G: 'static, R>(
|
||||
&self,
|
||||
read: impl FnOnce(&G, &AppContext) -> R,
|
||||
|
@ -217,11 +266,13 @@ impl TestAppContext {
|
|||
Some(read(lock.try_global()?, &lock))
|
||||
}
|
||||
|
||||
/// sets the global in this context.
|
||||
pub fn set_global<G: 'static>(&mut self, global: G) {
|
||||
let mut lock = self.app.borrow_mut();
|
||||
lock.set_global(global);
|
||||
}
|
||||
|
||||
/// updates the global in this context. (panics if `has_global` would return false)
|
||||
pub fn update_global<G: 'static, R>(
|
||||
&mut self,
|
||||
update: impl FnOnce(&mut G, &mut AppContext) -> R,
|
||||
|
@ -230,6 +281,8 @@ impl TestAppContext {
|
|||
lock.update_global(update)
|
||||
}
|
||||
|
||||
/// Returns an `AsyncAppContext` which can be used to run tasks that expect to be on a background
|
||||
/// thread on the current thread in tests.
|
||||
pub fn to_async(&self) -> AsyncAppContext {
|
||||
AsyncAppContext {
|
||||
app: Rc::downgrade(&self.app),
|
||||
|
@ -238,6 +291,12 @@ impl TestAppContext {
|
|||
}
|
||||
}
|
||||
|
||||
/// Wait until there are no more pending tasks.
|
||||
pub fn run_until_parked(&mut self) {
|
||||
self.background_executor.run_until_parked()
|
||||
}
|
||||
|
||||
/// Simulate dispatching an action to the currently focused node in the window.
|
||||
pub fn dispatch_action<A>(&mut self, window: AnyWindowHandle, action: A)
|
||||
where
|
||||
A: Action,
|
||||
|
@ -251,7 +310,8 @@ impl TestAppContext {
|
|||
|
||||
/// simulate_keystrokes takes a space-separated list of keys to type.
|
||||
/// cx.simulate_keystrokes("cmd-shift-p b k s p enter")
|
||||
/// will run backspace on the current editor through the command palette.
|
||||
/// in Zed, this will run backspace on the current editor through the command palette.
|
||||
/// This will also run the background executor until it's parked.
|
||||
pub fn simulate_keystrokes(&mut self, window: AnyWindowHandle, keystrokes: &str) {
|
||||
for keystroke in keystrokes
|
||||
.split(" ")
|
||||
|
@ -266,7 +326,8 @@ impl TestAppContext {
|
|||
|
||||
/// simulate_input takes a string of text to type.
|
||||
/// cx.simulate_input("abc")
|
||||
/// will type abc into your current editor.
|
||||
/// will type abc into your current editor
|
||||
/// This will also run the background executor until it's parked.
|
||||
pub fn simulate_input(&mut self, window: AnyWindowHandle, input: &str) {
|
||||
for keystroke in input.split("").map(Keystroke::parse).map(Result::unwrap) {
|
||||
self.dispatch_keystroke(window, keystroke.into(), false);
|
||||
|
@ -275,6 +336,7 @@ impl TestAppContext {
|
|||
self.background_executor.run_until_parked()
|
||||
}
|
||||
|
||||
/// dispatches a single Keystroke (see also `simulate_keystrokes` and `simulate_input`)
|
||||
pub fn dispatch_keystroke(
|
||||
&mut self,
|
||||
window: AnyWindowHandle,
|
||||
|
@ -285,6 +347,7 @@ impl TestAppContext {
|
|||
.simulate_keystroke(keystroke, is_held)
|
||||
}
|
||||
|
||||
/// Returns the `TestWindow` backing the given handle.
|
||||
pub fn test_window(&self, window: AnyWindowHandle) -> TestWindow {
|
||||
self.app
|
||||
.borrow_mut()
|
||||
|
@ -299,6 +362,7 @@ impl TestAppContext {
|
|||
.clone()
|
||||
}
|
||||
|
||||
/// Returns a stream of notifications whenever the View or Model is updated.
|
||||
pub fn notifications<T: 'static>(&mut self, entity: &impl Entity<T>) -> impl Stream<Item = ()> {
|
||||
let (tx, rx) = futures::channel::mpsc::unbounded();
|
||||
self.update(|cx| {
|
||||
|
@ -315,6 +379,7 @@ impl TestAppContext {
|
|||
rx
|
||||
}
|
||||
|
||||
/// Retuens a stream of events emitted by the given Model.
|
||||
pub fn events<Evt, T: 'static + EventEmitter<Evt>>(
|
||||
&mut self,
|
||||
entity: &Model<T>,
|
||||
|
@ -333,6 +398,8 @@ impl TestAppContext {
|
|||
rx
|
||||
}
|
||||
|
||||
/// Runs until the given condition becomes true. (Prefer `run_until_parked` if you
|
||||
/// don't need to jump in at a specific time).
|
||||
pub async fn condition<T: 'static>(
|
||||
&mut self,
|
||||
model: &Model<T>,
|
||||
|
@ -362,6 +429,7 @@ impl TestAppContext {
|
|||
}
|
||||
|
||||
impl<T: Send> Model<T> {
|
||||
/// Block until the next event is emitted by the model, then return it.
|
||||
pub fn next_event<Evt>(&self, cx: &mut TestAppContext) -> Evt
|
||||
where
|
||||
Evt: Send + Clone + 'static,
|
||||
|
@ -391,6 +459,7 @@ impl<T: Send> Model<T> {
|
|||
}
|
||||
|
||||
impl<V: 'static> View<V> {
|
||||
/// Returns a future that resolves when the view is next updated.
|
||||
pub fn next_notification(&self, cx: &TestAppContext) -> impl Future<Output = ()> {
|
||||
use postage::prelude::{Sink as _, Stream as _};
|
||||
|
||||
|
@ -417,6 +486,7 @@ impl<V: 'static> View<V> {
|
|||
}
|
||||
|
||||
impl<V> View<V> {
|
||||
/// Returns a future that resolves when the condition becomes true.
|
||||
pub fn condition<Evt>(
|
||||
&self,
|
||||
cx: &TestAppContext,
|
||||
|
@ -429,7 +499,7 @@ impl<V> View<V> {
|
|||
use postage::prelude::{Sink as _, Stream as _};
|
||||
|
||||
let (tx, mut rx) = postage::mpsc::channel(1024);
|
||||
let timeout_duration = Duration::from_millis(100); //todo!() cx.condition_duration();
|
||||
let timeout_duration = Duration::from_millis(100);
|
||||
|
||||
let mut cx = cx.app.borrow_mut();
|
||||
let subscriptions = (
|
||||
|
@ -467,12 +537,11 @@ impl<V> View<V> {
|
|||
}
|
||||
}
|
||||
|
||||
// todo!(start_waiting)
|
||||
// cx.borrow().foreground_executor().start_waiting();
|
||||
cx.borrow().background_executor().start_waiting();
|
||||
rx.recv()
|
||||
.await
|
||||
.expect("view dropped with pending condition");
|
||||
// cx.borrow().foreground_executor().finish_waiting();
|
||||
cx.borrow().background_executor().finish_waiting();
|
||||
}
|
||||
})
|
||||
.await
|
||||
|
@ -484,18 +553,25 @@ impl<V> View<V> {
|
|||
|
||||
use derive_more::{Deref, DerefMut};
|
||||
#[derive(Deref, DerefMut, Clone)]
|
||||
/// A VisualTestContext is the test-equivalent of a `WindowContext`. It allows you to
|
||||
/// run window-specific test code.
|
||||
pub struct VisualTestContext {
|
||||
#[deref]
|
||||
#[deref_mut]
|
||||
cx: TestAppContext,
|
||||
/// cx is the original TestAppContext (you can more easily access this using Deref)
|
||||
pub cx: TestAppContext,
|
||||
window: AnyWindowHandle,
|
||||
}
|
||||
|
||||
impl<'a> VisualTestContext {
|
||||
/// Provides the `WindowContext` for the duration of the closure.
|
||||
pub fn update<R>(&mut self, f: impl FnOnce(&mut WindowContext) -> R) -> R {
|
||||
self.cx.update_window(self.window, |_, cx| f(cx)).unwrap()
|
||||
}
|
||||
|
||||
/// Create a new VisualTestContext. You would typically shadow the passed in
|
||||
/// TestAppContext with this, as this is typically more useful.
|
||||
/// `let cx = VisualTestContext::from_window(window, cx);`
|
||||
pub fn from_window(window: AnyWindowHandle, cx: &TestAppContext) -> Self {
|
||||
Self {
|
||||
cx: cx.clone(),
|
||||
|
@ -503,10 +579,12 @@ impl<'a> VisualTestContext {
|
|||
}
|
||||
}
|
||||
|
||||
/// Wait until there are no more pending tasks.
|
||||
pub fn run_until_parked(&self) {
|
||||
self.cx.background_executor.run_until_parked();
|
||||
}
|
||||
|
||||
/// Dispatch the action to the currently focused node.
|
||||
pub fn dispatch_action<A>(&mut self, action: A)
|
||||
where
|
||||
A: Action,
|
||||
|
@ -514,24 +592,32 @@ impl<'a> VisualTestContext {
|
|||
self.cx.dispatch_action(self.window, action)
|
||||
}
|
||||
|
||||
/// Read the title off the window (set by `WindowContext#set_window_title`)
|
||||
pub fn window_title(&mut self) -> Option<String> {
|
||||
self.cx.test_window(self.window).0.lock().title.clone()
|
||||
}
|
||||
|
||||
/// Simulate a sequence of keystrokes `cx.simulate_keystrokes("cmd-p escape")`
|
||||
/// Automatically runs until parked.
|
||||
pub fn simulate_keystrokes(&mut self, keystrokes: &str) {
|
||||
self.cx.simulate_keystrokes(self.window, keystrokes)
|
||||
}
|
||||
|
||||
/// Simulate typing text `cx.simulate_input("hello")`
|
||||
/// Automatically runs until parked.
|
||||
pub fn simulate_input(&mut self, input: &str) {
|
||||
self.cx.simulate_input(self.window, input)
|
||||
}
|
||||
|
||||
/// Simulates the user blurring the window.
|
||||
pub fn deactivate_window(&mut self) {
|
||||
if Some(self.window) == self.test_platform.active_window() {
|
||||
self.test_platform.set_active_window(None)
|
||||
}
|
||||
self.background_executor.run_until_parked();
|
||||
}
|
||||
|
||||
/// Simulates the user closing the window.
|
||||
/// Returns true if the window was closed.
|
||||
pub fn simulate_close(&mut self) -> bool {
|
||||
let handler = self
|
||||
|
@ -668,6 +754,7 @@ impl VisualContext for VisualTestContext {
|
|||
}
|
||||
|
||||
impl AnyWindowHandle {
|
||||
/// Creates the given view in this window.
|
||||
pub fn build_view<V: Render + 'static>(
|
||||
&self,
|
||||
cx: &mut TestAppContext,
|
||||
|
@ -677,6 +764,7 @@ impl AnyWindowHandle {
|
|||
}
|
||||
}
|
||||
|
||||
/// An EmptyView for testing.
|
||||
pub struct EmptyView {}
|
||||
|
||||
impl Render for EmptyView {
|
||||
|
|
|
@ -66,18 +66,19 @@ impl Arena {
|
|||
}
|
||||
|
||||
unsafe {
|
||||
let layout = alloc::Layout::new::<T>().pad_to_align();
|
||||
let next_offset = self.offset.add(layout.size());
|
||||
assert!(next_offset <= self.end);
|
||||
let layout = alloc::Layout::new::<T>();
|
||||
let offset = self.offset.add(self.offset.align_offset(layout.align()));
|
||||
let next_offset = offset.add(layout.size());
|
||||
assert!(next_offset <= self.end, "not enough space in Arena");
|
||||
|
||||
let result = ArenaBox {
|
||||
ptr: self.offset.cast(),
|
||||
ptr: offset.cast(),
|
||||
valid: self.valid.clone(),
|
||||
};
|
||||
|
||||
inner_writer(result.ptr, f);
|
||||
self.elements.push(ArenaElement {
|
||||
value: self.offset,
|
||||
value: offset,
|
||||
drop: drop::<T>,
|
||||
});
|
||||
self.offset = next_offset;
|
||||
|
@ -199,4 +200,43 @@ mod tests {
|
|||
arena.clear();
|
||||
assert!(dropped.get());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "not enough space in Arena")]
|
||||
fn test_arena_overflow() {
|
||||
let mut arena = Arena::new(16);
|
||||
arena.alloc(|| 1u64);
|
||||
arena.alloc(|| 2u64);
|
||||
// This should panic.
|
||||
arena.alloc(|| 3u64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_arena_alignment() {
|
||||
let mut arena = Arena::new(256);
|
||||
let x1 = arena.alloc(|| 1u8);
|
||||
let x2 = arena.alloc(|| 2u16);
|
||||
let x3 = arena.alloc(|| 3u32);
|
||||
let x4 = arena.alloc(|| 4u64);
|
||||
let x5 = arena.alloc(|| 5u64);
|
||||
|
||||
assert_eq!(*x1, 1);
|
||||
assert_eq!(*x2, 2);
|
||||
assert_eq!(*x3, 3);
|
||||
assert_eq!(*x4, 4);
|
||||
assert_eq!(*x5, 5);
|
||||
|
||||
assert_eq!(x1.ptr.align_offset(std::mem::align_of_val(&*x1)), 0);
|
||||
assert_eq!(x2.ptr.align_offset(std::mem::align_of_val(&*x2)), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "attempted to dereference an ArenaRef after its Arena was cleared")]
|
||||
fn test_arena_use_after_clear() {
|
||||
let mut arena = Arena::new(16);
|
||||
let value = arena.alloc(|| 1u64);
|
||||
|
||||
arena.clear();
|
||||
let _read_value = *value;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -321,7 +321,7 @@ impl Hsla {
|
|||
///
|
||||
/// Assumptions:
|
||||
/// - Alpha values are contained in the range [0, 1], with 1 as fully opaque and 0 as fully transparent.
|
||||
/// - The relative contributions of `self` and `other` is based on `self`'s alpha value (`self.a`) and `other`'s alpha value (`other.a`), `self` contributing `self.a * (1.0 - other.a)` and `other` contributing it's own alpha value.
|
||||
/// - The relative contributions of `self` and `other` is based on `self`'s alpha value (`self.a`) and `other`'s alpha value (`other.a`), `self` contributing `self.a * (1.0 - other.a)` and `other` contributing its own alpha value.
|
||||
/// - RGB color components are contained in the range [0, 1].
|
||||
/// - If `self` and `other` colors are out of the valid range, the blend operation's output and behavior is undefined.
|
||||
pub fn blend(self, other: Hsla) -> Hsla {
|
||||
|
|
|
@ -31,14 +31,14 @@ pub trait IntoElement: Sized {
|
|||
/// The specific type of element into which the implementing type is converted.
|
||||
type Element: Element;
|
||||
|
||||
/// The [ElementId] of self once converted into an [Element].
|
||||
/// The [`ElementId`] of self once converted into an [`Element`].
|
||||
/// If present, the resulting element's state will be carried across frames.
|
||||
fn element_id(&self) -> Option<ElementId>;
|
||||
|
||||
/// Convert self into a type that implements [Element].
|
||||
/// Convert self into a type that implements [`Element`].
|
||||
fn into_element(self) -> Self::Element;
|
||||
|
||||
/// Convert self into a dynamically-typed [AnyElement].
|
||||
/// Convert self into a dynamically-typed [`AnyElement`].
|
||||
fn into_any_element(self) -> AnyElement {
|
||||
self.into_element().into_any()
|
||||
}
|
||||
|
@ -115,7 +115,7 @@ pub trait Render: 'static + Sized {
|
|||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement;
|
||||
}
|
||||
|
||||
/// You can derive [IntoElement] on any type that implements this trait.
|
||||
/// You can derive [`IntoElement`] on any type that implements this trait.
|
||||
/// It is used to allow views to be expressed in terms of abstract data.
|
||||
pub trait RenderOnce: 'static {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement;
|
||||
|
@ -224,7 +224,7 @@ enum ElementDrawPhase<S> {
|
|||
},
|
||||
}
|
||||
|
||||
/// A wrapper around an implementer of [Element] that allows it to be drawn in a window.
|
||||
/// A wrapper around an implementer of [`Element`] that allows it to be drawn in a window.
|
||||
impl<E: Element> DrawableElement<E> {
|
||||
fn new(element: E) -> Self {
|
||||
DrawableElement {
|
||||
|
|
|
@ -1003,7 +1003,7 @@ impl Interactivity {
|
|||
if let Some(text) = cx
|
||||
.text_system()
|
||||
.shape_text(
|
||||
&element_id,
|
||||
element_id.into(),
|
||||
FONT_SIZE,
|
||||
&[cx.text_style().to_run(str_len)],
|
||||
None,
|
||||
|
@ -1055,22 +1055,11 @@ impl Interactivity {
|
|||
};
|
||||
|
||||
eprintln!(
|
||||
"This element is created at:\n{}:{}:{}",
|
||||
location.file(),
|
||||
"This element was created at:\n{}:{}:{}",
|
||||
dir.join(location.file()).to_string_lossy(),
|
||||
location.line(),
|
||||
location.column()
|
||||
);
|
||||
|
||||
std::process::Command::new("zed")
|
||||
.arg(format!(
|
||||
"{}/{}:{}:{}",
|
||||
dir.to_string_lossy(),
|
||||
location.file(),
|
||||
location.line(),
|
||||
location.column()
|
||||
))
|
||||
.spawn()
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -2,7 +2,7 @@ use std::sync::Arc;
|
|||
|
||||
use crate::{
|
||||
point, size, BorrowWindow, Bounds, DevicePixels, Element, ImageData, InteractiveElement,
|
||||
InteractiveElementState, Interactivity, IntoElement, LayoutId, Pixels, SharedString, Size,
|
||||
InteractiveElementState, Interactivity, IntoElement, LayoutId, Pixels, SharedUrl, Size,
|
||||
StyleRefinement, Styled, WindowContext,
|
||||
};
|
||||
use futures::FutureExt;
|
||||
|
@ -12,13 +12,13 @@ use util::ResultExt;
|
|||
#[derive(Clone, Debug)]
|
||||
pub enum ImageSource {
|
||||
/// Image content will be loaded from provided URI at render time.
|
||||
Uri(SharedString),
|
||||
Uri(SharedUrl),
|
||||
Data(Arc<ImageData>),
|
||||
Surface(CVImageBuffer),
|
||||
}
|
||||
|
||||
impl From<SharedString> for ImageSource {
|
||||
fn from(value: SharedString) -> Self {
|
||||
impl From<SharedUrl> for ImageSource {
|
||||
fn from(value: SharedUrl) -> Self {
|
||||
Self::Uri(value)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,9 +14,8 @@ pub struct Overlay {
|
|||
children: SmallVec<[AnyElement; 2]>,
|
||||
anchor_corner: AnchorCorner,
|
||||
fit_mode: OverlayFitMode,
|
||||
// todo!();
|
||||
anchor_position: Option<Point<Pixels>>,
|
||||
// position_mode: OverlayPositionMode,
|
||||
position_mode: OverlayPositionMode,
|
||||
}
|
||||
|
||||
/// overlay gives you a floating element that will avoid overflowing the window bounds.
|
||||
|
@ -27,6 +26,7 @@ pub fn overlay() -> Overlay {
|
|||
anchor_corner: AnchorCorner::TopLeft,
|
||||
fit_mode: OverlayFitMode::SwitchAnchor,
|
||||
anchor_position: None,
|
||||
position_mode: OverlayPositionMode::Window,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -44,6 +44,14 @@ impl Overlay {
|
|||
self
|
||||
}
|
||||
|
||||
/// Sets the position mode for this overlay. Local will have this
|
||||
/// interpret its [`Overlay::position`] as relative to the parent element.
|
||||
/// While Window will have it interpret the position as relative to the window.
|
||||
pub fn position_mode(mut self, mode: OverlayPositionMode) -> Self {
|
||||
self.position_mode = mode;
|
||||
self
|
||||
}
|
||||
|
||||
/// Snap to window edge instead of switching anchor corner when an overflow would occur.
|
||||
pub fn snap_to_window(mut self) -> Self {
|
||||
self.fit_mode = OverlayFitMode::SnapToWindow;
|
||||
|
@ -100,9 +108,14 @@ impl Element for Overlay {
|
|||
child_max = child_max.max(&child_bounds.lower_right());
|
||||
}
|
||||
let size: Size<Pixels> = (child_max - child_min).into();
|
||||
let origin = self.anchor_position.unwrap_or(bounds.origin);
|
||||
|
||||
let mut desired = self.anchor_corner.get_bounds(origin, size);
|
||||
let (origin, mut desired) = self.position_mode.get_position_and_bounds(
|
||||
self.anchor_position,
|
||||
self.anchor_corner,
|
||||
size,
|
||||
bounds,
|
||||
);
|
||||
|
||||
let limits = Bounds {
|
||||
origin: Point::default(),
|
||||
size: cx.viewport_size(),
|
||||
|
@ -184,6 +197,35 @@ pub enum OverlayFitMode {
|
|||
SwitchAnchor,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq)]
|
||||
pub enum OverlayPositionMode {
|
||||
Window,
|
||||
Local,
|
||||
}
|
||||
|
||||
impl OverlayPositionMode {
|
||||
fn get_position_and_bounds(
|
||||
&self,
|
||||
anchor_position: Option<Point<Pixels>>,
|
||||
anchor_corner: AnchorCorner,
|
||||
size: Size<Pixels>,
|
||||
bounds: Bounds<Pixels>,
|
||||
) -> (Point<Pixels>, Bounds<Pixels>) {
|
||||
match self {
|
||||
OverlayPositionMode::Window => {
|
||||
let anchor_position = anchor_position.unwrap_or_else(|| bounds.origin);
|
||||
let bounds = anchor_corner.get_bounds(anchor_position, size);
|
||||
(anchor_position, bounds)
|
||||
}
|
||||
OverlayPositionMode::Local => {
|
||||
let anchor_position = anchor_position.unwrap_or_default();
|
||||
let bounds = anchor_corner.get_bounds(bounds.origin + anchor_position, size);
|
||||
(anchor_position, bounds)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub enum AnchorCorner {
|
||||
TopLeft,
|
||||
|
|
|
@ -202,7 +202,10 @@ impl TextState {
|
|||
let Some(lines) = cx
|
||||
.text_system()
|
||||
.shape_text(
|
||||
&text, font_size, &runs, wrap_width, // Wrap if we know the width.
|
||||
text.clone(),
|
||||
font_size,
|
||||
&runs,
|
||||
wrap_width, // Wrap if we know the width.
|
||||
)
|
||||
.log_err()
|
||||
else {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use crate::{
|
||||
point, px, size, AnyElement, AvailableSpace, BorrowWindow, Bounds, ContentMask, Element,
|
||||
ElementId, InteractiveElement, InteractiveElementState, Interactivity, IntoElement, LayoutId,
|
||||
Pixels, Point, Render, Size, StyleRefinement, Styled, View, ViewContext, WindowContext,
|
||||
Pixels, Render, Size, StyleRefinement, Styled, View, ViewContext, WindowContext,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
|
||||
|
@ -64,40 +64,19 @@ pub struct UniformList {
|
|||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct UniformListScrollHandle(Rc<RefCell<Option<ScrollHandleState>>>);
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct ScrollHandleState {
|
||||
item_height: Pixels,
|
||||
list_height: Pixels,
|
||||
scroll_offset: Rc<RefCell<Point<Pixels>>>,
|
||||
pub struct UniformListScrollHandle {
|
||||
deferred_scroll_to_item: Rc<RefCell<Option<usize>>>,
|
||||
}
|
||||
|
||||
impl UniformListScrollHandle {
|
||||
pub fn new() -> Self {
|
||||
Self(Rc::new(RefCell::new(None)))
|
||||
}
|
||||
|
||||
pub fn scroll_to_item(&self, ix: usize) {
|
||||
if let Some(state) = &*self.0.borrow() {
|
||||
let mut scroll_offset = state.scroll_offset.borrow_mut();
|
||||
let item_top = state.item_height * ix;
|
||||
let item_bottom = item_top + state.item_height;
|
||||
let scroll_top = -scroll_offset.y;
|
||||
if item_top < scroll_top {
|
||||
scroll_offset.y = -item_top;
|
||||
} else if item_bottom > scroll_top + state.list_height {
|
||||
scroll_offset.y = -(item_bottom - state.list_height);
|
||||
}
|
||||
Self {
|
||||
deferred_scroll_to_item: Rc::new(RefCell::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scroll_top(&self) -> Pixels {
|
||||
if let Some(state) = &*self.0.borrow() {
|
||||
-state.scroll_offset.borrow().y
|
||||
} else {
|
||||
Pixels::ZERO
|
||||
}
|
||||
pub fn scroll_to_item(&mut self, ix: usize) {
|
||||
self.deferred_scroll_to_item.replace(Some(ix));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -190,18 +169,14 @@ impl Element for UniformList {
|
|||
let shared_scroll_offset = element_state
|
||||
.interactive
|
||||
.scroll_offset
|
||||
.get_or_insert_with(|| {
|
||||
if let Some(scroll_handle) = self.scroll_handle.as_ref() {
|
||||
if let Some(scroll_handle) = scroll_handle.0.borrow().as_ref() {
|
||||
return scroll_handle.scroll_offset.clone();
|
||||
}
|
||||
}
|
||||
|
||||
Rc::default()
|
||||
})
|
||||
.get_or_insert_with(|| Rc::default())
|
||||
.clone();
|
||||
|
||||
let item_height = self.measure_item(Some(padded_bounds.size.width), cx).height;
|
||||
let shared_scroll_to_item = self
|
||||
.scroll_handle
|
||||
.as_mut()
|
||||
.and_then(|handle| handle.deferred_scroll_to_item.take());
|
||||
|
||||
self.interactivity.paint(
|
||||
bounds,
|
||||
|
@ -228,12 +203,18 @@ impl Element for UniformList {
|
|||
scroll_offset.y = min_scroll_offset;
|
||||
}
|
||||
|
||||
if let Some(scroll_handle) = self.scroll_handle.clone() {
|
||||
scroll_handle.0.borrow_mut().replace(ScrollHandleState {
|
||||
item_height,
|
||||
list_height: padded_bounds.size.height,
|
||||
scroll_offset: shared_scroll_offset,
|
||||
});
|
||||
if let Some(ix) = shared_scroll_to_item {
|
||||
let list_height = padded_bounds.size.height;
|
||||
let mut updated_scroll_offset = shared_scroll_offset.borrow_mut();
|
||||
let item_top = item_height * ix;
|
||||
let item_bottom = item_top + item_height;
|
||||
let scroll_top = -updated_scroll_offset.y;
|
||||
if item_top < scroll_top {
|
||||
updated_scroll_offset.y = -item_top;
|
||||
} else if item_bottom > scroll_top + list_height {
|
||||
updated_scroll_offset.y = -(item_bottom - list_height);
|
||||
}
|
||||
scroll_offset = *updated_scroll_offset;
|
||||
}
|
||||
|
||||
let first_visible_element_ix =
|
||||
|
|
|
@ -32,6 +32,12 @@ pub struct ForegroundExecutor {
|
|||
not_send: PhantomData<Rc<()>>,
|
||||
}
|
||||
|
||||
/// Task is a primitive that allows work to happen in the background.
|
||||
///
|
||||
/// It implements [`Future`] so you can `.await` on it.
|
||||
///
|
||||
/// If you drop a task it will be cancelled immediately. Calling [`Task::detach`] allows
|
||||
/// the task to continue running in the background, but with no way to return a value.
|
||||
#[must_use]
|
||||
#[derive(Debug)]
|
||||
pub enum Task<T> {
|
||||
|
@ -40,10 +46,12 @@ pub enum Task<T> {
|
|||
}
|
||||
|
||||
impl<T> Task<T> {
|
||||
/// Create a new task that will resolve with the value
|
||||
pub fn ready(val: T) -> Self {
|
||||
Task::Ready(Some(val))
|
||||
}
|
||||
|
||||
/// Detaching a task runs it to completion in the background
|
||||
pub fn detach(self) {
|
||||
match self {
|
||||
Task::Ready(_) => {}
|
||||
|
@ -57,6 +65,8 @@ where
|
|||
T: 'static,
|
||||
E: 'static + Debug,
|
||||
{
|
||||
/// Run the task to completion in the background and log any
|
||||
/// errors that occur.
|
||||
#[track_caller]
|
||||
pub fn detach_and_log_err(self, cx: &mut AppContext) {
|
||||
let location = core::panic::Location::caller();
|
||||
|
@ -97,6 +107,10 @@ type AnyLocalFuture<R> = Pin<Box<dyn 'static + Future<Output = R>>>;
|
|||
|
||||
type AnyFuture<R> = Pin<Box<dyn 'static + Send + Future<Output = R>>>;
|
||||
|
||||
/// BackgroundExecutor lets you run things on background threads.
|
||||
/// In production this is a thread pool with no ordering guarantees.
|
||||
/// In tests this is simalated by running tasks one by one in a deterministic
|
||||
/// (but arbitrary) order controlled by the `SEED` environment variable.
|
||||
impl BackgroundExecutor {
|
||||
pub fn new(dispatcher: Arc<dyn PlatformDispatcher>) -> Self {
|
||||
Self { dispatcher }
|
||||
|
@ -135,6 +149,7 @@ impl BackgroundExecutor {
|
|||
Task::Spawned(task)
|
||||
}
|
||||
|
||||
/// Used by the test harness to run an async test in a syncronous fashion.
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
#[track_caller]
|
||||
pub fn block_test<R>(&self, future: impl Future<Output = R>) -> R {
|
||||
|
@ -145,6 +160,8 @@ impl BackgroundExecutor {
|
|||
}
|
||||
}
|
||||
|
||||
/// Block the current thread until the given future resolves.
|
||||
/// Consider using `block_with_timeout` instead.
|
||||
pub fn block<R>(&self, future: impl Future<Output = R>) -> R {
|
||||
if let Ok(value) = self.block_internal(true, future, usize::MAX) {
|
||||
value
|
||||
|
@ -206,6 +223,8 @@ impl BackgroundExecutor {
|
|||
}
|
||||
}
|
||||
|
||||
/// Block the current thread until the given future resolves
|
||||
/// or `duration` has elapsed.
|
||||
pub fn block_with_timeout<R>(
|
||||
&self,
|
||||
duration: Duration,
|
||||
|
@ -238,6 +257,8 @@ impl BackgroundExecutor {
|
|||
}
|
||||
}
|
||||
|
||||
/// Scoped lets you start a number of tasks and waits
|
||||
/// for all of them to complete before returning.
|
||||
pub async fn scoped<'scope, F>(&self, scheduler: F)
|
||||
where
|
||||
F: FnOnce(&mut Scope<'scope>),
|
||||
|
@ -253,6 +274,9 @@ impl BackgroundExecutor {
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns a task that will complete after the given duration.
|
||||
/// Depending on other concurrent tasks the elapsed duration may be longer
|
||||
/// than reqested.
|
||||
pub fn timer(&self, duration: Duration) -> Task<()> {
|
||||
let (runnable, task) = async_task::spawn(async move {}, {
|
||||
let dispatcher = self.dispatcher.clone();
|
||||
|
@ -262,65 +286,81 @@ impl BackgroundExecutor {
|
|||
Task::Spawned(task)
|
||||
}
|
||||
|
||||
/// in tests, start_waiting lets you indicate which task is waiting (for debugging only)
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn start_waiting(&self) {
|
||||
self.dispatcher.as_test().unwrap().start_waiting();
|
||||
}
|
||||
|
||||
/// in tests, removes the debugging data added by start_waiting
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn finish_waiting(&self) {
|
||||
self.dispatcher.as_test().unwrap().finish_waiting();
|
||||
}
|
||||
|
||||
/// in tests, run an arbitrary number of tasks (determined by the SEED environment variable)
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn simulate_random_delay(&self) -> impl Future<Output = ()> {
|
||||
self.dispatcher.as_test().unwrap().simulate_random_delay()
|
||||
}
|
||||
|
||||
/// in tests, indicate that a given task from `spawn_labeled` should run after everything else
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn deprioritize(&self, task_label: TaskLabel) {
|
||||
self.dispatcher.as_test().unwrap().deprioritize(task_label)
|
||||
}
|
||||
|
||||
/// in tests, move time forward. This does not run any tasks, but does make `timer`s ready.
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn advance_clock(&self, duration: Duration) {
|
||||
self.dispatcher.as_test().unwrap().advance_clock(duration)
|
||||
}
|
||||
|
||||
/// in tests, run one task.
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn tick(&self) -> bool {
|
||||
self.dispatcher.as_test().unwrap().tick(false)
|
||||
}
|
||||
|
||||
/// in tests, run all tasks that are ready to run. If after doing so
|
||||
/// the test still has outstanding tasks, this will panic. (See also `allow_parking`)
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn run_until_parked(&self) {
|
||||
self.dispatcher.as_test().unwrap().run_until_parked()
|
||||
}
|
||||
|
||||
/// in tests, prevents `run_until_parked` from panicking if there are outstanding tasks.
|
||||
/// This is useful when you are integrating other (non-GPUI) futures, like disk access, that
|
||||
/// do take real async time to run.
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn allow_parking(&self) {
|
||||
self.dispatcher.as_test().unwrap().allow_parking();
|
||||
}
|
||||
|
||||
/// in tests, returns the rng used by the dispatcher and seeded by the `SEED` environment variable
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn rng(&self) -> StdRng {
|
||||
self.dispatcher.as_test().unwrap().rng()
|
||||
}
|
||||
|
||||
/// How many CPUs are available to the dispatcher
|
||||
pub fn num_cpus(&self) -> usize {
|
||||
num_cpus::get()
|
||||
}
|
||||
|
||||
/// Whether we're on the main thread.
|
||||
pub fn is_main_thread(&self) -> bool {
|
||||
self.dispatcher.is_main_thread()
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
/// in tests, control the number of ticks that `block_with_timeout` will run before timing out.
|
||||
pub fn set_block_on_ticks(&self, range: std::ops::RangeInclusive<usize>) {
|
||||
self.dispatcher.as_test().unwrap().set_block_on_ticks(range);
|
||||
}
|
||||
}
|
||||
|
||||
/// ForegroundExecutor runs things on the main thread.
|
||||
impl ForegroundExecutor {
|
||||
pub fn new(dispatcher: Arc<dyn PlatformDispatcher>) -> Self {
|
||||
Self {
|
||||
|
@ -329,8 +369,7 @@ impl ForegroundExecutor {
|
|||
}
|
||||
}
|
||||
|
||||
/// Enqueues the given closure to be run on any thread. The closure returns
|
||||
/// a future which will be run to completion on any available thread.
|
||||
/// Enqueues the given Task to run on the main thread at some point in the future.
|
||||
pub fn spawn<R>(&self, future: impl Future<Output = R> + 'static) -> Task<R>
|
||||
where
|
||||
R: 'static,
|
||||
|
@ -350,6 +389,7 @@ impl ForegroundExecutor {
|
|||
}
|
||||
}
|
||||
|
||||
/// Scope manages a set of tasks that are enqueued and waited on together. See [`BackgroundExecutor::scoped`].
|
||||
pub struct Scope<'a> {
|
||||
executor: BackgroundExecutor,
|
||||
futures: Vec<Pin<Box<dyn Future<Output = ()> + Send + 'static>>>,
|
||||
|
|
|
@ -18,6 +18,7 @@ mod platform;
|
|||
pub mod prelude;
|
||||
mod scene;
|
||||
mod shared_string;
|
||||
mod shared_url;
|
||||
mod style;
|
||||
mod styled;
|
||||
mod subscription;
|
||||
|
@ -67,6 +68,7 @@ pub use refineable::*;
|
|||
pub use scene::*;
|
||||
use seal::Sealed;
|
||||
pub use shared_string::*;
|
||||
pub use shared_url::*;
|
||||
pub use smol::Timer;
|
||||
pub use style::*;
|
||||
pub use styled::*;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::{ImageData, ImageId, SharedString};
|
||||
use crate::{ImageData, ImageId, SharedUrl};
|
||||
use collections::HashMap;
|
||||
use futures::{
|
||||
future::{BoxFuture, Shared},
|
||||
|
@ -44,7 +44,7 @@ impl From<ImageError> for Error {
|
|||
|
||||
pub struct ImageCache {
|
||||
client: Arc<dyn HttpClient>,
|
||||
images: Arc<Mutex<HashMap<SharedString, FetchImageFuture>>>,
|
||||
images: Arc<Mutex<HashMap<SharedUrl, FetchImageFuture>>>,
|
||||
}
|
||||
|
||||
type FetchImageFuture = Shared<BoxFuture<'static, Result<Arc<ImageData>, Error>>>;
|
||||
|
@ -59,7 +59,7 @@ impl ImageCache {
|
|||
|
||||
pub fn get(
|
||||
&self,
|
||||
uri: impl Into<SharedString>,
|
||||
uri: impl Into<SharedUrl>,
|
||||
) -> Shared<BoxFuture<'static, Result<Arc<ImageData>, Error>>> {
|
||||
let uri = uri.into();
|
||||
let mut images = self.images.lock();
|
||||
|
|
|
@ -34,7 +34,7 @@ pub trait InputHandler: 'static + Sized {
|
|||
) -> Option<Bounds<Pixels>>;
|
||||
}
|
||||
|
||||
/// The canonical implementation of `PlatformInputHandler`. Call `WindowContext::handle_input`
|
||||
/// The canonical implementation of [`PlatformInputHandler`]. Call [`WindowContext::handle_input`]
|
||||
/// with an instance during your element's paint.
|
||||
pub struct ElementInputHandler<V> {
|
||||
view: View<V>,
|
||||
|
|
|
@ -178,6 +178,20 @@ impl ScrollDelta {
|
|||
ScrollDelta::Lines(delta) => point(line_height * delta.x, line_height * delta.y),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn coalesce(self, other: ScrollDelta) -> ScrollDelta {
|
||||
match (self, other) {
|
||||
(ScrollDelta::Pixels(px_a), ScrollDelta::Pixels(px_b)) => {
|
||||
ScrollDelta::Pixels(px_a + px_b)
|
||||
}
|
||||
|
||||
(ScrollDelta::Lines(lines_a), ScrollDelta::Lines(lines_b)) => {
|
||||
ScrollDelta::Lines(lines_a + lines_b)
|
||||
}
|
||||
|
||||
_ => other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
|
|
|
@ -192,8 +192,8 @@ impl DispatchTree {
|
|||
keymap
|
||||
.bindings_for_action(action)
|
||||
.filter(|binding| {
|
||||
for i in 1..context_stack.len() {
|
||||
let context = &context_stack[0..i];
|
||||
for i in 0..context_stack.len() {
|
||||
let context = &context_stack[0..=i];
|
||||
if keymap.binding_enabled(binding, context) {
|
||||
return true;
|
||||
}
|
||||
|
@ -283,3 +283,76 @@ impl DispatchTree {
|
|||
*self.node_stack.last().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{rc::Rc, sync::Arc};
|
||||
|
||||
use parking_lot::Mutex;
|
||||
|
||||
use crate::{Action, ActionRegistry, DispatchTree, KeyBinding, KeyContext, Keymap};
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
struct TestAction;
|
||||
|
||||
impl Action for TestAction {
|
||||
fn name(&self) -> &'static str {
|
||||
"test::TestAction"
|
||||
}
|
||||
|
||||
fn debug_name() -> &'static str
|
||||
where
|
||||
Self: ::std::marker::Sized,
|
||||
{
|
||||
"test::TestAction"
|
||||
}
|
||||
|
||||
fn partial_eq(&self, action: &dyn Action) -> bool {
|
||||
action
|
||||
.as_any()
|
||||
.downcast_ref::<Self>()
|
||||
.map_or(false, |a| self == a)
|
||||
}
|
||||
|
||||
fn boxed_clone(&self) -> std::boxed::Box<dyn Action> {
|
||||
Box::new(TestAction)
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn ::std::any::Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn build(_value: serde_json::Value) -> anyhow::Result<Box<dyn Action>>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Ok(Box::new(TestAction))
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_keybinding_for_action_bounds() {
|
||||
let keymap = Keymap::new(vec![KeyBinding::new(
|
||||
"cmd-n",
|
||||
TestAction,
|
||||
Some("ProjectPanel"),
|
||||
)]);
|
||||
|
||||
let mut registry = ActionRegistry::default();
|
||||
|
||||
registry.load_action::<TestAction>();
|
||||
|
||||
let keymap = Arc::new(Mutex::new(keymap));
|
||||
|
||||
let tree = DispatchTree::new(keymap, Rc::new(registry));
|
||||
|
||||
let contexts = vec![
|
||||
KeyContext::parse("Workspace").unwrap(),
|
||||
KeyContext::parse("ProjectPanel").unwrap(),
|
||||
];
|
||||
|
||||
let keybinding = tree.bindings_for_action(&TestAction, &contexts);
|
||||
|
||||
assert!(keybinding[0].action.partial_eq(&TestAction))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,12 +14,12 @@ pub struct MacDisplay(pub(crate) CGDirectDisplayID);
|
|||
unsafe impl Send for MacDisplay {}
|
||||
|
||||
impl MacDisplay {
|
||||
/// Get the screen with the given [DisplayId].
|
||||
/// Get the screen with the given [`DisplayId`].
|
||||
pub fn find_by_id(id: DisplayId) -> Option<Self> {
|
||||
Self::all().find(|screen| screen.id() == id)
|
||||
}
|
||||
|
||||
/// Get the screen with the given persistent [Uuid].
|
||||
/// Get the screen with the given persistent [`Uuid`].
|
||||
pub fn find_by_uuid(uuid: Uuid) -> Option<Self> {
|
||||
Self::all().find(|screen| screen.uuid().ok() == Some(uuid))
|
||||
}
|
||||
|
|
|
@ -338,6 +338,7 @@ struct MacWindowState {
|
|||
ime_state: ImeState,
|
||||
// Retains the last IME Text
|
||||
ime_text: Option<String>,
|
||||
external_files_dragged: bool,
|
||||
}
|
||||
|
||||
impl MacWindowState {
|
||||
|
@ -567,6 +568,7 @@ impl MacWindow {
|
|||
previous_modifiers_changed_event: None,
|
||||
ime_state: ImeState::None,
|
||||
ime_text: None,
|
||||
external_files_dragged: false,
|
||||
})));
|
||||
|
||||
(*native_window).set_ivar(
|
||||
|
@ -1223,15 +1225,20 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
|
|||
..
|
||||
},
|
||||
) => {
|
||||
lock.synthetic_drag_counter += 1;
|
||||
let executor = lock.executor.clone();
|
||||
executor
|
||||
.spawn(synthetic_drag(
|
||||
weak_window_state,
|
||||
lock.synthetic_drag_counter,
|
||||
event.clone(),
|
||||
))
|
||||
.detach();
|
||||
// Synthetic drag is used for selecting long buffer contents while buffer is being scrolled.
|
||||
// External file drag and drop is able to emit its own synthetic mouse events which will conflict
|
||||
// with these ones.
|
||||
if !lock.external_files_dragged {
|
||||
lock.synthetic_drag_counter += 1;
|
||||
let executor = lock.executor.clone();
|
||||
executor
|
||||
.spawn(synthetic_drag(
|
||||
weak_window_state,
|
||||
lock.synthetic_drag_counter,
|
||||
event.clone(),
|
||||
))
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
InputEvent::MouseMove(_) if !(is_active || lock.kind == WindowKind::PopUp) => return,
|
||||
|
@ -1675,6 +1682,7 @@ extern "C" fn dragging_entered(this: &Object, _: Sel, dragging_info: id) -> NSDr
|
|||
let paths = external_paths_from_event(dragging_info);
|
||||
InputEvent::FileDrop(FileDropEvent::Entered { position, paths })
|
||||
}) {
|
||||
window_state.lock().external_files_dragged = true;
|
||||
NSDragOperationCopy
|
||||
} else {
|
||||
NSDragOperationNone
|
||||
|
@ -1697,6 +1705,7 @@ extern "C" fn dragging_updated(this: &Object, _: Sel, dragging_info: id) -> NSDr
|
|||
extern "C" fn dragging_exited(this: &Object, _: Sel, _: id) {
|
||||
let window_state = unsafe { get_window_state(this) };
|
||||
send_new_event(&window_state, InputEvent::FileDrop(FileDropEvent::Exited));
|
||||
window_state.lock().external_files_dragged = false;
|
||||
}
|
||||
|
||||
extern "C" fn perform_drag_operation(this: &Object, _: Sel, dragging_info: id) -> BOOL {
|
||||
|
|
|
@ -32,7 +32,7 @@ impl PlatformDisplay for TestDisplay {
|
|||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
todo!()
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn bounds(&self) -> crate::Bounds<crate::GlobalPixels> {
|
||||
|
|
|
@ -15,6 +15,7 @@ use std::{
|
|||
time::Duration,
|
||||
};
|
||||
|
||||
/// TestPlatform implements the Platform trait for use in tests.
|
||||
pub struct TestPlatform {
|
||||
background_executor: BackgroundExecutor,
|
||||
foreground_executor: ForegroundExecutor,
|
||||
|
@ -101,9 +102,12 @@ impl TestPlatform {
|
|||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub(crate) fn did_prompt_for_new_path(&self) -> bool {
|
||||
self.prompts.borrow().new_path.len() > 0
|
||||
}
|
||||
}
|
||||
|
||||
// todo!("implement out what our tests needed in GPUI 1")
|
||||
impl Platform for TestPlatform {
|
||||
fn background_executor(&self) -> BackgroundExecutor {
|
||||
self.background_executor.clone()
|
||||
|
@ -278,8 +282,7 @@ impl Platform for TestPlatform {
|
|||
}
|
||||
|
||||
fn should_auto_hide_scrollbars(&self) -> bool {
|
||||
// todo()
|
||||
true
|
||||
false
|
||||
}
|
||||
|
||||
fn write_to_clipboard(&self, item: ClipboardItem) {
|
||||
|
|
25
crates/gpui/src/shared_url.rs
Normal file
25
crates/gpui/src/shared_url.rs
Normal file
|
@ -0,0 +1,25 @@
|
|||
use derive_more::{Deref, DerefMut};
|
||||
|
||||
use crate::SharedString;
|
||||
|
||||
/// A [`SharedString`] containing a URL.
|
||||
#[derive(Deref, DerefMut, Default, PartialEq, Eq, Hash, Clone)]
|
||||
pub struct SharedUrl(SharedString);
|
||||
|
||||
impl std::fmt::Debug for SharedUrl {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SharedUrl {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Into<SharedString>> From<T> for SharedUrl {
|
||||
fn from(value: T) -> Self {
|
||||
Self(value.into())
|
||||
}
|
||||
}
|
|
@ -165,7 +165,8 @@ impl Default for TextStyle {
|
|||
fn default() -> Self {
|
||||
TextStyle {
|
||||
color: black(),
|
||||
font_family: "Helvetica".into(), // todo!("Get a font we know exists on the system")
|
||||
// Helvetica is a web safe font, so it should be available
|
||||
font_family: "Helvetica".into(),
|
||||
font_features: FontFeatures::default(),
|
||||
font_size: rems(1.).into(),
|
||||
line_height: phi(),
|
||||
|
|
|
@ -37,10 +37,10 @@ where
|
|||
})))
|
||||
}
|
||||
|
||||
/// Inserts a new `[Subscription]` for the given `emitter_key`. By default, subscriptions
|
||||
/// Inserts a new [`Subscription`] for the given `emitter_key`. By default, subscriptions
|
||||
/// are inert, meaning that they won't be listed when calling `[SubscriberSet::remove]` or `[SubscriberSet::retain]`.
|
||||
/// This method returns a tuple of a `[Subscription]` and an `impl FnOnce`, and you can use the latter
|
||||
/// to activate the `[Subscription]`.
|
||||
/// This method returns a tuple of a [`Subscription`] and an `impl FnOnce`, and you can use the latter
|
||||
/// to activate the [`Subscription`].
|
||||
#[must_use]
|
||||
pub fn insert(
|
||||
&self,
|
||||
|
|
|
@ -1,3 +1,30 @@
|
|||
//! Test support for GPUI.
|
||||
//!
|
||||
//! GPUI provides first-class support for testing, which includes a macro to run test that rely on having a context,
|
||||
//! and a test implementation of the `ForegroundExecutor` and `BackgroundExecutor` which ensure that your tests run
|
||||
//! deterministically even in the face of arbitrary parallelism.
|
||||
//!
|
||||
//! The output of the `gpui::test` macro is understood by other rust test runners, so you can use it with `cargo test`
|
||||
//! or `cargo-nextest`, or another runner of your choice.
|
||||
//!
|
||||
//! To make it possible to test collaborative user interfaces (like Zed) you can ask for as many different contexts
|
||||
//! as you need.
|
||||
//!
|
||||
//! ## Example
|
||||
//!
|
||||
//! ```
|
||||
//! use gpui;
|
||||
//!
|
||||
//! #[gpui::test]
|
||||
//! async fn test_example(cx: &TestAppContext) {
|
||||
//! assert!(true)
|
||||
//! }
|
||||
//!
|
||||
//! #[gpui::test]
|
||||
//! async fn test_collaboration_example(cx_a: &TestAppContext, cx_b: &TestAppContext) {
|
||||
//! assert!(true)
|
||||
//! }
|
||||
//! ```
|
||||
use crate::{Entity, Subscription, TestAppContext, TestDispatcher};
|
||||
use futures::StreamExt as _;
|
||||
use rand::prelude::*;
|
||||
|
@ -12,7 +39,6 @@ pub fn run_test(
|
|||
max_retries: usize,
|
||||
test_fn: &mut (dyn RefUnwindSafe + Fn(TestDispatcher, u64)),
|
||||
on_fail_fn: Option<fn()>,
|
||||
_fn_name: String, // todo!("re-enable fn_name")
|
||||
) {
|
||||
let starting_seed = env::var("SEED")
|
||||
.map(|seed| seed.parse().expect("invalid SEED variable"))
|
||||
|
@ -68,6 +94,7 @@ impl<T: 'static> futures::Stream for Observation<T> {
|
|||
}
|
||||
}
|
||||
|
||||
/// observe returns a stream of the change events from the given `View` or `Model`
|
||||
pub fn observe<T: 'static>(entity: &impl Entity<T>, cx: &mut TestAppContext) -> Observation<()> {
|
||||
let (tx, rx) = smol::channel::unbounded();
|
||||
let _subscription = cx.update(|cx| {
|
||||
|
|
|
@ -258,7 +258,7 @@ impl TextSystem {
|
|||
|
||||
pub fn shape_text(
|
||||
&self,
|
||||
text: &str, // todo!("pass a SharedString and preserve it when passed a single line?")
|
||||
text: SharedString,
|
||||
font_size: Pixels,
|
||||
runs: &[TextRun],
|
||||
wrap_width: Option<Pixels>,
|
||||
|
@ -268,8 +268,8 @@ impl TextSystem {
|
|||
|
||||
let mut lines = SmallVec::new();
|
||||
let mut line_start = 0;
|
||||
for line_text in text.split('\n') {
|
||||
let line_text = SharedString::from(line_text.to_string());
|
||||
|
||||
let mut process_line = |line_text: SharedString| {
|
||||
let line_end = line_start + line_text.len();
|
||||
|
||||
let mut last_font: Option<Font> = None;
|
||||
|
@ -335,6 +335,24 @@ impl TextSystem {
|
|||
}
|
||||
|
||||
font_runs.clear();
|
||||
};
|
||||
|
||||
let mut split_lines = text.split('\n');
|
||||
let mut processed = false;
|
||||
|
||||
if let Some(first_line) = split_lines.next() {
|
||||
if let Some(second_line) = split_lines.next() {
|
||||
processed = true;
|
||||
process_line(first_line.to_string().into());
|
||||
process_line(second_line.to_string().into());
|
||||
for line_text in split_lines {
|
||||
process_line(line_text.to_string().into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !processed {
|
||||
process_line(text);
|
||||
}
|
||||
|
||||
self.font_runs_pool.lock().push(font_runs);
|
||||
|
|
|
@ -143,7 +143,7 @@ mod tests {
|
|||
#[test]
|
||||
fn test_wrap_line() {
|
||||
let dispatcher = TestDispatcher::new(StdRng::seed_from_u64(0));
|
||||
let cx = TestAppContext::new(dispatcher);
|
||||
let cx = TestAppContext::new(dispatcher, None);
|
||||
|
||||
cx.update(|cx| {
|
||||
let text_system = cx.text_system().clone();
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#![deny(missing_docs)]
|
||||
|
||||
use crate::{
|
||||
px, size, transparent_black, Action, AnyDrag, AnyView, AppContext, Arena, ArenaBox, ArenaRef,
|
||||
AsyncWindowContext, AvailableSpace, Bounds, BoxShadow, Context, Corners, CursorStyle,
|
||||
|
@ -85,10 +87,12 @@ pub enum DispatchPhase {
|
|||
}
|
||||
|
||||
impl DispatchPhase {
|
||||
/// Returns true if this represents the "bubble" phase.
|
||||
pub fn bubble(self) -> bool {
|
||||
self == DispatchPhase::Bubble
|
||||
}
|
||||
|
||||
/// Returns true if this represents the "capture" phase.
|
||||
pub fn capture(self) -> bool {
|
||||
self == DispatchPhase::Capture
|
||||
}
|
||||
|
@ -103,7 +107,10 @@ struct FocusEvent {
|
|||
current_focus_path: SmallVec<[FocusId; 8]>,
|
||||
}
|
||||
|
||||
slotmap::new_key_type! { pub struct FocusId; }
|
||||
slotmap::new_key_type! {
|
||||
/// A globally unique identifier for a focusable element.
|
||||
pub struct FocusId;
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
pub(crate) static ELEMENT_ARENA: RefCell<Arena> = RefCell::new(Arena::new(4 * 1024 * 1024));
|
||||
|
@ -231,6 +238,7 @@ impl Drop for FocusHandle {
|
|||
/// FocusableView allows users of your view to easily
|
||||
/// focus it (using cx.focus_view(view))
|
||||
pub trait FocusableView: 'static + Render {
|
||||
/// Returns the focus handle associated with this view.
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle;
|
||||
}
|
||||
|
||||
|
@ -240,9 +248,11 @@ pub trait ManagedView: FocusableView + EventEmitter<DismissEvent> {}
|
|||
|
||||
impl<M: FocusableView + EventEmitter<DismissEvent>> ManagedView for M {}
|
||||
|
||||
/// Emitted by implementers of [`ManagedView`] to indicate the view should be dismissed, such as when a view is presented as a modal.
|
||||
pub struct DismissEvent;
|
||||
|
||||
// Holds the state for a specific window.
|
||||
#[doc(hidden)]
|
||||
pub struct Window {
|
||||
pub(crate) handle: AnyWindowHandle,
|
||||
pub(crate) removed: bool,
|
||||
|
@ -259,7 +269,7 @@ pub struct Window {
|
|||
frame_arena: Arena,
|
||||
pub(crate) focus_handles: Arc<RwLock<SlotMap<FocusId, AtomicUsize>>>,
|
||||
focus_listeners: SubscriberSet<(), AnyWindowFocusListener>,
|
||||
blur_listeners: SubscriberSet<(), AnyObserver>,
|
||||
focus_lost_listeners: SubscriberSet<(), AnyObserver>,
|
||||
default_prevented: bool,
|
||||
mouse_position: Point<Pixels>,
|
||||
modifiers: Modifiers,
|
||||
|
@ -286,6 +296,7 @@ pub(crate) struct ElementStateBox {
|
|||
|
||||
pub(crate) struct Frame {
|
||||
focus: Option<FocusId>,
|
||||
window_active: bool,
|
||||
pub(crate) element_states: FxHashMap<GlobalElementId, ElementStateBox>,
|
||||
mouse_listeners: FxHashMap<TypeId, Vec<(StackingOrder, AnyMouseListener)>>,
|
||||
pub(crate) dispatch_tree: DispatchTree,
|
||||
|
@ -301,6 +312,7 @@ impl Frame {
|
|||
fn new(dispatch_tree: DispatchTree) -> Self {
|
||||
Frame {
|
||||
focus: None,
|
||||
window_active: false,
|
||||
element_states: FxHashMap::default(),
|
||||
mouse_listeners: FxHashMap::default(),
|
||||
dispatch_tree,
|
||||
|
@ -407,7 +419,7 @@ impl Window {
|
|||
frame_arena: Arena::new(1024 * 1024),
|
||||
focus_handles: Arc::new(RwLock::new(SlotMap::with_key())),
|
||||
focus_listeners: SubscriberSet::new(),
|
||||
blur_listeners: SubscriberSet::new(),
|
||||
focus_lost_listeners: SubscriberSet::new(),
|
||||
default_prevented: true,
|
||||
mouse_position,
|
||||
modifiers,
|
||||
|
@ -434,6 +446,7 @@ impl Window {
|
|||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
#[repr(C)]
|
||||
pub struct ContentMask<P: Clone + Default + Debug> {
|
||||
/// The bounds
|
||||
pub bounds: Bounds<P>,
|
||||
}
|
||||
|
||||
|
@ -453,8 +466,8 @@ impl ContentMask<Pixels> {
|
|||
}
|
||||
|
||||
/// Provides access to application state in the context of a single window. Derefs
|
||||
/// to an `AppContext`, so you can also pass a `WindowContext` to any method that takes
|
||||
/// an `AppContext` and call any `AppContext` methods.
|
||||
/// to an [`AppContext`], so you can also pass a [`WindowContext`] to any method that takes
|
||||
/// an [`AppContext`] and call any [`AppContext`] methods.
|
||||
pub struct WindowContext<'a> {
|
||||
pub(crate) app: &'a mut AppContext,
|
||||
pub(crate) window: &'a mut Window,
|
||||
|
@ -482,20 +495,20 @@ impl<'a> WindowContext<'a> {
|
|||
self.window.removed = true;
|
||||
}
|
||||
|
||||
/// Obtain a new `FocusHandle`, which allows you to track and manipulate the keyboard focus
|
||||
/// Obtain a new [`FocusHandle`], which allows you to track and manipulate the keyboard focus
|
||||
/// for elements rendered within this window.
|
||||
pub fn focus_handle(&mut self) -> FocusHandle {
|
||||
FocusHandle::new(&self.window.focus_handles)
|
||||
}
|
||||
|
||||
/// Obtain the currently focused `FocusHandle`. If no elements are focused, returns `None`.
|
||||
/// Obtain the currently focused [`FocusHandle`]. If no elements are focused, returns `None`.
|
||||
pub fn focused(&self) -> Option<FocusHandle> {
|
||||
self.window
|
||||
.focus
|
||||
.and_then(|id| FocusHandle::for_id(id, &self.window.focus_handles))
|
||||
}
|
||||
|
||||
/// Move focus to the element associated with the given `FocusHandle`.
|
||||
/// Move focus to the element associated with the given [`FocusHandle`].
|
||||
pub fn focus(&mut self, handle: &FocusHandle) {
|
||||
if !self.window.focus_enabled || self.window.focus == Some(handle.id) {
|
||||
return;
|
||||
|
@ -525,11 +538,13 @@ impl<'a> WindowContext<'a> {
|
|||
self.notify();
|
||||
}
|
||||
|
||||
/// Blur the window and don't allow anything in it to be focused again.
|
||||
pub fn disable_focus(&mut self) {
|
||||
self.blur();
|
||||
self.window.focus_enabled = false;
|
||||
}
|
||||
|
||||
/// Dispatch the given action on the currently focused element.
|
||||
pub fn dispatch_action(&mut self, action: Box<dyn Action>) {
|
||||
let focus_handle = self.focused();
|
||||
|
||||
|
@ -591,6 +606,9 @@ impl<'a> WindowContext<'a> {
|
|||
});
|
||||
}
|
||||
|
||||
/// Subscribe to events emitted by a model or view.
|
||||
/// The entity to which you're subscribing must implement the [`EventEmitter`] trait.
|
||||
/// The callback will be invoked a handle to the emitting entity (either a [`View`] or [`Model`]), the event, and a window context for the current window.
|
||||
pub fn subscribe<Emitter, E, Evt>(
|
||||
&mut self,
|
||||
entity: &E,
|
||||
|
@ -754,6 +772,9 @@ impl<'a> WindowContext<'a> {
|
|||
.request_measured_layout(style, rem_size, measure)
|
||||
}
|
||||
|
||||
/// Compute the layout for the given id within the given available space.
|
||||
/// This method is called for its side effect, typically by the framework prior to painting.
|
||||
/// After calling it, you can request the bounds of the given layout node id or any descendant.
|
||||
pub fn compute_layout(&mut self, layout_id: LayoutId, available_space: Size<AvailableSpace>) {
|
||||
let mut layout_engine = self.window.layout_engine.take().unwrap();
|
||||
layout_engine.compute_layout(layout_id, available_space, self);
|
||||
|
@ -788,30 +809,37 @@ impl<'a> WindowContext<'a> {
|
|||
.retain(&(), |callback| callback(self));
|
||||
}
|
||||
|
||||
/// Returns the bounds of the current window in the global coordinate space, which could span across multiple displays.
|
||||
pub fn window_bounds(&self) -> WindowBounds {
|
||||
self.window.bounds
|
||||
}
|
||||
|
||||
/// Returns the size of the drawable area within the window.
|
||||
pub fn viewport_size(&self) -> Size<Pixels> {
|
||||
self.window.viewport_size
|
||||
}
|
||||
|
||||
/// Returns whether this window is focused by the operating system (receiving key events).
|
||||
pub fn is_window_active(&self) -> bool {
|
||||
self.window.active
|
||||
}
|
||||
|
||||
/// Toggle zoom on the window.
|
||||
pub fn zoom_window(&self) {
|
||||
self.window.platform_window.zoom();
|
||||
}
|
||||
|
||||
/// Update the window's title at the platform level.
|
||||
pub fn set_window_title(&mut self, title: &str) {
|
||||
self.window.platform_window.set_title(title);
|
||||
}
|
||||
|
||||
/// Mark the window as dirty at the platform level.
|
||||
pub fn set_window_edited(&mut self, edited: bool) {
|
||||
self.window.platform_window.set_edited(edited);
|
||||
}
|
||||
|
||||
/// Determine the display on which the window is visible.
|
||||
pub fn display(&self) -> Option<Rc<dyn PlatformDisplay>> {
|
||||
self.platform
|
||||
.displays()
|
||||
|
@ -819,6 +847,7 @@ impl<'a> WindowContext<'a> {
|
|||
.find(|display| display.id() == self.window.display_id)
|
||||
}
|
||||
|
||||
/// Show the platform character palette.
|
||||
pub fn show_character_palette(&self) {
|
||||
self.window.platform_window.show_character_palette();
|
||||
}
|
||||
|
@ -936,6 +965,7 @@ impl<'a> WindowContext<'a> {
|
|||
.on_action(action_type, ArenaRef::from(listener));
|
||||
}
|
||||
|
||||
/// Determine whether the given action is available along the dispatch path to the currently focused element.
|
||||
pub fn is_action_available(&self, action: &dyn Action) -> bool {
|
||||
let target = self
|
||||
.focused()
|
||||
|
@ -962,6 +992,7 @@ impl<'a> WindowContext<'a> {
|
|||
self.window.modifiers
|
||||
}
|
||||
|
||||
/// Update the cursor style at the platform level.
|
||||
pub fn set_cursor_style(&mut self, style: CursorStyle) {
|
||||
self.window.requested_cursor_style = Some(style)
|
||||
}
|
||||
|
@ -991,7 +1022,7 @@ impl<'a> WindowContext<'a> {
|
|||
true
|
||||
}
|
||||
|
||||
pub fn was_top_layer_under_active_drag(
|
||||
pub(crate) fn was_top_layer_under_active_drag(
|
||||
&self,
|
||||
point: &Point<Pixels>,
|
||||
level: &StackingOrder,
|
||||
|
@ -1377,29 +1408,14 @@ impl<'a> WindowContext<'a> {
|
|||
self.window.focus,
|
||||
);
|
||||
self.window.next_frame.focus = self.window.focus;
|
||||
self.window.next_frame.window_active = self.window.active;
|
||||
self.window.root_view = Some(root_view);
|
||||
|
||||
let previous_focus_path = self.window.rendered_frame.focus_path();
|
||||
let previous_window_active = self.window.rendered_frame.window_active;
|
||||
mem::swap(&mut self.window.rendered_frame, &mut self.window.next_frame);
|
||||
let current_focus_path = self.window.rendered_frame.focus_path();
|
||||
|
||||
if previous_focus_path != current_focus_path {
|
||||
if !previous_focus_path.is_empty() && current_focus_path.is_empty() {
|
||||
self.window
|
||||
.blur_listeners
|
||||
.clone()
|
||||
.retain(&(), |listener| listener(self));
|
||||
}
|
||||
|
||||
let event = FocusEvent {
|
||||
previous_focus_path,
|
||||
current_focus_path,
|
||||
};
|
||||
self.window
|
||||
.focus_listeners
|
||||
.clone()
|
||||
.retain(&(), |listener| listener(&event, self));
|
||||
}
|
||||
let current_window_active = self.window.rendered_frame.window_active;
|
||||
|
||||
let scene = self.window.rendered_frame.scene_builder.build();
|
||||
|
||||
|
@ -1416,6 +1432,34 @@ impl<'a> WindowContext<'a> {
|
|||
self.window.drawing = false;
|
||||
ELEMENT_ARENA.with_borrow_mut(|element_arena| element_arena.clear());
|
||||
|
||||
if previous_focus_path != current_focus_path
|
||||
|| previous_window_active != current_window_active
|
||||
{
|
||||
if !previous_focus_path.is_empty() && current_focus_path.is_empty() {
|
||||
self.window
|
||||
.focus_lost_listeners
|
||||
.clone()
|
||||
.retain(&(), |listener| listener(self));
|
||||
}
|
||||
|
||||
let event = FocusEvent {
|
||||
previous_focus_path: if previous_window_active {
|
||||
previous_focus_path
|
||||
} else {
|
||||
Default::default()
|
||||
},
|
||||
current_focus_path: if current_window_active {
|
||||
current_focus_path
|
||||
} else {
|
||||
Default::default()
|
||||
},
|
||||
};
|
||||
self.window
|
||||
.focus_listeners
|
||||
.clone()
|
||||
.retain(&(), |listener| listener(&event, self));
|
||||
}
|
||||
|
||||
scene
|
||||
}
|
||||
|
||||
|
@ -1445,9 +1489,7 @@ impl<'a> WindowContext<'a> {
|
|||
InputEvent::MouseUp(mouse_up)
|
||||
}
|
||||
InputEvent::MouseExited(mouse_exited) => {
|
||||
// todo!("Should we record that the mouse is outside of the window somehow? Or are these global pixels?")
|
||||
self.window.modifiers = mouse_exited.modifiers;
|
||||
|
||||
InputEvent::MouseExited(mouse_exited)
|
||||
}
|
||||
InputEvent::ModifiersChanged(modifiers_changed) => {
|
||||
|
@ -1649,6 +1691,7 @@ impl<'a> WindowContext<'a> {
|
|||
self.dispatch_keystroke_observers(event, None);
|
||||
}
|
||||
|
||||
/// Determine whether a potential multi-stroke key binding is in progress on this window.
|
||||
pub fn has_pending_keystrokes(&self) -> bool {
|
||||
self.window
|
||||
.rendered_frame
|
||||
|
@ -1715,27 +1758,34 @@ impl<'a> WindowContext<'a> {
|
|||
subscription
|
||||
}
|
||||
|
||||
/// Focus the current window and bring it to the foreground at the platform level.
|
||||
pub fn activate_window(&self) {
|
||||
self.window.platform_window.activate();
|
||||
}
|
||||
|
||||
/// Minimize the current window at the platform level.
|
||||
pub fn minimize_window(&self) {
|
||||
self.window.platform_window.minimize();
|
||||
}
|
||||
|
||||
/// Toggle full screen status on the current window at the platform level.
|
||||
pub fn toggle_full_screen(&self) {
|
||||
self.window.platform_window.toggle_full_screen();
|
||||
}
|
||||
|
||||
/// Present a platform dialog.
|
||||
/// The provided message will be presented, along with buttons for each answer.
|
||||
/// When a button is clicked, the returned Receiver will receive the index of the clicked button.
|
||||
pub fn prompt(
|
||||
&self,
|
||||
level: PromptLevel,
|
||||
msg: &str,
|
||||
message: &str,
|
||||
answers: &[&str],
|
||||
) -> oneshot::Receiver<usize> {
|
||||
self.window.platform_window.prompt(level, msg, answers)
|
||||
self.window.platform_window.prompt(level, message, answers)
|
||||
}
|
||||
|
||||
/// Returns all available actions for the focused element.
|
||||
pub fn available_actions(&self) -> Vec<Box<dyn Action>> {
|
||||
let node_id = self
|
||||
.window
|
||||
|
@ -1754,6 +1804,7 @@ impl<'a> WindowContext<'a> {
|
|||
.available_actions(node_id)
|
||||
}
|
||||
|
||||
/// Returns key bindings that invoke the given action on the currently focused element.
|
||||
pub fn bindings_for_action(&self, action: &dyn Action) -> Vec<KeyBinding> {
|
||||
self.window
|
||||
.rendered_frame
|
||||
|
@ -1764,6 +1815,7 @@ impl<'a> WindowContext<'a> {
|
|||
)
|
||||
}
|
||||
|
||||
/// Returns any bindings that would invoke the given action on the given focus handle if it were focused.
|
||||
pub fn bindings_for_action_in(
|
||||
&self,
|
||||
action: &dyn Action,
|
||||
|
@ -1782,6 +1834,7 @@ impl<'a> WindowContext<'a> {
|
|||
dispatch_tree.bindings_for_action(action, &context_stack)
|
||||
}
|
||||
|
||||
/// Returns a generic event listener that invokes the given listener with the view and context associated with the given view handle.
|
||||
pub fn listener_for<V: Render, E>(
|
||||
&self,
|
||||
view: &View<V>,
|
||||
|
@ -1793,6 +1846,7 @@ impl<'a> WindowContext<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns a generic handler that invokes the given handler with the view and context associated with the given view handle.
|
||||
pub fn handler_for<V: Render>(
|
||||
&self,
|
||||
view: &View<V>,
|
||||
|
@ -1804,7 +1858,8 @@ impl<'a> WindowContext<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
//========== ELEMENT RELATED FUNCTIONS ===========
|
||||
/// Invoke the given function with the given focus handle present on the key dispatch stack.
|
||||
/// If you want an element to participate in key dispatch, use this method to push its key context and focus handle into the stack during paint.
|
||||
pub fn with_key_dispatch<R>(
|
||||
&mut self,
|
||||
context: Option<KeyContext>,
|
||||
|
@ -1843,6 +1898,8 @@ impl<'a> WindowContext<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Register a callback that can interrupt the closing of the current window based the returned boolean.
|
||||
/// If the callback returns false, the window won't be closed.
|
||||
pub fn on_window_should_close(&mut self, f: impl Fn(&mut WindowContext) -> bool + 'static) {
|
||||
let mut this = self.to_async();
|
||||
self.window
|
||||
|
@ -2017,19 +2074,24 @@ impl<'a> BorrowMut<AppContext> for WindowContext<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
/// This trait contains functionality that is shared across [`ViewContext`] and [`WindowContext`]
|
||||
pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
|
||||
#[doc(hidden)]
|
||||
fn app_mut(&mut self) -> &mut AppContext {
|
||||
self.borrow_mut()
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
fn app(&self) -> &AppContext {
|
||||
self.borrow()
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
fn window(&self) -> &Window {
|
||||
self.borrow()
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
fn window_mut(&mut self) -> &mut Window {
|
||||
self.borrow_mut()
|
||||
}
|
||||
|
@ -2279,6 +2341,10 @@ impl BorrowMut<Window> for WindowContext<'_> {
|
|||
|
||||
impl<T> BorrowWindow for T where T: BorrowMut<AppContext> + BorrowMut<Window> {}
|
||||
|
||||
/// Provides access to application state that is specialized for a particular [`View`].
|
||||
/// Allows you to interact with focus, emit events, etc.
|
||||
/// ViewContext also derefs to [`WindowContext`], giving you access to all of its methods as well.
|
||||
/// When you call [`View::update`], you're passed a `&mut V` and an `&mut ViewContext<V>`.
|
||||
pub struct ViewContext<'a, V> {
|
||||
window_cx: WindowContext<'a>,
|
||||
view: &'a View<V>,
|
||||
|
@ -2316,14 +2382,17 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Get the entity_id of this view.
|
||||
pub fn entity_id(&self) -> EntityId {
|
||||
self.view.entity_id()
|
||||
}
|
||||
|
||||
/// Get the view pointer underlying this context.
|
||||
pub fn view(&self) -> &View<V> {
|
||||
self.view
|
||||
}
|
||||
|
||||
/// Get the model underlying this view.
|
||||
pub fn model(&self) -> &Model<V> {
|
||||
&self.view.model
|
||||
}
|
||||
|
@ -2333,6 +2402,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
|||
&mut self.window_cx
|
||||
}
|
||||
|
||||
/// Set a given callback to be run on the next frame.
|
||||
pub fn on_next_frame(&mut self, f: impl FnOnce(&mut V, &mut ViewContext<V>) + 'static)
|
||||
where
|
||||
V: 'static,
|
||||
|
@ -2350,6 +2420,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
|||
});
|
||||
}
|
||||
|
||||
/// Observe another model or view for changes to its state, as tracked by [`ModelContext::notify`].
|
||||
pub fn observe<V2, E>(
|
||||
&mut self,
|
||||
entity: &E,
|
||||
|
@ -2383,6 +2454,9 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
|||
subscription
|
||||
}
|
||||
|
||||
/// Subscribe to events emitted by another model or view.
|
||||
/// The entity to which you're subscribing must implement the [`EventEmitter`] trait.
|
||||
/// The callback will be invoked with a reference to the current view, a handle to the emitting entity (either a [`View`] or [`Model`]), the event, and a view context for the current view.
|
||||
pub fn subscribe<V2, E, Evt>(
|
||||
&mut self,
|
||||
entity: &E,
|
||||
|
@ -2440,6 +2514,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
|||
subscription
|
||||
}
|
||||
|
||||
/// Register a callback to be invoked when the given Model or View is released.
|
||||
pub fn observe_release<V2, E>(
|
||||
&mut self,
|
||||
entity: &E,
|
||||
|
@ -2466,6 +2541,8 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
|||
subscription
|
||||
}
|
||||
|
||||
/// Indicate that this view has changed, which will invoke any observers and also mark the window as dirty.
|
||||
/// If this view or any of its ancestors are *cached*, notifying it will cause it or its ancestors to be redrawn.
|
||||
pub fn notify(&mut self) {
|
||||
if !self.window.drawing {
|
||||
self.window_cx.notify();
|
||||
|
@ -2475,6 +2552,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Register a callback to be invoked when the window is resized.
|
||||
pub fn observe_window_bounds(
|
||||
&mut self,
|
||||
mut callback: impl FnMut(&mut V, &mut ViewContext<V>) + 'static,
|
||||
|
@ -2488,6 +2566,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
|||
subscription
|
||||
}
|
||||
|
||||
/// Register a callback to be invoked when the window is activated or deactivated.
|
||||
pub fn observe_window_activation(
|
||||
&mut self,
|
||||
mut callback: impl FnMut(&mut V, &mut ViewContext<V>) + 'static,
|
||||
|
@ -2579,14 +2658,16 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
|||
subscription
|
||||
}
|
||||
|
||||
/// Register a listener to be called when the window loses focus.
|
||||
/// Register a listener to be called when nothing in the window has focus.
|
||||
/// This typically happens when the node that was focused is removed from the tree,
|
||||
/// and this callback lets you chose a default place to restore the users focus.
|
||||
/// Returns a subscription and persists until the subscription is dropped.
|
||||
pub fn on_blur_window(
|
||||
pub fn on_focus_lost(
|
||||
&mut self,
|
||||
mut listener: impl FnMut(&mut V, &mut ViewContext<V>) + 'static,
|
||||
) -> Subscription {
|
||||
let view = self.view.downgrade();
|
||||
let (subscription, activate) = self.window.blur_listeners.insert(
|
||||
let (subscription, activate) = self.window.focus_lost_listeners.insert(
|
||||
(),
|
||||
Box::new(move |cx| view.update(cx, |view, cx| listener(view, cx)).is_ok()),
|
||||
);
|
||||
|
@ -2620,6 +2701,10 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
|||
subscription
|
||||
}
|
||||
|
||||
/// Schedule a future to be run asynchronously.
|
||||
/// The given callback is invoked with a [`WeakView<V>`] to avoid leaking the view for a long-running process.
|
||||
/// It's also given an [`AsyncWindowContext`], which can be used to access the state of the view across await points.
|
||||
/// The returned future will be polled on the main thread.
|
||||
pub fn spawn<Fut, R>(
|
||||
&mut self,
|
||||
f: impl FnOnce(WeakView<V>, AsyncWindowContext) -> Fut,
|
||||
|
@ -2632,6 +2717,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
|||
self.window_cx.spawn(|cx| f(view, cx))
|
||||
}
|
||||
|
||||
/// Update the global state of the given type.
|
||||
pub fn update_global<G, R>(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R
|
||||
where
|
||||
G: 'static,
|
||||
|
@ -2642,6 +2728,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
|||
result
|
||||
}
|
||||
|
||||
/// Register a callback to be invoked when the given global state changes.
|
||||
pub fn observe_global<G: 'static>(
|
||||
&mut self,
|
||||
mut f: impl FnMut(&mut V, &mut ViewContext<'_, V>) + 'static,
|
||||
|
@ -2660,6 +2747,9 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
|||
subscription
|
||||
}
|
||||
|
||||
/// Add a listener for any mouse event that occurs in the window.
|
||||
/// This is a fairly low level method.
|
||||
/// Typically, you'll want to use methods on UI elements, which perform bounds checking etc.
|
||||
pub fn on_mouse_event<Event: 'static>(
|
||||
&mut self,
|
||||
handler: impl Fn(&mut V, &Event, DispatchPhase, &mut ViewContext<V>) + 'static,
|
||||
|
@ -2672,6 +2762,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
|||
});
|
||||
}
|
||||
|
||||
/// Register a callback to be invoked when the given Key Event is dispatched to the window.
|
||||
pub fn on_key_event<Event: 'static>(
|
||||
&mut self,
|
||||
handler: impl Fn(&mut V, &Event, DispatchPhase, &mut ViewContext<V>) + 'static,
|
||||
|
@ -2684,6 +2775,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
|||
});
|
||||
}
|
||||
|
||||
/// Register a callback to be invoked when the given Action type is dispatched to the window.
|
||||
pub fn on_action(
|
||||
&mut self,
|
||||
action_type: TypeId,
|
||||
|
@ -2698,6 +2790,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
|||
});
|
||||
}
|
||||
|
||||
/// Emit an event to be handled any other views that have subscribed via [ViewContext::subscribe].
|
||||
pub fn emit<Evt>(&mut self, event: Evt)
|
||||
where
|
||||
Evt: 'static,
|
||||
|
@ -2711,6 +2804,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
|||
});
|
||||
}
|
||||
|
||||
/// Move focus to the current view, assuming it implements [`FocusableView`].
|
||||
pub fn focus_self(&mut self)
|
||||
where
|
||||
V: FocusableView,
|
||||
|
@ -2718,6 +2812,11 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
|||
self.defer(|view, cx| view.focus_handle(cx).focus(cx))
|
||||
}
|
||||
|
||||
/// Convenience method for accessing view state in an event callback.
|
||||
///
|
||||
/// Many GPUI callbacks take the form of `Fn(&E, &mut WindowContext)`,
|
||||
/// but it's often useful to be able to access view state in these
|
||||
/// callbacks. This method provides a convenient way to do so.
|
||||
pub fn listener<E>(
|
||||
&self,
|
||||
f: impl Fn(&mut V, &E, &mut ViewContext<V>) + 'static,
|
||||
|
@ -2827,14 +2926,20 @@ impl<'a, V> std::ops::DerefMut for ViewContext<'a, V> {
|
|||
}
|
||||
|
||||
// #[derive(Clone, Copy, Eq, PartialEq, Hash)]
|
||||
slotmap::new_key_type! { pub struct WindowId; }
|
||||
slotmap::new_key_type! {
|
||||
/// A unique identifier for a window.
|
||||
pub struct WindowId;
|
||||
}
|
||||
|
||||
impl WindowId {
|
||||
/// Converts this window ID to a `u64`.
|
||||
pub fn as_u64(&self) -> u64 {
|
||||
self.0.as_ffi()
|
||||
}
|
||||
}
|
||||
|
||||
/// A handle to a window with a specific root view type.
|
||||
/// Note that this does not keep the window alive on its own.
|
||||
#[derive(Deref, DerefMut)]
|
||||
pub struct WindowHandle<V> {
|
||||
#[deref]
|
||||
|
@ -2844,6 +2949,8 @@ pub struct WindowHandle<V> {
|
|||
}
|
||||
|
||||
impl<V: 'static + Render> WindowHandle<V> {
|
||||
/// Create a new handle from a window ID.
|
||||
/// This does not check if the root type of the window is `V`.
|
||||
pub fn new(id: WindowId) -> Self {
|
||||
WindowHandle {
|
||||
any_handle: AnyWindowHandle {
|
||||
|
@ -2854,6 +2961,9 @@ impl<V: 'static + Render> WindowHandle<V> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Get the root view out of this window.
|
||||
///
|
||||
/// This will fail if the window is closed or if the root view's type does not match `V`.
|
||||
pub fn root<C>(&self, cx: &mut C) -> Result<View<V>>
|
||||
where
|
||||
C: Context,
|
||||
|
@ -2865,6 +2975,9 @@ impl<V: 'static + Render> WindowHandle<V> {
|
|||
}))
|
||||
}
|
||||
|
||||
/// Update the root view of this window.
|
||||
///
|
||||
/// This will fail if the window has been closed or if the root view's type does not match
|
||||
pub fn update<C, R>(
|
||||
&self,
|
||||
cx: &mut C,
|
||||
|
@ -2881,6 +2994,9 @@ impl<V: 'static + Render> WindowHandle<V> {
|
|||
})?
|
||||
}
|
||||
|
||||
/// Read the root view out of this window.
|
||||
///
|
||||
/// This will fail if the window is closed or if the root view's type does not match `V`.
|
||||
pub fn read<'a>(&self, cx: &'a AppContext) -> Result<&'a V> {
|
||||
let x = cx
|
||||
.windows
|
||||
|
@ -2897,6 +3013,9 @@ impl<V: 'static + Render> WindowHandle<V> {
|
|||
Ok(x.read(cx))
|
||||
}
|
||||
|
||||
/// Read the root view out of this window, with a callback
|
||||
///
|
||||
/// This will fail if the window is closed or if the root view's type does not match `V`.
|
||||
pub fn read_with<C, R>(&self, cx: &C, read_with: impl FnOnce(&V, &AppContext) -> R) -> Result<R>
|
||||
where
|
||||
C: Context,
|
||||
|
@ -2904,6 +3023,9 @@ impl<V: 'static + Render> WindowHandle<V> {
|
|||
cx.read_window(self, |root_view, cx| read_with(root_view.read(cx), cx))
|
||||
}
|
||||
|
||||
/// Read the root view pointer off of this window.
|
||||
///
|
||||
/// This will fail if the window is closed or if the root view's type does not match `V`.
|
||||
pub fn root_view<C>(&self, cx: &C) -> Result<View<V>>
|
||||
where
|
||||
C: Context,
|
||||
|
@ -2911,6 +3033,9 @@ impl<V: 'static + Render> WindowHandle<V> {
|
|||
cx.read_window(self, |root_view, _cx| root_view.clone())
|
||||
}
|
||||
|
||||
/// Check if this window is 'active'.
|
||||
///
|
||||
/// Will return `None` if the window is closed.
|
||||
pub fn is_active(&self, cx: &AppContext) -> Option<bool> {
|
||||
cx.windows
|
||||
.get(self.id)
|
||||
|
@ -2946,6 +3071,7 @@ impl<V: 'static> From<WindowHandle<V>> for AnyWindowHandle {
|
|||
}
|
||||
}
|
||||
|
||||
/// A handle to a window with any root view type, which can be downcast to a window with a specific root view type.
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct AnyWindowHandle {
|
||||
pub(crate) id: WindowId,
|
||||
|
@ -2953,10 +3079,13 @@ pub struct AnyWindowHandle {
|
|||
}
|
||||
|
||||
impl AnyWindowHandle {
|
||||
/// Get the ID of this window.
|
||||
pub fn window_id(&self) -> WindowId {
|
||||
self.id
|
||||
}
|
||||
|
||||
/// Attempt to convert this handle to a window handle with a specific root view type.
|
||||
/// If the types do not match, this will return `None`.
|
||||
pub fn downcast<T: 'static>(&self) -> Option<WindowHandle<T>> {
|
||||
if TypeId::of::<T>() == self.state_type {
|
||||
Some(WindowHandle {
|
||||
|
@ -2968,6 +3097,9 @@ impl AnyWindowHandle {
|
|||
}
|
||||
}
|
||||
|
||||
/// Update the state of the root view of this window.
|
||||
///
|
||||
/// This will fail if the window has been closed.
|
||||
pub fn update<C, R>(
|
||||
self,
|
||||
cx: &mut C,
|
||||
|
@ -2979,6 +3111,9 @@ impl AnyWindowHandle {
|
|||
cx.update_window(self, update)
|
||||
}
|
||||
|
||||
/// Read the state of the root view of this window.
|
||||
///
|
||||
/// This will fail if the window has been closed.
|
||||
pub fn read<T, C, R>(self, cx: &C, read: impl FnOnce(View<T>, &AppContext) -> R) -> Result<R>
|
||||
where
|
||||
C: Context,
|
||||
|
@ -2999,12 +3134,21 @@ impl AnyWindowHandle {
|
|||
// }
|
||||
// }
|
||||
|
||||
/// An identifier for an [`Element`](crate::Element).
|
||||
///
|
||||
/// Can be constructed with a string, a number, or both, as well
|
||||
/// as other internal representations.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
|
||||
pub enum ElementId {
|
||||
/// The ID of a View element
|
||||
View(EntityId),
|
||||
/// An integer ID.
|
||||
Integer(usize),
|
||||
/// A string based ID.
|
||||
Name(SharedString),
|
||||
/// An ID that's equated with a focus handle.
|
||||
FocusHandle(FocusId),
|
||||
/// A combination of a name and an integer.
|
||||
NamedInteger(SharedString, usize),
|
||||
}
|
||||
|
||||
|
@ -3074,7 +3218,8 @@ impl From<(&'static str, u64)> for ElementId {
|
|||
}
|
||||
}
|
||||
|
||||
/// A rectangle, to be rendered on the screen by GPUI at the given position and size.
|
||||
/// A rectangle to be rendered in the window at the given position and size.
|
||||
/// Passed as an argument [`WindowContext::paint_quad`].
|
||||
#[derive(Clone)]
|
||||
pub struct PaintQuad {
|
||||
bounds: Bounds<Pixels>,
|
||||
|
|
|
@ -18,33 +18,33 @@ fn test_action_macros() {
|
|||
|
||||
impl gpui::Action for RegisterableAction {
|
||||
fn boxed_clone(&self) -> Box<dyn gpui::Action> {
|
||||
todo!()
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
todo!()
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn partial_eq(&self, _action: &dyn gpui::Action) -> bool {
|
||||
todo!()
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
todo!()
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn debug_name() -> &'static str
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
todo!()
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn build(_value: serde_json::Value) -> anyhow::Result<Box<dyn gpui::Action>>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
todo!()
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,26 +7,56 @@ mod test;
|
|||
use proc_macro::TokenStream;
|
||||
|
||||
#[proc_macro]
|
||||
/// register_action! can be used to register an action with the GPUI runtime.
|
||||
/// You should typically use `gpui::actions!` or `gpui::impl_actions!` instead,
|
||||
/// but this can be used for fine grained customization.
|
||||
pub fn register_action(ident: TokenStream) -> TokenStream {
|
||||
register_action::register_action_macro(ident)
|
||||
}
|
||||
|
||||
#[proc_macro_derive(IntoElement)]
|
||||
// #[derive(IntoElement)] is used to create a Component out of anything that implements
|
||||
// the `RenderOnce` trait.
|
||||
pub fn derive_into_element(input: TokenStream) -> TokenStream {
|
||||
derive_into_element::derive_into_element(input)
|
||||
}
|
||||
|
||||
#[proc_macro_derive(Render)]
|
||||
#[doc(hidden)]
|
||||
pub fn derive_render(input: TokenStream) -> TokenStream {
|
||||
derive_render::derive_render(input)
|
||||
}
|
||||
|
||||
// Used by gpui to generate the style helpers.
|
||||
#[proc_macro]
|
||||
#[doc(hidden)]
|
||||
pub fn style_helpers(input: TokenStream) -> TokenStream {
|
||||
style_helpers::style_helpers(input)
|
||||
}
|
||||
|
||||
#[proc_macro_attribute]
|
||||
/// #[gpui::test] can be used to annotate test functions that run with GPUI support.
|
||||
/// it supports both synchronous and asynchronous tests, and can provide you with
|
||||
/// as many `TestAppContext` instances as you need.
|
||||
/// The output contains a `#[test]` annotation so this can be used with any existing
|
||||
/// test harness (`cargo test` or `cargo-nextest`).
|
||||
///
|
||||
/// ```
|
||||
/// #[gpui::test]
|
||||
/// async fn test_foo(mut cx: &TestAppContext) { }
|
||||
/// ```
|
||||
///
|
||||
/// In addition to passing a TestAppContext, you can also ask for a `StdRnd` instance.
|
||||
/// this will be seeded with the `SEED` environment variable and is used internally by
|
||||
/// the ForegroundExecutor and BackgroundExecutor to run tasks deterministically in tests.
|
||||
/// Using the same `StdRng` for behaviour in your test will allow you to exercise a wide
|
||||
/// variety of scenarios and interleavings just by changing the seed.
|
||||
///
|
||||
/// #[gpui::test] also takes three different arguments:
|
||||
/// - `#[gpui::test(interations=10)]` will run the test ten times with a different initial SEED.
|
||||
/// - `#[gpui::test(retries=3)]` will run the test up to four times if it fails to try and make it pass.
|
||||
/// - `#[gpui::test(on_failure="crate::test::report_failure")]` will call the specified function after the
|
||||
/// tests fail so that you can write out more detail about the failure.
|
||||
pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
|
||||
test::test(args, function)
|
||||
}
|
||||
|
|
|
@ -106,7 +106,8 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
|
|||
let cx_varname = format_ident!("cx_{}", ix);
|
||||
cx_vars.extend(quote!(
|
||||
let mut #cx_varname = gpui::TestAppContext::new(
|
||||
dispatcher.clone()
|
||||
dispatcher.clone(),
|
||||
Some(stringify!(#outer_fn_name)),
|
||||
);
|
||||
));
|
||||
cx_teardowns.extend(quote!(
|
||||
|
@ -140,8 +141,7 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
|
|||
executor.block_test(#inner_fn_name(#inner_fn_args));
|
||||
#cx_teardowns
|
||||
},
|
||||
#on_failure_fn_name,
|
||||
stringify!(#outer_fn_name).to_string(),
|
||||
#on_failure_fn_name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -169,7 +169,8 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
|
|||
let cx_varname_lock = format_ident!("cx_{}_lock", ix);
|
||||
cx_vars.extend(quote!(
|
||||
let mut #cx_varname = gpui::TestAppContext::new(
|
||||
dispatcher.clone()
|
||||
dispatcher.clone(),
|
||||
Some(stringify!(#outer_fn_name))
|
||||
);
|
||||
let mut #cx_varname_lock = #cx_varname.app.borrow_mut();
|
||||
));
|
||||
|
@ -186,7 +187,8 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
|
|||
let cx_varname = format_ident!("cx_{}", ix);
|
||||
cx_vars.extend(quote!(
|
||||
let mut #cx_varname = gpui::TestAppContext::new(
|
||||
dispatcher.clone()
|
||||
dispatcher.clone(),
|
||||
Some(stringify!(#outer_fn_name))
|
||||
);
|
||||
));
|
||||
cx_teardowns.extend(quote!(
|
||||
|
@ -222,7 +224,6 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
|
|||
#cx_teardowns
|
||||
},
|
||||
#on_failure_fn_name,
|
||||
stringify!(#outer_fn_name).to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -254,6 +254,7 @@ pub enum Event {
|
|||
LanguageChanged,
|
||||
Reparsed,
|
||||
DiagnosticsUpdated,
|
||||
CapabilityChanged,
|
||||
Closed,
|
||||
}
|
||||
|
||||
|
@ -631,6 +632,11 @@ impl Buffer {
|
|||
.set_language_registry(language_registry);
|
||||
}
|
||||
|
||||
pub fn set_capability(&mut self, capability: Capability, cx: &mut ModelContext<Self>) {
|
||||
self.capability = capability;
|
||||
cx.emit(Event::CapabilityChanged)
|
||||
}
|
||||
|
||||
pub fn did_save(
|
||||
&mut self,
|
||||
version: clock::Global,
|
||||
|
|
|
@ -258,19 +258,19 @@ fn test_typing_multiple_new_injections() {
|
|||
let (buffer, syntax_map) = test_edit_sequence(
|
||||
"Rust",
|
||||
&[
|
||||
"fn a() { dbg }",
|
||||
"fn a() { dbg«!» }",
|
||||
"fn a() { dbg!«()» }",
|
||||
"fn a() { dbg!(«b») }",
|
||||
"fn a() { dbg!(b«.») }",
|
||||
"fn a() { dbg!(b.«c») }",
|
||||
"fn a() { dbg!(b.c«()») }",
|
||||
"fn a() { dbg!(b.c(«vec»)) }",
|
||||
"fn a() { dbg!(b.c(vec«!»)) }",
|
||||
"fn a() { dbg!(b.c(vec!«[]»)) }",
|
||||
"fn a() { dbg!(b.c(vec![«d»])) }",
|
||||
"fn a() { dbg!(b.c(vec![d«.»])) }",
|
||||
"fn a() { dbg!(b.c(vec![d.«e»])) }",
|
||||
"fn a() { test_macro }",
|
||||
"fn a() { test_macro«!» }",
|
||||
"fn a() { test_macro!«()» }",
|
||||
"fn a() { test_macro!(«b») }",
|
||||
"fn a() { test_macro!(b«.») }",
|
||||
"fn a() { test_macro!(b.«c») }",
|
||||
"fn a() { test_macro!(b.c«()») }",
|
||||
"fn a() { test_macro!(b.c(«vec»)) }",
|
||||
"fn a() { test_macro!(b.c(vec«!»)) }",
|
||||
"fn a() { test_macro!(b.c(vec!«[]»)) }",
|
||||
"fn a() { test_macro!(b.c(vec![«d»])) }",
|
||||
"fn a() { test_macro!(b.c(vec![d«.»])) }",
|
||||
"fn a() { test_macro!(b.c(vec![d.«e»])) }",
|
||||
],
|
||||
);
|
||||
|
||||
|
@ -278,7 +278,7 @@ fn test_typing_multiple_new_injections() {
|
|||
&syntax_map,
|
||||
&buffer,
|
||||
&["field"],
|
||||
"fn a() { dbg!(b.«c»(vec![d.«e»])) }",
|
||||
"fn a() { test_macro!(b.«c»(vec![d.«e»])) }",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -2,13 +2,12 @@ use editor::{scroll::autoscroll::Autoscroll, Anchor, Editor, ExcerptId};
|
|||
use gpui::{
|
||||
actions, canvas, div, rems, uniform_list, AnyElement, AppContext, AvailableSpace, Div,
|
||||
EventEmitter, FocusHandle, FocusableView, Hsla, InteractiveElement, IntoElement, Model,
|
||||
MouseButton, MouseDownEvent, MouseMoveEvent, ParentElement, Pixels, Render, Styled,
|
||||
MouseButton, MouseDownEvent, MouseMoveEvent, ParentElement, Render, Styled,
|
||||
UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WindowContext,
|
||||
};
|
||||
use language::{Buffer, OwnedSyntaxLayerInfo};
|
||||
use settings::Settings;
|
||||
use std::{mem, ops::Range};
|
||||
use theme::{ActiveTheme, ThemeSettings};
|
||||
use theme::ActiveTheme;
|
||||
use tree_sitter::{Node, TreeCursor};
|
||||
use ui::{h_stack, popover_menu, ButtonLike, Color, ContextMenu, Label, LabelCommon, PopoverMenu};
|
||||
use workspace::{
|
||||
|
@ -34,8 +33,6 @@ pub fn init(cx: &mut AppContext) {
|
|||
pub struct SyntaxTreeView {
|
||||
workspace_handle: WeakView<Workspace>,
|
||||
editor: Option<EditorState>,
|
||||
mouse_y: Option<Pixels>,
|
||||
line_height: Option<Pixels>,
|
||||
list_scroll_handle: UniformListScrollHandle,
|
||||
selected_descendant_ix: Option<usize>,
|
||||
hovered_descendant_ix: Option<usize>,
|
||||
|
@ -70,8 +67,6 @@ impl SyntaxTreeView {
|
|||
workspace_handle: workspace_handle.clone(),
|
||||
list_scroll_handle: UniformListScrollHandle::new(),
|
||||
editor: None,
|
||||
mouse_y: None,
|
||||
line_height: None,
|
||||
hovered_descendant_ix: None,
|
||||
selected_descendant_ix: None,
|
||||
focus_handle: cx.focus_handle(),
|
||||
|
@ -208,39 +203,6 @@ impl SyntaxTreeView {
|
|||
Some(())
|
||||
}
|
||||
|
||||
fn handle_click(&mut self, y: Pixels, cx: &mut ViewContext<SyntaxTreeView>) -> Option<()> {
|
||||
let line_height = self.line_height?;
|
||||
let ix = ((self.list_scroll_handle.scroll_top() + y) / line_height) as usize;
|
||||
|
||||
self.update_editor_with_range_for_descendant_ix(ix, cx, |editor, mut range, cx| {
|
||||
// Put the cursor at the beginning of the node.
|
||||
mem::swap(&mut range.start, &mut range.end);
|
||||
|
||||
editor.change_selections(Some(Autoscroll::newest()), cx, |selections| {
|
||||
selections.select_ranges(vec![range]);
|
||||
});
|
||||
});
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn hover_state_changed(&mut self, cx: &mut ViewContext<SyntaxTreeView>) {
|
||||
if let Some((y, line_height)) = self.mouse_y.zip(self.line_height) {
|
||||
let ix = ((self.list_scroll_handle.scroll_top() + y) / line_height) as usize;
|
||||
if self.hovered_descendant_ix != Some(ix) {
|
||||
self.hovered_descendant_ix = Some(ix);
|
||||
self.update_editor_with_range_for_descendant_ix(ix, cx, |editor, range, cx| {
|
||||
editor.clear_background_highlights::<Self>(cx);
|
||||
editor.highlight_background::<Self>(
|
||||
vec![range],
|
||||
|theme| theme.editor_document_highlight_write_background,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_editor_with_range_for_descendant_ix(
|
||||
&self,
|
||||
descendant_ix: usize,
|
||||
|
@ -306,15 +268,6 @@ impl SyntaxTreeView {
|
|||
|
||||
impl Render for SyntaxTreeView {
|
||||
fn render(&mut self, cx: &mut gpui::ViewContext<'_, Self>) -> impl IntoElement {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let line_height = cx
|
||||
.text_style()
|
||||
.line_height_in_pixels(settings.buffer_font_size(cx));
|
||||
if Some(line_height) != self.line_height {
|
||||
self.line_height = Some(line_height);
|
||||
self.hover_state_changed(cx);
|
||||
}
|
||||
|
||||
let mut rendered = div().flex_1();
|
||||
|
||||
if let Some(layer) = self
|
||||
|
@ -345,12 +298,51 @@ impl Render for SyntaxTreeView {
|
|||
break;
|
||||
}
|
||||
} else {
|
||||
items.push(Self::render_node(
|
||||
&cursor,
|
||||
depth,
|
||||
Some(descendant_ix) == this.selected_descendant_ix,
|
||||
cx,
|
||||
));
|
||||
items.push(
|
||||
Self::render_node(
|
||||
&cursor,
|
||||
depth,
|
||||
Some(descendant_ix) == this.selected_descendant_ix,
|
||||
cx,
|
||||
)
|
||||
.on_mouse_down(
|
||||
MouseButton::Left,
|
||||
cx.listener(move |tree_view, _: &MouseDownEvent, cx| {
|
||||
tree_view.update_editor_with_range_for_descendant_ix(
|
||||
descendant_ix,
|
||||
cx,
|
||||
|editor, mut range, cx| {
|
||||
// Put the cursor at the beginning of the node.
|
||||
mem::swap(&mut range.start, &mut range.end);
|
||||
|
||||
editor.change_selections(
|
||||
Some(Autoscroll::newest()),
|
||||
cx,
|
||||
|selections| {
|
||||
selections.select_ranges(vec![range]);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
)
|
||||
.on_mouse_move(cx.listener(
|
||||
move |tree_view, _: &MouseMoveEvent, cx| {
|
||||
if tree_view.hovered_descendant_ix != Some(descendant_ix) {
|
||||
tree_view.hovered_descendant_ix = Some(descendant_ix);
|
||||
tree_view.update_editor_with_range_for_descendant_ix(descendant_ix, cx, |editor, range, cx| {
|
||||
editor.clear_background_highlights::<Self>(cx);
|
||||
editor.highlight_background::<Self>(
|
||||
vec![range],
|
||||
|theme| theme.editor_document_highlight_write_background,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
},
|
||||
)),
|
||||
);
|
||||
descendant_ix += 1;
|
||||
if cursor.goto_first_child() {
|
||||
depth += 1;
|
||||
|
@ -364,16 +356,6 @@ impl Render for SyntaxTreeView {
|
|||
)
|
||||
.size_full()
|
||||
.track_scroll(self.list_scroll_handle.clone())
|
||||
.on_mouse_move(cx.listener(move |tree_view, event: &MouseMoveEvent, cx| {
|
||||
tree_view.mouse_y = Some(event.position.y);
|
||||
tree_view.hover_state_changed(cx);
|
||||
}))
|
||||
.on_mouse_down(
|
||||
MouseButton::Left,
|
||||
cx.listener(move |tree_view, event: &MouseDownEvent, cx| {
|
||||
tree_view.handle_click(event.position.y, cx);
|
||||
}),
|
||||
)
|
||||
.text_bg(cx.theme().colors().background);
|
||||
|
||||
rendered = rendered.child(
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use futures::StreamExt;
|
||||
use gpui::{actions, KeyBinding};
|
||||
use gpui::{actions, KeyBinding, Menu, MenuItem};
|
||||
use live_kit_client::{
|
||||
LocalAudioTrack, LocalVideoTrack, RemoteAudioTrackUpdate, RemoteVideoTrackUpdate, Room,
|
||||
};
|
||||
|
@ -26,15 +26,14 @@ fn main() {
|
|||
cx.on_action(quit);
|
||||
cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]);
|
||||
|
||||
// todo!()
|
||||
// cx.set_menus(vec![Menu {
|
||||
// name: "Zed",
|
||||
// items: vec![MenuItem::Action {
|
||||
// name: "Quit",
|
||||
// action: Box::new(Quit),
|
||||
// os_action: None,
|
||||
// }],
|
||||
// }]);
|
||||
cx.set_menus(vec![Menu {
|
||||
name: "Zed",
|
||||
items: vec![MenuItem::Action {
|
||||
name: "Quit",
|
||||
action: Box::new(Quit),
|
||||
os_action: None,
|
||||
}],
|
||||
}]);
|
||||
|
||||
let live_kit_url = std::env::var("LIVE_KIT_URL").unwrap_or("http://localhost:7880".into());
|
||||
let live_kit_key = std::env::var("LIVE_KIT_KEY").unwrap_or("devkey".into());
|
||||
|
|
|
@ -164,29 +164,26 @@ pub enum ConnectionState {
|
|||
}
|
||||
|
||||
pub struct Room {
|
||||
native_room: Mutex<swift::Room>,
|
||||
native_room: swift::Room,
|
||||
connection: Mutex<(
|
||||
watch::Sender<ConnectionState>,
|
||||
watch::Receiver<ConnectionState>,
|
||||
)>,
|
||||
remote_audio_track_subscribers: Mutex<Vec<mpsc::UnboundedSender<RemoteAudioTrackUpdate>>>,
|
||||
remote_video_track_subscribers: Mutex<Vec<mpsc::UnboundedSender<RemoteVideoTrackUpdate>>>,
|
||||
_delegate: Mutex<RoomDelegate>,
|
||||
_delegate: RoomDelegate,
|
||||
}
|
||||
|
||||
trait AssertSendSync: Send {}
|
||||
impl AssertSendSync for Room {}
|
||||
|
||||
impl Room {
|
||||
pub fn new() -> Arc<Self> {
|
||||
Arc::new_cyclic(|weak_room| {
|
||||
let delegate = RoomDelegate::new(weak_room.clone());
|
||||
Self {
|
||||
native_room: Mutex::new(unsafe { LKRoomCreate(delegate.native_delegate) }),
|
||||
native_room: unsafe { LKRoomCreate(delegate.native_delegate) },
|
||||
connection: Mutex::new(watch::channel_with(ConnectionState::Disconnected)),
|
||||
remote_audio_track_subscribers: Default::default(),
|
||||
remote_video_track_subscribers: Default::default(),
|
||||
_delegate: Mutex::new(delegate),
|
||||
_delegate: delegate,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -201,7 +198,7 @@ impl Room {
|
|||
let (did_connect, tx, rx) = Self::build_done_callback();
|
||||
unsafe {
|
||||
LKRoomConnect(
|
||||
*self.native_room.lock(),
|
||||
self.native_room,
|
||||
url.as_concrete_TypeRef(),
|
||||
token.as_concrete_TypeRef(),
|
||||
did_connect,
|
||||
|
@ -271,7 +268,7 @@ impl Room {
|
|||
}
|
||||
unsafe {
|
||||
LKRoomPublishVideoTrack(
|
||||
*self.native_room.lock(),
|
||||
self.native_room,
|
||||
track.0,
|
||||
callback,
|
||||
Box::into_raw(Box::new(tx)) as *mut c_void,
|
||||
|
@ -301,7 +298,7 @@ impl Room {
|
|||
}
|
||||
unsafe {
|
||||
LKRoomPublishAudioTrack(
|
||||
*self.native_room.lock(),
|
||||
self.native_room,
|
||||
track.0,
|
||||
callback,
|
||||
Box::into_raw(Box::new(tx)) as *mut c_void,
|
||||
|
@ -312,14 +309,14 @@ impl Room {
|
|||
|
||||
pub fn unpublish_track(&self, publication: LocalTrackPublication) {
|
||||
unsafe {
|
||||
LKRoomUnpublishTrack(*self.native_room.lock(), publication.0);
|
||||
LKRoomUnpublishTrack(self.native_room, publication.0);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remote_video_tracks(&self, participant_id: &str) -> Vec<Arc<RemoteVideoTrack>> {
|
||||
unsafe {
|
||||
let tracks = LKRoomVideoTracksForRemoteParticipant(
|
||||
*self.native_room.lock(),
|
||||
self.native_room,
|
||||
CFString::new(participant_id).as_concrete_TypeRef(),
|
||||
);
|
||||
|
||||
|
@ -348,7 +345,7 @@ impl Room {
|
|||
pub fn remote_audio_tracks(&self, participant_id: &str) -> Vec<Arc<RemoteAudioTrack>> {
|
||||
unsafe {
|
||||
let tracks = LKRoomAudioTracksForRemoteParticipant(
|
||||
*self.native_room.lock(),
|
||||
self.native_room,
|
||||
CFString::new(participant_id).as_concrete_TypeRef(),
|
||||
);
|
||||
|
||||
|
@ -380,7 +377,7 @@ impl Room {
|
|||
) -> Vec<Arc<RemoteTrackPublication>> {
|
||||
unsafe {
|
||||
let tracks = LKRoomAudioTrackPublicationsForRemoteParticipant(
|
||||
*self.native_room.lock(),
|
||||
self.native_room,
|
||||
CFString::new(participant_id).as_concrete_TypeRef(),
|
||||
);
|
||||
|
||||
|
@ -508,23 +505,23 @@ impl Room {
|
|||
impl Drop for Room {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
let native_room = &*self.native_room.lock();
|
||||
LKRoomDisconnect(*native_room);
|
||||
CFRelease(native_room.0);
|
||||
LKRoomDisconnect(self.native_room);
|
||||
CFRelease(self.native_room.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct RoomDelegate {
|
||||
native_delegate: swift::RoomDelegate,
|
||||
_weak_room: Weak<Room>,
|
||||
weak_room: *mut c_void,
|
||||
}
|
||||
|
||||
impl RoomDelegate {
|
||||
fn new(weak_room: Weak<Room>) -> Self {
|
||||
let weak_room = weak_room.into_raw() as *mut c_void;
|
||||
let native_delegate = unsafe {
|
||||
LKRoomDelegateCreate(
|
||||
weak_room.as_ptr() as *mut c_void,
|
||||
weak_room,
|
||||
Self::on_did_disconnect,
|
||||
Self::on_did_subscribe_to_remote_audio_track,
|
||||
Self::on_did_unsubscribe_from_remote_audio_track,
|
||||
|
@ -536,7 +533,7 @@ impl RoomDelegate {
|
|||
};
|
||||
Self {
|
||||
native_delegate,
|
||||
_weak_room: weak_room,
|
||||
weak_room,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -651,6 +648,7 @@ impl Drop for RoomDelegate {
|
|||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
CFRelease(self.native_delegate.0);
|
||||
let _ = Weak::from_raw(self.weak_room);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -725,31 +723,22 @@ impl Drop for LocalTrackPublication {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct RemoteTrackPublication {
|
||||
native_publication: Mutex<swift::RemoteTrackPublication>,
|
||||
}
|
||||
pub struct RemoteTrackPublication(swift::RemoteTrackPublication);
|
||||
|
||||
impl RemoteTrackPublication {
|
||||
pub fn new(native_track_publication: swift::RemoteTrackPublication) -> Self {
|
||||
unsafe {
|
||||
CFRetain(native_track_publication.0);
|
||||
}
|
||||
Self {
|
||||
native_publication: Mutex::new(native_track_publication),
|
||||
}
|
||||
Self(native_track_publication)
|
||||
}
|
||||
|
||||
pub fn sid(&self) -> String {
|
||||
unsafe {
|
||||
CFString::wrap_under_get_rule(LKRemoteTrackPublicationGetSid(
|
||||
*self.native_publication.lock(),
|
||||
))
|
||||
.to_string()
|
||||
}
|
||||
unsafe { CFString::wrap_under_get_rule(LKRemoteTrackPublicationGetSid(self.0)).to_string() }
|
||||
}
|
||||
|
||||
pub fn is_muted(&self) -> bool {
|
||||
unsafe { LKRemoteTrackPublicationIsMuted(*self.native_publication.lock()) }
|
||||
unsafe { LKRemoteTrackPublicationIsMuted(self.0) }
|
||||
}
|
||||
|
||||
pub fn set_enabled(&self, enabled: bool) -> impl Future<Output = Result<()>> {
|
||||
|
@ -767,7 +756,7 @@ impl RemoteTrackPublication {
|
|||
|
||||
unsafe {
|
||||
LKRemoteTrackPublicationSetEnabled(
|
||||
*self.native_publication.lock(),
|
||||
self.0,
|
||||
enabled,
|
||||
complete_callback,
|
||||
Box::into_raw(Box::new(tx)) as *mut c_void,
|
||||
|
@ -780,13 +769,13 @@ impl RemoteTrackPublication {
|
|||
|
||||
impl Drop for RemoteTrackPublication {
|
||||
fn drop(&mut self) {
|
||||
unsafe { CFRelease((*self.native_publication.lock()).0) }
|
||||
unsafe { CFRelease(self.0 .0) }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RemoteAudioTrack {
|
||||
native_track: Mutex<swift::RemoteAudioTrack>,
|
||||
native_track: swift::RemoteAudioTrack,
|
||||
sid: Sid,
|
||||
publisher_id: String,
|
||||
}
|
||||
|
@ -797,7 +786,7 @@ impl RemoteAudioTrack {
|
|||
CFRetain(native_track.0);
|
||||
}
|
||||
Self {
|
||||
native_track: Mutex::new(native_track),
|
||||
native_track,
|
||||
sid,
|
||||
publisher_id,
|
||||
}
|
||||
|
@ -822,13 +811,13 @@ impl RemoteAudioTrack {
|
|||
|
||||
impl Drop for RemoteAudioTrack {
|
||||
fn drop(&mut self) {
|
||||
unsafe { CFRelease(self.native_track.lock().0) }
|
||||
unsafe { CFRelease(self.native_track.0) }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RemoteVideoTrack {
|
||||
native_track: Mutex<swift::RemoteVideoTrack>,
|
||||
native_track: swift::RemoteVideoTrack,
|
||||
sid: Sid,
|
||||
publisher_id: String,
|
||||
}
|
||||
|
@ -839,7 +828,7 @@ impl RemoteVideoTrack {
|
|||
CFRetain(native_track.0);
|
||||
}
|
||||
Self {
|
||||
native_track: Mutex::new(native_track),
|
||||
native_track,
|
||||
sid,
|
||||
publisher_id,
|
||||
}
|
||||
|
@ -888,7 +877,7 @@ impl RemoteVideoTrack {
|
|||
on_frame,
|
||||
on_drop,
|
||||
);
|
||||
LKVideoTrackAddRenderer(*self.native_track.lock(), renderer);
|
||||
LKVideoTrackAddRenderer(self.native_track, renderer);
|
||||
rx
|
||||
}
|
||||
}
|
||||
|
@ -896,7 +885,7 @@ impl RemoteVideoTrack {
|
|||
|
||||
impl Drop for RemoteVideoTrack {
|
||||
fn drop(&mut self) {
|
||||
unsafe { CFRelease(self.native_track.lock().0) }
|
||||
unsafe { CFRelease(self.native_track.0) }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ use async_trait::async_trait;
|
|||
use collections::{BTreeMap, HashMap};
|
||||
use futures::Stream;
|
||||
use gpui::BackgroundExecutor;
|
||||
use live_kit_server::token;
|
||||
use live_kit_server::{proto, token};
|
||||
use media::core_video::CVImageBuffer;
|
||||
use parking_lot::Mutex;
|
||||
use postage::watch;
|
||||
|
@ -151,6 +151,21 @@ impl TestServer {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_participant(
|
||||
&self,
|
||||
room_name: String,
|
||||
identity: String,
|
||||
permission: proto::ParticipantPermission,
|
||||
) -> Result<()> {
|
||||
self.executor.simulate_random_delay().await;
|
||||
let mut server_rooms = self.rooms.lock();
|
||||
let room = server_rooms
|
||||
.get_mut(&room_name)
|
||||
.ok_or_else(|| anyhow!("room {} does not exist", room_name))?;
|
||||
room.participant_permissions.insert(identity, permission);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn disconnect_client(&self, client_identity: String) {
|
||||
self.executor.simulate_random_delay().await;
|
||||
let mut server_rooms = self.rooms.lock();
|
||||
|
@ -172,6 +187,17 @@ impl TestServer {
|
|||
.get_mut(&*room_name)
|
||||
.ok_or_else(|| anyhow!("room {} does not exist", room_name))?;
|
||||
|
||||
let can_publish = room
|
||||
.participant_permissions
|
||||
.get(&identity)
|
||||
.map(|permission| permission.can_publish)
|
||||
.or(claims.video.can_publish)
|
||||
.unwrap_or(true);
|
||||
|
||||
if !can_publish {
|
||||
return Err(anyhow!("user is not allowed to publish"));
|
||||
}
|
||||
|
||||
let track = Arc::new(RemoteVideoTrack {
|
||||
sid: nanoid::nanoid!(17),
|
||||
publisher_id: identity.clone(),
|
||||
|
@ -210,6 +236,17 @@ impl TestServer {
|
|||
.get_mut(&*room_name)
|
||||
.ok_or_else(|| anyhow!("room {} does not exist", room_name))?;
|
||||
|
||||
let can_publish = room
|
||||
.participant_permissions
|
||||
.get(&identity)
|
||||
.map(|permission| permission.can_publish)
|
||||
.or(claims.video.can_publish)
|
||||
.unwrap_or(true);
|
||||
|
||||
if !can_publish {
|
||||
return Err(anyhow!("user is not allowed to publish"));
|
||||
}
|
||||
|
||||
let track = Arc::new(RemoteAudioTrack {
|
||||
sid: nanoid::nanoid!(17),
|
||||
publisher_id: identity.clone(),
|
||||
|
@ -265,6 +302,7 @@ struct TestServerRoom {
|
|||
client_rooms: HashMap<Sid, Arc<Room>>,
|
||||
video_tracks: Vec<Arc<RemoteVideoTrack>>,
|
||||
audio_tracks: Vec<Arc<RemoteAudioTrack>>,
|
||||
participant_permissions: HashMap<Sid, proto::ParticipantPermission>,
|
||||
}
|
||||
|
||||
impl TestServerRoom {}
|
||||
|
@ -297,6 +335,19 @@ impl live_kit_server::api::Client for TestApiClient {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_participant(
|
||||
&self,
|
||||
room: String,
|
||||
identity: String,
|
||||
permission: live_kit_server::proto::ParticipantPermission,
|
||||
) -> Result<()> {
|
||||
let server = TestServer::get(&self.url)?;
|
||||
server
|
||||
.update_participant(room, identity, permission)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn room_token(&self, room: &str, identity: &str) -> Result<String> {
|
||||
let server = TestServer::get(&self.url)?;
|
||||
token::create(
|
||||
|
|
|
@ -11,10 +11,18 @@ pub trait Client: Send + Sync {
|
|||
async fn create_room(&self, name: String) -> Result<()>;
|
||||
async fn delete_room(&self, name: String) -> Result<()>;
|
||||
async fn remove_participant(&self, room: String, identity: String) -> Result<()>;
|
||||
async fn update_participant(
|
||||
&self,
|
||||
room: String,
|
||||
identity: String,
|
||||
permission: proto::ParticipantPermission,
|
||||
) -> Result<()>;
|
||||
fn room_token(&self, room: &str, identity: &str) -> Result<String>;
|
||||
fn guest_token(&self, room: &str, identity: &str) -> Result<String>;
|
||||
}
|
||||
|
||||
pub struct LiveKitParticipantUpdate {}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct LiveKitClient {
|
||||
http: reqwest::Client,
|
||||
|
@ -131,6 +139,27 @@ impl Client for LiveKitClient {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_participant(
|
||||
&self,
|
||||
room: String,
|
||||
identity: String,
|
||||
permission: proto::ParticipantPermission,
|
||||
) -> Result<()> {
|
||||
let _: proto::ParticipantInfo = self
|
||||
.request(
|
||||
"twirp/livekit.RoomService/UpdateParticipant",
|
||||
token::VideoGrant::to_admin(&room),
|
||||
proto::UpdateParticipantRequest {
|
||||
room: room.clone(),
|
||||
identity,
|
||||
metadata: "".to_string(),
|
||||
permission: Some(permission),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn room_token(&self, room: &str, identity: &str) -> Result<String> {
|
||||
token::create(
|
||||
&self.key,
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
pub mod api;
|
||||
mod proto;
|
||||
pub mod proto;
|
||||
pub mod token;
|
||||
|
|
|
@ -80,6 +80,7 @@ pub enum Event {
|
|||
Reloaded,
|
||||
DiffBaseChanged,
|
||||
LanguageChanged,
|
||||
CapabilityChanged,
|
||||
Reparsed,
|
||||
Saved,
|
||||
FileHandleChanged,
|
||||
|
@ -1404,7 +1405,7 @@ impl MultiBuffer {
|
|||
|
||||
fn on_buffer_event(
|
||||
&mut self,
|
||||
_: Model<Buffer>,
|
||||
buffer: Model<Buffer>,
|
||||
event: &language::Event,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
|
@ -1421,6 +1422,10 @@ impl MultiBuffer {
|
|||
language::Event::Reparsed => Event::Reparsed,
|
||||
language::Event::DiagnosticsUpdated => Event::DiagnosticsUpdated,
|
||||
language::Event::Closed => Event::Closed,
|
||||
language::Event::CapabilityChanged => {
|
||||
self.capability = buffer.read(cx).capability();
|
||||
Event::CapabilityChanged
|
||||
}
|
||||
|
||||
//
|
||||
language::Event::Operation(_) => return,
|
||||
|
|
|
@ -34,7 +34,7 @@ Rhai actually exposes a pretty nice interface for working with native Rust types
|
|||
|
||||
> **Note**: Rhai uses strings, but I wonder if you could get away with something more compact using `TypeIds`. Maybe not, given that `TypeId`s are not deterministic across builds, and we'd need matching IDs both host-side and guest side.
|
||||
|
||||
In Rhai, we can alternatively use the method `Engine::register_type_with_name::<T: Variant + Clone>(name: &str)` if we have a different type name host-side (in Rust) and guest-side (in Rhai).
|
||||
In Rhai, we can alternatively use the method `Engine::register_type_with_name::<T: Variant + Clone>(name: &str)` if we have a different type name host-side (in Rust) and guest-side (in Rhai).
|
||||
|
||||
With respect to Wasm plugins, I think an interface like this is fairly important, because we don't know whether the original plugin was written in Rust. (This may not be true now, because we write all the plugins Zed uses, but once we allow packaging and shipping plugins, it's important to maintain a consistent interface, because even Rust changes over time.)
|
||||
|
||||
|
@ -72,15 +72,15 @@ Union::Variant(v, ..) => (*v).as_boxed_any().downcast().ok().map(|x| *x),
|
|||
Now Rhai can do this because it's implemented in Rust. In other words, unlike Wasm, Rhai scripts can, indirectly, hold references to places in host memory. For us to implement something like this for Wasm plugins, we'd have to keep track of a "`ResourcePool`"—alive for the duration of each function call—that we can check rust types into and out of.
|
||||
|
||||
I think I've got a handle on how Rhai works now, so let's stop talking about Rhai and discuss what this opaque object system would look like if we implemented it in Rust.
|
||||
|
||||
|
||||
# Design Sketch
|
||||
|
||||
|
||||
First things first, we'd have to generalize the arguments we can pass to and return from functions host-side. Currently, we support anything that's `serde`able. We'd have to create a new trait, say `Value`, that has blanket implementations for both `serde` and `Clone` (or something like this; if a type is both `serde` and `clone`, we'd have to figure out a way to disambiguate).
|
||||
|
||||
We'd also create a `ResourcePool` struct that essentially is a `Vec` of `Box<dyn Any>`. When calling a function, all `Value` arguments that are resources (e.g. `Clone` instead of `serde`) would be typecasted to `dyn Any` and stored in the `ResourcePool`.
|
||||
|
||||
|
||||
We'd also create a `ResourcePool` struct that essentially is a `Vec` of `Box<dyn Any>`. When calling a function, all `Value` arguments that are resources (e.g. `Clone` instead of `serde`) would be typecasted to `dyn Any` and stored in the `ResourcePool`.
|
||||
|
||||
We'd probably also need a `Resource` trait that defines an associated handle for a resource. Something like this:
|
||||
|
||||
|
||||
```rust
|
||||
pub trait Resource {
|
||||
type Handle: Serialize + DeserializeOwned;
|
||||
|
@ -88,24 +88,24 @@ First things first, we'd have to generalize the arguments we can pass to and ret
|
|||
fn index(handle: Self) -> u32;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
Where a handle is just a dead-simple wrapper around a `u32`:
|
||||
|
||||
```rust
|
||||
|
||||
```rust
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct CoolHandle(u32);
|
||||
```
|
||||
|
||||
|
||||
It's important that this handle be accessible *both* host-side and plugin side. I don't know if this means that we have another crate, like `plugin_handles`, that contains a bunch of u32 wrappers, or something else. Because a `Resource::Handle` is just a u32, it's trivially `serde`, and can cross the ABI boundary.
|
||||
|
||||
So when we add each `T: Resource` to the `ResourcePool`, the resource pool typecasts it to `Any`, appends it to the `Vec`, and returns the associated `Resource::Handle`. This handle is what we pass through to Wasm.
|
||||
|
||||
|
||||
So when we add each `T: Resource` to the `ResourcePool`, the resource pool typecasts it to `Any`, appends it to the `Vec`, and returns the associated `Resource::Handle`. This handle is what we pass through to Wasm.
|
||||
|
||||
```rust
|
||||
// Implementations and attributes omitted
|
||||
pub struct Rope { ... };
|
||||
pub struct RopeHandle(u32);
|
||||
impl Resource for Arc<RwLock<Rope>> { ... }
|
||||
|
||||
|
||||
let builder: PluginBuilder = ...;
|
||||
let builder = builder
|
||||
.host_fn_async(
|
||||
|
@ -127,7 +127,7 @@ use plugin_handles::RopeHandle;
|
|||
pub fn append(rope: RopeHandle, string: &str);
|
||||
```
|
||||
|
||||
This allows us to perform an operation on a `Rope`, but how do we get a `RopeHandle` into a plugin? Well, as plugins, we can only acquire resources to handles we're given, so we'd need to expose a function that takes a handle.
|
||||
This allows us to perform an operation on a `Rope`, but how do we get a `RopeHandle` into a plugin? Well, as plugins, we can only acquire resources to handles we're given, so we'd need to expose a function that takes a handle.
|
||||
|
||||
To illustrate that point, here's an example. First, we'd define a plugin-side function as follows:
|
||||
|
||||
|
@ -185,4 +185,4 @@ Using this approach, it should be possible to add fairly good support for resour
|
|||
|
||||
This next week, I'll try to get a production-ready version of this working, using the `Language` resource required by some Language Server Adapters.
|
||||
|
||||
Hope this guide made sense!
|
||||
Hope this guide made sense!
|
||||
|
|
|
@ -164,7 +164,7 @@ To call the functions that a plugin exports host-side, you need to have 'handles
|
|||
|
||||
For example, let's suppose we're creating a plugin that:
|
||||
|
||||
1. formats a message
|
||||
1. formats a message
|
||||
2. processes a list of numbers somehow
|
||||
|
||||
We could create a struct for this plugin as follows:
|
||||
|
@ -179,7 +179,7 @@ pub struct CoolPlugin {
|
|||
}
|
||||
```
|
||||
|
||||
Note that this plugin also holds an owned reference to the runtime, which is stored in the `Plugin` type. In asynchronous or multithreaded contexts, it may be required to put `Plugin` behind an `Arc<Mutex<Plugin>>`. Although plugins expose an asynchronous interface, the underlying Wasm engine can only execute a single function at a time.
|
||||
Note that this plugin also holds an owned reference to the runtime, which is stored in the `Plugin` type. In asynchronous or multithreaded contexts, it may be required to put `Plugin` behind an `Arc<Mutex<Plugin>>`. Although plugins expose an asynchronous interface, the underlying Wasm engine can only execute a single function at a time.
|
||||
|
||||
> **Note**: This is a limitation of the WebAssembly standard itself. In the future, to work around this, we've been considering starting a pool of plugins, or instantiating a new plugin per call (this isn't as bad as it sounds, as instantiating a new plugin only takes about 30µs).
|
||||
|
||||
|
@ -203,7 +203,7 @@ To add a sync native function to a plugin, use the `.host_function` method:
|
|||
|
||||
```rust
|
||||
let builder = builder.host_function(
|
||||
"add_f64",
|
||||
"add_f64",
|
||||
|(a, b): (f64, f64)| a + b,
|
||||
).unwrap();
|
||||
```
|
||||
|
@ -224,7 +224,7 @@ To add an async native function to a plugin, use the `.host_function_async` meth
|
|||
|
||||
```rust
|
||||
let builder = builder.host_function_async(
|
||||
"half",
|
||||
"half",
|
||||
|n: f64| async move { n / 2.0 },
|
||||
).unwrap();
|
||||
```
|
||||
|
@ -252,9 +252,9 @@ let plugin = builder
|
|||
.unwrap();
|
||||
```
|
||||
|
||||
The `.init` method takes a single argument containing the plugin binary.
|
||||
The `.init` method takes a single argument containing the plugin binary.
|
||||
|
||||
1. If not precompiled, use `PluginBinary::Wasm(bytes)`. This supports both the WebAssembly Textual format (`.wat`) and the WebAssembly Binary format (`.wasm`).
|
||||
1. If not precompiled, use `PluginBinary::Wasm(bytes)`. This supports both the WebAssembly Textual format (`.wat`) and the WebAssembly Binary format (`.wasm`).
|
||||
|
||||
2. If precompiled, use `PluginBinary::Precompiled(bytes)`. This supports precompiled plugins ending in `.wasm.pre`. You need to be extra-careful when using precompiled plugins to ensure that the plugin target matches the target of the binary you are compiling.
|
||||
|
||||
|
@ -317,4 +317,4 @@ The `.call` method takes two arguments:
|
|||
This method is async, and must be `.await`ed. If something goes wrong (e.g. the plugin panics, or there is a type mismatch between the plugin and `WasiFn`), then this method will return an error.
|
||||
|
||||
## Last Notes
|
||||
This has been a brief overview of how the plugin system currently works in Zed. We hope to implement higher-level affordances as time goes on, to make writing plugins easier, and providing tooling so that users of Zed may also write plugins to extend their own editors.
|
||||
This has been a brief overview of how the plugin system currently works in Zed. We hope to implement higher-level affordances as time goes on, to make writing plugins easier, and providing tooling so that users of Zed may also write plugins to extend their own editors.
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue