diff --git a/Cargo.lock b/Cargo.lock index fd549665ec..66d26f14c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12938,7 +12938,6 @@ dependencies = [ "gpui", "install_cli", "isahc", - "itertools 0.11.0", "journal", "language", "language_selector", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index b46dd7f1bd..90c969c0cb 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -54,7 +54,6 @@ go_to_line.workspace = true gpui.workspace = true install_cli.workspace = true isahc.workspace = true -itertools.workspace = true journal.workspace = true language.workspace = true language_selector.workspace = true diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index dfb66e7aa0..8769af21a3 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -13,7 +13,7 @@ use env_logger::Builder; use fs::RealFs; #[cfg(target_os = "macos")] use fsevent::StreamFlags; -use futures::StreamExt; +use futures::{future, StreamExt}; use gpui::{App, AppContext, AsyncAppContext, Context, SemanticVersion, Task}; use isahc::{prelude::Configurable, Request}; use language::LanguageRegistry; @@ -36,7 +36,7 @@ use std::{ fs::OpenOptions, io::{IsTerminal, Write}, panic, - path::{Path, PathBuf}, + path::Path, sync::{ atomic::{AtomicU32, Ordering}, Arc, @@ -48,14 +48,15 @@ use util::{ async_maybe, http::{HttpClient, HttpClientWithUrl}, paths::{self, CRASHES_DIR, CRASHES_RETIRED_DIR}, - ResultExt, + ResultExt, TryFutureExt, }; use uuid::Uuid; use welcome::{show_welcome_view, BaseKeymap, FIRST_OPEN}; use workspace::{AppState, WorkspaceStore}; use zed::{ app_menus, build_window_options, ensure_only_instance, handle_cli_connection, - handle_keymap_file_changes, initialize_workspace, IsOnlyInstance, OpenListener, OpenRequest, + handle_keymap_file_changes, initialize_workspace, open_paths_with_positions, IsOnlyInstance, + OpenListener, OpenRequest, }; #[global_allocator] @@ -325,68 +326,82 @@ fn main() { }); } -fn open_paths_and_log_errs(paths: &[PathBuf], app_state: Arc, cx: &mut AppContext) { - let task = workspace::open_paths(&paths, app_state, None, cx); - cx.spawn(|_| async move { - if let Some((_window, results)) = task.await.log_err() { - for result in results.into_iter().flatten() { - if let Err(err) = result { - log::error!("Error opening path: {err}",); - } - } - } - }) - .detach(); -} - fn handle_open_request( request: OpenRequest, app_state: Arc, cx: &mut AppContext, ) -> bool { - let mut triggered_authentication = false; - match request { - OpenRequest::Paths { paths } => open_paths_and_log_errs(&paths, app_state, cx), - OpenRequest::CliConnection { connection } => { - let app_state = app_state.clone(); - cx.spawn(move |cx| handle_cli_connection(connection, app_state, cx)) - .detach(); - } - OpenRequest::JoinChannel { channel_id } => { - triggered_authentication = true; - cx.spawn(|cx| async move { - // ignore errors here, we'll show a generic "not signed in" - let _ = authenticate(app_state.client.clone(), &cx).await; - cx.update(|cx| { - workspace::join_channel(client::ChannelId(channel_id), app_state, None, cx) - })? - .await?; - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - } - OpenRequest::OpenChannelNotes { - channel_id, - heading, - } => { - triggered_authentication = true; - let client = app_state.client.clone(); - cx.spawn(|mut cx| async move { - // ignore errors here, we'll show a generic "not signed in" - let _ = authenticate(client, &cx).await; - let workspace_window = - workspace::get_any_active_workspace(app_state, cx.clone()).await?; - let workspace = workspace_window.root_view(&cx)?; - cx.update_window(workspace_window.into(), |_, cx| { - ChannelView::open(client::ChannelId(channel_id), heading, workspace, cx) - })? - .await?; - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - } + if let Some(connection) = request.cli_connection { + let app_state = app_state.clone(); + cx.spawn(move |cx| handle_cli_connection(connection, app_state, cx)) + .detach(); + return false; + } + + let mut task = None; + if !request.open_paths.is_empty() { + let app_state = app_state.clone(); + task = Some(cx.spawn(|mut cx| async move { + let (_window, results) = + open_paths_with_positions(&request.open_paths, app_state, &mut cx).await?; + for result in results.into_iter().flatten() { + if let Err(err) = result { + log::error!("Error opening path: {err}",); + } + } + anyhow::Ok(()) + })); + } + + if !request.open_channel_notes.is_empty() || request.join_channel.is_some() { + cx.spawn(|mut cx| async move { + if let Some(task) = task { + task.await?; + } + let client = app_state.client.clone(); + // we continue even if authentication fails as join_channel/ open channel notes will + // show a visible error message. + authenticate(client, &cx).await.log_err(); + + if let Some(channel_id) = request.join_channel { + cx.update(|cx| { + workspace::join_channel( + client::ChannelId(channel_id), + app_state.clone(), + None, + cx, + ) + })? + .await?; + } + + let workspace_window = + workspace::get_any_active_workspace(app_state, cx.clone()).await?; + let workspace = workspace_window.root_view(&cx)?; + + let mut promises = Vec::new(); + for (channel_id, heading) in request.open_channel_notes { + promises.push(cx.update_window(workspace_window.into(), |_, cx| { + ChannelView::open( + client::ChannelId(channel_id), + heading, + workspace.clone(), + cx, + ) + .log_err() + })?) + } + future::join_all(promises).await; + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + true + } else { + if let Some(task) = task { + task.detach_and_log_err(cx) + } + false } - triggered_authentication } async fn authenticate(client: Arc, cx: &AsyncAppContext) -> Result<()> { @@ -888,7 +903,9 @@ fn collect_url_args(cx: &AppContext) -> Vec { .filter_map(|arg| match std::fs::canonicalize(Path::new(&arg)) { Ok(path) => Some(format!("file://{}", path.to_string_lossy())), Err(error) => { - if let Some(_) = parse_zed_link(&arg, cx) { + if arg.starts_with("file://") { + Some(arg) + } else if let Some(_) = parse_zed_link(&arg, cx) { Some(arg) } else { log::error!("error parsing path argument: {}", error); diff --git a/crates/zed/src/open_listener.rs b/crates/zed/src/open_listener.rs index d875219da6..a119623b0a 100644 --- a/crates/zed/src/open_listener.rs +++ b/crates/zed/src/open_listener.rs @@ -8,8 +8,7 @@ use editor::Editor; use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender}; use futures::channel::{mpsc, oneshot}; use futures::{FutureExt, SinkExt, StreamExt}; -use gpui::{AppContext, AsyncAppContext, Global}; -use itertools::Itertools; +use gpui::{AppContext, AsyncAppContext, Global, WindowHandle}; use language::{Bias, Point}; use std::path::Path; use std::sync::atomic::Ordering; @@ -17,62 +16,68 @@ use std::sync::Arc; use std::thread; use std::time::Duration; use std::{path::PathBuf, sync::atomic::AtomicBool}; -use util::paths::{PathExt, PathLikeWithPosition}; +use util::paths::PathLikeWithPosition; use util::ResultExt; -use workspace::AppState; +use workspace::item::ItemHandle; +use workspace::{AppState, Workspace}; -pub enum OpenRequest { - Paths { - paths: Vec, - }, - CliConnection { - connection: (mpsc::Receiver, IpcSender), - }, - JoinChannel { - channel_id: u64, - }, - OpenChannelNotes { - channel_id: u64, - heading: Option, - }, +#[derive(Default, Debug)] +pub struct OpenRequest { + pub cli_connection: Option<(mpsc::Receiver, IpcSender)>, + pub open_paths: Vec>, + pub open_channel_notes: Vec<(u64, Option)>, + pub join_channel: Option, } impl OpenRequest { pub fn parse(urls: Vec, cx: &AppContext) -> Result { - if let Some(server_name) = urls.first().and_then(|url| url.strip_prefix("zed-cli://")) { - Self::parse_cli_connection(server_name) - } else if let Some(request_path) = urls.first().and_then(|url| parse_zed_link(url, cx)) { - Self::parse_zed_url(request_path) - } else { - Ok(Self::parse_file_urls(urls)) + let mut this = Self::default(); + for url in urls { + if let Some(server_name) = url.strip_prefix("zed-cli://") { + this.cli_connection = Some(connect_to_cli(server_name)?); + } else if let Some(file) = url.strip_prefix("file://") { + this.parse_file_path(file) + } else if let Some(file) = url.strip_prefix("zed://file") { + this.parse_file_path(file) + } else if let Some(request_path) = parse_zed_link(&url, cx) { + this.parse_request_path(request_path).log_err(); + } else { + log::error!("unhandled url: {}", url); + } + } + + Ok(this) + } + + fn parse_file_path(&mut self, file: &str) { + if let Some(decoded) = urlencoding::decode(file).log_err() { + if let Some(path_buf) = + PathLikeWithPosition::parse_str(&decoded, |s| PathBuf::try_from(s)).log_err() + { + self.open_paths.push(path_buf) + } } } - fn parse_cli_connection(server_name: &str) -> Result { - let connection = connect_to_cli(server_name)?; - Ok(OpenRequest::CliConnection { connection }) - } - - fn parse_zed_url(request_path: &str) -> Result { + fn parse_request_path(&mut self, request_path: &str) -> Result<()> { 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::() { let Some(next) = parts.next() else { - return Ok(OpenRequest::JoinChannel { channel_id }); + self.join_channel = Some(channel_id); + return Ok(()); }; if let Some(heading) = next.strip_prefix("notes#") { - return Ok(OpenRequest::OpenChannelNotes { - channel_id, - heading: Some([heading].into_iter().chain(parts).join("/")), - }); - } else if next == "notes" { - return Ok(OpenRequest::OpenChannelNotes { - channel_id, - heading: None, - }); + self.open_channel_notes + .push((channel_id, Some(heading.to_string()))); + return Ok(()); + } + if next == "notes" { + self.open_channel_notes.push((channel_id, None)); + return Ok(()); } } } @@ -80,19 +85,6 @@ impl OpenRequest { } Err(anyhow!("invalid zed url: {}", request_path)) } - - fn parse_file_urls(urls: Vec) -> OpenRequest { - let paths: Vec<_> = urls - .iter() - .flat_map(|url| url.strip_prefix("file://")) - .flat_map(|url| { - let decoded = urlencoding::decode_binary(url.as_bytes()); - PathBuf::try_from_bytes(decoded.as_ref()).log_err() - }) - .collect(); - - OpenRequest::Paths { paths } - } } pub struct OpenListener { @@ -162,6 +154,60 @@ fn connect_to_cli( Ok((async_request_rx, response_tx)) } +pub async fn open_paths_with_positions( + path_likes: &Vec>, + app_state: Arc, + cx: &mut AsyncAppContext, +) -> Result<( + WindowHandle, + Vec>>>, +)> { + let mut caret_positions = HashMap::default(); + + let paths = path_likes + .iter() + .map(|path_with_position| { + let path = path_with_position.path_like.clone(); + if let Some(row) = path_with_position.row { + if path.is_file() { + let row = row.saturating_sub(1); + let col = path_with_position.column.unwrap_or(0).saturating_sub(1); + caret_positions.insert(path.clone(), Point::new(row, col)); + } + } + path + }) + .collect::>(); + + let (workspace, items) = cx + .update(|cx| workspace::open_paths(&paths, app_state, None, cx))? + .await?; + + for (item, path) in items.iter().zip(&paths) { + let Some(Ok(item)) = item else { + continue; + }; + let Some(point) = caret_positions.remove(path) else { + continue; + }; + if let Some(active_editor) = item.downcast::() { + workspace + .update(cx, |_, cx| { + active_editor.update(cx, |editor, cx| { + let snapshot = editor.snapshot(cx).display_snapshot; + let point = snapshot.buffer_snapshot.clip_point(point, Bias::Left); + editor.change_selections(Some(Autoscroll::center()), cx, |s| { + s.select_ranges([point..point]) + }); + }); + }) + .log_err(); + } + } + + Ok((workspace, items)) +} + pub async fn handle_cli_connection( (mut requests, responses): (mpsc::Receiver, IpcSender), app_state: Arc, @@ -170,18 +216,26 @@ pub async fn handle_cli_connection( if let Some(request) = requests.next().await { match request { CliRequest::Open { paths, wait } => { - let mut caret_positions = HashMap::default(); - let paths = if paths.is_empty() { workspace::last_opened_workspace_paths() .await - .map(|location| location.paths().to_vec()) + .map(|location| { + location + .paths() + .iter() + .map(|path| PathLikeWithPosition { + path_like: path.clone(), + row: None, + column: None, + }) + .collect::>() + }) .unwrap_or_default() } else { paths .into_iter() .map(|path_with_position_string| { - let path_with_position = PathLikeWithPosition::parse_str( + PathLikeWithPosition::parse_str( &path_with_position_string, |path_str| { Ok::<_, std::convert::Infallible>( @@ -189,125 +243,87 @@ pub async fn handle_cli_connection( ) }, ) - .expect("Infallible"); - let path = path_with_position.path_like; - if let Some(row) = path_with_position.row { - if path.is_file() { - let row = row.saturating_sub(1); - let col = - path_with_position.column.unwrap_or(0).saturating_sub(1); - caret_positions.insert(path.clone(), Point::new(row, col)); - } - } - path + .expect("Infallible") }) .collect() }; let mut errored = false; - match cx.update(|cx| workspace::open_paths(&paths, app_state, None, cx)) { - Ok(task) => match task.await { - Ok((workspace, items)) => { - let mut item_release_futures = Vec::new(); + match open_paths_with_positions(&paths, app_state, &mut cx).await { + Ok((workspace, items)) => { + let mut item_release_futures = Vec::new(); - for (item, path) in items.into_iter().zip(&paths) { - match item { - Some(Ok(item)) => { - if let Some(point) = caret_positions.remove(path) { - if let Some(active_editor) = item.downcast::() { - workspace - .update(&mut cx, |_, cx| { - active_editor.update(cx, |editor, cx| { - let snapshot = editor - .snapshot(cx) - .display_snapshot; - let point = snapshot - .buffer_snapshot - .clip_point(point, Bias::Left); - editor.change_selections( - Some(Autoscroll::center()), - cx, - |s| s.select_ranges([point..point]), - ); - }); - }) - .log_err(); - } - } - - cx.update(|cx| { - let released = oneshot::channel(); - item.on_release( - cx, - Box::new(move |_| { - let _ = released.0.send(()); - }), - ) - .detach(); - item_release_futures.push(released.1); + for (item, path) in items.into_iter().zip(&paths) { + match item { + Some(Ok(item)) => { + cx.update(|cx| { + let released = oneshot::channel(); + item.on_release( + cx, + Box::new(move |_| { + let _ = released.0.send(()); + }), + ) + .detach(); + item_release_futures.push(released.1); + }) + .log_err(); + } + Some(Err(err)) => { + responses + .send(CliResponse::Stderr { + message: format!("error opening {:?}: {}", path, err), }) .log_err(); - } - Some(Err(err)) => { - responses - .send(CliResponse::Stderr { - message: format!( - "error opening {:?}: {}", - path, err - ), - }) - .log_err(); - errored = true; - } - None => {} + errored = true; } + None => {} } + } - if wait { - let background = cx.background_executor().clone(); - let wait = async move { - if paths.is_empty() { - let (done_tx, done_rx) = oneshot::channel(); - let _subscription = workspace.update(&mut cx, |_, cx| { - cx.on_release(move |_, _, _| { - let _ = done_tx.send(()); - }) - }); - let _ = done_rx.await; - } else { - let _ = futures::future::try_join_all(item_release_futures) - .await; - }; - } - .fuse(); - futures::pin_mut!(wait); + if wait { + let background = cx.background_executor().clone(); + let wait = async move { + if paths.is_empty() { + let (done_tx, done_rx) = oneshot::channel(); + let _subscription = workspace.update(&mut cx, |_, cx| { + cx.on_release(move |_, _, _| { + let _ = done_tx.send(()); + }) + }); + let _ = done_rx.await; + } else { + let _ = + futures::future::try_join_all(item_release_futures).await; + }; + } + .fuse(); + futures::pin_mut!(wait); - loop { - // Repeatedly check if CLI is still open to avoid wasting resources - // waiting for files or workspaces to close. - let mut timer = background.timer(Duration::from_secs(1)).fuse(); - futures::select_biased! { - _ = wait => break, - _ = timer => { - if responses.send(CliResponse::Ping).is_err() { - break; - } + loop { + // Repeatedly check if CLI is still open to avoid wasting resources + // waiting for files or workspaces to close. + let mut timer = background.timer(Duration::from_secs(1)).fuse(); + futures::select_biased! { + _ = wait => break, + _ = timer => { + if responses.send(CliResponse::Ping).is_err() { + break; } } } } } - Err(error) => { - errored = true; - responses - .send(CliResponse::Stderr { - message: format!("error opening {:?}: {}", paths, error), - }) - .log_err(); - } - }, - Err(_) => errored = true, + } + Err(error) => { + errored = true; + responses + .send(CliResponse::Stderr { + message: format!("error opening {:?}: {}", paths, error), + }) + .log_err(); + } } responses diff --git a/script/bundle-mac b/script/bundle-mac index 863c1960c3..a333c67be3 100755 --- a/script/bundle-mac +++ b/script/bundle-mac @@ -179,7 +179,7 @@ fi # Note: The app identifier for our development builds is the same as the app identifier for nightly. cp crates/${zed_crate}/contents/$channel/embedded.provisionprofile "${app_path}/Contents/" -if [[ -n $MACOS_CERTIFICATE && -n $MACOS_CERTIFICATE_PASSWORD && -n $APPLE_NOTARIZATION_USERNAME && -n $APPLE_NOTARIZATION_PASSWORD ]]; then +if [[ -n "${MACOS_CERTIFICATE:-}" && -n "${MACOS_CERTIFICATE_PASSWORD:-}" && -n "${APPLE_NOTARIZATION_USERNAME:-}" && -n "${APPLE_NOTARIZATION_PASSWORD:-}" ]]; then echo "Signing bundle with Apple-issued certificate" security create-keychain -p "$MACOS_CERTIFICATE_PASSWORD" zed.keychain || echo "" security default-keychain -s zed.keychain