diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 2cd2050013f7639ff3d3a4ea10379584f0e5f387..147402b2858dce92355d91e9bd11e20cb43293fc 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -2,11 +2,4 @@ Release Notes: -- N/A - -or - - (Added|Fixed|Improved) ... ([#](https://github.com/zed-industries/community/issues/)). - -If the release notes are only intended for a specific release channel only, add `(-only)` to the end of the release note line. -These will be removed by the person making the release. diff --git a/.github/workflows/release_actions.yml b/.github/workflows/release_actions.yml index f767324e4f6e2bc2edc825af512bbb5561ac6b62..c1df24a8e5cb98bdbea594fecc0fd4090e222877 100644 --- a/.github/workflows/release_actions.yml +++ b/.github/workflows/release_actions.yml @@ -6,8 +6,8 @@ jobs: discord_release: runs-on: ubuntu-latest steps: - - name: Get appropriate URL - id: get-appropriate-url + - name: Get release URL + id: get-release-url run: | if [ "${{ github.event.release.prerelease }}" == "true" ]; then URL="https://zed.dev/releases/preview/latest" @@ -15,14 +15,19 @@ jobs: URL="https://zed.dev/releases/stable/latest" fi echo "::set-output name=URL::$URL" - - - name: Discord Webhook Action - uses: tsickert/discord-webhook@v5.3.0 + - name: Get content + uses: 2428392/gh-truncate-string-action@v1.2.0 + id: get-content with: - webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }} - content: | + stringToTruncate: | 📣 Zed ${{ github.event.release.tag_name }} was just released! - Restart your Zed or head to ${{ steps.get-appropriate-url.outputs.URL }} to grab it. + Restart your Zed or head to ${{ steps.get-release-url.outputs.URL }} to grab it. ${{ github.event.release.body }} + maxLength: 2000 + - name: Discord Webhook Action + uses: tsickert/discord-webhook@v5.3.0 + with: + webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }} + content: ${{ steps.get-content.outputs.string }} diff --git a/Cargo.lock b/Cargo.lock index f5452ac2e0e4e565d7949d26aca2e249e70d8441..42f01b0eb1fc30db8ed182f14c1cb39f4b5f4f50 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1093,7 +1093,6 @@ dependencies = [ "anyhow", "async-broadcast", "audio", - "channel", "client", "collections", "fs", @@ -1497,7 +1496,7 @@ dependencies = [ [[package]] name = "collab" -version = "0.23.3" +version = "0.24.0" dependencies = [ "anyhow", "async-trait", @@ -2109,9 +2108,9 @@ dependencies = [ [[package]] name = "curl-sys" -version = "0.4.66+curl-8.3.0" +version = "0.4.67+curl-8.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70c44a72e830f0e40ad90dda8a6ab6ed6314d39776599a58a2e5e37fbc6db5b9" +checksum = "3cc35d066510b197a0f72de863736641539957628c8a42e70e27c66849e77c34" dependencies = [ "cc", "libc", @@ -2862,7 +2861,6 @@ dependencies = [ "parking_lot 0.11.2", "regex", "rope", - "rpc", "serde", "serde_derive", "serde_json", @@ -9832,6 +9830,7 @@ dependencies = [ "theme", "theme_selector", "util", + "vim", "workspace", ] @@ -10138,7 +10137,6 @@ dependencies = [ "async-recursion 1.0.5", "bincode", "call", - "channel", "client", "collections", "context_menu", @@ -10250,7 +10248,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.108.0" +version = "0.109.0" dependencies = [ "activity_indicator", "anyhow", diff --git a/README.md b/README.md index b3d4987526a46be3304ca649d90166566c03029e..eed8dd4d91c249dfa4de57f47d79c6ad1e2749e9 100644 --- a/README.md +++ b/README.md @@ -83,9 +83,7 @@ foreman start If you want to run Zed pointed at the local servers, you can run: ``` -script/zed-with-local-servers -# or... -script/zed-with-local-servers --release +script/zed-local ``` ### Dump element JSON diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 07ba8a121f3ee45132c4354c7a072338273b2692..ea025747d8da99128a3d465d46fff5a26e7fb7e3 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -408,6 +408,7 @@ "vim::PushOperator", "Yank" ], + "shift-y": "vim::YankLine", "i": "vim::InsertBefore", "shift-i": "vim::InsertFirstNonWhitespace", "a": "vim::InsertAfter", diff --git a/assets/settings/default.json b/assets/settings/default.json index 8fb73a2ecb0b8143f7e42981a71966327edd0f54..cc724657c0c20411b17d0ad0ea293b0a08ddc6f5 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -76,7 +76,7 @@ // Settings related to calls in Zed "calls": { // Join calls with the microphone muted by default - "mute_on_join": true + "mute_on_join": false }, // Scrollbar related settings "scrollbar": { diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index b69c12a2a328ed8643315f091be11d764dcdc00d..b1c6038602b77465cef3f994b02cdc0635ed6776 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -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 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::(cx).is_empty() { + + // 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: selection.start, + position: snapshot.anchor_after(point_selection.start), } } else { CodegenKind::Transform { - range: selection.start..selection.end, + 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 provider = Arc::new(OpenAICompletionProvider::new( + api_key, + cx.background().clone(), + )); + 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::(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; diff --git a/crates/assistant/src/codegen.rs b/crates/assistant/src/codegen.rs index e956d722606f6db27c73385d7cf54d58bc82958b..b6ef6b5cfa7fef58936828e0f121946290bc8b48 100644 --- a/crates/assistant/src/codegen.rs +++ b/crates/assistant/src/codegen.rs @@ -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, - mut kind: CodegenKind, + kind: CodegenKind, provider: Arc, cx: &mut ModelContext, ) -> 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| { diff --git a/crates/assistant/src/prompts.rs b/crates/assistant/src/prompts.rs index bf041dff523d57d62cfbc3f312a350ad4766d160..d326a7f44547ee977095484006a2867a2546d525 100644 --- a/crates/assistant/src/prompts.rs +++ b/crates/assistant/src/prompts.rs @@ -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) -> String { #[derive(Debug)] struct Match { @@ -121,6 +122,7 @@ pub fn generate_content_prompt( range: Range, 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 { diff --git a/crates/call/Cargo.toml b/crates/call/Cargo.toml index b4e94fe56c3b12533d232eacf30c5edd633d5a03..eb448d8d8d089369c724f49e5911a8946598f8a4 100644 --- a/crates/call/Cargo.toml +++ b/crates/call/Cargo.toml @@ -20,7 +20,6 @@ test-support = [ [dependencies] audio = { path = "../audio" } -channel = { path = "../channel" } client = { path = "../client" } collections = { path = "../collections" } gpui = { path = "../gpui" } diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index d86ed1be37e38da86bb9715e187528447e5b1abc..08463413257b4b10972ad08ab7c64d181599d914 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -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 { + pub fn channel_id(&self, cx: &AppContext) -> Option { self.room()?.read(cx).channel_id() } diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index 72db174d7256b0a5686bee0210bb5c37464bc97d..4e52f57f60b36c0f83400279c2feb45ee9e2b713 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -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, follows_by_leader_id_project_id: HashMap<(PeerId, u64), Vec>, subscriptions: Vec, + room_update_completed_tx: watch::Sender>, + room_update_completed_rx: watch::Receiver>, pending_room_update: Option>, maintain_connection: 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::, 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 { + 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, diff --git a/crates/channel/src/channel.rs b/crates/channel/src/channel.rs index 160b8441ffd74f1ca835c70234fcbb166c7fa477..d31d4b3c8c9e77e94661835c06ea234c70ded416 100644 --- a/crates/channel/src/channel.rs +++ b/crates/channel/src/channel.rs @@ -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) { +pub fn init(client: &Arc, user_store: ModelHandle, cx: &mut AppContext) { + channel_store::init(client, user_store, cx); channel_buffer::init(client); channel_chat::init(client); } diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index bd72c92c7db768c558f7bc1b39a371f01f5dfd6c..2a2fa454f2b4435a806d90304940a4ce61450d09 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -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, user_store: ModelHandle, 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, } +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 { } impl ChannelStore { + pub fn global(cx: &AppContext) -> ModelHandle { + cx.global::>().clone() + } + pub fn new( client: Arc, user_store: ModelHandle, diff --git a/crates/channel/src/channel_store_tests.rs b/crates/channel/src/channel_store_tests.rs index 41acafa3a30525b4c1fd54ecf479a674b2f67df0..9303a52092e13a1592e4a5786c4ed636b969cb73 100644 --- a/crates/channel/src/channel_store_tests.rs +++ b/crates/channel/src/channel_store_tests.rs @@ -340,10 +340,10 @@ fn init_test(cx: &mut AppContext) -> ModelHandle { 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( diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 2f742814a8dbfdfeb7719fbe906858448d8253f8..69cfb7102bbe1b7ef4bb2d182794027e30a45148 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -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 { diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 5767ac54b7893f7425dfd56202b7512d17314f0f..9f63d0e2bed327fd306692a0c28952ea18c854fd 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -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::(cx); @@ -102,6 +102,17 @@ pub fn init(client: &Arc, 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, cx: &AsyncAppContext) { + self.peer.teardown(); + self.set_status(Status::ConnectionLost, cx); + } + fn connection_id(&self) -> Result { if let Status::Connected { connection_id, .. } = *self.status().borrow() { Ok(connection_id) diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 6177c236203d6a2343a5af673d5ac78eadfd7151..8fd1cd438085740a4cac2448903b3d38ce6c0864 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] default-run = "collab" edition = "2021" name = "collab" -version = "0.23.3" +version = "0.24.0" publish = false [[bin]] diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 2d963ff15fa4717aa7faee092356f4b06d8a5814..5a84bfd796a88e09769004cc40d0cc6a06e3118a 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -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, diff --git a/crates/collab/migrations/20231009181554_add_release_channel_to_rooms.sql b/crates/collab/migrations/20231009181554_add_release_channel_to_rooms.sql new file mode 100644 index 0000000000000000000000000000000000000000..8f3a704adde0c385b26bd553d273eff322a17702 --- /dev/null +++ b/crates/collab/migrations/20231009181554_add_release_channel_to_rooms.sql @@ -0,0 +1 @@ +ALTER TABLE rooms ADD COLUMN enviroment TEXT; diff --git a/crates/collab/migrations/20231010114600_add_unique_index_on_rooms_channel_id.sql b/crates/collab/migrations/20231010114600_add_unique_index_on_rooms_channel_id.sql new file mode 100644 index 0000000000000000000000000000000000000000..21ec4cfbb75a574ad3704179a0ae14c8050149d1 --- /dev/null +++ b/crates/collab/migrations/20231010114600_add_unique_index_on_rooms_channel_id.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "index_rooms_on_channel_id" ON "rooms" ("channel_id"); diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index ab31f59541887ac0c7a70db7ab60ade2bce79dbf..c576d2406b81279c38561406c3801c02ddaf4377 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -19,21 +19,14 @@ impl Database { .await } - pub async fn create_root_channel( - &self, - name: &str, - live_kit_room: &str, - creator_id: UserId, - ) -> Result { - self.create_channel(name, None, live_kit_room, creator_id) - .await + pub async fn create_root_channel(&self, name: &str, creator_id: UserId) -> Result { + self.create_channel(name, None, creator_id).await } pub async fn create_channel( &self, name: &str, parent: Option, - live_kit_room: &str, creator_id: UserId, ) -> Result { 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 { + pub async fn get_or_create_channel_room( + &self, + channel_id: ChannelId, + live_kit_room: &str, + enviroment: &str, + ) -> Result { 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 } diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs index b103ae1c737cfdd977418d528585c6fdd9ebb4b7..a38c77dc0fc67eab6ea18da14dad43dd612bf69d 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/crates/collab/src/db/queries/rooms.rs @@ -107,10 +107,12 @@ impl Database { user_id: UserId, connection: ConnectionId, live_kit_room: &str, + release_channel: &str, ) -> Result { 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> { 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, Option) = + 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 = 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 diff --git a/crates/collab/src/db/tables/room.rs b/crates/collab/src/db/tables/room.rs index f72f7000a783570d2cac4f6aebc3cf68846ab3a3..4150c741ac19ef39e09c19116ba3bca819e24a3f 100644 --- a/crates/collab/src/db/tables/room.rs +++ b/crates/collab/src/db/tables/room.rs @@ -8,6 +8,7 @@ pub struct Model { pub id: RoomId, pub live_kit_room: String, pub channel_id: Option, + pub enviroment: Option, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/crates/collab/src/db/tests.rs b/crates/collab/src/db/tests.rs index 75584ff90b68cf4fea0b4151a835c77e970daae5..6a91fd6ffe145c1a31f9b5264029a04ddb1ef1de 100644 --- a/crates/collab/src/db/tests.rs +++ b/crates/collab/src/db/tests.rs @@ -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>, pub connection: Option, diff --git a/crates/collab/src/db/tests/buffer_tests.rs b/crates/collab/src/db/tests/buffer_tests.rs index f6e91b91f011046d6a20431ac10099b21e543774..0ac41a8b0b4267fdd50e8e2c8392319169194888 100644 --- a/crates/collab/src/db/tests/buffer_tests.rs +++ b/crates/collab/src/db/tests/buffer_tests.rs @@ -54,7 +54,7 @@ async fn test_channel_buffers(db: &Arc) { 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) { 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(); diff --git a/crates/collab/src/db/tests/channel_tests.rs b/crates/collab/src/db/tests/channel_tests.rs index 429852d12870a232da68d165d75de68d3a7b9be0..7d2bc04a35aac3adb30ead310705d2ee192ed54a 100644 --- a/crates/collab/src/db/tests/channel_tests.rs +++ b/crates/collab/src/db/tests/channel_tests.rs @@ -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) { .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) { .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) { 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) { .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) { 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) { .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) { .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) { .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) { .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) { // ======================================================================== // 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) { .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(); diff --git a/crates/collab/src/db/tests/db_tests.rs b/crates/collab/src/db/tests/db_tests.rs index 9a617166fead82ca5b538b84ec268329f1f8de22..1520e081c07ead1afc376f84d2e12918fef40db2 100644 --- a/crates/collab/src/db/tests/db_tests.rs +++ b/crates/collab/src/db/tests/db_tests.rs @@ -479,7 +479,7 @@ async fn test_project_count(db: &Arc) { .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) { ) .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) { + 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 { Deterministic::new(0).build_background() } diff --git a/crates/collab/src/db/tests/message_tests.rs b/crates/collab/src/db/tests/message_tests.rs index 464aaba2073bfd11d22d8565d7eeb6ce006a6290..e758fcfb5d0104a61ad84dd82ce10fad784fdf5a 100644 --- a/crates/collab/src/db/tests/message_tests.rs +++ b/crates/collab/src/db/tests/message_tests.rs @@ -25,10 +25,7 @@ async fn test_channel_message_retrieval(db: &Arc) { .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) { .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) { .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 diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 5eb434e167cc115c7ec9f08dd24bc7b12f04e30a..e5c6d94ce03b8b3b1d64ed58be4da53f9dcca112 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -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| { diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index 6bdcee6af3eebc5a885f7a60b780b9f13e7ca0a3..7cfcce832b4cd4e05a953156828e517ef85af9fe 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -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( diff --git a/crates/collab/src/tests/following_tests.rs b/crates/collab/src/tests/following_tests.rs index 3a489b9ac32e82be4ce33733fd8fa3efdc7f72a5..f3857e3db37343aee1d4ba68116a0bc236f61e98 100644 --- a/crates/collab/src/tests/following_tests.rs +++ b/crates/collab/src/tests/following_tests.rs @@ -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)> { + 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::>(); + result.sort_by_key(|e| e.0); + result + }) +} + fn pane_summaries(workspace: &ViewHandle, cx: &mut TestAppContext) -> Vec { workspace.read_with(cx, |workspace, cx| { let active_pane = workspace.active_pane(); diff --git a/crates/collab/src/tests/random_channel_buffer_tests.rs b/crates/collab/src/tests/random_channel_buffer_tests.rs index ad0181602c9ac3bd5ab25d6029ad84d7ba74ce3e..6e0bef225c9fc2d3bc78faa5ddca6e65e28f5495 100644 --- a/crates/collab/src/tests/random_channel_buffer_tests.rs +++ b/crates/collab/src/tests/random_channel_buffer_tests.rs @@ -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..] { diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index e10ded7d953f3872a3056a29940a7610db73de41..2e13874125472cd53b68d4d688c90ca02569615a 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -44,6 +44,7 @@ pub struct TestServer { pub struct TestClient { pub username: String, pub app_state: Arc, + channel_store: ModelHandle, state: RefCell, } @@ -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 { - &self.app_state.channel_store + &self.channel_store } pub fn user_store(&self) -> &ModelHandle { @@ -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(); diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index a95576805074d63a5a74de8d1d107899cb956714..b2e65eb2fa1bd5e92ddf5436115b938958f1383e 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -73,7 +73,7 @@ impl ChannelView { ) -> Task>> { 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 diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index b446521c5ab6120840ec311425171d0dc231e1f3..1a17b48f19c303d9cd26915ff331c2fcc340cc89 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -81,7 +81,7 @@ impl ChatPanel { pub fn new(workspace: &mut Workspace, cx: &mut ViewContext) -> ViewHandle { 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| { diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 951c8bf70ca647644943b3804f45cc6646eb395b..30505b0876c054d8f1b05fcec531d6d04bde5fa1 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -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| { @@ -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) { - 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::() 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) { @@ -3246,6 +3230,15 @@ impl CollabPanel { }); } } + + fn copy_channel_link(&mut self, action: &CopyChannelLink, cx: &mut ViewContext) { + 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( diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index d85aca164a412b3651fbdd197ef4c3418ec8bb93..211ee863e89f6ce7bfe3aa44826c0a8f827a6f85 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -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::(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::(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, } } diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index dee27e0121256edaa38a90bb175d436eba768f96..dc723c70127496cef84b6667799ee04e4e558326 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -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,54 +1344,41 @@ 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 + &r#"one two - + ˇ three four five - sixˇ"# + six"# .unindent(), ); 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 - four - five - ˇ six"# .unindent(), ); diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 974af4bc24070ffb46870badbcb57915a802c1c7..245c2d99770c6c04f6d8e438f8bc62cea1e762ea 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -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; diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 6e587d8c98650e3ccd57974b09f22f37d6e8dd15..b7a4a387ab63a8592992c538e606c6e0d915290f 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -107,13 +107,23 @@ fn matching_history_item_paths( ) -> HashMap, 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, + 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::().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::>(); + 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, + 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::().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::>(); + 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, diff --git a/crates/fs/Cargo.toml b/crates/fs/Cargo.toml index 441ce6f9c7b678101d676d7e8f31a90143c42d3b..11a34bcecb2674a652322409a072752cdc167ee6 100644 --- a/crates/fs/Cargo.toml +++ b/crates/fs/Cargo.toml @@ -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 diff --git a/crates/fs/src/repository.rs b/crates/fs/src/repository.rs index 2b2aebe67959ed977c96ed60b36cf4c7ce3d23d2..4637a7f75408c74a4d398b8eb60f21d6ba76ab33 100644 --- a/crates/fs/src/repository.rs +++ b/crates/fs/src/repository.rs @@ -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, } + #[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) -> Option { - 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)] diff --git a/crates/fuzzy/src/matcher.rs b/crates/fuzzy/src/matcher.rs index dafafe40a0630ebe364f5d29963ef2475aa3c2b8..e808a4886f91152894dbaf4686fa51a786926d29 100644 --- a/crates/fuzzy/src/matcher.rs +++ b/crates/fuzzy/src/matcher.rs @@ -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, }, diff --git a/crates/fuzzy/src/paths.rs b/crates/fuzzy/src/paths.rs index 4eb31936a8cc487696fb3c51c784bdea6f0aeeac..d8fae471e199105953554b6336ffd8dbe057bbf9 100644 --- a/crates/fuzzy/src/paths.rs +++ b/crates/fuzzy/src/paths.rs @@ -14,7 +14,7 @@ use crate::{ #[derive(Clone, Debug)] pub struct PathMatchCandidate<'a> { - pub path: &'a Arc, + 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, diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index 656f2e44750641d752fce2ff4f639cce59601c25..24ad7747590d9c218ec9b86e99e248f3ff6d3cb7 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -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); diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index 885b14f2dd5e23ecbe0e6b7eff5f27687fb48de1..2fd85f6465cdb67f7aea39fabd5e1b3675c87d41 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -13,12 +13,12 @@ use gpui::{ scene::{self}, LayoutId, }; -use refineable::{Refineable, RefinementCascade}; +use refineable::{Cascade, Refineable}; use smallvec::SmallVec; use util::ResultExt; pub struct Div { - styles: RefinementCascade