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)