Merge branch 'main' into gpui2

This commit is contained in:
Antonio Scandurra
2023-10-12 10:55:17 +02:00
71 changed files with 1588 additions and 783 deletions
+32 -16
View File
@@ -17,7 +17,7 @@ use editor::{
BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, ToDisplayPoint,
},
scroll::autoscroll::{Autoscroll, AutoscrollStrategy},
Anchor, Editor, MoveDown, MoveUp, MultiBufferSnapshot, ToOffset,
Anchor, Editor, MoveDown, MoveUp, MultiBufferSnapshot, ToOffset, ToPoint,
};
use fs::Fs;
use futures::StreamExt;
@@ -278,22 +278,36 @@ impl AssistantPanel {
if selection.start.excerpt_id() != selection.end.excerpt_id() {
return;
}
let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
// Extend the selection to the start and the end of the line.
let mut point_selection = selection.map(|selection| selection.to_point(&snapshot));
if point_selection.end > point_selection.start {
point_selection.start.column = 0;
// If the selection ends at the start of the line, we don't want to include it.
if point_selection.end.column == 0 {
point_selection.end.row -= 1;
}
point_selection.end.column = snapshot.line_len(point_selection.end.row);
}
let codegen_kind = if point_selection.start == point_selection.end {
CodegenKind::Generate {
position: snapshot.anchor_after(point_selection.start),
}
} else {
CodegenKind::Transform {
range: snapshot.anchor_before(point_selection.start)
..snapshot.anchor_after(point_selection.end),
}
};
let inline_assist_id = post_inc(&mut self.next_inline_assist_id);
let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
let provider = Arc::new(OpenAICompletionProvider::new(
api_key,
cx.background().clone(),
));
let codegen_kind = if editor.read(cx).selections.newest::<usize>(cx).is_empty() {
CodegenKind::Generate {
position: selection.start,
}
} else {
CodegenKind::Transform {
range: selection.start..selection.end,
}
};
let codegen = cx.add_model(|cx| {
Codegen::new(editor.read(cx).buffer().clone(), codegen_kind, provider, cx)
});
@@ -319,7 +333,7 @@ impl AssistantPanel {
editor.insert_blocks(
[BlockProperties {
style: BlockStyle::Flex,
position: selection.head().bias_left(&snapshot),
position: snapshot.anchor_before(point_selection.head()),
height: 2,
render: Arc::new({
let inline_assistant = inline_assistant.clone();
@@ -578,10 +592,7 @@ impl AssistantPanel {
let codegen_kind = codegen.read(cx).kind().clone();
let user_prompt = user_prompt.to_string();
let prompt = cx.background().spawn(async move {
let language_name = language_name.as_deref();
generate_content_prompt(user_prompt, language_name, &buffer, range, codegen_kind)
});
let mut messages = Vec::new();
let mut model = settings::get::<AssistantSettings>(cx)
.default_open_ai_model
@@ -597,6 +608,11 @@ impl AssistantPanel {
model = conversation.model.clone();
}
let prompt = cx.background().spawn(async move {
let language_name = language_name.as_deref();
generate_content_prompt(user_prompt, language_name, &buffer, range, codegen_kind)
});
cx.spawn(|_, mut cx| async move {
let prompt = prompt.await;
+3 -20
View File
@@ -1,9 +1,7 @@
use crate::streaming_diff::{Hunk, StreamingDiff};
use ai::completion::{CompletionProvider, OpenAIRequest};
use anyhow::Result;
use editor::{
multi_buffer, Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint,
};
use editor::{multi_buffer, Anchor, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint};
use futures::{channel::mpsc, SinkExt, Stream, StreamExt};
use gpui::{Entity, ModelContext, ModelHandle, Task};
use language::{Rope, TransactionId};
@@ -40,26 +38,11 @@ impl Entity for Codegen {
impl Codegen {
pub fn new(
buffer: ModelHandle<MultiBuffer>,
mut kind: CodegenKind,
kind: CodegenKind,
provider: Arc<dyn CompletionProvider>,
cx: &mut ModelContext<Self>,
) -> Self {
let snapshot = buffer.read(cx).snapshot(cx);
match &mut kind {
CodegenKind::Transform { range } => {
let mut point_range = range.to_point(&snapshot);
point_range.start.column = 0;
if point_range.end.column > 0 || point_range.start.row == point_range.end.row {
point_range.end.column = snapshot.line_len(point_range.end.row);
}
range.start = snapshot.anchor_before(point_range.start);
range.end = snapshot.anchor_after(point_range.end);
}
CodegenKind::Generate { position } => {
*position = position.bias_right(&snapshot);
}
}
Self {
provider,
buffer: buffer.clone(),
@@ -386,7 +369,7 @@ mod tests {
let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
let range = buffer.read_with(cx, |buffer, cx| {
let snapshot = buffer.snapshot(cx);
snapshot.anchor_before(Point::new(1, 4))..snapshot.anchor_after(Point::new(4, 4))
snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(4, 5))
});
let provider = Arc::new(TestCompletionProvider::new());
let codegen = cx.add_model(|cx| {
+18 -4
View File
@@ -4,6 +4,7 @@ use std::cmp::{self, Reverse};
use std::fmt::Write;
use std::ops::Range;
#[allow(dead_code)]
fn summarize(buffer: &BufferSnapshot, selected_range: Range<impl ToOffset>) -> String {
#[derive(Debug)]
struct Match {
@@ -121,6 +122,7 @@ pub fn generate_content_prompt(
range: Range<impl ToOffset>,
kind: CodegenKind,
) -> String {
let range = range.to_offset(buffer);
let mut prompt = String::new();
// General Preamble
@@ -130,17 +132,29 @@ pub fn generate_content_prompt(
writeln!(prompt, "You're an expert engineer.\n").unwrap();
}
let outline = summarize(buffer, range);
let mut content = String::new();
content.extend(buffer.text_for_range(0..range.start));
if range.start == range.end {
content.push_str("<|START|>");
} else {
content.push_str("<|START|");
}
content.extend(buffer.text_for_range(range.clone()));
if range.start != range.end {
content.push_str("|END|>");
}
content.extend(buffer.text_for_range(range.end..buffer.len()));
writeln!(
prompt,
"The file you are currently working on has the following outline:"
"The file you are currently working on has the following content:"
)
.unwrap();
if let Some(language_name) = language_name {
let language_name = language_name.to_lowercase();
writeln!(prompt, "```{language_name}\n{outline}\n```").unwrap();
writeln!(prompt, "```{language_name}\n{content}\n```").unwrap();
} else {
writeln!(prompt, "```\n{outline}\n```").unwrap();
writeln!(prompt, "```\n{content}\n```").unwrap();
}
match kind {
-1
View File
@@ -20,7 +20,6 @@ test-support = [
[dependencies]
audio = { path = "../audio" }
channel = { path = "../channel" }
client = { path = "../client" }
collections = { path = "../collections" }
gpui = { path = "../gpui" }
+1 -2
View File
@@ -5,7 +5,6 @@ pub mod room;
use anyhow::{anyhow, Result};
use audio::Audio;
use call_settings::CallSettings;
use channel::ChannelId;
use client::{
proto, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore,
ZED_ALWAYS_ACTIVE,
@@ -79,7 +78,7 @@ impl ActiveCall {
}
}
pub fn channel_id(&self, cx: &AppContext) -> Option<ChannelId> {
pub fn channel_id(&self, cx: &AppContext) -> Option<u64> {
self.room()?.read(cx).channel_id()
}
+46 -16
View File
@@ -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,28 +605,40 @@ impl Room {
}
/// Returns the most 'active' projects, defined as most people in the project
pub fn most_active_project(&self) -> Option<(u64, u64)> {
let mut projects = HashMap::default();
let mut hosts = HashMap::default();
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 {
ParticipantLocation::SharedProject { project_id } => {
*projects.entry(project_id).or_insert(0) += 1;
project_hosts_and_guest_counts
.entry(project_id)
.or_default()
.1 += 1;
}
ParticipantLocation::External | ParticipantLocation::UnsharedProject => {}
}
for project in &participant.projects {
*projects.entry(project.id).or_insert(0) += 1;
hosts.insert(project.id, participant.user.id);
project_hosts_and_guest_counts
.entry(project.id)
.or_default()
.0 = Some(participant.user.id);
}
}
let mut pairs: Vec<(u64, usize)> = projects.into_iter().collect();
pairs.sort_by_key(|(_, count)| *count as i32);
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);
}
}
pairs
.first()
.map(|(project_id, _)| (*project_id, hosts[&project_id]))
project_hosts_and_guest_counts
.into_iter()
.filter_map(|(id, (host, guest_count))| Some((id, host?, guest_count)))
.max_by_key(|(_, _, guest_count)| *guest_count)
.map(|(id, host, _)| (id, host))
}
async fn handle_room_updated(
@@ -686,6 +704,7 @@ impl Room {
let Some(peer_id) = participant.peer_id else {
continue;
};
let participant_index = ParticipantIndex(participant.participant_index);
this.participant_user_ids.insert(participant.user_id);
let old_projects = this
@@ -736,8 +755,9 @@ impl Room {
if let Some(remote_participant) =
this.remote_participants.get_mut(&participant.user_id)
{
remote_participant.projects = participant.projects;
remote_participant.peer_id = peer_id;
remote_participant.projects = participant.projects;
remote_participant.participant_index = participant_index;
if location != remote_participant.location {
remote_participant.location = location;
cx.emit(Event::ParticipantLocationChanged {
@@ -749,9 +769,7 @@ impl Room {
participant.user_id,
RemoteParticipant {
user: user.clone(),
participant_index: ParticipantIndex(
participant.participant_index,
),
participant_index,
peer_id,
projects: participant.projects,
location,
@@ -855,6 +873,7 @@ impl Room {
});
this.check_invariants();
this.room_update_completed_tx.try_send(Some(())).ok();
cx.notify();
});
}));
@@ -863,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,
+6 -4
View File
@@ -2,19 +2,21 @@ mod channel_buffer;
mod channel_chat;
mod channel_store;
use client::{Client, UserStore};
use gpui::{AppContext, ModelHandle};
use std::sync::Arc;
pub use channel_buffer::{ChannelBuffer, ChannelBufferEvent, ACKNOWLEDGE_DEBOUNCE_INTERVAL};
pub use channel_chat::{ChannelChat, ChannelChatEvent, ChannelMessage, ChannelMessageId};
pub use channel_store::{
Channel, ChannelData, ChannelEvent, ChannelId, ChannelMembership, ChannelPath, ChannelStore,
};
use client::Client;
use std::sync::Arc;
#[cfg(test)]
mod channel_store_tests;
pub fn init(client: &Arc<Client>) {
pub fn init(client: &Arc<Client>, user_store: ModelHandle<UserStore>, cx: &mut AppContext) {
channel_store::init(client, user_store, cx);
channel_buffer::init(client);
channel_chat::init(client);
}
+31 -1
View File
@@ -2,8 +2,10 @@ mod channel_index;
use crate::{channel_buffer::ChannelBuffer, channel_chat::ChannelChat};
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::{
@@ -14,7 +16,11 @@ use serde_derive::{Deserialize, Serialize};
use std::{borrow::Cow, hash::Hash, mem, ops::Deref, sync::Arc, time::Duration};
use util::ResultExt;
use self::channel_index::ChannelIndex;
pub fn init(client: &Arc<Client>, user_store: ModelHandle<UserStore>, cx: &mut AppContext) {
let channel_store =
cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
cx.set_global(channel_store);
}
pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
@@ -47,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]>);
@@ -71,6 +97,10 @@ enum OpenedModelHandle<E: Entity> {
}
impl ChannelStore {
pub fn global(cx: &AppContext) -> ModelHandle<Self> {
cx.global::<ModelHandle<Self>>().clone()
}
pub fn new(
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
+2 -2
View File
@@ -340,10 +340,10 @@ fn init_test(cx: &mut AppContext) -> ModelHandle<ChannelStore> {
cx.foreground().forbid_parking();
cx.set_global(SettingsStore::test(cx));
crate::init(&client);
client::init(&client, cx);
crate::init(&client, user_store, cx);
cx.add_model(|cx| ChannelStore::new(client, user_store, cx))
ChannelStore::global(cx)
}
fn update_channels(
+1
View File
@@ -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 {
+17 -1
View File
@@ -70,7 +70,7 @@ pub const ZED_SECRET_CLIENT_TOKEN: &str = "618033988749894";
pub const INITIAL_RECONNECTION_DELAY: Duration = Duration::from_millis(100);
pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(5);
actions!(client, [SignIn, SignOut]);
actions!(client, [SignIn, SignOut, Reconnect]);
pub fn init_settings(cx: &mut AppContext) {
settings::register::<TelemetrySettings>(cx);
@@ -102,6 +102,17 @@ pub fn init(client: &Arc<Client>, cx: &mut AppContext) {
}
}
});
cx.add_global_action({
let client = client.clone();
move |_: &Reconnect, cx| {
if let Some(client) = client.upgrade() {
cx.spawn(|cx| async move {
client.reconnect(&cx);
})
.detach();
}
}
});
}
pub struct Client {
@@ -1212,6 +1223,11 @@ impl Client {
self.set_status(Status::SignedOut, cx);
}
pub fn reconnect(self: &Arc<Self>, cx: &AsyncAppContext) {
self.peer.teardown();
self.set_status(Status::ConnectionLost, cx);
}
fn connection_id(&self) -> Result<ConnectionId> {
if let Status::Connected { connection_id, .. } = *self.status().borrow() {
Ok(connection_id)
+1 -1
View File
@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
default-run = "collab"
edition = "2021"
name = "collab"
version = "0.23.3"
version = "0.24.0"
publish = false
[[bin]]
@@ -37,8 +37,10 @@ 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,
"enviroment" VARCHAR,
"channel_id" INTEGER REFERENCES channels (id) ON DELETE CASCADE
);
CREATE UNIQUE INDEX "index_rooms_on_channel_id" ON "rooms" ("channel_id");
CREATE TABLE "projects" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -0,0 +1 @@
ALTER TABLE rooms ADD COLUMN enviroment TEXT;
@@ -0,0 +1 @@
CREATE UNIQUE INDEX "index_rooms_on_channel_id" ON "rooms" ("channel_id");
+30 -27
View File
@@ -19,21 +19,14 @@ impl Database {
.await
}
pub async fn create_root_channel(
&self,
name: &str,
live_kit_room: &str,
creator_id: UserId,
) -> Result<ChannelId> {
self.create_channel(name, None, live_kit_room, creator_id)
.await
pub async fn create_root_channel(&self, name: &str, creator_id: UserId) -> Result<ChannelId> {
self.create_channel(name, None, creator_id).await
}
pub async fn create_channel(
&self,
name: &str,
parent: Option<ChannelId>,
live_kit_room: &str,
creator_id: UserId,
) -> Result<ChannelId> {
let name = Self::sanitize_channel_name(name)?;
@@ -90,14 +83,6 @@ impl Database {
.insert(&*tx)
.await?;
room::ActiveModel {
channel_id: ActiveValue::Set(Some(channel.id)),
live_kit_room: ActiveValue::Set(live_kit_room.to_string()),
..Default::default()
}
.insert(&*tx)
.await?;
Ok(channel.id)
})
.await
@@ -797,18 +782,36 @@ impl Database {
.await
}
pub async fn room_id_for_channel(&self, channel_id: ChannelId) -> Result<RoomId> {
pub async fn get_or_create_channel_room(
&self,
channel_id: ChannelId,
live_kit_room: &str,
enviroment: &str,
) -> Result<RoomId> {
self.transaction(|tx| async move {
let tx = tx;
let room = channel::Model {
id: channel_id,
..Default::default()
}
.find_related(room::Entity)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("invalid channel"))?;
Ok(room.id)
let room = room::Entity::find()
.filter(room::Column::ChannelId.eq(channel_id))
.one(&*tx)
.await?;
let room_id = if let Some(room) = room {
room.id
} else {
let result = room::Entity::insert(room::ActiveModel {
channel_id: ActiveValue::Set(Some(channel_id)),
live_kit_room: ActiveValue::Set(live_kit_room.to_string()),
enviroment: ActiveValue::Set(Some(enviroment.to_string())),
..Default::default()
})
.exec(&*tx)
.await?;
result.last_insert_id
};
Ok(room_id)
})
.await
}
+24 -13
View File
@@ -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()),
enviroment: 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,
enviroment: &str,
) -> Result<RoomGuard<JoinRoom>> {
self.room_transaction(room_id, |tx| async move {
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
enum QueryChannelId {
enum QueryChannelIdAndEnviroment {
ChannelId,
Enviroment,
}
let (channel_id, release_channel): (Option<ChannelId>, Option<String>) =
room::Entity::find()
.select_only()
.column(room::Column::ChannelId)
.column(room::Column::Enviroment)
.filter(room::Column::Id.eq(room_id))
.into_values::<_, QueryChannelIdAndEnviroment>()
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no such room"))?;
if let Some(release_channel) = release_channel {
if &release_channel != enviroment {
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;
@@ -818,10 +832,7 @@ impl Database {
let (channel_id, room) = self.get_channel_room(room_id, &tx).await?;
let deleted = if room.participants.is_empty() {
let result = room::Entity::delete_by_id(room_id)
.filter(room::Column::ChannelId.is_null())
.exec(&*tx)
.await?;
let result = room::Entity::delete_by_id(room_id).exec(&*tx).await?;
result.rows_affected > 0
} else {
false
+1
View File
@@ -8,6 +8,7 @@ pub struct Model {
pub id: RoomId,
pub live_kit_room: String,
pub channel_id: Option<ChannelId>,
pub enviroment: Option<String>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
+2
View File
@@ -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>,
+3 -3
View File
@@ -54,7 +54,7 @@ async fn test_channel_buffers(db: &Arc<Database>) {
let owner_id = db.create_server("production").await.unwrap().0 as u32;
let zed_id = db.create_root_channel("zed", "1", a_id).await.unwrap();
let zed_id = db.create_root_channel("zed", a_id).await.unwrap();
db.invite_channel_member(zed_id, b_id, a_id, false)
.await
@@ -141,7 +141,7 @@ async fn test_channel_buffers(db: &Arc<Database>) {
assert_eq!(left_buffer.connections, &[connection_id_a],);
let cargo_id = db.create_root_channel("cargo", "2", a_id).await.unwrap();
let cargo_id = db.create_root_channel("cargo", a_id).await.unwrap();
let _ = db
.join_channel_buffer(cargo_id, a_id, connection_id_a)
.await
@@ -207,7 +207,7 @@ async fn test_channel_buffers_last_operations(db: &Database) {
let mut text_buffers = Vec::new();
for i in 0..3 {
let channel = db
.create_root_channel(&format!("channel-{i}"), &format!("room-{i}"), user_id)
.create_root_channel(&format!("channel-{i}"), user_id)
.await
.unwrap();
+40 -43
View File
@@ -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;
@@ -41,7 +45,7 @@ async fn test_channels(db: &Arc<Database>) {
.unwrap()
.user_id;
let zed_id = db.create_root_channel("zed", "1", a_id).await.unwrap();
let zed_id = db.create_root_channel("zed", a_id).await.unwrap();
// Make sure that people cannot read channels they haven't been invited to
assert!(db.get_channel(zed_id, b_id).await.unwrap().is_none());
@@ -54,16 +58,13 @@ async fn test_channels(db: &Arc<Database>) {
.await
.unwrap();
let crdb_id = db
.create_channel("crdb", Some(zed_id), "2", a_id)
.await
.unwrap();
let crdb_id = db.create_channel("crdb", Some(zed_id), a_id).await.unwrap();
let livestreaming_id = db
.create_channel("livestreaming", Some(zed_id), "3", a_id)
.create_channel("livestreaming", Some(zed_id), a_id)
.await
.unwrap();
let replace_id = db
.create_channel("replace", Some(zed_id), "4", a_id)
.create_channel("replace", Some(zed_id), a_id)
.await
.unwrap();
@@ -71,14 +72,14 @@ async fn test_channels(db: &Arc<Database>) {
members.sort();
assert_eq!(members, &[a_id, b_id]);
let rust_id = db.create_root_channel("rust", "5", a_id).await.unwrap();
let rust_id = db.create_root_channel("rust", a_id).await.unwrap();
let cargo_id = db
.create_channel("cargo", Some(rust_id), "6", a_id)
.create_channel("cargo", Some(rust_id), a_id)
.await
.unwrap();
let cargo_ra_id = db
.create_channel("cargo-ra", Some(cargo_id), "7", a_id)
.create_channel("cargo-ra", Some(cargo_id), a_id)
.await
.unwrap();
@@ -198,15 +199,20 @@ async fn test_joining_channels(db: &Arc<Database>) {
.unwrap()
.user_id;
let channel_1 = db
.create_root_channel("channel_1", "1", user_1)
let channel_1 = db.create_root_channel("channel_1", user_1).await.unwrap();
let room_1 = db
.get_or_create_channel_room(channel_1, "1", TEST_RELEASE_CHANNEL)
.await
.unwrap();
let room_1 = db.room_id_for_channel(channel_1).await.unwrap();
// 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 +220,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());
}
@@ -269,15 +280,9 @@ async fn test_channel_invites(db: &Arc<Database>) {
.unwrap()
.user_id;
let channel_1_1 = db
.create_root_channel("channel_1", "1", user_1)
.await
.unwrap();
let channel_1_1 = db.create_root_channel("channel_1", user_1).await.unwrap();
let channel_1_2 = db
.create_root_channel("channel_2", "2", user_1)
.await
.unwrap();
let channel_1_2 = db.create_root_channel("channel_2", user_1).await.unwrap();
db.invite_channel_member(channel_1_1, user_2, user_1, false)
.await
@@ -339,7 +344,7 @@ async fn test_channel_invites(db: &Arc<Database>) {
.unwrap();
let channel_1_3 = db
.create_channel("channel_3", Some(channel_1_1), "1", user_1)
.create_channel("channel_3", Some(channel_1_1), user_1)
.await
.unwrap();
@@ -401,7 +406,7 @@ async fn test_channel_renames(db: &Arc<Database>) {
.unwrap()
.user_id;
let zed_id = db.create_root_channel("zed", "1", user_1).await.unwrap();
let zed_id = db.create_root_channel("zed", user_1).await.unwrap();
db.rename_channel(zed_id, user_1, "#zed-archive")
.await
@@ -446,25 +451,22 @@ async fn test_db_channel_moving(db: &Arc<Database>) {
.unwrap()
.user_id;
let zed_id = db.create_root_channel("zed", "1", a_id).await.unwrap();
let zed_id = db.create_root_channel("zed", a_id).await.unwrap();
let crdb_id = db
.create_channel("crdb", Some(zed_id), "2", a_id)
.await
.unwrap();
let crdb_id = db.create_channel("crdb", Some(zed_id), a_id).await.unwrap();
let gpui2_id = db
.create_channel("gpui2", Some(zed_id), "3", a_id)
.create_channel("gpui2", Some(zed_id), a_id)
.await
.unwrap();
let livestreaming_id = db
.create_channel("livestreaming", Some(crdb_id), "4", a_id)
.create_channel("livestreaming", Some(crdb_id), a_id)
.await
.unwrap();
let livestreaming_dag_id = db
.create_channel("livestreaming_dag", Some(livestreaming_id), "5", a_id)
.create_channel("livestreaming_dag", Some(livestreaming_id), a_id)
.await
.unwrap();
@@ -517,12 +519,7 @@ async fn test_db_channel_moving(db: &Arc<Database>) {
// ========================================================================
// Create a new channel below a channel with multiple parents
let livestreaming_dag_sub_id = db
.create_channel(
"livestreaming_dag_sub",
Some(livestreaming_dag_id),
"6",
a_id,
)
.create_channel("livestreaming_dag_sub", Some(livestreaming_dag_id), a_id)
.await
.unwrap();
@@ -812,15 +809,15 @@ async fn test_db_channel_moving_bugs(db: &Arc<Database>) {
.unwrap()
.user_id;
let zed_id = db.create_root_channel("zed", "1", user_id).await.unwrap();
let zed_id = db.create_root_channel("zed", user_id).await.unwrap();
let projects_id = db
.create_channel("projects", Some(zed_id), "2", user_id)
.create_channel("projects", Some(zed_id), user_id)
.await
.unwrap();
let livestreaming_id = db
.create_channel("livestreaming", Some(projects_id), "3", user_id)
.create_channel("livestreaming", Some(projects_id), user_id)
.await
.unwrap();
+88 -4
View File
@@ -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()
}
+4 -16
View File
@@ -25,10 +25,7 @@ async fn test_channel_message_retrieval(db: &Arc<Database>) {
.await
.unwrap()
.user_id;
let channel = db
.create_channel("channel", None, "room", user)
.await
.unwrap();
let channel = db.create_channel("channel", None, user).await.unwrap();
let owner_id = db.create_server("test").await.unwrap().0 as u32;
db.join_channel_chat(channel, rpc::ConnectionId { owner_id, id: 0 }, user)
@@ -90,10 +87,7 @@ async fn test_channel_message_nonces(db: &Arc<Database>) {
.await
.unwrap()
.user_id;
let channel = db
.create_channel("channel", None, "room", user)
.await
.unwrap();
let channel = db.create_channel("channel", None, user).await.unwrap();
let owner_id = db.create_server("test").await.unwrap().0 as u32;
@@ -157,15 +151,9 @@ async fn test_channel_message_new_notification(db: &Arc<Database>) {
.unwrap()
.user_id;
let channel_1 = db
.create_channel("channel", None, "room", user)
.await
.unwrap();
let channel_1 = db.create_channel("channel", None, user).await.unwrap();
let channel_2 = db
.create_channel("channel-2", None, "room", user)
.await
.unwrap();
let channel_2 = db.create_channel("channel-2", None, user).await.unwrap();
db.invite_channel_member(channel_1, observer, user, false)
.await
+24 -15
View File
@@ -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);
@@ -937,11 +938,6 @@ async fn create_room(
util::async_iife!({
let live_kit = live_kit?;
live_kit
.create_room(live_kit_room.clone())
.await
.trace_err()?;
let token = live_kit
.room_token(&live_kit_room, &session.user_id.to_string())
.trace_err()?;
@@ -957,7 +953,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 +980,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()
@@ -2195,15 +2201,10 @@ async fn create_channel(
session: Session,
) -> Result<()> {
let db = session.db().await;
let live_kit_room = format!("channel-{}", nanoid::nanoid!(30));
if let Some(live_kit) = session.live_kit_client.as_ref() {
live_kit.create_room(live_kit_room.clone()).await?;
}
let parent_id = request.parent_id.map(|id| ChannelId::from_proto(id));
let id = db
.create_channel(&request.name, parent_id, &live_kit_room, session.user_id)
.create_channel(&request.name, parent_id, session.user_id)
.await?;
let channel = proto::Channel {
@@ -2608,15 +2609,23 @@ async fn join_channel(
session: Session,
) -> Result<()> {
let channel_id = ChannelId::from_proto(request.channel_id);
let live_kit_room = format!("channel-{}", nanoid::nanoid!(30));
let joined_room = {
leave_room_for_session(&session).await?;
let db = session.db().await;
let room_id = db.room_id_for_channel(channel_id).await?;
let room_id = db
.get_or_create_channel_room(channel_id, &live_kit_room, &*RELEASE_CHANNEL_NAME)
.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| {
+2
View File
@@ -380,6 +380,8 @@ async fn test_channel_room(
// Give everyone a chance to observe user A joining
deterministic.run_until_parked();
let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
room_a.read_with(cx_a, |room, _| assert!(room.is_connected()));
client_a.channel_store().read_with(cx_a, |channels, _| {
assert_participants_eq(
+70 -86
View File
@@ -184,20 +184,12 @@ async fn test_basic_following(
// All clients see that clients B and C are following client A.
cx_c.foreground().run_until_parked();
for (name, active_call, cx) in [
("A", &active_call_a, &cx_a),
("B", &active_call_b, &cx_b),
("C", &active_call_c, &cx_c),
("D", &active_call_d, &cx_d),
] {
active_call.read_with(*cx, |call, cx| {
let room = call.room().unwrap().read(cx);
assert_eq!(
room.followers_for(peer_id_a, project_id),
&[peer_id_b, peer_id_c],
"checking followers for A as {name}"
);
});
for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
assert_eq!(
followers_by_leader(project_id, cx),
&[(peer_id_a, vec![peer_id_b, peer_id_c])],
"followers seen by {name}"
);
}
// Client C unfollows client A.
@@ -207,46 +199,39 @@ async fn test_basic_following(
// All clients see that clients B is following client A.
cx_c.foreground().run_until_parked();
for (name, active_call, cx) in [
("A", &active_call_a, &cx_a),
("B", &active_call_b, &cx_b),
("C", &active_call_c, &cx_c),
("D", &active_call_d, &cx_d),
] {
active_call.read_with(*cx, |call, cx| {
let room = call.room().unwrap().read(cx);
assert_eq!(
room.followers_for(peer_id_a, project_id),
&[peer_id_b],
"checking followers for A as {name}"
);
});
for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
assert_eq!(
followers_by_leader(project_id, cx),
&[(peer_id_a, vec![peer_id_b])],
"followers seen by {name}"
);
}
// Client C re-follows client A.
workspace_c.update(cx_c, |workspace, cx| {
workspace.follow(peer_id_a, cx);
});
workspace_c
.update(cx_c, |workspace, cx| {
workspace.follow(peer_id_a, cx).unwrap()
})
.await
.unwrap();
// All clients see that clients B and C are following client A.
cx_c.foreground().run_until_parked();
for (name, active_call, cx) in [
("A", &active_call_a, &cx_a),
("B", &active_call_b, &cx_b),
("C", &active_call_c, &cx_c),
("D", &active_call_d, &cx_d),
] {
active_call.read_with(*cx, |call, cx| {
let room = call.room().unwrap().read(cx);
assert_eq!(
room.followers_for(peer_id_a, project_id),
&[peer_id_b, peer_id_c],
"checking followers for A as {name}"
);
});
for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
assert_eq!(
followers_by_leader(project_id, cx),
&[(peer_id_a, vec![peer_id_b, peer_id_c])],
"followers seen by {name}"
);
}
// Client D follows client C.
// Client D follows client B, then switches to following client C.
workspace_d
.update(cx_d, |workspace, cx| {
workspace.follow(peer_id_b, cx).unwrap()
})
.await
.unwrap();
workspace_d
.update(cx_d, |workspace, cx| {
workspace.follow(peer_id_c, cx).unwrap()
@@ -256,20 +241,15 @@ async fn test_basic_following(
// All clients see that D is following C
cx_d.foreground().run_until_parked();
for (name, active_call, cx) in [
("A", &active_call_a, &cx_a),
("B", &active_call_b, &cx_b),
("C", &active_call_c, &cx_c),
("D", &active_call_d, &cx_d),
] {
active_call.read_with(*cx, |call, cx| {
let room = call.room().unwrap().read(cx);
assert_eq!(
room.followers_for(peer_id_c, project_id),
&[peer_id_d],
"checking followers for C as {name}"
);
});
for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
assert_eq!(
followers_by_leader(project_id, cx),
&[
(peer_id_a, vec![peer_id_b, peer_id_c]),
(peer_id_c, vec![peer_id_d])
],
"followers seen by {name}"
);
}
// Client C closes the project.
@@ -278,32 +258,12 @@ async fn test_basic_following(
// Clients A and B see that client B is following A, and client C is not present in the followers.
cx_c.foreground().run_until_parked();
for (name, active_call, cx) in [("A", &active_call_a, &cx_a), ("B", &active_call_b, &cx_b)] {
active_call.read_with(*cx, |call, cx| {
let room = call.room().unwrap().read(cx);
assert_eq!(
room.followers_for(peer_id_a, project_id),
&[peer_id_b],
"checking followers for A as {name}"
);
});
}
// All clients see that no-one is following C
for (name, active_call, cx) in [
("A", &active_call_a, &cx_a),
("B", &active_call_b, &cx_b),
("C", &active_call_c, &cx_c),
("D", &active_call_d, &cx_d),
] {
active_call.read_with(*cx, |call, cx| {
let room = call.room().unwrap().read(cx);
assert_eq!(
room.followers_for(peer_id_c, project_id),
&[],
"checking followers for C as {name}"
);
});
for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
assert_eq!(
followers_by_leader(project_id, cx),
&[(peer_id_a, vec![peer_id_b]),],
"followers seen by {name}"
);
}
// When client A activates a different editor, client B does so as well.
@@ -1667,6 +1627,30 @@ struct PaneSummary {
items: Vec<(bool, String)>,
}
fn followers_by_leader(project_id: u64, cx: &TestAppContext) -> Vec<(PeerId, Vec<PeerId>)> {
cx.read(|cx| {
let active_call = ActiveCall::global(cx).read(cx);
let peer_id = active_call.client().peer_id();
let room = active_call.room().unwrap().read(cx);
let mut result = room
.remote_participants()
.values()
.map(|participant| participant.peer_id)
.chain(peer_id)
.filter_map(|peer_id| {
let followers = room.followers_for(peer_id, project_id);
if followers.is_empty() {
None
} else {
Some((peer_id, followers.to_vec()))
}
})
.collect::<Vec<_>>();
result.sort_by_key(|e| e.0);
result
})
}
fn pane_summaries(workspace: &ViewHandle<Workspace>, cx: &mut TestAppContext) -> Vec<PaneSummary> {
workspace.read_with(cx, |workspace, cx| {
let active_pane = workspace.active_pane();
@@ -46,12 +46,7 @@ impl RandomizedTest for RandomChannelBufferTest {
let db = &server.app_state.db;
for ix in 0..CHANNEL_COUNT {
let id = db
.create_channel(
&format!("channel-{ix}"),
None,
&format!("livekit-room-{ix}"),
users[0].user_id,
)
.create_channel(&format!("channel-{ix}"), None, users[0].user_id)
.await
.unwrap();
for user in &users[1..] {
+17 -22
View File
@@ -44,6 +44,7 @@ pub struct TestServer {
pub struct TestClient {
pub username: String,
pub app_state: Arc<workspace::AppState>,
channel_store: ModelHandle<ChannelStore>,
state: RefCell<TestClientState>,
}
@@ -206,15 +207,12 @@ impl TestServer {
let fs = FakeFs::new(cx.background());
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx));
let channel_store =
cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
let mut language_registry = LanguageRegistry::test();
language_registry.set_executor(cx.background());
let app_state = Arc::new(workspace::AppState {
client: client.clone(),
user_store: user_store.clone(),
workspace_store,
channel_store: channel_store.clone(),
languages: Arc::new(language_registry),
fs: fs.clone(),
build_window_options: |_, _, _| Default::default(),
@@ -231,7 +229,7 @@ impl TestServer {
workspace::init(app_state.clone(), cx);
audio::init((), cx);
call::init(client.clone(), user_store.clone(), cx);
channel::init(&client);
channel::init(&client, user_store, cx);
});
client
@@ -242,6 +240,7 @@ impl TestServer {
let client = TestClient {
app_state,
username: name.to_string(),
channel_store: cx.read(ChannelStore::global).clone(),
state: Default::default(),
};
client.wait_for_current_user(cx).await;
@@ -310,10 +309,9 @@ impl TestServer {
admin: (&TestClient, &mut TestAppContext),
members: &mut [(&TestClient, &mut TestAppContext)],
) -> u64 {
let (admin_client, admin_cx) = admin;
let channel_id = admin_client
.app_state
.channel_store
let (_, admin_cx) = admin;
let channel_id = admin_cx
.read(ChannelStore::global)
.update(admin_cx, |channel_store, cx| {
channel_store.create_channel(channel, parent, cx)
})
@@ -321,9 +319,8 @@ impl TestServer {
.unwrap();
for (member_client, member_cx) in members {
admin_client
.app_state
.channel_store
admin_cx
.read(ChannelStore::global)
.update(admin_cx, |channel_store, cx| {
channel_store.invite_member(
channel_id,
@@ -337,9 +334,8 @@ impl TestServer {
admin_cx.foreground().run_until_parked();
member_client
.app_state
.channel_store
member_cx
.read(ChannelStore::global)
.update(*member_cx, |channels, _| {
channels.respond_to_channel_invite(channel_id, true)
})
@@ -447,7 +443,7 @@ impl TestClient {
}
pub fn channel_store(&self) -> &ModelHandle<ChannelStore> {
&self.app_state.channel_store
&self.channel_store
}
pub fn user_store(&self) -> &ModelHandle<UserStore> {
@@ -614,8 +610,8 @@ impl TestClient {
) {
let (other_client, other_cx) = user;
self.app_state
.channel_store
cx_self
.read(ChannelStore::global)
.update(cx_self, |channel_store, cx| {
channel_store.invite_member(channel, other_client.user_id().unwrap(), true, cx)
})
@@ -624,11 +620,10 @@ impl TestClient {
cx_self.foreground().run_until_parked();
other_client
.app_state
.channel_store
.update(other_cx, |channels, _| {
channels.respond_to_channel_invite(channel, true)
other_cx
.read(ChannelStore::global)
.update(other_cx, |channel_store, _| {
channel_store.respond_to_channel_invite(channel, true)
})
.await
.unwrap();
+1 -1
View File
@@ -73,7 +73,7 @@ impl ChannelView {
) -> Task<Result<ViewHandle<Self>>> {
let workspace = workspace.read(cx);
let project = workspace.project().to_owned();
let channel_store = workspace.app_state().channel_store.clone();
let channel_store = ChannelStore::global(cx);
let markdown = workspace
.app_state()
.languages
+1 -1
View File
@@ -81,7 +81,7 @@ impl ChatPanel {
pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
let fs = workspace.app_state().fs.clone();
let client = workspace.app_state().client.clone();
let channel_store = workspace.app_state().channel_store.clone();
let channel_store = ChannelStore::global(cx);
let languages = workspace.app_state().languages.clone();
let input_editor = cx.add_view(|cx| {
+39 -46
View File
@@ -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>| {
@@ -648,7 +655,7 @@ impl CollabPanel {
channel_editing_state: None,
selection: None,
user_store: workspace.user_store().clone(),
channel_store: workspace.app_state().channel_store.clone(),
channel_store: ChannelStore::global(cx),
project: workspace.project().clone(),
subscriptions: Vec::default(),
match_candidates: Vec::default(),
@@ -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(
+33 -16
View File
@@ -2,6 +2,7 @@ use crate::{
contact_notification::ContactNotification, face_pile::FacePile, toggle_deafen, toggle_mute,
toggle_screen_sharing, LeaveCall, ToggleDeafen, ToggleMute, ToggleScreenSharing,
};
use auto_update::AutoUpdateStatus;
use call::{ActiveCall, ParticipantLocation, Room};
use client::{proto::PeerId, Client, ContactEventKind, SignIn, SignOut, User, UserStore};
use clock::ReplicaId;
@@ -1177,22 +1178,38 @@ impl CollabTitlebarItem {
.with_style(theme.titlebar.offline_icon.container)
.into_any(),
),
client::Status::UpgradeRequired => Some(
MouseEventHandler::new::<ConnectionStatusButton, _>(0, cx, |_, _| {
Label::new(
"Please update Zed to collaborate",
theme.titlebar.outdated_warning.text.clone(),
)
.contained()
.with_style(theme.titlebar.outdated_warning.container)
.aligned()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, _, cx| {
auto_update::check(&Default::default(), cx);
})
.into_any(),
),
client::Status::UpgradeRequired => {
let auto_updater = auto_update::AutoUpdater::get(cx);
let label = match auto_updater.map(|auto_update| auto_update.read(cx).status()) {
Some(AutoUpdateStatus::Updated) => "Please restart Zed to Collaborate",
Some(AutoUpdateStatus::Installing)
| Some(AutoUpdateStatus::Downloading)
| Some(AutoUpdateStatus::Checking) => "Updating...",
Some(AutoUpdateStatus::Idle) | Some(AutoUpdateStatus::Errored) | None => {
"Please update Zed to Collaborate"
}
};
Some(
MouseEventHandler::new::<ConnectionStatusButton, _>(0, cx, |_, _| {
Label::new(label, theme.titlebar.outdated_warning.text.clone())
.contained()
.with_style(theme.titlebar.outdated_warning.container)
.aligned()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, _, cx| {
if let Some(auto_updater) = auto_update::AutoUpdater::get(cx) {
if auto_updater.read(cx).status() == AutoUpdateStatus::Updated {
workspace::restart(&Default::default(), cx);
return;
}
}
auto_update::check(&Default::default(), cx);
})
.into_any(),
)
}
_ => None,
}
}
+16 -29
View File
@@ -1333,7 +1333,7 @@ async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppCon
cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx));
cx.assert_editor_state(
&r#"ˇone
&r#"one
two
three
@@ -1344,9 +1344,22 @@ async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppCon
.unindent(),
);
cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx));
cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
cx.assert_editor_state(
&r#"ˇone
&r#"one
two
three
four
five
ˇ
six"#
.unindent(),
);
cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
cx.assert_editor_state(
&r#"one
two
ˇ
three
@@ -1366,32 +1379,6 @@ async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppCon
four
five
sixˇ"#
.unindent(),
);
cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
cx.assert_editor_state(
&r#"one
two
three
four
five
ˇ
sixˇ"#
.unindent(),
);
cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
cx.assert_editor_state(
&r#"one
two
ˇ
three
four
five
ˇ
six"#
.unindent(),
);
+2 -2
View File
@@ -234,7 +234,7 @@ pub fn start_of_paragraph(
) -> DisplayPoint {
let point = display_point.to_point(map);
if point.row == 0 {
return map.max_point();
return DisplayPoint::zero();
}
let mut found_non_blank_line = false;
@@ -261,7 +261,7 @@ pub fn end_of_paragraph(
) -> DisplayPoint {
let point = display_point.to_point(map);
if point.row == map.max_buffer_row() {
return DisplayPoint::zero();
return map.max_point();
}
let mut found_non_blank_line = false;
+226 -11
View File
@@ -107,13 +107,23 @@ fn matching_history_item_paths(
) -> HashMap<Arc<Path>, PathMatch> {
let history_items_by_worktrees = history_items
.iter()
.map(|found_path| {
let path = &found_path.project.path;
.filter_map(|found_path| {
let candidate = PathMatchCandidate {
path,
char_bag: CharBag::from_iter(path.to_string_lossy().to_lowercase().chars()),
path: &found_path.project.path,
// Only match history items names, otherwise their paths may match too many queries, producing false positives.
// E.g. `foo` would match both `something/foo/bar.rs` and `something/foo/foo.rs` and if the former is a history item,
// it would be shown first always, despite the latter being a better match.
char_bag: CharBag::from_iter(
found_path
.project
.path
.file_name()?
.to_string_lossy()
.to_lowercase()
.chars(),
),
};
(found_path.project.worktree_id, candidate)
Some((found_path.project.worktree_id, candidate))
})
.fold(
HashMap::default(),
@@ -212,6 +222,10 @@ fn toggle_or_cycle_file_finder(
.as_ref()
.and_then(|found_path| found_path.absolute.as_ref())
})
.filter(|(_, history_abs_path)| match history_abs_path {
Some(abs_path) => history_file_exists(abs_path),
None => true,
})
.map(|(history_path, abs_path)| FoundPath::new(history_path, abs_path)),
)
.collect();
@@ -236,6 +250,16 @@ fn toggle_or_cycle_file_finder(
}
}
#[cfg(not(test))]
fn history_file_exists(abs_path: &PathBuf) -> bool {
abs_path.exists()
}
#[cfg(test)]
fn history_file_exists(abs_path: &PathBuf) -> bool {
!abs_path.ends_with("nonexistent.rs")
}
pub enum Event {
Selected(ProjectPath),
Dismissed,
@@ -505,12 +529,7 @@ impl PickerDelegate for FileFinderDelegate {
project
.worktree_for_id(history_item.project.worktree_id, cx)
.is_some()
|| (project.is_local()
&& history_item
.absolute
.as_ref()
.filter(|abs_path| abs_path.exists())
.is_some())
|| (project.is_local() && history_item.absolute.is_some())
})
.cloned()
.map(|p| (p, None))
@@ -1803,6 +1822,202 @@ mod tests {
});
}
#[gpui::test]
async fn test_history_items_vs_very_good_external_match(
deterministic: Arc<gpui::executor::Deterministic>,
cx: &mut gpui::TestAppContext,
) {
let app_state = init_test(cx);
app_state
.fs
.as_fake()
.insert_tree(
"/src",
json!({
"collab_ui": {
"first.rs": "// First Rust file",
"second.rs": "// Second Rust file",
"third.rs": "// Third Rust file",
"collab_ui.rs": "// Fourth Rust file",
}
}),
)
.await;
let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
let window = cx.add_window(|cx| Workspace::test_new(project, cx));
let workspace = window.root(cx);
// generate some history to select from
open_close_queried_buffer(
"fir",
1,
"first.rs",
window.into(),
&workspace,
&deterministic,
cx,
)
.await;
open_close_queried_buffer(
"sec",
1,
"second.rs",
window.into(),
&workspace,
&deterministic,
cx,
)
.await;
open_close_queried_buffer(
"thi",
1,
"third.rs",
window.into(),
&workspace,
&deterministic,
cx,
)
.await;
open_close_queried_buffer(
"sec",
1,
"second.rs",
window.into(),
&workspace,
&deterministic,
cx,
)
.await;
cx.dispatch_action(window.into(), Toggle);
let query = "collab_ui";
let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
finder
.update(cx, |finder, cx| {
finder.delegate_mut().update_matches(query.to_string(), cx)
})
.await;
finder.read_with(cx, |finder, _| {
let delegate = finder.delegate();
assert!(
delegate.matches.history.is_empty(),
"History items should not math query {query}, they should be matched by name only"
);
let search_entries = delegate
.matches
.search
.iter()
.map(|path_match| path_match.path.to_path_buf())
.collect::<Vec<_>>();
assert_eq!(
search_entries,
vec![
PathBuf::from("collab_ui/collab_ui.rs"),
PathBuf::from("collab_ui/third.rs"),
PathBuf::from("collab_ui/first.rs"),
PathBuf::from("collab_ui/second.rs"),
],
"Despite all search results having the same directory name, the most matching one should be on top"
);
});
}
#[gpui::test]
async fn test_nonexistent_history_items_not_shown(
deterministic: Arc<gpui::executor::Deterministic>,
cx: &mut gpui::TestAppContext,
) {
let app_state = init_test(cx);
app_state
.fs
.as_fake()
.insert_tree(
"/src",
json!({
"test": {
"first.rs": "// First Rust file",
"nonexistent.rs": "// Second Rust file",
"third.rs": "// Third Rust file",
}
}),
)
.await;
let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
let window = cx.add_window(|cx| Workspace::test_new(project, cx));
let workspace = window.root(cx);
// generate some history to select from
open_close_queried_buffer(
"fir",
1,
"first.rs",
window.into(),
&workspace,
&deterministic,
cx,
)
.await;
open_close_queried_buffer(
"non",
1,
"nonexistent.rs",
window.into(),
&workspace,
&deterministic,
cx,
)
.await;
open_close_queried_buffer(
"thi",
1,
"third.rs",
window.into(),
&workspace,
&deterministic,
cx,
)
.await;
open_close_queried_buffer(
"fir",
1,
"first.rs",
window.into(),
&workspace,
&deterministic,
cx,
)
.await;
cx.dispatch_action(window.into(), Toggle);
let query = "rs";
let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
finder
.update(cx, |finder, cx| {
finder.delegate_mut().update_matches(query.to_string(), cx)
})
.await;
finder.read_with(cx, |finder, _| {
let delegate = finder.delegate();
let history_entries = delegate
.matches
.history
.iter()
.map(|(_, path_match)| path_match.as_ref().expect("should have a path match").path.to_path_buf())
.collect::<Vec<_>>();
assert_eq!(
history_entries,
vec![
PathBuf::from("test/first.rs"),
PathBuf::from("test/third.rs"),
],
"Should have all opened files in the history, except the ones that do not exist on disk"
);
});
}
async fn open_close_queried_buffer(
input: &str,
expected_matches: usize,
-1
View File
@@ -13,7 +13,6 @@ rope = { path = "../rope" }
text = { path = "../text" }
util = { path = "../util" }
sum_tree = { path = "../sum_tree" }
rpc = { path = "../rpc" }
anyhow.workspace = true
async-trait.workspace = true
+1 -19
View File
@@ -2,7 +2,6 @@ use anyhow::Result;
use collections::HashMap;
use git2::{BranchType, StatusShow};
use parking_lot::Mutex;
use rpc::proto;
use serde_derive::{Deserialize, Serialize};
use std::{
cmp::Ordering,
@@ -23,6 +22,7 @@ pub struct Branch {
/// Timestamp of most recent commit, normalized to Unix Epoch format.
pub unix_timestamp: Option<i64>,
}
#[async_trait::async_trait]
pub trait GitRepository: Send {
fn reload_index(&self);
@@ -358,24 +358,6 @@ impl GitFileStatus {
}
}
}
pub fn from_proto(git_status: Option<i32>) -> Option<GitFileStatus> {
git_status.and_then(|status| {
proto::GitStatus::from_i32(status).map(|status| match status {
proto::GitStatus::Added => GitFileStatus::Added,
proto::GitStatus::Modified => GitFileStatus::Modified,
proto::GitStatus::Conflict => GitFileStatus::Conflict,
})
})
}
pub fn to_proto(self) -> i32 {
match self {
GitFileStatus::Added => proto::GitStatus::Added as i32,
GitFileStatus::Modified => proto::GitStatus::Modified as i32,
GitFileStatus::Conflict => proto::GitStatus::Conflict as i32,
}
}
}
#[derive(Clone, Debug, Ord, Hash, PartialOrd, Eq, PartialEq)]
+1 -1
View File
@@ -441,7 +441,7 @@ mod tests {
score,
worktree_id: 0,
positions: Vec::new(),
path: candidate.path.clone(),
path: Arc::from(candidate.path),
path_prefix: "".into(),
distance_to_relative_ancestor: usize::MAX,
},
+3 -3
View File
@@ -14,7 +14,7 @@ use crate::{
#[derive(Clone, Debug)]
pub struct PathMatchCandidate<'a> {
pub path: &'a Arc<Path>,
pub path: &'a Path,
pub char_bag: CharBag,
}
@@ -120,7 +120,7 @@ pub fn match_fixed_path_set(
score,
worktree_id,
positions: Vec::new(),
path: candidate.path.clone(),
path: Arc::from(candidate.path),
path_prefix: Arc::from(""),
distance_to_relative_ancestor: usize::MAX,
},
@@ -195,7 +195,7 @@ pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>(
score,
worktree_id,
positions: Vec::new(),
path: candidate.path.clone(),
path: Arc::from(candidate.path),
path_prefix: candidate_set.prefix(),
distance_to_relative_ancestor: relative_to.as_ref().map_or(
usize::MAX,
+24
View File
@@ -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);
-1
View File
@@ -22,7 +22,6 @@ test-support = [
]
[dependencies]
client = { path = "../client" }
clock = { path = "../clock" }
collections = { path = "../collections" }
fuzzy = { path = "../fuzzy" }
+2 -3
View File
@@ -91,9 +91,8 @@ impl TestServer {
let identity = claims.sub.unwrap().to_string();
let room_name = claims.video.room.unwrap();
let mut server_rooms = self.rooms.lock();
let room = server_rooms
.get_mut(&*room_name)
.ok_or_else(|| anyhow!("room {:?} does not exist", room_name))?;
let room = (*server_rooms).entry(room_name.to_string()).or_default();
if room.client_rooms.contains_key(&identity) {
Err(anyhow!(
"{:?} attempted to join room {:?} twice",
+20 -2
View File
@@ -4310,7 +4310,7 @@ impl<'a> From<&'a Entry> for proto::Entry {
is_symlink: entry.is_symlink,
is_ignored: entry.is_ignored,
is_external: entry.is_external,
git_status: entry.git_status.map(|status| status.to_proto()),
git_status: entry.git_status.map(git_status_to_proto),
}
}
}
@@ -4337,7 +4337,7 @@ impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry {
is_symlink: entry.is_symlink,
is_ignored: entry.is_ignored,
is_external: entry.is_external,
git_status: GitFileStatus::from_proto(entry.git_status),
git_status: git_status_from_proto(entry.git_status),
})
} else {
Err(anyhow!(
@@ -4366,3 +4366,21 @@ fn combine_git_statuses(
unstaged
}
}
fn git_status_from_proto(git_status: Option<i32>) -> Option<GitFileStatus> {
git_status.and_then(|status| {
proto::GitStatus::from_i32(status).map(|status| match status {
proto::GitStatus::Added => GitFileStatus::Added,
proto::GitStatus::Modified => GitFileStatus::Modified,
proto::GitStatus::Conflict => GitFileStatus::Conflict,
})
})
}
fn git_status_to_proto(status: GitFileStatus) -> i32 {
match status {
GitFileStatus::Added => proto::GitStatus::Added as i32,
GitFileStatus::Modified => proto::GitStatus::Modified as i32,
GitFileStatus::Conflict => proto::GitStatus::Conflict as i32,
}
}
+32
View File
@@ -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 -1
View File
@@ -139,6 +139,12 @@ impl<P> PathLikeWithPosition<P> {
column: None,
})
} else {
let maybe_col_str =
if maybe_col_str.ends_with(FILE_ROW_COLUMN_DELIMITER) {
&maybe_col_str[..maybe_col_str.len() - 1]
} else {
maybe_col_str
};
match maybe_col_str.parse::<u32>() {
Ok(col) => Ok(Self {
path_like: parse_path_like_str(path_like_str)?,
@@ -241,7 +247,6 @@ mod tests {
"test_file.rs:1::",
"test_file.rs::1:2",
"test_file.rs:1::2",
"test_file.rs:1:2:",
"test_file.rs:1:2:3",
] {
let actual = parse_str(input);
@@ -277,6 +282,14 @@ mod tests {
column: None,
},
),
(
"crates/file_finder/src/file_finder.rs:1902:13:",
PathLikeWithPosition {
path_like: "crates/file_finder/src/file_finder.rs".to_string(),
row: Some(1902),
column: Some(13),
},
),
];
for (input, expected) in input_and_expected {
+9
View File
@@ -46,6 +46,7 @@ actions!(
ChangeToEndOfLine,
DeleteToEndOfLine,
Yank,
YankLine,
ChangeCase,
JoinLines,
]
@@ -66,6 +67,7 @@ pub fn init(cx: &mut AppContext) {
cx.add_action(insert_line_above);
cx.add_action(insert_line_below);
cx.add_action(change_case);
cx.add_action(yank_line);
cx.add_action(|_: &mut Workspace, _: &DeleteLeft, cx| {
Vim::update(cx, |vim, cx| {
@@ -308,6 +310,13 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex
});
}
fn yank_line(_: &mut Workspace, _: &YankLine, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| {
let count = vim.take_count(cx);
yank_motion(vim, motion::Motion::CurrentLine, count, cx)
})
}
pub(crate) fn normal_replace(text: Arc<str>, cx: &mut WindowContext) {
Vim::update(cx, |vim, cx| {
vim.stop_recording();
+25
View File
@@ -652,3 +652,28 @@ async fn test_selection_goal(cx: &mut gpui::TestAppContext) {
Lorem Ipsum"})
.await;
}
#[gpui::test]
async fn test_paragraphs_dont_wrap(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.set_shared_state(indoc! {"
one
ˇ
two"})
.await;
cx.simulate_shared_keystrokes(["}", "}"]).await;
cx.assert_shared_state(indoc! {"
one
twˇo"})
.await;
cx.simulate_shared_keystrokes(["{", "{", "{"]).await;
cx.assert_shared_state(indoc! {"
ˇone
two"})
.await;
}
+1 -1
View File
@@ -33,7 +33,7 @@ use workspace::{self, Workspace};
use crate::state::ReplayableAction;
struct VimModeSetting(bool);
pub struct VimModeSetting(pub bool);
#[derive(Clone, Deserialize, PartialEq)]
pub struct SwitchMode(pub Mode);
@@ -0,0 +1,8 @@
{"Put":{"state":"one\nˇ\ntwo"}}
{"Key":"}"}
{"Key":"}"}
{"Get":{"state":"one\n\ntwˇo","mode":"Normal"}}
{"Key":"{"}
{"Key":"{"}
{"Key":"{"}
{"Get":{"state":"ˇone\n\ntwo","mode":"Normal"}}
+1
View File
@@ -25,6 +25,7 @@ theme_selector = { path = "../theme_selector" }
util = { path = "../util" }
picker = { path = "../picker" }
workspace = { path = "../workspace" }
vim = { path = "../vim" }
anyhow.workspace = true
log.workspace = true
+24 -1
View File
@@ -10,6 +10,7 @@ use gpui::{
};
use settings::{update_settings_file, SettingsStore};
use std::{borrow::Cow, sync::Arc};
use vim::VimModeSetting;
use workspace::{
dock::DockPosition, item::Item, open_new, AppState, PaneBackdrop, Welcome, Workspace,
WorkspaceId,
@@ -65,6 +66,7 @@ impl View for WelcomePage {
let width = theme.welcome.page_width;
let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
let vim_mode_setting = settings::get::<VimModeSetting>(cx).0;
enum Metrics {}
enum Diagnostics {}
@@ -144,6 +146,27 @@ impl View for WelcomePage {
)
.with_child(
Flex::column()
.with_child(
theme::ui::checkbox::<Diagnostics, Self, _>(
"Enable vim mode",
&theme.welcome.checkbox,
vim_mode_setting,
0,
cx,
|this, checked, cx| {
if let Some(workspace) = this.workspace.upgrade(cx) {
let fs = workspace.read(cx).app_state().fs.clone();
update_settings_file::<VimModeSetting>(
fs,
cx,
move |setting| *setting = Some(checked),
)
}
},
)
.contained()
.with_style(theme.welcome.checkbox_container),
)
.with_child(
theme::ui::checkbox_with_label::<Metrics, _, Self, _>(
Flex::column()
@@ -186,7 +209,7 @@ impl View for WelcomePage {
"Send crash reports",
&theme.welcome.checkbox,
telemetry_settings.diagnostics,
0,
1,
cx,
|this, checked, cx| {
if let Some(workspace) = this.workspace.upgrade(cx) {
-1
View File
@@ -22,7 +22,6 @@ test-support = [
db = { path = "../db" }
call = { path = "../call" }
client = { path = "../client" }
channel = { path = "../channel" }
collections = { path = "../collections" }
context_menu = { path = "../context_menu" }
drag_and_drop = { path = "../drag_and_drop" }
+11 -22
View File
@@ -1,10 +1,7 @@
use std::{cell::RefCell, rc::Rc, sync::Arc};
use crate::{
pane_group::element::PaneAxisElement, AppState, FollowerStatesByLeader, Pane, Workspace,
};
use crate::{pane_group::element::PaneAxisElement, AppState, FollowerState, Pane, Workspace};
use anyhow::{anyhow, Result};
use call::{ActiveCall, ParticipantLocation};
use collections::HashMap;
use gpui::{
elements::*,
geometry::{rect::RectF, vector::Vector2F},
@@ -13,6 +10,7 @@ use gpui::{
};
use project::Project;
use serde::Deserialize;
use std::{cell::RefCell, rc::Rc, sync::Arc};
use theme::Theme;
const HANDLE_HITBOX_SIZE: f32 = 4.0;
@@ -95,7 +93,7 @@ impl PaneGroup {
&self,
project: &ModelHandle<Project>,
theme: &Theme,
follower_states: &FollowerStatesByLeader,
follower_states: &HashMap<ViewHandle<Pane>, FollowerState>,
active_call: Option<&ModelHandle<ActiveCall>>,
active_pane: &ViewHandle<Pane>,
zoomed: Option<&AnyViewHandle>,
@@ -162,7 +160,7 @@ impl Member {
project: &ModelHandle<Project>,
basis: usize,
theme: &Theme,
follower_states: &FollowerStatesByLeader,
follower_states: &HashMap<ViewHandle<Pane>, FollowerState>,
active_call: Option<&ModelHandle<ActiveCall>>,
active_pane: &ViewHandle<Pane>,
zoomed: Option<&AnyViewHandle>,
@@ -179,19 +177,10 @@ impl Member {
ChildView::new(pane, cx).into_any()
};
let leader = follower_states
.iter()
.find_map(|(leader_id, follower_states)| {
if follower_states.contains_key(pane) {
Some(leader_id)
} else {
None
}
})
.and_then(|leader_id| {
let room = active_call?.read(cx).room()?.read(cx);
room.remote_participant_for_peer_id(*leader_id)
});
let leader = follower_states.get(pane).and_then(|state| {
let room = active_call?.read(cx).room()?.read(cx);
room.remote_participant_for_peer_id(state.leader_id)
});
let mut leader_border = Border::default();
let mut leader_status_box = None;
@@ -486,7 +475,7 @@ impl PaneAxis {
project: &ModelHandle<Project>,
basis: usize,
theme: &Theme,
follower_state: &FollowerStatesByLeader,
follower_states: &HashMap<ViewHandle<Pane>, FollowerState>,
active_call: Option<&ModelHandle<ActiveCall>>,
active_pane: &ViewHandle<Pane>,
zoomed: Option<&AnyViewHandle>,
@@ -515,7 +504,7 @@ impl PaneAxis {
project,
(basis + ix) * 10,
theme,
follower_state,
follower_states,
active_call,
active_pane,
zoomed,
+263 -91
View File
@@ -12,10 +12,9 @@ mod workspace_settings;
use anyhow::{anyhow, Context, Result};
use call::ActiveCall;
use channel::ChannelStore;
use client::{
proto::{self, PeerId},
Client, TypedEnvelope, UserStore,
Client, Status, TypedEnvelope, UserStore,
};
use collections::{hash_map, HashMap, HashSet};
use drag_and_drop::DragAndDrop;
@@ -36,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;
@@ -450,7 +449,6 @@ pub struct AppState {
pub languages: Arc<LanguageRegistry>,
pub client: Arc<Client>,
pub user_store: ModelHandle<UserStore>,
pub channel_store: ModelHandle<ChannelStore>,
pub workspace_store: ModelHandle<WorkspaceStore>,
pub fs: Arc<dyn fs::Fs>,
pub build_window_options:
@@ -487,8 +485,6 @@ impl AppState {
let http_client = util::http::FakeHttpClient::with_404_response();
let client = Client::new(http_client.clone(), cx);
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
let channel_store =
cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx));
theme::init((), cx);
@@ -500,7 +496,7 @@ impl AppState {
fs,
languages,
user_store,
channel_store,
// channel_store,
workspace_store,
initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
build_window_options: |_, _, _| Default::default(),
@@ -578,7 +574,7 @@ pub struct Workspace {
titlebar_item: Option<AnyViewHandle>,
notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
project: ModelHandle<Project>,
follower_states_by_leader: FollowerStatesByLeader,
follower_states: HashMap<ViewHandle<Pane>, FollowerState>,
last_leaders_by_pane: HashMap<WeakViewHandle<Pane>, PeerId>,
window_edited: bool,
active_call: Option<(ModelHandle<ActiveCall>, Vec<Subscription>)>,
@@ -603,10 +599,9 @@ pub struct ViewId {
pub id: u64,
}
type FollowerStatesByLeader = HashMap<PeerId, HashMap<ViewHandle<Pane>, FollowerState>>;
#[derive(Default)]
struct FollowerState {
leader_id: PeerId,
active_view_id: Option<ViewId>,
items_by_leader_view_id: HashMap<ViewId, Box<dyn FollowableItemHandle>>,
}
@@ -795,7 +790,7 @@ impl Workspace {
bottom_dock,
right_dock,
project: project.clone(),
follower_states_by_leader: Default::default(),
follower_states: Default::default(),
last_leaders_by_pane: Default::default(),
window_edited: false,
active_call,
@@ -2513,13 +2508,16 @@ impl Workspace {
}
fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
if let Some(states_by_pane) = self.follower_states_by_leader.remove(&peer_id) {
for state in states_by_pane.into_values() {
for item in state.items_by_leader_view_id.into_values() {
self.follower_states.retain(|_, state| {
if state.leader_id == peer_id {
for item in state.items_by_leader_view_id.values() {
item.set_leader_peer_id(None, cx);
}
false
} else {
true
}
}
});
cx.notify();
}
@@ -2532,10 +2530,15 @@ impl Workspace {
self.last_leaders_by_pane
.insert(pane.downgrade(), leader_id);
self.follower_states_by_leader
.entry(leader_id)
.or_default()
.insert(pane.clone(), Default::default());
self.unfollow(&pane, cx);
self.follower_states.insert(
pane.clone(),
FollowerState {
leader_id,
active_view_id: None,
items_by_leader_view_id: Default::default(),
},
);
cx.notify();
let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
@@ -2550,9 +2553,8 @@ impl Workspace {
let response = request.await?;
this.update(&mut cx, |this, _| {
let state = this
.follower_states_by_leader
.get_mut(&leader_id)
.and_then(|states_by_pane| states_by_pane.get_mut(&pane))
.follower_states
.get_mut(&pane)
.ok_or_else(|| anyhow!("following interrupted"))?;
state.active_view_id = if let Some(active_view_id) = response.active_view_id {
Some(ViewId::from_proto(active_view_id)?)
@@ -2647,12 +2649,10 @@ impl Workspace {
}
// if you're already following, find the right pane and focus it.
for (existing_leader_id, states_by_pane) in &mut self.follower_states_by_leader {
if leader_id == *existing_leader_id {
for (pane, _) in states_by_pane {
cx.focus(pane);
return None;
}
for (pane, state) in &self.follower_states {
if leader_id == state.leader_id {
cx.focus(pane);
return None;
}
}
@@ -2665,36 +2665,37 @@ impl Workspace {
pane: &ViewHandle<Pane>,
cx: &mut ViewContext<Self>,
) -> Option<PeerId> {
for (leader_id, states_by_pane) in &mut self.follower_states_by_leader {
let leader_id = *leader_id;
if let Some(state) = states_by_pane.remove(pane) {
for (_, item) in state.items_by_leader_view_id {
item.set_leader_peer_id(None, cx);
}
if states_by_pane.is_empty() {
self.follower_states_by_leader.remove(&leader_id);
let project_id = self.project.read(cx).remote_id();
let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
self.app_state
.client
.send(proto::Unfollow {
room_id,
project_id,
leader_id: Some(leader_id),
})
.log_err();
}
cx.notify();
return Some(leader_id);
}
let state = self.follower_states.remove(pane)?;
let leader_id = state.leader_id;
for (_, item) in state.items_by_leader_view_id {
item.set_leader_peer_id(None, cx);
}
None
if self
.follower_states
.values()
.all(|state| state.leader_id != state.leader_id)
{
let project_id = self.project.read(cx).remote_id();
let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
self.app_state
.client
.send(proto::Unfollow {
room_id,
project_id,
leader_id: Some(leader_id),
})
.log_err();
}
cx.notify();
Some(leader_id)
}
pub fn is_being_followed(&self, peer_id: PeerId) -> bool {
self.follower_states_by_leader.contains_key(&peer_id)
self.follower_states
.values()
.any(|state| state.leader_id == peer_id)
}
fn render_titlebar(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
@@ -2877,8 +2878,7 @@ impl Workspace {
let cx = &cx;
move |item| {
let item = item.to_followable_item_handle(cx)?;
if project_id.is_some()
&& project_id != follower_project_id
if (project_id.is_none() || project_id != follower_project_id)
&& item.is_project_item(cx)
{
return None;
@@ -2917,8 +2917,8 @@ impl Workspace {
match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
this.update(cx, |this, _| {
if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
for state in state.values_mut() {
for (_, state) in &mut this.follower_states {
if state.leader_id == leader_id {
state.active_view_id =
if let Some(active_view_id) = update_active_view.id.clone() {
Some(ViewId::from_proto(active_view_id)?)
@@ -2940,8 +2940,8 @@ impl Workspace {
let mut tasks = Vec::new();
this.update(cx, |this, cx| {
let project = this.project.clone();
if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
for state in state.values_mut() {
for (_, state) in &mut this.follower_states {
if state.leader_id == leader_id {
let view_id = ViewId::from_proto(id.clone())?;
if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
tasks.push(item.apply_update_proto(&project, variant.clone(), cx));
@@ -2954,10 +2954,9 @@ impl Workspace {
}
proto::update_followers::Variant::CreateView(view) => {
let panes = this.read_with(cx, |this, _| {
this.follower_states_by_leader
.get(&leader_id)
.into_iter()
.flat_map(|states_by_pane| states_by_pane.keys())
this.follower_states
.iter()
.filter_map(|(pane, state)| (state.leader_id == leader_id).then_some(pane))
.cloned()
.collect()
})?;
@@ -3016,11 +3015,7 @@ impl Workspace {
for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane {
let items = futures::future::try_join_all(item_tasks).await?;
this.update(cx, |this, cx| {
let state = this
.follower_states_by_leader
.get_mut(&leader_id)?
.get_mut(&pane)?;
let state = this.follower_states.get_mut(&pane)?;
for (id, item) in leader_view_ids.into_iter().zip(items) {
item.set_leader_peer_id(Some(leader_id), cx);
state.items_by_leader_view_id.insert(id, item);
@@ -3077,15 +3072,7 @@ impl Workspace {
}
pub fn leader_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<PeerId> {
self.follower_states_by_leader
.iter()
.find_map(|(leader_id, state)| {
if state.contains_key(pane) {
Some(*leader_id)
} else {
None
}
})
self.follower_states.get(pane).map(|state| state.leader_id)
}
fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
@@ -3113,17 +3100,23 @@ impl Workspace {
}
};
for (pane, state) in self.follower_states_by_leader.get(&leader_id)? {
if leader_in_this_app {
let item = state
.active_view_id
.and_then(|id| state.items_by_leader_view_id.get(&id));
if let Some(item) = item {
for (pane, state) in &self.follower_states {
if state.leader_id != leader_id {
continue;
}
if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) {
if let Some(item) = state.items_by_leader_view_id.get(&active_view_id) {
if leader_in_this_project || !item.is_project_item(cx) {
items_to_activate.push((pane.clone(), item.boxed_clone()));
}
continue;
} else {
log::warn!(
"unknown view id {:?} for leader {:?}",
active_view_id,
leader_id
);
}
continue;
}
if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
items_to_activate.push((pane.clone(), Box::new(shared_screen)));
@@ -3527,15 +3520,12 @@ impl Workspace {
let client = project.read(cx).client();
let user_store = project.read(cx).user_store();
let channel_store =
cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx));
let app_state = Arc::new(AppState {
languages: project.read(cx).languages().clone(),
workspace_store,
client,
user_store,
channel_store,
fs: project.read(cx).fs().clone(),
build_window_options: |_, _, _| Default::default(),
initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
@@ -3811,7 +3801,7 @@ impl View for Workspace {
self.center.render(
&project,
&theme,
&self.follower_states_by_leader,
&self.follower_states,
self.active_call(),
self.active_pane(),
self.zoomed
@@ -4148,6 +4138,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],
+4 -1
View File
@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
description = "The fast, collaborative code editor."
edition = "2021"
name = "zed"
version = "0.108.0"
version = "0.109.0"
publish = false
[lib]
@@ -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"]
+104 -123
View File
@@ -3,12 +3,13 @@
use anyhow::{anyhow, Context, Result};
use backtrace::Backtrace;
use channel::ChannelStore;
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::{
@@ -32,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,
@@ -45,7 +44,7 @@ use std::{
};
use sum_tree::Bias;
use util::{
channel::ReleaseChannel,
channel::{parse_zed_link, ReleaseChannel},
http::{self, HttpClient},
paths::PathLikeWithPosition,
};
@@ -61,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();
@@ -93,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);
@@ -138,8 +132,6 @@ fn main() {
languages::init(languages.clone(), node_runtime.clone(), cx);
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx));
let channel_store =
cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx));
cx.set_global(client.clone());
@@ -156,7 +148,7 @@ fn main() {
outline::init(cx);
project_symbols::init(cx);
project_panel::init(Assets, cx);
channel::init(&client);
channel::init(&client, user_store.clone(), cx);
diagnostics::init(cx);
search::init(cx);
semantic_index::init(fs.clone(), http.clone(), languages.clone(), cx);
@@ -184,7 +176,6 @@ fn main() {
languages,
client: client.clone(),
user_store,
channel_store,
fs,
build_window_options,
initialize_workspace,
@@ -214,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);
@@ -227,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";
@@ -298,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))
@@ -495,11 +476,11 @@ fn init_panic_hook(app: &App, installation_id: Option<String>, session_id: Strin
session_id: session_id.clone(),
};
if is_pty {
if let Some(panic_data_json) = serde_json::to_string_pretty(&panic_data).log_err() {
eprintln!("{}", panic_data_json);
}
} else {
if let Some(panic_data_json) = serde_json::to_string_pretty(&panic_data).log_err() {
log::error!("{}", panic_data_json);
}
if !is_pty {
if let Some(panic_data_json) = serde_json::to_string(&panic_data).log_err() {
let timestamp = chrono::Utc::now().format("%Y_%m_%d %H_%M_%S").to_string();
let panic_file_path = paths::LOGS_DIR.join(format!("zed-{}.panic", timestamp));
@@ -638,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());
+98
View File
@@ -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 })
}
}
+1
View File
@@ -2424,6 +2424,7 @@ mod tests {
state.build_window_options = build_window_options;
theme::init((), cx);
audio::init((), cx);
channel::init(&app_state.client, app_state.user_store.clone(), cx);
call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
workspace::init(app_state.clone(), cx);
Project::init_settings(cx);