Detailed changes
@@ -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 {
@@ -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::<ChannelTooltip>(
- 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::<ChannelTooltip>(
+ 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<Self>) {
- 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::<Workspace>() 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<Self>) {
@@ -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
- }
}
@@ -4154,6 +4154,88 @@ pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
DB.last_workspace().await.log_err().flatten()
}
+pub fn join_channel(
+ channel_id: u64,
+ app_state: Arc<AppState>,
+ requesting_window: Option<WindowHandle<Workspace>>,
+ cx: &mut AppContext,
+) -> Task<Result<()>> {
+ 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::<Workspace>().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],
@@ -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::<Weak<AppState>>() {
- if let Some(app_state) = cx.global::<Weak<AppState>>().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::<Weak<AppState>>() {
+ if let Some(app_state) = cx.global::<Weak<AppState>>().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<String> {
}
}
-fn open_urls(
- urls: Vec<String>,
- cli_connections_tx: &mpsc::UnboundedSender<(
- mpsc::Receiver<CliRequest>,
- IpcSender<CliResponse>,
- )>,
- open_paths_tx: &mpsc::UnboundedSender<Vec<PathBuf>>,
-) {
- 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<AppState>, 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))
@@ -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<PathBuf>,
+ },
+ CliConnection {
+ connection: (mpsc::Receiver<CliRequest>, IpcSender<CliResponse>),
+ },
+ JoinChannel {
+ channel_id: u64,
+ },
+}
+
+pub struct OpenListener {
+ tx: UnboundedSender<OpenRequest>,
+ pub triggered: AtomicBool,
+}
+
+impl OpenListener {
+ pub fn new() -> (Self, UnboundedReceiver<OpenRequest>) {
+ let (tx, rx) = mpsc::unbounded();
+ (
+ OpenListener {
+ tx,
+ triggered: AtomicBool::new(false),
+ },
+ rx,
+ )
+ }
+
+ pub fn open_urls(&self, urls: Vec<String>) {
+ 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<OpenRequest> {
+ 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<OpenRequest> {
+ 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>() {
+ return Some(OpenRequest::JoinChannel { channel_id });
+ }
+ }
+ }
+ }
+ None
+ }
+
+ fn handle_file_urls(&self, urls: Vec<String>) -> Option<OpenRequest> {
+ 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 })
+ }
+}