Add channel links (#3093)
Release notes: - `mute_on_join` setting now defaults to false. - Right click on a channel to "Copy Channel Link", these links work to open Zed and auto-join the channel Blocked on: https://github.com/zed-industries/zed.dev/pull/388
This commit is contained in:
@@ -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": {
|
||||
|
||||
+29
-2
@@ -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<UserStore>,
|
||||
follows_by_leader_id_project_id: HashMap<(PeerId, u64), Vec<PeerId>>,
|
||||
subscriptions: Vec<client::Subscription>,
|
||||
room_update_completed_tx: watch::Sender<Option<()>>,
|
||||
room_update_completed_rx: watch::Receiver<Option<()>>,
|
||||
pending_room_update: Option<Task<()>>,
|
||||
maintain_connection: Option<Task<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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -599,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 project_hosts_and_guest_counts = HashMap::<u64, (Option<u64>, u32)>::default();
|
||||
for participant in self.remote_participants.values() {
|
||||
match participant.location {
|
||||
@@ -619,6 +625,15 @@ impl Room {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(user) = self.user_store.read(cx).current_user() {
|
||||
for project in &self.local_participant.projects {
|
||||
project_hosts_and_guest_counts
|
||||
.entry(project.id)
|
||||
.or_default()
|
||||
.0 = Some(user.id);
|
||||
}
|
||||
}
|
||||
|
||||
project_hosts_and_guest_counts
|
||||
.into_iter()
|
||||
.filter_map(|(id, (host, guest_count))| Some((id, host?, guest_count)))
|
||||
@@ -858,6 +873,7 @@ impl Room {
|
||||
});
|
||||
|
||||
this.check_invariants();
|
||||
this.room_update_completed_tx.try_send(Some(())).ok();
|
||||
cx.notify();
|
||||
});
|
||||
}));
|
||||
@@ -866,6 +882,17 @@ impl Room {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn room_update_completed(&mut self) -> impl Future<Output = ()> {
|
||||
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,
|
||||
|
||||
@@ -5,6 +5,7 @@ use anyhow::{anyhow, Result};
|
||||
use channel_index::ChannelIndex;
|
||||
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::{
|
||||
@@ -52,6 +53,26 @@ pub struct Channel {
|
||||
pub unseen_message_id: Option<u64>,
|
||||
}
|
||||
|
||||
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]>);
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE rooms ADD COLUMN release_channel TEXT;
|
||||
@@ -107,10 +107,12 @@ impl Database {
|
||||
user_id: UserId,
|
||||
connection: ConnectionId,
|
||||
live_kit_room: &str,
|
||||
release_channel: &str,
|
||||
) -> Result<proto::Room> {
|
||||
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<RoomGuard<JoinRoom>> {
|
||||
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<ChannelId>, Option<String>) =
|
||||
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<ChannelId> = 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;
|
||||
|
||||
@@ -8,6 +8,7 @@ pub struct Model {
|
||||
pub id: RoomId,
|
||||
pub live_kit_room: String,
|
||||
pub channel_id: Option<ChannelId>,
|
||||
pub release_channel: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
|
||||
@@ -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<Arc<Database>>,
|
||||
pub connection: Option<sqlx::AnyConnection>,
|
||||
|
||||
@@ -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<Database>) {
|
||||
|
||||
// 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<Database>) {
|
||||
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());
|
||||
}
|
||||
|
||||
@@ -479,7 +479,7 @@ async fn test_project_count(db: &Arc<Database>) {
|
||||
.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<Database>) {
|
||||
)
|
||||
.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<Database>) {
|
||||
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<Background> {
|
||||
Deterministic::new(0).build_background()
|
||||
}
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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<CollabPanel>| {
|
||||
@@ -2568,6 +2575,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();
|
||||
|
||||
@@ -3187,49 +3201,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>) {
|
||||
@@ -3246,6 +3230,15 @@ impl CollabPanel {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn copy_channel_link(&mut self, action: &CopyChannelLink, cx: &mut ViewContext<Self>) {
|
||||
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(
|
||||
|
||||
@@ -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,26 @@ 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) {
|
||||
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(),
|
||||
)
|
||||
}
|
||||
};
|
||||
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);
|
||||
|
||||
@@ -41,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
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ use anyhow::{anyhow, Context, Result};
|
||||
use call::ActiveCall;
|
||||
use client::{
|
||||
proto::{self, PeerId},
|
||||
Client, TypedEnvelope, UserStore,
|
||||
Client, Status, TypedEnvelope, UserStore,
|
||||
};
|
||||
use collections::{hash_map, HashMap, HashSet};
|
||||
use drag_and_drop::DragAndDrop;
|
||||
@@ -35,9 +35,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;
|
||||
@@ -4139,6 +4139,188 @@ pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
|
||||
DB.last_workspace().await.log_err().flatten()
|
||||
}
|
||||
|
||||
async fn join_channel_internal(
|
||||
channel_id: u64,
|
||||
app_state: &Arc<AppState>,
|
||||
requesting_window: Option<WindowHandle<Workspace>>,
|
||||
active_call: &ModelHandle<ActiveCall>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<bool> {
|
||||
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, None);
|
||||
};
|
||||
|
||||
let already_in_channel = room.channel_id() == Some(channel_id);
|
||||
let should_prompt = room.is_sharing_project()
|
||||
&& room.remote_participants().len() > 0
|
||||
&& !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()) {
|
||||
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 client_status = client.status();
|
||||
|
||||
// this loop will terminate within client::CONNECTION_TIMEOUT seconds.
|
||||
'outer: loop {
|
||||
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 => 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?;
|
||||
|
||||
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) {
|
||||
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<AppState>,
|
||||
requesting_window: Option<WindowHandle<Workspace>>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
let active_call = ActiveCall::global(cx);
|
||||
cx.spawn(|mut cx| async move {
|
||||
let result = join_channel_internal(
|
||||
channel_id,
|
||||
&app_state,
|
||||
requesting_window,
|
||||
&active_call,
|
||||
&mut cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
// join channel succeeded, and opened a window
|
||||
if matches!(result, Ok(true)) {
|
||||
return anyhow::Ok(());
|
||||
}
|
||||
|
||||
if requesting_window.is_some() {
|
||||
return anyhow::Ok(());
|
||||
}
|
||||
|
||||
// find an existing workspace to focus and show call controls
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// return ok, we showed the error to the user.
|
||||
return anyhow::Ok(());
|
||||
})
|
||||
}
|
||||
|
||||
pub fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option<AnyWindowHandle> {
|
||||
for window in cx.windows() {
|
||||
let found = window.update(cx, |cx| {
|
||||
let is_workspace = cx.root_view().clone().downcast::<Workspace>().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],
|
||||
|
||||
@@ -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"]
|
||||
|
||||
+98
-113
@@ -7,7 +7,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::{
|
||||
@@ -31,12 +33,10 @@ use std::{
|
||||
ffi::OsStr,
|
||||
fs::OpenOptions,
|
||||
io::{IsTerminal, Write as _},
|
||||
os::unix::prelude::OsStrExt,
|
||||
panic,
|
||||
path::{Path, PathBuf},
|
||||
str,
|
||||
path::Path,
|
||||
sync::{
|
||||
atomic::{AtomicBool, AtomicU32, Ordering},
|
||||
atomic::{AtomicU32, Ordering},
|
||||
Arc, Weak,
|
||||
},
|
||||
thread,
|
||||
@@ -44,7 +44,7 @@ use std::{
|
||||
};
|
||||
use sum_tree::Bias;
|
||||
use util::{
|
||||
channel::ReleaseChannel,
|
||||
channel::{parse_zed_link, ReleaseChannel},
|
||||
http::{self, HttpClient},
|
||||
paths::PathLikeWithPosition,
|
||||
};
|
||||
@@ -60,6 +60,10 @@ use zed::{
|
||||
only_instance::{ensure_only_instance, IsOnlyInstance},
|
||||
};
|
||||
|
||||
use crate::open_listener::{OpenListener, OpenRequest};
|
||||
|
||||
mod open_listener;
|
||||
|
||||
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);
|
||||
@@ -210,12 +205,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,61 +215,85 @@ 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() {
|
||||
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))
|
||||
.detach();
|
||||
} else {
|
||||
cx.spawn({
|
||||
}
|
||||
Ok(Some(OpenRequest::CliConnection { connection })) => {
|
||||
cx.spawn(|cx| handle_cli_connection(connection, app_state.clone(), cx))
|
||||
.detach();
|
||||
}
|
||||
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();
|
||||
|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;
|
||||
}
|
||||
}
|
||||
})
|
||||
.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();
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
.detach(),
|
||||
}
|
||||
|
||||
cx.spawn(|cx| async move {
|
||||
if stdout_is_a_pty() {
|
||||
if client::IMPERSONATE_LOGIN.is_some() {
|
||||
client.authenticate_and_connect(false, &cx).await?;
|
||||
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))
|
||||
.detach();
|
||||
}
|
||||
OpenRequest::JoinChannel { channel_id } => cx
|
||||
.update(|cx| {
|
||||
workspace::join_channel(channel_id, app_state.clone(), None, cx)
|
||||
})
|
||||
.detach(),
|
||||
}
|
||||
}
|
||||
} else if client.has_keychain_credentials(&cx) {
|
||||
client.authenticate_and_connect(true, &cx).await?;
|
||||
}
|
||||
Ok::<_, anyhow::Error>(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
.detach();
|
||||
|
||||
if !triggered_authentication {
|
||||
cx.spawn(|cx| async move { authenticate(client, &cx).await })
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async fn authenticate(client: Arc<Client>, 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<String> {
|
||||
let legacy_key_name = "device_id";
|
||||
|
||||
@@ -294,37 +310,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))
|
||||
@@ -634,23 +619,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<PathBuf> {
|
||||
fn collect_url_args() -> Vec<String> {
|
||||
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<String> {
|
||||
env::args().skip(1).collect()
|
||||
}
|
||||
|
||||
fn load_embedded_fonts(app: &App) {
|
||||
let font_paths = Assets.list("fonts");
|
||||
let embedded_fonts = Mutex::new(Vec::new());
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
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::parse_zed_link;
|
||||
use util::ResultExt;
|
||||
|
||||
use crate::connect_to_cli;
|
||||
|
||||
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);
|
||||
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| parse_zed_link(url)) {
|
||||
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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
log::error!("invalid zed url: {}", request_path);
|
||||
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 })
|
||||
}
|
||||
}
|
||||
+26
-9
@@ -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 and print the app bundle's path.
|
||||
-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,7 +102,7 @@ sed \
|
||||
"s/package.metadata.bundle-${channel}/package.metadata.bundle/" \
|
||||
Cargo.toml
|
||||
|
||||
if [ "$local_only" = true ]; then
|
||||
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)
|
||||
@@ -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 \
|
||||
@@ -117,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"
|
||||
@@ -133,10 +148,12 @@ 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=<email address of signing key>"
|
||||
codesign --force --deep --entitlements crates/zed/resources/zed.entitlements --sign ${MACOS_SIGNING_KEY:- -} "${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
|
||||
|
||||
Reference in New Issue
Block a user