Cargo.lock 🔗
@@ -12938,7 +12938,6 @@ dependencies = [
"gpui",
"install_cli",
"isahc",
- "itertools 0.11.0",
"journal",
"language",
"language_selector",
Conrad Irwin created
Release Notes:
- Added support for opening files on the zed protocol `open
zed:///Users/example/Desktop/a.txt`
([#8482](https://github.com/zed-industries/zed/issues/8482)).
Cargo.lock | 1
crates/zed/Cargo.toml | 1
crates/zed/src/main.rs | 129 +++++++-----
crates/zed/src/open_listener.rs | 328 ++++++++++++++++++----------------
script/bundle-mac | 2
5 files changed, 246 insertions(+), 215 deletions(-)
@@ -12938,7 +12938,6 @@ dependencies = [
"gpui",
"install_cli",
"isahc",
- "itertools 0.11.0",
"journal",
"language",
"language_selector",
@@ -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
@@ -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<AppState>, 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() {
+fn handle_open_request(
+ request: OpenRequest,
+ app_state: Arc<AppState>,
+ cx: &mut AppContext,
+) -> bool {
+ 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}",);
}
}
- }
- })
- .detach();
-}
+ anyhow::Ok(())
+ }));
+ }
-fn handle_open_request(
- request: OpenRequest,
- app_state: Arc<AppState>,
- 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;
+ 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();
- 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)
+ // 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?;
- anyhow::Ok(())
- })
- .detach_and_log_err(cx);
+ }
+
+ 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<Client>, cx: &AsyncAppContext) -> Result<()> {
@@ -888,7 +903,9 @@ fn collect_url_args(cx: &AppContext) -> Vec<String> {
.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);
@@ -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<PathBuf>,
- },
- CliConnection {
- connection: (mpsc::Receiver<CliRequest>, IpcSender<CliResponse>),
- },
- JoinChannel {
- channel_id: u64,
- },
- OpenChannelNotes {
- channel_id: u64,
- heading: Option<String>,
- },
+#[derive(Default, Debug)]
+pub struct OpenRequest {
+ pub cli_connection: Option<(mpsc::Receiver<CliRequest>, IpcSender<CliResponse>)>,
+ pub open_paths: Vec<PathLikeWithPosition<PathBuf>>,
+ pub open_channel_notes: Vec<(u64, Option<String>)>,
+ pub join_channel: Option<u64>,
}
impl OpenRequest {
pub fn parse(urls: Vec<String>, cx: &AppContext) -> Result<Self> {
- 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_cli_connection(server_name: &str) -> Result<OpenRequest> {
- let connection = connect_to_cli(server_name)?;
- Ok(OpenRequest::CliConnection { connection })
+ 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_zed_url(request_path: &str) -> Result<OpenRequest> {
+ 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::<u64>() {
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<String>) -> 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<PathLikeWithPosition<PathBuf>>,
+ app_state: Arc<AppState>,
+ cx: &mut AsyncAppContext,
+) -> Result<(
+ WindowHandle<Workspace>,
+ Vec<Option<Result<Box<dyn ItemHandle>>>>,
+)> {
+ 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::<Vec<_>>();
+
+ 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::<Editor>() {
+ 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<CliRequest>, IpcSender<CliResponse>),
app_state: Arc<AppState>,
@@ -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::<Vec<_>>()
+ })
.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();
-
- 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::<Editor>() {
- 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();
- }
- }
+ match open_paths_with_positions(&paths, app_state, &mut cx).await {
+ Ok((workspace, items)) => {
+ let mut item_release_futures = Vec::new();
- 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
@@ -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