fix Cargo.lock for merge
This commit is contained in:
commit
183758a7c5
592 changed files with 3418 additions and 4563 deletions
|
@ -16,8 +16,8 @@ use workspace::{item::ItemHandle, StatusItemView, Workspace};
|
|||
|
||||
actions!(lsp_status, [ShowErrorMessage]);
|
||||
|
||||
const DOWNLOAD_ICON: &str = "icons/download_12.svg";
|
||||
const WARNING_ICON: &str = "icons/triangle_exclamation_12.svg";
|
||||
const DOWNLOAD_ICON: &str = "icons/download.svg";
|
||||
const WARNING_ICON: &str = "icons/warning.svg";
|
||||
|
||||
pub enum Event {
|
||||
ShowError { lsp_name: Arc<str>, error: String },
|
||||
|
|
|
@ -9,6 +9,7 @@ path = "src/ai.rs"
|
|||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
client = { path = "../client" }
|
||||
collections = { path = "../collections"}
|
||||
editor = { path = "../editor" }
|
||||
fs = { path = "../fs" }
|
||||
|
@ -19,6 +20,7 @@ search = { path = "../search" }
|
|||
settings = { path = "../settings" }
|
||||
theme = { path = "../theme" }
|
||||
util = { path = "../util" }
|
||||
uuid = { version = "1.1.2", features = ["v4"] }
|
||||
workspace = { path = "../workspace" }
|
||||
|
||||
anyhow.workspace = true
|
||||
|
|
|
@ -61,6 +61,7 @@ struct SavedMessage {
|
|||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SavedConversation {
|
||||
id: Option<String>,
|
||||
zed: String,
|
||||
version: String,
|
||||
text: String,
|
||||
|
|
|
@ -6,6 +6,7 @@ use crate::{
|
|||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use chrono::{DateTime, Local};
|
||||
use client::{telemetry::AssistantKind, ClickhouseEvent, TelemetrySettings};
|
||||
use collections::{hash_map, HashMap, HashSet, VecDeque};
|
||||
use editor::{
|
||||
display_map::{
|
||||
|
@ -48,6 +49,7 @@ use theme::{
|
|||
AssistantStyle,
|
||||
};
|
||||
use util::{paths::CONVERSATIONS_DIR, post_inc, ResultExt, TryFutureExt};
|
||||
use uuid::Uuid;
|
||||
use workspace::{
|
||||
dock::{DockPosition, Panel},
|
||||
searchable::Direction,
|
||||
|
@ -296,6 +298,7 @@ impl AssistantPanel {
|
|||
self.include_conversation_in_next_inline_assist,
|
||||
self.inline_prompt_history.clone(),
|
||||
codegen.clone(),
|
||||
self.workspace.clone(),
|
||||
cx,
|
||||
);
|
||||
cx.focus_self();
|
||||
|
@ -724,6 +727,7 @@ impl AssistantPanel {
|
|||
self.api_key.clone(),
|
||||
self.languages.clone(),
|
||||
self.fs.clone(),
|
||||
self.workspace.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
@ -1059,6 +1063,7 @@ impl AssistantPanel {
|
|||
}
|
||||
|
||||
let fs = self.fs.clone();
|
||||
let workspace = self.workspace.clone();
|
||||
let api_key = self.api_key.clone();
|
||||
let languages = self.languages.clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
|
@ -1073,8 +1078,9 @@ impl AssistantPanel {
|
|||
if let Some(ix) = this.editor_index_for_path(&path, cx) {
|
||||
this.set_active_editor_index(Some(ix), cx);
|
||||
} else {
|
||||
let editor = cx
|
||||
.add_view(|cx| ConversationEditor::for_conversation(conversation, fs, cx));
|
||||
let editor = cx.add_view(|cx| {
|
||||
ConversationEditor::for_conversation(conversation, fs, workspace, cx)
|
||||
});
|
||||
this.add_conversation(editor, cx);
|
||||
}
|
||||
})?;
|
||||
|
@ -1348,6 +1354,7 @@ struct Summary {
|
|||
}
|
||||
|
||||
struct Conversation {
|
||||
id: Option<String>,
|
||||
buffer: ModelHandle<Buffer>,
|
||||
message_anchors: Vec<MessageAnchor>,
|
||||
messages_metadata: HashMap<MessageId, MessageMetadata>,
|
||||
|
@ -1398,6 +1405,7 @@ impl Conversation {
|
|||
let model = settings.default_open_ai_model.clone();
|
||||
|
||||
let mut this = Self {
|
||||
id: Some(Uuid::new_v4().to_string()),
|
||||
message_anchors: Default::default(),
|
||||
messages_metadata: Default::default(),
|
||||
next_message_id: Default::default(),
|
||||
|
@ -1435,6 +1443,7 @@ impl Conversation {
|
|||
|
||||
fn serialize(&self, cx: &AppContext) -> SavedConversation {
|
||||
SavedConversation {
|
||||
id: self.id.clone(),
|
||||
zed: "conversation".into(),
|
||||
version: SavedConversation::VERSION.into(),
|
||||
text: self.buffer.read(cx).text(),
|
||||
|
@ -1462,6 +1471,10 @@ impl Conversation {
|
|||
language_registry: Arc<LanguageRegistry>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
let id = match saved_conversation.id {
|
||||
Some(id) => Some(id),
|
||||
None => Some(Uuid::new_v4().to_string()),
|
||||
};
|
||||
let model = saved_conversation.model;
|
||||
let markdown = language_registry.language_for_name("Markdown");
|
||||
let mut message_anchors = Vec::new();
|
||||
|
@ -1491,6 +1504,7 @@ impl Conversation {
|
|||
});
|
||||
|
||||
let mut this = Self {
|
||||
id,
|
||||
message_anchors,
|
||||
messages_metadata: saved_conversation.message_metadata,
|
||||
next_message_id,
|
||||
|
@ -2108,6 +2122,7 @@ struct ScrollPosition {
|
|||
struct ConversationEditor {
|
||||
conversation: ModelHandle<Conversation>,
|
||||
fs: Arc<dyn Fs>,
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
editor: ViewHandle<Editor>,
|
||||
blocks: HashSet<BlockId>,
|
||||
scroll_position: Option<ScrollPosition>,
|
||||
|
@ -2119,15 +2134,17 @@ impl ConversationEditor {
|
|||
api_key: Rc<RefCell<Option<String>>>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
fs: Arc<dyn Fs>,
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let conversation = cx.add_model(|cx| Conversation::new(api_key, language_registry, cx));
|
||||
Self::for_conversation(conversation, fs, cx)
|
||||
Self::for_conversation(conversation, fs, workspace, cx)
|
||||
}
|
||||
|
||||
fn for_conversation(
|
||||
conversation: ModelHandle<Conversation>,
|
||||
fs: Arc<dyn Fs>,
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let editor = cx.add_view(|cx| {
|
||||
|
@ -2150,6 +2167,7 @@ impl ConversationEditor {
|
|||
blocks: Default::default(),
|
||||
scroll_position: None,
|
||||
fs,
|
||||
workspace,
|
||||
_subscriptions,
|
||||
};
|
||||
this.update_message_headers(cx);
|
||||
|
@ -2157,6 +2175,13 @@ impl ConversationEditor {
|
|||
}
|
||||
|
||||
fn assist(&mut self, _: &Assist, cx: &mut ViewContext<Self>) {
|
||||
report_assistant_event(
|
||||
self.workspace.clone(),
|
||||
self.conversation.read(cx).id.clone(),
|
||||
AssistantKind::Panel,
|
||||
cx,
|
||||
);
|
||||
|
||||
let cursors = self.cursors(cx);
|
||||
|
||||
let user_messages = self.conversation.update(cx, |conversation, cx| {
|
||||
|
@ -2376,7 +2401,7 @@ impl ConversationEditor {
|
|||
.with_children(
|
||||
if let MessageStatus::Error(error) = &message.status {
|
||||
Some(
|
||||
Svg::new("icons/circle_x_mark_12.svg")
|
||||
Svg::new("icons/error.svg")
|
||||
.with_color(style.error_icon.color)
|
||||
.constrained()
|
||||
.with_width(style.error_icon.width)
|
||||
|
@ -2665,6 +2690,7 @@ enum InlineAssistantEvent {
|
|||
struct InlineAssistant {
|
||||
id: usize,
|
||||
prompt_editor: ViewHandle<Editor>,
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
confirmed: bool,
|
||||
has_focus: bool,
|
||||
include_conversation: bool,
|
||||
|
@ -2704,7 +2730,7 @@ impl View for InlineAssistant {
|
|||
)
|
||||
.with_children(if let Some(error) = self.codegen.read(cx).error() {
|
||||
Some(
|
||||
Svg::new("icons/circle_x_mark_12.svg")
|
||||
Svg::new("icons/error.svg")
|
||||
.with_color(theme.assistant.error_icon.color)
|
||||
.constrained()
|
||||
.with_width(theme.assistant.error_icon.width)
|
||||
|
@ -2780,6 +2806,7 @@ impl InlineAssistant {
|
|||
include_conversation: bool,
|
||||
prompt_history: VecDeque<String>,
|
||||
codegen: ModelHandle<Codegen>,
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let prompt_editor = cx.add_view(|cx| {
|
||||
|
@ -2801,6 +2828,7 @@ impl InlineAssistant {
|
|||
Self {
|
||||
id,
|
||||
prompt_editor,
|
||||
workspace,
|
||||
confirmed: false,
|
||||
has_focus: false,
|
||||
include_conversation,
|
||||
|
@ -2859,6 +2887,8 @@ impl InlineAssistant {
|
|||
if self.confirmed {
|
||||
cx.emit(InlineAssistantEvent::Dismissed);
|
||||
} else {
|
||||
report_assistant_event(self.workspace.clone(), None, AssistantKind::Inline, cx);
|
||||
|
||||
let prompt = self.prompt_editor.read(cx).text(cx);
|
||||
self.prompt_editor.update(cx, |editor, cx| {
|
||||
editor.set_read_only(true);
|
||||
|
@ -3347,3 +3377,30 @@ mod tests {
|
|||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
fn report_assistant_event(
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
conversation_id: Option<String>,
|
||||
assistant_kind: AssistantKind,
|
||||
cx: &AppContext,
|
||||
) {
|
||||
let Some(workspace) = workspace.upgrade(cx) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let client = workspace.read(cx).project().read(cx).client();
|
||||
let telemetry = client.telemetry();
|
||||
|
||||
let model = settings::get::<AssistantSettings>(cx)
|
||||
.default_open_ai_model
|
||||
.clone();
|
||||
|
||||
let event = ClickhouseEvent::Assistant {
|
||||
conversation_id,
|
||||
kind: assistant_kind,
|
||||
model: model.full_name(),
|
||||
};
|
||||
let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
|
||||
|
||||
telemetry.report_clickhouse_event(event, telemetry_settings)
|
||||
}
|
||||
|
|
|
@ -50,7 +50,7 @@ impl View for UpdateNotification {
|
|||
.with_child(
|
||||
MouseEventHandler::new::<Cancel, _>(0, cx, |state, _| {
|
||||
let style = theme.dismiss_button.style_for(state);
|
||||
Svg::new("icons/x_mark_8.svg")
|
||||
Svg::new("icons/x.svg")
|
||||
.with_color(style.color)
|
||||
.constrained()
|
||||
.with_width(style.icon_width)
|
||||
|
|
|
@ -172,7 +172,7 @@ impl Room {
|
|||
cx.spawn(|this, mut cx| async move {
|
||||
connect.await?;
|
||||
|
||||
if !cx.read(|cx| settings::get::<CallSettings>(cx).mute_on_join) {
|
||||
if !cx.read(Self::mute_on_join) {
|
||||
this.update(&mut cx, |this, cx| this.share_microphone(cx))
|
||||
.await?;
|
||||
}
|
||||
|
@ -301,6 +301,10 @@ impl Room {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn mute_on_join(cx: &AppContext) -> bool {
|
||||
settings::get::<CallSettings>(cx).mute_on_join || client::IMPERSONATE_LOGIN.is_some()
|
||||
}
|
||||
|
||||
fn from_join_response(
|
||||
response: proto::JoinRoomResponse,
|
||||
client: Arc<Client>,
|
||||
|
@ -1124,7 +1128,7 @@ impl Room {
|
|||
self.live_kit
|
||||
.as_ref()
|
||||
.and_then(|live_kit| match &live_kit.microphone_track {
|
||||
LocalTrack::None => Some(settings::get::<CallSettings>(cx).mute_on_join),
|
||||
LocalTrack::None => Some(Self::mute_on_join(cx)),
|
||||
LocalTrack::Pending { muted, .. } => Some(*muted),
|
||||
LocalTrack::Published { muted, .. } => Some(*muted),
|
||||
})
|
||||
|
|
|
@ -47,5 +47,6 @@ tempfile = "3"
|
|||
collections = { path = "../collections", features = ["test-support"] }
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
rpc = { path = "../rpc", features = ["test-support"] }
|
||||
client = { path = "../client", features = ["test-support"] }
|
||||
settings = { path = "../settings", features = ["test-support"] }
|
||||
util = { path = "../util", features = ["test-support"] }
|
||||
|
|
|
@ -1,14 +1,18 @@
|
|||
mod channel_buffer;
|
||||
mod channel_chat;
|
||||
mod channel_store;
|
||||
|
||||
pub mod channel_buffer;
|
||||
use std::sync::Arc;
|
||||
pub use channel_buffer::{ChannelBuffer, ChannelBufferEvent};
|
||||
pub use channel_chat::{ChannelChat, ChannelChatEvent, ChannelMessage, ChannelMessageId};
|
||||
pub use channel_store::{Channel, ChannelEvent, ChannelId, ChannelMembership, ChannelStore};
|
||||
|
||||
pub use channel_store::*;
|
||||
use client::Client;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[cfg(test)]
|
||||
mod channel_store_tests;
|
||||
|
||||
pub fn init(client: &Arc<Client>) {
|
||||
channel_buffer::init(client);
|
||||
channel_chat::init(client);
|
||||
}
|
||||
|
|
|
@ -23,13 +23,13 @@ pub struct ChannelBuffer {
|
|||
subscription: Option<client::Subscription>,
|
||||
}
|
||||
|
||||
pub enum Event {
|
||||
pub enum ChannelBufferEvent {
|
||||
CollaboratorsChanged,
|
||||
Disconnected,
|
||||
}
|
||||
|
||||
impl Entity for ChannelBuffer {
|
||||
type Event = Event;
|
||||
type Event = ChannelBufferEvent;
|
||||
|
||||
fn release(&mut self, _: &mut AppContext) {
|
||||
if self.connected {
|
||||
|
@ -101,7 +101,7 @@ impl ChannelBuffer {
|
|||
}
|
||||
}
|
||||
self.collaborators = collaborators;
|
||||
cx.emit(Event::CollaboratorsChanged);
|
||||
cx.emit(ChannelBufferEvent::CollaboratorsChanged);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
|
@ -141,7 +141,7 @@ impl ChannelBuffer {
|
|||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.collaborators.push(collaborator);
|
||||
cx.emit(Event::CollaboratorsChanged);
|
||||
cx.emit(ChannelBufferEvent::CollaboratorsChanged);
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
|
@ -165,7 +165,7 @@ impl ChannelBuffer {
|
|||
true
|
||||
}
|
||||
});
|
||||
cx.emit(Event::CollaboratorsChanged);
|
||||
cx.emit(ChannelBufferEvent::CollaboratorsChanged);
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
|
@ -185,7 +185,7 @@ impl ChannelBuffer {
|
|||
break;
|
||||
}
|
||||
}
|
||||
cx.emit(Event::CollaboratorsChanged);
|
||||
cx.emit(ChannelBufferEvent::CollaboratorsChanged);
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
|
@ -230,7 +230,7 @@ impl ChannelBuffer {
|
|||
if self.connected {
|
||||
self.connected = false;
|
||||
self.subscription.take();
|
||||
cx.emit(Event::Disconnected);
|
||||
cx.emit(ChannelBufferEvent::Disconnected);
|
||||
cx.notify()
|
||||
}
|
||||
}
|
||||
|
|
505
crates/channel/src/channel_chat.rs
Normal file
505
crates/channel/src/channel_chat.rs
Normal file
|
@ -0,0 +1,505 @@
|
|||
use crate::Channel;
|
||||
use anyhow::{anyhow, Result};
|
||||
use client::{
|
||||
proto,
|
||||
user::{User, UserStore},
|
||||
Client, Subscription, TypedEnvelope,
|
||||
};
|
||||
use futures::lock::Mutex;
|
||||
use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task};
|
||||
use rand::prelude::*;
|
||||
use std::{collections::HashSet, mem, ops::Range, sync::Arc};
|
||||
use sum_tree::{Bias, SumTree};
|
||||
use time::OffsetDateTime;
|
||||
use util::{post_inc, ResultExt as _, TryFutureExt};
|
||||
|
||||
pub struct ChannelChat {
|
||||
channel: Arc<Channel>,
|
||||
messages: SumTree<ChannelMessage>,
|
||||
loaded_all_messages: bool,
|
||||
next_pending_message_id: usize,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
rpc: Arc<Client>,
|
||||
outgoing_messages_lock: Arc<Mutex<()>>,
|
||||
rng: StdRng,
|
||||
_subscription: Subscription,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ChannelMessage {
|
||||
pub id: ChannelMessageId,
|
||||
pub body: String,
|
||||
pub timestamp: OffsetDateTime,
|
||||
pub sender: Arc<User>,
|
||||
pub nonce: u128,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum ChannelMessageId {
|
||||
Saved(u64),
|
||||
Pending(usize),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ChannelMessageSummary {
|
||||
max_id: ChannelMessageId,
|
||||
count: usize,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
|
||||
struct Count(usize);
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum ChannelChatEvent {
|
||||
MessagesUpdated {
|
||||
old_range: Range<usize>,
|
||||
new_count: usize,
|
||||
},
|
||||
}
|
||||
|
||||
pub fn init(client: &Arc<Client>) {
|
||||
client.add_model_message_handler(ChannelChat::handle_message_sent);
|
||||
client.add_model_message_handler(ChannelChat::handle_message_removed);
|
||||
}
|
||||
|
||||
impl Entity for ChannelChat {
|
||||
type Event = ChannelChatEvent;
|
||||
|
||||
fn release(&mut self, _: &mut AppContext) {
|
||||
self.rpc
|
||||
.send(proto::LeaveChannelChat {
|
||||
channel_id: self.channel.id,
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
|
||||
impl ChannelChat {
|
||||
pub async fn new(
|
||||
channel: Arc<Channel>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
client: Arc<Client>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<ModelHandle<Self>> {
|
||||
let channel_id = channel.id;
|
||||
let subscription = client.subscribe_to_entity(channel_id).unwrap();
|
||||
|
||||
let response = client
|
||||
.request(proto::JoinChannelChat { channel_id })
|
||||
.await?;
|
||||
let messages = messages_from_proto(response.messages, &user_store, &mut cx).await?;
|
||||
let loaded_all_messages = response.done;
|
||||
|
||||
Ok(cx.add_model(|cx| {
|
||||
let mut this = Self {
|
||||
channel,
|
||||
user_store,
|
||||
rpc: client,
|
||||
outgoing_messages_lock: Default::default(),
|
||||
messages: Default::default(),
|
||||
loaded_all_messages,
|
||||
next_pending_message_id: 0,
|
||||
rng: StdRng::from_entropy(),
|
||||
_subscription: subscription.set_model(&cx.handle(), &mut cx.to_async()),
|
||||
};
|
||||
this.insert_messages(messages, cx);
|
||||
this
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn channel(&self) -> &Arc<Channel> {
|
||||
&self.channel
|
||||
}
|
||||
|
||||
pub fn send_message(
|
||||
&mut self,
|
||||
body: String,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Result<Task<Result<()>>> {
|
||||
if body.is_empty() {
|
||||
Err(anyhow!("message body can't be empty"))?;
|
||||
}
|
||||
|
||||
let current_user = self
|
||||
.user_store
|
||||
.read(cx)
|
||||
.current_user()
|
||||
.ok_or_else(|| anyhow!("current_user is not present"))?;
|
||||
|
||||
let channel_id = self.channel.id;
|
||||
let pending_id = ChannelMessageId::Pending(post_inc(&mut self.next_pending_message_id));
|
||||
let nonce = self.rng.gen();
|
||||
self.insert_messages(
|
||||
SumTree::from_item(
|
||||
ChannelMessage {
|
||||
id: pending_id,
|
||||
body: body.clone(),
|
||||
sender: current_user,
|
||||
timestamp: OffsetDateTime::now_utc(),
|
||||
nonce,
|
||||
},
|
||||
&(),
|
||||
),
|
||||
cx,
|
||||
);
|
||||
let user_store = self.user_store.clone();
|
||||
let rpc = self.rpc.clone();
|
||||
let outgoing_messages_lock = self.outgoing_messages_lock.clone();
|
||||
Ok(cx.spawn(|this, mut cx| async move {
|
||||
let outgoing_message_guard = outgoing_messages_lock.lock().await;
|
||||
let request = rpc.request(proto::SendChannelMessage {
|
||||
channel_id,
|
||||
body,
|
||||
nonce: Some(nonce.into()),
|
||||
});
|
||||
let response = request.await?;
|
||||
drop(outgoing_message_guard);
|
||||
let message = ChannelMessage::from_proto(
|
||||
response.message.ok_or_else(|| anyhow!("invalid message"))?,
|
||||
&user_store,
|
||||
&mut cx,
|
||||
)
|
||||
.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.insert_messages(SumTree::from_item(message, &()), cx);
|
||||
Ok(())
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn remove_message(&mut self, id: u64, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
let response = self.rpc.request(proto::RemoveChannelMessage {
|
||||
channel_id: self.channel.id,
|
||||
message_id: id,
|
||||
});
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
response.await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.message_removed(id, cx);
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn load_more_messages(&mut self, cx: &mut ModelContext<Self>) -> bool {
|
||||
if !self.loaded_all_messages {
|
||||
let rpc = self.rpc.clone();
|
||||
let user_store = self.user_store.clone();
|
||||
let channel_id = self.channel.id;
|
||||
if let Some(before_message_id) =
|
||||
self.messages.first().and_then(|message| match message.id {
|
||||
ChannelMessageId::Saved(id) => Some(id),
|
||||
ChannelMessageId::Pending(_) => None,
|
||||
})
|
||||
{
|
||||
cx.spawn(|this, mut cx| {
|
||||
async move {
|
||||
let response = rpc
|
||||
.request(proto::GetChannelMessages {
|
||||
channel_id,
|
||||
before_message_id,
|
||||
})
|
||||
.await?;
|
||||
let loaded_all_messages = response.done;
|
||||
let messages =
|
||||
messages_from_proto(response.messages, &user_store, &mut cx).await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.loaded_all_messages = loaded_all_messages;
|
||||
this.insert_messages(messages, cx);
|
||||
});
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err()
|
||||
})
|
||||
.detach();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn rejoin(&mut self, cx: &mut ModelContext<Self>) {
|
||||
let user_store = self.user_store.clone();
|
||||
let rpc = self.rpc.clone();
|
||||
let channel_id = self.channel.id;
|
||||
cx.spawn(|this, mut cx| {
|
||||
async move {
|
||||
let response = rpc.request(proto::JoinChannelChat { channel_id }).await?;
|
||||
let messages = messages_from_proto(response.messages, &user_store, &mut cx).await?;
|
||||
let loaded_all_messages = response.done;
|
||||
|
||||
let pending_messages = this.update(&mut cx, |this, cx| {
|
||||
if let Some((first_new_message, last_old_message)) =
|
||||
messages.first().zip(this.messages.last())
|
||||
{
|
||||
if first_new_message.id > last_old_message.id {
|
||||
let old_messages = mem::take(&mut this.messages);
|
||||
cx.emit(ChannelChatEvent::MessagesUpdated {
|
||||
old_range: 0..old_messages.summary().count,
|
||||
new_count: 0,
|
||||
});
|
||||
this.loaded_all_messages = loaded_all_messages;
|
||||
}
|
||||
}
|
||||
|
||||
this.insert_messages(messages, cx);
|
||||
if loaded_all_messages {
|
||||
this.loaded_all_messages = loaded_all_messages;
|
||||
}
|
||||
|
||||
this.pending_messages().cloned().collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
for pending_message in pending_messages {
|
||||
let request = rpc.request(proto::SendChannelMessage {
|
||||
channel_id,
|
||||
body: pending_message.body,
|
||||
nonce: Some(pending_message.nonce.into()),
|
||||
});
|
||||
let response = request.await?;
|
||||
let message = ChannelMessage::from_proto(
|
||||
response.message.ok_or_else(|| anyhow!("invalid message"))?,
|
||||
&user_store,
|
||||
&mut cx,
|
||||
)
|
||||
.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.insert_messages(SumTree::from_item(message, &()), cx);
|
||||
});
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err()
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn message_count(&self) -> usize {
|
||||
self.messages.summary().count
|
||||
}
|
||||
|
||||
pub fn messages(&self) -> &SumTree<ChannelMessage> {
|
||||
&self.messages
|
||||
}
|
||||
|
||||
pub fn message(&self, ix: usize) -> &ChannelMessage {
|
||||
let mut cursor = self.messages.cursor::<Count>();
|
||||
cursor.seek(&Count(ix), Bias::Right, &());
|
||||
cursor.item().unwrap()
|
||||
}
|
||||
|
||||
pub fn messages_in_range(&self, range: Range<usize>) -> impl Iterator<Item = &ChannelMessage> {
|
||||
let mut cursor = self.messages.cursor::<Count>();
|
||||
cursor.seek(&Count(range.start), Bias::Right, &());
|
||||
cursor.take(range.len())
|
||||
}
|
||||
|
||||
pub fn pending_messages(&self) -> impl Iterator<Item = &ChannelMessage> {
|
||||
let mut cursor = self.messages.cursor::<ChannelMessageId>();
|
||||
cursor.seek(&ChannelMessageId::Pending(0), Bias::Left, &());
|
||||
cursor
|
||||
}
|
||||
|
||||
async fn handle_message_sent(
|
||||
this: ModelHandle<Self>,
|
||||
message: TypedEnvelope<proto::ChannelMessageSent>,
|
||||
_: Arc<Client>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
let user_store = this.read_with(&cx, |this, _| this.user_store.clone());
|
||||
let message = message
|
||||
.payload
|
||||
.message
|
||||
.ok_or_else(|| anyhow!("empty message"))?;
|
||||
|
||||
let message = ChannelMessage::from_proto(message, &user_store, &mut cx).await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.insert_messages(SumTree::from_item(message, &()), cx)
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_message_removed(
|
||||
this: ModelHandle<Self>,
|
||||
message: TypedEnvelope<proto::RemoveChannelMessage>,
|
||||
_: Arc<Client>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.message_removed(message.payload.message_id, cx)
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn insert_messages(&mut self, messages: SumTree<ChannelMessage>, cx: &mut ModelContext<Self>) {
|
||||
if let Some((first_message, last_message)) = messages.first().zip(messages.last()) {
|
||||
let nonces = messages
|
||||
.cursor::<()>()
|
||||
.map(|m| m.nonce)
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
let mut old_cursor = self.messages.cursor::<(ChannelMessageId, Count)>();
|
||||
let mut new_messages = old_cursor.slice(&first_message.id, Bias::Left, &());
|
||||
let start_ix = old_cursor.start().1 .0;
|
||||
let removed_messages = old_cursor.slice(&last_message.id, Bias::Right, &());
|
||||
let removed_count = removed_messages.summary().count;
|
||||
let new_count = messages.summary().count;
|
||||
let end_ix = start_ix + removed_count;
|
||||
|
||||
new_messages.append(messages, &());
|
||||
|
||||
let mut ranges = Vec::<Range<usize>>::new();
|
||||
if new_messages.last().unwrap().is_pending() {
|
||||
new_messages.append(old_cursor.suffix(&()), &());
|
||||
} else {
|
||||
new_messages.append(
|
||||
old_cursor.slice(&ChannelMessageId::Pending(0), Bias::Left, &()),
|
||||
&(),
|
||||
);
|
||||
|
||||
while let Some(message) = old_cursor.item() {
|
||||
let message_ix = old_cursor.start().1 .0;
|
||||
if nonces.contains(&message.nonce) {
|
||||
if ranges.last().map_or(false, |r| r.end == message_ix) {
|
||||
ranges.last_mut().unwrap().end += 1;
|
||||
} else {
|
||||
ranges.push(message_ix..message_ix + 1);
|
||||
}
|
||||
} else {
|
||||
new_messages.push(message.clone(), &());
|
||||
}
|
||||
old_cursor.next(&());
|
||||
}
|
||||
}
|
||||
|
||||
drop(old_cursor);
|
||||
self.messages = new_messages;
|
||||
|
||||
for range in ranges.into_iter().rev() {
|
||||
cx.emit(ChannelChatEvent::MessagesUpdated {
|
||||
old_range: range,
|
||||
new_count: 0,
|
||||
});
|
||||
}
|
||||
cx.emit(ChannelChatEvent::MessagesUpdated {
|
||||
old_range: start_ix..end_ix,
|
||||
new_count,
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn message_removed(&mut self, id: u64, cx: &mut ModelContext<Self>) {
|
||||
let mut cursor = self.messages.cursor::<ChannelMessageId>();
|
||||
let mut messages = cursor.slice(&ChannelMessageId::Saved(id), Bias::Left, &());
|
||||
if let Some(item) = cursor.item() {
|
||||
if item.id == ChannelMessageId::Saved(id) {
|
||||
let ix = messages.summary().count;
|
||||
cursor.next(&());
|
||||
messages.append(cursor.suffix(&()), &());
|
||||
drop(cursor);
|
||||
self.messages = messages;
|
||||
cx.emit(ChannelChatEvent::MessagesUpdated {
|
||||
old_range: ix..ix + 1,
|
||||
new_count: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn messages_from_proto(
|
||||
proto_messages: Vec<proto::ChannelMessage>,
|
||||
user_store: &ModelHandle<UserStore>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<SumTree<ChannelMessage>> {
|
||||
let unique_user_ids = proto_messages
|
||||
.iter()
|
||||
.map(|m| m.sender_id)
|
||||
.collect::<HashSet<_>>()
|
||||
.into_iter()
|
||||
.collect();
|
||||
user_store
|
||||
.update(cx, |user_store, cx| {
|
||||
user_store.get_users(unique_user_ids, cx)
|
||||
})
|
||||
.await?;
|
||||
|
||||
let mut messages = Vec::with_capacity(proto_messages.len());
|
||||
for message in proto_messages {
|
||||
messages.push(ChannelMessage::from_proto(message, user_store, cx).await?);
|
||||
}
|
||||
let mut result = SumTree::new();
|
||||
result.extend(messages, &());
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
impl ChannelMessage {
|
||||
pub async fn from_proto(
|
||||
message: proto::ChannelMessage,
|
||||
user_store: &ModelHandle<UserStore>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<Self> {
|
||||
let sender = user_store
|
||||
.update(cx, |user_store, cx| {
|
||||
user_store.get_user(message.sender_id, cx)
|
||||
})
|
||||
.await?;
|
||||
Ok(ChannelMessage {
|
||||
id: ChannelMessageId::Saved(message.id),
|
||||
body: message.body,
|
||||
timestamp: OffsetDateTime::from_unix_timestamp(message.timestamp as i64)?,
|
||||
sender,
|
||||
nonce: message
|
||||
.nonce
|
||||
.ok_or_else(|| anyhow!("nonce is required"))?
|
||||
.into(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_pending(&self) -> bool {
|
||||
matches!(self.id, ChannelMessageId::Pending(_))
|
||||
}
|
||||
}
|
||||
|
||||
impl sum_tree::Item for ChannelMessage {
|
||||
type Summary = ChannelMessageSummary;
|
||||
|
||||
fn summary(&self) -> Self::Summary {
|
||||
ChannelMessageSummary {
|
||||
max_id: self.id,
|
||||
count: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ChannelMessageId {
|
||||
fn default() -> Self {
|
||||
Self::Saved(0)
|
||||
}
|
||||
}
|
||||
|
||||
impl sum_tree::Summary for ChannelMessageSummary {
|
||||
type Context = ();
|
||||
|
||||
fn add_summary(&mut self, summary: &Self, _: &()) {
|
||||
self.max_id = summary.max_id;
|
||||
self.count += summary.count;
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> sum_tree::Dimension<'a, ChannelMessageSummary> for ChannelMessageId {
|
||||
fn add_summary(&mut self, summary: &'a ChannelMessageSummary, _: &()) {
|
||||
debug_assert!(summary.max_id > *self);
|
||||
*self = summary.max_id;
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> sum_tree::Dimension<'a, ChannelMessageSummary> for Count {
|
||||
fn add_summary(&mut self, summary: &'a ChannelMessageSummary, _: &()) {
|
||||
self.0 += summary.count;
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
use crate::channel_buffer::ChannelBuffer;
|
||||
use crate::{channel_buffer::ChannelBuffer, channel_chat::ChannelChat};
|
||||
use anyhow::{anyhow, Result};
|
||||
use client::{Client, Subscription, User, UserId, UserStore};
|
||||
use collections::{hash_map, HashMap, HashSet};
|
||||
|
@ -20,7 +20,8 @@ pub struct ChannelStore {
|
|||
channels_with_admin_privileges: HashSet<ChannelId>,
|
||||
outgoing_invites: HashSet<(ChannelId, UserId)>,
|
||||
update_channels_tx: mpsc::UnboundedSender<proto::UpdateChannels>,
|
||||
opened_buffers: HashMap<ChannelId, OpenedChannelBuffer>,
|
||||
opened_buffers: HashMap<ChannelId, OpenedModelHandle<ChannelBuffer>>,
|
||||
opened_chats: HashMap<ChannelId, OpenedModelHandle<ChannelChat>>,
|
||||
client: Arc<Client>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
_rpc_subscription: Subscription,
|
||||
|
@ -50,15 +51,9 @@ impl Entity for ChannelStore {
|
|||
type Event = ChannelEvent;
|
||||
}
|
||||
|
||||
pub enum ChannelMemberStatus {
|
||||
Invited,
|
||||
Member,
|
||||
NotMember,
|
||||
}
|
||||
|
||||
enum OpenedChannelBuffer {
|
||||
Open(WeakModelHandle<ChannelBuffer>),
|
||||
Loading(Shared<Task<Result<ModelHandle<ChannelBuffer>, Arc<anyhow::Error>>>>),
|
||||
enum OpenedModelHandle<E: Entity> {
|
||||
Open(WeakModelHandle<E>),
|
||||
Loading(Shared<Task<Result<ModelHandle<E>, Arc<anyhow::Error>>>>),
|
||||
}
|
||||
|
||||
impl ChannelStore {
|
||||
|
@ -94,6 +89,7 @@ impl ChannelStore {
|
|||
channels_with_admin_privileges: Default::default(),
|
||||
outgoing_invites: Default::default(),
|
||||
opened_buffers: Default::default(),
|
||||
opened_chats: Default::default(),
|
||||
update_channels_tx,
|
||||
client,
|
||||
user_store,
|
||||
|
@ -115,6 +111,10 @@ impl ChannelStore {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn client(&self) -> Arc<Client> {
|
||||
self.client.clone()
|
||||
}
|
||||
|
||||
pub fn has_children(&self, channel_id: ChannelId) -> bool {
|
||||
self.channel_paths.iter().any(|path| {
|
||||
if let Some(ix) = path.iter().position(|id| *id == channel_id) {
|
||||
|
@ -129,6 +129,12 @@ impl ChannelStore {
|
|||
self.channel_paths.len()
|
||||
}
|
||||
|
||||
pub fn index_of_channel(&self, channel_id: ChannelId) -> Option<usize> {
|
||||
self.channel_paths
|
||||
.iter()
|
||||
.position(|path| path.ends_with(&[channel_id]))
|
||||
}
|
||||
|
||||
pub fn channels(&self) -> impl '_ + Iterator<Item = (usize, &Arc<Channel>)> {
|
||||
self.channel_paths.iter().map(move |path| {
|
||||
let id = path.last().unwrap();
|
||||
|
@ -154,7 +160,7 @@ impl ChannelStore {
|
|||
|
||||
pub fn has_open_channel_buffer(&self, channel_id: ChannelId, cx: &AppContext) -> bool {
|
||||
if let Some(buffer) = self.opened_buffers.get(&channel_id) {
|
||||
if let OpenedChannelBuffer::Open(buffer) = buffer {
|
||||
if let OpenedModelHandle::Open(buffer) = buffer {
|
||||
return buffer.upgrade(cx).is_some();
|
||||
}
|
||||
}
|
||||
|
@ -166,24 +172,62 @@ impl ChannelStore {
|
|||
channel_id: ChannelId,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<ModelHandle<ChannelBuffer>>> {
|
||||
// Make sure that a given channel buffer is only opened once per
|
||||
// app instance, even if this method is called multiple times
|
||||
// with the same channel id while the first task is still running.
|
||||
let client = self.client.clone();
|
||||
self.open_channel_resource(
|
||||
channel_id,
|
||||
|this| &mut this.opened_buffers,
|
||||
|channel, cx| ChannelBuffer::new(channel, client, cx),
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn open_channel_chat(
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<ModelHandle<ChannelChat>>> {
|
||||
let client = self.client.clone();
|
||||
let user_store = self.user_store.clone();
|
||||
self.open_channel_resource(
|
||||
channel_id,
|
||||
|this| &mut this.opened_chats,
|
||||
|channel, cx| ChannelChat::new(channel, user_store, client, cx),
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
/// Asynchronously open a given resource associated with a channel.
|
||||
///
|
||||
/// Make sure that the resource is only opened once, even if this method
|
||||
/// is called multiple times with the same channel id while the first task
|
||||
/// is still running.
|
||||
fn open_channel_resource<T: Entity, F, Fut>(
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
get_map: fn(&mut Self) -> &mut HashMap<ChannelId, OpenedModelHandle<T>>,
|
||||
load: F,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<ModelHandle<T>>>
|
||||
where
|
||||
F: 'static + FnOnce(Arc<Channel>, AsyncAppContext) -> Fut,
|
||||
Fut: Future<Output = Result<ModelHandle<T>>>,
|
||||
{
|
||||
let task = loop {
|
||||
match self.opened_buffers.entry(channel_id) {
|
||||
match get_map(self).entry(channel_id) {
|
||||
hash_map::Entry::Occupied(e) => match e.get() {
|
||||
OpenedChannelBuffer::Open(buffer) => {
|
||||
if let Some(buffer) = buffer.upgrade(cx) {
|
||||
break Task::ready(Ok(buffer)).shared();
|
||||
OpenedModelHandle::Open(model) => {
|
||||
if let Some(model) = model.upgrade(cx) {
|
||||
break Task::ready(Ok(model)).shared();
|
||||
} else {
|
||||
self.opened_buffers.remove(&channel_id);
|
||||
get_map(self).remove(&channel_id);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
OpenedChannelBuffer::Loading(task) => break task.clone(),
|
||||
OpenedModelHandle::Loading(task) => {
|
||||
break task.clone();
|
||||
}
|
||||
},
|
||||
hash_map::Entry::Vacant(e) => {
|
||||
let client = self.client.clone();
|
||||
let task = cx
|
||||
.spawn(|this, cx| async move {
|
||||
let channel = this.read_with(&cx, |this, _| {
|
||||
|
@ -192,30 +236,24 @@ impl ChannelStore {
|
|||
})
|
||||
})?;
|
||||
|
||||
ChannelBuffer::new(channel, client, cx)
|
||||
.await
|
||||
.map_err(Arc::new)
|
||||
load(channel, cx).await.map_err(Arc::new)
|
||||
})
|
||||
.shared();
|
||||
e.insert(OpenedChannelBuffer::Loading(task.clone()));
|
||||
|
||||
e.insert(OpenedModelHandle::Loading(task.clone()));
|
||||
cx.spawn({
|
||||
let task = task.clone();
|
||||
|this, mut cx| async move {
|
||||
let result = task.await;
|
||||
this.update(&mut cx, |this, cx| match result {
|
||||
Ok(buffer) => {
|
||||
cx.observe_release(&buffer, move |this, _, _| {
|
||||
this.opened_buffers.remove(&channel_id);
|
||||
})
|
||||
.detach();
|
||||
this.opened_buffers.insert(
|
||||
this.update(&mut cx, |this, _| match result {
|
||||
Ok(model) => {
|
||||
get_map(this).insert(
|
||||
channel_id,
|
||||
OpenedChannelBuffer::Open(buffer.downgrade()),
|
||||
OpenedModelHandle::Open(model.downgrade()),
|
||||
);
|
||||
}
|
||||
Err(error) => {
|
||||
log::error!("failed to open channel buffer {error:?}");
|
||||
this.opened_buffers.remove(&channel_id);
|
||||
Err(_) => {
|
||||
get_map(this).remove(&channel_id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -494,9 +532,19 @@ impl ChannelStore {
|
|||
fn handle_connect(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
self.disconnect_channel_buffers_task.take();
|
||||
|
||||
for chat in self.opened_chats.values() {
|
||||
if let OpenedModelHandle::Open(chat) = chat {
|
||||
if let Some(chat) = chat.upgrade(cx) {
|
||||
chat.update(cx, |chat, cx| {
|
||||
chat.rejoin(cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut buffer_versions = Vec::new();
|
||||
for buffer in self.opened_buffers.values() {
|
||||
if let OpenedChannelBuffer::Open(buffer) = buffer {
|
||||
if let OpenedModelHandle::Open(buffer) = buffer {
|
||||
if let Some(buffer) = buffer.upgrade(cx) {
|
||||
let channel_buffer = buffer.read(cx);
|
||||
let buffer = channel_buffer.buffer().read(cx);
|
||||
|
@ -522,7 +570,7 @@ impl ChannelStore {
|
|||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.opened_buffers.retain(|_, buffer| match buffer {
|
||||
OpenedChannelBuffer::Open(channel_buffer) => {
|
||||
OpenedModelHandle::Open(channel_buffer) => {
|
||||
let Some(channel_buffer) = channel_buffer.upgrade(cx) else {
|
||||
return false;
|
||||
};
|
||||
|
@ -583,7 +631,7 @@ impl ChannelStore {
|
|||
false
|
||||
})
|
||||
}
|
||||
OpenedChannelBuffer::Loading(_) => true,
|
||||
OpenedModelHandle::Loading(_) => true,
|
||||
});
|
||||
});
|
||||
anyhow::Ok(())
|
||||
|
@ -605,7 +653,7 @@ impl ChannelStore {
|
|||
if let Some(this) = this.upgrade(&cx) {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
for (_, buffer) in this.opened_buffers.drain() {
|
||||
if let OpenedChannelBuffer::Open(buffer) = buffer {
|
||||
if let OpenedModelHandle::Open(buffer) = buffer {
|
||||
if let Some(buffer) = buffer.upgrade(cx) {
|
||||
buffer.update(cx, |buffer, cx| buffer.disconnect(cx));
|
||||
}
|
||||
|
@ -654,7 +702,7 @@ impl ChannelStore {
|
|||
|
||||
for channel_id in &payload.remove_channels {
|
||||
let channel_id = *channel_id;
|
||||
if let Some(OpenedChannelBuffer::Open(buffer)) =
|
||||
if let Some(OpenedModelHandle::Open(buffer)) =
|
||||
self.opened_buffers.remove(&channel_id)
|
||||
{
|
||||
if let Some(buffer) = buffer.upgrade(cx) {
|
||||
|
|
|
@ -1,16 +1,15 @@
|
|||
use crate::channel_chat::ChannelChatEvent;
|
||||
|
||||
use super::*;
|
||||
use client::{Client, UserStore};
|
||||
use gpui::{AppContext, ModelHandle};
|
||||
use client::{test::FakeServer, Client, UserStore};
|
||||
use gpui::{AppContext, ModelHandle, TestAppContext};
|
||||
use rpc::proto;
|
||||
use settings::SettingsStore;
|
||||
use util::http::FakeHttpClient;
|
||||
|
||||
#[gpui::test]
|
||||
fn test_update_channels(cx: &mut AppContext) {
|
||||
let http = FakeHttpClient::with_404_response();
|
||||
let client = Client::new(http.clone(), cx);
|
||||
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
|
||||
|
||||
let channel_store = cx.add_model(|cx| ChannelStore::new(client, user_store, cx));
|
||||
let channel_store = init_test(cx);
|
||||
|
||||
update_channels(
|
||||
&channel_store,
|
||||
|
@ -78,11 +77,7 @@ fn test_update_channels(cx: &mut AppContext) {
|
|||
|
||||
#[gpui::test]
|
||||
fn test_dangling_channel_paths(cx: &mut AppContext) {
|
||||
let http = FakeHttpClient::with_404_response();
|
||||
let client = Client::new(http.clone(), cx);
|
||||
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
|
||||
|
||||
let channel_store = cx.add_model(|cx| ChannelStore::new(client, user_store, cx));
|
||||
let channel_store = init_test(cx);
|
||||
|
||||
update_channels(
|
||||
&channel_store,
|
||||
|
@ -137,6 +132,208 @@ fn test_dangling_channel_paths(cx: &mut AppContext) {
|
|||
assert_channels(&channel_store, &[(0, "a".to_string(), true)], cx);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_channel_messages(cx: &mut TestAppContext) {
|
||||
let user_id = 5;
|
||||
let channel_id = 5;
|
||||
let channel_store = cx.update(init_test);
|
||||
let client = channel_store.read_with(cx, |s, _| s.client());
|
||||
let server = FakeServer::for_client(user_id, &client, cx).await;
|
||||
|
||||
// Get the available channels.
|
||||
server.send(proto::UpdateChannels {
|
||||
channels: vec![proto::Channel {
|
||||
id: channel_id,
|
||||
name: "the-channel".to_string(),
|
||||
parent_id: None,
|
||||
}],
|
||||
..Default::default()
|
||||
});
|
||||
cx.foreground().run_until_parked();
|
||||
cx.read(|cx| {
|
||||
assert_channels(&channel_store, &[(0, "the-channel".to_string(), false)], cx);
|
||||
});
|
||||
|
||||
let get_users = server.receive::<proto::GetUsers>().await.unwrap();
|
||||
assert_eq!(get_users.payload.user_ids, vec![5]);
|
||||
server.respond(
|
||||
get_users.receipt(),
|
||||
proto::UsersResponse {
|
||||
users: vec![proto::User {
|
||||
id: 5,
|
||||
github_login: "nathansobo".into(),
|
||||
avatar_url: "http://avatar.com/nathansobo".into(),
|
||||
}],
|
||||
},
|
||||
);
|
||||
|
||||
// Join a channel and populate its existing messages.
|
||||
let channel = channel_store.update(cx, |store, cx| {
|
||||
let channel_id = store.channels().next().unwrap().1.id;
|
||||
store.open_channel_chat(channel_id, cx)
|
||||
});
|
||||
let join_channel = server.receive::<proto::JoinChannelChat>().await.unwrap();
|
||||
server.respond(
|
||||
join_channel.receipt(),
|
||||
proto::JoinChannelChatResponse {
|
||||
messages: vec![
|
||||
proto::ChannelMessage {
|
||||
id: 10,
|
||||
body: "a".into(),
|
||||
timestamp: 1000,
|
||||
sender_id: 5,
|
||||
nonce: Some(1.into()),
|
||||
},
|
||||
proto::ChannelMessage {
|
||||
id: 11,
|
||||
body: "b".into(),
|
||||
timestamp: 1001,
|
||||
sender_id: 6,
|
||||
nonce: Some(2.into()),
|
||||
},
|
||||
],
|
||||
done: false,
|
||||
},
|
||||
);
|
||||
|
||||
cx.foreground().start_waiting();
|
||||
|
||||
// Client requests all users for the received messages
|
||||
let mut get_users = server.receive::<proto::GetUsers>().await.unwrap();
|
||||
get_users.payload.user_ids.sort();
|
||||
assert_eq!(get_users.payload.user_ids, vec![6]);
|
||||
server.respond(
|
||||
get_users.receipt(),
|
||||
proto::UsersResponse {
|
||||
users: vec![proto::User {
|
||||
id: 6,
|
||||
github_login: "maxbrunsfeld".into(),
|
||||
avatar_url: "http://avatar.com/maxbrunsfeld".into(),
|
||||
}],
|
||||
},
|
||||
);
|
||||
|
||||
let channel = channel.await.unwrap();
|
||||
channel.read_with(cx, |channel, _| {
|
||||
assert_eq!(
|
||||
channel
|
||||
.messages_in_range(0..2)
|
||||
.map(|message| (message.sender.github_login.clone(), message.body.clone()))
|
||||
.collect::<Vec<_>>(),
|
||||
&[
|
||||
("nathansobo".into(), "a".into()),
|
||||
("maxbrunsfeld".into(), "b".into())
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
// Receive a new message.
|
||||
server.send(proto::ChannelMessageSent {
|
||||
channel_id,
|
||||
message: Some(proto::ChannelMessage {
|
||||
id: 12,
|
||||
body: "c".into(),
|
||||
timestamp: 1002,
|
||||
sender_id: 7,
|
||||
nonce: Some(3.into()),
|
||||
}),
|
||||
});
|
||||
|
||||
// Client requests user for message since they haven't seen them yet
|
||||
let get_users = server.receive::<proto::GetUsers>().await.unwrap();
|
||||
assert_eq!(get_users.payload.user_ids, vec![7]);
|
||||
server.respond(
|
||||
get_users.receipt(),
|
||||
proto::UsersResponse {
|
||||
users: vec![proto::User {
|
||||
id: 7,
|
||||
github_login: "as-cii".into(),
|
||||
avatar_url: "http://avatar.com/as-cii".into(),
|
||||
}],
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
channel.next_event(cx).await,
|
||||
ChannelChatEvent::MessagesUpdated {
|
||||
old_range: 2..2,
|
||||
new_count: 1,
|
||||
}
|
||||
);
|
||||
channel.read_with(cx, |channel, _| {
|
||||
assert_eq!(
|
||||
channel
|
||||
.messages_in_range(2..3)
|
||||
.map(|message| (message.sender.github_login.clone(), message.body.clone()))
|
||||
.collect::<Vec<_>>(),
|
||||
&[("as-cii".into(), "c".into())]
|
||||
)
|
||||
});
|
||||
|
||||
// Scroll up to view older messages.
|
||||
channel.update(cx, |channel, cx| {
|
||||
assert!(channel.load_more_messages(cx));
|
||||
});
|
||||
let get_messages = server.receive::<proto::GetChannelMessages>().await.unwrap();
|
||||
assert_eq!(get_messages.payload.channel_id, 5);
|
||||
assert_eq!(get_messages.payload.before_message_id, 10);
|
||||
server.respond(
|
||||
get_messages.receipt(),
|
||||
proto::GetChannelMessagesResponse {
|
||||
done: true,
|
||||
messages: vec![
|
||||
proto::ChannelMessage {
|
||||
id: 8,
|
||||
body: "y".into(),
|
||||
timestamp: 998,
|
||||
sender_id: 5,
|
||||
nonce: Some(4.into()),
|
||||
},
|
||||
proto::ChannelMessage {
|
||||
id: 9,
|
||||
body: "z".into(),
|
||||
timestamp: 999,
|
||||
sender_id: 6,
|
||||
nonce: Some(5.into()),
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
channel.next_event(cx).await,
|
||||
ChannelChatEvent::MessagesUpdated {
|
||||
old_range: 0..0,
|
||||
new_count: 2,
|
||||
}
|
||||
);
|
||||
channel.read_with(cx, |channel, _| {
|
||||
assert_eq!(
|
||||
channel
|
||||
.messages_in_range(0..2)
|
||||
.map(|message| (message.sender.github_login.clone(), message.body.clone()))
|
||||
.collect::<Vec<_>>(),
|
||||
&[
|
||||
("nathansobo".into(), "y".into()),
|
||||
("maxbrunsfeld".into(), "z".into())
|
||||
]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut AppContext) -> ModelHandle<ChannelStore> {
|
||||
let http = FakeHttpClient::with_404_response();
|
||||
let client = Client::new(http.clone(), cx);
|
||||
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
|
||||
|
||||
cx.foreground().forbid_parking();
|
||||
cx.set_global(SettingsStore::test(cx));
|
||||
crate::init(&client);
|
||||
client::init(&client, cx);
|
||||
|
||||
cx.add_model(|cx| ChannelStore::new(client, user_store, cx))
|
||||
}
|
||||
|
||||
fn update_channels(
|
||||
channel_store: &ModelHandle<ChannelStore>,
|
||||
message: proto::UpdateChannels,
|
||||
|
|
|
@ -56,6 +56,13 @@ struct ClickhouseEventWrapper {
|
|||
event: ClickhouseEvent,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AssistantKind {
|
||||
Panel,
|
||||
Inline,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum ClickhouseEvent {
|
||||
|
@ -76,6 +83,11 @@ pub enum ClickhouseEvent {
|
|||
room_id: Option<u64>,
|
||||
channel_id: Option<u64>,
|
||||
},
|
||||
Assistant {
|
||||
conversation_id: Option<String>,
|
||||
kind: AssistantKind,
|
||||
model: &'static str,
|
||||
},
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
|
|
|
@ -170,8 +170,7 @@ impl FakeServer {
|
|||
staff: false,
|
||||
flags: Default::default(),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -182,11 +181,7 @@ impl FakeServer {
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn respond<T: proto::RequestMessage>(
|
||||
&self,
|
||||
receipt: Receipt<T>,
|
||||
response: T::Response,
|
||||
) {
|
||||
pub fn respond<T: proto::RequestMessage>(&self, receipt: Receipt<T>, response: T::Response) {
|
||||
self.peer.respond(receipt, response).unwrap()
|
||||
}
|
||||
|
||||
|
|
|
@ -192,6 +192,26 @@ CREATE TABLE "channels" (
|
|||
"created_at" TIMESTAMP NOT NULL DEFAULT now
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "channel_chat_participants" (
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
"user_id" INTEGER NOT NULL REFERENCES users (id),
|
||||
"channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE,
|
||||
"connection_id" INTEGER NOT NULL,
|
||||
"connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX "index_channel_chat_participants_on_channel_id" ON "channel_chat_participants" ("channel_id");
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "channel_messages" (
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
"channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE,
|
||||
"sender_id" INTEGER NOT NULL REFERENCES users (id),
|
||||
"body" TEXT NOT NULL,
|
||||
"sent_at" TIMESTAMP,
|
||||
"nonce" BLOB NOT NULL
|
||||
);
|
||||
CREATE INDEX "index_channel_messages_on_channel_id" ON "channel_messages" ("channel_id");
|
||||
CREATE UNIQUE INDEX "index_channel_messages_on_nonce" ON "channel_messages" ("nonce");
|
||||
|
||||
CREATE TABLE "channel_paths" (
|
||||
"id_path" TEXT NOT NULL PRIMARY KEY,
|
||||
"channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
CREATE TABLE IF NOT EXISTS "channel_messages" (
|
||||
"id" SERIAL PRIMARY KEY,
|
||||
"channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE,
|
||||
"sender_id" INTEGER NOT NULL REFERENCES users (id),
|
||||
"body" TEXT NOT NULL,
|
||||
"sent_at" TIMESTAMP,
|
||||
"nonce" UUID NOT NULL
|
||||
);
|
||||
CREATE INDEX "index_channel_messages_on_channel_id" ON "channel_messages" ("channel_id");
|
||||
CREATE UNIQUE INDEX "index_channel_messages_on_nonce" ON "channel_messages" ("nonce");
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "channel_chat_participants" (
|
||||
"id" SERIAL PRIMARY KEY,
|
||||
"user_id" INTEGER NOT NULL REFERENCES users (id),
|
||||
"channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE,
|
||||
"connection_id" INTEGER NOT NULL,
|
||||
"connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX "index_channel_chat_participants_on_channel_id" ON "channel_chat_participants" ("channel_id");
|
|
@ -112,8 +112,10 @@ fn value_to_integer(v: Value) -> Result<i32, ValueTypeErr> {
|
|||
|
||||
id_type!(BufferId);
|
||||
id_type!(AccessTokenId);
|
||||
id_type!(ChannelChatParticipantId);
|
||||
id_type!(ChannelId);
|
||||
id_type!(ChannelMemberId);
|
||||
id_type!(MessageId);
|
||||
id_type!(ContactId);
|
||||
id_type!(FollowerId);
|
||||
id_type!(RoomId);
|
||||
|
|
|
@ -4,6 +4,7 @@ pub mod access_tokens;
|
|||
pub mod buffers;
|
||||
pub mod channels;
|
||||
pub mod contacts;
|
||||
pub mod messages;
|
||||
pub mod projects;
|
||||
pub mod rooms;
|
||||
pub mod servers;
|
||||
|
|
|
@ -249,6 +249,29 @@ impl Database {
|
|||
.await
|
||||
}
|
||||
|
||||
pub async fn channel_buffer_connection_lost(
|
||||
&self,
|
||||
connection: ConnectionId,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<()> {
|
||||
channel_buffer_collaborator::Entity::update_many()
|
||||
.filter(
|
||||
Condition::all()
|
||||
.add(channel_buffer_collaborator::Column::ConnectionId.eq(connection.id as i32))
|
||||
.add(
|
||||
channel_buffer_collaborator::Column::ConnectionServerId
|
||||
.eq(connection.owner_id as i32),
|
||||
),
|
||||
)
|
||||
.set(channel_buffer_collaborator::ActiveModel {
|
||||
connection_lost: ActiveValue::set(true),
|
||||
..Default::default()
|
||||
})
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn leave_channel_buffers(
|
||||
&self,
|
||||
connection: ConnectionId,
|
||||
|
|
214
crates/collab/src/db/queries/messages.rs
Normal file
214
crates/collab/src/db/queries/messages.rs
Normal file
|
@ -0,0 +1,214 @@
|
|||
use super::*;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
impl Database {
|
||||
pub async fn join_channel_chat(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
connection_id: ConnectionId,
|
||||
user_id: UserId,
|
||||
) -> Result<()> {
|
||||
self.transaction(|tx| async move {
|
||||
self.check_user_is_channel_member(channel_id, user_id, &*tx)
|
||||
.await?;
|
||||
channel_chat_participant::ActiveModel {
|
||||
id: ActiveValue::NotSet,
|
||||
channel_id: ActiveValue::Set(channel_id),
|
||||
user_id: ActiveValue::Set(user_id),
|
||||
connection_id: ActiveValue::Set(connection_id.id as i32),
|
||||
connection_server_id: ActiveValue::Set(ServerId(connection_id.owner_id as i32)),
|
||||
}
|
||||
.insert(&*tx)
|
||||
.await?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn channel_chat_connection_lost(
|
||||
&self,
|
||||
connection_id: ConnectionId,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<()> {
|
||||
channel_chat_participant::Entity::delete_many()
|
||||
.filter(
|
||||
Condition::all()
|
||||
.add(
|
||||
channel_chat_participant::Column::ConnectionServerId
|
||||
.eq(connection_id.owner_id),
|
||||
)
|
||||
.add(channel_chat_participant::Column::ConnectionId.eq(connection_id.id)),
|
||||
)
|
||||
.exec(tx)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn leave_channel_chat(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
connection_id: ConnectionId,
|
||||
_user_id: UserId,
|
||||
) -> Result<()> {
|
||||
self.transaction(|tx| async move {
|
||||
channel_chat_participant::Entity::delete_many()
|
||||
.filter(
|
||||
Condition::all()
|
||||
.add(
|
||||
channel_chat_participant::Column::ConnectionServerId
|
||||
.eq(connection_id.owner_id),
|
||||
)
|
||||
.add(channel_chat_participant::Column::ConnectionId.eq(connection_id.id))
|
||||
.add(channel_chat_participant::Column::ChannelId.eq(channel_id)),
|
||||
)
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_channel_messages(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
user_id: UserId,
|
||||
count: usize,
|
||||
before_message_id: Option<MessageId>,
|
||||
) -> Result<Vec<proto::ChannelMessage>> {
|
||||
self.transaction(|tx| async move {
|
||||
self.check_user_is_channel_member(channel_id, user_id, &*tx)
|
||||
.await?;
|
||||
|
||||
let mut condition =
|
||||
Condition::all().add(channel_message::Column::ChannelId.eq(channel_id));
|
||||
|
||||
if let Some(before_message_id) = before_message_id {
|
||||
condition = condition.add(channel_message::Column::Id.lt(before_message_id));
|
||||
}
|
||||
|
||||
let mut rows = channel_message::Entity::find()
|
||||
.filter(condition)
|
||||
.limit(count as u64)
|
||||
.stream(&*tx)
|
||||
.await?;
|
||||
|
||||
let mut messages = Vec::new();
|
||||
while let Some(row) = rows.next().await {
|
||||
let row = row?;
|
||||
let nonce = row.nonce.as_u64_pair();
|
||||
messages.push(proto::ChannelMessage {
|
||||
id: row.id.to_proto(),
|
||||
sender_id: row.sender_id.to_proto(),
|
||||
body: row.body,
|
||||
timestamp: row.sent_at.assume_utc().unix_timestamp() as u64,
|
||||
nonce: Some(proto::Nonce {
|
||||
upper_half: nonce.0,
|
||||
lower_half: nonce.1,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(messages)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn create_channel_message(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
user_id: UserId,
|
||||
body: &str,
|
||||
timestamp: OffsetDateTime,
|
||||
nonce: u128,
|
||||
) -> Result<(MessageId, Vec<ConnectionId>)> {
|
||||
self.transaction(|tx| async move {
|
||||
let mut rows = channel_chat_participant::Entity::find()
|
||||
.filter(channel_chat_participant::Column::ChannelId.eq(channel_id))
|
||||
.stream(&*tx)
|
||||
.await?;
|
||||
|
||||
let mut is_participant = false;
|
||||
let mut participant_connection_ids = Vec::new();
|
||||
while let Some(row) = rows.next().await {
|
||||
let row = row?;
|
||||
if row.user_id == user_id {
|
||||
is_participant = true;
|
||||
}
|
||||
participant_connection_ids.push(row.connection());
|
||||
}
|
||||
drop(rows);
|
||||
|
||||
if !is_participant {
|
||||
Err(anyhow!("not a chat participant"))?;
|
||||
}
|
||||
|
||||
let timestamp = timestamp.to_offset(time::UtcOffset::UTC);
|
||||
let timestamp = time::PrimitiveDateTime::new(timestamp.date(), timestamp.time());
|
||||
|
||||
let message = channel_message::Entity::insert(channel_message::ActiveModel {
|
||||
channel_id: ActiveValue::Set(channel_id),
|
||||
sender_id: ActiveValue::Set(user_id),
|
||||
body: ActiveValue::Set(body.to_string()),
|
||||
sent_at: ActiveValue::Set(timestamp),
|
||||
nonce: ActiveValue::Set(Uuid::from_u128(nonce)),
|
||||
id: ActiveValue::NotSet,
|
||||
})
|
||||
.on_conflict(
|
||||
OnConflict::column(channel_message::Column::Nonce)
|
||||
.update_column(channel_message::Column::Nonce)
|
||||
.to_owned(),
|
||||
)
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
|
||||
#[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)]
|
||||
enum QueryConnectionId {
|
||||
ConnectionId,
|
||||
}
|
||||
|
||||
Ok((message.last_insert_id, participant_connection_ids))
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn remove_channel_message(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
message_id: MessageId,
|
||||
user_id: UserId,
|
||||
) -> Result<Vec<ConnectionId>> {
|
||||
self.transaction(|tx| async move {
|
||||
let mut rows = channel_chat_participant::Entity::find()
|
||||
.filter(channel_chat_participant::Column::ChannelId.eq(channel_id))
|
||||
.stream(&*tx)
|
||||
.await?;
|
||||
|
||||
let mut is_participant = false;
|
||||
let mut participant_connection_ids = Vec::new();
|
||||
while let Some(row) = rows.next().await {
|
||||
let row = row?;
|
||||
if row.user_id == user_id {
|
||||
is_participant = true;
|
||||
}
|
||||
participant_connection_ids.push(row.connection());
|
||||
}
|
||||
drop(rows);
|
||||
|
||||
if !is_participant {
|
||||
Err(anyhow!("not a chat participant"))?;
|
||||
}
|
||||
|
||||
let result = channel_message::Entity::delete_by_id(message_id)
|
||||
.filter(channel_message::Column::SenderId.eq(user_id))
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
if result.rows_affected == 0 {
|
||||
Err(anyhow!("no such message"))?;
|
||||
}
|
||||
|
||||
Ok(participant_connection_ids)
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
|
@ -890,54 +890,43 @@ impl Database {
|
|||
|
||||
pub async fn connection_lost(&self, connection: ConnectionId) -> Result<()> {
|
||||
self.transaction(|tx| async move {
|
||||
let participant = room_participant::Entity::find()
|
||||
.filter(
|
||||
Condition::all()
|
||||
.add(
|
||||
room_participant::Column::AnsweringConnectionId
|
||||
.eq(connection.id as i32),
|
||||
)
|
||||
.add(
|
||||
room_participant::Column::AnsweringConnectionServerId
|
||||
.eq(connection.owner_id as i32),
|
||||
),
|
||||
)
|
||||
.one(&*tx)
|
||||
self.room_connection_lost(connection, &*tx).await?;
|
||||
self.channel_buffer_connection_lost(connection, &*tx)
|
||||
.await?;
|
||||
|
||||
if let Some(participant) = participant {
|
||||
room_participant::Entity::update(room_participant::ActiveModel {
|
||||
answering_connection_lost: ActiveValue::set(true),
|
||||
..participant.into_active_model()
|
||||
})
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
}
|
||||
|
||||
channel_buffer_collaborator::Entity::update_many()
|
||||
.filter(
|
||||
Condition::all()
|
||||
.add(
|
||||
channel_buffer_collaborator::Column::ConnectionId
|
||||
.eq(connection.id as i32),
|
||||
)
|
||||
.add(
|
||||
channel_buffer_collaborator::Column::ConnectionServerId
|
||||
.eq(connection.owner_id as i32),
|
||||
),
|
||||
)
|
||||
.set(channel_buffer_collaborator::ActiveModel {
|
||||
connection_lost: ActiveValue::set(true),
|
||||
..Default::default()
|
||||
})
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
|
||||
self.channel_chat_connection_lost(connection, &*tx).await?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn room_connection_lost(
|
||||
&self,
|
||||
connection: ConnectionId,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<()> {
|
||||
let participant = room_participant::Entity::find()
|
||||
.filter(
|
||||
Condition::all()
|
||||
.add(room_participant::Column::AnsweringConnectionId.eq(connection.id as i32))
|
||||
.add(
|
||||
room_participant::Column::AnsweringConnectionServerId
|
||||
.eq(connection.owner_id as i32),
|
||||
),
|
||||
)
|
||||
.one(&*tx)
|
||||
.await?;
|
||||
|
||||
if let Some(participant) = participant {
|
||||
room_participant::Entity::update(room_participant::ActiveModel {
|
||||
answering_connection_lost: ActiveValue::set(true),
|
||||
..participant.into_active_model()
|
||||
})
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_incoming_call(
|
||||
room: &proto::Room,
|
||||
called_user_id: UserId,
|
||||
|
|
|
@ -4,7 +4,9 @@ pub mod buffer_operation;
|
|||
pub mod buffer_snapshot;
|
||||
pub mod channel;
|
||||
pub mod channel_buffer_collaborator;
|
||||
pub mod channel_chat_participant;
|
||||
pub mod channel_member;
|
||||
pub mod channel_message;
|
||||
pub mod channel_path;
|
||||
pub mod contact;
|
||||
pub mod feature_flag;
|
||||
|
|
|
@ -21,6 +21,8 @@ pub enum Relation {
|
|||
Member,
|
||||
#[sea_orm(has_many = "super::channel_buffer_collaborator::Entity")]
|
||||
BufferCollaborators,
|
||||
#[sea_orm(has_many = "super::channel_chat_participant::Entity")]
|
||||
ChatParticipants,
|
||||
}
|
||||
|
||||
impl Related<super::channel_member::Entity> for Entity {
|
||||
|
@ -46,3 +48,9 @@ impl Related<super::channel_buffer_collaborator::Entity> for Entity {
|
|||
Relation::BufferCollaborators.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::channel_chat_participant::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::ChatParticipants.def()
|
||||
}
|
||||
}
|
||||
|
|
41
crates/collab/src/db/tables/channel_chat_participant.rs
Normal file
41
crates/collab/src/db/tables/channel_chat_participant.rs
Normal file
|
@ -0,0 +1,41 @@
|
|||
use crate::db::{ChannelChatParticipantId, ChannelId, ServerId, UserId};
|
||||
use rpc::ConnectionId;
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "channel_chat_participants")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: ChannelChatParticipantId,
|
||||
pub channel_id: ChannelId,
|
||||
pub user_id: UserId,
|
||||
pub connection_id: i32,
|
||||
pub connection_server_id: ServerId,
|
||||
}
|
||||
|
||||
impl Model {
|
||||
pub fn connection(&self) -> ConnectionId {
|
||||
ConnectionId {
|
||||
owner_id: self.connection_server_id.0 as u32,
|
||||
id: self.connection_id as u32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::channel::Entity",
|
||||
from = "Column::ChannelId",
|
||||
to = "super::channel::Column::Id"
|
||||
)]
|
||||
Channel,
|
||||
}
|
||||
|
||||
impl Related<super::channel::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Channel.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
45
crates/collab/src/db/tables/channel_message.rs
Normal file
45
crates/collab/src/db/tables/channel_message.rs
Normal file
|
@ -0,0 +1,45 @@
|
|||
use crate::db::{ChannelId, MessageId, UserId};
|
||||
use sea_orm::entity::prelude::*;
|
||||
use time::PrimitiveDateTime;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "channel_messages")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: MessageId,
|
||||
pub channel_id: ChannelId,
|
||||
pub sender_id: UserId,
|
||||
pub body: String,
|
||||
pub sent_at: PrimitiveDateTime,
|
||||
pub nonce: Uuid,
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::channel::Entity",
|
||||
from = "Column::ChannelId",
|
||||
to = "super::channel::Column::Id"
|
||||
)]
|
||||
Channel,
|
||||
#[sea_orm(
|
||||
belongs_to = "super::user::Entity",
|
||||
from = "Column::SenderId",
|
||||
to = "super::user::Column::Id"
|
||||
)]
|
||||
Sender,
|
||||
}
|
||||
|
||||
impl Related<super::channel::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Channel.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::user::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Sender.def()
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
mod buffer_tests;
|
||||
mod db_tests;
|
||||
mod feature_flag_tests;
|
||||
mod message_tests;
|
||||
|
||||
use super::*;
|
||||
use gpui::executor::Background;
|
||||
|
|
59
crates/collab/src/db/tests/message_tests.rs
Normal file
59
crates/collab/src/db/tests/message_tests.rs
Normal file
|
@ -0,0 +1,59 @@
|
|||
use crate::{
|
||||
db::{Database, NewUserParams},
|
||||
test_both_dbs,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
test_both_dbs!(
|
||||
test_channel_message_nonces,
|
||||
test_channel_message_nonces_postgres,
|
||||
test_channel_message_nonces_sqlite
|
||||
);
|
||||
|
||||
async fn test_channel_message_nonces(db: &Arc<Database>) {
|
||||
let user = db
|
||||
.create_user(
|
||||
"user@example.com",
|
||||
false,
|
||||
NewUserParams {
|
||||
github_login: "user".into(),
|
||||
github_user_id: 1,
|
||||
invite_count: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.user_id;
|
||||
let channel = db
|
||||
.create_channel("channel", None, "room", 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)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let msg1_id = db
|
||||
.create_channel_message(channel, user, "1", OffsetDateTime::now_utc(), 1)
|
||||
.await
|
||||
.unwrap();
|
||||
let msg2_id = db
|
||||
.create_channel_message(channel, user, "2", OffsetDateTime::now_utc(), 2)
|
||||
.await
|
||||
.unwrap();
|
||||
let msg3_id = db
|
||||
.create_channel_message(channel, user, "3", OffsetDateTime::now_utc(), 1)
|
||||
.await
|
||||
.unwrap();
|
||||
let msg4_id = db
|
||||
.create_channel_message(channel, user, "4", OffsetDateTime::now_utc(), 2)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_ne!(msg1_id, msg2_id);
|
||||
assert_eq!(msg1_id, msg3_id);
|
||||
assert_eq!(msg2_id, msg4_id);
|
||||
}
|
|
@ -2,7 +2,10 @@ mod connection_pool;
|
|||
|
||||
use crate::{
|
||||
auth,
|
||||
db::{self, ChannelId, ChannelsForUser, Database, ProjectId, RoomId, ServerId, User, UserId},
|
||||
db::{
|
||||
self, ChannelId, ChannelsForUser, Database, MessageId, ProjectId, RoomId, ServerId, User,
|
||||
UserId,
|
||||
},
|
||||
executor::Executor,
|
||||
AppState, Result,
|
||||
};
|
||||
|
@ -56,6 +59,7 @@ use std::{
|
|||
},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use time::OffsetDateTime;
|
||||
use tokio::sync::{watch, Semaphore};
|
||||
use tower::ServiceBuilder;
|
||||
use tracing::{info_span, instrument, Instrument};
|
||||
|
@ -63,6 +67,9 @@ use tracing::{info_span, instrument, Instrument};
|
|||
pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
pub const CLEANUP_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
||||
const MESSAGE_COUNT_PER_PAGE: usize = 100;
|
||||
const MAX_MESSAGE_LEN: usize = 1024;
|
||||
|
||||
lazy_static! {
|
||||
static ref METRIC_CONNECTIONS: IntGauge =
|
||||
register_int_gauge!("connections", "number of connections").unwrap();
|
||||
|
@ -255,6 +262,11 @@ impl Server {
|
|||
.add_request_handler(get_channel_members)
|
||||
.add_request_handler(respond_to_channel_invite)
|
||||
.add_request_handler(join_channel)
|
||||
.add_request_handler(join_channel_chat)
|
||||
.add_message_handler(leave_channel_chat)
|
||||
.add_request_handler(send_channel_message)
|
||||
.add_request_handler(remove_channel_message)
|
||||
.add_request_handler(get_channel_messages)
|
||||
.add_request_handler(follow)
|
||||
.add_message_handler(unfollow)
|
||||
.add_message_handler(update_followers)
|
||||
|
@ -885,9 +897,8 @@ async fn connection_lost(
|
|||
room_updated(&room, &session.peer);
|
||||
}
|
||||
}
|
||||
|
||||
update_user_contacts(session.user_id, &session).await?;
|
||||
|
||||
|
||||
}
|
||||
_ = teardown.changed().fuse() => {}
|
||||
}
|
||||
|
@ -2633,6 +2644,131 @@ fn channel_buffer_updated<T: EnvelopedMessage>(
|
|||
});
|
||||
}
|
||||
|
||||
async fn send_channel_message(
|
||||
request: proto::SendChannelMessage,
|
||||
response: Response<proto::SendChannelMessage>,
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
// Validate the message body.
|
||||
let body = request.body.trim().to_string();
|
||||
if body.len() > MAX_MESSAGE_LEN {
|
||||
return Err(anyhow!("message is too long"))?;
|
||||
}
|
||||
if body.is_empty() {
|
||||
return Err(anyhow!("message can't be blank"))?;
|
||||
}
|
||||
|
||||
let timestamp = OffsetDateTime::now_utc();
|
||||
let nonce = request
|
||||
.nonce
|
||||
.ok_or_else(|| anyhow!("nonce can't be blank"))?;
|
||||
|
||||
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||
let (message_id, connection_ids) = session
|
||||
.db()
|
||||
.await
|
||||
.create_channel_message(
|
||||
channel_id,
|
||||
session.user_id,
|
||||
&body,
|
||||
timestamp,
|
||||
nonce.clone().into(),
|
||||
)
|
||||
.await?;
|
||||
let message = proto::ChannelMessage {
|
||||
sender_id: session.user_id.to_proto(),
|
||||
id: message_id.to_proto(),
|
||||
body,
|
||||
timestamp: timestamp.unix_timestamp() as u64,
|
||||
nonce: Some(nonce),
|
||||
};
|
||||
broadcast(Some(session.connection_id), connection_ids, |connection| {
|
||||
session.peer.send(
|
||||
connection,
|
||||
proto::ChannelMessageSent {
|
||||
channel_id: channel_id.to_proto(),
|
||||
message: Some(message.clone()),
|
||||
},
|
||||
)
|
||||
});
|
||||
response.send(proto::SendChannelMessageResponse {
|
||||
message: Some(message),
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_channel_message(
|
||||
request: proto::RemoveChannelMessage,
|
||||
response: Response<proto::RemoveChannelMessage>,
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||
let message_id = MessageId::from_proto(request.message_id);
|
||||
let connection_ids = session
|
||||
.db()
|
||||
.await
|
||||
.remove_channel_message(channel_id, message_id, session.user_id)
|
||||
.await?;
|
||||
broadcast(Some(session.connection_id), connection_ids, |connection| {
|
||||
session.peer.send(connection, request.clone())
|
||||
});
|
||||
response.send(proto::Ack {})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn join_channel_chat(
|
||||
request: proto::JoinChannelChat,
|
||||
response: Response<proto::JoinChannelChat>,
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||
|
||||
let db = session.db().await;
|
||||
db.join_channel_chat(channel_id, session.connection_id, session.user_id)
|
||||
.await?;
|
||||
let messages = db
|
||||
.get_channel_messages(channel_id, session.user_id, MESSAGE_COUNT_PER_PAGE, None)
|
||||
.await?;
|
||||
response.send(proto::JoinChannelChatResponse {
|
||||
done: messages.len() < MESSAGE_COUNT_PER_PAGE,
|
||||
messages,
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn leave_channel_chat(request: proto::LeaveChannelChat, session: Session) -> Result<()> {
|
||||
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||
session
|
||||
.db()
|
||||
.await
|
||||
.leave_channel_chat(channel_id, session.connection_id, session.user_id)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_channel_messages(
|
||||
request: proto::GetChannelMessages,
|
||||
response: Response<proto::GetChannelMessages>,
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||
let messages = session
|
||||
.db()
|
||||
.await
|
||||
.get_channel_messages(
|
||||
channel_id,
|
||||
session.user_id,
|
||||
MESSAGE_COUNT_PER_PAGE,
|
||||
Some(MessageId::from_proto(request.before_message_id)),
|
||||
)
|
||||
.await?;
|
||||
response.send(proto::GetChannelMessagesResponse {
|
||||
done: messages.len() < MESSAGE_COUNT_PER_PAGE,
|
||||
messages,
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_diff_base(request: proto::UpdateDiffBase, session: Session) -> Result<()> {
|
||||
let project_id = ProjectId::from_proto(request.project_id);
|
||||
let project_connection_ids = session
|
||||
|
|
|
@ -2,6 +2,7 @@ use call::Room;
|
|||
use gpui::{ModelHandle, TestAppContext};
|
||||
|
||||
mod channel_buffer_tests;
|
||||
mod channel_message_tests;
|
||||
mod channel_tests;
|
||||
mod integration_tests;
|
||||
mod random_channel_buffer_tests;
|
||||
|
|
214
crates/collab/src/tests/channel_message_tests.rs
Normal file
214
crates/collab/src/tests/channel_message_tests.rs
Normal file
|
@ -0,0 +1,214 @@
|
|||
use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer};
|
||||
use channel::{ChannelChat, ChannelMessageId};
|
||||
use gpui::{executor::Deterministic, ModelHandle, TestAppContext};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_basic_channel_messages(
|
||||
deterministic: Arc<Deterministic>,
|
||||
cx_a: &mut TestAppContext,
|
||||
cx_b: &mut TestAppContext,
|
||||
) {
|
||||
deterministic.forbid_parking();
|
||||
let mut server = TestServer::start(&deterministic).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
|
||||
let channel_id = server
|
||||
.make_channel("the-channel", (&client_a, cx_a), &mut [(&client_b, cx_b)])
|
||||
.await;
|
||||
|
||||
let channel_chat_a = client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |store, cx| store.open_channel_chat(channel_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let channel_chat_b = client_b
|
||||
.channel_store()
|
||||
.update(cx_b, |store, cx| store.open_channel_chat(channel_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
channel_chat_a
|
||||
.update(cx_a, |c, cx| c.send_message("one".into(), cx).unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
channel_chat_a
|
||||
.update(cx_a, |c, cx| c.send_message("two".into(), cx).unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
deterministic.run_until_parked();
|
||||
channel_chat_b
|
||||
.update(cx_b, |c, cx| c.send_message("three".into(), cx).unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
deterministic.run_until_parked();
|
||||
channel_chat_a.update(cx_a, |c, _| {
|
||||
assert_eq!(
|
||||
c.messages()
|
||||
.iter()
|
||||
.map(|m| m.body.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["one", "two", "three"]
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_rejoin_channel_chat(
|
||||
deterministic: Arc<Deterministic>,
|
||||
cx_a: &mut TestAppContext,
|
||||
cx_b: &mut TestAppContext,
|
||||
) {
|
||||
deterministic.forbid_parking();
|
||||
let mut server = TestServer::start(&deterministic).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
|
||||
let channel_id = server
|
||||
.make_channel("the-channel", (&client_a, cx_a), &mut [(&client_b, cx_b)])
|
||||
.await;
|
||||
|
||||
let channel_chat_a = client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |store, cx| store.open_channel_chat(channel_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let channel_chat_b = client_b
|
||||
.channel_store()
|
||||
.update(cx_b, |store, cx| store.open_channel_chat(channel_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
channel_chat_a
|
||||
.update(cx_a, |c, cx| c.send_message("one".into(), cx).unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
channel_chat_b
|
||||
.update(cx_b, |c, cx| c.send_message("two".into(), cx).unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
server.forbid_connections();
|
||||
server.disconnect_client(client_a.peer_id().unwrap());
|
||||
|
||||
// While client A is disconnected, clients A and B both send new messages.
|
||||
channel_chat_a
|
||||
.update(cx_a, |c, cx| c.send_message("three".into(), cx).unwrap())
|
||||
.await
|
||||
.unwrap_err();
|
||||
channel_chat_a
|
||||
.update(cx_a, |c, cx| c.send_message("four".into(), cx).unwrap())
|
||||
.await
|
||||
.unwrap_err();
|
||||
channel_chat_b
|
||||
.update(cx_b, |c, cx| c.send_message("five".into(), cx).unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
channel_chat_b
|
||||
.update(cx_b, |c, cx| c.send_message("six".into(), cx).unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Client A reconnects.
|
||||
server.allow_connections();
|
||||
deterministic.advance_clock(RECONNECT_TIMEOUT);
|
||||
|
||||
// Client A fetches the messages that were sent while they were disconnected
|
||||
// and resends their own messages which failed to send.
|
||||
let expected_messages = &["one", "two", "five", "six", "three", "four"];
|
||||
assert_messages(&channel_chat_a, expected_messages, cx_a);
|
||||
assert_messages(&channel_chat_b, expected_messages, cx_b);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_remove_channel_message(
|
||||
deterministic: Arc<Deterministic>,
|
||||
cx_a: &mut TestAppContext,
|
||||
cx_b: &mut TestAppContext,
|
||||
cx_c: &mut TestAppContext,
|
||||
) {
|
||||
deterministic.forbid_parking();
|
||||
let mut server = TestServer::start(&deterministic).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
let client_c = server.create_client(cx_c, "user_c").await;
|
||||
|
||||
let channel_id = server
|
||||
.make_channel(
|
||||
"the-channel",
|
||||
(&client_a, cx_a),
|
||||
&mut [(&client_b, cx_b), (&client_c, cx_c)],
|
||||
)
|
||||
.await;
|
||||
|
||||
let channel_chat_a = client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |store, cx| store.open_channel_chat(channel_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let channel_chat_b = client_b
|
||||
.channel_store()
|
||||
.update(cx_b, |store, cx| store.open_channel_chat(channel_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Client A sends some messages.
|
||||
channel_chat_a
|
||||
.update(cx_a, |c, cx| c.send_message("one".into(), cx).unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
channel_chat_a
|
||||
.update(cx_a, |c, cx| c.send_message("two".into(), cx).unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
channel_chat_a
|
||||
.update(cx_a, |c, cx| c.send_message("three".into(), cx).unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Clients A and B see all of the messages.
|
||||
deterministic.run_until_parked();
|
||||
let expected_messages = &["one", "two", "three"];
|
||||
assert_messages(&channel_chat_a, expected_messages, cx_a);
|
||||
assert_messages(&channel_chat_b, expected_messages, cx_b);
|
||||
|
||||
// Client A deletes one of their messages.
|
||||
channel_chat_a
|
||||
.update(cx_a, |c, cx| {
|
||||
let ChannelMessageId::Saved(id) = c.message(1).id else {
|
||||
panic!("message not saved")
|
||||
};
|
||||
c.remove_message(id, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Client B sees that the message is gone.
|
||||
deterministic.run_until_parked();
|
||||
let expected_messages = &["one", "three"];
|
||||
assert_messages(&channel_chat_a, expected_messages, cx_a);
|
||||
assert_messages(&channel_chat_b, expected_messages, cx_b);
|
||||
|
||||
// Client C joins the channel chat, and does not see the deleted message.
|
||||
let channel_chat_c = client_c
|
||||
.channel_store()
|
||||
.update(cx_c, |store, cx| store.open_channel_chat(channel_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_messages(&channel_chat_c, expected_messages, cx_c);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_messages(chat: &ModelHandle<ChannelChat>, messages: &[&str], cx: &mut TestAppContext) {
|
||||
assert_eq!(
|
||||
chat.read_with(cx, |chat, _| chat
|
||||
.messages()
|
||||
.iter()
|
||||
.map(|m| m.body.clone())
|
||||
.collect::<Vec<_>>(),),
|
||||
messages
|
||||
);
|
||||
}
|
|
@ -4825,7 +4825,7 @@ async fn test_project_search(
|
|||
let mut results = HashMap::default();
|
||||
let mut search_rx = project_b.update(cx_b, |project, cx| {
|
||||
project.search(
|
||||
SearchQuery::text("world", false, false, Vec::new(), Vec::new()),
|
||||
SearchQuery::text("world", false, false, Vec::new(), Vec::new()).unwrap(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
|
|
@ -869,7 +869,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
|||
|
||||
let mut search = project.update(cx, |project, cx| {
|
||||
project.search(
|
||||
SearchQuery::text(query, false, false, Vec::new(), Vec::new()),
|
||||
SearchQuery::text(query, false, false, Vec::new(), Vec::new()).unwrap(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
|
|
@ -6,7 +6,7 @@ use crate::{
|
|||
};
|
||||
use anyhow::anyhow;
|
||||
use call::ActiveCall;
|
||||
use channel::{channel_buffer::ChannelBuffer, ChannelStore};
|
||||
use channel::{ChannelBuffer, ChannelStore};
|
||||
use client::{
|
||||
self, proto::PeerId, Client, Connection, Credentials, EstablishConnectionError, UserStore,
|
||||
};
|
||||
|
|
|
@ -55,6 +55,7 @@ schemars.workspace = true
|
|||
postage.workspace = true
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
time.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
call = { path = "../call", features = ["test-support"] }
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
use anyhow::{anyhow, Result};
|
||||
use channel::{
|
||||
channel_buffer::{self, ChannelBuffer},
|
||||
ChannelId,
|
||||
};
|
||||
use call::ActiveCall;
|
||||
use channel::{ChannelBuffer, ChannelBufferEvent, ChannelId};
|
||||
use client::proto;
|
||||
use clock::ReplicaId;
|
||||
use collections::HashMap;
|
||||
|
@ -38,6 +36,30 @@ pub struct ChannelView {
|
|||
}
|
||||
|
||||
impl ChannelView {
|
||||
pub fn deploy(channel_id: ChannelId, workspace: ViewHandle<Workspace>, cx: &mut AppContext) {
|
||||
let pane = workspace.read(cx).active_pane().clone();
|
||||
let channel_view = Self::open(channel_id, pane.clone(), workspace.clone(), cx);
|
||||
cx.spawn(|mut cx| async move {
|
||||
let channel_view = channel_view.await?;
|
||||
pane.update(&mut cx, |pane, cx| {
|
||||
let room_id = ActiveCall::global(cx)
|
||||
.read(cx)
|
||||
.room()
|
||||
.map(|room| room.read(cx).id());
|
||||
ActiveCall::report_call_event_for_room(
|
||||
"open channel notes",
|
||||
room_id,
|
||||
Some(channel_id),
|
||||
&workspace.read(cx).app_state().client,
|
||||
cx,
|
||||
);
|
||||
pane.add_item(Box::new(channel_view), true, true, None, cx);
|
||||
});
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn open(
|
||||
channel_id: ChannelId,
|
||||
pane: ViewHandle<Pane>,
|
||||
|
@ -56,6 +78,7 @@ impl ChannelView {
|
|||
|
||||
cx.spawn(|mut cx| async move {
|
||||
let channel_buffer = channel_buffer.await?;
|
||||
|
||||
let markdown = markdown.await?;
|
||||
channel_buffer.update(&mut cx, |buffer, cx| {
|
||||
buffer.buffer().update(cx, |buffer, cx| {
|
||||
|
@ -78,7 +101,6 @@ impl ChannelView {
|
|||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let buffer = channel_buffer.read(cx).buffer();
|
||||
// buffer.update(cx, |buffer, cx| buffer.set_language(language, cx));
|
||||
let editor = cx.add_view(|cx| Editor::for_buffer(buffer, None, cx));
|
||||
let _editor_event_subscription = cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone()));
|
||||
|
||||
|
@ -118,14 +140,14 @@ impl ChannelView {
|
|||
fn handle_channel_buffer_event(
|
||||
&mut self,
|
||||
_: ModelHandle<ChannelBuffer>,
|
||||
event: &channel_buffer::Event,
|
||||
event: &ChannelBufferEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
channel_buffer::Event::CollaboratorsChanged => {
|
||||
ChannelBufferEvent::CollaboratorsChanged => {
|
||||
self.refresh_replica_id_map(cx);
|
||||
}
|
||||
channel_buffer::Event::Disconnected => self.editor.update(cx, |editor, cx| {
|
||||
ChannelBufferEvent::Disconnected => self.editor.update(cx, |editor, cx| {
|
||||
editor.set_read_only(true);
|
||||
cx.notify();
|
||||
}),
|
||||
|
|
701
crates/collab_ui/src/chat_panel.rs
Normal file
701
crates/collab_ui/src/chat_panel.rs
Normal file
|
@ -0,0 +1,701 @@
|
|||
use crate::{channel_view::ChannelView, ChatPanelSettings};
|
||||
use anyhow::Result;
|
||||
use call::ActiveCall;
|
||||
use channel::{ChannelChat, ChannelChatEvent, ChannelMessageId, ChannelStore};
|
||||
use client::Client;
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use editor::Editor;
|
||||
use feature_flags::{ChannelsAlpha, FeatureFlagAppExt};
|
||||
use gpui::{
|
||||
actions,
|
||||
elements::*,
|
||||
platform::{CursorStyle, MouseButton},
|
||||
serde_json,
|
||||
views::{ItemType, Select, SelectStyle},
|
||||
AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle, Subscription, Task, View,
|
||||
ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use language::language_settings::SoftWrap;
|
||||
use menu::Confirm;
|
||||
use project::Fs;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::SettingsStore;
|
||||
use std::sync::Arc;
|
||||
use theme::{IconButton, Theme};
|
||||
use time::{OffsetDateTime, UtcOffset};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
use workspace::{
|
||||
dock::{DockPosition, Panel},
|
||||
Workspace,
|
||||
};
|
||||
|
||||
const MESSAGE_LOADING_THRESHOLD: usize = 50;
|
||||
const CHAT_PANEL_KEY: &'static str = "ChatPanel";
|
||||
|
||||
pub struct ChatPanel {
|
||||
client: Arc<Client>,
|
||||
channel_store: ModelHandle<ChannelStore>,
|
||||
active_chat: Option<(ModelHandle<ChannelChat>, Subscription)>,
|
||||
message_list: ListState<ChatPanel>,
|
||||
input_editor: ViewHandle<Editor>,
|
||||
channel_select: ViewHandle<Select>,
|
||||
local_timezone: UtcOffset,
|
||||
fs: Arc<dyn Fs>,
|
||||
width: Option<f32>,
|
||||
pending_serialization: Task<Option<()>>,
|
||||
subscriptions: Vec<gpui::Subscription>,
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
has_focus: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SerializedChatPanel {
|
||||
width: Option<f32>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Event {
|
||||
DockPositionChanged,
|
||||
Focus,
|
||||
Dismissed,
|
||||
}
|
||||
|
||||
actions!(
|
||||
chat_panel,
|
||||
[LoadMoreMessages, ToggleFocus, OpenChannelNotes, JoinCall]
|
||||
);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(ChatPanel::send);
|
||||
cx.add_action(ChatPanel::load_more_messages);
|
||||
cx.add_action(ChatPanel::open_notes);
|
||||
cx.add_action(ChatPanel::join_call);
|
||||
}
|
||||
|
||||
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 input_editor = cx.add_view(|cx| {
|
||||
let mut editor = Editor::auto_height(
|
||||
4,
|
||||
Some(Arc::new(|theme| theme.chat_panel.input_editor.clone())),
|
||||
cx,
|
||||
);
|
||||
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
|
||||
editor
|
||||
});
|
||||
|
||||
let workspace_handle = workspace.weak_handle();
|
||||
|
||||
let channel_select = cx.add_view(|cx| {
|
||||
let channel_store = channel_store.clone();
|
||||
let workspace = workspace_handle.clone();
|
||||
Select::new(0, cx, {
|
||||
move |ix, item_type, is_hovered, cx| {
|
||||
Self::render_channel_name(
|
||||
&channel_store,
|
||||
ix,
|
||||
item_type,
|
||||
is_hovered,
|
||||
workspace,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.with_style(move |cx| {
|
||||
let style = &theme::current(cx).chat_panel.channel_select;
|
||||
SelectStyle {
|
||||
header: Default::default(),
|
||||
menu: style.menu,
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
let mut message_list =
|
||||
ListState::<Self>::new(0, Orientation::Bottom, 1000., move |this, ix, cx| {
|
||||
this.render_message(ix, cx)
|
||||
});
|
||||
message_list.set_scroll_handler(|visible_range, this, cx| {
|
||||
if visible_range.start < MESSAGE_LOADING_THRESHOLD {
|
||||
this.load_more_messages(&LoadMoreMessages, cx);
|
||||
}
|
||||
});
|
||||
|
||||
cx.add_view(|cx| {
|
||||
let mut this = Self {
|
||||
fs,
|
||||
client,
|
||||
channel_store,
|
||||
active_chat: Default::default(),
|
||||
pending_serialization: Task::ready(None),
|
||||
message_list,
|
||||
input_editor,
|
||||
channel_select,
|
||||
local_timezone: cx.platform().local_timezone(),
|
||||
has_focus: false,
|
||||
subscriptions: Vec::new(),
|
||||
workspace: workspace_handle,
|
||||
width: None,
|
||||
};
|
||||
|
||||
let mut old_dock_position = this.position(cx);
|
||||
this.subscriptions
|
||||
.push(
|
||||
cx.observe_global::<SettingsStore, _>(move |this: &mut Self, cx| {
|
||||
let new_dock_position = this.position(cx);
|
||||
if new_dock_position != old_dock_position {
|
||||
old_dock_position = new_dock_position;
|
||||
cx.emit(Event::DockPositionChanged);
|
||||
}
|
||||
cx.notify();
|
||||
}),
|
||||
);
|
||||
|
||||
this.init_active_channel(cx);
|
||||
cx.observe(&this.channel_store, |this, _, cx| {
|
||||
this.init_active_channel(cx);
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.observe(&this.channel_select, |this, channel_select, cx| {
|
||||
let selected_ix = channel_select.read(cx).selected_index();
|
||||
|
||||
let selected_channel_id = this
|
||||
.channel_store
|
||||
.read(cx)
|
||||
.channel_at_index(selected_ix)
|
||||
.map(|e| e.1.id);
|
||||
if let Some(selected_channel_id) = selected_channel_id {
|
||||
this.select_channel(selected_channel_id, cx)
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
this
|
||||
})
|
||||
}
|
||||
|
||||
pub fn load(
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
cx: AsyncAppContext,
|
||||
) -> Task<Result<ViewHandle<Self>>> {
|
||||
cx.spawn(|mut cx| async move {
|
||||
let serialized_panel = if let Some(panel) = cx
|
||||
.background()
|
||||
.spawn(async move { KEY_VALUE_STORE.read_kvp(CHAT_PANEL_KEY) })
|
||||
.await
|
||||
.log_err()
|
||||
.flatten()
|
||||
{
|
||||
Some(serde_json::from_str::<SerializedChatPanel>(&panel)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
let panel = Self::new(workspace, cx);
|
||||
if let Some(serialized_panel) = serialized_panel {
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.width = serialized_panel.width;
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
panel
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn serialize(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let width = self.width;
|
||||
self.pending_serialization = cx.background().spawn(
|
||||
async move {
|
||||
KEY_VALUE_STORE
|
||||
.write_kvp(
|
||||
CHAT_PANEL_KEY.into(),
|
||||
serde_json::to_string(&SerializedChatPanel { width })?,
|
||||
)
|
||||
.await?;
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err(),
|
||||
);
|
||||
}
|
||||
|
||||
fn init_active_channel(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let channel_count = self.channel_store.read(cx).channel_count();
|
||||
self.message_list.reset(0);
|
||||
self.active_chat = None;
|
||||
self.channel_select.update(cx, |select, cx| {
|
||||
select.set_item_count(channel_count, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn set_active_chat(&mut self, chat: ModelHandle<ChannelChat>, cx: &mut ViewContext<Self>) {
|
||||
if self.active_chat.as_ref().map(|e| &e.0) != Some(&chat) {
|
||||
let id = chat.read(cx).channel().id;
|
||||
{
|
||||
let chat = chat.read(cx);
|
||||
self.message_list.reset(chat.message_count());
|
||||
let placeholder = format!("Message #{}", chat.channel().name);
|
||||
self.input_editor.update(cx, move |editor, cx| {
|
||||
editor.set_placeholder_text(placeholder, cx);
|
||||
});
|
||||
}
|
||||
let subscription = cx.subscribe(&chat, Self::channel_did_change);
|
||||
self.active_chat = Some((chat, subscription));
|
||||
self.channel_select.update(cx, |select, cx| {
|
||||
if let Some(ix) = self.channel_store.read(cx).index_of_channel(id) {
|
||||
select.set_selected_index(ix, cx);
|
||||
}
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn channel_did_change(
|
||||
&mut self,
|
||||
_: ModelHandle<ChannelChat>,
|
||||
event: &ChannelChatEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
ChannelChatEvent::MessagesUpdated {
|
||||
old_range,
|
||||
new_count,
|
||||
} => {
|
||||
self.message_list.splice(old_range.clone(), *new_count);
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn render_channel(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
let theme = theme::current(cx);
|
||||
Flex::column()
|
||||
.with_child(
|
||||
ChildView::new(&self.channel_select, cx)
|
||||
.contained()
|
||||
.with_style(theme.chat_panel.channel_select.container),
|
||||
)
|
||||
.with_child(self.render_active_channel_messages(&theme))
|
||||
.with_child(self.render_input_box(&theme, cx))
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_active_channel_messages(&self, theme: &Arc<Theme>) -> AnyElement<Self> {
|
||||
let messages = if self.active_chat.is_some() {
|
||||
List::new(self.message_list.clone())
|
||||
.contained()
|
||||
.with_style(theme.chat_panel.list)
|
||||
.into_any()
|
||||
} else {
|
||||
Empty::new().into_any()
|
||||
};
|
||||
|
||||
messages.flex(1., true).into_any()
|
||||
}
|
||||
|
||||
fn render_message(&self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
let message = self.active_chat.as_ref().unwrap().0.read(cx).message(ix);
|
||||
|
||||
let now = OffsetDateTime::now_utc();
|
||||
let theme = theme::current(cx);
|
||||
let style = if message.is_pending() {
|
||||
&theme.chat_panel.pending_message
|
||||
} else {
|
||||
&theme.chat_panel.message
|
||||
};
|
||||
|
||||
let belongs_to_user = Some(message.sender.id) == self.client.user_id();
|
||||
let message_id_to_remove =
|
||||
if let (ChannelMessageId::Saved(id), true) = (message.id, belongs_to_user) {
|
||||
Some(id)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
enum DeleteMessage {}
|
||||
|
||||
let body = message.body.clone();
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Label::new(
|
||||
message.sender.github_login.clone(),
|
||||
style.sender.text.clone(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(style.sender.container),
|
||||
)
|
||||
.with_child(
|
||||
Label::new(
|
||||
format_timestamp(message.timestamp, now, self.local_timezone),
|
||||
style.timestamp.text.clone(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(style.timestamp.container),
|
||||
)
|
||||
.with_children(message_id_to_remove.map(|id| {
|
||||
MouseEventHandler::new::<DeleteMessage, _>(
|
||||
id as usize,
|
||||
cx,
|
||||
|mouse_state, _| {
|
||||
let button_style =
|
||||
theme.chat_panel.icon_button.style_for(mouse_state);
|
||||
render_icon_button(button_style, "icons/x.svg")
|
||||
.aligned()
|
||||
.into_any()
|
||||
},
|
||||
)
|
||||
.with_padding(Padding::uniform(2.))
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
this.remove_message(id, cx);
|
||||
})
|
||||
.flex_float()
|
||||
})),
|
||||
)
|
||||
.with_child(Text::new(body, style.body.clone()))
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_input_box(&self, theme: &Arc<Theme>, cx: &AppContext) -> AnyElement<Self> {
|
||||
ChildView::new(&self.input_editor, cx)
|
||||
.contained()
|
||||
.with_style(theme.chat_panel.input_editor.container)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_channel_name(
|
||||
channel_store: &ModelHandle<ChannelStore>,
|
||||
ix: usize,
|
||||
item_type: ItemType,
|
||||
is_hovered: bool,
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
cx: &mut ViewContext<Select>,
|
||||
) -> AnyElement<Select> {
|
||||
let theme = theme::current(cx);
|
||||
let tooltip_style = &theme.tooltip;
|
||||
let theme = &theme.chat_panel;
|
||||
let style = match (&item_type, is_hovered) {
|
||||
(ItemType::Header, _) => &theme.channel_select.header,
|
||||
(ItemType::Selected, _) => &theme.channel_select.active_item,
|
||||
(ItemType::Unselected, false) => &theme.channel_select.item,
|
||||
(ItemType::Unselected, true) => &theme.channel_select.hovered_item,
|
||||
};
|
||||
|
||||
let channel = &channel_store.read(cx).channel_at_index(ix).unwrap().1;
|
||||
let channel_id = channel.id;
|
||||
|
||||
let mut row = Flex::row()
|
||||
.with_child(
|
||||
Label::new("#".to_string(), style.hash.text.clone())
|
||||
.contained()
|
||||
.with_style(style.hash.container),
|
||||
)
|
||||
.with_child(Label::new(channel.name.clone(), style.name.clone()));
|
||||
|
||||
if matches!(item_type, ItemType::Header) {
|
||||
row.add_children([
|
||||
MouseEventHandler::new::<OpenChannelNotes, _>(0, cx, |mouse_state, _| {
|
||||
render_icon_button(theme.icon_button.style_for(mouse_state), "icons/file.svg")
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, _, cx| {
|
||||
if let Some(workspace) = workspace.upgrade(cx) {
|
||||
ChannelView::deploy(channel_id, workspace, cx);
|
||||
}
|
||||
})
|
||||
.with_tooltip::<OpenChannelNotes>(
|
||||
channel_id as usize,
|
||||
"Open Notes",
|
||||
Some(Box::new(OpenChannelNotes)),
|
||||
tooltip_style.clone(),
|
||||
cx,
|
||||
)
|
||||
.flex_float(),
|
||||
MouseEventHandler::new::<ActiveCall, _>(0, cx, |mouse_state, _| {
|
||||
render_icon_button(
|
||||
theme.icon_button.style_for(mouse_state),
|
||||
"icons/speaker-loud.svg",
|
||||
)
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, _, cx| {
|
||||
ActiveCall::global(cx)
|
||||
.update(cx, |call, cx| call.join_channel(channel_id, cx))
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
.with_tooltip::<ActiveCall>(
|
||||
channel_id as usize,
|
||||
"Join Call",
|
||||
Some(Box::new(JoinCall)),
|
||||
tooltip_style.clone(),
|
||||
cx,
|
||||
)
|
||||
.flex_float(),
|
||||
]);
|
||||
}
|
||||
|
||||
row.align_children_center()
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_sign_in_prompt(
|
||||
&self,
|
||||
theme: &Arc<Theme>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> AnyElement<Self> {
|
||||
enum SignInPromptLabel {}
|
||||
|
||||
MouseEventHandler::new::<SignInPromptLabel, _>(0, cx, |mouse_state, _| {
|
||||
Label::new(
|
||||
"Sign in to use chat".to_string(),
|
||||
theme
|
||||
.chat_panel
|
||||
.sign_in_prompt
|
||||
.style_for(mouse_state)
|
||||
.clone(),
|
||||
)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
let client = this.client.clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
if client
|
||||
.authenticate_and_connect(true, &cx)
|
||||
.log_err()
|
||||
.await
|
||||
.is_some()
|
||||
{
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if cx.handle().is_focused(cx) {
|
||||
cx.focus(&this.input_editor);
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
})
|
||||
.aligned()
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn send(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
|
||||
if let Some((chat, _)) = self.active_chat.as_ref() {
|
||||
let body = self.input_editor.update(cx, |editor, cx| {
|
||||
let body = editor.text(cx);
|
||||
editor.clear(cx);
|
||||
body
|
||||
});
|
||||
|
||||
if let Some(task) = chat
|
||||
.update(cx, |chat, cx| chat.send_message(body, cx))
|
||||
.log_err()
|
||||
{
|
||||
task.detach();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_message(&mut self, id: u64, cx: &mut ViewContext<Self>) {
|
||||
if let Some((chat, _)) = self.active_chat.as_ref() {
|
||||
chat.update(cx, |chat, cx| chat.remove_message(id, cx).detach())
|
||||
}
|
||||
}
|
||||
|
||||
fn load_more_messages(&mut self, _: &LoadMoreMessages, cx: &mut ViewContext<Self>) {
|
||||
if let Some((chat, _)) = self.active_chat.as_ref() {
|
||||
chat.update(cx, |channel, cx| {
|
||||
channel.load_more_messages(cx);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_channel(
|
||||
&mut self,
|
||||
selected_channel_id: u64,
|
||||
cx: &mut ViewContext<ChatPanel>,
|
||||
) -> Task<Result<()>> {
|
||||
if let Some((chat, _)) = &self.active_chat {
|
||||
if chat.read(cx).channel().id == selected_channel_id {
|
||||
return Task::ready(Ok(()));
|
||||
}
|
||||
}
|
||||
|
||||
let open_chat = self.channel_store.update(cx, |store, cx| {
|
||||
store.open_channel_chat(selected_channel_id, cx)
|
||||
});
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let chat = open_chat.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.set_active_chat(chat, cx);
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn open_notes(&mut self, _: &OpenChannelNotes, cx: &mut ViewContext<Self>) {
|
||||
if let Some((chat, _)) = &self.active_chat {
|
||||
let channel_id = chat.read(cx).channel().id;
|
||||
if let Some(workspace) = self.workspace.upgrade(cx) {
|
||||
ChannelView::deploy(channel_id, workspace, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn join_call(&mut self, _: &JoinCall, cx: &mut ViewContext<Self>) {
|
||||
if let Some((chat, _)) = &self.active_chat {
|
||||
let channel_id = chat.read(cx).channel().id;
|
||||
ActiveCall::global(cx)
|
||||
.update(cx, |call, cx| call.join_channel(channel_id, cx))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for ChatPanel {
|
||||
type Event = Event;
|
||||
}
|
||||
|
||||
impl View for ChatPanel {
|
||||
fn ui_name() -> &'static str {
|
||||
"ChatPanel"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
let theme = theme::current(cx);
|
||||
let element = if self.client.user_id().is_some() {
|
||||
self.render_channel(cx)
|
||||
} else {
|
||||
self.render_sign_in_prompt(&theme, cx)
|
||||
};
|
||||
element
|
||||
.contained()
|
||||
.with_style(theme.chat_panel.container)
|
||||
.constrained()
|
||||
.with_min_width(150.)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
self.has_focus = true;
|
||||
if matches!(
|
||||
*self.client.status().borrow(),
|
||||
client::Status::Connected { .. }
|
||||
) {
|
||||
cx.focus(&self.input_editor);
|
||||
}
|
||||
}
|
||||
|
||||
fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
|
||||
self.has_focus = false;
|
||||
}
|
||||
}
|
||||
|
||||
impl Panel for ChatPanel {
|
||||
fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
|
||||
settings::get::<ChatPanelSettings>(cx).dock
|
||||
}
|
||||
|
||||
fn position_is_valid(&self, position: DockPosition) -> bool {
|
||||
matches!(position, DockPosition::Left | DockPosition::Right)
|
||||
}
|
||||
|
||||
fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
|
||||
settings::update_settings_file::<ChatPanelSettings>(self.fs.clone(), cx, move |settings| {
|
||||
settings.dock = Some(position)
|
||||
});
|
||||
}
|
||||
|
||||
fn size(&self, cx: &gpui::WindowContext) -> f32 {
|
||||
self.width
|
||||
.unwrap_or_else(|| settings::get::<ChatPanelSettings>(cx).default_width)
|
||||
}
|
||||
|
||||
fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
|
||||
self.width = size;
|
||||
self.serialize(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
|
||||
if active && !is_chat_feature_enabled(cx) {
|
||||
cx.emit(Event::Dismissed);
|
||||
}
|
||||
}
|
||||
|
||||
fn icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> {
|
||||
(settings::get::<ChatPanelSettings>(cx).button && is_chat_feature_enabled(cx))
|
||||
.then(|| "icons/conversations.svg")
|
||||
}
|
||||
|
||||
fn icon_tooltip(&self) -> (String, Option<Box<dyn gpui::Action>>) {
|
||||
("Chat Panel".to_string(), Some(Box::new(ToggleFocus)))
|
||||
}
|
||||
|
||||
fn should_change_position_on_event(event: &Self::Event) -> bool {
|
||||
matches!(event, Event::DockPositionChanged)
|
||||
}
|
||||
|
||||
fn should_close_on_event(event: &Self::Event) -> bool {
|
||||
matches!(event, Event::Dismissed)
|
||||
}
|
||||
|
||||
fn has_focus(&self, _cx: &gpui::WindowContext) -> bool {
|
||||
self.has_focus
|
||||
}
|
||||
|
||||
fn is_focus_event(event: &Self::Event) -> bool {
|
||||
matches!(event, Event::Focus)
|
||||
}
|
||||
}
|
||||
|
||||
fn is_chat_feature_enabled(cx: &gpui::WindowContext<'_>) -> bool {
|
||||
cx.is_staff() || cx.has_flag::<ChannelsAlpha>()
|
||||
}
|
||||
|
||||
fn format_timestamp(
|
||||
mut timestamp: OffsetDateTime,
|
||||
mut now: OffsetDateTime,
|
||||
local_timezone: UtcOffset,
|
||||
) -> String {
|
||||
timestamp = timestamp.to_offset(local_timezone);
|
||||
now = now.to_offset(local_timezone);
|
||||
|
||||
let today = now.date();
|
||||
let date = timestamp.date();
|
||||
let mut hour = timestamp.hour();
|
||||
let mut part = "am";
|
||||
if hour > 12 {
|
||||
hour -= 12;
|
||||
part = "pm";
|
||||
}
|
||||
if date == today {
|
||||
format!("{:02}:{:02}{}", hour, timestamp.minute(), part)
|
||||
} else if date.next_day() == Some(today) {
|
||||
format!("yesterday at {:02}:{:02}{}", hour, timestamp.minute(), part)
|
||||
} else {
|
||||
format!("{:02}/{}/{}", date.month() as u32, date.day(), date.year())
|
||||
}
|
||||
}
|
||||
|
||||
fn render_icon_button<V: View>(style: &IconButton, svg_path: &'static str) -> impl Element<V> {
|
||||
Svg::new(svg_path)
|
||||
.with_color(style.color)
|
||||
.constrained()
|
||||
.with_width(style.icon_width)
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_width(style.button_width)
|
||||
.with_height(style.button_width)
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
}
|
|
@ -1,15 +1,21 @@
|
|||
mod channel_modal;
|
||||
mod contact_finder;
|
||||
mod panel_settings;
|
||||
|
||||
use crate::{
|
||||
channel_view::{self, ChannelView},
|
||||
chat_panel::ChatPanel,
|
||||
face_pile::FacePile,
|
||||
CollaborationPanelSettings,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use call::ActiveCall;
|
||||
use channel::{Channel, ChannelEvent, ChannelId, ChannelStore};
|
||||
use channel_modal::ChannelModal;
|
||||
use client::{proto::PeerId, Client, Contact, User, UserStore};
|
||||
use contact_finder::ContactFinder;
|
||||
use context_menu::{ContextMenu, ContextMenuItem};
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use editor::{Cancel, Editor};
|
||||
|
||||
use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt};
|
||||
use futures::StreamExt;
|
||||
use fuzzy::{match_strings, StringMatchCandidate};
|
||||
|
@ -31,7 +37,6 @@ use gpui::{
|
|||
Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use menu::{Confirm, SelectNext, SelectPrev};
|
||||
use panel_settings::{CollaborationPanelDockPosition, CollaborationPanelSettings};
|
||||
use project::{Fs, Project};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use settings::SettingsStore;
|
||||
|
@ -44,14 +49,6 @@ use workspace::{
|
|||
Workspace,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
channel_view::{self, ChannelView},
|
||||
face_pile::FacePile,
|
||||
};
|
||||
use channel_modal::ChannelModal;
|
||||
|
||||
use self::contact_finder::ContactFinder;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
struct RemoveChannel {
|
||||
channel_id: u64,
|
||||
|
@ -83,8 +80,13 @@ struct RenameChannel {
|
|||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
struct OpenChannelBuffer {
|
||||
channel_id: u64,
|
||||
pub struct OpenChannelNotes {
|
||||
pub channel_id: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct JoinChannelCall {
|
||||
pub channel_id: u64,
|
||||
}
|
||||
|
||||
actions!(
|
||||
|
@ -107,14 +109,14 @@ impl_actions!(
|
|||
ManageMembers,
|
||||
RenameChannel,
|
||||
ToggleCollapse,
|
||||
OpenChannelBuffer
|
||||
OpenChannelNotes,
|
||||
JoinChannelCall,
|
||||
]
|
||||
);
|
||||
|
||||
const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel";
|
||||
|
||||
pub fn init(_client: Arc<Client>, cx: &mut AppContext) {
|
||||
settings::register::<panel_settings::CollaborationPanelSettings>(cx);
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
contact_finder::init(cx);
|
||||
channel_modal::init(cx);
|
||||
channel_view::init(cx);
|
||||
|
@ -134,7 +136,7 @@ pub fn init(_client: Arc<Client>, cx: &mut AppContext) {
|
|||
cx.add_action(CollabPanel::toggle_channel_collapsed);
|
||||
cx.add_action(CollabPanel::collapse_selected_channel);
|
||||
cx.add_action(CollabPanel::expand_selected_channel);
|
||||
cx.add_action(CollabPanel::open_channel_buffer);
|
||||
cx.add_action(CollabPanel::open_channel_notes);
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -208,7 +210,7 @@ enum Section {
|
|||
|
||||
#[derive(Clone, Debug)]
|
||||
enum ListEntry {
|
||||
Header(Section, usize),
|
||||
Header(Section),
|
||||
CallParticipant {
|
||||
user: Arc<User>,
|
||||
is_pending: bool,
|
||||
|
@ -274,7 +276,7 @@ impl CollabPanel {
|
|||
this.selection = this
|
||||
.entries
|
||||
.iter()
|
||||
.position(|entry| !matches!(entry, ListEntry::Header(_, _)));
|
||||
.position(|entry| !matches!(entry, ListEntry::Header(_)));
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -310,16 +312,9 @@ impl CollabPanel {
|
|||
let current_project_id = this.project.read(cx).remote_id();
|
||||
|
||||
match &this.entries[ix] {
|
||||
ListEntry::Header(section, depth) => {
|
||||
ListEntry::Header(section) => {
|
||||
let is_collapsed = this.collapsed_sections.contains(section);
|
||||
this.render_header(
|
||||
*section,
|
||||
&theme,
|
||||
*depth,
|
||||
is_selected,
|
||||
is_collapsed,
|
||||
cx,
|
||||
)
|
||||
this.render_header(*section, &theme, is_selected, is_collapsed, cx)
|
||||
}
|
||||
ListEntry::CallParticipant { user, is_pending } => {
|
||||
Self::render_call_participant(
|
||||
|
@ -452,7 +447,7 @@ impl CollabPanel {
|
|||
let mut old_dock_position = this.position(cx);
|
||||
this.subscriptions
|
||||
.push(
|
||||
cx.observe_global::<SettingsStore, _>(move |this: &mut CollabPanel, cx| {
|
||||
cx.observe_global::<SettingsStore, _>(move |this: &mut Self, cx| {
|
||||
let new_dock_position = this.position(cx);
|
||||
if new_dock_position != old_dock_position {
|
||||
old_dock_position = new_dock_position;
|
||||
|
@ -563,7 +558,7 @@ impl CollabPanel {
|
|||
let old_entries = mem::take(&mut self.entries);
|
||||
|
||||
if let Some(room) = ActiveCall::global(cx).read(cx).room() {
|
||||
self.entries.push(ListEntry::Header(Section::ActiveCall, 0));
|
||||
self.entries.push(ListEntry::Header(Section::ActiveCall));
|
||||
|
||||
if !self.collapsed_sections.contains(&Section::ActiveCall) {
|
||||
let room = room.read(cx);
|
||||
|
@ -678,7 +673,7 @@ impl CollabPanel {
|
|||
let mut request_entries = Vec::new();
|
||||
|
||||
if cx.has_flag::<ChannelsAlpha>() {
|
||||
self.entries.push(ListEntry::Header(Section::Channels, 0));
|
||||
self.entries.push(ListEntry::Header(Section::Channels));
|
||||
|
||||
if channel_store.channel_count() > 0 || self.channel_editing_state.is_some() {
|
||||
self.match_candidates.clear();
|
||||
|
@ -781,7 +776,7 @@ impl CollabPanel {
|
|||
|
||||
if !request_entries.is_empty() {
|
||||
self.entries
|
||||
.push(ListEntry::Header(Section::ChannelInvites, 1));
|
||||
.push(ListEntry::Header(Section::ChannelInvites));
|
||||
if !self.collapsed_sections.contains(&Section::ChannelInvites) {
|
||||
self.entries.append(&mut request_entries);
|
||||
}
|
||||
|
@ -789,7 +784,7 @@ impl CollabPanel {
|
|||
}
|
||||
}
|
||||
|
||||
self.entries.push(ListEntry::Header(Section::Contacts, 0));
|
||||
self.entries.push(ListEntry::Header(Section::Contacts));
|
||||
|
||||
request_entries.clear();
|
||||
let incoming = user_store.incoming_contact_requests();
|
||||
|
@ -852,7 +847,7 @@ impl CollabPanel {
|
|||
|
||||
if !request_entries.is_empty() {
|
||||
self.entries
|
||||
.push(ListEntry::Header(Section::ContactRequests, 1));
|
||||
.push(ListEntry::Header(Section::ContactRequests));
|
||||
if !self.collapsed_sections.contains(&Section::ContactRequests) {
|
||||
self.entries.append(&mut request_entries);
|
||||
}
|
||||
|
@ -891,7 +886,7 @@ impl CollabPanel {
|
|||
(offline_contacts, Section::Offline),
|
||||
] {
|
||||
if !matches.is_empty() {
|
||||
self.entries.push(ListEntry::Header(section, 1));
|
||||
self.entries.push(ListEntry::Header(section));
|
||||
if !self.collapsed_sections.contains(§ion) {
|
||||
let active_call = &ActiveCall::global(cx).read(cx);
|
||||
for mat in matches {
|
||||
|
@ -1132,7 +1127,7 @@ impl CollabPanel {
|
|||
cx.font_cache(),
|
||||
))
|
||||
.with_child(
|
||||
Svg::new("icons/disable_screen_sharing_12.svg")
|
||||
Svg::new("icons/desktop.svg")
|
||||
.with_color(theme.channel_hash.color)
|
||||
.constrained()
|
||||
.with_width(theme.channel_hash.width)
|
||||
|
@ -1179,7 +1174,6 @@ impl CollabPanel {
|
|||
&self,
|
||||
section: Section,
|
||||
theme: &theme::Theme,
|
||||
depth: usize,
|
||||
is_selected: bool,
|
||||
is_collapsed: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
|
@ -1249,7 +1243,7 @@ impl CollabPanel {
|
|||
.collab_panel
|
||||
.add_contact_button
|
||||
.style_for(is_selected, state),
|
||||
"icons/plus_16.svg",
|
||||
"icons/plus.svg",
|
||||
)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
|
@ -1287,7 +1281,13 @@ impl CollabPanel {
|
|||
_ => None,
|
||||
};
|
||||
|
||||
let can_collapse = depth > 0;
|
||||
let can_collapse = match section {
|
||||
Section::ActiveCall | Section::Channels | Section::Contacts => false,
|
||||
Section::ChannelInvites
|
||||
| Section::ContactRequests
|
||||
| Section::Online
|
||||
| Section::Offline => true,
|
||||
};
|
||||
let icon_size = (&theme.collab_panel).section_icon_size;
|
||||
let mut result = MouseEventHandler::new::<Header, _>(section as usize, cx, |state, _| {
|
||||
let header_style = if can_collapse {
|
||||
|
@ -1567,7 +1567,11 @@ impl CollabPanel {
|
|||
|
||||
const FACEPILE_LIMIT: usize = 3;
|
||||
|
||||
enum ChannelCall {}
|
||||
|
||||
MouseEventHandler::new::<Channel, _>(channel.id as usize, cx, |state, cx| {
|
||||
let row_hovered = state.hovered();
|
||||
|
||||
Flex::<Self>::row()
|
||||
.with_child(
|
||||
Svg::new("icons/hash.svg")
|
||||
|
@ -1585,37 +1589,52 @@ impl CollabPanel {
|
|||
.left()
|
||||
.flex(1., true),
|
||||
)
|
||||
.with_children({
|
||||
let participants = self.channel_store.read(cx).channel_participants(channel_id);
|
||||
if !participants.is_empty() {
|
||||
let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT);
|
||||
.with_child(
|
||||
MouseEventHandler::new::<ChannelCall, _>(
|
||||
channel.id as usize,
|
||||
cx,
|
||||
move |_, cx| {
|
||||
let participants =
|
||||
self.channel_store.read(cx).channel_participants(channel_id);
|
||||
if !participants.is_empty() {
|
||||
let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT);
|
||||
|
||||
Some(
|
||||
FacePile::new(theme.face_overlap)
|
||||
.with_children(
|
||||
participants
|
||||
.iter()
|
||||
.filter_map(|user| {
|
||||
Some(
|
||||
Image::from_data(user.avatar.clone()?)
|
||||
.with_style(theme.channel_avatar),
|
||||
)
|
||||
})
|
||||
.take(FACEPILE_LIMIT),
|
||||
)
|
||||
.with_children((extra_count > 0).then(|| {
|
||||
Label::new(
|
||||
format!("+{}", extra_count),
|
||||
theme.extra_participant_label.text.clone(),
|
||||
FacePile::new(theme.face_overlap)
|
||||
.with_children(
|
||||
participants
|
||||
.iter()
|
||||
.filter_map(|user| {
|
||||
Some(
|
||||
Image::from_data(user.avatar.clone()?)
|
||||
.with_style(theme.channel_avatar),
|
||||
)
|
||||
})
|
||||
.take(FACEPILE_LIMIT),
|
||||
)
|
||||
.contained()
|
||||
.with_style(theme.extra_participant_label.container)
|
||||
})),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.with_children((extra_count > 0).then(|| {
|
||||
Label::new(
|
||||
format!("+{}", extra_count),
|
||||
theme.extra_participant_label.text.clone(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(theme.extra_participant_label.container)
|
||||
}))
|
||||
.into_any()
|
||||
} else if row_hovered {
|
||||
Svg::new("icons/speaker-loud.svg")
|
||||
.with_color(theme.channel_hash.color)
|
||||
.constrained()
|
||||
.with_width(theme.channel_hash.width)
|
||||
.into_any()
|
||||
} else {
|
||||
Empty::new().into_any()
|
||||
}
|
||||
},
|
||||
)
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
this.join_channel_call(channel_id, cx);
|
||||
}),
|
||||
)
|
||||
.align_children_center()
|
||||
.styleable_component()
|
||||
.disclosable(disclosed, Box::new(ToggleCollapse { channel_id }))
|
||||
|
@ -1632,7 +1651,7 @@ impl CollabPanel {
|
|||
)
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
this.join_channel(channel_id, cx);
|
||||
this.join_channel_chat(channel_id, cx);
|
||||
})
|
||||
.on_click(MouseButton::Right, move |e, this, cx| {
|
||||
this.deploy_channel_context_menu(Some(e.position), channel_id, cx);
|
||||
|
@ -1668,7 +1687,7 @@ impl CollabPanel {
|
|||
cx.font_cache(),
|
||||
))
|
||||
.with_child(
|
||||
Svg::new("icons/radix/file.svg")
|
||||
Svg::new("icons/file.svg")
|
||||
.with_color(theme.channel_hash.color)
|
||||
.constrained()
|
||||
.with_width(theme.channel_hash.width)
|
||||
|
@ -1690,7 +1709,7 @@ impl CollabPanel {
|
|||
.with_padding_left(theme.channel_row.default_style().padding.left)
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
this.open_channel_buffer(&OpenChannelBuffer { channel_id }, cx);
|
||||
this.open_channel_notes(&OpenChannelNotes { channel_id }, cx);
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.into_any()
|
||||
|
@ -1902,7 +1921,7 @@ impl CollabPanel {
|
|||
|
||||
let mut items = vec![
|
||||
ContextMenuItem::action(expand_action_name, ToggleCollapse { channel_id }),
|
||||
ContextMenuItem::action("Open Notes", OpenChannelBuffer { channel_id }),
|
||||
ContextMenuItem::action("Open Notes", OpenChannelNotes { channel_id }),
|
||||
];
|
||||
|
||||
if self.channel_store.read(cx).is_user_admin(channel_id) {
|
||||
|
@ -1987,7 +2006,7 @@ impl CollabPanel {
|
|||
if let Some(selection) = self.selection {
|
||||
if let Some(entry) = self.entries.get(selection) {
|
||||
match entry {
|
||||
ListEntry::Header(section, _) => match section {
|
||||
ListEntry::Header(section) => match section {
|
||||
Section::ActiveCall => Self::leave_call(cx),
|
||||
Section::Channels => self.new_root_channel(cx),
|
||||
Section::Contacts => self.toggle_contact_finder(cx),
|
||||
|
@ -2027,7 +2046,7 @@ impl CollabPanel {
|
|||
}
|
||||
}
|
||||
ListEntry::Channel { channel, .. } => {
|
||||
self.join_channel(channel.id, cx);
|
||||
self.join_channel_chat(channel.id, cx);
|
||||
}
|
||||
ListEntry::ContactPlaceholder => self.toggle_contact_finder(cx),
|
||||
_ => {}
|
||||
|
@ -2237,31 +2256,9 @@ impl CollabPanel {
|
|||
}
|
||||
}
|
||||
|
||||
fn open_channel_buffer(&mut self, action: &OpenChannelBuffer, cx: &mut ViewContext<Self>) {
|
||||
fn open_channel_notes(&mut self, action: &OpenChannelNotes, cx: &mut ViewContext<Self>) {
|
||||
if let Some(workspace) = self.workspace.upgrade(cx) {
|
||||
let pane = workspace.read(cx).active_pane().clone();
|
||||
let channel_id = action.channel_id;
|
||||
let channel_view = ChannelView::open(channel_id, pane.clone(), workspace, cx);
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
let channel_view = channel_view.await?;
|
||||
pane.update(&mut cx, |pane, cx| {
|
||||
pane.add_item(Box::new(channel_view), true, true, None, cx)
|
||||
});
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
let room_id = ActiveCall::global(cx)
|
||||
.read(cx)
|
||||
.room()
|
||||
.map(|room| room.read(cx).id());
|
||||
|
||||
ActiveCall::report_call_event_for_room(
|
||||
"open channel notes",
|
||||
room_id,
|
||||
Some(channel_id),
|
||||
&self.client,
|
||||
cx,
|
||||
);
|
||||
ChannelView::deploy(action.channel_id, workspace, cx);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2416,11 +2413,25 @@ impl CollabPanel {
|
|||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn join_channel(&self, channel: u64, cx: &mut ViewContext<Self>) {
|
||||
fn join_channel_call(&self, channel: u64, cx: &mut ViewContext<Self>) {
|
||||
ActiveCall::global(cx)
|
||||
.update(cx, |call, cx| call.join_channel(channel, cx))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn join_channel_chat(&mut self, channel_id: u64, cx: &mut ViewContext<Self>) {
|
||||
if let Some(workspace) = self.workspace.upgrade(cx) {
|
||||
cx.app_context().defer(move |cx| {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
if let Some(panel) = workspace.focus_panel::<ChatPanel>(cx) {
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.select_channel(channel_id, cx).detach_and_log_err(cx);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_tree_branch(
|
||||
|
@ -2556,10 +2567,7 @@ impl View for CollabPanel {
|
|||
|
||||
impl Panel for CollabPanel {
|
||||
fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
|
||||
match settings::get::<CollaborationPanelSettings>(cx).dock {
|
||||
CollaborationPanelDockPosition::Left => DockPosition::Left,
|
||||
CollaborationPanelDockPosition::Right => DockPosition::Right,
|
||||
}
|
||||
settings::get::<CollaborationPanelSettings>(cx).dock
|
||||
}
|
||||
|
||||
fn position_is_valid(&self, position: DockPosition) -> bool {
|
||||
|
@ -2570,15 +2578,7 @@ impl Panel for CollabPanel {
|
|||
settings::update_settings_file::<CollaborationPanelSettings>(
|
||||
self.fs.clone(),
|
||||
cx,
|
||||
move |settings| {
|
||||
let dock = match position {
|
||||
DockPosition::Left | DockPosition::Bottom => {
|
||||
CollaborationPanelDockPosition::Left
|
||||
}
|
||||
DockPosition::Right => CollaborationPanelDockPosition::Right,
|
||||
};
|
||||
settings.dock = Some(dock);
|
||||
},
|
||||
move |settings| settings.dock = Some(position),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -2596,7 +2596,7 @@ impl Panel for CollabPanel {
|
|||
fn icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> {
|
||||
settings::get::<CollaborationPanelSettings>(cx)
|
||||
.button
|
||||
.then(|| "icons/conversations.svg")
|
||||
.then(|| "icons/user_group_16.svg")
|
||||
}
|
||||
|
||||
fn icon_tooltip(&self) -> (String, Option<Box<dyn gpui::Action>>) {
|
||||
|
@ -2622,9 +2622,9 @@ impl Panel for CollabPanel {
|
|||
impl PartialEq for ListEntry {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match self {
|
||||
ListEntry::Header(section_1, depth_1) => {
|
||||
if let ListEntry::Header(section_2, depth_2) = other {
|
||||
return section_1 == section_2 && depth_1 == depth_2;
|
||||
ListEntry::Header(section_1) => {
|
||||
if let ListEntry::Header(section_2) = other {
|
||||
return section_1 == section_2;
|
||||
}
|
||||
}
|
||||
ListEntry::CallParticipant { user: user_1, .. } => {
|
||||
|
|
|
@ -209,9 +209,9 @@ impl PickerDelegate for ContactFinderDelegate {
|
|||
|
||||
let icon_path = match request_status {
|
||||
ContactRequestStatus::None | ContactRequestStatus::RequestReceived => {
|
||||
Some("icons/check_8.svg")
|
||||
Some("icons/check.svg")
|
||||
}
|
||||
ContactRequestStatus::RequestSent => Some("icons/x_mark_8.svg"),
|
||||
ContactRequestStatus::RequestSent => Some("icons/x.svg"),
|
||||
ContactRequestStatus::RequestAccepted => None,
|
||||
};
|
||||
let button_style = if self.user_store.read(cx).is_contact_request_pending(user) {
|
||||
|
|
|
@ -483,10 +483,10 @@ impl CollabTitlebarItem {
|
|||
let icon;
|
||||
let tooltip;
|
||||
if room.read(cx).is_screen_sharing() {
|
||||
icon = "icons/radix/desktop.svg";
|
||||
icon = "icons/desktop.svg";
|
||||
tooltip = "Stop Sharing Screen"
|
||||
} else {
|
||||
icon = "icons/radix/desktop.svg";
|
||||
icon = "icons/desktop.svg";
|
||||
tooltip = "Share Screen";
|
||||
}
|
||||
|
||||
|
@ -533,10 +533,10 @@ impl CollabTitlebarItem {
|
|||
let tooltip;
|
||||
let is_muted = room.read(cx).is_muted(cx);
|
||||
if is_muted {
|
||||
icon = "icons/radix/mic-mute.svg";
|
||||
icon = "icons/mic-mute.svg";
|
||||
tooltip = "Unmute microphone";
|
||||
} else {
|
||||
icon = "icons/radix/mic.svg";
|
||||
icon = "icons/mic.svg";
|
||||
tooltip = "Mute microphone";
|
||||
}
|
||||
|
||||
|
@ -586,10 +586,10 @@ impl CollabTitlebarItem {
|
|||
let tooltip;
|
||||
let is_deafened = room.read(cx).is_deafened().unwrap_or(false);
|
||||
if is_deafened {
|
||||
icon = "icons/radix/speaker-off.svg";
|
||||
icon = "icons/speaker-off.svg";
|
||||
tooltip = "Unmute speakers";
|
||||
} else {
|
||||
icon = "icons/radix/speaker-loud.svg";
|
||||
icon = "icons/speaker-loud.svg";
|
||||
tooltip = "Mute speakers";
|
||||
}
|
||||
|
||||
|
@ -625,7 +625,7 @@ impl CollabTitlebarItem {
|
|||
.into_any()
|
||||
}
|
||||
fn render_leave_call(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
let icon = "icons/radix/exit.svg";
|
||||
let icon = "icons/exit.svg";
|
||||
let tooltip = "Leave call";
|
||||
|
||||
let titlebar = &theme.titlebar;
|
||||
|
@ -748,7 +748,7 @@ impl CollabTitlebarItem {
|
|||
|
||||
dropdown
|
||||
.with_child(
|
||||
Svg::new("icons/caret_down_8.svg")
|
||||
Svg::new("icons/caret_down.svg")
|
||||
.with_color(user_menu_button_style.icon.color)
|
||||
.constrained()
|
||||
.with_width(user_menu_button_style.icon.width)
|
||||
|
@ -1116,7 +1116,7 @@ impl CollabTitlebarItem {
|
|||
| client::Status::Reauthenticating { .. }
|
||||
| client::Status::Reconnecting { .. }
|
||||
| client::Status::ReconnectionError { .. } => Some(
|
||||
Svg::new("icons/cloud_slash_12.svg")
|
||||
Svg::new("icons/disconnected.svg")
|
||||
.with_color(theme.titlebar.offline_icon.color)
|
||||
.constrained()
|
||||
.with_width(theme.titlebar.offline_icon.width)
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
pub mod channel_view;
|
||||
pub mod chat_panel;
|
||||
pub mod collab_panel;
|
||||
mod collab_titlebar_item;
|
||||
mod contact_notification;
|
||||
mod face_pile;
|
||||
mod incoming_call_notification;
|
||||
mod notifications;
|
||||
mod panel_settings;
|
||||
mod project_shared_notification;
|
||||
mod sharing_status_indicator;
|
||||
|
||||
use call::{ActiveCall, Room};
|
||||
pub use collab_titlebar_item::CollabTitlebarItem;
|
||||
use gpui::{
|
||||
actions,
|
||||
geometry::{
|
||||
|
@ -23,15 +24,22 @@ use std::{rc::Rc, sync::Arc};
|
|||
use util::ResultExt;
|
||||
use workspace::AppState;
|
||||
|
||||
pub use collab_titlebar_item::CollabTitlebarItem;
|
||||
pub use panel_settings::{ChatPanelSettings, CollaborationPanelSettings};
|
||||
|
||||
actions!(
|
||||
collab,
|
||||
[ToggleScreenSharing, ToggleMute, ToggleDeafen, LeaveCall]
|
||||
);
|
||||
|
||||
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
settings::register::<CollaborationPanelSettings>(cx);
|
||||
settings::register::<ChatPanelSettings>(cx);
|
||||
|
||||
vcs_menu::init(cx);
|
||||
collab_titlebar_item::init(cx);
|
||||
collab_panel::init(app_state.client.clone(), cx);
|
||||
collab_panel::init(cx);
|
||||
chat_panel::init(cx);
|
||||
incoming_call_notification::init(&app_state, cx);
|
||||
project_shared_notification::init(&app_state, cx);
|
||||
sharing_status_indicator::init(cx);
|
||||
|
|
|
@ -53,7 +53,7 @@ where
|
|||
.with_child(
|
||||
MouseEventHandler::new::<Dismiss, _>(user.id as usize, cx, |state, _| {
|
||||
let style = theme.dismiss_button.style_for(state);
|
||||
Svg::new("icons/x_mark_8.svg")
|
||||
Svg::new("icons/x.svg")
|
||||
.with_color(style.color)
|
||||
.constrained()
|
||||
.with_width(style.icon_width)
|
||||
|
|
|
@ -2,32 +2,47 @@ use anyhow;
|
|||
use schemars::JsonSchema;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use settings::Setting;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CollaborationPanelDockPosition {
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
use workspace::dock::DockPosition;
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct CollaborationPanelSettings {
|
||||
pub button: bool,
|
||||
pub dock: CollaborationPanelDockPosition,
|
||||
pub dock: DockPosition,
|
||||
pub default_width: f32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct ChatPanelSettings {
|
||||
pub button: bool,
|
||||
pub dock: DockPosition,
|
||||
pub default_width: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
pub struct CollaborationPanelSettingsContent {
|
||||
pub struct PanelSettingsContent {
|
||||
pub button: Option<bool>,
|
||||
pub dock: Option<CollaborationPanelDockPosition>,
|
||||
pub dock: Option<DockPosition>,
|
||||
pub default_width: Option<f32>,
|
||||
}
|
||||
|
||||
impl Setting for CollaborationPanelSettings {
|
||||
const KEY: Option<&'static str> = Some("collaboration_panel");
|
||||
|
||||
type FileContent = CollaborationPanelSettingsContent;
|
||||
type FileContent = PanelSettingsContent;
|
||||
|
||||
fn load(
|
||||
default_value: &Self::FileContent,
|
||||
user_values: &[&Self::FileContent],
|
||||
_: &gpui::AppContext,
|
||||
) -> anyhow::Result<Self> {
|
||||
Self::load_via_json_merge(default_value, user_values)
|
||||
}
|
||||
}
|
||||
|
||||
impl Setting for ChatPanelSettings {
|
||||
const KEY: Option<&'static str> = Some("chat_panel");
|
||||
|
||||
type FileContent = PanelSettingsContent;
|
||||
|
||||
fn load(
|
||||
default_value: &Self::FileContent,
|
|
@ -48,7 +48,7 @@ impl View for SharingStatusIndicator {
|
|||
};
|
||||
|
||||
MouseEventHandler::new::<Self, _>(0, cx, |_, _| {
|
||||
Svg::new("icons/disable_screen_sharing_12.svg")
|
||||
Svg::new("icons/desktop.svg")
|
||||
.with_color(color)
|
||||
.constrained()
|
||||
.with_width(18.)
|
||||
|
|
|
@ -78,15 +78,15 @@ impl View for CopilotButton {
|
|||
.with_child(
|
||||
Svg::new({
|
||||
match status {
|
||||
Status::Error(_) => "icons/copilot_error_16.svg",
|
||||
Status::Error(_) => "icons/copilot_error.svg",
|
||||
Status::Authorized => {
|
||||
if enabled {
|
||||
"icons/copilot_16.svg"
|
||||
"icons/copilot.svg"
|
||||
} else {
|
||||
"icons/copilot_disabled_16.svg"
|
||||
"icons/copilot_disabled.svg"
|
||||
}
|
||||
}
|
||||
_ => "icons/copilot_init_16.svg",
|
||||
_ => "icons/copilot_init.svg",
|
||||
}
|
||||
})
|
||||
.with_color(style.icon_color)
|
||||
|
|
|
@ -686,11 +686,9 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
|
|||
let font_size = (style.text_scale_factor * settings.buffer_font_size(cx)).round();
|
||||
let icon_width = cx.em_width * style.icon_width_factor;
|
||||
let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
|
||||
Svg::new("icons/circle_x_mark_12.svg")
|
||||
.with_color(theme.error_diagnostic.message.text.color)
|
||||
Svg::new("icons/error.svg").with_color(theme.error_diagnostic.message.text.color)
|
||||
} else {
|
||||
Svg::new("icons/triangle_exclamation_12.svg")
|
||||
.with_color(theme.warning_diagnostic.message.text.color)
|
||||
Svg::new("icons/warning.svg").with_color(theme.warning_diagnostic.message.text.color)
|
||||
};
|
||||
|
||||
Flex::row()
|
||||
|
@ -748,7 +746,7 @@ pub(crate) fn render_summary<T: 'static>(
|
|||
let summary_spacing = theme.tab_summary_spacing;
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Svg::new("icons/circle_x_mark_12.svg")
|
||||
Svg::new("icons/error.svg")
|
||||
.with_color(text_style.color)
|
||||
.constrained()
|
||||
.with_width(icon_width)
|
||||
|
@ -767,7 +765,7 @@ pub(crate) fn render_summary<T: 'static>(
|
|||
.aligned(),
|
||||
)
|
||||
.with_child(
|
||||
Svg::new("icons/triangle_exclamation_12.svg")
|
||||
Svg::new("icons/warning.svg")
|
||||
.with_color(text_style.color)
|
||||
.constrained()
|
||||
.with_width(icon_width)
|
||||
|
|
|
@ -45,7 +45,7 @@ util = { path = "../util" }
|
|||
sqlez = { path = "../sqlez" }
|
||||
workspace = { path = "../workspace" }
|
||||
|
||||
aho-corasick = "0.7"
|
||||
aho-corasick = "1.1"
|
||||
anyhow.workspace = true
|
||||
convert_case = "0.6.0"
|
||||
futures.workspace = true
|
||||
|
|
|
@ -3827,7 +3827,7 @@ impl Editor {
|
|||
enum CodeActions {}
|
||||
Some(
|
||||
MouseEventHandler::new::<CodeActions, _>(0, cx, |state, _| {
|
||||
Svg::new("icons/bolt_8.svg").with_color(
|
||||
Svg::new("icons/bolt.svg").with_color(
|
||||
style
|
||||
.code_actions
|
||||
.indicator
|
||||
|
@ -5936,7 +5936,7 @@ impl Editor {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn select_next(&mut self, action: &SelectNext, cx: &mut ViewContext<Self>) {
|
||||
pub fn select_next(&mut self, action: &SelectNext, cx: &mut ViewContext<Self>) -> Result<()> {
|
||||
self.push_to_selection_history();
|
||||
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
let buffer = &display_map.buffer_snapshot;
|
||||
|
@ -6005,7 +6005,7 @@ impl Editor {
|
|||
.text_for_range(selection.start..selection.end)
|
||||
.collect::<String>();
|
||||
let select_state = SelectNextState {
|
||||
query: AhoCorasick::new_auto_configured(&[query]),
|
||||
query: AhoCorasick::new(&[query])?,
|
||||
wordwise: true,
|
||||
done: false,
|
||||
};
|
||||
|
@ -6019,16 +6019,21 @@ impl Editor {
|
|||
.text_for_range(selection.start..selection.end)
|
||||
.collect::<String>();
|
||||
self.select_next_state = Some(SelectNextState {
|
||||
query: AhoCorasick::new_auto_configured(&[query]),
|
||||
query: AhoCorasick::new(&[query])?,
|
||||
wordwise: false,
|
||||
done: false,
|
||||
});
|
||||
self.select_next(action, cx);
|
||||
self.select_next(action, cx)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn select_previous(&mut self, action: &SelectPrevious, cx: &mut ViewContext<Self>) {
|
||||
pub fn select_previous(
|
||||
&mut self,
|
||||
action: &SelectPrevious,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Result<()> {
|
||||
self.push_to_selection_history();
|
||||
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
let buffer = &display_map.buffer_snapshot;
|
||||
|
@ -6099,7 +6104,7 @@ impl Editor {
|
|||
.collect::<String>();
|
||||
let query = query.chars().rev().collect::<String>();
|
||||
let select_state = SelectNextState {
|
||||
query: AhoCorasick::new_auto_configured(&[query]),
|
||||
query: AhoCorasick::new(&[query])?,
|
||||
wordwise: true,
|
||||
done: false,
|
||||
};
|
||||
|
@ -6114,13 +6119,14 @@ impl Editor {
|
|||
.collect::<String>();
|
||||
let query = query.chars().rev().collect::<String>();
|
||||
self.select_prev_state = Some(SelectNextState {
|
||||
query: AhoCorasick::new_auto_configured(&[query]),
|
||||
query: AhoCorasick::new(&[query])?,
|
||||
wordwise: false,
|
||||
done: false,
|
||||
});
|
||||
self.select_previous(action, cx);
|
||||
self.select_previous(action, cx)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn toggle_comments(&mut self, action: &ToggleComments, cx: &mut ViewContext<Self>) {
|
||||
|
|
|
@ -3669,10 +3669,12 @@ async fn test_select_next(cx: &mut gpui::TestAppContext) {
|
|||
let mut cx = EditorTestContext::new(cx).await;
|
||||
cx.set_state("abc\nˇabc abc\ndefabc\nabc");
|
||||
|
||||
cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx));
|
||||
cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx))
|
||||
.unwrap();
|
||||
cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
|
||||
|
||||
cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx));
|
||||
cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx))
|
||||
.unwrap();
|
||||
cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
|
||||
|
||||
cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx));
|
||||
|
@ -3681,10 +3683,12 @@ async fn test_select_next(cx: &mut gpui::TestAppContext) {
|
|||
cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx));
|
||||
cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
|
||||
|
||||
cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx));
|
||||
cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx))
|
||||
.unwrap();
|
||||
cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
|
||||
|
||||
cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx));
|
||||
cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx))
|
||||
.unwrap();
|
||||
cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
|
||||
}
|
||||
|
||||
|
@ -3696,10 +3700,12 @@ async fn test_select_previous(cx: &mut gpui::TestAppContext) {
|
|||
let mut cx = EditorTestContext::new(cx).await;
|
||||
cx.set_state("abc\nˇabc abc\ndefabc\nabc");
|
||||
|
||||
cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx));
|
||||
cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
|
||||
.unwrap();
|
||||
cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
|
||||
|
||||
cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx));
|
||||
cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
|
||||
.unwrap();
|
||||
cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
|
||||
|
||||
cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx));
|
||||
|
@ -3708,10 +3714,12 @@ async fn test_select_previous(cx: &mut gpui::TestAppContext) {
|
|||
cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx));
|
||||
cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
|
||||
|
||||
cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx));
|
||||
cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
|
||||
.unwrap();
|
||||
cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
|
||||
|
||||
cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx));
|
||||
cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
|
||||
.unwrap();
|
||||
cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
|
||||
}
|
||||
{
|
||||
|
@ -3719,10 +3727,12 @@ async fn test_select_previous(cx: &mut gpui::TestAppContext) {
|
|||
let mut cx = EditorTestContext::new(cx).await;
|
||||
cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
|
||||
|
||||
cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx));
|
||||
cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
|
||||
.unwrap();
|
||||
cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
|
||||
|
||||
cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx));
|
||||
cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
|
||||
.unwrap();
|
||||
cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
|
||||
|
||||
cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx));
|
||||
|
@ -3731,10 +3741,12 @@ async fn test_select_previous(cx: &mut gpui::TestAppContext) {
|
|||
cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx));
|
||||
cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
|
||||
|
||||
cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx));
|
||||
cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
|
||||
.unwrap();
|
||||
cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndef«abcˇ»\n«abcˇ»");
|
||||
|
||||
cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx));
|
||||
cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
|
||||
.unwrap();
|
||||
cx.assert_editor_state("«abcˇ»\n«ˇabc» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1735,7 +1735,7 @@ impl EditorElement {
|
|||
enum JumpIcon {}
|
||||
MouseEventHandler::new::<JumpIcon, _>((*id).into(), cx, |state, _| {
|
||||
let style = style.jump_icon.style_for(state);
|
||||
Svg::new("icons/arrow_up_right_8.svg")
|
||||
Svg::new("icons/arrow_up_right.svg")
|
||||
.with_color(style.color)
|
||||
.constrained()
|
||||
.with_width(style.icon_width)
|
||||
|
|
|
@ -276,7 +276,7 @@ impl Item for FeedbackEditor {
|
|||
) -> AnyElement<T> {
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Svg::new("icons/feedback_16.svg")
|
||||
Svg::new("icons/feedback.svg")
|
||||
.with_color(style.label.text.color)
|
||||
.constrained()
|
||||
.with_width(style.type_icon_width)
|
||||
|
|
|
@ -67,6 +67,7 @@ dhat = "0.3"
|
|||
env_logger.workspace = true
|
||||
png = "0.16"
|
||||
simplelog = "0.9"
|
||||
util = { path = "../util", features = ["test-support"] }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
media = { path = "../media" }
|
||||
|
|
|
@ -684,23 +684,41 @@ impl AppContext {
|
|||
);
|
||||
},
|
||||
);
|
||||
fn inner(
|
||||
this: &mut AppContext,
|
||||
name: &'static str,
|
||||
deserializer: fn(serde_json::Value) -> anyhow::Result<Box<dyn Action>>,
|
||||
action_id: TypeId,
|
||||
view_id: TypeId,
|
||||
handler: Box<ActionCallback>,
|
||||
capture: bool,
|
||||
) {
|
||||
this.action_deserializers
|
||||
.entry(name)
|
||||
.or_insert((action_id.clone(), deserializer));
|
||||
|
||||
self.action_deserializers
|
||||
.entry(A::qualified_name())
|
||||
.or_insert((TypeId::of::<A>(), A::from_json_str));
|
||||
let actions = if capture {
|
||||
&mut this.capture_actions
|
||||
} else {
|
||||
&mut this.actions
|
||||
};
|
||||
|
||||
let actions = if capture {
|
||||
&mut self.capture_actions
|
||||
} else {
|
||||
&mut self.actions
|
||||
};
|
||||
|
||||
actions
|
||||
.entry(TypeId::of::<V>())
|
||||
.or_default()
|
||||
.entry(TypeId::of::<A>())
|
||||
.or_default()
|
||||
.push(handler);
|
||||
actions
|
||||
.entry(view_id)
|
||||
.or_default()
|
||||
.entry(action_id)
|
||||
.or_default()
|
||||
.push(handler);
|
||||
}
|
||||
inner(
|
||||
self,
|
||||
A::qualified_name(),
|
||||
A::from_json_str,
|
||||
TypeId::of::<A>(),
|
||||
TypeId::of::<V>(),
|
||||
handler,
|
||||
capture,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn add_async_action<A, V, F>(&mut self, mut handler: F)
|
||||
|
|
|
@ -75,7 +75,6 @@ impl KeymapMatcher {
|
|||
keystroke: Keystroke,
|
||||
mut dispatch_path: Vec<(usize, KeymapContext)>,
|
||||
) -> MatchResult {
|
||||
let mut any_pending = false;
|
||||
// Collect matched bindings into an ordered list using the position in the matching binding first,
|
||||
// and then the order the binding matched in the view tree second.
|
||||
// The key is the reverse position of the binding in the bindings list so that later bindings
|
||||
|
@ -84,7 +83,8 @@ impl KeymapMatcher {
|
|||
let no_action_id = (NoAction {}).id();
|
||||
|
||||
let first_keystroke = self.pending_keystrokes.is_empty();
|
||||
self.pending_keystrokes.push(keystroke.clone());
|
||||
let mut pending_key = None;
|
||||
let mut previous_keystrokes = self.pending_keystrokes.clone();
|
||||
|
||||
self.contexts.clear();
|
||||
self.contexts
|
||||
|
@ -106,24 +106,32 @@ impl KeymapMatcher {
|
|||
}
|
||||
|
||||
for binding in self.keymap.bindings().iter().rev() {
|
||||
match binding.match_keys_and_context(&self.pending_keystrokes, &self.contexts[i..])
|
||||
{
|
||||
BindingMatchResult::Complete(action) => {
|
||||
if action.id() != no_action_id {
|
||||
matched_bindings.push((*view_id, action));
|
||||
for possibility in keystroke.match_possibilities() {
|
||||
previous_keystrokes.push(possibility.clone());
|
||||
match binding.match_keys_and_context(&previous_keystrokes, &self.contexts[i..])
|
||||
{
|
||||
BindingMatchResult::Complete(action) => {
|
||||
if action.id() != no_action_id {
|
||||
matched_bindings.push((*view_id, action));
|
||||
}
|
||||
}
|
||||
BindingMatchResult::Partial => {
|
||||
if pending_key == None || pending_key == Some(possibility.clone()) {
|
||||
self.pending_views
|
||||
.insert(*view_id, self.contexts[i].clone());
|
||||
pending_key = Some(possibility)
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
BindingMatchResult::Partial => {
|
||||
self.pending_views
|
||||
.insert(*view_id, self.contexts[i].clone());
|
||||
any_pending = true;
|
||||
}
|
||||
_ => {}
|
||||
previous_keystrokes.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !any_pending {
|
||||
if pending_key.is_some() {
|
||||
self.pending_keystrokes.push(pending_key.unwrap());
|
||||
} else {
|
||||
self.clear_pending();
|
||||
}
|
||||
|
||||
|
@ -131,7 +139,7 @@ impl KeymapMatcher {
|
|||
// Collect the sorted matched bindings into the final vec for ease of use
|
||||
// Matched bindings are in order by precedence
|
||||
MatchResult::Matches(matched_bindings)
|
||||
} else if any_pending {
|
||||
} else if !self.pending_keystrokes.is_empty() {
|
||||
MatchResult::Pending
|
||||
} else {
|
||||
MatchResult::None
|
||||
|
@ -340,6 +348,7 @@ mod tests {
|
|||
shift: false,
|
||||
cmd: false,
|
||||
function: false,
|
||||
ime_key: None,
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -352,6 +361,7 @@ mod tests {
|
|||
shift: true,
|
||||
cmd: false,
|
||||
function: false,
|
||||
ime_key: None,
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -364,6 +374,7 @@ mod tests {
|
|||
shift: true,
|
||||
cmd: true,
|
||||
function: false,
|
||||
ime_key: None,
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -466,7 +477,7 @@ mod tests {
|
|||
#[derive(Clone, Deserialize, PartialEq, Eq, Debug)]
|
||||
pub struct A(pub String);
|
||||
impl_actions!(test, [A]);
|
||||
actions!(test, [B, Ab]);
|
||||
actions!(test, [B, Ab, Dollar, Quote, Ess, Backtick]);
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
struct ActionArg {
|
||||
|
@ -477,6 +488,10 @@ mod tests {
|
|||
Binding::new("a", A("x".to_string()), Some("a")),
|
||||
Binding::new("b", B, Some("a")),
|
||||
Binding::new("a b", Ab, Some("a || b")),
|
||||
Binding::new("$", Dollar, Some("a")),
|
||||
Binding::new("\"", Quote, Some("a")),
|
||||
Binding::new("alt-s", Ess, Some("a")),
|
||||
Binding::new("ctrl-`", Backtick, Some("a")),
|
||||
]);
|
||||
|
||||
let mut context_a = KeymapContext::default();
|
||||
|
@ -543,6 +558,30 @@ mod tests {
|
|||
MatchResult::Matches(vec![(1, Box::new(Ab))])
|
||||
);
|
||||
|
||||
// handle Czech $ (option + 4 key)
|
||||
assert_eq!(
|
||||
matcher.push_keystroke(Keystroke::parse("alt-ç->$")?, vec![(1, context_a.clone())]),
|
||||
MatchResult::Matches(vec![(1, Box::new(Dollar))])
|
||||
);
|
||||
|
||||
// handle Brazillian quote (quote key then space key)
|
||||
assert_eq!(
|
||||
matcher.push_keystroke(Keystroke::parse("space->\"")?, vec![(1, context_a.clone())]),
|
||||
MatchResult::Matches(vec![(1, Box::new(Quote))])
|
||||
);
|
||||
|
||||
// handle ctrl+` on a brazillian keyboard
|
||||
assert_eq!(
|
||||
matcher.push_keystroke(Keystroke::parse("ctrl-->`")?, vec![(1, context_a.clone())]),
|
||||
MatchResult::Matches(vec![(1, Box::new(Backtick))])
|
||||
);
|
||||
|
||||
// handle alt-s on a US keyboard
|
||||
assert_eq!(
|
||||
matcher.push_keystroke(Keystroke::parse("alt-s->ß")?, vec![(1, context_a.clone())]),
|
||||
MatchResult::Matches(vec![(1, Box::new(Ess))])
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -162,7 +162,8 @@ mod tests {
|
|||
shift: false,
|
||||
cmd: false,
|
||||
function: false,
|
||||
key: "q".to_string()
|
||||
key: "q".to_string(),
|
||||
ime_key: None,
|
||||
}],
|
||||
"{keystroke_duplicate_to_1:?} should have the expected keystroke in the keymap"
|
||||
);
|
||||
|
@ -179,7 +180,8 @@ mod tests {
|
|||
shift: false,
|
||||
cmd: false,
|
||||
function: false,
|
||||
key: "w".to_string()
|
||||
key: "w".to_string(),
|
||||
ime_key: None,
|
||||
},
|
||||
&Keystroke {
|
||||
ctrl: true,
|
||||
|
@ -187,7 +189,8 @@ mod tests {
|
|||
shift: false,
|
||||
cmd: false,
|
||||
function: false,
|
||||
key: "w".to_string()
|
||||
key: "w".to_string(),
|
||||
ime_key: None,
|
||||
}
|
||||
],
|
||||
"{full_duplicate_to_2:?} should have a duplicated keystroke in the keymap"
|
||||
|
@ -339,7 +342,8 @@ mod tests {
|
|||
shift: false,
|
||||
cmd: false,
|
||||
function: false,
|
||||
key: expected_key.to_string()
|
||||
key: expected_key.to_string(),
|
||||
ime_key: None,
|
||||
}],
|
||||
"{expected:?} should have the expected keystroke with key '{expected_key}' in the keymap for element {i}"
|
||||
);
|
||||
|
|
|
@ -2,6 +2,7 @@ use std::fmt::Write;
|
|||
|
||||
use anyhow::anyhow;
|
||||
use serde::Deserialize;
|
||||
use smallvec::SmallVec;
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)]
|
||||
pub struct Keystroke {
|
||||
|
@ -10,10 +11,47 @@ pub struct Keystroke {
|
|||
pub shift: bool,
|
||||
pub cmd: bool,
|
||||
pub function: bool,
|
||||
/// key is the character printed on the key that was pressed
|
||||
/// e.g. for option-s, key is "s"
|
||||
pub key: String,
|
||||
/// ime_key is the character inserted by the IME engine when that key was pressed.
|
||||
/// e.g. for option-s, ime_key is "ß"
|
||||
pub ime_key: Option<String>,
|
||||
}
|
||||
|
||||
impl Keystroke {
|
||||
// When matching a key we cannot know whether the user intended to type
|
||||
// the ime_key or the key. On some non-US keyboards keys we use in our
|
||||
// bindings are behind option (for example `$` is typed `alt-ç` on a Czech keyboard),
|
||||
// and on some keyboards the IME handler converts a sequence of keys into a
|
||||
// specific character (for example `"` is typed as `" space` on a brazillian keyboard).
|
||||
pub fn match_possibilities(&self) -> SmallVec<[Keystroke; 2]> {
|
||||
let mut possibilities = SmallVec::new();
|
||||
match self.ime_key.as_ref() {
|
||||
None => possibilities.push(self.clone()),
|
||||
Some(ime_key) => {
|
||||
possibilities.push(Keystroke {
|
||||
ctrl: self.ctrl,
|
||||
alt: false,
|
||||
shift: false,
|
||||
cmd: false,
|
||||
function: false,
|
||||
key: ime_key.to_string(),
|
||||
ime_key: None,
|
||||
});
|
||||
possibilities.push(Keystroke {
|
||||
ime_key: None,
|
||||
..self.clone()
|
||||
});
|
||||
}
|
||||
}
|
||||
possibilities
|
||||
}
|
||||
|
||||
/// key syntax is:
|
||||
/// [ctrl-][alt-][shift-][cmd-][fn-]key[->ime_key]
|
||||
/// ime_key is only used for generating test events,
|
||||
/// when matching a key with an ime_key set will be matched without it.
|
||||
pub fn parse(source: &str) -> anyhow::Result<Self> {
|
||||
let mut ctrl = false;
|
||||
let mut alt = false;
|
||||
|
@ -21,6 +59,7 @@ impl Keystroke {
|
|||
let mut cmd = false;
|
||||
let mut function = false;
|
||||
let mut key = None;
|
||||
let mut ime_key = None;
|
||||
|
||||
let mut components = source.split('-').peekable();
|
||||
while let Some(component) = components.next() {
|
||||
|
@ -31,10 +70,14 @@ impl Keystroke {
|
|||
"cmd" => cmd = true,
|
||||
"fn" => function = true,
|
||||
_ => {
|
||||
if let Some(component) = components.peek() {
|
||||
if component.is_empty() && source.ends_with('-') {
|
||||
if let Some(next) = components.peek() {
|
||||
if next.is_empty() && source.ends_with('-') {
|
||||
key = Some(String::from("-"));
|
||||
break;
|
||||
} else if next.len() > 1 && next.starts_with('>') {
|
||||
key = Some(String::from(component));
|
||||
ime_key = Some(String::from(&next[1..]));
|
||||
components.next();
|
||||
} else {
|
||||
return Err(anyhow!("Invalid keystroke `{}`", source));
|
||||
}
|
||||
|
@ -54,6 +97,7 @@ impl Keystroke {
|
|||
cmd,
|
||||
function,
|
||||
key,
|
||||
ime_key,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -327,6 +327,7 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
|
|||
cmd,
|
||||
function,
|
||||
key,
|
||||
ime_key: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -285,6 +285,7 @@ enum ImeState {
|
|||
None,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct InsertText {
|
||||
replacement_range: Option<Range<usize>>,
|
||||
text: String,
|
||||
|
@ -1006,40 +1007,7 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent:
|
|||
.flatten()
|
||||
.is_some();
|
||||
if !is_composing {
|
||||
// if the IME has changed the key, we'll first emit an event with the character
|
||||
// generated by the IME system; then fallback to the keystroke if that is not
|
||||
// handled.
|
||||
// cases that we have working:
|
||||
// - " on a brazillian layout by typing <quote><space>
|
||||
// - ctrl-` on a brazillian layout by typing <ctrl-`>
|
||||
// - $ on a czech QWERTY layout by typing <alt-4>
|
||||
// - 4 on a czech QWERTY layout by typing <shift-4>
|
||||
// - ctrl-4 on a czech QWERTY layout by typing <ctrl-alt-4> (or <ctrl-shift-4>)
|
||||
if ime_text.is_some() && ime_text.as_ref() != Some(&event.keystroke.key) {
|
||||
let event_with_ime_text = KeyDownEvent {
|
||||
is_held: false,
|
||||
keystroke: Keystroke {
|
||||
// we match ctrl because some use-cases need it.
|
||||
// we don't match alt because it's often used to generate the optional character
|
||||
// we don't match shift because we're not here with letters (usually)
|
||||
// we don't match cmd/fn because they don't seem to use IME
|
||||
ctrl: event.keystroke.ctrl,
|
||||
alt: false,
|
||||
shift: false,
|
||||
cmd: false,
|
||||
function: false,
|
||||
key: ime_text.clone().unwrap(),
|
||||
},
|
||||
};
|
||||
handled = callback(Event::KeyDown(event_with_ime_text));
|
||||
}
|
||||
if !handled {
|
||||
// empty key happens when you type a deadkey in input composition.
|
||||
// (e.g. on a brazillian keyboard typing quote is a deadkey)
|
||||
if !event.keystroke.key.is_empty() {
|
||||
handled = callback(Event::KeyDown(event));
|
||||
}
|
||||
}
|
||||
handled = callback(Event::KeyDown(event));
|
||||
}
|
||||
|
||||
if !handled {
|
||||
|
@ -1197,6 +1165,7 @@ extern "C" fn cancel_operation(this: &Object, _sel: Sel, _sender: id) {
|
|||
shift: false,
|
||||
function: false,
|
||||
key: ".".into(),
|
||||
ime_key: None,
|
||||
};
|
||||
let event = Event::KeyDown(KeyDownEvent {
|
||||
keystroke: keystroke.clone(),
|
||||
|
@ -1479,6 +1448,9 @@ extern "C" fn insert_text(this: &Object, _: Sel, text: id, replacement_range: NS
|
|||
replacement_range,
|
||||
text: text.to_string(),
|
||||
});
|
||||
if text.to_string().to_ascii_lowercase() != pending_key_down.0.keystroke.key {
|
||||
pending_key_down.0.keystroke.ime_key = Some(text.to_string());
|
||||
}
|
||||
window_state.borrow_mut().pending_key_down = Some(pending_key_down);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,4 @@ mod select;
|
|||
|
||||
pub use select::{ItemType, Select, SelectStyle};
|
||||
|
||||
pub fn init(cx: &mut super::AppContext) {
|
||||
select::init(cx);
|
||||
}
|
||||
pub fn init(_: &mut super::AppContext) {}
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
use serde::Deserialize;
|
||||
|
||||
use crate::{
|
||||
actions, elements::*, impl_actions, platform::MouseButton, AppContext, Entity, View,
|
||||
ViewContext, WeakViewHandle,
|
||||
elements::*,
|
||||
platform::{CursorStyle, MouseButton},
|
||||
AppContext, Entity, View, ViewContext, WeakViewHandle,
|
||||
};
|
||||
|
||||
pub struct Select {
|
||||
handle: WeakViewHandle<Self>,
|
||||
render_item: Box<dyn Fn(usize, ItemType, bool, &AppContext) -> AnyElement<Self>>,
|
||||
render_item: Box<dyn Fn(usize, ItemType, bool, &mut ViewContext<Select>) -> AnyElement<Self>>,
|
||||
selected_item_ix: usize,
|
||||
item_count: usize,
|
||||
is_open: bool,
|
||||
|
@ -27,21 +26,12 @@ pub enum ItemType {
|
|||
Unselected,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, PartialEq)]
|
||||
pub struct SelectItem(pub usize);
|
||||
|
||||
actions!(select, [ToggleSelect]);
|
||||
impl_actions!(select, [SelectItem]);
|
||||
|
||||
pub enum Event {}
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(Select::toggle);
|
||||
cx.add_action(Select::select_item);
|
||||
}
|
||||
|
||||
impl Select {
|
||||
pub fn new<F: 'static + Fn(usize, ItemType, bool, &AppContext) -> AnyElement<Self>>(
|
||||
pub fn new<
|
||||
F: 'static + Fn(usize, ItemType, bool, &mut ViewContext<Self>) -> AnyElement<Self>,
|
||||
>(
|
||||
item_count: usize,
|
||||
cx: &mut ViewContext<Self>,
|
||||
render_item: F,
|
||||
|
@ -67,13 +57,13 @@ impl Select {
|
|||
cx.notify();
|
||||
}
|
||||
|
||||
fn toggle(&mut self, _: &ToggleSelect, cx: &mut ViewContext<Self>) {
|
||||
fn toggle(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.is_open = !self.is_open;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn select_item(&mut self, action: &SelectItem, cx: &mut ViewContext<Self>) {
|
||||
self.selected_item_ix = action.0;
|
||||
pub fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Self>) {
|
||||
self.selected_item_ix = ix;
|
||||
self.is_open = false;
|
||||
cx.notify();
|
||||
}
|
||||
|
@ -116,8 +106,9 @@ impl View for Select {
|
|||
.contained()
|
||||
.with_style(style.header)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
this.toggle(&Default::default(), cx);
|
||||
this.toggle(cx);
|
||||
}),
|
||||
);
|
||||
if self.is_open {
|
||||
|
@ -142,8 +133,9 @@ impl View for Select {
|
|||
cx,
|
||||
)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
this.select_item(&SelectItem(ix), cx);
|
||||
this.set_selected_index(ix, cx);
|
||||
})
|
||||
.into_any()
|
||||
}))
|
||||
|
|
|
@ -37,7 +37,7 @@ sum_tree = { path = "../sum_tree" }
|
|||
terminal = { path = "../terminal" }
|
||||
util = { path = "../util" }
|
||||
|
||||
aho-corasick = "0.7"
|
||||
aho-corasick = "1.1"
|
||||
anyhow.workspace = true
|
||||
async-trait.workspace = true
|
||||
backtrace = "0.3"
|
||||
|
|
|
@ -3598,7 +3598,7 @@ async fn test_search(cx: &mut gpui::TestAppContext) {
|
|||
assert_eq!(
|
||||
search(
|
||||
&project,
|
||||
SearchQuery::text("TWO", false, true, Vec::new(), Vec::new()),
|
||||
SearchQuery::text("TWO", false, true, Vec::new(), Vec::new()).unwrap(),
|
||||
cx
|
||||
)
|
||||
.await
|
||||
|
@ -3623,7 +3623,7 @@ async fn test_search(cx: &mut gpui::TestAppContext) {
|
|||
assert_eq!(
|
||||
search(
|
||||
&project,
|
||||
SearchQuery::text("TWO", false, true, Vec::new(), Vec::new()),
|
||||
SearchQuery::text("TWO", false, true, Vec::new(), Vec::new()).unwrap(),
|
||||
cx
|
||||
)
|
||||
.await
|
||||
|
@ -3664,7 +3664,8 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
|
|||
true,
|
||||
vec![PathMatcher::new("*.odd").unwrap()],
|
||||
Vec::new()
|
||||
),
|
||||
)
|
||||
.unwrap(),
|
||||
cx
|
||||
)
|
||||
.await
|
||||
|
@ -3682,7 +3683,8 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
|
|||
true,
|
||||
vec![PathMatcher::new("*.rs").unwrap()],
|
||||
Vec::new()
|
||||
),
|
||||
)
|
||||
.unwrap(),
|
||||
cx
|
||||
)
|
||||
.await
|
||||
|
@ -3706,7 +3708,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
|
|||
PathMatcher::new("*.odd").unwrap(),
|
||||
],
|
||||
Vec::new()
|
||||
),
|
||||
).unwrap(),
|
||||
cx
|
||||
)
|
||||
.await
|
||||
|
@ -3731,7 +3733,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
|
|||
PathMatcher::new("*.odd").unwrap(),
|
||||
],
|
||||
Vec::new()
|
||||
),
|
||||
).unwrap(),
|
||||
cx
|
||||
)
|
||||
.await
|
||||
|
@ -3774,7 +3776,8 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
|
|||
true,
|
||||
Vec::new(),
|
||||
vec![PathMatcher::new("*.odd").unwrap()],
|
||||
),
|
||||
)
|
||||
.unwrap(),
|
||||
cx
|
||||
)
|
||||
.await
|
||||
|
@ -3797,7 +3800,8 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
|
|||
true,
|
||||
Vec::new(),
|
||||
vec![PathMatcher::new("*.rs").unwrap()],
|
||||
),
|
||||
)
|
||||
.unwrap(),
|
||||
cx
|
||||
)
|
||||
.await
|
||||
|
@ -3821,7 +3825,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
|
|||
PathMatcher::new("*.ts").unwrap(),
|
||||
PathMatcher::new("*.odd").unwrap(),
|
||||
],
|
||||
),
|
||||
).unwrap(),
|
||||
cx
|
||||
)
|
||||
.await
|
||||
|
@ -3846,7 +3850,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
|
|||
PathMatcher::new("*.ts").unwrap(),
|
||||
PathMatcher::new("*.odd").unwrap(),
|
||||
],
|
||||
),
|
||||
).unwrap(),
|
||||
cx
|
||||
)
|
||||
.await
|
||||
|
@ -3883,7 +3887,8 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
|
|||
true,
|
||||
vec![PathMatcher::new("*.odd").unwrap()],
|
||||
vec![PathMatcher::new("*.odd").unwrap()],
|
||||
),
|
||||
)
|
||||
.unwrap(),
|
||||
cx
|
||||
)
|
||||
.await
|
||||
|
@ -3901,7 +3906,7 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
|
|||
true,
|
||||
vec![PathMatcher::new("*.ts").unwrap()],
|
||||
vec![PathMatcher::new("*.ts").unwrap()],
|
||||
),
|
||||
).unwrap(),
|
||||
cx
|
||||
)
|
||||
.await
|
||||
|
@ -3925,7 +3930,8 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
|
|||
PathMatcher::new("*.ts").unwrap(),
|
||||
PathMatcher::new("*.odd").unwrap()
|
||||
],
|
||||
),
|
||||
)
|
||||
.unwrap(),
|
||||
cx
|
||||
)
|
||||
.await
|
||||
|
@ -3949,7 +3955,8 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
|
|||
PathMatcher::new("*.rs").unwrap(),
|
||||
PathMatcher::new("*.odd").unwrap()
|
||||
],
|
||||
),
|
||||
)
|
||||
.unwrap(),
|
||||
cx
|
||||
)
|
||||
.await
|
||||
|
|
|
@ -35,7 +35,7 @@ impl SearchInputs {
|
|||
#[derive(Clone, Debug)]
|
||||
pub enum SearchQuery {
|
||||
Text {
|
||||
search: Arc<AhoCorasick<usize>>,
|
||||
search: Arc<AhoCorasick>,
|
||||
replacement: Option<String>,
|
||||
whole_word: bool,
|
||||
case_sensitive: bool,
|
||||
|
@ -84,24 +84,23 @@ impl SearchQuery {
|
|||
case_sensitive: bool,
|
||||
files_to_include: Vec<PathMatcher>,
|
||||
files_to_exclude: Vec<PathMatcher>,
|
||||
) -> Self {
|
||||
) -> Result<Self> {
|
||||
let query = query.to_string();
|
||||
let search = AhoCorasickBuilder::new()
|
||||
.auto_configure(&[&query])
|
||||
.ascii_case_insensitive(!case_sensitive)
|
||||
.build(&[&query]);
|
||||
.build(&[&query])?;
|
||||
let inner = SearchInputs {
|
||||
query: query.into(),
|
||||
files_to_exclude,
|
||||
files_to_include,
|
||||
};
|
||||
Self::Text {
|
||||
Ok(Self::Text {
|
||||
search: Arc::new(search),
|
||||
replacement: None,
|
||||
whole_word,
|
||||
case_sensitive,
|
||||
inner,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn regex(
|
||||
|
@ -151,13 +150,13 @@ impl SearchQuery {
|
|||
deserialize_path_matches(&message.files_to_exclude)?,
|
||||
)
|
||||
} else {
|
||||
Ok(Self::text(
|
||||
Self::text(
|
||||
message.query,
|
||||
message.whole_word,
|
||||
message.case_sensitive,
|
||||
deserialize_path_matches(&message.files_to_include)?,
|
||||
deserialize_path_matches(&message.files_to_exclude)?,
|
||||
))
|
||||
)
|
||||
}
|
||||
}
|
||||
pub fn with_replacement(mut self, new_replacement: Option<String>) -> Self {
|
||||
|
|
|
@ -122,6 +122,7 @@ actions!(
|
|||
CopyPath,
|
||||
CopyRelativePath,
|
||||
RevealInFinder,
|
||||
OpenInTerminal,
|
||||
Cut,
|
||||
Paste,
|
||||
Delete,
|
||||
|
@ -156,6 +157,7 @@ pub fn init(assets: impl AssetSource, cx: &mut AppContext) {
|
|||
cx.add_action(ProjectPanel::copy_path);
|
||||
cx.add_action(ProjectPanel::copy_relative_path);
|
||||
cx.add_action(ProjectPanel::reveal_in_finder);
|
||||
cx.add_action(ProjectPanel::open_in_terminal);
|
||||
cx.add_action(ProjectPanel::new_search_in_directory);
|
||||
cx.add_action(
|
||||
|this: &mut ProjectPanel, action: &Paste, cx: &mut ViewContext<ProjectPanel>| {
|
||||
|
@ -423,24 +425,30 @@ impl ProjectPanel {
|
|||
menu_entries.push(ContextMenuItem::Separator);
|
||||
menu_entries.push(ContextMenuItem::action("Cut", Cut));
|
||||
menu_entries.push(ContextMenuItem::action("Copy", Copy));
|
||||
if let Some(clipboard_entry) = self.clipboard_entry {
|
||||
if clipboard_entry.worktree_id() == worktree.id() {
|
||||
menu_entries.push(ContextMenuItem::action("Paste", Paste));
|
||||
}
|
||||
}
|
||||
menu_entries.push(ContextMenuItem::Separator);
|
||||
menu_entries.push(ContextMenuItem::action("Copy Path", CopyPath));
|
||||
menu_entries.push(ContextMenuItem::action(
|
||||
"Copy Relative Path",
|
||||
CopyRelativePath,
|
||||
));
|
||||
|
||||
if entry.is_dir() {
|
||||
menu_entries.push(ContextMenuItem::Separator);
|
||||
}
|
||||
menu_entries.push(ContextMenuItem::action("Reveal in Finder", RevealInFinder));
|
||||
if entry.is_dir() {
|
||||
menu_entries.push(ContextMenuItem::action("Open in Terminal", OpenInTerminal));
|
||||
menu_entries.push(ContextMenuItem::action(
|
||||
"Search Inside",
|
||||
NewSearchInDirectory,
|
||||
));
|
||||
}
|
||||
if let Some(clipboard_entry) = self.clipboard_entry {
|
||||
if clipboard_entry.worktree_id() == worktree.id() {
|
||||
menu_entries.push(ContextMenuItem::action("Paste", Paste));
|
||||
}
|
||||
}
|
||||
|
||||
menu_entries.push(ContextMenuItem::Separator);
|
||||
menu_entries.push(ContextMenuItem::action("Rename", Rename));
|
||||
if !is_root {
|
||||
|
@ -965,6 +973,26 @@ impl ProjectPanel {
|
|||
}
|
||||
}
|
||||
|
||||
fn open_in_terminal(&mut self, _: &OpenInTerminal, cx: &mut ViewContext<Self>) {
|
||||
if let Some((worktree, entry)) = self.selected_entry(cx) {
|
||||
let window = cx.window();
|
||||
let view_id = cx.view_id();
|
||||
let path = worktree.abs_path().join(&entry.path);
|
||||
|
||||
cx.app_context()
|
||||
.spawn(|mut cx| async move {
|
||||
window.dispatch_action(
|
||||
view_id,
|
||||
&workspace::OpenTerminal {
|
||||
working_directory: path,
|
||||
},
|
||||
&mut cx,
|
||||
);
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_search_in_directory(
|
||||
&mut self,
|
||||
_: &NewSearchInDirectory,
|
||||
|
|
|
@ -94,7 +94,7 @@ impl View for QuickActionBar {
|
|||
|
||||
bar.add_child(render_quick_action_bar_button(
|
||||
2,
|
||||
"icons/radix/magic-wand.svg",
|
||||
"icons/magic-wand.svg",
|
||||
false,
|
||||
("Inline Assist".into(), Some(Box::new(InlineAssist))),
|
||||
cx,
|
||||
|
|
|
@ -155,7 +155,17 @@ message Envelope {
|
|||
RemoveChannelBufferCollaborator remove_channel_buffer_collaborator = 136;
|
||||
UpdateChannelBufferCollaborator update_channel_buffer_collaborator = 139;
|
||||
RejoinChannelBuffers rejoin_channel_buffers = 140;
|
||||
RejoinChannelBuffersResponse rejoin_channel_buffers_response = 141; // Current max
|
||||
RejoinChannelBuffersResponse rejoin_channel_buffers_response = 141;
|
||||
|
||||
JoinChannelChat join_channel_chat = 142;
|
||||
JoinChannelChatResponse join_channel_chat_response = 143;
|
||||
LeaveChannelChat leave_channel_chat = 144;
|
||||
SendChannelMessage send_channel_message = 145;
|
||||
SendChannelMessageResponse send_channel_message_response = 146;
|
||||
ChannelMessageSent channel_message_sent = 147;
|
||||
GetChannelMessages get_channel_messages = 148;
|
||||
GetChannelMessagesResponse get_channel_messages_response = 149;
|
||||
RemoveChannelMessage remove_channel_message = 150; // Current max
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1021,10 +1031,61 @@ message RenameChannel {
|
|||
string name = 2;
|
||||
}
|
||||
|
||||
message JoinChannelChat {
|
||||
uint64 channel_id = 1;
|
||||
}
|
||||
|
||||
message JoinChannelChatResponse {
|
||||
repeated ChannelMessage messages = 1;
|
||||
bool done = 2;
|
||||
}
|
||||
|
||||
message LeaveChannelChat {
|
||||
uint64 channel_id = 1;
|
||||
}
|
||||
|
||||
message SendChannelMessage {
|
||||
uint64 channel_id = 1;
|
||||
string body = 2;
|
||||
Nonce nonce = 3;
|
||||
}
|
||||
|
||||
message RemoveChannelMessage {
|
||||
uint64 channel_id = 1;
|
||||
uint64 message_id = 2;
|
||||
}
|
||||
|
||||
message SendChannelMessageResponse {
|
||||
ChannelMessage message = 1;
|
||||
}
|
||||
|
||||
message ChannelMessageSent {
|
||||
uint64 channel_id = 1;
|
||||
ChannelMessage message = 2;
|
||||
}
|
||||
|
||||
message GetChannelMessages {
|
||||
uint64 channel_id = 1;
|
||||
uint64 before_message_id = 2;
|
||||
}
|
||||
|
||||
message GetChannelMessagesResponse {
|
||||
repeated ChannelMessage messages = 1;
|
||||
bool done = 2;
|
||||
}
|
||||
|
||||
message JoinChannelBuffer {
|
||||
uint64 channel_id = 1;
|
||||
}
|
||||
|
||||
message ChannelMessage {
|
||||
uint64 id = 1;
|
||||
string body = 2;
|
||||
uint64 timestamp = 3;
|
||||
uint64 sender_id = 4;
|
||||
Nonce nonce = 5;
|
||||
}
|
||||
|
||||
message RejoinChannelBuffers {
|
||||
repeated ChannelBufferVersion buffers = 1;
|
||||
}
|
||||
|
|
|
@ -147,6 +147,7 @@ messages!(
|
|||
(CreateBufferForPeer, Foreground),
|
||||
(CreateChannel, Foreground),
|
||||
(ChannelResponse, Foreground),
|
||||
(ChannelMessageSent, Foreground),
|
||||
(CreateProjectEntry, Foreground),
|
||||
(CreateRoom, Foreground),
|
||||
(CreateRoomResponse, Foreground),
|
||||
|
@ -163,6 +164,10 @@ messages!(
|
|||
(GetCodeActionsResponse, Background),
|
||||
(GetHover, Background),
|
||||
(GetHoverResponse, Background),
|
||||
(GetChannelMessages, Background),
|
||||
(GetChannelMessagesResponse, Background),
|
||||
(SendChannelMessage, Background),
|
||||
(SendChannelMessageResponse, Background),
|
||||
(GetCompletions, Background),
|
||||
(GetCompletionsResponse, Background),
|
||||
(GetDefinition, Background),
|
||||
|
@ -184,6 +189,9 @@ messages!(
|
|||
(JoinProjectResponse, Foreground),
|
||||
(JoinRoom, Foreground),
|
||||
(JoinRoomResponse, Foreground),
|
||||
(JoinChannelChat, Foreground),
|
||||
(JoinChannelChatResponse, Foreground),
|
||||
(LeaveChannelChat, Foreground),
|
||||
(LeaveProject, Foreground),
|
||||
(LeaveRoom, Foreground),
|
||||
(OpenBufferById, Background),
|
||||
|
@ -209,6 +217,7 @@ messages!(
|
|||
(RejoinRoomResponse, Foreground),
|
||||
(RemoveContact, Foreground),
|
||||
(RemoveChannelMember, Foreground),
|
||||
(RemoveChannelMessage, Foreground),
|
||||
(ReloadBuffers, Foreground),
|
||||
(ReloadBuffersResponse, Foreground),
|
||||
(RemoveProjectCollaborator, Foreground),
|
||||
|
@ -293,6 +302,7 @@ request_messages!(
|
|||
(InviteChannelMember, Ack),
|
||||
(JoinProject, JoinProjectResponse),
|
||||
(JoinRoom, JoinRoomResponse),
|
||||
(JoinChannelChat, JoinChannelChatResponse),
|
||||
(LeaveRoom, Ack),
|
||||
(RejoinRoom, RejoinRoomResponse),
|
||||
(IncomingCall, Ack),
|
||||
|
@ -313,9 +323,12 @@ request_messages!(
|
|||
(RespondToContactRequest, Ack),
|
||||
(RespondToChannelInvite, Ack),
|
||||
(SetChannelMemberAdmin, Ack),
|
||||
(SendChannelMessage, SendChannelMessageResponse),
|
||||
(GetChannelMessages, GetChannelMessagesResponse),
|
||||
(GetChannelMembers, GetChannelMembersResponse),
|
||||
(JoinChannel, JoinRoomResponse),
|
||||
(RemoveChannel, Ack),
|
||||
(RemoveChannelMessage, Ack),
|
||||
(RenameProjectEntry, ProjectEntryResponse),
|
||||
(RenameChannel, ChannelResponse),
|
||||
(SaveBuffer, BufferSaved),
|
||||
|
@ -388,8 +401,10 @@ entity_messages!(
|
|||
|
||||
entity_messages!(
|
||||
channel_id,
|
||||
ChannelMessageSent,
|
||||
UpdateChannelBuffer,
|
||||
RemoveChannelBufferCollaborator,
|
||||
RemoveChannelMessage,
|
||||
AddChannelBufferCollaborator,
|
||||
UpdateChannelBufferCollaborator
|
||||
);
|
||||
|
|
|
@ -6,4 +6,4 @@ pub use conn::Connection;
|
|||
pub use peer::*;
|
||||
mod macros;
|
||||
|
||||
pub const PROTOCOL_VERSION: u32 = 62;
|
||||
pub const PROTOCOL_VERSION: u32 = 63;
|
||||
|
|
|
@ -783,14 +783,21 @@ impl BufferSearchBar {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
SearchQuery::text(
|
||||
match SearchQuery::text(
|
||||
query,
|
||||
self.search_options.contains(SearchOptions::WHOLE_WORD),
|
||||
self.search_options.contains(SearchOptions::CASE_SENSITIVE),
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
)
|
||||
.with_replacement(Some(self.replacement(cx)).filter(|s| !s.is_empty()))
|
||||
) {
|
||||
Ok(query) => query
|
||||
.with_replacement(Some(self.replacement(cx)).filter(|s| !s.is_empty())),
|
||||
Err(_) => {
|
||||
self.query_contains_error = true;
|
||||
cx.notify();
|
||||
return done_rx;
|
||||
}
|
||||
}
|
||||
}
|
||||
.into();
|
||||
self.active_search = Some(query.clone());
|
||||
|
|
|
@ -511,7 +511,7 @@ impl Item for ProjectSearchView {
|
|||
) -> AnyElement<T> {
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Svg::new("icons/magnifying_glass_12.svg")
|
||||
Svg::new("icons/magnifying_glass.svg")
|
||||
.with_color(tab_theme.label.text.color)
|
||||
.constrained()
|
||||
.with_width(tab_theme.type_icon_width)
|
||||
|
@ -1050,13 +1050,23 @@ impl ProjectSearchView {
|
|||
}
|
||||
}
|
||||
}
|
||||
_ => Some(SearchQuery::text(
|
||||
_ => match SearchQuery::text(
|
||||
text,
|
||||
self.search_options.contains(SearchOptions::WHOLE_WORD),
|
||||
self.search_options.contains(SearchOptions::CASE_SENSITIVE),
|
||||
included_files,
|
||||
excluded_files,
|
||||
)),
|
||||
) {
|
||||
Ok(query) => {
|
||||
self.panels_with_errors.remove(&InputPanel::Query);
|
||||
Some(query)
|
||||
}
|
||||
Err(_e) => {
|
||||
self.panels_with_errors.insert(InputPanel::Query);
|
||||
cx.notify();
|
||||
None
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1440,7 +1450,7 @@ impl View for ProjectSearchBar {
|
|||
let search = _search.read(cx);
|
||||
let filter_button = render_option_button_icon(
|
||||
search.filters_enabled,
|
||||
"icons/filter_12.svg",
|
||||
"icons/filter.svg",
|
||||
0,
|
||||
"Toggle filters",
|
||||
Box::new(ToggleFilters),
|
||||
|
@ -1471,14 +1481,14 @@ impl View for ProjectSearchBar {
|
|||
};
|
||||
let case_sensitive = is_semantic_disabled.then(|| {
|
||||
render_option_button_icon(
|
||||
"icons/case_insensitive_12.svg",
|
||||
"icons/case_insensitive.svg",
|
||||
SearchOptions::CASE_SENSITIVE,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let whole_word = is_semantic_disabled.then(|| {
|
||||
render_option_button_icon("icons/word_search_12.svg", SearchOptions::WHOLE_WORD, cx)
|
||||
render_option_button_icon("icons/word_search.svg", SearchOptions::WHOLE_WORD, cx)
|
||||
});
|
||||
|
||||
let search_button_for_mode = |mode, side, cx: &mut ViewContext<ProjectSearchBar>| {
|
||||
|
|
|
@ -63,8 +63,8 @@ impl SearchOptions {
|
|||
|
||||
pub fn icon(&self) -> &'static str {
|
||||
match *self {
|
||||
SearchOptions::WHOLE_WORD => "icons/word_search_12.svg",
|
||||
SearchOptions::CASE_SENSITIVE => "icons/case_insensitive_12.svg",
|
||||
SearchOptions::WHOLE_WORD => "icons/word_search.svg",
|
||||
SearchOptions::CASE_SENSITIVE => "icons/case_insensitive.svg",
|
||||
_ => panic!("{:?} is not a named SearchOption", self),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -714,7 +714,14 @@ impl SemanticIndex {
|
|||
|
||||
let search_start = Instant::now();
|
||||
let modified_buffer_results = this.update(&mut cx, |this, cx| {
|
||||
this.search_modified_buffers(&project, query.clone(), limit, &excludes, cx)
|
||||
this.search_modified_buffers(
|
||||
&project,
|
||||
query.clone(),
|
||||
limit,
|
||||
&includes,
|
||||
&excludes,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let file_results = this.update(&mut cx, |this, cx| {
|
||||
this.search_files(project, query, limit, includes, excludes, cx)
|
||||
|
@ -877,6 +884,7 @@ impl SemanticIndex {
|
|||
project: &ModelHandle<Project>,
|
||||
query: Embedding,
|
||||
limit: usize,
|
||||
includes: &[PathMatcher],
|
||||
excludes: &[PathMatcher],
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Vec<SearchResult>>> {
|
||||
|
@ -890,7 +898,16 @@ impl SemanticIndex {
|
|||
let excluded = snapshot.resolve_file_path(cx, false).map_or(false, |path| {
|
||||
excludes.iter().any(|matcher| matcher.is_match(&path))
|
||||
});
|
||||
if buffer.is_dirty() && !excluded {
|
||||
|
||||
let included = if includes.len() == 0 {
|
||||
true
|
||||
} else {
|
||||
snapshot.resolve_file_path(cx, false).map_or(false, |path| {
|
||||
includes.iter().any(|matcher| matcher.is_match(&path))
|
||||
})
|
||||
};
|
||||
|
||||
if buffer.is_dirty() && !excluded && included {
|
||||
Some((buffer_handle, snapshot))
|
||||
} else {
|
||||
None
|
||||
|
|
|
@ -132,9 +132,9 @@ impl<V: 'static> CollabPanelElement<V> {
|
|||
div().flex().h_full().gap_1().items_center().child(
|
||||
svg()
|
||||
.path(if expanded {
|
||||
"icons/radix/caret-down.svg"
|
||||
"icons/caret_down.svg"
|
||||
} else {
|
||||
"icons/radix/caret-up.svg"
|
||||
"icons/caret_up.svg"
|
||||
})
|
||||
.w_3p5()
|
||||
.h_3p5()
|
||||
|
|
|
@ -217,7 +217,7 @@ impl TitleBar {
|
|||
.fill(theme.lowest.base.pressed.background)
|
||||
.child(
|
||||
svg()
|
||||
.path("icons/radix/speaker-loud.svg")
|
||||
.path("icons/speaker-loud.svg")
|
||||
.size_3p5()
|
||||
.fill(theme.lowest.base.default.foreground),
|
||||
),
|
||||
|
@ -237,7 +237,7 @@ impl TitleBar {
|
|||
.fill(theme.lowest.base.pressed.background)
|
||||
.child(
|
||||
svg()
|
||||
.path("icons/radix/desktop.svg")
|
||||
.path("icons/desktop.svg")
|
||||
.size_3p5()
|
||||
.fill(theme.lowest.base.default.foreground),
|
||||
),
|
||||
|
@ -269,7 +269,7 @@ impl TitleBar {
|
|||
)
|
||||
.child(
|
||||
svg()
|
||||
.path("icons/caret_down_8.svg")
|
||||
.path("icons/caret_down.svg")
|
||||
.w_2()
|
||||
.h_2()
|
||||
.fill(theme.lowest.variant.default.foreground),
|
||||
|
|
|
@ -333,6 +333,7 @@ mod test {
|
|||
cmd: false,
|
||||
function: false,
|
||||
key: "🖖🏻".to_string(), //2 char string
|
||||
ime_key: None,
|
||||
};
|
||||
assert_eq!(to_esc_str(&ks, &TermMode::NONE, false), None);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use std::sync::Arc;
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
|
||||
use crate::TerminalView;
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
|
@ -23,6 +23,7 @@ actions!(terminal_panel, [ToggleFocus]);
|
|||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(TerminalPanel::new_terminal);
|
||||
cx.add_action(TerminalPanel::open_terminal);
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -70,7 +71,7 @@ impl TerminalPanel {
|
|||
Flex::row()
|
||||
.with_child(Pane::render_tab_bar_button(
|
||||
0,
|
||||
"icons/plus_12.svg",
|
||||
"icons/plus.svg",
|
||||
false,
|
||||
Some(("New Terminal", Some(Box::new(workspace::NewTerminal)))),
|
||||
cx,
|
||||
|
@ -79,7 +80,7 @@ impl TerminalPanel {
|
|||
cx.window_context().defer(move |cx| {
|
||||
if let Some(this) = this.upgrade(cx) {
|
||||
this.update(cx, |this, cx| {
|
||||
this.add_terminal(cx);
|
||||
this.add_terminal(None, cx);
|
||||
});
|
||||
}
|
||||
})
|
||||
|
@ -90,9 +91,9 @@ impl TerminalPanel {
|
|||
.with_child(Pane::render_tab_bar_button(
|
||||
1,
|
||||
if pane.is_zoomed() {
|
||||
"icons/minimize_8.svg"
|
||||
"icons/minimize.svg"
|
||||
} else {
|
||||
"icons/maximize_8.svg"
|
||||
"icons/maximize.svg"
|
||||
},
|
||||
pane.is_zoomed(),
|
||||
Some(("Toggle Zoom".into(), Some(Box::new(workspace::ToggleZoom)))),
|
||||
|
@ -230,6 +231,21 @@ impl TerminalPanel {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn open_terminal(
|
||||
workspace: &mut Workspace,
|
||||
action: &workspace::OpenTerminal,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
let Some(this) = workspace.focus_panel::<Self>(cx) else {
|
||||
return;
|
||||
};
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
this.add_terminal(Some(action.working_directory.clone()), cx)
|
||||
})
|
||||
}
|
||||
|
||||
///Create a new Terminal in the current working directory or the user's home directory
|
||||
fn new_terminal(
|
||||
workspace: &mut Workspace,
|
||||
_: &workspace::NewTerminal,
|
||||
|
@ -239,19 +255,23 @@ impl TerminalPanel {
|
|||
return;
|
||||
};
|
||||
|
||||
this.update(cx, |this, cx| this.add_terminal(cx))
|
||||
this.update(cx, |this, cx| this.add_terminal(None, cx))
|
||||
}
|
||||
|
||||
fn add_terminal(&mut self, cx: &mut ViewContext<Self>) {
|
||||
fn add_terminal(&mut self, working_directory: Option<PathBuf>, cx: &mut ViewContext<Self>) {
|
||||
let workspace = self.workspace.clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let pane = this.read_with(&cx, |this, _| this.pane.clone())?;
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
let working_directory_strategy = settings::get::<TerminalSettings>(cx)
|
||||
.working_directory
|
||||
.clone();
|
||||
let working_directory =
|
||||
crate::get_working_directory(workspace, cx, working_directory_strategy);
|
||||
let working_directory = if let Some(working_directory) = working_directory {
|
||||
Some(working_directory)
|
||||
} else {
|
||||
let working_directory_strategy = settings::get::<TerminalSettings>(cx)
|
||||
.working_directory
|
||||
.clone();
|
||||
crate::get_working_directory(workspace, cx, working_directory_strategy)
|
||||
};
|
||||
|
||||
let window = cx.window();
|
||||
if let Some(terminal) = workspace.project().update(cx, |project, cx| {
|
||||
project
|
||||
|
@ -389,7 +409,7 @@ impl Panel for TerminalPanel {
|
|||
|
||||
fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
|
||||
if active && self.pane.read(cx).items_len() == 0 {
|
||||
self.add_terminal(cx)
|
||||
self.add_terminal(None, cx)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -26,7 +26,6 @@ impl<C: SafeStylable> ComponentExt<C> for C {
|
|||
}
|
||||
|
||||
pub mod disclosure {
|
||||
|
||||
use gpui::{
|
||||
elements::{Component, ContainerStyle, Empty, Flex, ParentElement, SafeStylable},
|
||||
Action, Element,
|
||||
|
|
|
@ -52,6 +52,7 @@ pub struct Theme {
|
|||
pub copilot: Copilot,
|
||||
pub collab_panel: CollabPanel,
|
||||
pub project_panel: ProjectPanel,
|
||||
pub chat_panel: ChatPanel,
|
||||
pub command_palette: CommandPalette,
|
||||
pub picker: Picker,
|
||||
pub editor: Editor,
|
||||
|
@ -624,6 +625,19 @@ pub struct IconButton {
|
|||
pub button_width: f32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default, JsonSchema)]
|
||||
pub struct ChatPanel {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
pub list: ContainerStyle,
|
||||
pub channel_select: ChannelSelect,
|
||||
pub input_editor: FieldEditor,
|
||||
pub message: ChatMessage,
|
||||
pub pending_message: ChatMessage,
|
||||
pub sign_in_prompt: Interactive<TextStyle>,
|
||||
pub icon_button: Interactive<IconButton>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default, JsonSchema)]
|
||||
pub struct ChatMessage {
|
||||
#[serde(flatten)]
|
||||
|
@ -641,7 +655,6 @@ pub struct ChannelSelect {
|
|||
pub item: ChannelName,
|
||||
pub active_item: ChannelName,
|
||||
pub hovered_item: ChannelName,
|
||||
pub hovered_active_item: ChannelName,
|
||||
pub menu: ContainerStyle,
|
||||
}
|
||||
|
||||
|
|
|
@ -34,11 +34,11 @@ fn focused(EditorFocused(editor): &EditorFocused, cx: &mut AppContext) {
|
|||
fn blurred(EditorBlurred(editor): &EditorBlurred, cx: &mut AppContext) {
|
||||
editor.window().update(cx, |cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.clear_operator(cx);
|
||||
vim.workspace_state.recording = false;
|
||||
vim.workspace_state.recorded_actions.clear();
|
||||
if let Some(previous_editor) = vim.active_editor.clone() {
|
||||
if previous_editor == editor.clone() {
|
||||
vim.clear_operator(cx);
|
||||
vim.active_editor = None;
|
||||
}
|
||||
}
|
||||
|
@ -60,3 +60,31 @@ fn released(EditorReleased(editor): &EditorReleased, cx: &mut AppContext) {
|
|||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{test::VimTestContext, Vim};
|
||||
use editor::Editor;
|
||||
use gpui::View;
|
||||
use language::Buffer;
|
||||
|
||||
// regression test for blur called with a different active editor
|
||||
#[gpui::test]
|
||||
async fn test_blur_focus(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
let buffer = cx.add_model(|_| Buffer::new(0, 0, "a = 1\nb = 2\n"));
|
||||
let window2 = cx.add_window(|cx| Editor::for_buffer(buffer, None, cx));
|
||||
let editor2 = cx.read(|cx| window2.root(cx)).unwrap();
|
||||
|
||||
cx.update(|cx| {
|
||||
let vim = Vim::read(cx);
|
||||
assert_eq!(vim.active_editor.unwrap().id(), editor2.id())
|
||||
});
|
||||
|
||||
// no panic when blurring an editor in a different window.
|
||||
cx.update_editor(|editor1, cx| {
|
||||
editor1.focus_out(cx.handle().into_any(), cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -536,8 +536,12 @@ fn down(
|
|||
map.buffer_snapshot.max_point().row,
|
||||
);
|
||||
let new_col = cmp::min(goal_column, map.fold_snapshot.line_len(new_row));
|
||||
let point = map.fold_point_to_display_point(FoldPoint::new(new_row, new_col));
|
||||
let point = map.fold_point_to_display_point(
|
||||
map.fold_snapshot
|
||||
.clip_point(FoldPoint::new(new_row, new_col), Bias::Left),
|
||||
);
|
||||
|
||||
// clip twice to "clip at end of line"
|
||||
(map.clip_point(point, Bias::Left), goal)
|
||||
}
|
||||
|
||||
|
@ -573,7 +577,10 @@ pub(crate) fn up(
|
|||
|
||||
let new_row = start.row().saturating_sub(times as u32);
|
||||
let new_col = cmp::min(goal_column, map.fold_snapshot.line_len(new_row));
|
||||
let point = map.fold_point_to_display_point(FoldPoint::new(new_row, new_col));
|
||||
let point = map.fold_point_to_display_point(
|
||||
map.fold_snapshot
|
||||
.clip_point(FoldPoint::new(new_row, new_col), Bias::Left),
|
||||
);
|
||||
|
||||
(map.clip_point(point, Bias::Left), goal)
|
||||
}
|
||||
|
|
|
@ -26,10 +26,11 @@ pub fn copy_selections_content(editor: &mut Editor, linewise: bool, cx: &mut App
|
|||
let is_last_line = linewise
|
||||
&& end.row == buffer.max_buffer_row()
|
||||
&& buffer.max_point().column > 0
|
||||
&& start.row < buffer.max_buffer_row()
|
||||
&& start == Point::new(start.row, buffer.line_len(start.row));
|
||||
|
||||
if is_last_line {
|
||||
start = Point::new(buffer.max_buffer_row(), 0);
|
||||
start = Point::new(start.row + 1, 0);
|
||||
}
|
||||
for chunk in buffer.text_for_range(start..end) {
|
||||
text.push_str(chunk);
|
||||
|
|
|
@ -12,7 +12,7 @@ use language::{Selection, SelectionGoal};
|
|||
use workspace::Workspace;
|
||||
|
||||
use crate::{
|
||||
motion::Motion,
|
||||
motion::{start_of_line, Motion},
|
||||
object::Object,
|
||||
state::{Mode, Operator},
|
||||
utils::copy_selections_content,
|
||||
|
@ -326,7 +326,10 @@ pub fn yank(_: &mut Workspace, _: &VisualYank, cx: &mut ViewContext<Workspace>)
|
|||
let line_mode = editor.selections.line_mode;
|
||||
copy_selections_content(editor, line_mode, cx);
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.move_with(|_, selection| {
|
||||
s.move_with(|map, selection| {
|
||||
if line_mode {
|
||||
selection.start = start_of_line(map, false, selection.start);
|
||||
};
|
||||
selection.collapse_to(selection.start, SelectionGoal::None)
|
||||
});
|
||||
if vim.state().mode == Mode::VisualBlock {
|
||||
|
@ -672,6 +675,21 @@ mod test {
|
|||
the lazy dog"})
|
||||
.await;
|
||||
cx.assert_clipboard_content(Some("The q"));
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
The quick brown
|
||||
fox ˇjumps over
|
||||
the lazy dog"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["shift-v", "shift-g", "shift-y"])
|
||||
.await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
The quick brown
|
||||
ˇfox jumps over
|
||||
the lazy dog"})
|
||||
.await;
|
||||
cx.assert_shared_clipboard("fox jumps over\nthe lazy dog\n")
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
|
|
|
@ -27,3 +27,9 @@
|
|||
{"Key":"k"}
|
||||
{"Key":"y"}
|
||||
{"Get":{"state":"ˇThe quick brown\nfox jumps over\nthe lazy dog","mode":"Normal"}}
|
||||
{"Put":{"state":"The quick brown\nfox ˇjumps over\nthe lazy dog"}}
|
||||
{"Key":"shift-v"}
|
||||
{"Key":"shift-g"}
|
||||
{"Key":"shift-y"}
|
||||
{"Get":{"state":"The quick brown\nˇfox jumps over\nthe lazy dog","mode":"Normal"}}
|
||||
{"ReadRegister":{"name":"\"","value":"fox jumps over\nthe lazy dog\n"}}
|
||||
|
|
|
@ -4,7 +4,8 @@ use gpui::{
|
|||
elements::*, platform::CursorStyle, platform::MouseButton, Action, AnyViewHandle, AppContext,
|
||||
Axis, Entity, Subscription, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::rc::Rc;
|
||||
use theme::ThemeSettings;
|
||||
|
||||
|
@ -132,7 +133,8 @@ pub struct Dock {
|
|||
active_panel_index: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum DockPosition {
|
||||
Left,
|
||||
Bottom,
|
||||
|
|
|
@ -292,7 +292,7 @@ pub mod simple_message_notification {
|
|||
.with_child(
|
||||
MouseEventHandler::new::<Cancel, _>(0, cx, |state, _| {
|
||||
let style = theme.dismiss_button.style_for(state);
|
||||
Svg::new("icons/x_mark_8.svg")
|
||||
Svg::new("icons/x.svg")
|
||||
.with_color(style.color)
|
||||
.constrained()
|
||||
.with_width(style.icon_width)
|
||||
|
|
|
@ -337,7 +337,7 @@ impl Pane {
|
|||
// New menu
|
||||
.with_child(Self::render_tab_bar_button(
|
||||
0,
|
||||
"icons/plus_12.svg",
|
||||
"icons/plus.svg",
|
||||
false,
|
||||
Some(("New...".into(), None)),
|
||||
cx,
|
||||
|
@ -352,7 +352,7 @@ impl Pane {
|
|||
))
|
||||
.with_child(Self::render_tab_bar_button(
|
||||
1,
|
||||
"icons/split_12.svg",
|
||||
"icons/split.svg",
|
||||
false,
|
||||
Some(("Split Pane".into(), None)),
|
||||
cx,
|
||||
|
@ -369,10 +369,10 @@ impl Pane {
|
|||
let icon_path;
|
||||
let tooltip_label;
|
||||
if pane.is_zoomed() {
|
||||
icon_path = "icons/minimize_8.svg";
|
||||
icon_path = "icons/minimize.svg";
|
||||
tooltip_label = "Zoom In";
|
||||
} else {
|
||||
icon_path = "icons/maximize_8.svg";
|
||||
icon_path = "icons/maximize.svg";
|
||||
tooltip_label = "Zoom In";
|
||||
}
|
||||
|
||||
|
@ -1535,7 +1535,7 @@ impl Pane {
|
|||
let close_element = if hovered {
|
||||
let item_id = item.id();
|
||||
enum TabCloseButton {}
|
||||
let icon = Svg::new("icons/x_mark_8.svg");
|
||||
let icon = Svg::new("icons/x.svg");
|
||||
MouseEventHandler::new::<TabCloseButton, _>(item_id, cx, |mouse_state, _| {
|
||||
if mouse_state.hovered() {
|
||||
icon.with_color(tab_style.icon_close_active)
|
||||
|
@ -1701,7 +1701,7 @@ impl View for Pane {
|
|||
|
||||
let mut tab_row = Flex::row()
|
||||
.with_child(nav_button(
|
||||
"icons/arrow_left_16.svg",
|
||||
"icons/arrow_left.svg",
|
||||
button_style.clone(),
|
||||
nav_button_height,
|
||||
tooltip_style.clone(),
|
||||
|
@ -1726,7 +1726,7 @@ impl View for Pane {
|
|||
))
|
||||
.with_child(
|
||||
nav_button(
|
||||
"icons/arrow_right_16.svg",
|
||||
"icons/arrow_right.svg",
|
||||
button_style.clone(),
|
||||
nav_button_height,
|
||||
tooltip_style,
|
||||
|
|
|
@ -112,7 +112,7 @@ impl Item for SharedScreen {
|
|||
) -> gpui::AnyElement<V> {
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Svg::new("icons/disable_screen_sharing_12.svg")
|
||||
Svg::new("icons/desktop.svg")
|
||||
.with_color(style.label.text.color)
|
||||
.constrained()
|
||||
.with_width(style.type_icon_width)
|
||||
|
|
|
@ -203,7 +203,15 @@ impl Clone for Toast {
|
|||
}
|
||||
}
|
||||
|
||||
impl_actions!(workspace, [ActivatePane, ActivatePaneInDirection, Toast]);
|
||||
#[derive(Clone, Deserialize, PartialEq)]
|
||||
pub struct OpenTerminal {
|
||||
pub working_directory: PathBuf,
|
||||
}
|
||||
|
||||
impl_actions!(
|
||||
workspace,
|
||||
[ActivatePane, ActivatePaneInDirection, Toast, OpenTerminal]
|
||||
);
|
||||
|
||||
pub type WorkspaceId = i64;
|
||||
|
||||
|
|
|
@ -119,12 +119,6 @@ fn main() {
|
|||
app.run(move |cx| {
|
||||
cx.set_global(*RELEASE_CHANNEL);
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
use feature_flags::FeatureFlagAppExt;
|
||||
cx.set_staff(true);
|
||||
}
|
||||
|
||||
let mut store = SettingsStore::default();
|
||||
store
|
||||
.set_default_settings(default_settings().as_ref(), cx)
|
||||
|
|
|
@ -214,6 +214,13 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
|
|||
workspace.toggle_panel_focus::<collab_ui::collab_panel::CollabPanel>(cx);
|
||||
},
|
||||
);
|
||||
cx.add_action(
|
||||
|workspace: &mut Workspace,
|
||||
_: &collab_ui::chat_panel::ToggleFocus,
|
||||
cx: &mut ViewContext<Workspace>| {
|
||||
workspace.toggle_panel_focus::<collab_ui::chat_panel::ChatPanel>(cx);
|
||||
},
|
||||
);
|
||||
cx.add_action(
|
||||
|workspace: &mut Workspace,
|
||||
_: &terminal_panel::ToggleFocus,
|
||||
|
@ -338,11 +345,14 @@ pub fn initialize_workspace(
|
|||
let assistant_panel = AssistantPanel::load(workspace_handle.clone(), cx.clone());
|
||||
let channels_panel =
|
||||
collab_ui::collab_panel::CollabPanel::load(workspace_handle.clone(), cx.clone());
|
||||
let (project_panel, terminal_panel, assistant_panel, channels_panel) = futures::try_join!(
|
||||
let chat_panel =
|
||||
collab_ui::chat_panel::ChatPanel::load(workspace_handle.clone(), cx.clone());
|
||||
let (project_panel, terminal_panel, assistant_panel, channels_panel, chat_panel) = futures::try_join!(
|
||||
project_panel,
|
||||
terminal_panel,
|
||||
assistant_panel,
|
||||
channels_panel
|
||||
channels_panel,
|
||||
chat_panel,
|
||||
)?;
|
||||
workspace_handle.update(&mut cx, |workspace, cx| {
|
||||
let project_panel_position = project_panel.position(cx);
|
||||
|
@ -362,6 +372,7 @@ pub fn initialize_workspace(
|
|||
workspace.add_panel(terminal_panel, cx);
|
||||
workspace.add_panel(assistant_panel, cx);
|
||||
workspace.add_panel(channels_panel, cx);
|
||||
workspace.add_panel(chat_panel, cx);
|
||||
|
||||
if !was_deserialized
|
||||
&& workspace
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue