From 7cd416c63e6c8fd9d71bd4c343085266b11d9a39 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 25 Aug 2023 21:42:18 -0600 Subject: [PATCH 01/50] Always log panics --- crates/zed/src/main.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index da726eef65..22a22ab0cf 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -495,11 +495,11 @@ fn init_panic_hook(app: &App, installation_id: Option) { installation_id: installation_id.clone(), }; - if is_pty { - if let Some(panic_data_json) = serde_json::to_string_pretty(&panic_data).log_err() { - eprintln!("{}", panic_data_json); - } - } else { + if let Some(panic_data_json) = serde_json::to_string_pretty(&panic_data).log_err() { + log::error!("{}", panic_data_json); + } + + if !is_pty { if let Some(panic_data_json) = serde_json::to_string(&panic_data).log_err() { let timestamp = chrono::Utc::now().format("%Y_%m_%d %H_%M_%S").to_string(); let panic_file_path = paths::LOGS_DIR.join(format!("zed-{}.panic", timestamp)); From a63eccf18839ecbfd35f45bd2da87b0c73a4cb2d Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 4 Oct 2023 22:46:28 -0600 Subject: [PATCH 02/50] Add url schemes to Zed --- crates/util/src/channel.rs | 26 ++++++++++++++++++++++++++ crates/zed/Cargo.toml | 3 +++ 2 files changed, 29 insertions(+) diff --git a/crates/util/src/channel.rs b/crates/util/src/channel.rs index 274fd576a0..89d42ffba6 100644 --- a/crates/util/src/channel.rs +++ b/crates/util/src/channel.rs @@ -1,6 +1,7 @@ use std::env; use lazy_static::lazy_static; +use url::Url; lazy_static! { pub static ref RELEASE_CHANNEL_NAME: String = if cfg!(debug_assertions) { @@ -15,6 +16,23 @@ lazy_static! { "stable" => ReleaseChannel::Stable, _ => panic!("invalid release channel {}", *RELEASE_CHANNEL_NAME), }; + + static ref URL_SCHEME: Url = Url::parse(match RELEASE_CHANNEL_NAME.as_str() { + "dev" => "zed-dev:/", + "preview" => "zed-preview:/", + "stable" => "zed:/", + // NOTE: this must be kept in sync with ./script/bundle and https://zed.dev. + _ => unreachable!(), + }) + .unwrap(); + static ref LINK_PREFIX: Url = Url::parse(match RELEASE_CHANNEL_NAME.as_str() { + "dev" => "http://localhost:3000/dev/", + "preview" => "https://zed.dev/preview/", + "stable" => "https://zed.dev/", + // NOTE: this must be kept in sync with https://zed.dev. + _ => unreachable!(), + }) + .unwrap(); } #[derive(Copy, Clone, PartialEq, Eq, Default)] @@ -41,4 +59,12 @@ impl ReleaseChannel { ReleaseChannel::Stable => "stable", } } + + pub fn url_scheme(&self) -> &'static Url { + &URL_SCHEME + } + + pub fn link_prefix(&self) -> &'static Url { + &LINK_PREFIX + } } diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index d4ac972a5d..7eb14559be 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -162,6 +162,7 @@ identifier = "dev.zed.Zed-Dev" name = "Zed Dev" osx_minimum_system_version = "10.15.7" osx_info_plist_exts = ["resources/info/*"] +osx_url_schemes = ["zed-dev"] [package.metadata.bundle-preview] icon = ["resources/app-icon-preview@2x.png", "resources/app-icon-preview.png"] @@ -169,6 +170,7 @@ identifier = "dev.zed.Zed-Preview" name = "Zed Preview" osx_minimum_system_version = "10.15.7" osx_info_plist_exts = ["resources/info/*"] +osx_url_schemes = ["zed-preview"] [package.metadata.bundle-stable] @@ -177,3 +179,4 @@ identifier = "dev.zed.Zed" name = "Zed" osx_minimum_system_version = "10.15.7" osx_info_plist_exts = ["resources/info/*"] +osx_url_schemes = ["zed"] From b258ee5f77f9e84e2d0f7d7aad802dca9076b321 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 5 Oct 2023 11:04:23 -0600 Subject: [PATCH 03/50] Fix ./script/bundle -l --- crates/zed/Cargo.toml | 2 +- script/bundle | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 7eb14559be..3c93462d4b 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -156,7 +156,7 @@ workspace = { path = "../workspace", features = ["test-support"] } unindent.workspace = true -[package.metadata.bundle-dev] +[package.metadata.bundle] icon = ["resources/app-icon-preview@2x.png", "resources/app-icon-preview.png"] identifier = "dev.zed.Zed-Dev" name = "Zed Dev" diff --git a/script/bundle b/script/bundle index 49da1072ce..4882189c22 100755 --- a/script/bundle +++ b/script/bundle @@ -16,7 +16,7 @@ Usage: ${0##*/} [options] [bundle_name] Build the application bundle. Options: - -d Compile in debug mode and print the app bundle's path. + -d Compile in debug mode (doesn't currently work without -l) -l Compile for local architecture only and copy bundle to /Applications. -o Open the resulting DMG or the app itself in local mode. -f Overwrite the local app bundle if it exists. @@ -92,7 +92,7 @@ sed \ Cargo.toml if [ "$local_only" = true ]; then - app_path=$(cargo bundle ${build_flag} --select-workspace-root | xargs) + app_path=$(cargo bundle ${build_flag} --target "$local_target_triple" --select-workspace-root | xargs) else app_path=$(cargo bundle ${build_flag} --target x86_64-apple-darwin --select-workspace-root | xargs) fi From 13192fa03ca4548bd29abed93597c6ac1274340b Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 5 Oct 2023 14:23:14 -0600 Subject: [PATCH 04/50] Code to allow opening zed:/channel/1234 Refactored a bit how url arguments are handled to avoid adding too much extra complexity to main. --- crates/cli/src/main.rs | 1 + crates/collab_ui/src/collab_panel.rs | 83 ++++++---------- crates/util/src/channel.rs | 17 +--- crates/workspace/src/workspace.rs | 82 ++++++++++++++++ crates/zed/src/main.rs | 137 +++++++++++---------------- crates/zed/src/open_url.rs | 101 ++++++++++++++++++++ 6 files changed, 272 insertions(+), 149 deletions(-) create mode 100644 crates/zed/src/open_url.rs diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 2f742814a8..69cfb7102b 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -182,6 +182,7 @@ impl Bundle { kCFStringEncodingUTF8, ptr::null(), )); + // equivalent to: open zed-cli:... -a /Applications/Zed\ Preview.app let urls_to_open = CFArray::from_copyable(&[url_to_open.as_concrete_TypeRef()]); LSOpenFromURLSpec( &LSLaunchURLSpec { diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 951c8bf70c..3d66e8450a 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -1969,18 +1969,21 @@ impl CollabPanel { let style = collab_theme.channel_name.inactive_state(); Flex::row() .with_child( - Label::new(channel.name.clone(), style.text.clone()) - .contained() - .with_style(style.container) - .aligned() - .left() - .with_tooltip::( - ix, - "Join channel", - None, - theme.tooltip.clone(), - cx, - ), + Label::new( + channel.name.clone().to_owned() + channel_id.to_string().as_str(), + style.text.clone(), + ) + .contained() + .with_style(style.container) + .aligned() + .left() + .with_tooltip::( + ix, + "Join channel", + None, + theme.tooltip.clone(), + cx, + ), ) .with_children({ let participants = @@ -3187,49 +3190,19 @@ impl CollabPanel { } fn join_channel(&self, channel_id: u64, cx: &mut ViewContext) { - let workspace = self.workspace.clone(); - let window = cx.window(); - let active_call = ActiveCall::global(cx); - cx.spawn(|_, mut cx| async move { - if active_call.read_with(&mut cx, |active_call, cx| { - if let Some(room) = active_call.room() { - let room = room.read(cx); - room.is_sharing_project() && room.remote_participants().len() > 0 - } else { - false - } - }) { - let answer = window.prompt( - PromptLevel::Warning, - "Leaving this call will unshare your current project.\nDo you want to switch channels?", - &["Yes, Join Channel", "Cancel"], - &mut cx, - ); - - if let Some(mut answer) = answer { - if answer.next().await == Some(1) { - return anyhow::Ok(()); - } - } - } - - let room = active_call - .update(&mut cx, |call, cx| call.join_channel(channel_id, cx)) - .await?; - - let task = room.update(&mut cx, |room, cx| { - let workspace = workspace.upgrade(cx)?; - let (project, host) = room.most_active_project()?; - let app_state = workspace.read(cx).app_state().clone(); - Some(workspace::join_remote_project(project, host, app_state, cx)) - }); - if let Some(task) = task { - task.await?; - } - - anyhow::Ok(()) - }) - .detach_and_log_err(cx); + let Some(workspace) = self.workspace.upgrade(cx) else { + return; + }; + let Some(handle) = cx.window().downcast::() else { + return; + }; + workspace::join_channel( + channel_id, + workspace.read(cx).app_state().clone(), + Some(handle), + cx, + ) + .detach_and_log_err(cx) } fn join_channel_chat(&mut self, action: &JoinChannelChat, cx: &mut ViewContext) { diff --git a/crates/util/src/channel.rs b/crates/util/src/channel.rs index 89d42ffba6..761b17e6af 100644 --- a/crates/util/src/channel.rs +++ b/crates/util/src/channel.rs @@ -17,15 +17,14 @@ lazy_static! { _ => panic!("invalid release channel {}", *RELEASE_CHANNEL_NAME), }; - static ref URL_SCHEME: Url = Url::parse(match RELEASE_CHANNEL_NAME.as_str() { + pub static ref URL_SCHEME_PREFIX: String = match RELEASE_CHANNEL_NAME.as_str() { "dev" => "zed-dev:/", "preview" => "zed-preview:/", "stable" => "zed:/", - // NOTE: this must be kept in sync with ./script/bundle and https://zed.dev. + // NOTE: this must be kept in sync with osx_url_schemes in Cargo.toml and with https://zed.dev. _ => unreachable!(), - }) - .unwrap(); - static ref LINK_PREFIX: Url = Url::parse(match RELEASE_CHANNEL_NAME.as_str() { + }.to_string(); + pub static ref LINK_PREFIX: Url = Url::parse(match RELEASE_CHANNEL_NAME.as_str() { "dev" => "http://localhost:3000/dev/", "preview" => "https://zed.dev/preview/", "stable" => "https://zed.dev/", @@ -59,12 +58,4 @@ impl ReleaseChannel { ReleaseChannel::Stable => "stable", } } - - pub fn url_scheme(&self) -> &'static Url { - &URL_SCHEME - } - - pub fn link_prefix(&self) -> &'static Url { - &LINK_PREFIX - } } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index f7bb409229..5ec847b28b 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -4154,6 +4154,88 @@ pub async fn last_opened_workspace_paths() -> Option { DB.last_workspace().await.log_err().flatten() } +pub fn join_channel( + channel_id: u64, + app_state: Arc, + requesting_window: Option>, + cx: &mut AppContext, +) -> Task> { + let active_call = ActiveCall::global(cx); + cx.spawn(|mut cx| async move { + let should_prompt = active_call.read_with(&mut cx, |active_call, cx| { + let Some(room) = active_call.room().map( |room| room.read(cx) ) else { + return false + }; + + room.is_sharing_project() && room.remote_participants().len() > 0 && + room.channel_id() != Some(channel_id) + }); + + if should_prompt { + if let Some(workspace) = requesting_window { + if let Some(window) = workspace.update(&mut cx, |cx| { + cx.window() + }) { + let answer = window.prompt( + PromptLevel::Warning, + "Leaving this call will unshare your current project.\nDo you want to switch channels?", + &["Yes, Join Channel", "Cancel"], + &mut cx, + ); + + if let Some(mut answer) = answer { + if answer.next().await == Some(1) { + return Ok(()); + } + } + } + } + } + + let room = active_call.update(&mut cx, |active_call, cx| { + active_call.join_channel(channel_id, cx) + }).await?; + + let task = room.update(&mut cx, |room, cx| { + if let Some((project, host)) = room.most_active_project() { + return Some(join_remote_project(project, host, app_state.clone(), cx)) + } + + None + }); + if let Some(task) = task { + task.await?; + return anyhow::Ok(()); + } + + if requesting_window.is_some() { + return anyhow::Ok(()); + } + + // find an existing workspace to focus and show call controls + for window in cx.windows() { + let found = window.update(&mut cx, |cx| { + let is_workspace = cx.root_view().clone().downcast::().is_some(); + if is_workspace { + cx.activate_window(); + } + is_workspace + }); + + if found.unwrap_or(false) { + return anyhow::Ok(()) + } + } + + // no open workspaces + cx.update(|cx| { + Workspace::new_local(vec![], app_state.clone(), requesting_window, cx) + }).await; + + return anyhow::Ok(()); + }) +} + #[allow(clippy::type_complexity)] pub fn open_paths( abs_paths: &[PathBuf], diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index d6f3be2b46..c491d406af 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -45,7 +45,7 @@ use std::{ }; use sum_tree::Bias; use util::{ - channel::ReleaseChannel, + channel::{ReleaseChannel, URL_SCHEME_PREFIX}, http::{self, HttpClient}, paths::PathLikeWithPosition, }; @@ -61,6 +61,10 @@ use zed::{ only_instance::{ensure_only_instance, IsOnlyInstance}, }; +use crate::open_url::{OpenListener, OpenRequest}; + +mod open_url; + fn main() { let http = http::client(); init_paths(); @@ -92,29 +96,20 @@ fn main() { }) }; - let (cli_connections_tx, mut cli_connections_rx) = mpsc::unbounded(); - let cli_connections_tx = Arc::new(cli_connections_tx); - let (open_paths_tx, mut open_paths_rx) = mpsc::unbounded(); - let open_paths_tx = Arc::new(open_paths_tx); - let urls_callback_triggered = Arc::new(AtomicBool::new(false)); - - let callback_cli_connections_tx = Arc::clone(&cli_connections_tx); - let callback_open_paths_tx = Arc::clone(&open_paths_tx); - let callback_urls_callback_triggered = Arc::clone(&urls_callback_triggered); - app.on_open_urls(move |urls, _| { - callback_urls_callback_triggered.store(true, Ordering::Release); - open_urls(urls, &callback_cli_connections_tx, &callback_open_paths_tx); - }) - .on_reopen(move |cx| { - if cx.has_global::>() { - if let Some(app_state) = cx.global::>().upgrade() { - workspace::open_new(&app_state, cx, |workspace, cx| { - Editor::new_file(workspace, &Default::default(), cx) - }) - .detach(); + let (listener, mut open_rx) = OpenListener::new(); + let listener = Arc::new(listener); + let callback_listener = listener.clone(); + app.on_open_urls(move |urls, _| callback_listener.open_urls(urls)) + .on_reopen(move |cx| { + if cx.has_global::>() { + if let Some(app_state) = cx.global::>().upgrade() { + workspace::open_new(&app_state, cx, |workspace, cx| { + Editor::new_file(workspace, &Default::default(), cx) + }) + .detach(); + } } - } - }); + }); app.run(move |cx| { cx.set_global(*RELEASE_CHANNEL); @@ -226,41 +221,52 @@ fn main() { // TODO Development mode that forces the CLI mode usually runs Zed binary as is instead // of an *app, hence gets no specific callbacks run. Emulate them here, if needed. if std::env::var(FORCE_CLI_MODE_ENV_VAR_NAME).ok().is_some() - && !urls_callback_triggered.load(Ordering::Acquire) + && !listener.triggered.load(Ordering::Acquire) { - open_urls(collect_url_args(), &cli_connections_tx, &open_paths_tx) + listener.open_urls(collect_url_args()) } - if let Ok(Some(connection)) = cli_connections_rx.try_next() { - cx.spawn(|cx| handle_cli_connection(connection, app_state.clone(), cx)) - .detach(); - } else if let Ok(Some(paths)) = open_paths_rx.try_next() { - cx.update(|cx| workspace::open_paths(&paths, &app_state, None, cx)) - .detach(); - } else { - cx.spawn({ - let app_state = app_state.clone(); - |cx| async move { restore_or_create_workspace(&app_state, cx).await } - }) - .detach() - } - - cx.spawn(|cx| { - let app_state = app_state.clone(); - async move { - while let Some(connection) = cli_connections_rx.next().await { - handle_cli_connection(connection, app_state.clone(), cx.clone()).await; - } + match open_rx.try_next() { + Ok(Some(OpenRequest::Paths { paths })) => { + cx.update(|cx| workspace::open_paths(&paths, &app_state, None, cx)) + .detach(); } - }) - .detach(); + Ok(Some(OpenRequest::CliConnection { connection })) => { + cx.spawn(|cx| handle_cli_connection(connection, app_state.clone(), cx)) + .detach(); + } + Ok(Some(OpenRequest::JoinChannel { channel_id })) => cx + .update(|cx| workspace::join_channel(channel_id, app_state.clone(), None, cx)) + .detach(), + Ok(None) | Err(_) => cx + .spawn({ + let app_state = app_state.clone(); + |cx| async move { restore_or_create_workspace(&app_state, cx).await } + }) + .detach(), + } cx.spawn(|mut cx| { let app_state = app_state.clone(); async move { - while let Some(paths) = open_paths_rx.next().await { - cx.update(|cx| workspace::open_paths(&paths, &app_state, None, cx)) - .detach(); + while let Some(request) = open_rx.next().await { + match request { + OpenRequest::Paths { paths } => { + cx.update(|cx| workspace::open_paths(&paths, &app_state, None, cx)) + .detach(); + } + OpenRequest::CliConnection { connection } => { + cx.spawn(|cx| { + handle_cli_connection(connection, app_state.clone(), cx) + }) + .detach(); + } + OpenRequest::JoinChannel { channel_id } => cx + .update(|cx| { + workspace::join_channel(channel_id, app_state.clone(), None, cx) + }) + .detach(), + } } } }) @@ -297,37 +303,6 @@ async fn installation_id() -> Result { } } -fn open_urls( - urls: Vec, - cli_connections_tx: &mpsc::UnboundedSender<( - mpsc::Receiver, - IpcSender, - )>, - open_paths_tx: &mpsc::UnboundedSender>, -) { - if let Some(server_name) = urls.first().and_then(|url| url.strip_prefix("zed-cli://")) { - if let Some(cli_connection) = connect_to_cli(server_name).log_err() { - cli_connections_tx - .unbounded_send(cli_connection) - .map_err(|_| anyhow!("no listener for cli connections")) - .log_err(); - }; - } else { - let paths: Vec<_> = urls - .iter() - .flat_map(|url| url.strip_prefix("file://")) - .map(|url| { - let decoded = urlencoding::decode_binary(url.as_bytes()); - PathBuf::from(OsStr::from_bytes(decoded.as_ref())) - }) - .collect(); - open_paths_tx - .unbounded_send(paths) - .map_err(|_| anyhow!("no listener for open urls requests")) - .log_err(); - } -} - async fn restore_or_create_workspace(app_state: &Arc, mut cx: AsyncAppContext) { if let Some(location) = workspace::last_opened_workspace_paths().await { cx.update(|cx| workspace::open_paths(location.paths().as_ref(), app_state, None, cx)) diff --git a/crates/zed/src/open_url.rs b/crates/zed/src/open_url.rs new file mode 100644 index 0000000000..f421633d5b --- /dev/null +++ b/crates/zed/src/open_url.rs @@ -0,0 +1,101 @@ +use anyhow::anyhow; +use cli::{ipc::IpcSender, CliRequest, CliResponse}; +use futures::channel::mpsc; +use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender}; +use std::ffi::OsStr; +use std::os::unix::prelude::OsStrExt; +use std::sync::atomic::Ordering; +use std::{path::PathBuf, sync::atomic::AtomicBool}; +use util::channel::URL_SCHEME_PREFIX; +use util::ResultExt; + +use crate::{connect_to_cli, handle_cli_connection}; + +pub enum OpenRequest { + Paths { + paths: Vec, + }, + CliConnection { + connection: (mpsc::Receiver, IpcSender), + }, + JoinChannel { + channel_id: u64, + }, +} + +pub struct OpenListener { + tx: UnboundedSender, + pub triggered: AtomicBool, +} + +impl OpenListener { + pub fn new() -> (Self, UnboundedReceiver) { + let (tx, rx) = mpsc::unbounded(); + ( + OpenListener { + tx, + triggered: AtomicBool::new(false), + }, + rx, + ) + } + + pub fn open_urls(&self, urls: Vec) { + self.triggered.store(true, Ordering::Release); + dbg!(&urls); + let request = if let Some(server_name) = + urls.first().and_then(|url| url.strip_prefix("zed-cli://")) + { + self.handle_cli_connection(server_name) + } else if let Some(request_path) = urls + .first() + .and_then(|url| url.strip_prefix(URL_SCHEME_PREFIX.as_str())) + { + self.handle_zed_url_scheme(request_path) + } else { + self.handle_file_urls(urls) + }; + + if let Some(request) = request { + self.tx + .unbounded_send(request) + .map_err(|_| anyhow!("no listener for open requests")) + .log_err(); + } + } + + fn handle_cli_connection(&self, server_name: &str) -> Option { + if let Some(connection) = connect_to_cli(server_name).log_err() { + return Some(OpenRequest::CliConnection { connection }); + } + + None + } + + fn handle_zed_url_scheme(&self, request_path: &str) -> Option { + let mut parts = request_path.split("/"); + if parts.next() == Some("channel") { + if let Some(slug) = parts.next() { + if let Some(id_str) = slug.split("-").last() { + if let Ok(channel_id) = id_str.parse::() { + return Some(OpenRequest::JoinChannel { channel_id }); + } + } + } + } + None + } + + fn handle_file_urls(&self, urls: Vec) -> Option { + let paths: Vec<_> = urls + .iter() + .flat_map(|url| url.strip_prefix("file://")) + .map(|url| { + let decoded = urlencoding::decode_binary(url.as_bytes()); + PathBuf::from(OsStr::from_bytes(decoded.as_ref())) + }) + .collect(); + + Some(OpenRequest::Paths { paths }) + } +} From 31062d424f385c34ccc3496e74dbd0d57978cf90 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Thu, 5 Oct 2023 16:41:08 -0700 Subject: [PATCH 05/50] make bundle script incremental when using debug or local builds --- crates/zed/resources/zed.entitlements | 10 ++-------- crates/zed/src/main.rs | 6 ++---- crates/zed/src/open_url.rs | 2 +- script/bundle | 27 +++++++++++++++++++-------- 4 files changed, 24 insertions(+), 21 deletions(-) diff --git a/crates/zed/resources/zed.entitlements b/crates/zed/resources/zed.entitlements index f40a8a253a..dca0ff6766 100644 --- a/crates/zed/resources/zed.entitlements +++ b/crates/zed/resources/zed.entitlements @@ -10,14 +10,8 @@ com.apple.security.device.camera - com.apple.security.personal-information.addressbook - - com.apple.security.personal-information.calendars - - com.apple.security.personal-information.location - - com.apple.security.personal-information.photos-library - + com.apple.security.keychain-access-groups + MQ55VZLNZQ.dev.zed.Shared diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index c491d406af..9d0451ecfa 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -32,12 +32,10 @@ use std::{ ffi::OsStr, fs::OpenOptions, io::{IsTerminal, Write as _}, - os::unix::prelude::OsStrExt, panic, path::{Path, PathBuf}, - str, sync::{ - atomic::{AtomicBool, AtomicU32, Ordering}, + atomic::{AtomicU32, Ordering}, Arc, Weak, }, thread, @@ -45,7 +43,7 @@ use std::{ }; use sum_tree::Bias; use util::{ - channel::{ReleaseChannel, URL_SCHEME_PREFIX}, + channel::ReleaseChannel, http::{self, HttpClient}, paths::PathLikeWithPosition, }; diff --git a/crates/zed/src/open_url.rs b/crates/zed/src/open_url.rs index f421633d5b..1c741a02c8 100644 --- a/crates/zed/src/open_url.rs +++ b/crates/zed/src/open_url.rs @@ -9,7 +9,7 @@ use std::{path::PathBuf, sync::atomic::AtomicBool}; use util::channel::URL_SCHEME_PREFIX; use util::ResultExt; -use crate::{connect_to_cli, handle_cli_connection}; +use crate::connect_to_cli; pub enum OpenRequest { Paths { diff --git a/script/bundle b/script/bundle index 4882189c22..94741d290f 100755 --- a/script/bundle +++ b/script/bundle @@ -5,6 +5,7 @@ set -e build_flag="--release" target_dir="release" open_result=false +local_arch=false local_only=false overwrite_local_app=false bundle_name="" @@ -16,8 +17,8 @@ Usage: ${0##*/} [options] [bundle_name] Build the application bundle. Options: - -d Compile in debug mode (doesn't currently work without -l) - -l Compile for local architecture only and copy bundle to /Applications. + -d Compile in debug mode + -l Compile for local architecture and copy bundle to /Applications, implies -d. -o Open the resulting DMG or the app itself in local mode. -f Overwrite the local app bundle if it exists. -h Display this help and exit. @@ -32,10 +33,20 @@ do case "${flag}" in o) open_result=true;; d) + export CARGO_INCREMENTAL=true + export CARGO_BUNDLE_SKIP_BUILD=true build_flag=""; + local_arch=true + target_dir="debug" + ;; + l) + export CARGO_INCREMENTAL=true + export CARGO_BUNDLE_SKIP_BUILD=true + build_flag="" + local_arch=true + local_only=true target_dir="debug" ;; - l) local_only=true;; f) overwrite_local_app=true;; h) help_info @@ -67,7 +78,7 @@ version_info=$(rustc --version --verbose) host_line=$(echo "$version_info" | grep host) local_target_triple=${host_line#*: } -if [ "$local_only" = true ]; then +if [ "$local_arch" = true ]; then echo "Building for local target only." cargo build ${build_flag} --package zed cargo build ${build_flag} --package cli @@ -91,8 +102,8 @@ sed \ "s/package.metadata.bundle-${channel}/package.metadata.bundle/" \ Cargo.toml -if [ "$local_only" = true ]; then - app_path=$(cargo bundle ${build_flag} --target "$local_target_triple" --select-workspace-root | xargs) +if [ "$local_arch" = true ]; then + app_path=$(cargo bundle ${build_flag} --select-workspace-root | xargs) else app_path=$(cargo bundle ${build_flag} --target x86_64-apple-darwin --select-workspace-root | xargs) fi @@ -101,7 +112,7 @@ mv Cargo.toml.backup Cargo.toml popd echo "Bundled ${app_path}" -if [ "$local_only" = false ]; then +if [ "$local_arch" = false ]; then echo "Creating fat binaries" lipo \ -create \ @@ -136,7 +147,7 @@ else codesign --force --deep --entitlements crates/zed/resources/zed.entitlements --sign - "${app_path}" -v fi -if [ "$target_dir" = "debug" ]; then +if [[ "$target_dir" = "debug" && "$local_only" = false ]]; then if [ "$open_result" = true ]; then open "$app_path" else From 2d99b327fc2e7f094692ffd241193180d66a8818 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 5 Oct 2023 19:26:22 -0600 Subject: [PATCH 06/50] Don't wrap on paragraphs For zed-industries/community#2116 --- .cargo/config.toml | 2 +- crates/editor/src/editor_tests.rs | 45 +++++++------------ crates/editor/src/movement.rs | 4 +- crates/vim/src/test.rs | 25 +++++++++++ .../test_data/test_paragraphs_dont_wrap.json | 8 ++++ 5 files changed, 52 insertions(+), 32 deletions(-) create mode 100644 crates/vim/test_data/test_paragraphs_dont_wrap.json diff --git a/.cargo/config.toml b/.cargo/config.toml index 9da6b3be08..e22bdb0f2c 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -3,4 +3,4 @@ xtask = "run --package xtask --" [build] # v0 mangling scheme provides more detailed backtraces around closures -rustflags = ["-C", "symbol-mangling-version=v0"] +rustflags = ["-C", "symbol-mangling-version=v0", "-C", "link-arg=-fuse-ld=/opt/homebrew/Cellar/llvm/16.0.6/bin/ld64.lld"] diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index dee27e0121..dc723c7012 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -1333,7 +1333,7 @@ async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppCon cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx)); cx.assert_editor_state( - &r#"ˇone + &r#"one two three @@ -1344,9 +1344,22 @@ async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppCon .unindent(), ); - cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx)); + cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx)); cx.assert_editor_state( - &r#"ˇone + &r#"one + two + + three + four + five + ˇ + six"# + .unindent(), + ); + + cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx)); + cx.assert_editor_state( + &r#"one two ˇ three @@ -1366,32 +1379,6 @@ async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppCon four five - sixˇ"# - .unindent(), - ); - - cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx)); - cx.assert_editor_state( - &r#"one - two - - three - four - five - ˇ - sixˇ"# - .unindent(), - ); - - cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx)); - cx.assert_editor_state( - &r#"one - two - ˇ - three - four - five - ˇ six"# .unindent(), ); diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 974af4bc24..245c2d9977 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -234,7 +234,7 @@ pub fn start_of_paragraph( ) -> DisplayPoint { let point = display_point.to_point(map); if point.row == 0 { - return map.max_point(); + return DisplayPoint::zero(); } let mut found_non_blank_line = false; @@ -261,7 +261,7 @@ pub fn end_of_paragraph( ) -> DisplayPoint { let point = display_point.to_point(map); if point.row == map.max_buffer_row() { - return DisplayPoint::zero(); + return map.max_point(); } let mut found_non_blank_line = false; diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index 52dcb54ce2..34b9e38768 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -652,3 +652,28 @@ async fn test_selection_goal(cx: &mut gpui::TestAppContext) { Lorem Ipsum"}) .await; } + +#[gpui::test] +async fn test_paragraphs_dont_wrap(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state(indoc! {" + one + ˇ + two"}) + .await; + + cx.simulate_shared_keystrokes(["}", "}"]).await; + cx.assert_shared_state(indoc! {" + one + + twˇo"}) + .await; + + cx.simulate_shared_keystrokes(["{", "{", "{"]).await; + cx.assert_shared_state(indoc! {" + ˇone + + two"}) + .await; +} diff --git a/crates/vim/test_data/test_paragraphs_dont_wrap.json b/crates/vim/test_data/test_paragraphs_dont_wrap.json new file mode 100644 index 0000000000..9e729651be --- /dev/null +++ b/crates/vim/test_data/test_paragraphs_dont_wrap.json @@ -0,0 +1,8 @@ +{"Put":{"state":"one\nˇ\ntwo"}} +{"Key":"}"} +{"Key":"}"} +{"Get":{"state":"one\n\ntwˇo","mode":"Normal"}} +{"Key":"{"} +{"Key":"{"} +{"Key":"{"} +{"Get":{"state":"ˇone\n\ntwo","mode":"Normal"}} From b58c42cd536e2317a009ed48464a8f691199b731 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 6 Oct 2023 13:47:35 -0600 Subject: [PATCH 07/50] TEMP --- .cargo/config.toml | 2 +- crates/util/src/channel.rs | 49 +++++---- crates/workspace/src/workspace.rs | 161 ++++++++++++++++++++---------- crates/zed/src/main.rs | 107 ++++++++++---------- crates/zed/src/open_url.rs | 7 +- 5 files changed, 197 insertions(+), 129 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index 9da6b3be08..e22bdb0f2c 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -3,4 +3,4 @@ xtask = "run --package xtask --" [build] # v0 mangling scheme provides more detailed backtraces around closures -rustflags = ["-C", "symbol-mangling-version=v0"] +rustflags = ["-C", "symbol-mangling-version=v0", "-C", "link-arg=-fuse-ld=/opt/homebrew/Cellar/llvm/16.0.6/bin/ld64.lld"] diff --git a/crates/util/src/channel.rs b/crates/util/src/channel.rs index 761b17e6af..2364dcaad4 100644 --- a/crates/util/src/channel.rs +++ b/crates/util/src/channel.rs @@ -1,7 +1,6 @@ use std::env; use lazy_static::lazy_static; -use url::Url; lazy_static! { pub static ref RELEASE_CHANNEL_NAME: String = if cfg!(debug_assertions) { @@ -16,22 +15,6 @@ lazy_static! { "stable" => ReleaseChannel::Stable, _ => panic!("invalid release channel {}", *RELEASE_CHANNEL_NAME), }; - - pub static ref URL_SCHEME_PREFIX: String = match RELEASE_CHANNEL_NAME.as_str() { - "dev" => "zed-dev:/", - "preview" => "zed-preview:/", - "stable" => "zed:/", - // NOTE: this must be kept in sync with osx_url_schemes in Cargo.toml and with https://zed.dev. - _ => unreachable!(), - }.to_string(); - pub static ref LINK_PREFIX: Url = Url::parse(match RELEASE_CHANNEL_NAME.as_str() { - "dev" => "http://localhost:3000/dev/", - "preview" => "https://zed.dev/preview/", - "stable" => "https://zed.dev/", - // NOTE: this must be kept in sync with https://zed.dev. - _ => unreachable!(), - }) - .unwrap(); } #[derive(Copy, Clone, PartialEq, Eq, Default)] @@ -58,4 +41,36 @@ impl ReleaseChannel { ReleaseChannel::Stable => "stable", } } + + pub fn url_scheme(&self) -> &'static str { + match self { + ReleaseChannel::Dev => "zed-dev:/", + ReleaseChannel::Preview => "zed-preview:/", + ReleaseChannel::Stable => "zed:/", + } + } + + pub fn link_prefix(&self) -> &'static str { + match self { + ReleaseChannel::Dev => "https://zed.dev/dev/", + ReleaseChannel::Preview => "https://zed.dev/preview/", + ReleaseChannel::Stable => "https://zed.dev/", + } + } +} + +pub fn parse_zed_link(link: &str) -> Option<&str> { + for release in [ + ReleaseChannel::Dev, + ReleaseChannel::Preview, + ReleaseChannel::Stable, + ] { + if let Some(stripped) = link.strip_prefix(release.link_prefix()) { + return Some(stripped); + } + if let Some(stripped) = link.strip_prefix(release.url_scheme()) { + return Some(stripped); + } + } + None } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 5ec847b28b..1002ae29dc 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -15,14 +15,14 @@ use call::ActiveCall; use channel::ChannelStore; use client::{ proto::{self, PeerId}, - Client, TypedEnvelope, UserStore, + Client, Status, TypedEnvelope, UserStore, }; use collections::{hash_map, HashMap, HashSet}; use drag_and_drop::DragAndDrop; use futures::{ channel::{mpsc, oneshot}, future::try_join_all, - FutureExt, StreamExt, + select_biased, FutureExt, StreamExt, }; use gpui::{ actions, @@ -4154,6 +4154,100 @@ pub async fn last_opened_workspace_paths() -> Option { DB.last_workspace().await.log_err().flatten() } +async fn join_channel_internal( + channel_id: u64, + app_state: Arc, + requesting_window: Option>, + active_call: &ModelHandle, + cx: &mut AsyncAppContext, +) -> Result { + let should_prompt = active_call.read_with(cx, |active_call, cx| { + let Some(room) = active_call.room().map(|room| room.read(cx)) else { + return false; + }; + + room.is_sharing_project() + && room.remote_participants().len() > 0 + && room.channel_id() != Some(channel_id) + }); + + if should_prompt { + if let Some(workspace) = requesting_window { + if let Some(window) = workspace.update(cx, |cx| cx.window()) { + let answer = window.prompt( + PromptLevel::Warning, + "Leaving this call will unshare your current project.\nDo you want to switch channels?", + &["Yes, Join Channel", "Cancel"], + cx, + ); + + if let Some(mut answer) = answer { + if answer.next().await == Some(1) { + return Ok(false); + } + } + } else { + return Ok(false); // unreachable!() hopefully + } + } else { + return Ok(false); // unreachable!() hopefully + } + } + + let client = cx.read(|cx| active_call.read(cx).client()); + + let mut timer = cx.background().timer(Duration::from_secs(5)).fuse(); + let mut client_status = client.status(); + + 'outer: loop { + select_biased! { + _ = timer => { + return Err(anyhow!("connecting timed out")) + }, + status = client_status.recv().fuse() => { + let Some(status) = status else { + return Err(anyhow!("unexpected error reading connection status")) + }; + + match status { + Status::Connecting | Status::Authenticating | Status::Reconnecting | Status::Reauthenticating => continue, + Status::Connected { .. } => break 'outer, + Status::SignedOut => { + if client.has_keychain_credentials(&cx) { + client.authenticate_and_connect(true, &cx).await?; + timer = cx.background().timer(Duration::from_secs(5)).fuse(); + } else { + return Err(anyhow!("not signed in")) + } + }, + Status::UpgradeRequired => return Err(anyhow!("zed is out of date")), + | Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => return Err(anyhow!("zed is offline")) + } + } + } + } + + let room = active_call + .update(cx, |active_call, cx| { + active_call.join_channel(channel_id, cx) + }) + .await?; + + let task = room.update(cx, |room, cx| { + if let Some((project, host)) = room.most_active_project() { + return Some(join_remote_project(project, host, app_state.clone(), cx)); + } + + None + }); + if let Some(task) = task { + task.await?; + return anyhow::Ok(true); + } + + anyhow::Ok(false) +} + pub fn join_channel( channel_id: u64, app_state: Arc, @@ -4161,50 +4255,18 @@ pub fn join_channel( cx: &mut AppContext, ) -> Task> { let active_call = ActiveCall::global(cx); - cx.spawn(|mut cx| async move { - let should_prompt = active_call.read_with(&mut cx, |active_call, cx| { - let Some(room) = active_call.room().map( |room| room.read(cx) ) else { - return false - }; + cx.spawn(|mut cx| { + let result = join_channel_internal( + channel_id, + app_state, + requesting_window, + &active_call, + &mut cx, + ) + .await; - room.is_sharing_project() && room.remote_participants().len() > 0 && - room.channel_id() != Some(channel_id) - }); - - if should_prompt { - if let Some(workspace) = requesting_window { - if let Some(window) = workspace.update(&mut cx, |cx| { - cx.window() - }) { - let answer = window.prompt( - PromptLevel::Warning, - "Leaving this call will unshare your current project.\nDo you want to switch channels?", - &["Yes, Join Channel", "Cancel"], - &mut cx, - ); - - if let Some(mut answer) = answer { - if answer.next().await == Some(1) { - return Ok(()); - } - } - } - } - } - - let room = active_call.update(&mut cx, |active_call, cx| { - active_call.join_channel(channel_id, cx) - }).await?; - - let task = room.update(&mut cx, |room, cx| { - if let Some((project, host)) = room.most_active_project() { - return Some(join_remote_project(project, host, app_state.clone(), cx)) - } - - None - }); - if let Some(task) = task { - task.await?; + // join channel succeeded, and opened a window + if Some(true) = result { return anyhow::Ok(()); } @@ -4223,16 +4285,15 @@ pub fn join_channel( }); if found.unwrap_or(false) { - return anyhow::Ok(()) + return anyhow::Ok(()); } } // no open workspaces - cx.update(|cx| { - Workspace::new_local(vec![], app_state.clone(), requesting_window, cx) - }).await; + cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), requesting_window, cx)) + .await; - return anyhow::Ok(()); + return connected.map(|_| ()); }) } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 9d0451ecfa..861121a1cf 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -43,7 +43,7 @@ use std::{ }; use sum_tree::Bias; use util::{ - channel::ReleaseChannel, + channel::{parse_zed_link, ReleaseChannel}, http::{self, HttpClient}, paths::PathLikeWithPosition, }; @@ -206,12 +206,9 @@ fn main() { if stdout_is_a_pty() { cx.platform().activate(true); - let paths = collect_path_args(); - if paths.is_empty() { - cx.spawn(|cx| async move { restore_or_create_workspace(&app_state, cx).await }) - .detach() - } else { - workspace::open_paths(&paths, &app_state, None, cx).detach_and_log_err(cx); + let urls = collect_url_args(); + if !urls.is_empty() { + listener.open_urls(urls) } } else { upload_previous_panics(http.clone(), cx); @@ -223,53 +220,51 @@ fn main() { { listener.open_urls(collect_url_args()) } + } - match open_rx.try_next() { - Ok(Some(OpenRequest::Paths { paths })) => { - cx.update(|cx| workspace::open_paths(&paths, &app_state, None, cx)) - .detach(); - } - Ok(Some(OpenRequest::CliConnection { connection })) => { - cx.spawn(|cx| handle_cli_connection(connection, app_state.clone(), cx)) - .detach(); - } - Ok(Some(OpenRequest::JoinChannel { channel_id })) => cx - .update(|cx| workspace::join_channel(channel_id, app_state.clone(), None, cx)) - .detach(), - Ok(None) | Err(_) => cx - .spawn({ - let app_state = app_state.clone(); - |cx| async move { restore_or_create_workspace(&app_state, cx).await } - }) - .detach(), + match open_rx.try_next() { + Ok(Some(OpenRequest::Paths { paths })) => { + cx.update(|cx| workspace::open_paths(&paths, &app_state, None, cx)) + .detach(); } + Ok(Some(OpenRequest::CliConnection { connection })) => { + cx.spawn(|cx| handle_cli_connection(connection, app_state.clone(), cx)) + .detach(); + } + Ok(Some(OpenRequest::JoinChannel { channel_id })) => cx + .update(|cx| workspace::join_channel(channel_id, app_state.clone(), None, cx)) + .detach_and_log_err(cx), + Ok(None) | Err(_) => cx + .spawn({ + let app_state = app_state.clone(); + |cx| async move { restore_or_create_workspace(&app_state, cx).await } + }) + .detach(), + } - cx.spawn(|mut cx| { - let app_state = app_state.clone(); - async move { - while let Some(request) = open_rx.next().await { - match request { - OpenRequest::Paths { paths } => { - cx.update(|cx| workspace::open_paths(&paths, &app_state, None, cx)) - .detach(); - } - OpenRequest::CliConnection { connection } => { - cx.spawn(|cx| { - handle_cli_connection(connection, app_state.clone(), cx) - }) + cx.spawn(|mut cx| { + let app_state = app_state.clone(); + async move { + while let Some(request) = open_rx.next().await { + match request { + OpenRequest::Paths { paths } => { + cx.update(|cx| workspace::open_paths(&paths, &app_state, None, cx)) .detach(); - } - OpenRequest::JoinChannel { channel_id } => cx - .update(|cx| { - workspace::join_channel(channel_id, app_state.clone(), None, cx) - }) - .detach(), } + OpenRequest::CliConnection { connection } => { + cx.spawn(|cx| handle_cli_connection(connection, app_state.clone(), cx)) + .detach(); + } + OpenRequest::JoinChannel { channel_id } => cx + .update(|cx| { + workspace::join_channel(channel_id, app_state.clone(), None, cx) + }) + .detach(), } } - }) - .detach(); - } + } + }) + .detach(); cx.spawn(|cx| async move { if stdout_is_a_pty() { @@ -608,23 +603,23 @@ fn stdout_is_a_pty() -> bool { std::env::var(FORCE_CLI_MODE_ENV_VAR_NAME).ok().is_none() && std::io::stdout().is_terminal() } -fn collect_path_args() -> Vec { +fn collect_url_args() -> Vec { env::args() .skip(1) - .filter_map(|arg| match std::fs::canonicalize(arg) { - Ok(path) => Some(path), + .filter_map(|arg| match std::fs::canonicalize(Path::new(&arg)) { + Ok(path) => Some(format!("file://{}", path.to_string_lossy())), Err(error) => { - log::error!("error parsing path argument: {}", error); - None + if let Some(_) = parse_zed_link(&arg) { + Some(arg) + } else { + log::error!("error parsing path argument: {}", error); + None + } } }) .collect() } -fn collect_url_args() -> Vec { - env::args().skip(1).collect() -} - fn load_embedded_fonts(app: &App) { let font_paths = Assets.list("fonts"); let embedded_fonts = Mutex::new(Vec::new()); diff --git a/crates/zed/src/open_url.rs b/crates/zed/src/open_url.rs index 1c741a02c8..6f90953de2 100644 --- a/crates/zed/src/open_url.rs +++ b/crates/zed/src/open_url.rs @@ -6,7 +6,7 @@ use std::ffi::OsStr; use std::os::unix::prelude::OsStrExt; use std::sync::atomic::Ordering; use std::{path::PathBuf, sync::atomic::AtomicBool}; -use util::channel::URL_SCHEME_PREFIX; +use util::channel::parse_zed_link; use util::ResultExt; use crate::connect_to_cli; @@ -47,10 +47,7 @@ impl OpenListener { urls.first().and_then(|url| url.strip_prefix("zed-cli://")) { self.handle_cli_connection(server_name) - } else if let Some(request_path) = urls - .first() - .and_then(|url| url.strip_prefix(URL_SCHEME_PREFIX.as_str())) - { + } else if let Some(request_path) = urls.first().and_then(|url| parse_zed_link(url)) { self.handle_zed_url_scheme(request_path) } else { self.handle_file_urls(urls) From 43da36948bdb0a230823f173d9a3e339e0ce2811 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 6 Oct 2023 13:14:39 -0700 Subject: [PATCH 08/50] Add a crate-dep-graph script for showing the crate dependency graph --- script/crate-dep-graph | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100755 script/crate-dep-graph diff --git a/script/crate-dep-graph b/script/crate-dep-graph new file mode 100755 index 0000000000..25285cc097 --- /dev/null +++ b/script/crate-dep-graph @@ -0,0 +1,19 @@ +#!/bin/bash + +set -e + +if [[ -x cargo-depgraph ]]; then + cargo install cargo-depgraph +fi + +graph_file=target/crate-graph.html + +cargo depgraph \ + --workspace-only \ + --offline \ + --root=zed,cli,collab \ + --dedup-transitive-deps \ + | dot -Tsvg > $graph_file + +echo "open $graph_file" +open $graph_file From 17925ed5631f86a8564c885172cd9521012b2edc Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 6 Oct 2023 13:14:53 -0700 Subject: [PATCH 09/50] Remove unnecessary dependencies on client and rpc --- Cargo.lock | 1 - crates/fs/Cargo.toml | 1 - crates/fs/src/repository.rs | 20 +------------------- crates/language/Cargo.toml | 1 - crates/project/src/worktree.rs | 22 ++++++++++++++++++++-- 5 files changed, 21 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c57548057d..1b6583f067 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2832,7 +2832,6 @@ dependencies = [ "parking_lot 0.11.2", "regex", "rope", - "rpc", "serde", "serde_derive", "serde_json", diff --git a/crates/fs/Cargo.toml b/crates/fs/Cargo.toml index 441ce6f9c7..11a34bcecb 100644 --- a/crates/fs/Cargo.toml +++ b/crates/fs/Cargo.toml @@ -13,7 +13,6 @@ rope = { path = "../rope" } text = { path = "../text" } util = { path = "../util" } sum_tree = { path = "../sum_tree" } -rpc = { path = "../rpc" } anyhow.workspace = true async-trait.workspace = true diff --git a/crates/fs/src/repository.rs b/crates/fs/src/repository.rs index 2b2aebe679..4637a7f754 100644 --- a/crates/fs/src/repository.rs +++ b/crates/fs/src/repository.rs @@ -2,7 +2,6 @@ use anyhow::Result; use collections::HashMap; use git2::{BranchType, StatusShow}; use parking_lot::Mutex; -use rpc::proto; use serde_derive::{Deserialize, Serialize}; use std::{ cmp::Ordering, @@ -23,6 +22,7 @@ pub struct Branch { /// Timestamp of most recent commit, normalized to Unix Epoch format. pub unix_timestamp: Option, } + #[async_trait::async_trait] pub trait GitRepository: Send { fn reload_index(&self); @@ -358,24 +358,6 @@ impl GitFileStatus { } } } - - pub fn from_proto(git_status: Option) -> Option { - git_status.and_then(|status| { - proto::GitStatus::from_i32(status).map(|status| match status { - proto::GitStatus::Added => GitFileStatus::Added, - proto::GitStatus::Modified => GitFileStatus::Modified, - proto::GitStatus::Conflict => GitFileStatus::Conflict, - }) - }) - } - - pub fn to_proto(self) -> i32 { - match self { - GitFileStatus::Added => proto::GitStatus::Added as i32, - GitFileStatus::Modified => proto::GitStatus::Modified as i32, - GitFileStatus::Conflict => proto::GitStatus::Conflict as i32, - } - } } #[derive(Clone, Debug, Ord, Hash, PartialOrd, Eq, PartialEq)] diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index 4771fc7083..cf468020ce 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -22,7 +22,6 @@ test-support = [ ] [dependencies] -client = { path = "../client" } clock = { path = "../clock" } collections = { path = "../collections" } fuzzy = { path = "../fuzzy" } diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 2de3671033..a38e43cd87 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -4310,7 +4310,7 @@ impl<'a> From<&'a Entry> for proto::Entry { is_symlink: entry.is_symlink, is_ignored: entry.is_ignored, is_external: entry.is_external, - git_status: entry.git_status.map(|status| status.to_proto()), + git_status: entry.git_status.map(git_status_to_proto), } } } @@ -4337,7 +4337,7 @@ impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry { is_symlink: entry.is_symlink, is_ignored: entry.is_ignored, is_external: entry.is_external, - git_status: GitFileStatus::from_proto(entry.git_status), + git_status: git_status_from_proto(entry.git_status), }) } else { Err(anyhow!( @@ -4366,3 +4366,21 @@ fn combine_git_statuses( unstaged } } + +fn git_status_from_proto(git_status: Option) -> Option { + git_status.and_then(|status| { + proto::GitStatus::from_i32(status).map(|status| match status { + proto::GitStatus::Added => GitFileStatus::Added, + proto::GitStatus::Modified => GitFileStatus::Modified, + proto::GitStatus::Conflict => GitFileStatus::Conflict, + }) + }) +} + +fn git_status_to_proto(status: GitFileStatus) -> i32 { + match status { + GitFileStatus::Added => proto::GitStatus::Added as i32, + GitFileStatus::Modified => proto::GitStatus::Modified as i32, + GitFileStatus::Conflict => proto::GitStatus::Conflict as i32, + } +} From 3412bb75bebcc8ec4f77234baa9acca7fb1bc48f Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 6 Oct 2023 13:39:10 -0700 Subject: [PATCH 10/50] Remove call -> channel dependency --- Cargo.lock | 1 - crates/call/Cargo.toml | 1 - crates/call/src/call.rs | 3 +-- 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1b6583f067..60d7b0b7d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1082,7 +1082,6 @@ dependencies = [ "anyhow", "async-broadcast", "audio", - "channel", "client", "collections", "fs", diff --git a/crates/call/Cargo.toml b/crates/call/Cargo.toml index b4e94fe56c..eb448d8d8d 100644 --- a/crates/call/Cargo.toml +++ b/crates/call/Cargo.toml @@ -20,7 +20,6 @@ test-support = [ [dependencies] audio = { path = "../audio" } -channel = { path = "../channel" } client = { path = "../client" } collections = { path = "../collections" } gpui = { path = "../gpui" } diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index d86ed1be37..0846341325 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -5,7 +5,6 @@ pub mod room; use anyhow::{anyhow, Result}; use audio::Audio; use call_settings::CallSettings; -use channel::ChannelId; use client::{ proto, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE, @@ -79,7 +78,7 @@ impl ActiveCall { } } - pub fn channel_id(&self, cx: &AppContext) -> Option { + pub fn channel_id(&self, cx: &AppContext) -> Option { self.room()?.read(cx).channel_id() } From 4128e2ffcbb5317ffc668c0c851e3a51474649e2 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 6 Oct 2023 15:18:25 -0600 Subject: [PATCH 11/50] Fix panic if the host is not there. --- crates/call/src/room.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index 72db174d72..2543697bc0 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -602,6 +602,7 @@ impl Room { pub fn most_active_project(&self) -> Option<(u64, u64)> { let mut projects = HashMap::default(); let mut hosts = HashMap::default(); + for participant in self.remote_participants.values() { match participant.location { ParticipantLocation::SharedProject { project_id } => { @@ -619,8 +620,8 @@ impl Room { pairs.sort_by_key(|(_, count)| *count as i32); pairs - .first() - .map(|(project_id, _)| (*project_id, hosts[&project_id])) + .iter() + .find_map(|(project_id, _)| hosts.get(project_id).map(|host| (*project_id, *host))) } async fn handle_room_updated( From f8ca86c6a7d32f45293c6837fa23bf5bf4a6b060 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 6 Oct 2023 14:16:08 -0700 Subject: [PATCH 12/50] Remove workspace -> channel dependency --- Cargo.lock | 1 - crates/channel/src/channel.rs | 10 +++--- crates/channel/src/channel_store.rs | 11 ++++++- crates/channel/src/channel_store_tests.rs | 4 +-- crates/collab/src/tests/test_server.rs | 39 ++++++++++------------- crates/collab_ui/src/channel_view.rs | 2 +- crates/collab_ui/src/chat_panel.rs | 2 +- crates/collab_ui/src/collab_panel.rs | 2 +- crates/workspace/Cargo.toml | 1 - crates/workspace/src/workspace.rs | 9 +----- crates/zed/src/main.rs | 6 +--- crates/zed/src/zed.rs | 1 + 12 files changed, 41 insertions(+), 47 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 60d7b0b7d5..24d8359421 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9969,7 +9969,6 @@ dependencies = [ "async-recursion 1.0.5", "bincode", "call", - "channel", "client", "collections", "context_menu", diff --git a/crates/channel/src/channel.rs b/crates/channel/src/channel.rs index 160b8441ff..d31d4b3c8c 100644 --- a/crates/channel/src/channel.rs +++ b/crates/channel/src/channel.rs @@ -2,19 +2,21 @@ mod channel_buffer; mod channel_chat; mod channel_store; +use client::{Client, UserStore}; +use gpui::{AppContext, ModelHandle}; +use std::sync::Arc; + pub use channel_buffer::{ChannelBuffer, ChannelBufferEvent, ACKNOWLEDGE_DEBOUNCE_INTERVAL}; pub use channel_chat::{ChannelChat, ChannelChatEvent, ChannelMessage, ChannelMessageId}; pub use channel_store::{ Channel, ChannelData, ChannelEvent, ChannelId, ChannelMembership, ChannelPath, ChannelStore, }; -use client::Client; -use std::sync::Arc; - #[cfg(test)] mod channel_store_tests; -pub fn init(client: &Arc) { +pub fn init(client: &Arc, user_store: ModelHandle, cx: &mut AppContext) { + channel_store::init(client, user_store, cx); channel_buffer::init(client); channel_chat::init(client); } diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index bd72c92c7d..8cdd11b4ec 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -2,6 +2,7 @@ mod channel_index; use crate::{channel_buffer::ChannelBuffer, channel_chat::ChannelChat}; use anyhow::{anyhow, Result}; +use channel_index::ChannelIndex; use client::{Client, Subscription, User, UserId, UserStore}; use collections::{hash_map, HashMap, HashSet}; use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt}; @@ -14,7 +15,11 @@ use serde_derive::{Deserialize, Serialize}; use std::{borrow::Cow, hash::Hash, mem, ops::Deref, sync::Arc, time::Duration}; use util::ResultExt; -use self::channel_index::ChannelIndex; +pub fn init(client: &Arc, user_store: ModelHandle, cx: &mut AppContext) { + let channel_store = + cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx)); + cx.set_global(channel_store); +} pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30); @@ -71,6 +76,10 @@ enum OpenedModelHandle { } impl ChannelStore { + pub fn global(cx: &AppContext) -> ModelHandle { + cx.global::>().clone() + } + pub fn new( client: Arc, user_store: ModelHandle, diff --git a/crates/channel/src/channel_store_tests.rs b/crates/channel/src/channel_store_tests.rs index 41acafa3a3..9303a52092 100644 --- a/crates/channel/src/channel_store_tests.rs +++ b/crates/channel/src/channel_store_tests.rs @@ -340,10 +340,10 @@ fn init_test(cx: &mut AppContext) -> ModelHandle { cx.foreground().forbid_parking(); cx.set_global(SettingsStore::test(cx)); - crate::init(&client); client::init(&client, cx); + crate::init(&client, user_store, cx); - cx.add_model(|cx| ChannelStore::new(client, user_store, cx)) + ChannelStore::global(cx) } fn update_channels( diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index e10ded7d95..2e13874125 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -44,6 +44,7 @@ pub struct TestServer { pub struct TestClient { pub username: String, pub app_state: Arc, + channel_store: ModelHandle, state: RefCell, } @@ -206,15 +207,12 @@ impl TestServer { let fs = FakeFs::new(cx.background()); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx)); let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx)); - let channel_store = - cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx)); let mut language_registry = LanguageRegistry::test(); language_registry.set_executor(cx.background()); let app_state = Arc::new(workspace::AppState { client: client.clone(), user_store: user_store.clone(), workspace_store, - channel_store: channel_store.clone(), languages: Arc::new(language_registry), fs: fs.clone(), build_window_options: |_, _, _| Default::default(), @@ -231,7 +229,7 @@ impl TestServer { workspace::init(app_state.clone(), cx); audio::init((), cx); call::init(client.clone(), user_store.clone(), cx); - channel::init(&client); + channel::init(&client, user_store, cx); }); client @@ -242,6 +240,7 @@ impl TestServer { let client = TestClient { app_state, username: name.to_string(), + channel_store: cx.read(ChannelStore::global).clone(), state: Default::default(), }; client.wait_for_current_user(cx).await; @@ -310,10 +309,9 @@ impl TestServer { admin: (&TestClient, &mut TestAppContext), members: &mut [(&TestClient, &mut TestAppContext)], ) -> u64 { - let (admin_client, admin_cx) = admin; - let channel_id = admin_client - .app_state - .channel_store + let (_, admin_cx) = admin; + let channel_id = admin_cx + .read(ChannelStore::global) .update(admin_cx, |channel_store, cx| { channel_store.create_channel(channel, parent, cx) }) @@ -321,9 +319,8 @@ impl TestServer { .unwrap(); for (member_client, member_cx) in members { - admin_client - .app_state - .channel_store + admin_cx + .read(ChannelStore::global) .update(admin_cx, |channel_store, cx| { channel_store.invite_member( channel_id, @@ -337,9 +334,8 @@ impl TestServer { admin_cx.foreground().run_until_parked(); - member_client - .app_state - .channel_store + member_cx + .read(ChannelStore::global) .update(*member_cx, |channels, _| { channels.respond_to_channel_invite(channel_id, true) }) @@ -447,7 +443,7 @@ impl TestClient { } pub fn channel_store(&self) -> &ModelHandle { - &self.app_state.channel_store + &self.channel_store } pub fn user_store(&self) -> &ModelHandle { @@ -614,8 +610,8 @@ impl TestClient { ) { let (other_client, other_cx) = user; - self.app_state - .channel_store + cx_self + .read(ChannelStore::global) .update(cx_self, |channel_store, cx| { channel_store.invite_member(channel, other_client.user_id().unwrap(), true, cx) }) @@ -624,11 +620,10 @@ impl TestClient { cx_self.foreground().run_until_parked(); - other_client - .app_state - .channel_store - .update(other_cx, |channels, _| { - channels.respond_to_channel_invite(channel, true) + other_cx + .read(ChannelStore::global) + .update(other_cx, |channel_store, _| { + channel_store.respond_to_channel_invite(channel, true) }) .await .unwrap(); diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index a955768050..b2e65eb2fa 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -73,7 +73,7 @@ impl ChannelView { ) -> Task>> { let workspace = workspace.read(cx); let project = workspace.project().to_owned(); - let channel_store = workspace.app_state().channel_store.clone(); + let channel_store = ChannelStore::global(cx); let markdown = workspace .app_state() .languages diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index b446521c5a..1a17b48f19 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -81,7 +81,7 @@ impl ChatPanel { pub fn new(workspace: &mut Workspace, cx: &mut ViewContext) -> ViewHandle { let fs = workspace.app_state().fs.clone(); let client = workspace.app_state().client.clone(); - let channel_store = workspace.app_state().channel_store.clone(); + let channel_store = ChannelStore::global(cx); let languages = workspace.app_state().languages.clone(); let input_editor = cx.add_view(|cx| { diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 951c8bf70c..9e09abbd6a 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -648,7 +648,7 @@ impl CollabPanel { channel_editing_state: None, selection: None, user_store: workspace.user_store().clone(), - channel_store: workspace.app_state().channel_store.clone(), + channel_store: ChannelStore::global(cx), project: workspace.project().clone(), subscriptions: Vec::default(), match_candidates: Vec::default(), diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index 41c86e538d..d1240a45ce 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -22,7 +22,6 @@ test-support = [ db = { path = "../db" } call = { path = "../call" } client = { path = "../client" } -channel = { path = "../channel" } collections = { path = "../collections" } context_menu = { path = "../context_menu" } drag_and_drop = { path = "../drag_and_drop" } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 6496d81349..d39c1cce76 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -12,7 +12,6 @@ mod workspace_settings; use anyhow::{anyhow, Context, Result}; use call::ActiveCall; -use channel::ChannelStore; use client::{ proto::{self, PeerId}, Client, TypedEnvelope, UserStore, @@ -450,7 +449,6 @@ pub struct AppState { pub languages: Arc, pub client: Arc, pub user_store: ModelHandle, - pub channel_store: ModelHandle, pub workspace_store: ModelHandle, pub fs: Arc, pub build_window_options: @@ -487,8 +485,6 @@ impl AppState { let http_client = util::http::FakeHttpClient::with_404_response(); let client = Client::new(http_client.clone(), cx); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); - let channel_store = - cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx)); let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx)); theme::init((), cx); @@ -500,7 +496,7 @@ impl AppState { fs, languages, user_store, - channel_store, + // channel_store, workspace_store, initialize_workspace: |_, _, _, _| Task::ready(Ok(())), build_window_options: |_, _, _| Default::default(), @@ -3527,15 +3523,12 @@ impl Workspace { let client = project.read(cx).client(); let user_store = project.read(cx).user_store(); - let channel_store = - cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx)); let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx)); let app_state = Arc::new(AppState { languages: project.read(cx).languages().clone(), workspace_store, client, user_store, - channel_store, fs: project.read(cx).fs().clone(), build_window_options: |_, _, _| Default::default(), initialize_workspace: |_, _, _, _| Task::ready(Ok(())), diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 12ae0f2ffc..6c8d321850 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -3,7 +3,6 @@ use anyhow::{anyhow, Context, Result}; use backtrace::Backtrace; -use channel::ChannelStore; use cli::{ ipc::{self, IpcSender}, CliRequest, CliResponse, IpcHandshake, FORCE_CLI_MODE_ENV_VAR_NAME, @@ -138,8 +137,6 @@ fn main() { languages::init(languages.clone(), node_runtime.clone(), cx); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx)); - let channel_store = - cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx)); let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx)); cx.set_global(client.clone()); @@ -156,7 +153,7 @@ fn main() { outline::init(cx); project_symbols::init(cx); project_panel::init(Assets, cx); - channel::init(&client); + channel::init(&client, user_store.clone(), cx); diagnostics::init(cx); search::init(cx); semantic_index::init(fs.clone(), http.clone(), languages.clone(), cx); @@ -184,7 +181,6 @@ fn main() { languages, client: client.clone(), user_store, - channel_store, fs, build_window_options, initialize_workspace, diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index ce9b7e32a3..4e9a34c269 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -2424,6 +2424,7 @@ mod tests { state.build_window_options = build_window_options; theme::init((), cx); audio::init((), cx); + channel::init(&app_state.client, app_state.user_store.clone(), cx); call::init(app_state.client.clone(), app_state.user_store.clone(), cx); workspace::init(app_state.clone(), cx); Project::init_settings(cx); From 63a230f92e04e839f52855419dfb7e3477910866 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 6 Oct 2023 16:11:45 -0600 Subject: [PATCH 13/50] Make joining on boot work --- crates/call/src/room.rs | 20 +++++- crates/client/src/client.rs | 3 +- crates/workspace/src/workspace.rs | 110 +++++++++++++++++------------- crates/zed/src/main.rs | 49 ++++++++----- crates/zed/src/open_url.rs | 1 - 5 files changed, 118 insertions(+), 65 deletions(-) diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index 2543697bc0..5419e00f02 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -18,7 +18,7 @@ use live_kit_client::{ LocalAudioTrack, LocalTrackPublication, LocalVideoTrack, RemoteAudioTrackUpdate, RemoteVideoTrackUpdate, }; -use postage::stream::Stream; +use postage::{sink::Sink, stream::Stream, watch}; use project::Project; use std::{future::Future, mem, pin::Pin, sync::Arc, time::Duration}; use util::{post_inc, ResultExt, TryFutureExt}; @@ -70,6 +70,8 @@ pub struct Room { user_store: ModelHandle, follows_by_leader_id_project_id: HashMap<(PeerId, u64), Vec>, subscriptions: Vec, + room_update_completed_tx: watch::Sender>, + room_update_completed_rx: watch::Receiver>, pending_room_update: Option>, maintain_connection: Option>>, } @@ -211,6 +213,8 @@ impl Room { Audio::play_sound(Sound::Joined, cx); + let (room_update_completed_tx, room_update_completed_rx) = watch::channel(); + Self { id, channel_id, @@ -230,6 +234,8 @@ impl Room { user_store, follows_by_leader_id_project_id: Default::default(), maintain_connection: Some(maintain_connection), + room_update_completed_tx, + room_update_completed_rx, } } @@ -856,6 +862,7 @@ impl Room { }); this.check_invariants(); + this.room_update_completed_tx.try_send(Some(())).ok(); cx.notify(); }); })); @@ -864,6 +871,17 @@ impl Room { Ok(()) } + pub fn next_room_update(&mut self) -> impl Future { + let mut done_rx = self.room_update_completed_rx.clone(); + async move { + while let Some(result) = done_rx.next().await { + if result.is_some() { + break; + } + } + } + } + fn remote_video_track_updated( &mut self, change: RemoteVideoTrackUpdate, diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 5767ac54b7..62cd60c55f 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -798,7 +798,8 @@ impl Client { } } } - _ = status_rx.next().fuse() => { + status = status_rx.next().fuse() => { + dbg!(status); return Err(anyhow!("authentication canceled")); } } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 1002ae29dc..a124917309 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -22,7 +22,7 @@ use drag_and_drop::DragAndDrop; use futures::{ channel::{mpsc, oneshot}, future::try_join_all, - select_biased, FutureExt, StreamExt, + FutureExt, StreamExt, }; use gpui::{ actions, @@ -36,9 +36,9 @@ use gpui::{ CursorStyle, ModifiersChangedEvent, MouseButton, PathPromptOptions, Platform, PromptLevel, WindowBounds, WindowOptions, }, - AnyModelHandle, AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, - ModelContext, ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle, - WeakViewHandle, WindowContext, WindowHandle, + AnyModelHandle, AnyViewHandle, AnyWeakViewHandle, AnyWindowHandle, AppContext, AsyncAppContext, + Entity, ModelContext, ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, + ViewHandle, WeakViewHandle, WindowContext, WindowHandle, }; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem}; use itertools::Itertools; @@ -4156,7 +4156,7 @@ pub async fn last_opened_workspace_paths() -> Option { async fn join_channel_internal( channel_id: u64, - app_state: Arc, + app_state: &Arc, requesting_window: Option>, active_call: &ModelHandle, cx: &mut AsyncAppContext, @@ -4196,33 +4196,24 @@ async fn join_channel_internal( let client = cx.read(|cx| active_call.read(cx).client()); - let mut timer = cx.background().timer(Duration::from_secs(5)).fuse(); let mut client_status = client.status(); + // this loop will terminate within client::CONNECTION_TIMEOUT seconds. 'outer: loop { - select_biased! { - _ = timer => { - return Err(anyhow!("connecting timed out")) - }, - status = client_status.recv().fuse() => { - let Some(status) = status else { - return Err(anyhow!("unexpected error reading connection status")) - }; + let Some(status) = client_status.recv().await else { + return Err(anyhow!("error connecting")); + }; - match status { - Status::Connecting | Status::Authenticating | Status::Reconnecting | Status::Reauthenticating => continue, - Status::Connected { .. } => break 'outer, - Status::SignedOut => { - if client.has_keychain_credentials(&cx) { - client.authenticate_and_connect(true, &cx).await?; - timer = cx.background().timer(Duration::from_secs(5)).fuse(); - } else { - return Err(anyhow!("not signed in")) - } - }, - Status::UpgradeRequired => return Err(anyhow!("zed is out of date")), - | Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => return Err(anyhow!("zed is offline")) - } + match status { + Status::Connecting + | Status::Authenticating + | Status::Reconnecting + | Status::Reauthenticating => continue, + Status::Connected { .. } => break 'outer, + Status::SignedOut => return Err(anyhow!("not signed in")), + Status::UpgradeRequired => return Err(anyhow!("zed is out of date")), + Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => { + return Err(anyhow!("zed is offline")) } } } @@ -4233,6 +4224,8 @@ async fn join_channel_internal( }) .await?; + room.update(cx, |room, cx| room.next_room_update()).await; + let task = room.update(cx, |room, cx| { if let Some((project, host)) = room.most_active_project() { return Some(join_remote_project(project, host, app_state.clone(), cx)); @@ -4255,10 +4248,10 @@ pub fn join_channel( cx: &mut AppContext, ) -> Task> { let active_call = ActiveCall::global(cx); - cx.spawn(|mut cx| { + cx.spawn(|mut cx| async move { let result = join_channel_internal( channel_id, - app_state, + &app_state, requesting_window, &active_call, &mut cx, @@ -4266,7 +4259,7 @@ pub fn join_channel( .await; // join channel succeeded, and opened a window - if Some(true) = result { + if matches!(result, Ok(true)) { return anyhow::Ok(()); } @@ -4275,28 +4268,53 @@ pub fn join_channel( } // find an existing workspace to focus and show call controls - for window in cx.windows() { - let found = window.update(&mut cx, |cx| { - let is_workspace = cx.root_view().clone().downcast::().is_some(); - if is_workspace { - cx.activate_window(); - } - is_workspace - }); + let mut active_window = activate_any_workspace_window(&mut cx); + if active_window.is_none() { + // no open workspaces, make one to show the error in (blergh) + cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), requesting_window, cx)) + .await; + } - if found.unwrap_or(false) { - return anyhow::Ok(()); + active_window = activate_any_workspace_window(&mut cx); + if active_window.is_none() { + return result.map(|_| ()); // unreachable!() assuming new_local always opens a window + } + + if let Err(err) = result { + let prompt = active_window.unwrap().prompt( + PromptLevel::Critical, + &format!("Failed to join channel: {}", err), + &["Ok"], + &mut cx, + ); + if let Some(mut prompt) = prompt { + prompt.next().await; + } else { + return Err(err); } } - // no open workspaces - cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), requesting_window, cx)) - .await; - - return connected.map(|_| ()); + // return ok, we showed the error to the user. + return anyhow::Ok(()); }) } +pub fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option { + for window in cx.windows() { + let found = window.update(cx, |cx| { + let is_workspace = cx.root_view().clone().downcast::().is_some(); + if is_workspace { + cx.activate_window(); + } + is_workspace + }); + if found == Some(true) { + return Some(window); + } + } + None +} + #[allow(clippy::type_complexity)] pub fn open_paths( abs_paths: &[PathBuf], diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 861121a1cf..52aaf639ea 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -8,7 +8,9 @@ use cli::{ ipc::{self, IpcSender}, CliRequest, CliResponse, IpcHandshake, FORCE_CLI_MODE_ENV_VAR_NAME, }; -use client::{self, TelemetrySettings, UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN}; +use client::{ + self, Client, TelemetrySettings, UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN, +}; use db::kvp::KEY_VALUE_STORE; use editor::{scroll::autoscroll::Autoscroll, Editor}; use futures::{ @@ -33,7 +35,7 @@ use std::{ fs::OpenOptions, io::{IsTerminal, Write as _}, panic, - path::{Path, PathBuf}, + path::Path, sync::{ atomic::{AtomicU32, Ordering}, Arc, Weak, @@ -222,6 +224,8 @@ fn main() { } } + let mut triggered_authentication = false; + match open_rx.try_next() { Ok(Some(OpenRequest::Paths { paths })) => { cx.update(|cx| workspace::open_paths(&paths, &app_state, None, cx)) @@ -231,9 +235,18 @@ fn main() { cx.spawn(|cx| handle_cli_connection(connection, app_state.clone(), cx)) .detach(); } - Ok(Some(OpenRequest::JoinChannel { channel_id })) => cx - .update(|cx| workspace::join_channel(channel_id, app_state.clone(), None, cx)) - .detach_and_log_err(cx), + Ok(Some(OpenRequest::JoinChannel { channel_id })) => { + triggered_authentication = true; + let app_state = app_state.clone(); + let client = client.clone(); + cx.spawn(|mut cx| async move { + // ignore errors here, we'll show a generic "not signed in" + let _ = authenticate(client, &cx).await; + cx.update(|cx| workspace::join_channel(channel_id, app_state, None, cx)) + .await + }) + .detach_and_log_err(cx) + } Ok(None) | Err(_) => cx .spawn({ let app_state = app_state.clone(); @@ -266,20 +279,24 @@ fn main() { }) .detach(); - cx.spawn(|cx| async move { - if stdout_is_a_pty() { - if client::IMPERSONATE_LOGIN.is_some() { - client.authenticate_and_connect(false, &cx).await?; - } - } else if client.has_keychain_credentials(&cx) { - client.authenticate_and_connect(true, &cx).await?; - } - Ok::<_, anyhow::Error>(()) - }) - .detach_and_log_err(cx); + if !triggered_authentication { + cx.spawn(|cx| async move { authenticate(client, &cx).await }) + .detach_and_log_err(cx); + } }); } +async fn authenticate(client: Arc, cx: &AsyncAppContext) -> Result<()> { + if stdout_is_a_pty() { + if client::IMPERSONATE_LOGIN.is_some() { + client.authenticate_and_connect(false, &cx).await?; + } + } else if client.has_keychain_credentials(&cx) { + client.authenticate_and_connect(true, &cx).await?; + } + Ok::<_, anyhow::Error>(()) +} + async fn installation_id() -> Result { let legacy_key_name = "device_id"; diff --git a/crates/zed/src/open_url.rs b/crates/zed/src/open_url.rs index 6f90953de2..3e4902b978 100644 --- a/crates/zed/src/open_url.rs +++ b/crates/zed/src/open_url.rs @@ -42,7 +42,6 @@ impl OpenListener { pub fn open_urls(&self, urls: Vec) { self.triggered.store(true, Ordering::Release); - dbg!(&urls); let request = if let Some(server_name) = urls.first().and_then(|url| url.strip_prefix("zed-cli://")) { From f6bc229d1d4f4d46139ec268241fb69d925fceed Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 6 Oct 2023 16:48:29 -0600 Subject: [PATCH 14/50] More progress and some debug logs to remove --- assets/settings/default.json | 2 +- crates/util/src/channel.rs | 6 +++--- crates/workspace/src/workspace.rs | 21 +++++++++++++++++++-- crates/zed/src/main.rs | 10 ++++++++++ crates/zed/src/open_url.rs | 13 +++++++++++++ 5 files changed, 46 insertions(+), 6 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 8fb73a2ecb..cc724657c0 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -76,7 +76,7 @@ // Settings related to calls in Zed "calls": { // Join calls with the microphone muted by default - "mute_on_join": true + "mute_on_join": false }, // Scrollbar related settings "scrollbar": { diff --git a/crates/util/src/channel.rs b/crates/util/src/channel.rs index 2364dcaad4..47c6a570a1 100644 --- a/crates/util/src/channel.rs +++ b/crates/util/src/channel.rs @@ -44,9 +44,9 @@ impl ReleaseChannel { pub fn url_scheme(&self) -> &'static str { match self { - ReleaseChannel::Dev => "zed-dev:/", - ReleaseChannel::Preview => "zed-preview:/", - ReleaseChannel::Stable => "zed:/", + ReleaseChannel::Dev => "zed-dev://", + ReleaseChannel::Preview => "zed-preview://", + ReleaseChannel::Stable => "zed://", } } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index a124917309..1644f86b61 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -47,6 +47,7 @@ use std::{ any::TypeId, borrow::Cow, cmp, env, + fs::OpenOptions, future::Future, path::{Path, PathBuf}, rc::Rc, @@ -4161,6 +4162,7 @@ async fn join_channel_internal( active_call: &ModelHandle, cx: &mut AsyncAppContext, ) -> Result { + dbg!("join channel internal"); let should_prompt = active_call.read_with(cx, |active_call, cx| { let Some(room) = active_call.room().map(|room| room.read(cx)) else { return false; @@ -4193,6 +4195,7 @@ async fn join_channel_internal( return Ok(false); // unreachable!() hopefully } } + dbg!("asdajdkjkasd"); let client = cx.read(|cx| active_call.read(cx).client()); @@ -4218,13 +4221,17 @@ async fn join_channel_internal( } } + dbg!("past here"); + let room = active_call .update(cx, |active_call, cx| { active_call.join_channel(channel_id, cx) }) .await?; - room.update(cx, |room, cx| room.next_room_update()).await; + room.update(cx, |room, _| room.next_room_update()).await; + + dbg!("wow"); let task = room.update(cx, |room, cx| { if let Some((project, host)) = room.most_active_project() { @@ -4237,7 +4244,16 @@ async fn join_channel_internal( task.await?; return anyhow::Ok(true); } - + use std::io::Write; + writeln!( + OpenOptions::new() + .write(true) + .append(true) + .open("/Users/conrad/dbg") + .unwrap(), + "no jokes" + ) + .unwrap(); anyhow::Ok(false) } @@ -4257,6 +4273,7 @@ pub fn join_channel( &mut cx, ) .await; + dbg!("joined!"); // join channel succeeded, and opened a window if matches!(result, Ok(true)) { diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 52aaf639ea..f25a1e14be 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -66,6 +66,15 @@ use crate::open_url::{OpenListener, OpenRequest}; mod open_url; fn main() { + writeln!( + OpenOptions::new() + .write(true) + .append(true) + .open("/Users/conrad/dbg") + .unwrap(), + "HELLO" + ) + .unwrap(); let http = http::client(); init_paths(); init_logger(); @@ -270,6 +279,7 @@ fn main() { } OpenRequest::JoinChannel { channel_id } => cx .update(|cx| { + dbg!("joining channel"); workspace::join_channel(channel_id, app_state.clone(), None, cx) }) .detach(), diff --git a/crates/zed/src/open_url.rs b/crates/zed/src/open_url.rs index 3e4902b978..8cad217a8c 100644 --- a/crates/zed/src/open_url.rs +++ b/crates/zed/src/open_url.rs @@ -3,6 +3,8 @@ use cli::{ipc::IpcSender, CliRequest, CliResponse}; use futures::channel::mpsc; use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender}; use std::ffi::OsStr; +use std::fs::OpenOptions; +use std::io::Write; use std::os::unix::prelude::OsStrExt; use std::sync::atomic::Ordering; use std::{path::PathBuf, sync::atomic::AtomicBool}; @@ -41,6 +43,16 @@ impl OpenListener { } pub fn open_urls(&self, urls: Vec) { + writeln!( + OpenOptions::new() + .write(true) + .append(true) + .open("/Users/conrad/dbg") + .unwrap(), + "{:?}", + &urls, + ) + .unwrap(); self.triggered.store(true, Ordering::Release); let request = if let Some(server_name) = urls.first().and_then(|url| url.strip_prefix("zed-cli://")) @@ -79,6 +91,7 @@ impl OpenListener { } } } + log::error!("invalid zed url: {}", request_path); None } From 6de69de868fcee8214edca76b8a09d2210885646 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Fri, 6 Oct 2023 16:04:45 -0700 Subject: [PATCH 15/50] Remove change to linker args --- .cargo/config.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index e22bdb0f2c..9da6b3be08 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -3,4 +3,4 @@ xtask = "run --package xtask --" [build] # v0 mangling scheme provides more detailed backtraces around closures -rustflags = ["-C", "symbol-mangling-version=v0", "-C", "link-arg=-fuse-ld=/opt/homebrew/Cellar/llvm/16.0.6/bin/ld64.lld"] +rustflags = ["-C", "symbol-mangling-version=v0"] From 66120fb97a3dca9292a3c978152246f675198adb Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 6 Oct 2023 22:24:41 -0600 Subject: [PATCH 16/50] Try universal link entitlement too --- crates/zed/resources/zed.entitlements | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/zed/resources/zed.entitlements b/crates/zed/resources/zed.entitlements index dca0ff6766..9f5414243d 100644 --- a/crates/zed/resources/zed.entitlements +++ b/crates/zed/resources/zed.entitlements @@ -2,6 +2,8 @@ + com.apple.developer.associated-domains + applinks:cirw.in com.apple.security.automation.apple-events com.apple.security.cs.allow-jit From 34b75379485f82f531cba5325997a5e42600a379 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 6 Oct 2023 23:15:37 -0600 Subject: [PATCH 17/50] Add universal links support to mac platform --- crates/gpui/src/platform/mac/platform.rs | 26 ++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index 656f2e4475..47ad72eeb2 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -140,6 +140,10 @@ unsafe fn build_classes() { sel!(application:openURLs:), open_urls as extern "C" fn(&mut Object, Sel, id, id), ); + decl.add_method( + sel!(application:continueUserActivity:restorationHandler:), + continue_user_activity as extern "C" fn(&mut Object, Sel, id, id, id), + ); decl.register() } } @@ -1009,6 +1013,28 @@ extern "C" fn open_urls(this: &mut Object, _: Sel, _: id, urls: id) { } } +extern "C" fn continue_user_activity(this: &mut Object, _: Sel, _: id, user_activity: id, _: id) { + dbg!("yay!"); + let url = unsafe { + let url: id = msg_send!(user_activity, webpageURL); + if url == nil { + log::error!("got unexpected user activity"); + None + } else { + Some( + CStr::from_ptr(url.absoluteString().UTF8String()) + .to_string_lossy() + .to_string(), + ) + } + }; + dbg!(&url); + let platform = unsafe { get_foreground_platform(this) }; + if let Some(callback) = platform.0.borrow_mut().open_urls.as_mut() { + callback(url.into_iter().collect()); + } +} + extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) { unsafe { let platform = get_foreground_platform(this); From 4b2c24dd8c9836deb973935a46db70cdff3c74b6 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Sun, 8 Oct 2023 20:07:59 -0400 Subject: [PATCH 18/50] Add enable vim mode checkbox to welcome screen --- Cargo.lock | 1 + crates/vim/src/vim.rs | 2 +- crates/welcome/Cargo.toml | 1 + crates/welcome/src/welcome.rs | 25 ++++++++++++++++++++++++- 4 files changed, 27 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c57548057d..3c7affc76f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9665,6 +9665,7 @@ dependencies = [ "theme", "theme_selector", "util", + "vim", "workspace", ] diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index d27be2c54b..aad97c558e 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -33,7 +33,7 @@ use workspace::{self, Workspace}; use crate::state::ReplayableAction; -struct VimModeSetting(bool); +pub struct VimModeSetting(pub bool); #[derive(Clone, Deserialize, PartialEq)] pub struct SwitchMode(pub Mode); diff --git a/crates/welcome/Cargo.toml b/crates/welcome/Cargo.toml index ea01f822a7..c7ad62f155 100644 --- a/crates/welcome/Cargo.toml +++ b/crates/welcome/Cargo.toml @@ -25,6 +25,7 @@ theme_selector = { path = "../theme_selector" } util = { path = "../util" } picker = { path = "../picker" } workspace = { path = "../workspace" } +vim = { path = "../vim" } anyhow.workspace = true log.workspace = true diff --git a/crates/welcome/src/welcome.rs b/crates/welcome/src/welcome.rs index 4d8df53a1b..a5d95429bd 100644 --- a/crates/welcome/src/welcome.rs +++ b/crates/welcome/src/welcome.rs @@ -10,6 +10,7 @@ use gpui::{ }; use settings::{update_settings_file, SettingsStore}; use std::{borrow::Cow, sync::Arc}; +use vim::VimModeSetting; use workspace::{ dock::DockPosition, item::Item, open_new, AppState, PaneBackdrop, Welcome, Workspace, WorkspaceId, @@ -65,6 +66,7 @@ impl View for WelcomePage { let width = theme.welcome.page_width; let telemetry_settings = *settings::get::(cx); + let vim_mode_setting = settings::get::(cx).0; enum Metrics {} enum Diagnostics {} @@ -144,6 +146,27 @@ impl View for WelcomePage { ) .with_child( Flex::column() + .with_child( + theme::ui::checkbox::( + "Enable vim mode", + &theme.welcome.checkbox, + vim_mode_setting, + 0, + cx, + |this, checked, cx| { + if let Some(workspace) = this.workspace.upgrade(cx) { + let fs = workspace.read(cx).app_state().fs.clone(); + update_settings_file::( + fs, + cx, + move |setting| *setting = Some(checked), + ) + } + }, + ) + .contained() + .with_style(theme.welcome.checkbox_container), + ) .with_child( theme::ui::checkbox_with_label::( Flex::column() @@ -186,7 +209,7 @@ impl View for WelcomePage { "Send crash reports", &theme.welcome.checkbox, telemetry_settings.diagnostics, - 0, + 1, cx, |this, checked, cx| { if let Some(workspace) = this.workspace.upgrade(cx) { From 38d53a6fe24b49ccc198a37bc4c4e6b755a6286f Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 9 Oct 2023 17:09:58 +0300 Subject: [PATCH 19/50] Bump curl-sys to fix Sonoma issues with it See https://github.com/alexcrichton/curl-rust/issues/524 --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3c7affc76f..7c31fa6dd0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2079,9 +2079,9 @@ dependencies = [ [[package]] name = "curl-sys" -version = "0.4.66+curl-8.3.0" +version = "0.4.67+curl-8.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70c44a72e830f0e40ad90dda8a6ab6ed6314d39776599a58a2e5e37fbc6db5b9" +checksum = "3cc35d066510b197a0f72de863736641539957628c8a42e70e27c66849e77c34" dependencies = [ "cc", "libc", From 5dbda7023517b60720d250e4109c8bb08f7c6da7 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 9 Oct 2023 08:59:25 -0600 Subject: [PATCH 20/50] Fix ./script/bundle to allow passing key --- crates/client/src/client.rs | 1 - crates/gpui/src/platform/mac/platform.rs | 2 -- crates/workspace/src/workspace.rs | 7 ------- crates/zed/src/main.rs | 1 - script/bundle | 10 ++++++++-- 5 files changed, 8 insertions(+), 13 deletions(-) diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 62cd60c55f..22e8bc06d4 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -799,7 +799,6 @@ impl Client { } } status = status_rx.next().fuse() => { - dbg!(status); return Err(anyhow!("authentication canceled")); } } diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index 47ad72eeb2..24ad774759 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -1014,7 +1014,6 @@ extern "C" fn open_urls(this: &mut Object, _: Sel, _: id, urls: id) { } extern "C" fn continue_user_activity(this: &mut Object, _: Sel, _: id, user_activity: id, _: id) { - dbg!("yay!"); let url = unsafe { let url: id = msg_send!(user_activity, webpageURL); if url == nil { @@ -1028,7 +1027,6 @@ extern "C" fn continue_user_activity(this: &mut Object, _: Sel, _: id, user_acti ) } }; - dbg!(&url); let platform = unsafe { get_foreground_platform(this) }; if let Some(callback) = platform.0.borrow_mut().open_urls.as_mut() { callback(url.into_iter().collect()); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 1644f86b61..7773abc19c 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -4162,7 +4162,6 @@ async fn join_channel_internal( active_call: &ModelHandle, cx: &mut AsyncAppContext, ) -> Result { - dbg!("join channel internal"); let should_prompt = active_call.read_with(cx, |active_call, cx| { let Some(room) = active_call.room().map(|room| room.read(cx)) else { return false; @@ -4195,7 +4194,6 @@ async fn join_channel_internal( return Ok(false); // unreachable!() hopefully } } - dbg!("asdajdkjkasd"); let client = cx.read(|cx| active_call.read(cx).client()); @@ -4221,8 +4219,6 @@ async fn join_channel_internal( } } - dbg!("past here"); - let room = active_call .update(cx, |active_call, cx| { active_call.join_channel(channel_id, cx) @@ -4231,8 +4227,6 @@ async fn join_channel_internal( room.update(cx, |room, _| room.next_room_update()).await; - dbg!("wow"); - let task = room.update(cx, |room, cx| { if let Some((project, host)) = room.most_active_project() { return Some(join_remote_project(project, host, app_state.clone(), cx)); @@ -4273,7 +4267,6 @@ pub fn join_channel( &mut cx, ) .await; - dbg!("joined!"); // join channel succeeded, and opened a window if matches!(result, Ok(true)) { diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index f25a1e14be..004e5769d5 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -279,7 +279,6 @@ fn main() { } OpenRequest::JoinChannel { channel_id } => cx .update(|cx| { - dbg!("joining channel"); workspace::join_channel(channel_id, app_state.clone(), None, cx) }) .detach(), diff --git a/script/bundle b/script/bundle index 94741d290f..a1d0b305c8 100755 --- a/script/bundle +++ b/script/bundle @@ -128,7 +128,11 @@ fi echo "Copying WebRTC.framework into the frameworks folder" mkdir "${app_path}/Contents/Frameworks" -cp -R target/${local_target_triple}/${target_dir}/WebRTC.framework "${app_path}/Contents/Frameworks/" +if [ "$local_arch" = false ]; then + cp -R target/${local_target_triple}/${target_dir}/WebRTC.framework "${app_path}/Contents/Frameworks/" +else + cp -R target/${target_dir}/WebRTC.framework "${app_path}/Contents/Frameworks/" +fi if [[ -n $MACOS_CERTIFICATE && -n $MACOS_CERTIFICATE_PASSWORD && -n $APPLE_NOTARIZATION_USERNAME && -n $APPLE_NOTARIZATION_PASSWORD ]]; then echo "Signing bundle with Apple-issued certificate" @@ -144,7 +148,9 @@ if [[ -n $MACOS_CERTIFICATE && -n $MACOS_CERTIFICATE_PASSWORD && -n $APPLE_NOTAR else echo "One or more of the following variables are missing: MACOS_CERTIFICATE, MACOS_CERTIFICATE_PASSWORD, APPLE_NOTARIZATION_USERNAME, APPLE_NOTARIZATION_PASSWORD" echo "Performing an ad-hoc signature, but this bundle should not be distributed" - codesign --force --deep --entitlements crates/zed/resources/zed.entitlements --sign - "${app_path}" -v + echo "If you see 'The application cannot be opened for an unexpected reason,' you likely don't have the necessary entitlements to run the application in your signing keychain" + echo "You will need to download a new signing key from developer.apple.com, add it to keychain, and export MACOS_SIGNING_KEY=" + codesign --force --deep --entitlements crates/zed/resources/zed.entitlements --sign ${MACOS_SIGNING_KEY:- -} "${app_path}" -v fi if [[ "$target_dir" = "debug" && "$local_only" = false ]]; then From 8f4d81903c6b0322c7026c04b0df2830fbc02f10 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 9 Oct 2023 09:28:49 -0600 Subject: [PATCH 21/50] Add "Copy Link" to channel right click menu --- crates/channel/src/channel_store.rs | 21 +++++++++++++++++++++ crates/collab_ui/src/collab_panel.rs | 27 +++++++++++++++++++++++++-- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index bd72c92c7d..cd2153ad5c 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -4,6 +4,7 @@ use crate::{channel_buffer::ChannelBuffer, channel_chat::ChannelChat}; use anyhow::{anyhow, Result}; use client::{Client, Subscription, User, UserId, UserStore}; use collections::{hash_map, HashMap, HashSet}; +use db::RELEASE_CHANNEL; use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt}; use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle}; use rpc::{ @@ -47,6 +48,26 @@ pub struct Channel { pub unseen_message_id: Option, } +impl Channel { + pub fn link(&self) -> String { + RELEASE_CHANNEL.link_prefix().to_owned() + + "channel/" + + &self.slug() + + "-" + + &self.id.to_string() + } + + pub fn slug(&self) -> String { + let slug: String = self + .name + .chars() + .map(|c| if c.is_alphanumeric() { c } else { '-' }) + .collect(); + + slug.trim_matches(|c| c == '-').to_string() + } +} + #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)] pub struct ChannelPath(Arc<[ChannelId]>); diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 3d66e8450a..9ec05e07c8 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -34,8 +34,8 @@ use gpui::{ }, impl_actions, platform::{CursorStyle, MouseButton, PromptLevel}, - serde_json, AnyElement, AppContext, AsyncAppContext, Element, Entity, FontCache, ModelHandle, - Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, + serde_json, AnyElement, AppContext, AsyncAppContext, ClipboardItem, Element, Entity, FontCache, + ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, }; use menu::{Confirm, SelectNext, SelectPrev}; use project::{Fs, Project}; @@ -100,6 +100,11 @@ pub struct JoinChannelChat { pub channel_id: u64, } +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct CopyChannelLink { + pub channel_id: u64, +} + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] struct StartMoveChannelFor { channel_id: ChannelId, @@ -157,6 +162,7 @@ impl_actions!( OpenChannelNotes, JoinChannelCall, JoinChannelChat, + CopyChannelLink, LinkChannel, StartMoveChannelFor, StartLinkChannelFor, @@ -205,6 +211,7 @@ pub fn init(cx: &mut AppContext) { cx.add_action(CollabPanel::expand_selected_channel); cx.add_action(CollabPanel::open_channel_notes); cx.add_action(CollabPanel::join_channel_chat); + cx.add_action(CollabPanel::copy_channel_link); cx.add_action( |panel: &mut CollabPanel, action: &ToggleSelectedIx, cx: &mut ViewContext| { @@ -2571,6 +2578,13 @@ impl CollabPanel { }, )); + items.push(ContextMenuItem::action( + "Copy Channel Link", + CopyChannelLink { + channel_id: path.channel_id(), + }, + )); + if self.channel_store.read(cx).is_user_admin(path.channel_id()) { let parent_id = path.parent_id(); @@ -3219,6 +3233,15 @@ impl CollabPanel { }); } } + + fn copy_channel_link(&mut self, action: &CopyChannelLink, cx: &mut ViewContext) { + let channel_store = self.channel_store.read(cx); + let Some(channel) = channel_store.channel_for_id(action.channel_id) else { + return; + }; + let item = ClipboardItem::new(channel.link()); + cx.write_to_clipboard(item) + } } fn render_tree_branch( From 6084486dcdb98685cdc589cff385c64a5a63cb7e Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 9 Oct 2023 09:34:18 -0600 Subject: [PATCH 22/50] Code quality --- crates/client/src/client.rs | 2 +- crates/collab_ui/src/collab_panel.rs | 27 ++++++++++++--------------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 22e8bc06d4..5767ac54b7 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -798,7 +798,7 @@ impl Client { } } } - status = status_rx.next().fuse() => { + _ = status_rx.next().fuse() => { return Err(anyhow!("authentication canceled")); } } diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 9ec05e07c8..8a8fde88ee 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -1976,21 +1976,18 @@ impl CollabPanel { let style = collab_theme.channel_name.inactive_state(); Flex::row() .with_child( - Label::new( - channel.name.clone().to_owned() + channel_id.to_string().as_str(), - style.text.clone(), - ) - .contained() - .with_style(style.container) - .aligned() - .left() - .with_tooltip::( - ix, - "Join channel", - None, - theme.tooltip.clone(), - cx, - ), + Label::new(channel.name.clone(), style.text.clone()) + .contained() + .with_style(style.container) + .aligned() + .left() + .with_tooltip::( + ix, + "Join channel", + None, + theme.tooltip.clone(), + cx, + ), ) .with_children({ let participants = From abfb4490d590f33ffefce2d945b540b60cad4d99 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 9 Oct 2023 12:05:26 -0600 Subject: [PATCH 23/50] Focus the currently active project if there is one (also consider your own projects in "most_active_projects") --- crates/call/src/room.rs | 9 ++++++++- crates/workspace/src/workspace.rs | 31 ++++++++++++++++++++++++++----- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index 5419e00f02..b7aeee90e2 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -605,7 +605,7 @@ impl Room { } /// Returns the most 'active' projects, defined as most people in the project - pub fn most_active_project(&self) -> Option<(u64, u64)> { + pub fn most_active_project(&self, cx: &AppContext) -> Option<(u64, u64)> { let mut projects = HashMap::default(); let mut hosts = HashMap::default(); @@ -622,6 +622,13 @@ impl Room { } } + if let Some(user) = self.user_store.read(cx).current_user() { + for project in &self.local_participant.projects { + *projects.entry(project.id).or_insert(0) += 1; + hosts.insert(project.id, user.id); + } + } + let mut pairs: Vec<(u64, usize)> = projects.into_iter().collect(); pairs.sort_by_key(|(_, count)| *count as i32); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 7773abc19c..e690757af7 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -4162,16 +4162,37 @@ async fn join_channel_internal( active_call: &ModelHandle, cx: &mut AsyncAppContext, ) -> Result { - let should_prompt = active_call.read_with(cx, |active_call, cx| { + let (should_prompt, open_room) = active_call.read_with(cx, |active_call, cx| { let Some(room) = active_call.room().map(|room| room.read(cx)) else { - return false; + return (false, None); }; - room.is_sharing_project() + let already_in_channel = room.channel_id() == Some(channel_id); + let should_prompt = room.is_sharing_project() && room.remote_participants().len() > 0 - && room.channel_id() != Some(channel_id) + && !already_in_channel; + let open_room = if already_in_channel { + active_call.room().cloned() + } else { + None + }; + (should_prompt, open_room) }); + if let Some(room) = open_room { + let task = room.update(cx, |room, cx| { + if let Some((project, host)) = room.most_active_project(cx) { + return Some(join_remote_project(project, host, app_state.clone(), cx)); + } + + None + }); + if let Some(task) = task { + task.await?; + } + return anyhow::Ok(true); + } + if should_prompt { if let Some(workspace) = requesting_window { if let Some(window) = workspace.update(cx, |cx| cx.window()) { @@ -4228,7 +4249,7 @@ async fn join_channel_internal( room.update(cx, |room, _| room.next_room_update()).await; let task = room.update(cx, |room, cx| { - if let Some((project, host)) = room.most_active_project() { + if let Some((project, host)) = room.most_active_project(cx) { return Some(join_remote_project(project, host, app_state.clone(), cx)); } From 162cb19cff1efaaa127dd7619a5351facc545c1c Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 9 Oct 2023 12:59:18 -0600 Subject: [PATCH 24/50] Only allow one release channel in a call --- .../20221109000000_test_schema.sql | 1 + ...009181554_add_release_channel_to_rooms.sql | 1 + crates/collab/src/db/queries/rooms.rs | 32 +++++-- crates/collab/src/db/tables/room.rs | 1 + crates/collab/src/db/tests.rs | 2 + crates/collab/src/db/tests/channel_tests.rs | 20 +++- crates/collab/src/db/tests/db_tests.rs | 92 ++++++++++++++++++- crates/collab/src/rpc.rs | 22 ++++- 8 files changed, 152 insertions(+), 19 deletions(-) create mode 100644 crates/collab/migrations/20231009181554_add_release_channel_to_rooms.sql diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 2d963ff15f..a9b8d9709d 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -37,6 +37,7 @@ CREATE INDEX "index_contacts_user_id_b" ON "contacts" ("user_id_b"); CREATE TABLE "rooms" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "live_kit_room" VARCHAR NOT NULL, + "release_channel" VARCHAR, "channel_id" INTEGER REFERENCES channels (id) ON DELETE CASCADE ); diff --git a/crates/collab/migrations/20231009181554_add_release_channel_to_rooms.sql b/crates/collab/migrations/20231009181554_add_release_channel_to_rooms.sql new file mode 100644 index 0000000000..95d3c400fc --- /dev/null +++ b/crates/collab/migrations/20231009181554_add_release_channel_to_rooms.sql @@ -0,0 +1 @@ +ALTER TABLE rooms ADD COLUMN release_channel TEXT; diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs index b103ae1c73..6589f23791 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/crates/collab/src/db/queries/rooms.rs @@ -107,10 +107,12 @@ impl Database { user_id: UserId, connection: ConnectionId, live_kit_room: &str, + release_channel: &str, ) -> Result { self.transaction(|tx| async move { let room = room::ActiveModel { live_kit_room: ActiveValue::set(live_kit_room.into()), + release_channel: ActiveValue::set(Some(release_channel.to_string())), ..Default::default() } .insert(&*tx) @@ -270,20 +272,31 @@ impl Database { room_id: RoomId, user_id: UserId, connection: ConnectionId, + collab_release_channel: &str, ) -> Result> { self.room_transaction(room_id, |tx| async move { #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] - enum QueryChannelId { + enum QueryChannelIdAndReleaseChannel { ChannelId, + ReleaseChannel, + } + + let (channel_id, release_channel): (Option, Option) = + room::Entity::find() + .select_only() + .column(room::Column::ChannelId) + .column(room::Column::ReleaseChannel) + .filter(room::Column::Id.eq(room_id)) + .into_values::<_, QueryChannelIdAndReleaseChannel>() + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such room"))?; + + if let Some(release_channel) = release_channel { + if &release_channel != collab_release_channel { + Err(anyhow!("must join using the {} release", release_channel))?; + } } - let channel_id: Option = room::Entity::find() - .select_only() - .column(room::Column::ChannelId) - .filter(room::Column::Id.eq(room_id)) - .into_values::<_, QueryChannelId>() - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("no such room"))?; #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] enum QueryParticipantIndices { @@ -300,6 +313,7 @@ impl Database { .into_values::<_, QueryParticipantIndices>() .all(&*tx) .await?; + let mut participant_index = 0; while existing_participant_indices.contains(&participant_index) { participant_index += 1; diff --git a/crates/collab/src/db/tables/room.rs b/crates/collab/src/db/tables/room.rs index f72f7000a7..7f31edcdbd 100644 --- a/crates/collab/src/db/tables/room.rs +++ b/crates/collab/src/db/tables/room.rs @@ -8,6 +8,7 @@ pub struct Model { pub id: RoomId, pub live_kit_room: String, pub channel_id: Option, + pub release_channel: Option, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/crates/collab/src/db/tests.rs b/crates/collab/src/db/tests.rs index 75584ff90b..6a91fd6ffe 100644 --- a/crates/collab/src/db/tests.rs +++ b/crates/collab/src/db/tests.rs @@ -12,6 +12,8 @@ use sea_orm::ConnectionTrait; use sqlx::migrate::MigrateDatabase; use std::sync::Arc; +const TEST_RELEASE_CHANNEL: &'static str = "test"; + pub struct TestDb { pub db: Option>, pub connection: Option, diff --git a/crates/collab/src/db/tests/channel_tests.rs b/crates/collab/src/db/tests/channel_tests.rs index 429852d128..2631e0d191 100644 --- a/crates/collab/src/db/tests/channel_tests.rs +++ b/crates/collab/src/db/tests/channel_tests.rs @@ -5,7 +5,11 @@ use rpc::{ }; use crate::{ - db::{queries::channels::ChannelGraph, tests::graph, ChannelId, Database, NewUserParams}, + db::{ + queries::channels::ChannelGraph, + tests::{graph, TEST_RELEASE_CHANNEL}, + ChannelId, Database, NewUserParams, + }, test_both_dbs, }; use std::sync::Arc; @@ -206,7 +210,12 @@ async fn test_joining_channels(db: &Arc) { // can join a room with membership to its channel let joined_room = db - .join_room(room_1, user_1, ConnectionId { owner_id, id: 1 }) + .join_room( + room_1, + user_1, + ConnectionId { owner_id, id: 1 }, + TEST_RELEASE_CHANNEL, + ) .await .unwrap(); assert_eq!(joined_room.room.participants.len(), 1); @@ -214,7 +223,12 @@ async fn test_joining_channels(db: &Arc) { drop(joined_room); // cannot join a room without membership to its channel assert!(db - .join_room(room_1, user_2, ConnectionId { owner_id, id: 1 }) + .join_room( + room_1, + user_2, + ConnectionId { owner_id, id: 1 }, + TEST_RELEASE_CHANNEL + ) .await .is_err()); } diff --git a/crates/collab/src/db/tests/db_tests.rs b/crates/collab/src/db/tests/db_tests.rs index 9a617166fe..1520e081c0 100644 --- a/crates/collab/src/db/tests/db_tests.rs +++ b/crates/collab/src/db/tests/db_tests.rs @@ -479,7 +479,7 @@ async fn test_project_count(db: &Arc) { .unwrap(); let room_id = RoomId::from_proto( - db.create_room(user1.user_id, ConnectionId { owner_id, id: 0 }, "") + db.create_room(user1.user_id, ConnectionId { owner_id, id: 0 }, "", "dev") .await .unwrap() .id, @@ -493,9 +493,14 @@ async fn test_project_count(db: &Arc) { ) .await .unwrap(); - db.join_room(room_id, user2.user_id, ConnectionId { owner_id, id: 1 }) - .await - .unwrap(); + db.join_room( + room_id, + user2.user_id, + ConnectionId { owner_id, id: 1 }, + "dev", + ) + .await + .unwrap(); assert_eq!(db.project_count_excluding_admins().await.unwrap(), 0); db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[]) @@ -575,6 +580,85 @@ async fn test_fuzzy_search_users() { } } +test_both_dbs!( + test_non_matching_release_channels, + test_non_matching_release_channels_postgres, + test_non_matching_release_channels_sqlite +); + +async fn test_non_matching_release_channels(db: &Arc) { + let owner_id = db.create_server("test").await.unwrap().0 as u32; + + let user1 = db + .create_user( + &format!("admin@example.com"), + true, + NewUserParams { + github_login: "admin".into(), + github_user_id: 0, + invite_count: 0, + }, + ) + .await + .unwrap(); + let user2 = db + .create_user( + &format!("user@example.com"), + false, + NewUserParams { + github_login: "user".into(), + github_user_id: 1, + invite_count: 0, + }, + ) + .await + .unwrap(); + + let room = db + .create_room( + user1.user_id, + ConnectionId { owner_id, id: 0 }, + "", + "stable", + ) + .await + .unwrap(); + + db.call( + RoomId::from_proto(room.id), + user1.user_id, + ConnectionId { owner_id, id: 0 }, + user2.user_id, + None, + ) + .await + .unwrap(); + + // User attempts to join from preview + let result = db + .join_room( + RoomId::from_proto(room.id), + user2.user_id, + ConnectionId { owner_id, id: 1 }, + "preview", + ) + .await; + + assert!(result.is_err()); + + // User switches to stable + let result = db + .join_room( + RoomId::from_proto(room.id), + user2.user_id, + ConnectionId { owner_id, id: 1 }, + "stable", + ) + .await; + + assert!(result.is_ok()) +} + fn build_background_executor() -> Arc { Deterministic::new(0).build_background() } diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 6171803341..70052468d4 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -63,6 +63,7 @@ use time::OffsetDateTime; use tokio::sync::{watch, Semaphore}; use tower::ServiceBuilder; use tracing::{info_span, instrument, Instrument}; +use util::channel::RELEASE_CHANNEL_NAME; pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30); pub const CLEANUP_TIMEOUT: Duration = Duration::from_secs(10); @@ -957,7 +958,12 @@ async fn create_room( let room = session .db() .await - .create_room(session.user_id, session.connection_id, &live_kit_room) + .create_room( + session.user_id, + session.connection_id, + &live_kit_room, + RELEASE_CHANNEL_NAME.as_str(), + ) .await?; response.send(proto::CreateRoomResponse { @@ -979,7 +985,12 @@ async fn join_room( let room = session .db() .await - .join_room(room_id, session.user_id, session.connection_id) + .join_room( + room_id, + session.user_id, + session.connection_id, + RELEASE_CHANNEL_NAME.as_str(), + ) .await?; room_updated(&room.room, &session.peer); room.into_inner() @@ -2616,7 +2627,12 @@ async fn join_channel( let room_id = db.room_id_for_channel(channel_id).await?; let joined_room = db - .join_room(room_id, session.user_id, session.connection_id) + .join_room( + room_id, + session.user_id, + session.connection_id, + RELEASE_CHANNEL_NAME.as_str(), + ) .await?; let live_kit_connection_info = session.live_kit_client.as_ref().and_then(|live_kit| { From fb57299a1d61276999607f1da5a7670b0f793e76 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 9 Oct 2023 13:40:22 -0600 Subject: [PATCH 25/50] re-trigger build with new profile? From a0ab9fe56b4ceb952631e75e0917fc3cc64266c1 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 9 Oct 2023 12:40:36 -0700 Subject: [PATCH 26/50] Unify the 2 local zed scripts, take a flag for instance count --- script/start-local-collaboration | 59 --------------------- script/zed-local | 88 ++++++++++++++++++++++++++++++++ script/zed-with-local-servers | 6 --- 3 files changed, 88 insertions(+), 65 deletions(-) delete mode 100755 script/start-local-collaboration create mode 100755 script/zed-local delete mode 100755 script/zed-with-local-servers diff --git a/script/start-local-collaboration b/script/start-local-collaboration deleted file mode 100755 index 0c4e60f9c3..0000000000 --- a/script/start-local-collaboration +++ /dev/null @@ -1,59 +0,0 @@ -#!/bin/bash - -set -e - -if [[ -z "$GITHUB_TOKEN" ]]; then - cat <<-MESSAGE -Missing \`GITHUB_TOKEN\` environment variable. This token is needed -for fetching your GitHub identity from the command-line. - -Create an access token here: https://github.com/settings/tokens -Then edit your \`~/.zshrc\` (or other shell initialization script), -adding a line like this: - - export GITHUB_TOKEN="(the token)" - -MESSAGE - exit 1 -fi - -# Install jq if it's not installed -if ! command -v jq &> /dev/null; then - echo "Installing jq..." - brew install jq -fi - -# Start one Zed instance as the current user and a second instance with a different user. -username_1=$(curl -sH "Authorization: bearer $GITHUB_TOKEN" https://api.github.com/user | jq -r .login) -username_2=nathansobo -if [[ $username_1 == $username_2 ]]; then - username_2=as-cii -fi - -# Make each Zed instance take up half of the screen. -output=$(system_profiler SPDisplaysDataType -json) -main_display=$(echo "$output" | jq '.SPDisplaysDataType[].spdisplays_ndrvs[] | select(.spdisplays_main == "spdisplays_yes")') -resolution=$(echo "$main_display" | jq -r '._spdisplays_resolution') -width=$(echo "$resolution" | jq -Rr 'match("(\\d+) x (\\d+)").captures[0].string') -half_width=$(($width / 2)) -height=$(echo "$resolution" | jq -Rr 'match("(\\d+) x (\\d+)").captures[1].string') -y=0 - -position_1=0,${y} -position_2=${half_width},${y} - -# Authenticate using the collab server's admin secret. -export ZED_STATELESS=1 -export ZED_ALWAYS_ACTIVE=1 -export ZED_ADMIN_API_TOKEN=secret -export ZED_SERVER_URL=http://localhost:8080 -export ZED_WINDOW_SIZE=${half_width},${height} - -cargo build -sleep 0.5 - -# Start the two Zed child processes. Open the given paths with the first instance. -trap "trap - SIGTERM && kill -- -$$" SIGINT SIGTERM EXIT -ZED_IMPERSONATE=${ZED_IMPERSONATE:=${username_1}} ZED_WINDOW_POSITION=${position_1} target/debug/Zed $@ & -SECOND=true ZED_IMPERSONATE=${username_2} ZED_WINDOW_POSITION=${position_2} target/debug/Zed & -wait diff --git a/script/zed-local b/script/zed-local new file mode 100755 index 0000000000..683e31ef14 --- /dev/null +++ b/script/zed-local @@ -0,0 +1,88 @@ +#!/usr/bin/env node + +const {spawn, execFileSync} = require('child_process') + +const RESOLUTION_REGEX = /(\d+) x (\d+)/ +const DIGIT_FLAG_REGEX = /^--?(\d+)$/ + +const args = process.argv.slice(2) + +// Parse the number of Zed instances to spawn. +let instanceCount = 1 +const digitMatch = args[0]?.match(DIGIT_FLAG_REGEX) +if (digitMatch) { + instanceCount = parseInt(digitMatch[1]) + args.shift() +} +if (instanceCount > 4) { + throw new Error('Cannot spawn more than 4 instances') +} + +// Parse the resolution of the main screen +const displayInfo = JSON.parse( + execFileSync( + 'system_profiler', + ['SPDisplaysDataType', '-json'], + {encoding: 'utf8'} + ) +) +const mainDisplayResolution = displayInfo + ?.SPDisplaysDataType[0] + ?.spdisplays_ndrvs + ?.find(entry => entry.spdisplays_main === "spdisplays_yes") + ?._spdisplays_resolution + ?.match(RESOLUTION_REGEX) +if (!mainDisplayResolution) { + throw new Error('Could not parse screen resolution') +} +const screenWidth = parseInt(mainDisplayResolution[1]) +const screenHeight = parseInt(mainDisplayResolution[2]) + +// Determine the window size for each instance +let instanceWidth = screenWidth +let instanceHeight = screenHeight +if (instanceCount > 1) { + instanceWidth = Math.floor(screenWidth / 2) + if (instanceCount > 2) { + instanceHeight = Math.floor(screenHeight / 2) + } +} + +let users = [ + 'nathansobo', + 'as-cii', + 'maxbrunsfeld', + 'iamnbutler' +] + +// If a user is specified, make sure it's first in the list +const user = process.env.ZED_IMPERSONATE +if (user) { + users = [user].concat(users.filter(u => u !== user)) +} + +const positions = [ + '0,0', + `${instanceWidth},0`, + `0,${instanceHeight}`, + `${instanceWidth},${instanceHeight}` +] + +execFileSync('cargo', ['build'], {stdio: 'inherit'}) + +setTimeout(() => { + for (let i = 0; i < instanceCount; i++) { + spawn('target/debug/Zed', i == 0 ? args : [], { + stdio: 'inherit', + env: { + ZED_IMPERSONATE: users[i], + ZED_WINDOW_POSITION: positions[i], + ZED_STATELESS: '1', + ZED_ALWAYS_ACTIVE: '1', + ZED_SERVER_URL: 'http://localhost:8080', + ZED_ADMIN_API_TOKEN: 'secret', + ZED_WINDOW_SIZE: `${instanceWidth},${instanceHeight}` + } + }) + } +}, 0.1) diff --git a/script/zed-with-local-servers b/script/zed-with-local-servers deleted file mode 100755 index e1b224de60..0000000000 --- a/script/zed-with-local-servers +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash - -: "${ZED_IMPERSONATE:=as-cii}" -export ZED_IMPERSONATE - -ZED_ADMIN_API_TOKEN=secret ZED_SERVER_URL=http://localhost:8080 cargo run $@ From 1e4f5145cf156344e35f451ebb44722fde9111fc Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 9 Oct 2023 12:48:35 -0700 Subject: [PATCH 27/50] Update docs to refer to new zed-local script --- README.md | 4 +--- docs/building-zed.md | 3 +-- docs/local-collaboration.md | 2 +- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index b3d4987526..eed8dd4d91 100644 --- a/README.md +++ b/README.md @@ -83,9 +83,7 @@ foreman start If you want to run Zed pointed at the local servers, you can run: ``` -script/zed-with-local-servers -# or... -script/zed-with-local-servers --release +script/zed-local ``` ### Dump element JSON diff --git a/docs/building-zed.md b/docs/building-zed.md index 6981913285..ec4538cf85 100644 --- a/docs/building-zed.md +++ b/docs/building-zed.md @@ -75,8 +75,7 @@ Expect this to take 30min to an hour! Some of these steps will take quite a whil - If you are just using the latest version, but not working on zed: - `cargo run --release` - If you need to run the collaboration server locally: - - `script/zed-with-local-servers` - - If you need to test collaboration with mutl + - `script/zed-local` ## Troubleshooting diff --git a/docs/local-collaboration.md b/docs/local-collaboration.md index 7d8054af67..4c059c0878 100644 --- a/docs/local-collaboration.md +++ b/docs/local-collaboration.md @@ -17,6 +17,6 @@ ## Testing collab locally 1. Run `foreman start` from the root of the repo. -1. In another terminal run `script/start-local-collaboration`. +1. In another terminal run `script/zed-local -2`. 1. Two copies of Zed will open. Add yourself as a contact in the one that is not you. 1. Start a collaboration session as normal with any open project. From 9cba45910eab6e8009af6ad51b6ac13c0df61a0f Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 9 Oct 2023 18:54:57 +0300 Subject: [PATCH 28/50] Ignore history items' paths when matching search queries --- crates/file_finder/src/file_finder.rs | 127 +++++++++++++++++++++++++- crates/fuzzy/src/matcher.rs | 2 +- crates/fuzzy/src/paths.rs | 6 +- 3 files changed, 126 insertions(+), 9 deletions(-) diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 6e587d8c98..222a9c650a 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -107,13 +107,23 @@ fn matching_history_item_paths( ) -> HashMap, PathMatch> { let history_items_by_worktrees = history_items .iter() - .map(|found_path| { - let path = &found_path.project.path; + .filter_map(|found_path| { let candidate = PathMatchCandidate { - path, - char_bag: CharBag::from_iter(path.to_string_lossy().to_lowercase().chars()), + path: &found_path.project.path, + // Only match history items names, otherwise their paths may match too many queries, producing false positives. + // E.g. `foo` would match both `something/foo/bar.rs` and `something/foo/foo.rs` and if the former is a history item, + // it would be shown first always, despite the latter being a better match. + char_bag: CharBag::from_iter( + found_path + .project + .path + .file_name()? + .to_string_lossy() + .to_lowercase() + .chars(), + ), }; - (found_path.project.worktree_id, candidate) + Some((found_path.project.worktree_id, candidate)) }) .fold( HashMap::default(), @@ -1803,6 +1813,113 @@ mod tests { }); } + #[gpui::test] + async fn test_history_items_vs_very_good_external_match( + deterministic: Arc, + cx: &mut gpui::TestAppContext, + ) { + let app_state = init_test(cx); + + app_state + .fs + .as_fake() + .insert_tree( + "/src", + json!({ + "collab_ui": { + "first.rs": "// First Rust file", + "second.rs": "// Second Rust file", + "third.rs": "// Third Rust file", + "collab_ui.rs": "// Fourth Rust file", + } + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; + let window = cx.add_window(|cx| Workspace::test_new(project, cx)); + let workspace = window.root(cx); + // generate some history to select from + open_close_queried_buffer( + "fir", + 1, + "first.rs", + window.into(), + &workspace, + &deterministic, + cx, + ) + .await; + open_close_queried_buffer( + "sec", + 1, + "second.rs", + window.into(), + &workspace, + &deterministic, + cx, + ) + .await; + open_close_queried_buffer( + "thi", + 1, + "third.rs", + window.into(), + &workspace, + &deterministic, + cx, + ) + .await; + open_close_queried_buffer( + "sec", + 1, + "second.rs", + window.into(), + &workspace, + &deterministic, + cx, + ) + .await; + + cx.dispatch_action(window.into(), Toggle); + let query = "collab_ui"; + let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); + finder + .update(cx, |finder, cx| { + finder.delegate_mut().update_matches(query.to_string(), cx) + }) + .await; + finder.read_with(cx, |finder, _| { + let delegate = finder.delegate(); + assert!( + delegate.matches.history.is_empty(), + "History items should not math query {query}, they should be matched by name only" + ); + + let search_entries = delegate + .matches + .search + .iter() + .map(|e| e.path.to_path_buf()) + .collect::>(); + assert_eq!( + search_entries.len(), + 4, + "All history and the new file should be found after query {query} as search results" + ); + assert_eq!( + search_entries, + vec![ + PathBuf::from("collab_ui/collab_ui.rs"), + PathBuf::from("collab_ui/third.rs"), + PathBuf::from("collab_ui/first.rs"), + PathBuf::from("collab_ui/second.rs"), + ], + "Despite all search results having the same directory name, the most matching one should be on top" + ); + }); + } + async fn open_close_queried_buffer( input: &str, expected_matches: usize, diff --git a/crates/fuzzy/src/matcher.rs b/crates/fuzzy/src/matcher.rs index dafafe40a0..e808a4886f 100644 --- a/crates/fuzzy/src/matcher.rs +++ b/crates/fuzzy/src/matcher.rs @@ -441,7 +441,7 @@ mod tests { score, worktree_id: 0, positions: Vec::new(), - path: candidate.path.clone(), + path: Arc::from(candidate.path), path_prefix: "".into(), distance_to_relative_ancestor: usize::MAX, }, diff --git a/crates/fuzzy/src/paths.rs b/crates/fuzzy/src/paths.rs index 4eb31936a8..d8fae471e1 100644 --- a/crates/fuzzy/src/paths.rs +++ b/crates/fuzzy/src/paths.rs @@ -14,7 +14,7 @@ use crate::{ #[derive(Clone, Debug)] pub struct PathMatchCandidate<'a> { - pub path: &'a Arc, + pub path: &'a Path, pub char_bag: CharBag, } @@ -120,7 +120,7 @@ pub fn match_fixed_path_set( score, worktree_id, positions: Vec::new(), - path: candidate.path.clone(), + path: Arc::from(candidate.path), path_prefix: Arc::from(""), distance_to_relative_ancestor: usize::MAX, }, @@ -195,7 +195,7 @@ pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>( score, worktree_id, positions: Vec::new(), - path: candidate.path.clone(), + path: Arc::from(candidate.path), path_prefix: candidate_set.prefix(), distance_to_relative_ancestor: relative_to.as_ref().map_or( usize::MAX, From af90077a6aaf12a2af5871a506ce544bbf0e8046 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 9 Oct 2023 13:30:14 -0700 Subject: [PATCH 29/50] Add failing test for switching leaders in a pane --- crates/collab/src/tests/following_tests.rs | 156 +++++++++------------ 1 file changed, 70 insertions(+), 86 deletions(-) diff --git a/crates/collab/src/tests/following_tests.rs b/crates/collab/src/tests/following_tests.rs index 3a489b9ac3..f3857e3db3 100644 --- a/crates/collab/src/tests/following_tests.rs +++ b/crates/collab/src/tests/following_tests.rs @@ -184,20 +184,12 @@ async fn test_basic_following( // All clients see that clients B and C are following client A. cx_c.foreground().run_until_parked(); - for (name, active_call, cx) in [ - ("A", &active_call_a, &cx_a), - ("B", &active_call_b, &cx_b), - ("C", &active_call_c, &cx_c), - ("D", &active_call_d, &cx_d), - ] { - active_call.read_with(*cx, |call, cx| { - let room = call.room().unwrap().read(cx); - assert_eq!( - room.followers_for(peer_id_a, project_id), - &[peer_id_b, peer_id_c], - "checking followers for A as {name}" - ); - }); + for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] { + assert_eq!( + followers_by_leader(project_id, cx), + &[(peer_id_a, vec![peer_id_b, peer_id_c])], + "followers seen by {name}" + ); } // Client C unfollows client A. @@ -207,46 +199,39 @@ async fn test_basic_following( // All clients see that clients B is following client A. cx_c.foreground().run_until_parked(); - for (name, active_call, cx) in [ - ("A", &active_call_a, &cx_a), - ("B", &active_call_b, &cx_b), - ("C", &active_call_c, &cx_c), - ("D", &active_call_d, &cx_d), - ] { - active_call.read_with(*cx, |call, cx| { - let room = call.room().unwrap().read(cx); - assert_eq!( - room.followers_for(peer_id_a, project_id), - &[peer_id_b], - "checking followers for A as {name}" - ); - }); + for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] { + assert_eq!( + followers_by_leader(project_id, cx), + &[(peer_id_a, vec![peer_id_b])], + "followers seen by {name}" + ); } // Client C re-follows client A. - workspace_c.update(cx_c, |workspace, cx| { - workspace.follow(peer_id_a, cx); - }); + workspace_c + .update(cx_c, |workspace, cx| { + workspace.follow(peer_id_a, cx).unwrap() + }) + .await + .unwrap(); // All clients see that clients B and C are following client A. cx_c.foreground().run_until_parked(); - for (name, active_call, cx) in [ - ("A", &active_call_a, &cx_a), - ("B", &active_call_b, &cx_b), - ("C", &active_call_c, &cx_c), - ("D", &active_call_d, &cx_d), - ] { - active_call.read_with(*cx, |call, cx| { - let room = call.room().unwrap().read(cx); - assert_eq!( - room.followers_for(peer_id_a, project_id), - &[peer_id_b, peer_id_c], - "checking followers for A as {name}" - ); - }); + for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] { + assert_eq!( + followers_by_leader(project_id, cx), + &[(peer_id_a, vec![peer_id_b, peer_id_c])], + "followers seen by {name}" + ); } - // Client D follows client C. + // Client D follows client B, then switches to following client C. + workspace_d + .update(cx_d, |workspace, cx| { + workspace.follow(peer_id_b, cx).unwrap() + }) + .await + .unwrap(); workspace_d .update(cx_d, |workspace, cx| { workspace.follow(peer_id_c, cx).unwrap() @@ -256,20 +241,15 @@ async fn test_basic_following( // All clients see that D is following C cx_d.foreground().run_until_parked(); - for (name, active_call, cx) in [ - ("A", &active_call_a, &cx_a), - ("B", &active_call_b, &cx_b), - ("C", &active_call_c, &cx_c), - ("D", &active_call_d, &cx_d), - ] { - active_call.read_with(*cx, |call, cx| { - let room = call.room().unwrap().read(cx); - assert_eq!( - room.followers_for(peer_id_c, project_id), - &[peer_id_d], - "checking followers for C as {name}" - ); - }); + for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] { + assert_eq!( + followers_by_leader(project_id, cx), + &[ + (peer_id_a, vec![peer_id_b, peer_id_c]), + (peer_id_c, vec![peer_id_d]) + ], + "followers seen by {name}" + ); } // Client C closes the project. @@ -278,32 +258,12 @@ async fn test_basic_following( // Clients A and B see that client B is following A, and client C is not present in the followers. cx_c.foreground().run_until_parked(); - for (name, active_call, cx) in [("A", &active_call_a, &cx_a), ("B", &active_call_b, &cx_b)] { - active_call.read_with(*cx, |call, cx| { - let room = call.room().unwrap().read(cx); - assert_eq!( - room.followers_for(peer_id_a, project_id), - &[peer_id_b], - "checking followers for A as {name}" - ); - }); - } - - // All clients see that no-one is following C - for (name, active_call, cx) in [ - ("A", &active_call_a, &cx_a), - ("B", &active_call_b, &cx_b), - ("C", &active_call_c, &cx_c), - ("D", &active_call_d, &cx_d), - ] { - active_call.read_with(*cx, |call, cx| { - let room = call.room().unwrap().read(cx); - assert_eq!( - room.followers_for(peer_id_c, project_id), - &[], - "checking followers for C as {name}" - ); - }); + for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] { + assert_eq!( + followers_by_leader(project_id, cx), + &[(peer_id_a, vec![peer_id_b]),], + "followers seen by {name}" + ); } // When client A activates a different editor, client B does so as well. @@ -1667,6 +1627,30 @@ struct PaneSummary { items: Vec<(bool, String)>, } +fn followers_by_leader(project_id: u64, cx: &TestAppContext) -> Vec<(PeerId, Vec)> { + cx.read(|cx| { + let active_call = ActiveCall::global(cx).read(cx); + let peer_id = active_call.client().peer_id(); + let room = active_call.room().unwrap().read(cx); + let mut result = room + .remote_participants() + .values() + .map(|participant| participant.peer_id) + .chain(peer_id) + .filter_map(|peer_id| { + let followers = room.followers_for(peer_id, project_id); + if followers.is_empty() { + None + } else { + Some((peer_id, followers.to_vec())) + } + }) + .collect::>(); + result.sort_by_key(|e| e.0); + result + }) +} + fn pane_summaries(workspace: &ViewHandle, cx: &mut TestAppContext) -> Vec { workspace.read_with(cx, |workspace, cx| { let active_pane = workspace.active_pane(); From ca735ad70f5229693466e9fa3511a9e3447c47b2 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 9 Oct 2023 13:32:38 -0700 Subject: [PATCH 30/50] Ensure there's only one leader per pane --- crates/workspace/src/pane_group.rs | 33 +++---- crates/workspace/src/workspace.rs | 137 ++++++++++++++--------------- 2 files changed, 77 insertions(+), 93 deletions(-) diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index c12cb261c8..aef03dcda0 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -1,10 +1,7 @@ -use std::{cell::RefCell, rc::Rc, sync::Arc}; - -use crate::{ - pane_group::element::PaneAxisElement, AppState, FollowerStatesByLeader, Pane, Workspace, -}; +use crate::{pane_group::element::PaneAxisElement, AppState, FollowerState, Pane, Workspace}; use anyhow::{anyhow, Result}; use call::{ActiveCall, ParticipantLocation}; +use collections::HashMap; use gpui::{ elements::*, geometry::{rect::RectF, vector::Vector2F}, @@ -13,6 +10,7 @@ use gpui::{ }; use project::Project; use serde::Deserialize; +use std::{cell::RefCell, rc::Rc, sync::Arc}; use theme::Theme; const HANDLE_HITBOX_SIZE: f32 = 4.0; @@ -95,7 +93,7 @@ impl PaneGroup { &self, project: &ModelHandle, theme: &Theme, - follower_states: &FollowerStatesByLeader, + follower_states: &HashMap, FollowerState>, active_call: Option<&ModelHandle>, active_pane: &ViewHandle, zoomed: Option<&AnyViewHandle>, @@ -162,7 +160,7 @@ impl Member { project: &ModelHandle, basis: usize, theme: &Theme, - follower_states: &FollowerStatesByLeader, + follower_states: &HashMap, FollowerState>, active_call: Option<&ModelHandle>, active_pane: &ViewHandle, zoomed: Option<&AnyViewHandle>, @@ -179,19 +177,10 @@ impl Member { ChildView::new(pane, cx).into_any() }; - let leader = follower_states - .iter() - .find_map(|(leader_id, follower_states)| { - if follower_states.contains_key(pane) { - Some(leader_id) - } else { - None - } - }) - .and_then(|leader_id| { - let room = active_call?.read(cx).room()?.read(cx); - room.remote_participant_for_peer_id(*leader_id) - }); + let leader = follower_states.get(pane).and_then(|state| { + let room = active_call?.read(cx).room()?.read(cx); + room.remote_participant_for_peer_id(state.leader_id) + }); let mut leader_border = Border::default(); let mut leader_status_box = None; @@ -486,7 +475,7 @@ impl PaneAxis { project: &ModelHandle, basis: usize, theme: &Theme, - follower_state: &FollowerStatesByLeader, + follower_states: &HashMap, FollowerState>, active_call: Option<&ModelHandle>, active_pane: &ViewHandle, zoomed: Option<&AnyViewHandle>, @@ -515,7 +504,7 @@ impl PaneAxis { project, (basis + ix) * 10, theme, - follower_state, + follower_states, active_call, active_pane, zoomed, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index d39c1cce76..d97e444eb6 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -574,7 +574,7 @@ pub struct Workspace { titlebar_item: Option, notifications: Vec<(TypeId, usize, Box)>, project: ModelHandle, - follower_states_by_leader: FollowerStatesByLeader, + follower_states: HashMap, FollowerState>, last_leaders_by_pane: HashMap, PeerId>, window_edited: bool, active_call: Option<(ModelHandle, Vec)>, @@ -599,10 +599,9 @@ pub struct ViewId { pub id: u64, } -type FollowerStatesByLeader = HashMap, FollowerState>>; - #[derive(Default)] struct FollowerState { + leader_id: PeerId, active_view_id: Option, items_by_leader_view_id: HashMap>, } @@ -791,7 +790,7 @@ impl Workspace { bottom_dock, right_dock, project: project.clone(), - follower_states_by_leader: Default::default(), + follower_states: Default::default(), last_leaders_by_pane: Default::default(), window_edited: false, active_call, @@ -2509,13 +2508,16 @@ impl Workspace { } fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext) { - if let Some(states_by_pane) = self.follower_states_by_leader.remove(&peer_id) { - for state in states_by_pane.into_values() { - for item in state.items_by_leader_view_id.into_values() { + self.follower_states.retain(|_, state| { + if state.leader_id == peer_id { + for item in state.items_by_leader_view_id.values() { item.set_leader_peer_id(None, cx); } + false + } else { + true } - } + }); cx.notify(); } @@ -2528,10 +2530,15 @@ impl Workspace { self.last_leaders_by_pane .insert(pane.downgrade(), leader_id); - self.follower_states_by_leader - .entry(leader_id) - .or_default() - .insert(pane.clone(), Default::default()); + self.unfollow(&pane, cx); + self.follower_states.insert( + pane.clone(), + FollowerState { + leader_id, + active_view_id: None, + items_by_leader_view_id: Default::default(), + }, + ); cx.notify(); let room_id = self.active_call()?.read(cx).room()?.read(cx).id(); @@ -2546,9 +2553,8 @@ impl Workspace { let response = request.await?; this.update(&mut cx, |this, _| { let state = this - .follower_states_by_leader - .get_mut(&leader_id) - .and_then(|states_by_pane| states_by_pane.get_mut(&pane)) + .follower_states + .get_mut(&pane) .ok_or_else(|| anyhow!("following interrupted"))?; state.active_view_id = if let Some(active_view_id) = response.active_view_id { Some(ViewId::from_proto(active_view_id)?) @@ -2643,12 +2649,10 @@ impl Workspace { } // if you're already following, find the right pane and focus it. - for (existing_leader_id, states_by_pane) in &mut self.follower_states_by_leader { - if leader_id == *existing_leader_id { - for (pane, _) in states_by_pane { - cx.focus(pane); - return None; - } + for (pane, state) in &self.follower_states { + if leader_id == state.leader_id { + cx.focus(pane); + return None; } } @@ -2661,36 +2665,37 @@ impl Workspace { pane: &ViewHandle, cx: &mut ViewContext, ) -> Option { - for (leader_id, states_by_pane) in &mut self.follower_states_by_leader { - let leader_id = *leader_id; - if let Some(state) = states_by_pane.remove(pane) { - for (_, item) in state.items_by_leader_view_id { - item.set_leader_peer_id(None, cx); - } - - if states_by_pane.is_empty() { - self.follower_states_by_leader.remove(&leader_id); - let project_id = self.project.read(cx).remote_id(); - let room_id = self.active_call()?.read(cx).room()?.read(cx).id(); - self.app_state - .client - .send(proto::Unfollow { - room_id, - project_id, - leader_id: Some(leader_id), - }) - .log_err(); - } - - cx.notify(); - return Some(leader_id); - } + let state = self.follower_states.remove(pane)?; + let leader_id = state.leader_id; + for (_, item) in state.items_by_leader_view_id { + item.set_leader_peer_id(None, cx); } - None + + if self + .follower_states + .values() + .all(|state| state.leader_id != state.leader_id) + { + let project_id = self.project.read(cx).remote_id(); + let room_id = self.active_call()?.read(cx).room()?.read(cx).id(); + self.app_state + .client + .send(proto::Unfollow { + room_id, + project_id, + leader_id: Some(leader_id), + }) + .log_err(); + } + + cx.notify(); + Some(leader_id) } pub fn is_being_followed(&self, peer_id: PeerId) -> bool { - self.follower_states_by_leader.contains_key(&peer_id) + self.follower_states + .values() + .any(|state| state.leader_id == peer_id) } fn render_titlebar(&self, theme: &Theme, cx: &mut ViewContext) -> AnyElement { @@ -2913,8 +2918,8 @@ impl Workspace { match update.variant.ok_or_else(|| anyhow!("invalid update"))? { proto::update_followers::Variant::UpdateActiveView(update_active_view) => { this.update(cx, |this, _| { - if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) { - for state in state.values_mut() { + for (_, state) in &mut this.follower_states { + if state.leader_id == leader_id { state.active_view_id = if let Some(active_view_id) = update_active_view.id.clone() { Some(ViewId::from_proto(active_view_id)?) @@ -2936,8 +2941,8 @@ impl Workspace { let mut tasks = Vec::new(); this.update(cx, |this, cx| { let project = this.project.clone(); - if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) { - for state in state.values_mut() { + for (_, state) in &mut this.follower_states { + if state.leader_id == leader_id { let view_id = ViewId::from_proto(id.clone())?; if let Some(item) = state.items_by_leader_view_id.get(&view_id) { tasks.push(item.apply_update_proto(&project, variant.clone(), cx)); @@ -2950,10 +2955,9 @@ impl Workspace { } proto::update_followers::Variant::CreateView(view) => { let panes = this.read_with(cx, |this, _| { - this.follower_states_by_leader - .get(&leader_id) - .into_iter() - .flat_map(|states_by_pane| states_by_pane.keys()) + this.follower_states + .iter() + .filter_map(|(pane, state)| (state.leader_id == leader_id).then_some(pane)) .cloned() .collect() })?; @@ -3012,11 +3016,7 @@ impl Workspace { for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane { let items = futures::future::try_join_all(item_tasks).await?; this.update(cx, |this, cx| { - let state = this - .follower_states_by_leader - .get_mut(&leader_id)? - .get_mut(&pane)?; - + let state = this.follower_states.get_mut(&pane)?; for (id, item) in leader_view_ids.into_iter().zip(items) { item.set_leader_peer_id(Some(leader_id), cx); state.items_by_leader_view_id.insert(id, item); @@ -3073,15 +3073,7 @@ impl Workspace { } pub fn leader_for_pane(&self, pane: &ViewHandle) -> Option { - self.follower_states_by_leader - .iter() - .find_map(|(leader_id, state)| { - if state.contains_key(pane) { - Some(*leader_id) - } else { - None - } - }) + self.follower_states.get(pane).map(|state| state.leader_id) } fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext) -> Option<()> { @@ -3109,7 +3101,10 @@ impl Workspace { } }; - for (pane, state) in self.follower_states_by_leader.get(&leader_id)? { + for (pane, state) in &self.follower_states { + if state.leader_id != leader_id { + continue; + } if leader_in_this_app { let item = state .active_view_id @@ -3804,7 +3799,7 @@ impl View for Workspace { self.center.render( &project, &theme, - &self.follower_states_by_leader, + &self.follower_states, self.active_call(), self.active_pane(), self.zoomed From ba4f4e0a3e1806d4f44e0732f14f93620b86790b Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 9 Oct 2023 23:38:24 +0300 Subject: [PATCH 31/50] Detect file paths that end with `:` New rustc messages look like ``` thread 'tests::test_history_items_vs_very_good_external_match' panicked at crates/file_finder/src/file_finder.rs:1902:13: assertion `left == right` failed: Only one history item contains collab_ui, it should be present and others should be filtered out left: 0 right: 1 ``` now and we fail to parse that `13:` bit properly, fix that. --- crates/util/src/paths.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index e7e6e0ac72..4578ce0bc9 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -139,6 +139,12 @@ impl

PathLikeWithPosition

{ column: None, }) } else { + let maybe_col_str = + if maybe_col_str.ends_with(FILE_ROW_COLUMN_DELIMITER) { + &maybe_col_str[..maybe_col_str.len() - 1] + } else { + maybe_col_str + }; match maybe_col_str.parse::() { Ok(col) => Ok(Self { path_like: parse_path_like_str(path_like_str)?, @@ -241,7 +247,6 @@ mod tests { "test_file.rs:1::", "test_file.rs::1:2", "test_file.rs:1::2", - "test_file.rs:1:2:", "test_file.rs:1:2:3", ] { let actual = parse_str(input); @@ -277,6 +282,14 @@ mod tests { column: None, }, ), + ( + "crates/file_finder/src/file_finder.rs:1902:13:", + PathLikeWithPosition { + path_like: "crates/file_finder/src/file_finder.rs".to_string(), + row: Some(1902), + column: Some(13), + }, + ), ]; for (input, expected) in input_and_expected { From 90b54a45e850d038c8a0f4767f50eb2fb3e62706 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 9 Oct 2023 14:29:45 -0700 Subject: [PATCH 32/50] Log a warning when leader activates an unknown view --- crates/workspace/src/workspace.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index d97e444eb6..bf96558f5c 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -3105,16 +3105,19 @@ impl Workspace { if state.leader_id != leader_id { continue; } - if leader_in_this_app { - let item = state - .active_view_id - .and_then(|id| state.items_by_leader_view_id.get(&id)); - if let Some(item) = item { + if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) { + if let Some(item) = state.items_by_leader_view_id.get(&active_view_id) { if leader_in_this_project || !item.is_project_item(cx) { items_to_activate.push((pane.clone(), item.boxed_clone())); } - continue; + } else { + log::warn!( + "unknown view id {:?} for leader {:?}", + active_view_id, + leader_id + ); } + continue; } if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) { items_to_activate.push((pane.clone(), Box::new(shared_screen))); From b807b3c7852e717c7d7ac59c7c6a50083682229f Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 9 Oct 2023 14:45:19 -0700 Subject: [PATCH 33/50] Handle participants' participant index changing This normally doesn't happen, but it can happen if a participant loses connection ungracefully, restarts their app, and then explicitly joins again. --- crates/call/src/room.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index 72db174d72..2fb1cab40e 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -686,6 +686,7 @@ impl Room { let Some(peer_id) = participant.peer_id else { continue; }; + let participant_index = ParticipantIndex(participant.participant_index); this.participant_user_ids.insert(participant.user_id); let old_projects = this @@ -736,8 +737,9 @@ impl Room { if let Some(remote_participant) = this.remote_participants.get_mut(&participant.user_id) { - remote_participant.projects = participant.projects; remote_participant.peer_id = peer_id; + remote_participant.projects = participant.projects; + remote_participant.participant_index = participant_index; if location != remote_participant.location { remote_participant.location = location; cx.emit(Event::ParticipantLocationChanged { @@ -749,9 +751,7 @@ impl Room { participant.user_id, RemoteParticipant { user: user.clone(), - participant_index: ParticipantIndex( - participant.participant_index, - ), + participant_index, peer_id, projects: participant.projects, location, From bdcbf9b92eb637e1c4d4b00fb610da4845202d8b Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 9 Oct 2023 14:46:29 -0700 Subject: [PATCH 34/50] Add a Reconnect action, for simulating connection blips --- crates/client/src/client.rs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 5767ac54b7..9f63d0e2be 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -70,7 +70,7 @@ pub const ZED_SECRET_CLIENT_TOKEN: &str = "618033988749894"; pub const INITIAL_RECONNECTION_DELAY: Duration = Duration::from_millis(100); pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(5); -actions!(client, [SignIn, SignOut]); +actions!(client, [SignIn, SignOut, Reconnect]); pub fn init_settings(cx: &mut AppContext) { settings::register::(cx); @@ -102,6 +102,17 @@ pub fn init(client: &Arc, cx: &mut AppContext) { } } }); + cx.add_global_action({ + let client = client.clone(); + move |_: &Reconnect, cx| { + if let Some(client) = client.upgrade() { + cx.spawn(|cx| async move { + client.reconnect(&cx); + }) + .detach(); + } + } + }); } pub struct Client { @@ -1212,6 +1223,11 @@ impl Client { self.set_status(Status::SignedOut, cx); } + pub fn reconnect(self: &Arc, cx: &AsyncAppContext) { + self.peer.teardown(); + self.set_status(Status::ConnectionLost, cx); + } + fn connection_id(&self) -> Result { if let Status::Connected { connection_id, .. } = *self.status().borrow() { Ok(connection_id) From 1d29709c3232c40a51dd2397c2d9b0b4086453af Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 9 Oct 2023 15:04:01 -0700 Subject: [PATCH 35/50] Avoid possible panic in Room::most_active_project Participants' locations might momentarily reference projects that have already been unshared. --- crates/call/src/room.rs | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index 2fb1cab40e..43354fd5a2 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -600,27 +600,30 @@ impl Room { /// Returns the most 'active' projects, defined as most people in the project pub fn most_active_project(&self) -> Option<(u64, u64)> { - let mut projects = HashMap::default(); - let mut hosts = HashMap::default(); + let mut project_hosts_and_guest_counts = HashMap::, u32)>::default(); for participant in self.remote_participants.values() { match participant.location { ParticipantLocation::SharedProject { project_id } => { - *projects.entry(project_id).or_insert(0) += 1; + project_hosts_and_guest_counts + .entry(project_id) + .or_default() + .1 += 1; } ParticipantLocation::External | ParticipantLocation::UnsharedProject => {} } for project in &participant.projects { - *projects.entry(project.id).or_insert(0) += 1; - hosts.insert(project.id, participant.user.id); + project_hosts_and_guest_counts + .entry(project.id) + .or_default() + .0 = Some(participant.user.id); } } - let mut pairs: Vec<(u64, usize)> = projects.into_iter().collect(); - pairs.sort_by_key(|(_, count)| *count as i32); - - pairs - .first() - .map(|(project_id, _)| (*project_id, hosts[&project_id])) + project_hosts_and_guest_counts + .into_iter() + .filter_map(|(id, (host, guest_count))| Some((id, host?, guest_count))) + .max_by_key(|(_, _, guest_count)| *guest_count) + .map(|(id, host, _)| (id, host)) } async fn handle_room_updated( From 8922437fcd0ae13ab6e3ba23cdac071ff20e7f2a Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 9 Oct 2023 18:13:52 -0600 Subject: [PATCH 36/50] code review --- crates/call/src/room.rs | 2 +- crates/workspace/src/workspace.rs | 14 ++------------ crates/zed/Cargo.toml | 2 +- crates/zed/resources/zed.entitlements | 12 ++++++++---- crates/zed/src/main.rs | 13 ++----------- crates/zed/src/{open_url.rs => open_listener.rs} | 10 ---------- 6 files changed, 14 insertions(+), 39 deletions(-) rename crates/zed/src/{open_url.rs => open_listener.rs} (92%) diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index b7aeee90e2..32b8232b4f 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -878,7 +878,7 @@ impl Room { Ok(()) } - pub fn next_room_update(&mut self) -> impl Future { + pub fn room_update_completed(&mut self) -> impl Future { let mut done_rx = self.room_update_completed_rx.clone(); async move { while let Some(result) = done_rx.next().await { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index e690757af7..c021384d91 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -47,7 +47,6 @@ use std::{ any::TypeId, borrow::Cow, cmp, env, - fs::OpenOptions, future::Future, path::{Path, PathBuf}, rc::Rc, @@ -4246,7 +4245,8 @@ async fn join_channel_internal( }) .await?; - room.update(cx, |room, _| room.next_room_update()).await; + room.update(cx, |room, _| room.room_update_completed()) + .await; let task = room.update(cx, |room, cx| { if let Some((project, host)) = room.most_active_project(cx) { @@ -4259,16 +4259,6 @@ async fn join_channel_internal( task.await?; return anyhow::Ok(true); } - use std::io::Write; - writeln!( - OpenOptions::new() - .write(true) - .append(true) - .open("/Users/conrad/dbg") - .unwrap(), - "no jokes" - ) - .unwrap(); anyhow::Ok(false) } diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 3c93462d4b..7eb14559be 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -156,7 +156,7 @@ workspace = { path = "../workspace", features = ["test-support"] } unindent.workspace = true -[package.metadata.bundle] +[package.metadata.bundle-dev] icon = ["resources/app-icon-preview@2x.png", "resources/app-icon-preview.png"] identifier = "dev.zed.Zed-Dev" name = "Zed Dev" diff --git a/crates/zed/resources/zed.entitlements b/crates/zed/resources/zed.entitlements index 9f5414243d..f40a8a253a 100644 --- a/crates/zed/resources/zed.entitlements +++ b/crates/zed/resources/zed.entitlements @@ -2,8 +2,6 @@ - com.apple.developer.associated-domains - applinks:cirw.in com.apple.security.automation.apple-events com.apple.security.cs.allow-jit @@ -12,8 +10,14 @@ com.apple.security.device.camera - com.apple.security.keychain-access-groups - MQ55VZLNZQ.dev.zed.Shared + com.apple.security.personal-information.addressbook + + com.apple.security.personal-information.calendars + + com.apple.security.personal-information.location + + com.apple.security.personal-information.photos-library + diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 004e5769d5..a75caa54f6 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -61,20 +61,11 @@ use zed::{ only_instance::{ensure_only_instance, IsOnlyInstance}, }; -use crate::open_url::{OpenListener, OpenRequest}; +use crate::open_listener::{OpenListener, OpenRequest}; -mod open_url; +mod open_listener; fn main() { - writeln!( - OpenOptions::new() - .write(true) - .append(true) - .open("/Users/conrad/dbg") - .unwrap(), - "HELLO" - ) - .unwrap(); let http = http::client(); init_paths(); init_logger(); diff --git a/crates/zed/src/open_url.rs b/crates/zed/src/open_listener.rs similarity index 92% rename from crates/zed/src/open_url.rs rename to crates/zed/src/open_listener.rs index 8cad217a8c..e3c08ff2c8 100644 --- a/crates/zed/src/open_url.rs +++ b/crates/zed/src/open_listener.rs @@ -43,16 +43,6 @@ impl OpenListener { } pub fn open_urls(&self, urls: Vec) { - writeln!( - OpenOptions::new() - .write(true) - .append(true) - .open("/Users/conrad/dbg") - .unwrap(), - "{:?}", - &urls, - ) - .unwrap(); self.triggered.store(true, Ordering::Release); let request = if let Some(server_name) = urls.first().and_then(|url| url.strip_prefix("zed-cli://")) From dcdd74dff43b421acb2526a6c9592cde7ad78cf0 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Tue, 10 Oct 2023 00:00:57 -0400 Subject: [PATCH 37/50] Truncate Discord release note text --- .github/workflows/release_actions.yml | 29 ++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/.github/workflows/release_actions.yml b/.github/workflows/release_actions.yml index f767324e4f..54e5d07787 100644 --- a/.github/workflows/release_actions.yml +++ b/.github/workflows/release_actions.yml @@ -10,19 +10,30 @@ jobs: id: get-appropriate-url run: | if [ "${{ github.event.release.prerelease }}" == "true" ]; then - URL="https://zed.dev/releases/preview/latest" + url="https://zed.dev/releases/preview/latest" else - URL="https://zed.dev/releases/stable/latest" + url="https://zed.dev/releases/stable/latest" fi - echo "::set-output name=URL::$URL" + echo "::set-output name=url::$url" + + - name: Prepare release content + id: prepare-content + run: | + set -eu + + text="📣 Zed ${{ github.event.release.tag_name }} was just released!\n\nRestart your Zed or head to ${{ steps.get-appropriate-url.outputs.URL }} to grab it.\n\n${{ github.event.release.body }}" + + maxTextLength=2000 + truncationIndicator="..." + + if (( ${#text} > maxTextLength )); then + text=${text:0:maxTextLength - ${#truncationIndicator}}$truncationIndicator + fi + + echo "::set-output name=content::$text" - name: Discord Webhook Action uses: tsickert/discord-webhook@v5.3.0 with: webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }} - content: | - 📣 Zed ${{ github.event.release.tag_name }} was just released! - - Restart your Zed or head to ${{ steps.get-appropriate-url.outputs.URL }} to grab it. - - ${{ github.event.release.body }} + content: ${{ steps.prepare-content.outputs.content }} From 639ae671aed28847af279df03ac77f6825672027 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 10 Oct 2023 12:26:48 +0300 Subject: [PATCH 38/50] Omit history files with path that does not exist on disk anymore --- crates/file_finder/src/file_finder.rs | 122 +++++++++++++++++++++++--- 1 file changed, 110 insertions(+), 12 deletions(-) diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 222a9c650a..b7a4a387ab 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -222,6 +222,10 @@ fn toggle_or_cycle_file_finder( .as_ref() .and_then(|found_path| found_path.absolute.as_ref()) }) + .filter(|(_, history_abs_path)| match history_abs_path { + Some(abs_path) => history_file_exists(abs_path), + None => true, + }) .map(|(history_path, abs_path)| FoundPath::new(history_path, abs_path)), ) .collect(); @@ -246,6 +250,16 @@ fn toggle_or_cycle_file_finder( } } +#[cfg(not(test))] +fn history_file_exists(abs_path: &PathBuf) -> bool { + abs_path.exists() +} + +#[cfg(test)] +fn history_file_exists(abs_path: &PathBuf) -> bool { + !abs_path.ends_with("nonexistent.rs") +} + pub enum Event { Selected(ProjectPath), Dismissed, @@ -515,12 +529,7 @@ impl PickerDelegate for FileFinderDelegate { project .worktree_for_id(history_item.project.worktree_id, cx) .is_some() - || (project.is_local() - && history_item - .absolute - .as_ref() - .filter(|abs_path| abs_path.exists()) - .is_some()) + || (project.is_local() && history_item.absolute.is_some()) }) .cloned() .map(|p| (p, None)) @@ -1900,13 +1909,8 @@ mod tests { .matches .search .iter() - .map(|e| e.path.to_path_buf()) + .map(|path_match| path_match.path.to_path_buf()) .collect::>(); - assert_eq!( - search_entries.len(), - 4, - "All history and the new file should be found after query {query} as search results" - ); assert_eq!( search_entries, vec![ @@ -1920,6 +1924,100 @@ mod tests { }); } + #[gpui::test] + async fn test_nonexistent_history_items_not_shown( + deterministic: Arc, + cx: &mut gpui::TestAppContext, + ) { + let app_state = init_test(cx); + + app_state + .fs + .as_fake() + .insert_tree( + "/src", + json!({ + "test": { + "first.rs": "// First Rust file", + "nonexistent.rs": "// Second Rust file", + "third.rs": "// Third Rust file", + } + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; + let window = cx.add_window(|cx| Workspace::test_new(project, cx)); + let workspace = window.root(cx); + // generate some history to select from + open_close_queried_buffer( + "fir", + 1, + "first.rs", + window.into(), + &workspace, + &deterministic, + cx, + ) + .await; + open_close_queried_buffer( + "non", + 1, + "nonexistent.rs", + window.into(), + &workspace, + &deterministic, + cx, + ) + .await; + open_close_queried_buffer( + "thi", + 1, + "third.rs", + window.into(), + &workspace, + &deterministic, + cx, + ) + .await; + open_close_queried_buffer( + "fir", + 1, + "first.rs", + window.into(), + &workspace, + &deterministic, + cx, + ) + .await; + + cx.dispatch_action(window.into(), Toggle); + let query = "rs"; + let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); + finder + .update(cx, |finder, cx| { + finder.delegate_mut().update_matches(query.to_string(), cx) + }) + .await; + finder.read_with(cx, |finder, _| { + let delegate = finder.delegate(); + let history_entries = delegate + .matches + .history + .iter() + .map(|(_, path_match)| path_match.as_ref().expect("should have a path match").path.to_path_buf()) + .collect::>(); + assert_eq!( + history_entries, + vec![ + PathBuf::from("test/first.rs"), + PathBuf::from("test/third.rs"), + ], + "Should have all opened files in the history, except the ones that do not exist on disk" + ); + }); + } + async fn open_close_queried_buffer( input: &str, expected_matches: usize, From 5cf92980f0d7a4e6320bb172b7115ad59dfb25e9 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 10 Oct 2023 17:51:17 +0200 Subject: [PATCH 39/50] Revert summarizing file content until we can be more intelligent about what we send Co-Authored-By: Nathan Sobo --- crates/assistant/src/assistant_panel.rs | 10 ++++++---- crates/assistant/src/prompts.rs | 21 +++++++++++++++++---- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index b69c12a2a3..62e2f61111 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -578,10 +578,7 @@ impl AssistantPanel { let codegen_kind = codegen.read(cx).kind().clone(); let user_prompt = user_prompt.to_string(); - let prompt = cx.background().spawn(async move { - let language_name = language_name.as_deref(); - generate_content_prompt(user_prompt, language_name, &buffer, range, codegen_kind) - }); + let mut messages = Vec::new(); let mut model = settings::get::(cx) .default_open_ai_model @@ -597,6 +594,11 @@ impl AssistantPanel { model = conversation.model.clone(); } + let prompt = cx.background().spawn(async move { + let language_name = language_name.as_deref(); + generate_content_prompt(user_prompt, language_name, &buffer, range, codegen_kind) + }); + cx.spawn(|_, mut cx| async move { let prompt = prompt.await; diff --git a/crates/assistant/src/prompts.rs b/crates/assistant/src/prompts.rs index bf041dff52..7aca365776 100644 --- a/crates/assistant/src/prompts.rs +++ b/crates/assistant/src/prompts.rs @@ -121,6 +121,7 @@ pub fn generate_content_prompt( range: Range, kind: CodegenKind, ) -> String { + let range = range.to_offset(buffer); let mut prompt = String::new(); // General Preamble @@ -130,17 +131,29 @@ pub fn generate_content_prompt( writeln!(prompt, "You're an expert engineer.\n").unwrap(); } - let outline = summarize(buffer, range); + let mut content = String::new(); + content.extend(buffer.text_for_range(0..range.start)); + if range.start == range.end { + content.push_str("<|START|>"); + } else { + content.push_str("<|START|"); + } + content.extend(buffer.text_for_range(range.clone())); + if range.start != range.end { + content.push_str("|END|>"); + } + content.extend(buffer.text_for_range(range.end..buffer.len())); + writeln!( prompt, - "The file you are currently working on has the following outline:" + "The file you are currently working on has the following content:" ) .unwrap(); if let Some(language_name) = language_name { let language_name = language_name.to_lowercase(); - writeln!(prompt, "```{language_name}\n{outline}\n```").unwrap(); + writeln!(prompt, "```{language_name}\n{content}\n```").unwrap(); } else { - writeln!(prompt, "```\n{outline}\n```").unwrap(); + writeln!(prompt, "```\n{content}\n```").unwrap(); } match kind { From b366592878d01d3174fdbd2afb901bb354bea03b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 10 Oct 2023 19:00:05 +0200 Subject: [PATCH 40/50] Don't include start of a line when selection ends at start of line --- crates/assistant/src/assistant_panel.rs | 38 +++++++++++++++++-------- crates/assistant/src/codegen.rs | 23 ++------------- crates/assistant/src/prompts.rs | 1 + 3 files changed, 30 insertions(+), 32 deletions(-) diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 62e2f61111..b1c6038602 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -17,7 +17,7 @@ use editor::{ BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, ToDisplayPoint, }, scroll::autoscroll::{Autoscroll, AutoscrollStrategy}, - Anchor, Editor, MoveDown, MoveUp, MultiBufferSnapshot, ToOffset, + Anchor, Editor, MoveDown, MoveUp, MultiBufferSnapshot, ToOffset, ToPoint, }; use fs::Fs; use futures::StreamExt; @@ -278,22 +278,36 @@ impl AssistantPanel { if selection.start.excerpt_id() != selection.end.excerpt_id() { return; } + let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx); + + // Extend the selection to the start and the end of the line. + let mut point_selection = selection.map(|selection| selection.to_point(&snapshot)); + if point_selection.end > point_selection.start { + point_selection.start.column = 0; + // If the selection ends at the start of the line, we don't want to include it. + if point_selection.end.column == 0 { + point_selection.end.row -= 1; + } + point_selection.end.column = snapshot.line_len(point_selection.end.row); + } + + let codegen_kind = if point_selection.start == point_selection.end { + CodegenKind::Generate { + position: snapshot.anchor_after(point_selection.start), + } + } else { + CodegenKind::Transform { + range: snapshot.anchor_before(point_selection.start) + ..snapshot.anchor_after(point_selection.end), + } + }; let inline_assist_id = post_inc(&mut self.next_inline_assist_id); - let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx); let provider = Arc::new(OpenAICompletionProvider::new( api_key, cx.background().clone(), )); - let codegen_kind = if editor.read(cx).selections.newest::(cx).is_empty() { - CodegenKind::Generate { - position: selection.start, - } - } else { - CodegenKind::Transform { - range: selection.start..selection.end, - } - }; + let codegen = cx.add_model(|cx| { Codegen::new(editor.read(cx).buffer().clone(), codegen_kind, provider, cx) }); @@ -319,7 +333,7 @@ impl AssistantPanel { editor.insert_blocks( [BlockProperties { style: BlockStyle::Flex, - position: selection.head().bias_left(&snapshot), + position: snapshot.anchor_before(point_selection.head()), height: 2, render: Arc::new({ let inline_assistant = inline_assistant.clone(); diff --git a/crates/assistant/src/codegen.rs b/crates/assistant/src/codegen.rs index e956d72260..b6ef6b5cfa 100644 --- a/crates/assistant/src/codegen.rs +++ b/crates/assistant/src/codegen.rs @@ -1,9 +1,7 @@ use crate::streaming_diff::{Hunk, StreamingDiff}; use ai::completion::{CompletionProvider, OpenAIRequest}; use anyhow::Result; -use editor::{ - multi_buffer, Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint, -}; +use editor::{multi_buffer, Anchor, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint}; use futures::{channel::mpsc, SinkExt, Stream, StreamExt}; use gpui::{Entity, ModelContext, ModelHandle, Task}; use language::{Rope, TransactionId}; @@ -40,26 +38,11 @@ impl Entity for Codegen { impl Codegen { pub fn new( buffer: ModelHandle, - mut kind: CodegenKind, + kind: CodegenKind, provider: Arc, cx: &mut ModelContext, ) -> Self { let snapshot = buffer.read(cx).snapshot(cx); - match &mut kind { - CodegenKind::Transform { range } => { - let mut point_range = range.to_point(&snapshot); - point_range.start.column = 0; - if point_range.end.column > 0 || point_range.start.row == point_range.end.row { - point_range.end.column = snapshot.line_len(point_range.end.row); - } - range.start = snapshot.anchor_before(point_range.start); - range.end = snapshot.anchor_after(point_range.end); - } - CodegenKind::Generate { position } => { - *position = position.bias_right(&snapshot); - } - } - Self { provider, buffer: buffer.clone(), @@ -386,7 +369,7 @@ mod tests { let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); let range = buffer.read_with(cx, |buffer, cx| { let snapshot = buffer.snapshot(cx); - snapshot.anchor_before(Point::new(1, 4))..snapshot.anchor_after(Point::new(4, 4)) + snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(4, 5)) }); let provider = Arc::new(TestCompletionProvider::new()); let codegen = cx.add_model(|cx| { diff --git a/crates/assistant/src/prompts.rs b/crates/assistant/src/prompts.rs index 7aca365776..d326a7f445 100644 --- a/crates/assistant/src/prompts.rs +++ b/crates/assistant/src/prompts.rs @@ -4,6 +4,7 @@ use std::cmp::{self, Reverse}; use std::fmt::Write; use std::ops::Range; +#[allow(dead_code)] fn summarize(buffer: &BufferSnapshot, selected_range: Range) -> String { #[derive(Debug)] struct Match { From 40430cf01ba433a5f5a634b4779273f4c4f5c5da Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 10 Oct 2023 12:38:20 -0700 Subject: [PATCH 41/50] Update channel rooms to be ephemeral Remove redundant live kit initialization code Fix bug in recent channel links changes where channel rooms would have the incorrect release set co-authored-by: Conrad Irwin co-authored-by: Max --- .../20221109000000_test_schema.sql | 1 + ...0_add_unique_index_on_rooms_channel_id.sql | 1 + crates/collab/src/db/queries/channels.rs | 57 +++++++++-------- crates/collab/src/db/queries/rooms.rs | 5 +- crates/collab/src/db/tests/buffer_tests.rs | 6 +- crates/collab/src/db/tests/channel_tests.rs | 63 +++++++------------ crates/collab/src/db/tests/message_tests.rs | 20 ++---- crates/collab/src/rpc.rs | 17 ++--- crates/collab/src/tests/channel_tests.rs | 2 + .../src/tests/random_channel_buffer_tests.rs | 7 +-- crates/live_kit_client/src/test.rs | 5 +- 11 files changed, 73 insertions(+), 111 deletions(-) create mode 100644 crates/collab/migrations/20231010114600_add_unique_index_on_rooms_channel_id.sql diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index a9b8d9709d..e5104839b8 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -40,6 +40,7 @@ CREATE TABLE "rooms" ( "release_channel" VARCHAR, "channel_id" INTEGER REFERENCES channels (id) ON DELETE CASCADE ); +CREATE UNIQUE INDEX "index_rooms_on_channel_id" ON "rooms" ("channel_id"); CREATE TABLE "projects" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, diff --git a/crates/collab/migrations/20231010114600_add_unique_index_on_rooms_channel_id.sql b/crates/collab/migrations/20231010114600_add_unique_index_on_rooms_channel_id.sql new file mode 100644 index 0000000000..21ec4cfbb7 --- /dev/null +++ b/crates/collab/migrations/20231010114600_add_unique_index_on_rooms_channel_id.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "index_rooms_on_channel_id" ON "rooms" ("channel_id"); diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index ab31f59541..b2fa2eb9b5 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -19,21 +19,14 @@ impl Database { .await } - pub async fn create_root_channel( - &self, - name: &str, - live_kit_room: &str, - creator_id: UserId, - ) -> Result { - self.create_channel(name, None, live_kit_room, creator_id) - .await + pub async fn create_root_channel(&self, name: &str, creator_id: UserId) -> Result { + self.create_channel(name, None, creator_id).await } pub async fn create_channel( &self, name: &str, parent: Option, - live_kit_room: &str, creator_id: UserId, ) -> Result { let name = Self::sanitize_channel_name(name)?; @@ -90,14 +83,6 @@ impl Database { .insert(&*tx) .await?; - room::ActiveModel { - channel_id: ActiveValue::Set(Some(channel.id)), - live_kit_room: ActiveValue::Set(live_kit_room.to_string()), - ..Default::default() - } - .insert(&*tx) - .await?; - Ok(channel.id) }) .await @@ -797,18 +782,36 @@ impl Database { .await } - pub async fn room_id_for_channel(&self, channel_id: ChannelId) -> Result { + pub async fn get_or_create_channel_room( + &self, + channel_id: ChannelId, + live_kit_room: &str, + enviroment: &str, + ) -> Result { self.transaction(|tx| async move { let tx = tx; - let room = channel::Model { - id: channel_id, - ..Default::default() - } - .find_related(room::Entity) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("invalid channel"))?; - Ok(room.id) + + let room = room::Entity::find() + .filter(room::Column::ChannelId.eq(channel_id)) + .one(&*tx) + .await?; + + let room_id = if let Some(room) = room { + room.id + } else { + let result = room::Entity::insert(room::ActiveModel { + channel_id: ActiveValue::Set(Some(channel_id)), + live_kit_room: ActiveValue::Set(live_kit_room.to_string()), + release_channel: ActiveValue::Set(Some(enviroment.to_string())), + ..Default::default() + }) + .exec(&*tx) + .await?; + + result.last_insert_id + }; + + Ok(room_id) }) .await } diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs index 6589f23791..4763b3f5cd 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/crates/collab/src/db/queries/rooms.rs @@ -832,10 +832,7 @@ impl Database { let (channel_id, room) = self.get_channel_room(room_id, &tx).await?; let deleted = if room.participants.is_empty() { - let result = room::Entity::delete_by_id(room_id) - .filter(room::Column::ChannelId.is_null()) - .exec(&*tx) - .await?; + let result = room::Entity::delete_by_id(room_id).exec(&*tx).await?; result.rows_affected > 0 } else { false diff --git a/crates/collab/src/db/tests/buffer_tests.rs b/crates/collab/src/db/tests/buffer_tests.rs index f6e91b91f0..0ac41a8b0b 100644 --- a/crates/collab/src/db/tests/buffer_tests.rs +++ b/crates/collab/src/db/tests/buffer_tests.rs @@ -54,7 +54,7 @@ async fn test_channel_buffers(db: &Arc) { let owner_id = db.create_server("production").await.unwrap().0 as u32; - let zed_id = db.create_root_channel("zed", "1", a_id).await.unwrap(); + let zed_id = db.create_root_channel("zed", a_id).await.unwrap(); db.invite_channel_member(zed_id, b_id, a_id, false) .await @@ -141,7 +141,7 @@ async fn test_channel_buffers(db: &Arc) { assert_eq!(left_buffer.connections, &[connection_id_a],); - let cargo_id = db.create_root_channel("cargo", "2", a_id).await.unwrap(); + let cargo_id = db.create_root_channel("cargo", a_id).await.unwrap(); let _ = db .join_channel_buffer(cargo_id, a_id, connection_id_a) .await @@ -207,7 +207,7 @@ async fn test_channel_buffers_last_operations(db: &Database) { let mut text_buffers = Vec::new(); for i in 0..3 { let channel = db - .create_root_channel(&format!("channel-{i}"), &format!("room-{i}"), user_id) + .create_root_channel(&format!("channel-{i}"), user_id) .await .unwrap(); diff --git a/crates/collab/src/db/tests/channel_tests.rs b/crates/collab/src/db/tests/channel_tests.rs index 2631e0d191..7d2bc04a35 100644 --- a/crates/collab/src/db/tests/channel_tests.rs +++ b/crates/collab/src/db/tests/channel_tests.rs @@ -45,7 +45,7 @@ async fn test_channels(db: &Arc) { .unwrap() .user_id; - let zed_id = db.create_root_channel("zed", "1", a_id).await.unwrap(); + let zed_id = db.create_root_channel("zed", a_id).await.unwrap(); // Make sure that people cannot read channels they haven't been invited to assert!(db.get_channel(zed_id, b_id).await.unwrap().is_none()); @@ -58,16 +58,13 @@ async fn test_channels(db: &Arc) { .await .unwrap(); - let crdb_id = db - .create_channel("crdb", Some(zed_id), "2", a_id) - .await - .unwrap(); + let crdb_id = db.create_channel("crdb", Some(zed_id), a_id).await.unwrap(); let livestreaming_id = db - .create_channel("livestreaming", Some(zed_id), "3", a_id) + .create_channel("livestreaming", Some(zed_id), a_id) .await .unwrap(); let replace_id = db - .create_channel("replace", Some(zed_id), "4", a_id) + .create_channel("replace", Some(zed_id), a_id) .await .unwrap(); @@ -75,14 +72,14 @@ async fn test_channels(db: &Arc) { members.sort(); assert_eq!(members, &[a_id, b_id]); - let rust_id = db.create_root_channel("rust", "5", a_id).await.unwrap(); + let rust_id = db.create_root_channel("rust", a_id).await.unwrap(); let cargo_id = db - .create_channel("cargo", Some(rust_id), "6", a_id) + .create_channel("cargo", Some(rust_id), a_id) .await .unwrap(); let cargo_ra_id = db - .create_channel("cargo-ra", Some(cargo_id), "7", a_id) + .create_channel("cargo-ra", Some(cargo_id), a_id) .await .unwrap(); @@ -202,11 +199,11 @@ async fn test_joining_channels(db: &Arc) { .unwrap() .user_id; - let channel_1 = db - .create_root_channel("channel_1", "1", user_1) + let channel_1 = db.create_root_channel("channel_1", user_1).await.unwrap(); + let room_1 = db + .get_or_create_channel_room(channel_1, "1", TEST_RELEASE_CHANNEL) .await .unwrap(); - let room_1 = db.room_id_for_channel(channel_1).await.unwrap(); // can join a room with membership to its channel let joined_room = db @@ -283,15 +280,9 @@ async fn test_channel_invites(db: &Arc) { .unwrap() .user_id; - let channel_1_1 = db - .create_root_channel("channel_1", "1", user_1) - .await - .unwrap(); + let channel_1_1 = db.create_root_channel("channel_1", user_1).await.unwrap(); - let channel_1_2 = db - .create_root_channel("channel_2", "2", user_1) - .await - .unwrap(); + let channel_1_2 = db.create_root_channel("channel_2", user_1).await.unwrap(); db.invite_channel_member(channel_1_1, user_2, user_1, false) .await @@ -353,7 +344,7 @@ async fn test_channel_invites(db: &Arc) { .unwrap(); let channel_1_3 = db - .create_channel("channel_3", Some(channel_1_1), "1", user_1) + .create_channel("channel_3", Some(channel_1_1), user_1) .await .unwrap(); @@ -415,7 +406,7 @@ async fn test_channel_renames(db: &Arc) { .unwrap() .user_id; - let zed_id = db.create_root_channel("zed", "1", user_1).await.unwrap(); + let zed_id = db.create_root_channel("zed", user_1).await.unwrap(); db.rename_channel(zed_id, user_1, "#zed-archive") .await @@ -460,25 +451,22 @@ async fn test_db_channel_moving(db: &Arc) { .unwrap() .user_id; - let zed_id = db.create_root_channel("zed", "1", a_id).await.unwrap(); + let zed_id = db.create_root_channel("zed", a_id).await.unwrap(); - let crdb_id = db - .create_channel("crdb", Some(zed_id), "2", a_id) - .await - .unwrap(); + let crdb_id = db.create_channel("crdb", Some(zed_id), a_id).await.unwrap(); let gpui2_id = db - .create_channel("gpui2", Some(zed_id), "3", a_id) + .create_channel("gpui2", Some(zed_id), a_id) .await .unwrap(); let livestreaming_id = db - .create_channel("livestreaming", Some(crdb_id), "4", a_id) + .create_channel("livestreaming", Some(crdb_id), a_id) .await .unwrap(); let livestreaming_dag_id = db - .create_channel("livestreaming_dag", Some(livestreaming_id), "5", a_id) + .create_channel("livestreaming_dag", Some(livestreaming_id), a_id) .await .unwrap(); @@ -531,12 +519,7 @@ async fn test_db_channel_moving(db: &Arc) { // ======================================================================== // Create a new channel below a channel with multiple parents let livestreaming_dag_sub_id = db - .create_channel( - "livestreaming_dag_sub", - Some(livestreaming_dag_id), - "6", - a_id, - ) + .create_channel("livestreaming_dag_sub", Some(livestreaming_dag_id), a_id) .await .unwrap(); @@ -826,15 +809,15 @@ async fn test_db_channel_moving_bugs(db: &Arc) { .unwrap() .user_id; - let zed_id = db.create_root_channel("zed", "1", user_id).await.unwrap(); + let zed_id = db.create_root_channel("zed", user_id).await.unwrap(); let projects_id = db - .create_channel("projects", Some(zed_id), "2", user_id) + .create_channel("projects", Some(zed_id), user_id) .await .unwrap(); let livestreaming_id = db - .create_channel("livestreaming", Some(projects_id), "3", user_id) + .create_channel("livestreaming", Some(projects_id), user_id) .await .unwrap(); diff --git a/crates/collab/src/db/tests/message_tests.rs b/crates/collab/src/db/tests/message_tests.rs index 464aaba207..e758fcfb5d 100644 --- a/crates/collab/src/db/tests/message_tests.rs +++ b/crates/collab/src/db/tests/message_tests.rs @@ -25,10 +25,7 @@ async fn test_channel_message_retrieval(db: &Arc) { .await .unwrap() .user_id; - let channel = db - .create_channel("channel", None, "room", user) - .await - .unwrap(); + let channel = db.create_channel("channel", None, user).await.unwrap(); let owner_id = db.create_server("test").await.unwrap().0 as u32; db.join_channel_chat(channel, rpc::ConnectionId { owner_id, id: 0 }, user) @@ -90,10 +87,7 @@ async fn test_channel_message_nonces(db: &Arc) { .await .unwrap() .user_id; - let channel = db - .create_channel("channel", None, "room", user) - .await - .unwrap(); + let channel = db.create_channel("channel", None, user).await.unwrap(); let owner_id = db.create_server("test").await.unwrap().0 as u32; @@ -157,15 +151,9 @@ async fn test_channel_message_new_notification(db: &Arc) { .unwrap() .user_id; - let channel_1 = db - .create_channel("channel", None, "room", user) - .await - .unwrap(); + let channel_1 = db.create_channel("channel", None, user).await.unwrap(); - let channel_2 = db - .create_channel("channel-2", None, "room", user) - .await - .unwrap(); + let channel_2 = db.create_channel("channel-2", None, user).await.unwrap(); db.invite_channel_member(channel_1, observer, user, false) .await diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 268228077f..e5c6d94ce0 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -938,11 +938,6 @@ async fn create_room( util::async_iife!({ let live_kit = live_kit?; - live_kit - .create_room(live_kit_room.clone()) - .await - .trace_err()?; - let token = live_kit .room_token(&live_kit_room, &session.user_id.to_string()) .trace_err()?; @@ -2206,15 +2201,10 @@ async fn create_channel( session: Session, ) -> Result<()> { let db = session.db().await; - let live_kit_room = format!("channel-{}", nanoid::nanoid!(30)); - - if let Some(live_kit) = session.live_kit_client.as_ref() { - live_kit.create_room(live_kit_room.clone()).await?; - } let parent_id = request.parent_id.map(|id| ChannelId::from_proto(id)); let id = db - .create_channel(&request.name, parent_id, &live_kit_room, session.user_id) + .create_channel(&request.name, parent_id, session.user_id) .await?; let channel = proto::Channel { @@ -2619,12 +2609,15 @@ async fn join_channel( session: Session, ) -> Result<()> { let channel_id = ChannelId::from_proto(request.channel_id); + let live_kit_room = format!("channel-{}", nanoid::nanoid!(30)); let joined_room = { leave_room_for_session(&session).await?; let db = session.db().await; - let room_id = db.room_id_for_channel(channel_id).await?; + let room_id = db + .get_or_create_channel_room(channel_id, &live_kit_room, &*RELEASE_CHANNEL_NAME) + .await?; let joined_room = db .join_room( diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index 6bdcee6af3..7cfcce832b 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -380,6 +380,8 @@ async fn test_channel_room( // Give everyone a chance to observe user A joining deterministic.run_until_parked(); + let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone()); + room_a.read_with(cx_a, |room, _| assert!(room.is_connected())); client_a.channel_store().read_with(cx_a, |channels, _| { assert_participants_eq( diff --git a/crates/collab/src/tests/random_channel_buffer_tests.rs b/crates/collab/src/tests/random_channel_buffer_tests.rs index ad0181602c..6e0bef225c 100644 --- a/crates/collab/src/tests/random_channel_buffer_tests.rs +++ b/crates/collab/src/tests/random_channel_buffer_tests.rs @@ -46,12 +46,7 @@ impl RandomizedTest for RandomChannelBufferTest { let db = &server.app_state.db; for ix in 0..CHANNEL_COUNT { let id = db - .create_channel( - &format!("channel-{ix}"), - None, - &format!("livekit-room-{ix}"), - users[0].user_id, - ) + .create_channel(&format!("channel-{ix}"), None, users[0].user_id) .await .unwrap(); for user in &users[1..] { diff --git a/crates/live_kit_client/src/test.rs b/crates/live_kit_client/src/test.rs index 704760bab7..8df8ab4abb 100644 --- a/crates/live_kit_client/src/test.rs +++ b/crates/live_kit_client/src/test.rs @@ -91,9 +91,8 @@ impl TestServer { let identity = claims.sub.unwrap().to_string(); let room_name = claims.video.room.unwrap(); 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))?; + let room = (*server_rooms).entry(room_name.to_string()).or_default(); + if room.client_rooms.contains_key(&identity) { Err(anyhow!( "{:?} attempted to join room {:?} twice", From e6228ca682156f28476338096aab3f5cb9e214d9 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Tue, 10 Oct 2023 16:04:31 -0400 Subject: [PATCH 42/50] Slim down pull request template --- .github/pull_request_template.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 2cd2050013..147402b285 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -2,11 +2,4 @@ Release Notes: -- N/A - -or - - (Added|Fixed|Improved) ... ([#](https://github.com/zed-industries/community/issues/)). - -If the release notes are only intended for a specific release channel only, add `(-only)` to the end of the release note line. -These will be removed by the person making the release. From d7d027bcf1cd8b87232c94654d1f6c885d84c814 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 10 Oct 2023 13:23:03 -0700 Subject: [PATCH 43/50] Rename release channel to enviroment --- .../20221109000000_test_schema.sql | 2 +- ...20231009181554_add_release_channel_to_rooms.sql | 2 +- crates/collab/src/db/queries/channels.rs | 2 +- crates/collab/src/db/queries/rooms.rs | 14 +++++++------- crates/collab/src/db/tables/room.rs | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index e5104839b8..5a84bfd796 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -37,7 +37,7 @@ CREATE INDEX "index_contacts_user_id_b" ON "contacts" ("user_id_b"); CREATE TABLE "rooms" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "live_kit_room" VARCHAR NOT NULL, - "release_channel" VARCHAR, + "enviroment" VARCHAR, "channel_id" INTEGER REFERENCES channels (id) ON DELETE CASCADE ); CREATE UNIQUE INDEX "index_rooms_on_channel_id" ON "rooms" ("channel_id"); diff --git a/crates/collab/migrations/20231009181554_add_release_channel_to_rooms.sql b/crates/collab/migrations/20231009181554_add_release_channel_to_rooms.sql index 95d3c400fc..8f3a704add 100644 --- a/crates/collab/migrations/20231009181554_add_release_channel_to_rooms.sql +++ b/crates/collab/migrations/20231009181554_add_release_channel_to_rooms.sql @@ -1 +1 @@ -ALTER TABLE rooms ADD COLUMN release_channel TEXT; +ALTER TABLE rooms ADD COLUMN enviroment TEXT; diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index b2fa2eb9b5..c576d2406b 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -802,7 +802,7 @@ impl Database { let result = room::Entity::insert(room::ActiveModel { channel_id: ActiveValue::Set(Some(channel_id)), live_kit_room: ActiveValue::Set(live_kit_room.to_string()), - release_channel: ActiveValue::Set(Some(enviroment.to_string())), + enviroment: ActiveValue::Set(Some(enviroment.to_string())), ..Default::default() }) .exec(&*tx) diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs index 4763b3f5cd..a38c77dc0f 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/crates/collab/src/db/queries/rooms.rs @@ -112,7 +112,7 @@ impl Database { self.transaction(|tx| async move { let room = room::ActiveModel { live_kit_room: ActiveValue::set(live_kit_room.into()), - release_channel: ActiveValue::set(Some(release_channel.to_string())), + enviroment: ActiveValue::set(Some(release_channel.to_string())), ..Default::default() } .insert(&*tx) @@ -272,28 +272,28 @@ impl Database { room_id: RoomId, user_id: UserId, connection: ConnectionId, - collab_release_channel: &str, + enviroment: &str, ) -> Result> { self.room_transaction(room_id, |tx| async move { #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] - enum QueryChannelIdAndReleaseChannel { + enum QueryChannelIdAndEnviroment { ChannelId, - ReleaseChannel, + Enviroment, } let (channel_id, release_channel): (Option, Option) = room::Entity::find() .select_only() .column(room::Column::ChannelId) - .column(room::Column::ReleaseChannel) + .column(room::Column::Enviroment) .filter(room::Column::Id.eq(room_id)) - .into_values::<_, QueryChannelIdAndReleaseChannel>() + .into_values::<_, QueryChannelIdAndEnviroment>() .one(&*tx) .await? .ok_or_else(|| anyhow!("no such room"))?; if let Some(release_channel) = release_channel { - if &release_channel != collab_release_channel { + if &release_channel != enviroment { Err(anyhow!("must join using the {} release", release_channel))?; } } diff --git a/crates/collab/src/db/tables/room.rs b/crates/collab/src/db/tables/room.rs index 7f31edcdbd..4150c741ac 100644 --- a/crates/collab/src/db/tables/room.rs +++ b/crates/collab/src/db/tables/room.rs @@ -8,7 +8,7 @@ pub struct Model { pub id: RoomId, pub live_kit_room: String, pub channel_id: Option, - pub release_channel: Option, + pub enviroment: Option, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] From 96d60eff237ffd357b74ee53a154dd4f6e151b77 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 10 Oct 2023 15:40:40 -0700 Subject: [PATCH 44/50] Fix inclusion of spurious views from other projects in FollowResponse --- crates/workspace/src/workspace.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index c3c6f9a4b6..8b068fa10c 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2878,8 +2878,7 @@ impl Workspace { let cx = &cx; move |item| { let item = item.to_followable_item_handle(cx)?; - if project_id.is_some() - && project_id != follower_project_id + if (project_id.is_none() || project_id != follower_project_id) && item.is_project_item(cx) { return None; From 1de9add304f2d2e787becd3fe49f656d9e45561b Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 10 Oct 2023 18:46:49 -0600 Subject: [PATCH 45/50] vim: Add shift-y --- assets/keymaps/vim.json | 1 + crates/vim/src/normal.rs | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 07ba8a121f..ea025747d8 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -408,6 +408,7 @@ "vim::PushOperator", "Yank" ], + "shift-y": "vim::YankLine", "i": "vim::InsertBefore", "shift-i": "vim::InsertFirstNonWhitespace", "a": "vim::InsertAfter", diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 36eab2c4c0..c8b517edd0 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -46,6 +46,7 @@ actions!( ChangeToEndOfLine, DeleteToEndOfLine, Yank, + YankLine, ChangeCase, JoinLines, ] @@ -66,6 +67,7 @@ pub fn init(cx: &mut AppContext) { cx.add_action(insert_line_above); cx.add_action(insert_line_below); cx.add_action(change_case); + cx.add_action(yank_line); cx.add_action(|_: &mut Workspace, _: &DeleteLeft, cx| { Vim::update(cx, |vim, cx| { @@ -308,6 +310,13 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex }); } +fn yank_line(_: &mut Workspace, _: &YankLine, cx: &mut ViewContext) { + Vim::update(cx, |vim, cx| { + let count = vim.take_count(cx); + yank_motion(vim, motion::Motion::CurrentLine, count, cx) + }) +} + pub(crate) fn normal_replace(text: Arc, cx: &mut WindowContext) { Vim::update(cx, |vim, cx| { vim.stop_recording(); From 821997d3721911cdad9b2ed6c8537f38adc0e347 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 10 Oct 2023 19:59:57 -0600 Subject: [PATCH 46/50] Revert accidental build change --- .cargo/config.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index e22bdb0f2c..9da6b3be08 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -3,4 +3,4 @@ xtask = "run --package xtask --" [build] # v0 mangling scheme provides more detailed backtraces around closures -rustflags = ["-C", "symbol-mangling-version=v0", "-C", "link-arg=-fuse-ld=/opt/homebrew/Cellar/llvm/16.0.6/bin/ld64.lld"] +rustflags = ["-C", "symbol-mangling-version=v0"] From 76191fe47de407754c093053a5c39cda89f1590c Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 11 Oct 2023 01:54:32 -0400 Subject: [PATCH 47/50] Fix Discord text truncation --- .github/workflows/release_actions.yml | 36 +++++++++++---------------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/.github/workflows/release_actions.yml b/.github/workflows/release_actions.yml index 54e5d07787..c1df24a8e5 100644 --- a/.github/workflows/release_actions.yml +++ b/.github/workflows/release_actions.yml @@ -6,34 +6,28 @@ jobs: discord_release: runs-on: ubuntu-latest steps: - - name: Get appropriate URL - id: get-appropriate-url + - name: Get release URL + id: get-release-url run: | if [ "${{ github.event.release.prerelease }}" == "true" ]; then - url="https://zed.dev/releases/preview/latest" + URL="https://zed.dev/releases/preview/latest" else - url="https://zed.dev/releases/stable/latest" + URL="https://zed.dev/releases/stable/latest" fi - echo "::set-output name=url::$url" + echo "::set-output name=URL::$URL" + - name: Get content + uses: 2428392/gh-truncate-string-action@v1.2.0 + id: get-content + with: + stringToTruncate: | + 📣 Zed ${{ github.event.release.tag_name }} was just released! - - name: Prepare release content - id: prepare-content - run: | - set -eu - - text="📣 Zed ${{ github.event.release.tag_name }} was just released!\n\nRestart your Zed or head to ${{ steps.get-appropriate-url.outputs.URL }} to grab it.\n\n${{ github.event.release.body }}" - - maxTextLength=2000 - truncationIndicator="..." - - if (( ${#text} > maxTextLength )); then - text=${text:0:maxTextLength - ${#truncationIndicator}}$truncationIndicator - fi - - echo "::set-output name=content::$text" + Restart your Zed or head to ${{ steps.get-release-url.outputs.URL }} to grab it. + ${{ github.event.release.body }} + maxLength: 2000 - name: Discord Webhook Action uses: tsickert/discord-webhook@v5.3.0 with: webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }} - content: ${{ steps.prepare-content.outputs.content }} + content: ${{ steps.get-content.outputs.string }} From 2d6725a41a4f05e65c788ad200881a5dbce996bf Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 11 Oct 2023 09:45:57 -0600 Subject: [PATCH 48/50] Make collaboration warning more useful --- crates/collab_ui/src/collab_titlebar_item.rs | 49 +++++++++++++------- 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index d85aca164a..211ee863e8 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -2,6 +2,7 @@ use crate::{ contact_notification::ContactNotification, face_pile::FacePile, toggle_deafen, toggle_mute, toggle_screen_sharing, LeaveCall, ToggleDeafen, ToggleMute, ToggleScreenSharing, }; +use auto_update::AutoUpdateStatus; use call::{ActiveCall, ParticipantLocation, Room}; use client::{proto::PeerId, Client, ContactEventKind, SignIn, SignOut, User, UserStore}; use clock::ReplicaId; @@ -1177,22 +1178,38 @@ impl CollabTitlebarItem { .with_style(theme.titlebar.offline_icon.container) .into_any(), ), - client::Status::UpgradeRequired => Some( - MouseEventHandler::new::(0, cx, |_, _| { - Label::new( - "Please update Zed to collaborate", - theme.titlebar.outdated_warning.text.clone(), - ) - .contained() - .with_style(theme.titlebar.outdated_warning.container) - .aligned() - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, |_, _, cx| { - auto_update::check(&Default::default(), cx); - }) - .into_any(), - ), + client::Status::UpgradeRequired => { + let auto_updater = auto_update::AutoUpdater::get(cx); + let label = match auto_updater.map(|auto_update| auto_update.read(cx).status()) { + Some(AutoUpdateStatus::Updated) => "Please restart Zed to Collaborate", + Some(AutoUpdateStatus::Installing) + | Some(AutoUpdateStatus::Downloading) + | Some(AutoUpdateStatus::Checking) => "Updating...", + Some(AutoUpdateStatus::Idle) | Some(AutoUpdateStatus::Errored) | None => { + "Please update Zed to Collaborate" + } + }; + + Some( + MouseEventHandler::new::(0, cx, |_, _| { + Label::new(label, theme.titlebar.outdated_warning.text.clone()) + .contained() + .with_style(theme.titlebar.outdated_warning.container) + .aligned() + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, |_, _, cx| { + if let Some(auto_updater) = auto_update::AutoUpdater::get(cx) { + if auto_updater.read(cx).status() == AutoUpdateStatus::Updated { + workspace::restart(&Default::default(), cx); + return; + } + } + auto_update::check(&Default::default(), cx); + }) + .into_any(), + ) + } _ => None, } } From bdf1731db397516c1fde02dd1f745edb97e92179 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 11 Oct 2023 12:40:57 -0400 Subject: [PATCH 49/50] v0.109.x dev --- Cargo.lock | 2 +- crates/zed/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cd36221de0..faf493be09 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10081,7 +10081,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.108.0" +version = "0.109.0" dependencies = [ "activity_indicator", "anyhow", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 7eb14559be..4174f7d6d5 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] description = "The fast, collaborative code editor." edition = "2021" name = "zed" -version = "0.108.0" +version = "0.109.0" publish = false [lib] From d6fa06b3bee177ff02bc7c987a8c9b1c964eacfd Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 11 Oct 2023 13:51:01 -0400 Subject: [PATCH 50/50] collab 0.24.0 --- Cargo.lock | 2 +- crates/collab/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index faf493be09..72ee771f5d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1466,7 +1466,7 @@ dependencies = [ [[package]] name = "collab" -version = "0.23.3" +version = "0.24.0" dependencies = [ "anyhow", "async-trait", diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 6177c23620..8fd1cd4380 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] default-run = "collab" edition = "2021" name = "collab" -version = "0.23.3" +version = "0.24.0" publish = false [[bin]]